先日のFreeBSDワークショップでmrubyの話をしたところi2cも使えるようにしてみてはという意見があったので、ちょっといじってみました。
FreeBSDでi2cを使うにはハードウエアサポートがあるSOCなどでドライバを介してアクセスする方法と、GPIOの2本を使ってアクセスする方法があります。後者はBit Bangと言われています。
ハードウエアサポートがある場合に比べGPIO Bit BangはCPUを完全に占有して処理するので、効率はよくありません。 ハードウエアサポートがあってもビジーウエイトになるのでi2cのような低速のifは効率がよくありません。
Bit Bangの場合のカーネルのコンフィグレーションはgpioとともに下記を追加します。
device gpioiic
device iicbb
device iicbus
device iic
hintsは以下のように設定します。
hint.gpioiic.0.at="gpiobus0"
# GPIO pin 4,7
hint.gpioiic.0.pins=0x90
hint.gpioiic.0.sda=0
hint.gpioiic.0.scl=1
この設定ではpinsがビットフィールドになっていて、4ビットと7ビットがGPIOのピンであることを意味していて、小さい方(0)がSDAで大きい方(1)がSCLとなります。これにより4番ピンがSDAとなって、7番ピンがSCLになります。通常i2cはSDA/SCLともプルアップが必要です。
GPIOはLEDやプッシュボタンに接続されていて、仕様が不明なルータでどのピンが接続されているかを調べるにはまずgpioctl -cですべてをINにしてボタンを押しながらgpioctl -lで確認して数値が変わったところが、そのボタンのピンになります。LEDについては一つずつOUTにして値を変更してLEDが点灯するかどうかを確認します。
カーネル起動時のメッセージ
gpio0: <Atheros AR5315 GPIO driver> on apb0
gpio0: [GIANT-LOCKED]
gpio0: gpio pinmask=0x7fffff
gpiobus0: <GPIO bus> on gpio0
gpioiic0: <GPIO I2C bit-banging driver> at pins 4,7 on gpiobus0
gpioiic0: SCL pin: 7, SDA pin: 4
iicbb0: <I2C bit-banging driver> on gpioiic0
iicbus0: <Philips I2C bus> on iicbb0 master-only
iic0: <I2C generic I/O> on iicbus0
gpioc0: <GPIO controller> on gpio0
ログインして以下のように確認できます。
# ls -las /dev/iic0
0 crw------- 1 root wheel 0x21 Jan 1 00:00 /dev/iic0
# i2c -s
Scanning I2C devices on /dev/iic0: 50
# i2c -a 0x50 -d r -o 2
20
これでi2cが使えるようになります。上の例では手元にころがっていたちょっと古いEPSONのリアルタイムクロックRTC-8583を接続しています。i2cコマンドでスキャンすると0x50にチップがある事がわかります。RTC-8583の3バイト目はBCDフォーマットの秒レジスタです。何回かアクセスすると毎秒インクリメントされている事が確認できます。
ハードウエア絡みのハックの最初の一歩は、データシートを確認し、正しく値が拾えるようにする事です。
mrubyからもデータを読めるようにモジュールを作ってみました。
よくある1バイトのレジスタのアドレスとデータをアクセスできるようにしてあります。読み込みはアドレスを書き込むと1バイトのデータが読め、書き込みはアドレスとデータを書き込みます。これ以外の仕様には対応していません。この1バイトのアドレスとデータはi2cの仕様ではなくて、一般の実装になります。2バイトのアドレス空間や特集な形式もあります。
ioctlのslaveの値がaddressを1ビットシフトした値でないといけないことを知らなくて、ちょっとはまりました。
SB0802を使った小型液晶表示スクリプト(こちらを参考にしました)
CONT = 32
def lcd_cmd(i, x)
i.write(0x3e, 0, x)
end
def lcd_data(i, x)
i.write(0x3e, 0x40, x)
end
def lcd_init(i)
lcd_cmd(i, 0x38)
lcd_cmd(i, 0x39)
lcd_cmd(i, 0x14)
lcd_cmd(i, 0x70 | (CONT & 0xf))
lcd_cmd(i, 0x5c | ((CONT >> 4) & 0x3))
lcd_cmd(i, 0x6c)
usleep(100)
lcd_cmd(i, 0x38)
lcd_cmd(i, 0x0c)
lcd_cmd(i, 0x01)
usleep(100)
end
def lcd_puts(i, str)
bin = str.bytes
bin.each{|var|
lcd_data(i, var)
}
end
def lcd_move(i, pos)
lcd_cmd(i, 0x80 | pos)
end
str1 = "FreeBSD"
str2 = " & mruby"
t = BsdIic.new(1)
lcd_init(t)
lcd_puts(t, str1)
lcd_move(t, 0x40)
lcd_puts(t, str2)
後日追記:手元に初代ラズパイがないので確認できてないのですがおそらく初代ラズパイのi2cドライバでは使えないと思われます。(2017/10/20:対応しました)これはi2cのドライバは古いインターフェース(I2CSTARTなど)のものと新しいインターフェース(I2CRDWR)のものがありgpioiic(iicbb)は両方対応しているのですが、ラズパイのドライバは新しい方にしか対応していないためです。参考 (2017/05/23)
# mirb
mirb - Embeddable Interactive Ruby Shell
> t = BsdIic.new(0)
=> #<BsdIic:0x40ee4020>
> p t.read(0x50,2)
51
=> 51
読み込みの時の信号線は2バイト送信と1バイトの送信と受信なのでこんな感じです。
書き込みの時は3バイトの送信なんで、こんな感じになってます。
これを書いている時点ではreadのみのサポートの手抜き状態ですが、ぼちぼち整備していきたいとおもいます。
後日追記
FreeBSDのGPIO Bit BangでRTC-8583は問題なく動作していましたが、他のI2Cデバイスを試したところ認識に失敗するケースがありました。あまり安定していないようなので、試す時は注意が必要です。
AR2315で試したところI2Cのクロックが400Kのデバイスはほぼ全滅でした。100KなRTC-8583やインターネット温度計で使ったArduinoのI2C Slaveライブラリは安定して使えています。
I2CのSlaveを自作するのはPICなどでもできますが、PICは開発環境が重かったりコンパイラーに制限があったりで、ちょっと面倒です。mbedのI2CSlaveライブラリを試したらアドレス送信にはACKが返るのですが、データの送信にACKが返らず使えませんでした。ArduinoのI2C Slaveライブラリが一番簡単に試せるようです。
インターネット温度計を作るためにmruby-bsdiicにwriteメソッドも追加しました。
ラズバイで動かない件調べてみました。sys/armには2017/10現在以下のようなi2cホストのコードがあります。
ファイル | iicbus_transfer | iicbus_start |
---|---|---|
allwinner/aw_rsb.c | ○ | × |
at91/at91_twi.c | ○ | × |
nvidia/tegra_i2c.c | ○ | × |
broadcom/bcm2835/bcm2835_bsc.c | ○ | × |
freescale/imx/imx_i2c.c | ○ | ○ |
freescale/vybrid/vf_i2c.c | ○ | ○ |
samsung/exynos/exynos5_i2c.c | ○ | ○ |
dev/iicbus/iicbb.c | ○ | ○ |
ドライバ側でiicbus_start(I2CSTART)をすべてサポートするか、利用している方でiicbus_transfer(I2CRDWR)に移行するかのどちらかの必要があるようです。
I2CSTARTは変なところあって、アドレスは渡しているのですが、その後読み書きどちらかは分からないのに、一バイト送ってデバイスがある場合はackが帰ってくる事を確認しています。なぜこのような実装になってしまったのでしょうか?