はじめに
- Raspberry Pi5でGPIOを扱いたかった
- もくもく会のネタで色々と触ってた
- gpiozeroで色々と触ってみた
実行環境
- RaspberryPi 5 8GB
- RaspberryPi OS 12(Bookworm) x64
- CLIしか使わなかったけどwith desktopの方
おことわり
基本的にはClaude Sonnet 4にコードを書いてもらい、一部微修正するスタイルと取っています。
gpiozeroについて
GitHub: https://github.com/gpiozero/gpiozero
公式ドキュメント: https://gpiozero.readthedocs.io/en/stable/
- Ben Nuttall氏とDave Jones氏が主にメンテナンスされている
- RaspberryPi OSのデフォルトで入っている
-
when_pressed
やwhen_pressed
便利- Whileループの外に定義して、割り込み的にボタンが実行できていることを確認
- デフォルトがプルアップなのは素敵な気がする(ほかのモジュールもそうかも):https://gpiozero.readthedocs.io/en/stable/api_input.html#button
- pigpioみたいにi2cは負荷(smbusなどで代用)
PWM(LED)とButton押下で割り込み実行するサンプル
コードはとてもシンプル。PWMをsleepつかって無茶な実行するとかしていないのは嬉しい。
#!/usr/bin/env python3
from gpiozero import PWMLED, Button
from time import sleep
# ボタン押下時の処理
def say_hello():
print("Hello!")
# GPIO17ピンにボタン接続()
button = Button(17)
button.when_pressed = say_hello
# GPIO18ピンにLEDを接続
led = PWMLED(18)
print("PWM LEDテスト開始...")
try:
while True:
# 0から1まで徐々に明るく
for brightness in range(0, 101):
led.value = brightness / 100.0
sleep(0.02)
# 1から0まで徐々に暗く
for brightness in range(100, -1, -1):
led.value = brightness / 100.0
sleep(0.02)
except KeyboardInterrupt:
print("\n終了します")
led.close()
サーボモーターを回すだけのサンプル
Servo
むっちゃいい。パルスの最大値と最小値、パルス周期をそのまま引数で指定してできる。特に中央取りようのメソッドがすでに用意されているのは魅力的
ただ、ソフトウェアPWMを使っていてジッターは健在なので注意。
(一応PiGPIOFactoryをServo
のpin_factory
引数にわたせればGPIO12,13,18,19でハードウェアPWMは得られるはず。:https://gpiozero.readthedocs.io/en/stable/api_pins.html#gpiozero.pins.pigpio.PiGPIOFactory)
from gpiozero import Servo
from time import sleep
# GPIO18ピンにサーボを接続
servo = Servo(27, min_pulse_width=0.6/1000, max_pulse_width=2.4/1000, frame_width=>
try:
while True:
print("最小位置へ移動")
servo.min()
sleep(1)
print("中央位置へ移動")
servo.mid()
sleep(1)
print("最大位置へ移動")
servo.max()
sleep(1)
smbus(i2c)
- gpiozeroだとi2cを触れなかったのでこちらをインストール
-
sudo raspi-config
でi2cを有効化して下記実行
-
sudo apt install python3-smbus i2c-tools
- たまたま手元にあったオムロン製大気圧センサの値を取得
- 残念ながらディスコン。仕様書はこちらを見た:https://www.mouser.jp/datasheet/2/307/Omron-2SMPB-02E-1275261.pdf
大気圧センサの値取得コード
コードはあまり精査していないので注意。
取り合えず値が取れたことだけ確認
#!/usr/bin/env python3
import smbus
import time
class SMPB02E:
def __init__(self, i2c_address=0x56, i2c_bus=1):
"""
2SMPB-02Eセンサを初期化
"""
self.bus = smbus.SMBus(i2c_bus)
self.address = i2c_address
# 補償係数を保存する変数
self.coef = {}
print(f"センサアドレス: 0x{self.address:02X}")
self.init_sensor()
self.read_calibration_coefficients()
def init_sensor(self):
"""センサの初期化"""
print("センサ初期化を開始...")
# チップID確認
chip_id = self.bus.read_byte_data(self.address, 0xD1)
if chip_id != 0x5C:
raise ValueError(f"想定外のチップID: {chip_id:02X}")
print(f" ✓ チップID: {chip_id:02X}")
# IIRフィルタ設定
self.bus.write_byte_data(self.address, 0xF1, 0x00) # IIRフィルタOFF
print(" ✓ 初期化完了")
def read_calibration_coefficients(self):
"""補償係数の読み取り(データシート準拠)"""
print("補償係数を読み取り中...")
# 補償係数レジスタを読み取り(0xA0-0xB8)
coef_data = []
for addr in range(0xA0, 0xB9):
coef_data.append(self.bus.read_byte_data(self.address, addr))
# データシートに基づいて係数を計算
# b00 (20ビット)
b00_0 = coef_data[0x01] # 0xA1
b00_1 = coef_data[0x00] # 0xA0
b00_ex = coef_data[0x18] >> 4 # 0xB8の上位4ビット
b00_raw = (b00_1 << 12) | (b00_0 << 4) | b00_ex
if b00_raw & 0x80000:
b00_raw -= 0x100000
self.coef['b00'] = b00_raw / 16.0
# a0 (20ビット)
a0_0 = coef_data[0x13] # 0xB3
a0_1 = coef_data[0x12] # 0xB2
a0_ex = coef_data[0x18] & 0x0F # 0xB8の下位4ビット
a0_raw = (a0_1 << 12) | (a0_0 << 4) | a0_ex
if a0_raw & 0x80000:
a0_raw -= 0x100000
self.coef['a0'] = a0_raw / 16.0
# 16ビット係数の計算
coef_16bit = {
'bt1': (0x02, 0x03, -6.3E-03, 4.3E-04), # 0xA2, 0xA3
'bt2': (0x04, 0x05, 1.2E-08, 1.2E-06), # 0xA4, 0xA5
'bp1': (0x06, 0x07, 3.3E-02, 1.9E-02), # 0xA6, 0xA7
'b11': (0x08, 0x09, 2.1E-07, 1.4E-07), # 0xA8, 0xA9
'bp2': (0x0A, 0x0B, -6.3E-10, 3.5E-10), # 0xAA, 0xAB
'b12': (0x0C, 0x0D, 2.9E-13, 7.6E-13), # 0xAC, 0xAD
'b21': (0x0E, 0x0F, 2.1E-15, 1.2E-14), # 0xAE, 0xAF
'bp3': (0x10, 0x11, 1.3E-16, 7.9E-17), # 0xB0, 0xB1
'a1': (0x14, 0x15, -6.3E-03, 4.3E-04), # 0xB4, 0xB5
'a2': (0x16, 0x17, -1.9E-11, 1.2E-10), # 0xB6, 0xB7
}
for name, (idx0, idx1, a_val, s_val) in coef_16bit.items():
raw_val = (coef_data[idx1] << 8) | coef_data[idx0]
if raw_val & 0x8000:
raw_val -= 0x10000
self.coef[name] = (a_val + s_val * raw_val / 32767.0)
print(" ✓ 補償係数読み取り完了")
def trigger_measurement(self):
"""測定開始"""
# 強制測定開始 (temp_ave=2, press_ave=8, forced mode)
self.bus.write_byte_data(self.address, 0xF4, 0x51) # 0101 0001
time.sleep(0.02) # 測定完了待機
def read_raw_data(self):
"""生データの読み取り"""
# 温度データ
temp_data = []
for addr in [0xFA, 0xFB, 0xFC]: # TEMP_TXD2, TXD1, TXD0
temp_data.append(self.bus.read_byte_data(self.address, addr))
# 圧力データ
press_data = []
for addr in [0xF7, 0xF8, 0xF9]: # PRESS_TXD2, TXD1, TXD0
press_data.append(self.bus.read_byte_data(self.address, addr))
# 24ビットデータに結合
raw_temp = (temp_data[0] << 16) | (temp_data[1] << 8) | temp_data[2]
raw_press = (press_data[0] << 16) | (press_data[1] << 8) | press_data[2]
# データシートに従い2^23を減算
dt = raw_temp - (1 << 23)
dp = raw_press - (1 << 23)
return dt, dp
def calculate_temperature(self, dt):
"""温度計算(データシート準拠)"""
# Tr = a0 + a1*Dt + a2*Dt²
a0 = self.coef['a0']
a1 = self.coef['a1']
a2 = self.coef['a2']
tr = a0 + a1 * dt + a2 * (dt ** 2)
temperature_c = tr / 256.0 # データシートに従い256で除算
return temperature_c, tr
def calculate_pressure(self, dp, tr):
"""圧力計算(データシート準拠)"""
# Pr = b00 + bt1*Tr + bp1*Dp + b11*Tr*Dp + bt2*Tr² + bp2*Dp² + b12*Tr²*Dp + b21*Tr*Dp² + bp3*Dp³
b00 = self.coef['b00']
bt1 = self.coef['bt1']
bp1 = self.coef['bp1']
b11 = self.coef['b11']
bt2 = self.coef['bt2']
bp2 = self.coef['bp2']
b12 = self.coef['b12']
b21 = self.coef['b21']
bp3 = self.coef['bp3']
pressure_pa = (b00 +
bt1 * tr +
bp1 * dp +
b11 * tr * dp +
bt2 * (tr ** 2) +
bp2 * (dp ** 2) +
b12 * (tr ** 2) * dp +
b21 * tr * (dp ** 2) +
bp3 * (dp ** 3))
return max(pressure_pa, 0) # 負の値を防止
def read_pressure_temperature(self):
"""大気圧と温度を読み取り"""
self.trigger_measurement()
# 生データ取得
dt, dp = self.read_raw_data()
# 計算
temperature, tr = self.calculate_temperature(dt)
pressure = self.calculate_pressure(dp, tr)
return pressure, temperature, dt, dp
def close(self):
"""リソースの解放"""
self.bus.close()
def main():
"""メイン処理"""
sensor = None
try:
# センサの初期化
sensor = SMPB02E()
print("2SMPB-02E 大気圧センサを初期化しました")
print("大気圧の測定を開始します... (Ctrl+Cで終了)\n")
# 連続測定ループ
measurement_count = 0
while True:
try:
# 大気圧と温度を測定
pressure_pa, temperature_c, dt, dp = sensor.read_pressure_temperature()
pressure_hpa = pressure_pa / 100.0 # hPa変換
measurement_count += 1
print(f"[{measurement_count:3d}] 大気圧: {pressure_pa:.1f} Pa ({pressure_hpa:.2f} hPa) | 温度: {temperature_c:.1f}°C")
print(f" 生データ: Dt={dt} Dp={dp}")
time.sleep(2) # 2秒間隔
except KeyboardInterrupt:
print("\n測定を終了します")
break
except Exception as e:
print(f"測定エラー: {e}")
time.sleep(1)
except Exception as e:
print(f"センサ初期化エラー: {e}")
finally:
if sensor:
sensor.close()
if __name__ == "__main__":
main()
とりあえず値を取れてるみたい。i2cの動作確認ができれば一旦OK。
python3-serial(UART)
-
sudo raspi-config
にてSerial PortをEnable
, Serial LoginをDisable
に設定 -
sudo apt install python3-serial
にてインストール - GPIO14とGPIO15をループバックにて接続
簡単なUARTループバックコード
#!/usr/bin/env python3
import time
import serial
# Raspberry Pi 5では ttyAMA0 を使用
uart = serial.Serial('/dev/ttyAMA0', baudrate=9600, timeout=1)
for i in range(5):
message = f"Test{i}"
print(f"送信: {message}")
# 送信
uart.write(message.encode('utf-8'))
time.sleep(0.1)
# 受信
if uart.in_waiting > 0:
received = uart.read(uart.in_waiting).decode('utf-8')
print(f"受信: {received}")
else:
print("受信なし")
time.sleep(1)
uart.close()
print("テスト完了")
まとめ
- PWM, I2C, UARTを一通り試してみた
- gpiozeroはメソッドがきれいに抽象化されているので可読性が前に比べて良くなったかも
- I2CとUARTはgpiozeroにて実装できないため、smbus並びにpython3-serialなど別モジュールが必要
- claude sonnet 4は神