PR
AI・プログラミング

ラズパイで気温・湿度・気圧を測る ― SHT30 + BMP180 で詰まった話と、その意外な真犯人

これまでラズパイとカメラを使って、ベランダに営巣するコシアカツバメの定点観測を続けてきました(→リアルタイム動体検知で進化した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への保存やグラフ化、さらに野鳥の行動データとの掛け合わせに広げていく予定です。

タイトルとURLをコピーしました