普通の GPIO 経由で I2C できたらなんとなく嬉しいかなと思って作ってみた。つまりソフトウェアでピン状態をいじって I2C 通信するというもので、いわゆる bit banging というやつです。

[tech] Ruby の I2C ライブラリ | Sun, Feb 16. 2014 - 氾濫原 で I2C ライブラリを書いたけど、これは i2c-dev という Linux 組込みのドライバを使ったもので、ハードウェアが持ってる I2C の機能を使うので、もしハードウェアに I2C がない場合 (普通あるけど) で、GPIO はあるという場合に使えることがあるかもしれません。

使いかた

最終的には以下のような感じで使えるようになった。もちろん Raspberry Pi には I2C ついてるのでこんなことする必要は全くない。

require "i2c/device/mpl115a2"
require "i2c/driver/gpio"

mpl = MPL115A2.new(address: 0x60, driver: I2CDevice::Driver::GPIO.new(
    sda: 23, # pin 16 in raspberry pi
    scl: 24, # pin 18 in raspberry pi
))

p mpl.calculate_hPa

実装

コードは以下の通りだけど、けっこう時間がかかった割にはコードサイズはそうでもない。

普通に仕様通りに実装するだけだけど、いくつか問題があった

  • sysfs 経由のピン状態書きかえが非常に遅い
    • C + sysfs だと5KHz 程度 (Raspberry Pi 上)
    • Ruby + sysfs だと 1.2kHz 程度 (Raspberry Pi 上)
  • GPIO export 直後に EACCESS (パーミッションエラー) がでる
  • GPIO direction 直後での初期状態
遅い

I2C は Standard-mode でも 100kbit/s を規定してるので、全く届いていない。ただ、I2C はマスターが常に生成するクロックラインを持っていて、クロックが High のときだけデータが有効という感じで、大抵のデバイスはデータシート上では対応クロックを 0 〜 100kHz とか 0 〜 400kHz とかで書いてあるので、動くものは動くはず…… MPL115A2 というデバイスでは動いてくれた。

もちろんマスターとしてしか動かない。また、調停とかも実装してない。

ループ展開とかいろいろやったけど、多少改善はするものの、上記の通りのスピードは超えることができないので、諦めて可読性優先で富豪的に書いてある。

CPU 特化のネイティブなライブラリを使えば当然早くはなるんだけど、今回 sysfs 経由で抽象化された gpio でなければやる意味がないなと思ってやってない。

EACCESS

GPIO export 直後の EACCESS はよくわからなくて、pi を gpio グループに入れてやってるせいかもしれない。ただ、時間が経てばアクセスできるようになるので (たぶん sysfs がバグってる感じがするって人のせい的に書いといたら、カっとなって誰か調べてくれそう)、EACCESS を rescue して retry かけている。

direction 直後の状態

direction を単純に "out" に設定すると、たぶん active_low に従って初期状態が設定されると思うんだけど、うっかり SCL ラインを High にしてしまうとその後困ったことになるので、アトミックに direction 切り替え + ピンの High 又は Low 設定を行う必要がある。

しばらくわからなかったんだけど、普通に direction に対して "high" や "low" を書きこむと、初期状態が指定した状態の "out" になるみたいだった。

  1. トップ
  2. tech
  3. GPIO (sysfs) を使ったソフトウェア I2C

Locale::Maketext::Extract::Plugin 以下にはいろいろ対応してるフォーマットがあったりする。まぁ大抵一緒なので頑張って使う必要もないけど、これらを使って .po ファイルを作らないまでも、msgid の抽出だけ行いたいという場合があったりします。そんなときは直接 LME インスタンスを作って extract_file をかけて compile すれば、とれるようになるみたいです。

use Locale::Maketext::Extract;
use Locale::Maketext::Extract::Plugin::Xslate;
use File::Zglob;

my $lme = Locale::Maketext::Extract->new(
    plugins => {
        perl => [ 'pm' ],
        xslate  => {
            syntax     => 'TTerse',
            extensions => [qw/ tt /],
        },
        generic => [ 'js' ]
    },
    warnings => 1,
    verbose => 0,
);
 
 
for (zglob('lib/**/*.pm'), zglob('template/**/*.tt'), zglob('static/**/*.js')) {
    $lme->extract_file($_);
}

$lme->compile(1); # これをしないと msgids がとれない

for my $msgid ($lme->msgids) {
    say $msgid;
    $lme->msg_positions($msgid); # 見つかった場所がとれる
}
  1. トップ
  2. tech
  3. Locale::Maketext::Extract でスキャンだけする。
  1. トップ
  2. perl
  3. Locale::Maketext::Extract でスキャンだけする。

Ruby から Linux の i2c-dev 経由でデバイスを操作するライブラリを適当に書いた。

比較的汎用的な I2CDevice クラスと、サブクラスとして特定デバイス用のものをいくつかまとめた。汎用クラスはそんな重要ではなくて、特定デバイスの操作を纏めるのを主な目的とする感じ……

デバイスごとのドライバを必要になり次第書いておいていく感じにするつもりなので (そんなに増えないと思うけど)、rubygems としてはアップロードしてない。

GPIO でのインターフェイスを追加して、gem にあったほうが便利な気がしてきたので、gem push しました。https://rubygems.org/gems/i2c-devices

今のところあるのは

  • ACM1602NI
    • 秋月で売ってる I2C 16x02 液晶デバイス
  • AQM0802A
    • 秋月で売ってる I2C 8x2 液晶デバイス
  • HD44780
    • 液晶ドライバチップの HD44780 コンパチっぽいコマンドを I2C にインターフェイス変換した感じのものに使えるやつ
    • ACM1602NI, AQM0802A の親クラスとして実装してあるけど、実際は ACM1602NI 以外で動作確認はしていない
  • MPL115A2
    • 気圧計センサー
  • ADT7410
    • 高精度温度センサ (±0.5℃) データシートを元に実装しただけで、まだ動作確認してない
  1. トップ
  2. tech
  3. Ruby の I2C ライブラリ

CI とかで、あるディレクトリが毎回削除されて作りなおされ、そのディレクトリに package.json がある場合、毎回 npm install をすると大変非効率なので、別のディレクトリに node_modules ディレクトリを置いてそれを指定して使いたい。

しかし package.json に書かれた依存はそのディレクトリの node_modules ディレクトリにしか置けないようなので、以下のようにした。

  • 任意の場所に node_modules ディレクトリをつくる
  • npm install する前に、package.json があるディレクトリに ln -sf で、上記で作った node_modules ディレクトリへ symlink を貼る
  • npm install する

これで、インストール済みモジュールはそのまま使われるようになり、毎回 npm install で何もかもが install されなおされるということがなくなった。

  1. トップ
  2. tech
  3. node_modules を別の場所に置く