i2c-gpio (ソフトウエアI2C)を割り当てたRaspberry Pi Compute Module 4(以下CM4)とBME280センサモジュールで気圧、温度、湿度を測定した際の作業メモです。LTC4331モジュール(マスタ、スレーブ用の2個セット)を使って、CM4のGPIOからケース外にLANケーブルで10m延長した先にBME280センサモジュールをI2C接続しています。
CM4はRaspberry Pi 4 Model B(以下4B)の産業・組み込み用途向けモデル。CM4のセットアップ方法は多少異なるものの、OSやGPIOは4Bと互換性があるので同じアプリケーションや各種センサモジュールが動作します。
Raspberry Pi OS(bullseye)ではソフトウエアupdate通知アイコンがタスクバーに表示されるので作業前に更新しておきます。
SoC内蔵の I2C(ハードウエア I2C) と i2c-gpio(ソフトウエアI2C)
Raspberry PIにはSoC内蔵の I2C(ハードウエア I2C) とi2c-gpio(ソフトウエアI2C)があります。i2c-gpioは、ソフトウェアでGPIOを操作 (Bit banging)してSDAとSCL相当のシリアル波形を作って通信を実現します。
GPIOピンの利用状況に応じて使い分けています。
SoC内蔵の I2C(ハードウエア I2C)を利用する場合の設定
苺メニューの「設定」–>「Raspberry Piの設定」を開いて–>「インターフェース」でI2Cを有効にチェックして、再起動するとSoC内蔵の I2C通信が使えるようになります。
なお、当サイトではSoC内蔵の I2C(ハードウエア I2C)は、SCLに割り当てられているGPIO3を電源の起動・シャットダウンに利用しているので使わず、無効化しています。代わって、i2c-gpio(ソフトウエアI2C)を利用しています。
i2c-gpio(ソフトウエアI2C)を利用する場合の設定
/boot/config.txtにdtoverlayで指定したピン( 例えば、GPIO 5 と GPIO 6 )が i2c-gpio (例えば、バス番号 を3に設定)になります。Raspberry Pi OSを再起動すると反映します。バス番号が変わるのでPythonなどでプログラム処理する際には注意します。
dtoverlay=i2c-gpio,bus=3,i2c_gpio_sda=5,i2c_gpio_scl=6,i2c_gpio_delay_us=2
SoC内蔵のI2C通信速度の default は100kbps。i2c-gpioドライバ の場合、i2c_gpio_delay_usを”2″ (default)と設定すると同等のI2C通信速度となります。
i2c-gpioについての詳細は、Raspberry Pi OSの /boot/overlays/README に記載があります。
SoC内蔵の I2Cはプルアップ抵抗が実装されていますが、i2c-gpioの場合は外付けのプルアップ抵抗が必要です。マスタ側、スレーブ側ともにLTC4331モジュール基板上のランドパターンをショートしてプルアップ抵抗(10KΩ)を有効化します。
なお、SoC内蔵の I2Cを無効化して、同じGPIO2 とGPIO3 に i2c-gpio を割り当てる設定とすれば、外付けのプルアップ抵抗は不要です。
# | 呼称 | SDA GPIO名称 (ピン番号) | SCL GPIO名称 (ピン番号) | バス 番号 | 備考 |
1 | SoC内蔵の I2C (ハードウエアI2C) | GPIO2 (3番ピン) | GPIO3 (5番ピン) | 1 | ・Raspberry PIの設定でI2C有効化 ・プルアップ抵抗を内蔵 |
2 | i2c-gpioドライバ (ソフトウエアI2C) | GPIO5 (29番ピン) | GPIO6 (30番ピン) | 3 | ・SDA、SCL、バス番号は任意 ・/boot/config.txtにdtoverlay追加 ・外付けのプルアップ抵抗が必要 |
参考資料:GPIOピン配列 | raspberrypi.org
結線図、I2Cバスをケース外に10m延長(2022/8/2追記)
SoC内蔵の I2C(ハードウエア I2C)とBME280センサモジュールを接続する場合は、GPIOのGPIO2(SDA)、GPIO3(SCL)、3V3、GNDの4本のワイヤ接続のみです。
今回は離れた場所でもI2C接続したセンサモジュールを安定動作させるために、LTC4331モジュール(マスタ、スレーブ用の 2個セット)を使ってI2Cバスと電源ラインをLANケーブルで10m延長しました。LTC4331モジュール間はハードウエア処理なので、I2C通信やPythonなどのソフトウエア処理には影響ありません。
また、SoC内蔵の I2C(ハードウエア I2C)のSCLに割り当てられているGPIO3を電源の起動・シャットダウンに利用しているのでi2c-gpio(ソフトウエアI2C)としてGPIO5、GPIO6を利用しています。
LANケーブルの先には、スレーブ設定したLTC4331モジュールを介して、I2Cデバイスに4線(SDL、SCL、3.3V、GND)を供給します。
ユニバーサル基板を使って、LTC4331モジュール裏面の端子とBME280センサモジュールを挿すためのピンソケットを結線しています。
BME280センサモジュールをつないでI2Cアドレス、バス番号を確認
スレーブ側のLTC4331モジュール端子から引き出したI2C(SDA、SCL)と3.3V、GNDをBME280センサモジュールに結線して動作確認します。
LXTerminalで下記コマンドを投入すると、i2c-3(バス番号3)にBME280のアドレス(0x76)が見えます。
ls /dev/i2c*
i2cdetect -l
i2cdetect -y 3
I2C(SDAとSCL)の信号波形をデジタルオシロスコープで観察
LTC4331モジュールのマスタ側(GPIOピン)と10m先のスレーブ側(BME280端子)のI2C(SDA、SCL)信号波形をデジタルオシロスコープでモニタしました。I2C信号レベルは3.3Vです。
I2C信号波形を比べるとスレーブ側はパルス幅が狭くなっていますが問題なくBME280センサで測定できています。
ストロベリー・リナックス社のサポートページにパルス幅についての記載がありました。
マスター側の信号と同じようにスレーブ側に反映されない。
マスター側の信号はLTC4331でSTART,STOP,R/W,0/1が解読され、他の信号とミックス・変調されてLANケーブルを伝わります(LANケーブルを通る信号は特殊な信号です) スレーブ側で復調、解読され、同じようにSTART,STOP,R/W,0/1の信号が再構成されます。そのためI2C信号のクロック周波数やパルス幅はスレーブ側に反映しません。LTC4331の内蔵クロックに同期、調整されたものになります。つまりマスターで100μsのLOWレベルで送信したものをそのままスレーブに100μsのLOW信号として反映されないことになります。I2CはSCL, SDAとの立ち上がり(立ち下り)順番、SDAの信号の有無で情報を伝達しますので、パルス幅は関係ありません。
http://strawberry-linux.com/support/14331/48952
Python3:BME280から測定データを取得 (2022/7/31修正)
Raspberry Pi OS with desktop(bullseye)にプリインストール されているThonny Python IDEを使って動作確認しました。
BME280から測定データを取得するPythonコードは、i2c-gpio(ソフトウエアI2C)に合わせてbus_numberを変更する必要があったので、 スイッチサイエンスのPython2用のサンプルコード(bme280_sample.py)をPython3用に修正して使わせていただきました。
bme280_sample.pyコードをThonny Python IDEに新規ファイルとしてローカルフォルダに保存します。
・6行目のバス番号をSoC内蔵のI2C(bus_number=1)からi2c-gpioで割り当てた(bus_number=3)に修正
・95行、103行、117行のprint文の両端を()で囲む(Python3で変更になった部分)
bus_number = 3
print ("pressure : %7.2f hPa" % (pressure/100) )
print ("temp : %6.2f ℃" % (temperature) )
print ("hum : %6.2f %" % (var_h) )
pipはPythonパッケージのインストールなどを行うユーティリティで、Raspberry Pi OS with desktop(bullseye)にはPythonとともにインストールされています。Pythonとpipのバージョンを確認しておきます。バージョンはPythonが3.9.2、pipが20.3.4でした。なお、Python3のみでありコマンドではpipとpip3を区別していていません。
python -V
pip -V
i2C通信を行うためにsmbusをインストールします。
sudo pip install smbus2
Thonny Python IDEで Python3用に修正したbme280_sample.pyを実行(Run)。 10m離れたベランダに設置したLTC4331モジュール(スレーブ側)のI2Cに接続したBME280センサから測定データを取得できることが確認できました。
Python3:測定日時を加えたCSV形式でテキストファイル保存 (2022/8/2追記)
データ形式を、測定日時とBME280測定データ(気温、気圧、湿度)をカンマセパレータ形式に変更しました。
BME280から測定データを取得するPythonコードはスイッチサイエンスのPython2用のサンプルコード(bme280_sample.py)を利用させていただき、日時を加えた気温、気圧、湿度をcsv形式で出力する変更を行っています。
LXTerminalからwhileコマンドを実行。上段が60秒間隔で連続測定してLXTerminalに表示、下段が出力先をテキストファイル(BME280_log.txt)に変更して記録します。CSV形式なのでEXCEL等でグラフ化するにも容易です。
#LXTerminalに表示
while true; do /usr/bin/python3 ./bme280_sample_csv.py; sleep 60s; done
#テキストファイル(BME280_log.txt)に記録
while true; do /usr/bin/python3 ./bme280_sample_csv.py; sleep 60s; done >> /home/pi/BME280_log.txt
スイッチサイエンスのPython2用のサンプルコード(bme280_sample.py)をCSV形式出力に修正した bme280_sample_csv.py です。背景がダークグレイの部分が修正・追加箇所です。この部分のオリジナルコードは#をつけています。
9行目の bus_number = 3 はi2c-gpio(ソフトウエアI2C)で割り当てたバス番号。SoC内蔵の I2C(ハードウエア I2C)を利用する場合は bus_number =1です。
bme280_sample_csv.py
※ここをクリックするとコード表示を開閉できます。
#bme280_sample_csv.py
#coding: utf-8
from smbus2 import SMBus
import time
import datetime
#bus_number = 1
bus_number = 3
i2c_address = 0x76
bus = SMBus(bus_number)
digT = []
digP = []
digH = []
t_fine = 0.0
def writeReg(reg_address, data):
bus.write_byte_data(i2c_address,reg_address,data)
def get_calib_param():
calib = []
for i in range (0x88,0x88+24):
calib.append(bus.read_byte_data(i2c_address,i))
calib.append(bus.read_byte_data(i2c_address,0xA1))
for i in range (0xE1,0xE1+7):
calib.append(bus.read_byte_data(i2c_address,i))
digT.append((calib[1] << 8) | calib[0])
digT.append((calib[3] << 8) | calib[2])
digT.append((calib[5] << 8) | calib[4])
digP.append((calib[7] << 8) | calib[6])
digP.append((calib[9] << 8) | calib[8])
digP.append((calib[11]<< 8) | calib[10])
digP.append((calib[13]<< 8) | calib[12])
digP.append((calib[15]<< 8) | calib[14])
digP.append((calib[17]<< 8) | calib[16])
digP.append((calib[19]<< 8) | calib[18])
digP.append((calib[21]<< 8) | calib[20])
digP.append((calib[23]<< 8) | calib[22])
digH.append( calib[24] )
digH.append((calib[26]<< 8) | calib[25])
digH.append( calib[27] )
digH.append((calib[28]<< 4) | (0x0F & calib[29]))
digH.append((calib[30]<< 4) | ((calib[29] >> 4) & 0x0F))
digH.append( calib[31] )
for i in range(1,2):
if digT[i] & 0x8000:
digT[i] = (-digT[i] ^ 0xFFFF) + 1
for i in range(1,8):
if digP[i] & 0x8000:
digP[i] = (-digP[i] ^ 0xFFFF) + 1
for i in range(0,6):
if digH[i] & 0x8000:
digH[i] = (-digH[i] ^ 0xFFFF) + 1
def readData():
data = []
for i in range (0xF7, 0xF7+8):
data.append(bus.read_byte_data(i2c_address,i))
pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
hum_raw = (data[6] << 8) | data[7]
#compensate_T(temp_raw)
#compensate_P(pres_raw)
#compensate_H(hum_raw)
t = compensate_T(temp_raw)
p = compensate_P(pres_raw)
h = compensate_H(hum_raw)
dt = time.strftime('%Y/%m/%d %H:%M:%S', time.localtime())
P_csv = dt + "," + t + "," + p + "," + h
print(P_csv)
def compensate_P(adc_P):
global t_fine
pressure = 0.0
v1 = (t_fine / 2.0) - 64000.0
v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * digP[5]
v2 = v2 + ((v1 * digP[4]) * 2.0)
v2 = (v2 / 4.0) + (digP[3] * 65536.0)
v1 = (((digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8) + ((digP[1] * v1) / 2.0)) / 262144
v1 = ((32768 + v1) * digP[0]) / 32768
if v1 == 0:
return 0
pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
if pressure < 0x80000000:
pressure = (pressure * 2.0) / v1
else:
pressure = (pressure / v1) * 2
v1 = (digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
v2 = ((pressure / 4.0) * digP[7]) / 8192.0
pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)
#print ("pressure : %7.2f hPa" % (pressure/100) )
return ("%7.2f" % (pressure/100))
def compensate_T(adc_T):
global t_fine
v1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1]
v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2]
t_fine = v1 + v2
temperature = t_fine / 5120.0
#print ("temp : %-6.2f ℃" % (temperature) )
return ("%6.2f" % (temperature))
def compensate_H(adc_H):
global t_fine
var_h = t_fine - 76800.0
if var_h != 0:
var_h = (adc_H - (digH[3] * 64.0 + digH[4]/16384.0 * var_h)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_h * (1.0 + digH[2] / 67108864.0 * var_h)))
else:
return 0
var_h = var_h * (1.0 - digH[0] * var_h / 524288.0)
if var_h > 100.0:
var_h = 100.0
elif var_h < 0.0:
var_h = 0.0
#print ("hum : %6.2f %" % (var_h) )
return ("%6.2f" % (var_h))
def setup():
osrs_t = 1 #Temperature oversampling x 1
osrs_p = 1 #Pressure oversampling x 1
osrs_h = 1 #Humidity oversampling x 1
mode = 3 #Normal mode
t_sb = 5 #Tstandby 1000ms
filter = 0 #Filter off
spi3w_en = 0 #3-wire SPI Disable
ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode
config_reg = (t_sb << 5) | (filter << 2) | spi3w_en
ctrl_hum_reg = osrs_h
writeReg(0xF2,ctrl_hum_reg)
writeReg(0xF4,ctrl_meas_reg)
writeReg(0xF5,config_reg)
setup()
get_calib_param()
if __name__ == '__main__':
try:
readData()
except KeyboardInterrupt:
pass