今回のテーマはMoscoviumマイコンを使ったSPI通信です。手持ちのSPI通信モジュールはそれほど多くなかったのですが、「MoscoviumマイコンでもSPI通信できるよね?」と、言う事は確認しておきたいと思います。
SPIの通信仕様は単純で簡潔です。
これだけ。
I2Cみたいにマルチバスマスタにはならないし、スレーブ装置が「ちょっと待って!」って、クロックストレッチしてくることもありません。スレーブ毎にSSを複数本用意することで、複数のスレーブ装置の接続は可能となります。
通信仕様としては極めて簡単だという事です。その代わり、SPIにはものすごく沢山の「自由度」が設けられていて、これがウザったいのです。
上に挙げただけでもウンザリしてしまいます。その上、「SPIっぽいけど、ちょっと違う」みたいな亜種を挙げ始めると、もうキリがありません。追加のストローブ信号が必要だったり、MOSI/MISOが時分割で切り替わる物(双方向タイプ)だったり、様々です。
プロトコルが複雑だったI2Cに比べて、SPIはバリエーションの多さのせいで、ハードウェア化には消極的となってしまいます。
元々はモトローラ社が提唱した通信仕様で、現在はNXP社となっています。……I2Cと同じ会社なのね……。探しては見ましたが、「SPI仕様書」的なドキュメントを見つけることはできませんでした。
SPI仕様書を検索する(別ウインドウが開きます)
※そもそも、SPIの公式ドキュメントはモトローラ時代のAN991であるらしいです。見つけて読んではみましたが、AN=Application Noteなので、「仕様書」ではありませんでした。
即ち、「何を満たせばSPIなのか」を定義したドキュメントが見つからなかったわけで、I/Oユニットを作る(ハードウェア化する)立場で考えると、どういったユニットを作ったら「SPI」と呼べるのか、雲を掴む様な話となります。上で「亜種」と書いたバリエーションも、どこまでを「SPI」と呼ぶべきなのやら……。
一方で、SPIの最大の強みは何と言っても、通信速度の速さです。通信仕様が単純なだけに、通信クロック周波数を高速化し易いのが特徴です。
諸々考慮した結果、結論は以下となりました。
どんなにバリエーションがあったとて、ソフトウェアなら如何ようにも対処可能なので、下手にハードウェア化するより使い易いことでしょう。
手持ちのSPI通信モジュールである8×8[ドット]マトリクスLED表示器(8×8 LED)を使います(下図)。
以下、簡単におさらいしておきます。
MAX7219と通信する前提だと、SPI mode 0で、クロックは正極性&SCK↑がデータのラッチ(取り込み)タイミングとなります。
SPI通信に於いて、SCK↑でデータをラッチする場合、SCK↓で出力データを変化させるのが基本となります。この様なタイミングとすることで、データのセットアップにもホールドにも、クロック半相分の余裕が出来て、接続し易くなる利点があるのです。
汎用I/OポートPORT8I8Oユニットを使って、SPI信号線を制御します。
equ spi_ss,0x80 // spi: ss
equ spi_sck,0x40 // spi: sck
equ spi_mosi,0x20 // spi: mosi
equ spi_miso,0x20 // spi: miso (not used in max7219)
// port setting
ldbiu r0,spi_ss
ldwi r7,porset
stw [r7],r0
ldwi r7,pordir
ldw r0,[r7]
andi r0,~(spi_ss|spi_sck|spi_mosi)
stw [r7],r0
SS端子=port_out[7]、SCK端子=port_out[6]、MOSI端子=port_out[5]、MISO端子=port_inp[5]に割り当ててみました。MAX7219からのデータ読み出し機能は無いので、今回、MISO端子は不要ですが、MISO入力のあるSPI通信を実現する場合の参考として、実装しておきました。
まず、Moscoviumマクロアセンブラを使い、SPI端子制御用のユーザマクロを定義して行きます。
# user macros
macro spi_ss_0
// ss=0
ldbiu r0,spi_ss
stw [r6],r0
endm
macro spi_ss_1
// ss=1
ldbiu r0,spi_ss
stw [r7],r0
endm
macro spi_sck_0
// sck=0
ldbiu r0,spi_sck
stw [r6],r0
endm
macro spi_sck_1
// sck=1
ldbiu r0,spi_sck
stw [r7],r0
endm
macro spi_mosi_o
// mosi
ldbiu r0,spi_mosi
lsfti r1,1
bcs pcnt+4
stw [r6],r0 // clear
bra pcnt+2
stw [r7],r0 // set
endm
macro spi_miso_i
// miso
ldw r0,[r5]
andi r0,spi_miso
beq pcnt+2
ori r1,1
endm
※前提条件として、汎用レジスタr5=porind、r6=porclr、r7=porsetです。その他、r0を一時レジスタとして使い、r1に送受信データを格納しています。
これらのユーザマクロを使うと、MAX7219とのSPI通信サブルーチンは以下の様になります。
// ================================
// software spi, spi_transfer ; disturbed r0,r5~r7
// 16 bits, msb first, mode 0
// r1: 16 bits data to send
// return: r1=received 16 bits data
// ================================
spi_transfer:
ldwi r7,porset
ldwi r6,porclr
ldwi r5,porind // porind is better than porin.
// spi 16 bits transfer
spi_ss_0
rept 16
// spi bit $(reptr)
spi_mosi_o
spi_sck_1
spi_miso_i
spi_sck_0
endr
spi_ss_1
rtn
サブルーチンspi_transferをコールすると、汎用レジスタr1の16ビットデータを送信(MOSI)し、同時にMISO端子から読み取ったデータを同じr1に上書きして戻ります。
本来、ハードウェアでSPIユニットを作成する場合には、入力FF+SCK↑でデータMISOを取り込みます。しかし、ソフトウェアSPIの場合、データを取り込む動作とSCK↑出力の動作を同時には実行できません。以下いずれかの対応となります。
SPI mode 0では、SCK↓が来ない限りMISO端子に変化は無いので、極論では「どっちでも良い」です。今回は上コードの様に、spi_mosi_o → spi_sck_1 → spi_miso_i → spi_sck_0の順番としました。SCKの変化(spi_sck_1)と変化(spi_sck_0)の間で、spi_mosi_oとspi_miso_iの処理が分散することで、SCKのデューティが50%に近くなるのが特徴です。
spi_sck_1の行とspi_miso_iの行を入れ替えて、以下の様にしても良いです。
rept 16
// spi bit $(reptr)
spi_mosi_o
spi_miso_i // spi_sck_1とspi_miso_iの行を入れ替え。
spi_sck_1 // SCKのHIGH幅が狭い波形となるが、問題なし。
spi_sck_0
endr
この場合、SCKのHIGH幅が狭い波形となりますが、それだけのことです。MAX7219のACスペックは満たしているし、特に問題はありません。
MAX7219が相手の場合、読み出しデータMISOが必要ありませんので、そもそも、spi_miso_iの処理を丸ごと削除して使うことも可能です。そうすることで通信速度を、もう少し向上することが可能となります。
LSBファーストのSPI機器なら、spi_mosi_oマクロの中の「lsfti r1,1」を「lsfti r1,-1」として、spi_miso_iマクロの中の「ori r1,1」を「ldwi r0,0x8000<改行>or r1,r0」等とすると、対応できるはずです(未検証)。
この様に、通信相手の仕様に合わせて簡単に通信仕様をカスタマイズできるのは、ソフトウェアSPIの強みとなります。
※ソフトウェアSPIの弱みとしては、通信中CPU時間を拘束されることや、通信周波数がCPUの実行速度で律速される事などが挙げられます。
16ビットのデータ転送だったので、さすがにループを回した方が良かったのかもしれませんが、反復展開rept疑似命令を使って、16回分丸々展開してしまいました。マクロと言い、反復展開と言い、Moscoviumマクロアセンブラの機能でずいぶんと楽にコーディングできました。
※16ビット分の処理を展開してしまったので、記述量に比べてコードサイズはドカンと爆発💥しています。spi_transferサブルーチン1つで500バイト弱でした。ほどほどにした方が良いですね……。
iverilogを使ったシミュレーションでSPI通信を試したところ、以下図の様になりました。
上図シミュレーション波形では、SS=port_out[7]、SCK=port_out[6]、MOSI=port_out[5]、MISO=port_inp[5]となっていて、MOSIからMISOへローカルループバックしています。
データ(MOSI,MISO)の変化タイミングと、SCK↑のタイミングが非常に厳しく見えますが、MAX7219のACスペックにおけるデータセットアップ時間tDS=25ns(MIN)は余裕で満たしており、問題ありません。
複数回転送して1ビット当たりの平均実効クロック周波数を求めたところ、約1.4MHzでした。24MHzのMoscoviumマイコンで、かつソフトウェアSPIでこのスピードなら申し分ないでしょう。
今回のソフトウェアSPIでは、rept疑似命令でループアンローリング(手動)して、やっとこのスピードですので、これ以上の速度を求める場合にはハードウェア化が必要となります。
前節で作成したソフトウェアSPIルーチンを使って、アプリケーションを作って見ましょう。
8×8[ドット]のLEDモジュールですので、1文字程度のアルファベットや数字などを表示することは可能ですが、「1文字だけ」ですね。"hello,world"などの文字列情報を表示するにはドット数が少なすぎます。しかし、LED 1個や2個ではなく、8×8[ドット]=64個も搭載しているのですから、工夫すれば何かしらの文字列情報を伝えることはできるはずです。
以下、目標仕様とします。
文字コードを8ビットに拡張してS-JIS半角カタカナ文字まで広げたらカナ表示も可能ですし、8×8[ドット]で平仮名フォントを作れば平仮名表示も可能です。ただし、必要な方の各自対応と言う事でお願いしますm(1__)m。
※その後、「#18-4 Moscoviumと日本語フォント」にて、8×8[ドット]日本語フォントを導入してみた記事がありますので、ご覧ください。
例えば、来店したお客様へのメッセージとか、何か注意を惹きたい場合の表示器として使用できそうです。
MAX7219の制御(コーディング)は、Arduino UNOで作成したプログラム(matrix8x8.ino)が参考になるはずです。C言語の方が得意な方は、そちらを参照して見てください。Arduinoで「SPI.transfer16()」している周辺の部分を、前節で作成したソフトウェアSPIルーチンspi_transferに置き換えてコーディングすることで、MAX7219を制御して8×8 LEDにデータを表示することが出来ます(↓)。
// initialize max7219
ldwi r1,(led_decmd<<8)+0x00 // no decode mode
jalwr7 spi_transfer
ldwi r1,(led_inten<<8)+0x00 // minimum intensity
jalwr7 spi_transfer
ldwi r1,(led_scan<<8)+0x07 // full scan mode
jalwr7 spi_transfer
ldwi r1,(led_shut<<8)+0x00 // shutdown mode set
jalwr7 spi_transfer
以下に、完成したプログラム(Moscovium用アセンブラソース)をリンクしておきます。
完成プログラム:flash_mesg.asm
※必要なインクルードファイルなどはこちらからダウンロードできます。
アセンブルしてMIFファイルを作成したら、以下リンクからデザインデータをダウンロードして、MCOC115TW0408をFPGAボードに書き込みます。手順はこちらです。
FPGAボード(Sipeed Lichee Tang Primer)に書き込むと、シリアルモニタから以下のコマンドを受け付けるようになっています。
実験して実物のFPGAボードと8×8 LEDで動作を確認して行きましょう。FPGAボードとUSB⇔UART変換モジュール(FTDI)、8×8 LEDを以下表の様に接続します。
FTDI | 接続 | FPGAボード | 接続 | L/S | 接続 | 8×8 LED |
5V | → | HV | → | VCC | ||
GND | ⇔ | GND | ⇔ | GND | ⇔ | GND |
HV1 | → | CS | ||||
HV2 | → | CLK | ||||
HV3 | → | DIN | ||||
3.3V | → | LV | ||||
C15(port_out[7]、SS) | → | LV1 | ||||
C16(port_out[6]、SCK) | → | LV2 | ||||
B10(port_out[5]、MOSI) | → | LV3 | ||||
TXD | → | R7(uart_rxd) | ||||
RXD | ← | T7(uart_txd) | ||||
VCC(ジャンパ:3.3V側) | × |
8×8 LEDの表示ドライバMAX7219は5V定格なので、間にレベルシフタ(L/S)を挟む必要があります。
以下、実験中の様子です。
手元の8×8 LEDでは、上写真の方向で上下が正しくなるようなフォントを作成してあります。お使いの8×8 LEDによっては、90°単位で回転してしまったり、ことによると、天地や左右が反転してしまうかもしれませんが、それを補正するコードは入っていませんので、正しく読める方向に8×8 LED本体の物理的位置を回転させて使ってくださいm(1__)m。あるいは、ご自身でフォントデータを正しい方向で作成し直すとか。
実験サンプル:【 】内はシリアルモニタからの送信データ
【fmsg,500,HELLO! I AM Moscovium, ORIGINAL 16 BIT CPU】
OK: fmsg done
うまく表示されましたか?0.5秒毎に1文字づつ表示されたはずです。
プログラム上、コマンド名や改行コード、終端コード(NIL、'\0')まで含めて、max_lbuf(=64)バイトまでしか受信できないようになっているので、もっと長い文字列を表示したい場合は、定数max_lbufを大きな値に設定してアセンブルしてください。
【wr,0c01】
OK: wr=0x0c01
…… 最後の文字が表示されます(じっくり確認したい時用)。
小文字はフォントが小さめに作ってあるためか、少し読みにくいので、大文字のメッセージがお勧めです。
以下、動作中の様子を動画にしてみました。
Moscoviumマイコン+ソフトウェアSPIを使って、8×8 LEDを制御可能なことを確かめました。ハードウェア化(SPI通信ユニットの設計)が必要かどうか?は、ソフトウェアSPIの通信速度では足りない様な通信相手やアプリケーションの有無に掛かっています。基本的にはソフトウェアSPIで済ませたいな〜(どうだろ?)。