これまでラズパイとカメラを使って、ベランダに営巣するコシアカツバメの定点観測を続けてきました(→リアルタイム動体検知で進化したYouTube野鳥配信システム)。親鳥の訪問回数や滞在時間を記録するうちに、「気温や湿度、気圧といった環境データも一緒に記録すれば、鳥の行動パターンとの相関が見えてくるのでは?」と気になり始めました。
たとえば、気圧の急な低下は虫の飛翔高度に影響し、それが給餌行動に変化を起こす可能性があります。高温多湿の日は親鳥の訪問頻度が変わるのか。雨の前後で行動パターンに違いはあるのか。まだ仮説の段階ですが、まずはデータを取れる環境を作ろう、ということで、既存のラズパイにセンサーを追加してみました。
やりたかったのはシンプルで、
- SHT30(温度・湿度センサー、防水プローブ型) で気温と湿度
- BMP180(気圧センサー) で気圧
この2つを I2C で並列につないで、1台のラズパイから3つの値をまとめて取得する、というものです。
結論から言うと無事に動いたのですが、そこに至るまでに2つの罠にハマりました。1つは完全にハード的なオチ、もう1つは自分のプログラミングミスを「ラズパイの仕様」だと思い込んでしまった、というお恥ずかしい話です。同じところで悩む人がいそうなので、流れごと残しておきます。
構成と配線
センサーは両方とも I2C なので、4本の線(電源・GND・SDA・SCL)を分岐させて、同じバスに2つぶら下げるだけです。I2Cアドレスが違うので衝突しません。
| センサー | 役割 | I2Cアドレス |
|---|---|---|
| SHT30 | 温度・湿度 | 0x44 |
| BMP180 | 気圧(と温度) | 0x77 |
配線はこんな形です。電源は 3.3V で揃えます(5VだとBMP180が壊れる可能性があるため)。

気圧は屋内外でほぼ変わらない(広範囲で均一)ので、防水が面倒な気圧センサーは室内に置き、温湿度のSHT30だけをベランダに出す、という分担にしました。
接続はこんな感じです。途中から分岐させています。

罠その1 ― そもそも、はんだ付けされていなかった
配線して i2cdetect でデバイスを確認すると、SHT30(0x44)は見えるのに、BMP180(0x77)がまったく見えません。
$ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
40: -- -- -- -- 44 -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
電圧はテスターで来ていることを確認、SDA/SCLの導通もOK。なのに反応がない。「初期不良で死んでるのか…」と一度は諦めかけました。
ところがボードをよく見てみると ―― ピンヘッダがはんだ付けされておらず、グルーガンで固定されているだけでした。見た目はくっついているのに、電気的にはつながっていなかったわけです。以前、電子工作を始めたばかりの頃に組んだボードで、はんだ付けが面倒でグルーガンで固定していたようです。

はんだ付けのコツですが、ブレッドボードにピンヘッダを挿して固定すると作業しやすくなります。ただし、それだけだと基板が若干斜めになるので、ジャンパーワイヤを挟んで高さを揃えると、きれいに仕上がりました。

急いではんだ付けをやり直したら、あっさり 0x77 が出現。これは完全に物理の罠で、ソフトをいくらいじっても解決しない類のものでした。「見えないときはまず物理を疑う」を改めて学びました。

罠その2 ― 値が 0xFF で振り切れる
両方のセンサーが見えるようになり、いざ読み取ってみると、今度はSHT30の値がおかしい。
気温: 130.0°C 湿度: 100.0%
130℃に湿度100%。明らかに異常値です。生データを見ると、
raw: ['0xff', '0xff', '0xac', '0xff', '0xff', '0xff']
ほぼ全部 0xFF。これはセンサーから正しくデータが取れていないときの典型的な症状です。
ここで私は 「Raspberry Pi の I2C クロックストレッチ問題だ」 と早合点しました。これは実際に有名なバグで、調べると山ほど情報が出てきます。そして当時使っていたのは検証用の Raspberry Pi 4。
試しに pigpio というライブラリ(ソフトウェアでI2Cを制御する)に切り替えてみたところ、これが見事に動いた。
気温: 30.1 C 湿度: 53.2 %
「なるほど、クロックストレッチ対策が効いたんだな」と、このとき完全に納得してしまいました。これが後で大きなミスリードになります。
本番のラズパイ5で再び詰まる
検証がうまくいったので、本番環境の Raspberry Pi 5 に移そうとしたところ、いきなり問題発生。
Can't connect to pigpio at localhost(8888)
pigpio が Raspberry Pi 5 に対応していないのです。Pi 5はGPIO周りのチップ(RP1)が変わっており、pigpioのようなライブラリは軒並み動かなくなっています。
ここから迷走が始まります。「Pi 4で効いたクロックストレッチ対策を、Pi 5でどう再現するか」という方向で延々と悩みました。
試したこと:
- カーネルのソフトウェアI2C(
i2c-gpioオーバーレイ)を有効化 →0xFFのまま - ハードウェアI2Cを無効化して競合を排除 → 変わらず
- ビットバングの速度を落とす(
i2c_gpio_delay_us)を10、50、100と上げる → ぜんぶダメ
「Pi 5はクロックストレッチに対応しているはずなのに、なぜ?」「ソフトでできていたものが、なぜできなくなった?」と、頭の中は完全にクロックストレッチ一色。ハード的な問題か、ソフト的な問題か、と切り分けを進めても、まったく出口が見えませんでした。
真犯人は、不要な1バイトだった
行き詰まって、改めて「Pi 4でpigpioを使ったとき、具体的に何が違ったのか」をコードレベルで見比べました。そこでようやく気づきます。
問題は読み取り方でした。
最初のコードでは、SHT30の読み取りにこう書いていました。
data = bus.read_i2c_block_data(0x44, 0x00, 6)
この read_i2c_block_data は、名前のとおり「レジスタ 0x00 番地から読む」という関数です。つまり読み取り前に、余計なコマンドバイト 0x00 をセンサーに送ってしまいます。
ところが SHT30 は「レジスタ」という概念を持たないセンサーです。測定コマンドを送ったあとは、ただ素直に6バイト読むだけでいい。そこに余計な 0x00 を突っ込んでいたせいで、通信が崩れて 0xFF が返っていたのです。
一方、Pi 4で動いた pigpio の i2c_read_device は、コマンドバイトを送らない素のI2C読み取りでした。クロックストレッチ対策が効いていたのではなく、たまたま読み取りプロトコルが正しかっただけだったのです。
# NG: SHT30 does not have registers, so sending 0x00 corrupts communication
data = bus.read_i2c_block_data(0x44, 0x00, 6)
# OK: raw I2C read without sending a register byte
read = i2c_msg.read(0x44, 6)
bus.i2c_rdwr(read)
正しい書き方は、i2c_msg を使った素の読み取りです。
from smbus2 import SMBus, i2c_msg
bus.i2c_rdwr(i2c_msg.write(0x44, [0x2C, 0x06])) # 測定コマンド
time.sleep(0.5)
read = i2c_msg.read(0x44, 6) # 素の読み取り
bus.i2c_rdwr(read)
data = list(read)
これに直したら、Pi 5でも(余計なオーバーレイも遅延設定も一切なしで)あっさり正常値が出ました。
気温: 27.6 C 湿度: 61.0%
ちなみに BMP180 のほうは最初から問題が出にくかったのですが、これは逆にBMP180が「レジスタ」を持つセンサーだから。read_i2c_block_data のレジスタ指定読みが、BMP180には正しい作法だったわけです。同じI2Cでも、センサーによって正しい読み方が違う、という良い教訓でした。
教訓まとめ
整理すると、全体はこういう構図でした。
| 環境 | 読み取り方 | 結果 |
|---|---|---|
| Pi 4 / smbus2(従来) | 余計な 0x00 を送信 | 0xFF(失敗) |
| Pi 4 / pigpio | 素の読み取り | 成功 |
| Pi 5 / smbus2(従来) | 余計な 0x00 を送信 | 0xFF(失敗) |
| Pi 5 / smbus2 + i2c_msg | 素の読み取り | 成功 |
- 「見えないデバイス」はまず物理(はんだ・配線)を疑う。 グルーガン固定という伏兵もいる。
- 一度効いた対策の「効いた理由」を取り違えると、長く迷走する。 今回はpigpioで直った理由を「クロックストレッチ対策」と思い込んだのが致命傷でした。実際は読み取りプロトコルが正しくなっただけ。
- SHT30はレジスタを持たないので素の読み取り、BMP180はレジスタを持つのでレジスタ指定読み。 センサーごとに作法が違う。
- そして何より、最初から
i2c_msgで正しく読んでいれば、Pi 4でもPi 5でも、pigpioもオーバーレイも一切不要だった。 クロックストレッチ問題は、今回はそもそも無関係でした。
「有名なバグ」という知識があったせいで、かえって正解から遠ざかってしまう。思い込みの怖さを久しぶりに味わった一件でした。
動作した最終コード
最後に、3つの値(気温・湿度・気圧)をまとめて取得する全体のコードを載せておきます。標準のハードウェアI2C(bus 1)で動きます。
from smbus2 import SMBus, i2c_msg
import time
BUS = 1
bus = SMBus(BUS)
# ===== SHT30 (0x44) : 温度・湿度 =====
def read_sht30():
bus.i2c_rdwr(i2c_msg.write(0x44, [0x2C, 0x06]))
time.sleep(0.5)
read = i2c_msg.read(0x44, 6)
bus.i2c_rdwr(read)
data = list(read)
if data[0] == 0xFF and data[1] == 0xFF:
return None, None
temp = -45 + (175 * (data[0] << 8 | data[1]) / 65535.0)
humi = 100 * (data[3] << 8 | data[4]) / 65535.0
return temp, humi
# ===== BMP180 (0x77) : 気圧 =====
# キャリブレーションデータ読み込み(起動時に一度だけ)
raw = bus.read_i2c_block_data(0x77, 0xAA, 22)
def s(v): return v - 65536 if v > 32767 else v
n = ['AC1', 'AC2', 'AC3', 'AC4', 'AC5', 'AC6', 'B1', 'B2', 'MB', 'MC', 'MD']
sg = [1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1]
c = {}
for i in range(11):
v = raw[i * 2] << 8 | raw[i * 2 + 1]
c[n[i]] = s(v) if sg[i] else v
def read_bmp180():
bus.write_byte_data(0x77, 0xF4, 0x2E)
time.sleep(0.005)
rt = bus.read_i2c_block_data(0x77, 0xF6, 2)
UT = rt[0] << 8 | rt[1]
bus.write_byte_data(0x77, 0xF4, 0xF4) # oss=3(高精度)
time.sleep(0.026)
rp = bus.read_i2c_block_data(0x77, 0xF6, 3)
UP = ((rp[0] << 16) + (rp[1] << 8) + rp[2]) >> 5
X1 = (UT - c['AC6']) * c['AC5'] / 32768
X2 = c['MC'] * 2048 / (X1 + c['MD'])
B5 = X1 + X2
temp = (B5 + 8) / 16 / 10.0
B6 = B5 - 4000
X1 = (c['B2'] * (B6 * B6 / 4096)) / 2048
X2 = c['AC2'] * B6 / 2048
X3 = X1 + X2
B3 = (((c['AC1'] * 4 + int(X3)) << 3) + 2) / 4
X1 = c['AC3'] * B6 / 8192
X2 = (c['B1'] * (B6 * B6 / 4096)) / 65536
X3 = ((X1 + X2) + 2) / 4
B4 = c['AC4'] * (X3 + 32768) / 32768
B7 = (UP - B3) * 6250
p = (B7 * 2) / B4 if B7 < 0x80000000 else (B7 / B4) * 2
X1 = (p / 256) ** 2 * 3038 / 65536
X2 = -7357 * p / 65536
p = p + (X1 + X2 + 3791) / 16
return temp, p / 100 # hPa
# ===== メインループ =====
try:
print("気象観測開始 (Ctrl+Cで終了)\n")
while True:
temp, humi = read_sht30()
bmp_temp, pressure = read_bmp180()
ts = time.strftime('%Y-%m-%d %H:%M:%S')
if temp is not None:
print(f"{ts} 気温:{temp:.1f}°C 湿度:{humi:.1f}% 気圧:{pressure:.1f}hPa")
else:
print(f"{ts} SHT30エラー 気圧:{pressure:.1f}hPa")
time.sleep(5)
except KeyboardInterrupt:
print("\n終了")
finally:
bus.close()
実行するとこうなります。
気象観測開始 (Ctrl+Cで終了)
2026-06-03 14:13:53 気温:28.0°C 湿度:60.2% 気圧:998.2hPa
今後はCSVへの保存やグラフ化、さらに野鳥の行動データとの掛け合わせに広げていく予定です。


