NetBSDでRaspberryPiのI2Cを使う

これはNetBSD Advent Calendar 2018の17日めの記事です。

はじめに

Raspberry PiのGPIOのうちGIO2,3はInter Integrated Circuit=I2Cの端子として利用できるようになっています。I2Cで接続できるデバイスはたくさんあり、RPIにセンサや出力デバイスを繋げるときのメジャーな手法の一つのようです。また1つのマスタデバイスと複数のスレーブデバイスを同時に接続することができるため、LCD等の出力デバイスと各種センサ等を同時に複数使いたい場合にも便利です。

ラズパイマガジン10月号付属基板(IoTCAP)では、LCDキャラクタディスプレイ AQM0802Aと気象センサBME280がこのI2Cで接続されていました。実はこの基板入手以前にブレッドボード上で組んでいたのとほとんど同じ構成です(LCDディスプレイが16桁のAQM1602だったという点だけ異なる)。

ということでこれをNetBSDで使ってみます。

対象ハードウェアについて

昨日と同じ環境ですが、今回のターゲットはLCDディスプレイ(AQM0802A)とします。

NetBSDのI2C

NetBSDではデフォルトでRaspberry PIのI2Cが使用可能になっており、kernelでは以下のように認識されています。

bsciic0 at obio0 intr 181: BSC0
iic0 at bsciic0: I2C bus
bsciic1 at obio0 intr 181: BSC1
iic1 at bsciic1: I2C bus

I2Cはiic(4)というデバイスで認識されています。iic0とiic1の2つがありますが、GPIOピン2,3に接続されているのはiic1の方です(RPIのリビジョンによって異なると聞いたことがありますが、RPI2ではおそらく1だけではないかと思います。これに対応するスペシャルファイルは/dev/iic1となります。

2018.12現在、current kernelでは認識されるデバイスの数が異なります。手元ではiic0からiic2の3つのデバイスが認識され、また、該当のデバイスはiic2となるようです。ただし手元では最近のcurrentカーネルでiicデバイスをアクセスすると無応答(kernel panic?)になることが多いみたいなのでちょっとおすすめできません(まだ調べてません)。

I2Cデバイスを検出する

I2Cには1つのバスに複数のスレーブデバイスが接続され、各デバイスは固有のアドレスが付けられています。 接続されたデバイス一覧を検出するには、NetBSD標準のi2cscan(8)コマンドで行います。Linuxのi2cdetectコマンドでの表示に相当します。とぃうことで実行してみましょう。

# i2cscan /dev/iic1
i2cscan: couldn't open /dev/iic1: No such file or directory

えっ? と思ったら確かに/dev/iic0も1もありません。なぜか/dev/MAKEDEV allでも作られないようです。仕方がないので手動で作成します。

# cd /dev
# ./MAKEDEV iic
# ls -l iic*
crw-------  1 root  wheel  201, 0 Dec 17 01:46 iic0
crw-------  1 root  wheel  201, 1 Dec 17 01:46 iic1
crw-------  1 root  wheel  201, 2 Dec 17 01:46 iic2
crw-------  1 root  wheel  201, 3 Dec 17 01:46 iic3

デバイスが4つ出来ました。ファイルモードはrootのみの読み書きとなっています。グループはwheelのままですね。
改めてi2cscanを実行してみましょう。

# i2cscan /dev/iic1
/dev/iic1: found device at 0x3e
/dev/iic1: found device at 0x76
/dev/iic1: 2 devices found

2つのデバイス、0x3eと0x76 が検出されました。秋月のデータシートには0x7Cと書かれていますが、0x3eで間違いないようです(アドレスの次のR/Wフラグ 1bitを足して書かれている感じ)。もう一つの0x76は気象センサBME280のアドレスです。

I2Cデバイスを使用する その1 とりあえずアクセスしたい(希望)

さてi2cデバイスのアドレスも正常に認識されたのでこのデバイスにアクセスしてみたいところです。

しかし、なんということか、デバイスにアクセスするためのコマンドはNetBSDには標準で用意されていません。i2cscan(8)のman pageにも、SEE ALSO項目に他のコマンドはありません。Linuxではi2cset, i2cget, i2cdump等が用意されているのに。

うーん、(まさかな)と思いながら/dev/iic1をhexdumpしてみます。

# hexdump -C /dev/iic1 | more
hexdump: /dev/iic1: Operation not supported by device

そもそもデバイスのスペシャルファイル直接読み書きはサポートされていないようです。うーん。

とはいえスペシャルファイルがあり、デバイス検出コマンドはあるので通常のユーザ空間からデバイスにアクセスする方法はあります。ただそのためにプログラム作成が必須になっているだけです。なかなかのストロングスタイルですがそんなことにめげていてはNetBSDを使っていられません。「いつものこと」です。むしろ検出コマンドが標準で用意されているだけ親切です。

I2Cデバイスを使用する その2 プログラムインターフェースを使う

ではプログラムから使ってみます。プログラムインターフェースは iic(4)のman page を見ると…何も書いてありませんね… gpio(4)にはIOCTL INTERFACEという項目があるのに。 iic(9)を見ると、iic_smbus_read_byte()やiic_smbus_write_byte()、iic_exec()というものがありますが、これらはあくまでもカーネル内ドライバのエントリポイントで、ドライバを作るときにこれらを実装する必要がある、ということだと思います。

とはいえ、こんなのもいつものことです。こんなことにめげていてはNetBSDを使(略

次にすべきことは「ググる」…のではなく、 i2cscan(8)コマンドのソース を見ます。i2cscan(8)は少なくともユーザ空間からiic(4)にアクセスし、デバイス検出をしているのだから間違いなくカーネルドライバにアクセスしているのですからその方法がわかるはずです。それ以上の情報が必要になったら iic(4)のソース およびRPIのドライバ実装である bsciicのソース を開いて'i2c'や'iic', 'ioctl' という文字列があるところを読み、ドライバ側が何を呼び出されるか、その時何を期待しているかを調べます(※あくまでも私個人のやり方です。情報が得られやすいかどうかの感覚には個人差があります)。

でソースは至ってシンプルです。ここからわかることは、こんな感じ。

ということで、dev/i2c/i2c_io.hを見てみると、なんとなく以下のようなことが分かります。

ということで、アプリケーション側の処理はなんとなく以下の用な感じになりそうです。

#include <sys/ioctl.h>
#include <dev/i2c/i2c_io.h>

int i2c_write(int fd, int addr, uint8_t *cmd, size_t cmdlen, uint8_t *buf, size_t buflen) {
	i2c_ioctl_exec_t i2cop;

	i2cop.iie_op = I2C_OP_WRITE_WITH_STOP; /* データの連続書き込み */
	i2cop.iie_addr = (i2c_addr_t)addr;     /* スレーブアドレス */
	i2cop.iie_cmd = cmd;                   /* コマンドポインタ */
	i2cop.iie_cmdlen = cmdlen;             /* コマンドの長さ */
	i2cop.iie_buf = buf;                   /* データバッファポインタ */
	i2cop.iie_buflen = buflen;             /* データ長 */
	return ioctl( fd, I2C_IOCTL_EXEC, &i2cop);  /* ioctl発行 */
}
  :
void func() {
	uint8_t *cmd; 	/* 1バイトコマンド*/
	uint8_t *buf = malloc(32);
	size_t buflen;
	int addr = ??  /* ターゲットアドレス */

	int fd=open("/dev/iic1",O_RDWR);

	/* *buf に書き込む内容を設定*/
	/* buflenに長さ */
	if (i2c_write( fd, addr, &cmd, cmdlen, &data, datalen)!=0){
		:

これはデータ書き込みの場合ですが、データ読み出しの場合はI2C_OP_READまたはI2C_OP_READ_WITH_STOPを使いioctl発行後に*bufの中身を参照すれば良さそうです。

キャラクタディスプレイ AQM0802Aにアクセスする

ターゲットデバイスAQM0802Aの仕様をデータシートで確認すると、初期化用のコマンドを数回送信し、その後に表示用のデータを送ればよさそうです。なお、このデバイスはデータの書き込みのみとなっており、マスターデバイス側から内容を読み出すことはできないようです。

表示に関する機能は表示位置の指定、一文字表示後の文字送り、表示箇所のプロンプトの表示など、キャラクタディスプレイ用の機能がなかなか揃っています。また内蔵キャラクタはいわゆるASCII文字だけではなく、カタカナ、ギリシャ文字、各種記号を含み1バイトコードの256文字分ぎっしりつまっており、5文字分はユーザ定義文字(CG RAM)もあるようです。

今回は任意の文字列を任意の場所に出力するという機能の実装を行ってみます。

初期設定を行う

初期設定シーケンスは、次のデータを上から順にデバイスに書き込みます。

No.cmddatawait
10x000x38>26.3us
20x000x39>26.3us
30x000x14>26.3us
40x000x70>26.3us
50x000x56>26.3us
60x000x6C>200ms
70x000x38>26.3us
80x000x0C>26.3us
90x000x01>1.08ms

waitはコマンドを送った後の待ち時間(通信クロック380KHz時)で、これよりも短い間隔になると正常な動作を期待できません。コマンド6の200msは特に長いと言え、これは明確に待ちを入れるべきでしょう。26.3マイクロ秒は結構微妙なところです。というのも、仮に1コマンド毎にioctlを発行すれば実行時間は十分に越えてしまいます。連続実行として1回で送った場合はドライバの処理と送信レート次第といったところでしょうか。ドライバの処理を見ると一応ネゴシエーションで待ち処理もあったりしてそのままでもいけそうな気もしないでもないところです。

ということでここでは1-6までと7-9までの2回に分けてioctlを発行することにします。

	int addr = 0x3e;
	uint8_t cmd = 0x00;
	uint8_t setup1[] = { 0x38, 0x39, 0x14, 0x70,0x56, 0x6c };
	uint8_t setup2[] = { 0x38, 0x0C, 0x01 };

	if (i2c_write(fd, addr, &cmd, 1, setup1, 6)==-1)
		return -1;
	usleep (200 * 1000); /* 200ms */
	if (i2c_write(fd, addr, &cmd, 1, setup2, 3)==-1)
		return -1;
	usleep (2 * 1000); /* 2ms */

表示する文字列を書き込む

文字列の表示は以下の手順になります。

表示位置は自動的にインクリメントされるので、連続した文字列を表示する場合は1度だけ指定すれば良いようです。

                   :
	uint8_t pos;
	char text1[]="NetBSD";
	char text2[]="evbarm";
                   :
	cmd = 0x00;
	pos = (uint8_t)0x81; 				/* 位置 (0x80 | (row << 6) | col) */
	if (i2c_write(fd, addr, &cmd, 1, &pos, 1)==-1)
		return -1;
	cmd = 0x40;     				/* データ設定コマンド*/
	if (i2c_write(fd, addr, &cmd, 1, text1, strlen(text1))==-1)
		return -1;
	cmd = 0x00;                                     /* 制御コマンド */
	pos = (uint8_t)(0x80 | (1<<6) | 2); 		/* 下行の3文字目から */
	if (i2c_write(fd, addr, &cmd, 1, &pos, 1)==-1)
		return -1;
	cmd = 0x40;     				/* データ設定コマンド */
	if (i2c_write(fd, addr, &cmd, 1, text2, strlen(text2))==-1)
		return -1;

こんな感じで作ったサンプルコードでI2C接続のLCDディスプレイに文字が表示できました。どうやらwaitもms単位で必要な部分以外は不要のようです。

iic(4)デバイスのアクセス権限

iic(4)はgpio(4)と異なり、特別な制御はされていません。デバイスのスペシャルファイルもroot:wheelオーナーのみの読み書きといささか使い難いものになっています。開発中はこんなでもいいでしょうが、実際にアプリケーションを動かす場合は最低でもgpio(4)と同じように、_iicのようなグループを作り、/dev/iic*のグループの設定とファイルモードを660にするのが良いのではと思います(MAKDEVで作り直すと変わってしまうので注意)。

まとめ

この基盤にはもう一つ、気象センサーのBME280がついています。こちらはセンサーということでほぼRead Onlyなデバイスですが、同じようにioctl(I2C_IOCTL_EXEC)のみでデータ取得が可能です。温度湿度気圧が計れてなかなおもしろいのですが、データのエンコードがかなり面倒だったりします。このデバイスも秋月電子等で買えるのでむしろLCDとこのセンサだけつなげ、適当な小箱にRPI Zeroとバッテリを入れて持ち運べるようにしてみたいと思ってます。山歩きのお供に。