i2c-gpio (ソフトウエアI2C)を割り当てたRaspbery Pi CM4とBME280センサで気圧、温度、湿度の測定、CSV形式で保存

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通知アイコンがタスクバーに表示されるので作業前に更新しておきます。

BME280センサモジュールとスレーブ設定したLTC4331モジュール
BME280センサモジュールとスレーブ設定したLTC4331モジュール
目次

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を有効化
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 に記載があります。

/boot/overlays/README掲載のi2c-gpioより引用
/boot/overlays/README掲載のi2c-gpioより引用

SoC内蔵の I2Cはプルアップ抵抗が実装されていますが、i2c-gpioの場合は外付けのプルアップ抵抗が必要です。マスタ側、スレーブ側ともにLTC4331モジュール基板上のランドパターンをショートしてプルアップ抵抗(10KΩ)を有効化します。

呼称SDA
GPIO名称
(ピン番号)
SCL
GPIO名称
(ピン番号)
バス
番号
備考
1SoC内蔵の I2C
(ハードウエアI2C)
GPIO2
(3番ピン)
GPIO3
(5番ピン)
1・Raspberry PIの設定でI2C有効化
・プルアップ抵抗を内蔵
2i2c-gpioドライバ
(ソフトウエアI2C)
GPIO5
(29番ピン)
GPIO6
(30番ピン)
3・SDA、SCL、バス番号は任意
・/boot/config.txtにdtoverlay追加
・外付けのプルアップ抵抗が必要

なお、SoC内蔵の I2Cを無効化して、同じGPIO2 とGPIO3 に i2c-gpio を割り当てる設定とすれば、外付けのプルアップ抵抗は不要です。

参考資料:GPIOピン配列 | raspberrypi.org

結線図、I2Cバスをケース外に10m延長(2022/8/2追記)

SoC内蔵の I2C(ハードウエア I2C)とBME280センサモジュールを接続する場合は、GPIOのGPIO2(SDA)、GPIO3(SCL)、3V3、GNDの4本のワイヤ接続のみです。

SoC内蔵の I2CとBME280センサの結線
SoC内蔵の I2CとBME280センサの結線

今回は離れた場所でもI2C接続したセンサモジュールを安定動作させるために、LTC4331モジュール(マスタ、スレーブ用の 2個セット)を使ってI2Cバスと電源ラインをLANケーブルで10m延長しました。LTC4331モジュール間はハードウエア処理なので、I2C通信やPythonなどのソフトウエア処理には影響ありません。

また、SoC内蔵の I2C(ハードウエア I2C)のSCLに割り当てられているGPIO3を電源の起動・シャットダウンに利用しているのでi2c-gpio(ソフトウエアI2C)としてGPIO5、GPIO6を利用しています。

i2c-gpio(ソフトウエアI2C)からLTC4331モジュールを介してBME280センサに結線
i2c-gpio(ソフトウエアI2C)からLTC4331モジュールを介してBME280センサに結線

LANケーブルの先には、スレーブ設定したLTC4331モジュールを介して、I2Cデバイスに4線(SDL、SCL、3.3V、GND)を供給します。

ユニバーサル基板を使って、LTC4331モジュール裏面の端子とBME280センサモジュールを挿すためのピンソケットを結線しています。

BME280センサモジュールとスレーブ設定したLTC4331モジュール
ユニバーサル基板上の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バスにつながったデバイスの確認
I2Cバスにつながったデバイスの確認

I2C(SDAとSCL)の信号波形をデジタルオシロスコープで観察

LTC4331モジュールのマスタ側(GPIOピン)と10m先のスレーブ側(BME280端子)のI2C(SDA、SCL)信号波形をデジタルオシロスコープでモニタしました。I2C信号レベルは3.3Vです。
I2C信号波形を比べるとスレーブ側はパルス幅が狭くなっていますが問題なくBME280センサで測定できています。

ストロベリー・リナックス社のサポートページにパルス幅についての記載がありました。『マスター側の信号はLTC4331で他の信号とミックス、変調されてLANケーブルを伝わり、スレーブ側で復調されます。そのためI2C信号のパルス幅はスレーブ側に反映しません。LTC4331の内蔵クロックに同期、調整されたものになります・・(略)・・I2CはSCL, SDAとの立ち上がり(立ち下り)順番、SDAの信号の有無で情報を伝達しますので、パルス幅は関係ありません。』

LTC4331(マスタ側)の波形
LTC4331(マスタ側)の波形 。上段がSCL、下段がSDA
LTC4331(スレーブ側)の波形
LTC4331(スレーブ側)の波形 。上段がSCL、下段がSDA

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センサから測定データを取得できることが確認できました。

Python、pipのバージョン確認、smbusをインストール
Python、pipのバージョン確認、smbusをインストール
Python3用に修正したbme280_sample.py
Python3用に修正したbme280_sample.py をThonny Python IDEで実行(Run)

Python3:測定日時を加えたCSV形式でテキストファイル保存 (2022/8/2追記)

データ形式を、測定日時とBME280測定データ(気温、気圧、湿度)をカンマセパレータ形式に変更しました。

BME280から測定データを取得するPythonコードはスイッチサイエンスのPython2用のサンプルコードbme280_sample.py)を利用させていただき、日時を加えた気温、気圧、湿度をcsv形式で出力する変更を行っています。

csv形式出力の bme280_sample_csv.py を実行
csv形式出力の bme280_sample_csv.py を実行

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
LXTerminalに連続表示
LXTerminalに連続表示
テキストファイル(BME280_log.txt)に連続して記録
テキストファイル(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
よかったらシェアしてね!
  • URLをコピーしました!
目次