前回設計したSTWSER(Synchronous Two Wire Serial)ユニットをMoscoviumのソフトウェアで制御して、実際のI2C通信機器と通信してみたいと思います。
今回のテーマはI2Cのマスタ通信となります。スレーブ通信の方は、また次回に譲ります。
STWSERマスタユニットのレジスタに値を設定すると、基本的には指定した通りに動作します。たとえその動作がI2C仕様にそぐわない動作であったとしても、です。従って、ソフトウェア(ドライバ)を設計するプログラマが、I2C仕様を理解してI2C通信プロトコルに則ったサイクルを発生させなければなりません。以下、不正なバスサイクルの例を挙げます。
※本ページでは、こちらで示した記法でI2Cバスサイクルを表記します。
この様なアクセスを実行すると、I2C仕様に準じたモジュールは誤った解釈をして、誤動作する場合があります。
上に挙げた例の様な不正なバスサイクルも、全てSTWSERマスタユニットから生成可能です。STWSERユニットは、言われた通りに、指示された通りに、動作します。ソフトウェアが間違っていれば、いくらでも不正なバスサイクルを発生できるようになっています。
ソフトウェアのプログラマはまず、I2C仕様の理解が必要となります。
STWSERユニットを使ってI2C通信するには、STWSERユニットのI/Oレジスタを叩いて所望の動作をさせる必要があります。
「難しい、難解、わかり難い」のは、嫌いなので、それほど難しくしたつもりはないのですが、STWSERユニットのレジスタ数は非常に多く、設定・確認すべきビット数も相当数に及んでいます。元々のI2C仕様が複雑なため、この程度で抑え込むのがやっとでした。
そこで、ソフトウェアの作成を少しでも容易にするため、プログラマ(ユーザ)からSTWSERユニットのレジスタを隠ぺいして、簡単にコーディングが可能な様に、ドライバソフトウェアを作っておこうと思います。
I2C機器との通信を実行したいユーザは、以下に示す「I2Cフレーム」をRAM上に作成し、I2Cフレームの先頭アドレスを汎用レジスタr1に代入して、サブルーチンmcvm_wireをコール(jal命令)します。
I2Cフレーム仕様:
サブルーチン内では、I2Cバス仕様に関するエラーチェック等は実行していないので、I2Cバス仕様に準拠したプロトコルでI2Cフレームを構成するのはユーザ責任となります。
以下、サブルーチンmcvm_wire内の処理フロウを示しておきます。
サブルーチンmcvm_wire内の処理フロウ:
サブルーチンmcvm_wireは、I2Cとの通信を実行する間、処理をブロックします。割り込みを使ってリアルタイム処理に対応したい場合など、STWSERユニットのレジスタを直接叩いて制御したいケースでは、サブルーチンmcvm_wireによる制御(レジスタの設定手順等)を参考に、各自対応ください。
※ArduinoのWireライブラリでは、Wire.endTransmission(stop=false)で、STOPコンディションを発行せずに次のbeginTransmission()で反復STARTコンディションを発行できるので、間に別処理を挟める仕様です。ただ、一連の送信動作と受信動作は処理をブロックしているので、使い勝手はそんなに大きく変わらないはずです。
以下に、Moscovium Wireライブラリのマスタドライバファイルをリンクしておきます。
Moscovium Wireライブラリ:
ヘッダファイル:mcvm_lib_wire_h.incl
マスタドライバ:mcvm_lib_wire_m.incl
ヘッダファイルはアセンブラソースの先頭付近で読み込んで(incl疑似命令)ください。マスタドライバは、アセンブラソースの末尾付近など、通常、処理の届かない場所で読み込んでください。
所望のI2CフレームをRAM上に構築したら、汎用レジスタr1に先頭アドレスを格納して、サブルーチンmcvm_wireへ分岐します(↓)。
// STWSERマスタユニットの初期設定
ldwi r0,fcpu/400000-1 // scl speed=400kHz
ldwi r7,stwmbaud
stw [r7],r0
// I2Cフレームを作成する
:
:
ldwi r1,i2c_frame // 先頭アドレスをr1にセット
jalwr7 mcvm_wire
サブルーチンmcvm_wireをコールする場合の初期設定は、上のコード例に示した通り、マスタ通信速度レジスタstwmbaudに所望の通信周波数を設定するだけです。
※ArduinoのWireライブラリの様に、ユーザからI2Cの通信仕様まで隠ぺいするためには、更にもう一枚皮を被せないとなりません。本ページでは「ここまで」、としておきます。
用意したサブルーチンmcvm_wireでは割り込みを使っていませんが、使う場合、どのタイミングで割り込みがかかるかを説明しておきます。
STWSERマスタユニットでの割り込みタイミング:
スレーブユニットの割り込みタイミングについては、次回解説します。
I2Cバス上に接続されている機器をスキャンして検出する場合、以下の様な短いバスサイクルをアドレスADR毎にキャストして、毎回スレーブからのアクノリッジ(A)またはノットアクノリッジ(N)応答を確認します。
I2C検出バスサイクル(凡例): S [ADR,W](A/N) P
アドレスバイト[ADR,W]を受信して、スレーブアドレスADRが自身のアドレスと一致する機器は、[ADR,W]に対してアクノリッジ(A)を返します。アドレスが一致しない場合または、I2Cバスに機器が接続されていない場合、ノットアクノリッジ(N)が返ります。
全7ビットアドレスADR=0x08〜0x77をスキャンして、アドレスバイト[ADR,W]に対するアクノリッジ状態を確認すると、機器の有無を判定することが可能です。
マスタドライバを使う前提でプログラムを組んで行きます。まず、I2CフレームをRAM上に作成します。
とても短いバスサイクルなので、以上2ワードで終了です。I2Cフレーム先頭のアドレスをr1に設定して、「jalwr7 mcvm_wire」で終了です。返り値r0を確認して、機器の有無を判定します。
完成したプログラムを以下にリンクしておきます。
I2C検出プログラム:i2c_detect.asm
※必要なインクルードファイルなどはこちらからダウンロードできます。
Moscoviumシリアルモニタから、以下のコマンドを受け付けるようになっています。
ここからは、前回作成したデザインMCOC115TW0408を使って、FPGAボード(Sipeed Lichee Tang Primer)で動作を確認して行きます。必要なデザインファイルを以下リンクよりダウンロードしたら、コンパイル(論理合成・配置配線)してFPGAボードに書き込んでください。手順はこちらです。
この時、プログラム(i2c_detect.asm)をアセンブルして完成したMIFファイルを、ROM用RAMマクロの所定のディレクトリに格納しておくことをお忘れなき様。
実験サンプル:【 】内はMoscoviumシリアルモニタからの送信データ
【scan】
OK+ start scan -- -- -- -- -- -- -- --
10 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40 -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- --
50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60 60 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70 -- -- -- -- -- -- -- --
OK: i2c detect
I2C機器からノットアクノリッジ(N)が返ったアドレスには「--」が表示され、アクノリッジ(A)が返ったアドレスには、その16進数値が表示されます。
この実験で積極的に接続したのは、次節で実験に使用する12ビットDA変換器モジュールMCP4725だけで、アドレスは0x60です。ちゃんと0x60が検出できていますね。よしよし。……と、何か居ますね?アドレス0x48に。
プログラムまたはSTWSERユニットのバグでごみが見えたのか?と思いましたが、ずいぶんと安定して見えています。何回やり直してみても、毎回0x48が検出されます。FPGAボードの回路図を入手して確認したところ、SCL,SDA端子に指定したP2,R2端子には、FPGAボード上のタッチスクリーンコントローラNS2009が接続されているようです。
NS2009のデータシートを検索する(別ウインドウが開きます)
データシートによると、NS2009のスレーブアドレスは0x48と言う事で、ビンゴです。今のところ使い道は無いのですが、FPGAボード上に搭載されたI2C機器もちゃんと検出できました。OKです。
※凄く遅いI2C機器が接続されているかもしれないので、i2c_detect.asmのI2Cクロックスピードは10kHzに設定してあります。もっと遅い機器の検出が必要な場合、プログラム先頭付近の定数設定で調整してください。
12ビットDA変換器モジュールMCP4725(下図)を接続して、I2Cで制御してみたいと思います。
使い方については、「#7-9 WAVファイルを再生する」をご覧ください。基本的には、I2Cを使って12ビットのデジタルデータを送信すると、送信した値に従ったアナログ電圧を出力してくれるモジュールです。従って、必要なI2Cバスサイクルは以下の通りとなります。
MCP4725のI2Cバスサイクル(凡例): S [ADR,W] [DATH] [DATL] P
これに従って、mcvm_wireドライバ用のI2Cフレームを作って見ましょう。
MCP4725用I2Cフレーム:
以上4ワードのデータをRAM上に生成し、先頭アドレスを汎用レジスタr1にセットしてから「jalwr7 mcvm_wire」です。
MCP4725を使って、指定した周波数の正弦波を出力するプログラムを作って見ました。以下にリンクしておきます。
正弦波発生プログラム:sin_wave.asm
※必要なインクルードファイルなどはこちらからダウンロードできます。
アセンブル後、デザインMCOC115TW0408をコンパイル(論理合成・配置配線)して、FPGAボードに書き込んでおいてください。手順はこちらです。
Moscoviumのシリアルモニタから、以下のコマンドを受け付けるようになっています。
FPGAボードとUSB⇔UART変換モジュール(FTDI)、MCP4725モジュール、正弦波観測用のSTM32(Arduino)を以下の様に配線します。
FTDI | 接続 | FPGAボード | 接続 | MCP4725モジュール | 接続 | STM32 |
GND | ⇔ | GND | ⇔ | GND | ⇔ | GND |
3.3V | → | VCC | ||||
P2(stws_scl) | ⇔ | SCL | ||||
R2(stws_sda) | ⇔ | SDA | ||||
OUT | → | PA0(ADC0) | ||||
TXD | → | R7(uart_rxd) | ||||
RXD | ← | T7(uart_txd) | ||||
VCC(ジャンパ:3.3V側) | × |
※FPGAボードの端子設定(MCOC_full.adc)によって、P2端子(SCL)とR2端子(SDA)には入力プルアップ抵抗が利いています。加えて、MCP4725モジュールのボード上にもプルアップ抵抗が付いているので、外付けのプルアップ抵抗は不要です。
本ページでは、正弦波観測用ArduinoにSTM32を使用しました。Arduino(STM32等)には、Logiscopeを書き込んでおいてください。
以下、実験中の様子です。
実験サンプル:【 】内はLogiscopeからの送信データ
【osc,0,4,4,0】
…… タイムアウトする前に、以下のコマンドを実行する。
実験サンプル:【 】内はMoscoviumシリアルモニタからの送信データ
【sin,100】
OK: sin output finished
Arduino(STM32)+Logiscopeで取得したアナログ波形は以下の様になりました。
【sin,100】コマンドによって、100Hzの正弦波を生成するよう指示しました。正弦波の山→山間隔を測定して周波数を求めたところ、約99.8Hzと、指定した通りの周波数であることが判りました。
所望の波形を生成できていることから、STWSERユニットとMCP4725間のI2C通信は正常に実行できている、と判定します。実機検証合格です。
正直、I2C通信プロトコルに則ったI/Oユニットを自分で作るなんて、泣きそう😰だったのですが、実機でI2C機器と通信できてほっとしています。この調子で、次回はスレーブ通信も実機検証してみます。