はじめに
NSB-3NR1T1MLVのチャームポイントの一つである、前面のOLED画面を自前で制御してみました。
この製品のOLEDの通信インタフェースはI2Cですが、マイコン側はGPIOで接続されているようです。
本記事では通信専用のハードウェアは使わずにGPIOで信号生成するソフトを作ります。他でも応用が利くかもしれません。
I2C
詳しい説明は他にゆずるとして、OLEDを制御する最小限だけ書きます。
この製品では、マイコンがマスタ、OLEDがスレーブとなって通信をします。I2Cの信号線はSCL・SDAの2本です。
1回の通信はこんな感じで行われます。
- スタートコンディション送信 (マスタ→スレーブ)
- SCLが1のときにSDAを1→0に変化させると通信開始の合図になります。
- スレーブアドレス+R/W送信 (マスタ→スレーブ)
- 通信相手となるスレーブアドレス(7ビット)と、送受信の方向(送信=0、受信=1)を指定します。各ビットの値は、SCLが0→1に変化した瞬間のSDAの値です。スレーブアドレスは上位ビットから順に送信します。
- ACK (受信側の応答)
- 通信がうまくいっていれば、受信側はSDAに0を出力します。
- データ (送信時はマスタ→スレーブ、受信時はスレーブ→マスタ)
- 8ビットのデータとACKの繰り返しです。通信内容はスレーブの仕様によります。
- ストップコンディション (マスタ→スレーブ)
- SCLが1のときにSDAを0→1に変化させると通信終了の合図になります。
ソフトI2Cの実装
I2C通信用のハードウェアを利用することが多いですが、諸般の事情でハードウェアが使えないことがあります。その場合は、GPIOでSCL・SDAを制御する方法がとられます。ここではソフトI2Cとよびます。
今回はデータを一方的に送りつけるだけのマスタを実装します。
通信タイミング生成
1ビット出力する処理は以下のようになります。
SCL = 0;
wait();
SDA = (出力する値);
wait();
SCL = 1;
wait();
信号を操作するごとにwaitが必要なのは、I2Cは信号の立ち上がり・立ち下がりのタイミングを重視した通信方法だからです。SCLとSDAの変化がほぼ同時だと、ビットが正しく伝わらなかったり、スタートコンディションなどと間違われたりして、失敗の原因となります。
ここでwait()の実装としてusleep()などを使いたくなりますが、思ったよりも長い待ち時間となってしまいます。実機で測定したところ、usleep(1)が60マイクロ秒ぐらいでした。
usleep() 関数は (少なくとも) usecマイクロ秒の間、 呼び出し元スレッドの実行を延期する。
ということで仕様通りの挙動なのですが、これだとI2C通信には遅すぎます。
こんなときの奥の手としてビジーループを使うことにしました。通常のアプリ開発では御法度ですが、動かすハードウェアが決まっていてごく短時間のウェイト生成であることから、ここでは例外的にビジーループを使ってもよいかなと自分の中では思っています。
受信処理
今回はマスタが一方的に送信するだけで十分なので、受信処理は実装しませんでした。スレーブから送られてくるACKも無視します。
GPIO
/sys/class/gpio
経由で制御します。
GPIOとして使用する端子をexportする
/sys/class/gpio/export
に端子番号を書き込みます。
例えばGPIO64を使う場合。
# cat 64 > /sys/class/gpio/export
これで、 /sys/class/gpio/gpio64
が現れます。
GPIO端子を使用終了したら /sys/class/gpio/unexport
に端子番号を書き込んで後片付けができます。
本製品に限ったバグなのかもしれませんが、 export済のGPIO端子をさらにexportするとOSが暴走します のでご注意を。一旦unexportした端子をもう一度exportするのはOKです。
入出力の方向を設定する
/sys/class/gpio/gpio**/direction
に in または out と書き込みます。
# echo out > /sys/class/gpio/gpio64/direction
出力値を変更する
/sys/class/gpio/gpio**/value
に 0 または 1 と書き込みます。
# echo 1 > /sys/class/gpio/gpio64/value
ということで、ファイルの読み書きだけでGPIO制御ができます。
C言語での実装例。エラー処理は省いてます。
// GPIO64を出力端子として使用する
system("echo 64 > /sys/class/gpio/export");
system("echo out > /sys/class/gpio/gpio64/direction");
// 出力値を更新
fd = open("/sys/class/gpio/gpio64/value", O_RDWR|O_SYNC);
write(fd, "0\n", 2);
write(fd, "1\n", 2);
close(fd);
OLEDの制御
まったくもって当てずっぽうですが、秋月で売られている有機ELキャラクタディスプレイモジュール SO1602 と同じコマンドが使えそうです。
OLEDとつながっているGPIOはI2C通信以外に電源制御もあるようで、こちらも想像ですがこんな感じでしょうか。
端子 | 機能 |
---|---|
GPIO64 | Vcc (表示用電源) |
GPIO65 | Vdd (制御用電源) |
GPIO66 | SCL |
GPIO67 | SDA |
動作例
このOLEDにはASCII文字以外にも半角カタカナや記号も表示可能で、表示領域が1行余ったので実際に表示させてみました。
ソースコードはこちら。残骸が入っていますが気にせず。
https://github.com/ac100v/nsb-3nrv_examples/tree/master/lcd
適当なARM用のコンパイラでビルドできると思います。本製品のファームウェアではglibcではなくuClibcが使われていますが、スタティックリンクにしておけば面倒は回避できます。
$ arm-linux-gnueabi-gcc -static lcd.c
まとめ
- NSB-3NR1T1MLVの前面OLED画面はI2C制御です。
- GPIOでI2Cマスタ通信をソフト的に生成することで、OLEDを制御することができました。
- コマンド体系は SO1602 と同じようです。