『ルーターにつなぐ』I2C温度センサ編

本記事はルーターハックAdvent Calendar 13日目の記事です。12日枠の記事は srchack さんの「LinkIt 7688のビルド手順。(Chaos Calmer 15.05.1)」です。バリバリ遅れてるところに助け舟を出していただいてほんとありがとうございます。

www.srchack.org

今回は、さまざまなペリフェラルを実装しているルーターのために、元々なかったパーツをつないで遊んでしまおうという企画『ルーターにつなぐ』の第2弾だ。

おことわり

技適 に関する筆者の配慮や考えについてはカレンダー1日目「技適とルーターハック」をご覧ください。本記事で紹介する内容は、法を遵守するための慎重な注意をもって書かれています。

www.zopfco.de

I2C とは

アイスクエアドシーとか、アイツーシーとか読まれるシリアルバスの一種。クロックが低速(100kHz〜400kHzが普通)でこれを搭載しないマイコンはないというほどポピュラー。

I2C をしゃべることのできるセンサーを Raspberry Pi につないだことがあるという方も多いだろう。

バスなので1つの信号線に複数のデバイスが接続されるが、スレーブの区別は SPI とは異なっていて、データ線を流れる信号にスレーブアドレスも織り込まれる形式をとっている。これにより、データ (SDA) と クロック (SCL) の2本のみという非常に少ない信号線で通信ができるようになっている。

ルーターで I2C を使うには

今回は、やはり前回に引き続き 多摩電子工業 (Axing) W06 を題材に使うことにする。

ルーターで I2C を使うには、Device Tree の i2c ノードを有効化して Linux kernel に認識させることになる。

ファーム準備

ここから、

  • I2C 関連ツール用意
  • Device Tree の変更
  • I2C の信号が出ている回路を探す

の3つのステップを経て I2C を使えるようにする。

I2C 関連ツール用意

ファームの config に以下追加する。

まず、i2cdetect などが入っているi2c-tools。

  • Utilities -> i2c-tools

カーネルで I2C を扱えるようにするモジュール類。

  • Kernel Modules -> I2C support -> kmod-i2c-core
  • Kernel Modules -> I2C support -> kmod-i2c-mt7628

watch コマンドがあった方が便利だ。

  • Basy system -> busybox -> Process Utilities -> watch

Device Tree の変更

W06 の Device Tree は、 openwrt/target/linux/ramips/dts/W06.dts に入っている。この中で mt7628an.dtsi という SoC 共通の dtsi (include用の dts は拡張子として dtsi がつく) を include している。

mt7628an.dtsi は汎用なものなので、ルーターで使わない I2C のノードも記載されているものの標準で disabled になっている。include する側、つまり W06 の dts でこれを有効化する。

W06.dts を開き、I2C のノードを okay にするように以下追記する。

diff --git a/target/linux/ramips/dts/W06.dts b/target/linux/ramips/dts/W06.dts
index 8c3bbe4058..cd70ebbb2f 100644
--- a/target/linux/ramips/dts/W06.dts
+++ b/target/linux/ramips/dts/W06.dts
@@ -66,6 +66,10 @@
        };
 };

+&i2c {
+    status = "okay";
+};
+
 &spi0 {
        status = "okay";

I2C の信号が出ている回路を探す

探りを入れる前に、ひとまず i2cdetect してみよう。このコマンドを使うと、I2Cの各スレーブアドレスに対して応答を要請することができる(しくみは後述)。応答のあったアドレスはそのアドレス自体が表示され、応答がないと -- になる。

root@OpenWrt:~# i2cdetect -y 0
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: 30 31 32 33 34 35 36 37 -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

What? 何も繋がってないはずなのに亡霊がいる。オシロで探りを入れるしかないな。 watch -n 0.5 i2cdetect -y 0 でずっと信号が出るようにしよう。

Every 1s: i2cdetect -y 0                                                                                                                                                   2018-12-09 13:45:51

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: 30 31 32 33 34 35 36 37 -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

データシート曰く、I2C は21ピンが SDA、20ピンが SCL らしい。近くに先日いじった SPI のピンも見える。

f:id:puhitaku:20181218005741p:plain

実際の写真はこれ。該当するピンと、その配線の先にあるチップ抵抗のランドをピンクで示した。

f:id:puhitaku:20181218005608p:plain

よく見ると…どちらのピンも、共通のランドにチップ抵抗で接続されている。もしや… I2Cのために最初からPull-upしてくれてるのか!!?*1

21ピンの SDA からのびた先のチップ抵抗に試しにプローブを当ててみる。

f:id:puhitaku:20181218004447j:plain

f:id:puhitaku:20181218004823p:plain
SDAぽい。

うんうん、それっぽい!

なんと、ご丁寧にも I2C の配線は最初からPull-upされていた。なんて親切なルーターなんだ(感激)

あっさり I2C の配線が特定できたが、本来出ない亡霊が i2cdetect で出てきているのが気になる。配線をこのランドから伸ばし、ロジアナに接続してみる。

信号をより詳細に計測する

ロジアナで詳しく調べる前に、I2C の probe のしくみを解説しよう。

I2C では、スレーブアドレス および レジスタアドレス を使ってアドレッシングする。

  • スレーブアドレス = バスに繋がれたデバイスを特定する値
  • レジスタアドレス = デバイス内のレジスタを特定する値

マスターが何らかの値を送るときには、まずスレーブアドレスを流し、そして書き込みたいレジスタアドレスを流し、最後に入れたいデータを流すことになる。これをビット単位で表したのが以下の図である。

f:id:puhitaku:20181222155653p:plain
出典: TI - Understanding the I2C Bus http://www.ti.com/lit/an/slva704/slva704.pdf

示されている各ビットは、以下のようなタイミングで表現され、伝送される。クロックとデータのエッジが重なる SPI などとは異なり、クロック (SCL) が High である時間をいわば前後に「包み込む」ようにしてデータ (SDA) のエッジが描かれるのが I2C の特徴である。

f:id:puhitaku:20181222160041p:plain
出典: 同上

ここで、上記2つの図で ACK と表現されているビットが、スレーブ側と正しくコミュニケーションできているかどうかを示す。このビットの区間だけは、マスターは SDA をコントロールしていない。つまり、スレーブ側が意図的に Low に落とせば「スレーブは指示を理解した」とマスター側に伝えられることになる。

よって、ACK に相当する区間が Low であれば ACK (OK)、High であれば NACK (NG) となる。

f:id:puhitaku:20181222161333p:plain
出典: 同上 ACKビットが High のため、スレーブに指示が伝わっていない = NACK となる。

それでは、本来居ないはずのアドレス 0x30 を実際に i2cdetect した時のスクリーンショットを見てみよう。Digilent - Analog Discovery 2 に接続し、Scope(オシロ) と Logic (ロジアナ)の両方で観察したのが以下のスクリーンショット。上が Scope、下が Logic。

f:id:puhitaku:20181222154844p:plain

まず、SDA が Low に落ちて START condition になる。次に、アドレス 0x30 を Read したいと伝える。さらにその次が件の ACK になるのだが、High のままになっているため NACK (NCK) を示している。

電気回路レベルでは特に問題がないのだが、やはり i2cdetect は 0x30 を検知したように表示している。

Every 1s: i2cdetect -y 0 0x30 0x30                                                                                                                                         2018-12-14 03:56:11

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                                                 
10:                                                 
20:                                                 
30: 30                                              
40:                                                 
50:                                                 
60:                                                 
70:

回路構成はどうだろうか?リファレンスとなる LinkIt Smart 7688 の Schematic から I2C 部分を抜粋したのが以下の画像だ。

f:id:puhitaku:20181222163025p:plain
出典: LinkIt Smart 7688 Resources - Documentation https://docs.labs.mediatek.com/resource/linkit-smart-7688/en/documentation

Pull-up抵抗は 4.7 kΩ となっている。W06 の表面実装抵抗にテスターを当てたところ、やはり 4.7 kΩ を示したため、これも問題ないようだ。う〜んわかんなくなってきたぞw

ドライバにパッチを当てる

電気的に問題が無さそうなのに i2cdetect が false-positive を示すということは、SPI の時と同じく物理層以上ソフトウェア以下で問題がありそうである。早速 OpenWrt の Issue/PR を検索すると…

あった!

github.com

クロックの扱いや ACK のハンドリングがダメだったらしい。この PR で出ているパッチは Upstream (Mainline) に出すとだけ書かれて閉じられているが、Fork 先を git add remote からの fetch して cherry-pick すればよい。

パッチを当てると静かになった。行けそう。

root@OpenWrt:~# i2cdetect -y 0
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

温度センサを繋げてみる

I2C の亡霊問題は大丈夫っぽいので実際に何か繋ごう。今回は秋月で雑に買った 高精度温度センサ STTS751 を接続する。

akizukidenshi.com

ただ、あまりにも無計画に買ったのでDIP化基板を買い忘れてしまった。カプトンで固定して配線。そしてブレッドボードへ。

f:id:puhitaku:20181225221318j:plain
ルーペで撮った。

f:id:puhitaku:20181225223245p:plain
データシートより

左の点がある足から、反時計回りに1〜6ピンがのびる。4ピンはデータシート曰く may not float らしいがなくてもそれっぽく動くので今回はよしとした。

1ピンを Pull-up する抵抗の値によってアドレスが決まる。テストのため 10k + 10k = 20kΩ を接続したところ、データシートどおり 0x38 で認識された。

root@OpenWrt:~# i2cdetect -y 0
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- 38 -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

計測してみる

温度レジスタの構成は以下のようになっている。0x00 の値を読むだけでも1℃単位の温度が得られるので、今回は簡易的に 0x00 の値だけ読むことにする。

f:id:puhitaku:20181225223408p:plain
データシートより

i2cdetect の兄弟に i2cget があるのでそれで読んでみる。

root@OpenWrt:~# i2cget -y 0 0x38 0
0x13

19℃。部屋にある別の温度計曰く19.6℃なので正しそうだ。指で温めると温度が上がり、離すと下がることも確認できた。

やったぜ!!!!!!!

次回

次回というか、Advent Calendarではない更新でこれからもこういったネタを投稿していけたらと思う。今回は以上!

*1:I2Cは上りも下りも単一の信号線で通信するため、ホストとスレーブどちらでも信号線を操作できるようにPull-upされている。このような、信号線上の誰かがLowを出力すると全体がLowになるような回路構成をワイヤードORという。