Tiny861のGPIOによるソフトI2Cスレーブソースの公開
久しぶりにプログラミングに熱中していた。AVRのGPIOだけで動くI2Cスレーブインターフェースのライブラリ開発である。前の記事で、曲りなりにもデータの送受信が出来たことはお伝えしたが、安定して動作するまでには、やはり相当な時間が必要だった。
ソフトI2Cスレーブを作ろうと思いついたきっかけは、前記事のとおり超音波距離測定ユニットHC-SR04の出力インターフェースをI2Cにするためである。超廉価な中華コピー品のSR04のインターフェースはアナログで、トリガーをかけると、距離に応じた長さのパルスを返してくる。しかも電源が5Vなので、EdisonやRasPiとは相性が余り良くない。
で、ミニ基板にDC-DCコンバーターと、I2C変換用の8ピンAVRあたりを載せたブレークアウト基板を作れば面白かろうと始めたのだが、これがえらく難しい。巷(ちまた)にGPIOだけでI2Cのスレーブインターフェースを実現した制作例が極めて少ない理由が良くわかった。
難しい理由はI2C通信の特徴であるスタート/ストップコンディションの認識である。マスター側は作る方だから楽だが、スレーブ側でこれを検知するのはとても厄介だ。プログラムを割り込み駆動にしなければならないため、よほどCPUクロックを早くしないと、2本(データとクロック)の割り込みをとりこぼしてしまう(あとから来たものが無視されるか遅れで正しい判断ができない)。
I2Cスレーブと言えば7年前、AVRのTinyシリーズについているハードの通信インターフェースUSI(Universal Serial Interface)を使ってライブラリを作成し公開したことがあるが、USIには、もとからスタート/ストップコンディションを検知する特別なハードがついているのでこの難しさはない。
今度の最終ターゲットであるTinyの8ピンシリーズには、そもそもUSIがない。出来ないとなると無性に作りたくなる性格で、今度もかなり強引なテクニックを駆使して(前記事参照)、GPIOだけでI2Cのスレーブインターフェースをでっちあげた。
さらに、一工夫してリピートスタートコンディションも受け取れるようにした。まだテストが十分ではないので、胸を張れる状況ではないが、今のところ問題なく動いている。テストバージョンとしてソースコードをとりあえず公開することにする。
さて、これがどういう風に出来上がったのか。例によっていつものドタバタ劇をご紹介する。
データが化ける。ロジアナでは正確なのに(8/25/2015)
テストベンチはブレッドボード上である。Tiny861が2台載っており、一台は超音波距離測定ユニットHC-SR04がつながり、こちらがスレーブである。I2Cインタフェースの相手方の861はマスターで、両方ともテスト用のUARTで実行処理の制御と結果の表示をする。
テストは、双方のUARTコンソールにコマンドを入れてはメッセージを確認し、ロジックアナライザーで波形を調べる。そのくりかえしである。前回記事までに、1バイトのマスターからの送信(コマンドのつもり)と、2バイトのスレーブからマスターへのデータ読み込み(データ送出のつもり)は、単独では成功している。
スレーブ側のソースはライブラリを意識して、I2Cのインターフェースすべてをソースファイルひとつ(i2c_slave.c)とヘッダーファイル(i2c_slave.h)につめこんだ。メインであるi2c_HR04_861.cに組み込んだのは、SR04のドライブ部分だけでI2Cスレーブ機能とはすべて関数経由でやりとりする。データは直接渡さない(外部参照もさせない)。
I2Cのスレーブ側は、すべて割り込みによって始まる。通信が始まってからは、クロック(SCL)のエッジを追っていけば良いのだが、通信シーケンスの最初を示すスタートコンディションは、SCLがHighのときにデータ(SDA)がHighからLowに下がるという決まりになっている(送受信中はSCLがHighのときにSDAが変化してはいけない)。これが厄介である。
前の記事にあるように、これをSCLとSDAの双方とも割り込みで受けようとすると、割り込みのオーバーヘッドが大きく、よほどI2Cそのもののクロックを遅くしない限りスタートコンディションを把握できない(I2Cの転送速度は100kbpsが目標)。
そこで今回は、最初、SDAの割り込みだけを待ち構え、SDAがHighからLowになったとき、SCLを調べてスタートコンディションを判定し、そのあとすぐSDA側の割り込みを止め、SCLの割り込を有効にして、このあとのデータシーケンスをSCLのエッジで受け取るという裏ワザに近いロジックを考え付いた。これで何とか多バイトの送受信に成功した。
しかし、最初は良いが暫くするとデータの中味がおかしくなる。それに、マスター書き込みのあとのマスターからのストップコンディションを認知していないので、通信はタイムアウトで終わるしかない。こんな状況ではまだとても実用になるレベルではない。さらなる作りこみが必要である。
不思議なことに、字化けはロジアナで見る限りマスターは常に正しい値を送っているのに、スレーブが違った文字として受け取っていることである。もっとも、I2Cはピンの入力方向設定(HiZ)とプルアップ抵抗でHigh(1)となり、ピンの出力方向設定でLow(0)になるという微妙な仕掛けで0、1を表現しているので、このロジアナの示すデータが本当にスレーブ側のピンの状態を示しているのかは自信がない。
UARTとかぶっていた。これは解決。次はストップコンディション(8/27/2015)
何度もテストを繰り返すうち、不具合に種類が2つあることがわかった。ひとつは先のデータ化けだが、もうひとつは通信エラーである。タイムアウトのおかげでプログラムはハングしないで戻るが、通信そのものがおかしくなっている。
こちらの方は、ロジアナのプローブをスレーブのテスト用UARTの前後に入れてみて原因が判明した。時間的には十分離れていると思っていたUART出力部が、I2Cの最後の方と被っていたのだ。このUARTはソフトUARTで送信のときは割り込みを禁止している。これが割り込みで動くスレーブシーケンスの中に入ると目茶目茶になるのは当たり前のことだ。
究極の解決ではないが、適当な待ち時間をデバッグ用のUART出力の前に入れて衝突を回避した。これで通信エラーは全くなくなった。データ化けは根が深そうなので、もうひとつの課題、ストップコンディションの認識が出来ないか、ロジアナの波形やウェブの情報を元に頭をひねる。
現在は、受信も送信も、マスターから送られたストップコンディションを認知せず、通信終了はタイムアウトに頼っている。実用的にはSR04の距離測定の最小間隔は、人間相手なら0.5秒前後、ロボットに使っても0.1秒程度(そもそもSR04の測定時間が30ms近く必要)なので、100ms程度のタイムアウト(テストのため現在は1秒)で問題ないのだが、ライブラリとして公開するためには、何とかストップコンディション認知を実現させたい。
ストップコンディションのキャプチャーに成功(8/28/2015)
ロジアナの波形を目を皿のようにして分析しているうち、ストップコンディションが出される場所はノーマルなときは必ず決まっていることに気付いた。1ブロックのデータの送受信が終わって、ACK/NACKを採取(または発行)するビットのあと、次の最初のデータ(第一ビット)のSCLパルスの部分しかストップコンディションは発行されない。
もちろん、本式のハードのI2Cはデータストリームの途中でも、スタート/ストップコンディションを拾えるようになっているのだろうが、ソフトI2Cにここまでを要求するのは酷だろう。ストップコンディションが出てくる場所が限定されているなら実装は可能だ。
つまり、データをマスターから1ビットづつ受け取るステートマシンのステップで、第一ビットのSCLがHighの段階でデータをとったあと、割り込みを抜けずにSDAの変化をそのままループで調べ続ける。SCLが下がるところまで調べて、割り込みルーチンを抜ける。
SDAに変化がないときは、ペンディングになっていた立下りの割り込みがすぐここで入って通常通りのデータ処理が続く。SDAが変わっていたら、これがストップコンディションである。処理を中断して通信終了処理をする。
うむ、我ながら良くできた。動く予感がする。いそいそとステートマシンのマスターデータ書き込みの部分にコードを加える。たいしたコード量ではない。何度もロジックを確かめて、テストに入る。
マスター側のPCコンソールからデータを送る。スレーブに送られたデータがPCのスレーブ側のUARTに表示される。 これまでは、このあと必ずタイムアウトのメッセージが出ていたのだが今度はどうだ。おーし、いくら待ってもメッセージは出て来ない。良いぞ。何度も確かめて、タイムアウトしないことを確認する。
データはまだ化けてしまうが、送信のストップコンディションの認知には成功したようだ。思っていたロジックが絵に描いたようにうまく動いたときは、どんな小さなものでも格別に嬉しいものだ。何度もコマンドを入れては子供のように喜ぶ。
16Mhzにクロックを上げると安定するが、改善せず(8/29/2015)
字化けの問題である。不思議なことに、マスター読み込み(スレーブ送信)の方はほとんどないのだが、マスター書き込みは正しいのは最初だけで、そのあとは殆ど字が化ける。書き込み宣言のアドレス読み込みはエラーとなったことがないので、おかしいのは、データ書き込みのステートマシンのところであることは明らかだ(ビットハンドリングは全く同じロジックである)。
ライブラリを意識しているので、I2Cのデータは、すべてバッファーに貯め非同期でメインとやりとりするようになっている。このあたりがあやしい。I2Cの送受信シーケンスのミスというより、バッファーのハンドリングがうまく行っていない可能性が高い。
ということで、バッファーの中味をダンプするコマンドを追加して、送られたデータの中味を検証した。しかし問題はなかった。多数バイトのバッファーでも正確に、データを入れ込んで最後でまた最初に戻る。データの汚れは、これ以前に起きている。バッファーの中を誰かが汚しているわけでもなさそうだ。
マスター受信の方はほぼ完全である。NACKがとれないのが問題として残るが、データは問題ない。しかし送信で相変わらずデータが汚れる。ロジアナでは全く問題ないのに何回かやるとデータが化けてくる。化け方が気に入らない。何かビットが増えて行くような感じである。
そうなると原因は、I2Cのデータを読み込むビットハンドリングに疑いがかかる。前から気になっていたのだが、SCLの割り込みから実際に割り込みルーチンに制御がわたるのは、8Mhzのクロックで早くても5.4μsかかっている。
I2Cの現在のクロックはおよそ60kbpsで、SCLが0の区間は、8μs。次のSCLの立ち上がりに近くちょっと不安である。もしかすると、次のSCLクロックにはいってしまってエラーになっている可能性がある。
というので、意を決して、スレーブのCPUクロックを上げることにした。8Mhzから倍の16Mhzにしてみる。ロジアナで見ると、割り込みのオーバーヘッドは、半分の2.7μsまで下がり、波形がずいぶんすっきりした。ack/nackの判定ビットの処理も余裕である。しかし事態は改善しない。マスター受信は問題ないが、マスター書き込みはゴミが出る。頭が痛い。
最後はやっぱりドジなミス。I2Cスレーブとりあえず完成(8/31/2015)
この1週間頭を悩ませていた不具合は、解決してみたらどうしてこれまで気が付かなかったのだろうという、またもお馬鹿な不具合だった。同じような文章をこれで何度書いたことか。
ここに書くのもお恥ずかしい、単なるバッファーのクリア忘れである。データはビット単位に送られてくるので、1ビット単位のORで受ける。1バイト受ければ、バッファーの次のバイトに移る。これを使い増ししたら、次の回からは、前のデータにORがかかってデータは化けるというわけである。
ははは、情けなくて笑うしかない。タイミングとデータライン(SDA)のHiZ化、ACK/NACKのときのSDAの値など微妙なところを探し回っていたが、問題は全く別の基本的なところだった。クロックなど上げなくても良かったのだ。
変数の初期化忘れなど初心者がやるミスである。バッファー経由なので、ビットを受けるところは毎回新しいデータで始めているとばかり思っていた。ソースコードをよく見れば、ORを受けるところがバッファーの配列であることにすぐ気が付くはずなのに、思い込みと言うのは恐ろしい。
送信がうまく行くように見えたときは、同じデータを送っていたからである。ああ、何というお粗末。これを直して、データの汚れは全くなくなった。やっと一歩前進である。肩の荷が下りた。あとは、マスター受信をタイムアウトでなく、まともなストップコンディションで、通信を終了することだ。
遂に送受信ともOK。GPIOによるAVRのスレーブインターフェース完成(9/1/2015)
やった、やりました。久しぶりの勝利の美酒。マスター読み込みが最後までトラブっていたが、本日、遂に、タイムアウトを起こさず、一連のデータ送信(スレーブから)が問題なく終了した。
マスター読み込みは、通信の終わりにマスターからNACKビットが来るのでストップコンディションを調べる必要もない(マスターは送っているが)はずなのだが、このNACKを認めず、常にタイムアウトになっていた。
送信のエラーがとれて余裕が出来たので、ロジアナのプローブをさらに追加し後処理のプロセスに入れ、受信のシーケンスを仔細に追った。その結果、意外なところが原因であることが分かった。いやいやロジアナさまさまである。
実は、NACKを認めていなかったのではなく、プログラムはNACKを見てとっくに通信を終了させていたのだが、次のストップコンディションの発行をスタートコンディションと勘違いして、この通信のタイムアウトが発生していた。これがタイムアウトの正体だった。
間違っていたのは、スレーブ側ではなく、何と送り側(マスター)のストップコンディション発行の仕方だった。ストップコンディションの前に準備するSDAとSCLの変化のタイミングがロジアナを見ると少し短すぎる。このためSDAの変化の把握が割り込みで遅れ、SCLがHighになったところで認識されるのでスタートコンディションになってしまっていたのだ。
うーむ、これは現在のロジックでは解決できない。暫く、頭を抱えていたが、SCLの変化を少し遅らせればこれは回避できることに気が付いた。 マスターでのロジックにwaitを加え試してみる。めでたくスレーブ受信のタイムアウトはなくなった。
あわてて、おおもとのNXP社の正式仕様を調べる。P48に、スタート/ストップコンディションのSDAとSCLの変化は半クロック(100kbpsで4μs)空けることというスペックを見つけた。良かった。マスターの修正はスペック通りだった。いやあ気分が良い。
だいぶ、I2Cに自信がついてきた。特に、最初良くわからなかった、データラインのChaNさんの方式(入力方向にしてラインHigh、出力方向にしてLow)が完全に理解できたのは大きい。I2Cの基本の部分である、プルアップされたライン上で複数のディバイスの間で通信するテクニックである(ワイヤードORというのだそうだ)。
これまでは見よう見まねでI2Cマスターなどを書いていたが、こんな複雑な(面白い)しかけがあったということがわかっただけでも(いまさらながら)、こういう自作ならではの収穫である。Arduinoなどのラッパー製品を使っているだけではまず出来ない経験だろうと胸を張る(単なる負け惜しみに近いが)。
お約束のソースコードの公開は整理してからと考えたが、少し気が変わった。このテストステートメントてんこ盛りの形のままの方が、かえって参考になるような気もする。何に苦労したかが一目瞭然で情報量は多い。整形した完成品のソースより参考になると思う。
最後のシーケンスの連続化でまたてこずる。しかし遂に完成(9/5/2015)
とはいうものの、もう少し動かして動作を確認しておいた方が良いだろう。当初予定したSR04とI2Cの送受信の組み合わせも動かしておきたい。これが動けば大威張りだ。自信を持ってソース公開に踏み切れる。
マスター側からコマンドを送って、SR04の測定をスタートさせ、得られたデータを送り返させるシーケンスである。これが当初の目的である。I2Cを使ったSR04の制御の第一歩だ。ところが、これがまた難航したのである。
送信、受信とも個別では全く問題なく動作する。ロジアナで見ても、綺麗な波形が想定通りに出ている。今度は、これの組み合わせだが、やることは同じだ。簡単に動くだろう。マスター側のプログラムに手を入れて、通信シーケンスを出すコマンドを作って動かした。
これがまた全く動かない。やれやれどこが悪いのか。ロジアナで波形を見る。ありゃりゃあ、書き込みが終わった後の読み込みのシーケンスがでたらめである。そもそも最初のスタートコンディションを全く拾っていない。これは何か別のスレーブでの動作が邪魔していることは明らかである。すぐ原因に思い当った。最大の容疑者はUARTだ。ここのUARTはソフトUARTなので文字送信中は割り込みを禁止している。
ロジアナに、送信関数の割り込み禁止部分にGPIOピンをあてて、波形をとってみたら一目瞭然だった。考えてみたら至極当然のことなのだが、マスター側で待ちを予想してwaitをとっていたのだが圧倒的に少なすぎた。今度もロジアナのお世話に大変なった。いやあ、こいつは無敵だ。
リピートスタートコンディションの認知も出来た(9/12/2015)
最後に残ったリピートスタートコンディションの実装である。EEPROMのI2Cのように、コマンドを送ってストップコンディションを発行せず、再びスタートコンディション(リピートスタートコンディション)を出して、データ読み込みに切替え、1シーケンスでデータを受け取る機能である。
今度のSR04のシーケンスでは使いにくい(測定に要する時間が長すぎて1シーケンスにしにくい)が、内部のディバイスの状況を伝えるときには便利で実装しておきたい。それに、ストップコンディションを実装しているとき、ここに少し手を加えれば、リピートスタートコンディションもすぐ作れそうな感触だった。
久しぶりの仕事があって電子工作に手が出ない日が続いていたが、一段落したので早速、リピートスタートコンディションの実装にとりかかる。かねて考えていた通り、マスター送信のストップコンディション検知部にコードを追加する。if文にelseを加えるだけである。
ほどなく出来上がった。こいつはテストが厄介で、送り側のプログラムにも手を入れる必要があるが、乗りかかった船である。黙々とこちらも準備する。マスター書き込み宣言で1バイト送った後、連続してスタートコンディションを発行し、マスター読み込みのシーケンスを送り込むコマンドである。
スレーブ側バッファーに残っているSR04の距離データを呼び出すコマンドである。スレーブ側は、多重処理になるのでスレーブ側のテスト用のUART出力は厳禁だ。慎重にwaitステートメントを入れてUARTをずらす(これがないとやっぱり不安だ)。
案の定、頻々とエラーが起きる。うぬー、この _delay_us()のマクロは、ある程度以上の待ちになるとおかしくなって、ちゃんと遅れてくれないようだ。ウェブを見るがどうも要領を得ない。_delay_msなどに切り替えて凌ぐ。
ロジアナで波形を見ながら徐々にデータを正確にしていく。最後に残ったのが、リピートスタートをしたシーケンスのあとに、必ずマスター書き込みデータが化ける不具合だった。リピートスタートは、データの第一ビットを読んだあと初期化されるので、バッファーにデータが残る可能性があるのだが、それがどうして汚れるのか理屈がつかない(必ずというのが気に入らない)。
こうなったら対症療法になるけれど、シーケンスに入る前に必ずバッファーをクリアしてから始めることにした。よーし、直った。これで想定した機能はすべて揃ったことになる(マスター読み込みのあとのリピートスタートコンディションと、ストップコンディションは未実装だが)。 完成まで1ヶ月を要したが、GPIOだけによるI2Cスレーブインターフェースが完成した。ベータバージョンだけれど、とりあえずソースコードを公開することにする。回路図も掲載した。スレーブの861の空いているI/Oピンは、殆どがロジアナのプローブに使われている。
以下にスレーブ側のソース一式(プロジェクト名がSR04でなくHR04になっています。プロジェクト名を変える方法がわからないのでそのまま。ご了承ください)と、マスター側のプロジェクトをAtmelStudioのフォルダーの形でかためたものを置きます。ソースコードにはあえてロジアナのプローブステップを沢山残しています。ご参考まで
| 固定リンク
「AVR」カテゴリの記事
- ソフトI2Cはクロックストレッチまで手を出してあえなく沈没(2017.09.02)
- オシロのテストどころかソフト開発で大はまり(2017.07.26)
- 超音波方式の人感センサーI2C化と新しいオシロ(2017.06.29)
- motionの動体検知はRaspi3の電源が安定しない(2017.04.16)
- 赤外線学習リモコンはデータ再現で挫折したまま進まず(2016.07.21)
「電子工作」カテゴリの記事
- 生存証明2(2022.10.19)
- 生存証明(2022.01.23)
- パソコン連動テーブルタップの修理を諦めて自作(2021.02.16)
- 半年ぶりのブログ更新に漕ぎつけた(2019.09.19)
- 研究所活動は停滞したままでCCDカメラ顕微鏡導入など(2019.02.08)
コメント