Teensy 4.0 で I2C 通信を実行しようとして苦労した話を紹介します。
最終的には I2C 関数の自作に至ります。
ysin1128.hatenablog.com
さらに、I2C に加えて SPI と GPIO も制御する Sketch とツールも作成しました。
ysin1128.hatenablog.com
はじめに
Arduino IDE (+ Teensyduino) には Teensy 4.0 でも動作する Wireライブラリが含まれているのですが、この記事ではそれは使わずに I2C 通信を行っています。I2C 通信が目的であれば素直に Wireライブラリを使った方が遥かに簡単で確実です。私がそれを使わずに苦労したのは、ただの趣味です。
準備
Teensyduino をインストールして Arduino IDE を拡張します。Teensyduino に関しては以下のリンクをご参照ください。
Teensyduino: Download and Install Teensy support into the Arduino IDE
レジスタにアクセスするまで
Wire ライブラリを使わないのであればマイコンのレジスタを直接読み書きしなければならないのですが、これができるようになるまでも一苦労でした。
レジスタマップの入手
レジスタの読み書きをしようと思ったら、どこにどんなレジスタがあってその値を変えるとどうなるのかを教えてくれるレジスタマップが必須です。というわけで、第一歩は Teensy 4.0 に搭載されているマイコン IMXRT1062 のレジスタマップの入手です。
メーカーである NXP の製品ページに行って Data Sheet をダウンロードして完了、と思ったのですが、データシートにはレジスタマップが載っていません。Documation のリンクを辿って関連するドキュメント一覧を探すと i.MX RT1060 Processor Reference Manual というのがそれっぽいです。では、それをダウンロードしようと思ったら、このファイルをダウンロードするためには登録が必要とのこと。面倒ではありましたが登録するしかないので、素直に登録して入手しました。なお、レジスタマップが載っているのはこのファイルで正解でした。
レジスタを読み書きする方法
次は、Sketch でレジスタを読み書きするための記述の確認です。これはライブラリの記述を参考にしました。Arduino のレジスタの読み書きでもそうだったのですが、Teensy でも Arduino IDE がレジスタの名前とレジスタを関連付けてくれているのでそれに従えば良いようです。
関連付けの詳細は \arduino-1.8.13\hardware\teensy\avr\cores\teensy4\imxrt.h で確認できます。テキストエディタで開くことができます。
imxrt.h では、例えば以下の記述で LPI2C1 関連のレジスタが定義されています。
#define IMXRT_LPI2C1 (*(IMXRT_LPI2C_t *)0x403F0000) #define LPI2C1_VERID (IMXRT_LPI2C1.VERID) #define LPI2C1_PARAM (IMXRT_LPI2C1.PARAM) #define LPI2C1_MCR (IMXRT_LPI2C1.MCR) #define LPI2C1_MSR (IMXRT_LPI2C1.MSR) #define LPI2C1_MIER (IMXRT_LPI2C1.MIER) #define LPI2C1_MDER (IMXRT_LPI2C1.MDER) #define LPI2C1_MCFGR0 (IMXRT_LPI2C1.MCFGR0)
そこで Sketch では以下の記述で VERID の値を取り出すことができます。
uint32_t pageTMP = LPI2C1_VERID;
ちなみに Teensy のレジスタは 32-bit / page (たまに 16-bit / page もあり) なので、正しく読み出すためには 32-bit の変数が必要となります。
レジスタの読み書きを試す
レジスタマップと読み書きの方法が分かったので、さっそく下記のコードでレジスタの読み書きを試してみました。
int cntDAT; byte inDAT; IMXRT_LPI2C_t *port_i2c; void setup() { Serial.begin(9600); port_i2c = &IMXRT_LPI2C1; } void loop() { cntDAT = 0; while(Serial.available()){ inDAT = Serial.read(); cntDAT++; delay(5); } if(cntDAT > 0){ Serial.print(port_i2c->VERID); Serial.print("-"); Serial.print(port_i2c->PARAM); Serial.print("-"); Serial.print(port_i2c->MCR); Serial.print("-"); port_i2c->MCR = 0x00000001; Serial.print(port_i2c->MCR); Serial.print("-"); } delay(1000); }
ここではレジスタを指定する方法を変えています。
まず、imxrt.h の中で定義されている IMXRT_LPI2C1 という名前の構造体があるのですが、これを port_i2c という変数にコピーしています。
IMXRT_LPI2C1 は I2C 設定用レジスタを指すポインタを集めたアドレス帳のようなものとなっています。IMXRT_LPI2C1 そのものを使ってアドレスの指定を行っても良いのですが、私の好みの問題で、自分で作った変数である port_i2c にこのアドレス帳をコピーしています。
IMXRT_LPI2C1 は、これも imxrt.h の中で定義されている IMXRT_LPI2C_t という構造体なので、それをコピーする port_i2c も同じ構造体で定義しています。
IMXRT_LPI2C1 = port_i2c は中に VERID や PARAM といった名前の変数を持っていて、これら名前はレジスタマップで I2C 設定用レジスタの各アドレスに付けられている名前と一致するように付けられています。そして、各変数の中身が該当するアドレスへのポインタとなっています。よって、port_i2c->VERID が I2C 設定用レジスタの中の VERID と名付けられたレジスタへのアクセスになります。
さて、上のコードは Arduino IDE のシリアルモニタで何かを送信すれば、それをトリガに Teensy が指定したレジスタ値を読み出して送り返してくれるというものになります。読み出すのは I2C 設定用レジスタから VERID (Version ID)、PARAM (とあるパラメータ)、MCR (制御レジスタ) の3つのレジスタの値と、それらを読み出した後に MCR に 0x00000001 を書き込んでから MCR の値をもう一度読み出した値となります。ちなみに値を書き込む MCR[0] は I2C Master の Enable で、まさにこれから使いたい機能です。
そして下図がその結果です。
VERID = 16842755 = 0x01010003
PARAM = 514 = 0x00000202
MCR (書き込み前) = 0 = 0x00000000
MCR (書き込み後) = 0 = 0x00000000
レジスタマップと比較して VERID の Minor Version Number が一つ上がっていましたが、それは特に問題ありません (ちなみにレジスタマップでは 0x01000003)。PARAM と MCR の書き込み前の値はレジスタマップの通りです。問題は、書き込み後の MCR の値も 0 のままとなっていることです。つまり、書き込みに失敗したのです。
書き込み失敗の原因探し
色々と考えましたが、これらは全部上手くいきませんでした。
- レジスタ指定の方法が悪い?
同じ方法で I2C 以外の機能を制御する他のレジスタをいくつか指定して読み書きを試したところ、書き込みできるところ、できないところとがありました。書き込みできない理由は不明なままですがレジスタ指定の方法が悪いというわけではなさそうです。
- Clock が来ていない?
Teensy は省電力のために、使わない機能ブロックの Clock を遮断することができるようになっています。これは CG (Clock Gating) という機能で CCM_CCGRx レジスタで設定します。使おうとしている LPI2C1 というブロックの Clock も CCM_CCGR2[7:6] で制御されているのですが、レジスタマップによると初期設定は 11 (常に ON) となっているので問題無さそうです。
- 書き込みを禁止する Bit がある?
レジスタ値の変更を禁止する Lock bit のようなものが無いかと考えましたが、それらしいものは見つかりませんでした。また、I2C 以外のところでは書き込みできるレジスタもあることから、これは可能性が低いと考えました。
この問題で1月以上悩みました (仕事が忙しくなって Teesy を触ってる暇が無くなったというのもありますが)。一時は不良品の可能性すら疑ったのですが、どこかに答えが落ちていないかと Google を漂っているうちに Teensy の Forum を見付けたので、ここで相談することにしました。答えは実にあっさりと得られました。
書き込み成功
下記のコードでレジスタの値の書き込みに成功しました。
int cntDAT; byte inDAT; IMXRT_LPI2C_t *port_i2c; void setup() { Serial.begin(9600); port_i2c = &IMXRT_LPI2C1; CCM_CCGR2 |= 0x000000C0; // CCM_CCGR2[7:6] = 11 を書き込み } void loop() { cntDAT = 0; while(Serial.available()){ inDAT = Serial.read(); cntDAT++; delay(5); } if(cntDAT > 0){ Serial.print(port_i2c->VERID); Serial.print("-"); Serial.print(port_i2c->PARAM); Serial.print("-"); Serial.print(port_i2c->MCR); Serial.print("-"); port_i2c->MCR = 0x00000001; Serial.print(port_i2c->MCR); Serial.print("-"); } delay(1000); }
追加したのは LPI2C1 の Clock Gating のレジスタへの 11 (常に ON) の書き込みです。なお、該当レジスタの指定が CCM_CCGR2 で良いかどうかは imxrt.h で確認しました。
原因は Clock Gating が OFF になって Clock が届いていなかったことでした。レジスタマップの初期値では Clock Gating は常に ON だったのですが、どうやら Arduino IDE が書き込む際にここを OFF にするコードも紛れ込ませていたようです。Arduino IDE が、ユーザーが使いやすいようにと色々と設定してくれているのは知っていて、これもその一部だと思うので「Arduino IDE のせいで」などとは言わないものの、1月以上も悩んだ原因がこれだったのかと思うと、素直に納得できないものがあります。
しかし、後から振り返ってみると、この苦労が以後のコード作成で物凄く役に立ったので、これは必要な経験だったのだと思います。
ともあれ、無事にレジスタの読み書きはできるようになったので、ようやく次のステップに進めます。