i2c-gpio (ソフトウエアI2C)を割り当てたRaspberry Pi Compute Module 4(以下CM4)とBME280センサモジュールの測定データをSPI接続した2.8インチTFT液晶(240×320、ILI9341)モジュールに日本語フォントで表示した際の作業メモです。
LTC4331モジュール(マスタ、スレーブ用の2個セット)を使って、CM4のGPIOからケース外にLANケーブルで10m延長した先にBME280センサモジュールをI2C接続してベランダの気温を測っています。
CM4はRaspberry Pi 4 Model B(以下4B)の産業・組み込み用途向けモデル。CM4のセットアップ方法は多少異なるものの、OSやGPIOは4Bと互換性があるので同じアプリケーションや各種センサモジュールが動作します。
Raspberry Pi OS(bullseye)ではソフトウエアupdate通知アイコンがタスクバーに表示されるので作業前に更新しておきます。
ピン配置図と結線図
ILI9341を使った2.8インチTFT液晶(240×320)モジュールとCM4のGPIOとのピン接続と結線図です。
2.8インチTFT液晶モジュール(ILI9341)
2.8インチ液晶モジュールには、ドライバーICとしてILI9341を使った240×320の液晶モジュールです。今回、XPT2046を使ったタッチパネル、SDカードスロットは使っていません。
秋月電子通商サイトに「3.3Vで使用する場合はJ1をショートさせて三端子レギュレータ(U1)をバイパスさせてください。」とあったので、J1のランドパターンをはんだ付けしてショートします。
SDカード側コネクタ(J4)にはピンヘッダが未実装なので手持ちのピンヘッダをはんだ付けしています。
参考:
・ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶(akizukidenshi.com)
・2.8inch SPI Module ILI9341 SKU-MSP2807 – LCD wiki(回路図)
Raspberry PiとSPI接続
SPI はマスタ(ここではCM4)とスレーブ(ここでは2.8インチTFT液晶)の間をつなぐ4線(CS、CLK、MOSI、MISO)を使った双方向のクロック同期方式のシリアルインターフェース。これらのSPIピンとDC、RESETピンは、今回使ったPython3ライブラリのサンプルで使われるピン番号に合わせています。
2.8インチTFT液晶モジュールにはタッチパネルもありますが、液晶パネルのみの利用であればMOSIによる単方向通信なのでMISOは結線しなくでも動作します。
Raspberry PiとSPI接続
SPI はマスタ(ここではCM4)とスレーブ(ここでは2.8インチTFT液晶)の間をつなぐ4線(CS、CLK、MOSI、MISO)を使った双方向のクロック同期方式のシリアルインターフェース。これらのSPIピンとDC、RESETピンは、今回使ったPython3ライブラリのサンプルで使われるピン番号に合わせています。
2.8インチTFT液晶モジュールにはタッチパネルもありますが、液晶パネルのみの利用であればMOSIによる単方向通信なのでMISOは結線しなくでも動作します。
# | J2シルク 印刷 | ピン 番号 | GPIO (機能) | 備考 |
1 | VCC | 1 | 3V3 power | – |
2 | GND | 30 | Ground | – |
3 | CS | 24 | GPIO8 (CE0) | SPI:マスタ(Raspberry Pi)が通信相手のスレーブ(2.8″TFT液晶等)を選択するための信号線。CS:Chip Select、又はCE: Chip Enable、SS:Slave Select |
4 | RESET | 18 | GPIO24 | リセット信号 |
5 | DC | 22 | GPIO25 | 伝送される情報のコマンドとデータを区別する信号線。DC: Data/Command、又はRS Register Select)。 |
6 | SDI (MOSI) | 19 | GPIO10 (MOSI) | SPI:マスタ(Raspberry Pi)からスレーブ(2.8″TFT液晶)に転送するデータ信号線。MOSI:Master Out Slave In |
7 | SCK | 23 | GPIO11 (SCLK) | SPI:シリアルクロック。MISOやMOSIのデータが同期して転送。SCLK:Serial CLocK |
8 | LED | 1 | 3V3 power | – |
9 | SDO (MISO) | 21 | GPIO9 (MISO) | SPI:スレーブ(2.8″TFT液晶)からマスタ(Raspberry Pi)に転送するデータ信号線。MISO:Master In Slave Out |
パーツの結線図
Raspberry PiのI2C接続方式には、SoC内蔵の I2C(ハードウエア I2C) とi2c-gpio(ソフトウエアI2C)があります。i2c-gpioはソフトウェアでGPIOを操作 (Bit banging)してSDAとSCL相当のシリアル波形を作って通信を実現します。
GPIOピンの利用状況に応じて2つのI2C方式を使い分けています。
結線図(1)
2.8インチTFT液晶 (240×320)モジュールをSPIで、BME280センサモジュールをSoC内蔵の I2C(ハードウエア I2C)で接続する結線図(1)です。
苺メニューの「設定」–>「Raspberry Piの設定」を開いて–>「インターフェース」でI2Cを有効にチェックして、再起動するとSoC内蔵の I2C通信が使えるようになります。
結線図(2)
2.8インチTFT液晶(240×320)モジュールをSPIで、BME280をi2c-gpio(ソフトウエアI2C)で接続する結線図(2)です。
当サイトではSCLに割り当てられているGPIO3を電源の起動・シャットダウンに利用しているので、SoC内蔵の I2C(ハードウエア I2C)は無効化しています。代わって、i2c-gpio(ソフトウエアI2C)を利用しています。
ベランダなど遠隔地の測定のため、LTC4331モジュールを使ってI2Cを延長(今回は10mのLANケーブル)しています。LTC4331モジュール間はハードウエア処理なので、I2C通信やPythonなどのソフトウエア処理には影響ありません(※デジタルオシロスコープで信号波形を観察するとパルス幅は狭くなります)。LTC4331モジュール(マスタ、スレーブ両基板)の内蔵プルアップ抵抗(10kΩ)を有効化(はんだパターンをショート)します。
i2c-gpio(ソフトウエアI2C)を利用する場合、/boot/config.txtにdtoverlayでSDAやSCLピン( 例えば、GPIO 5 と GPIO 6 )やバス番号(例えば「3」)を設定します。Raspberry Pi OSを再起動すると反映します。
バス番号が変わるのでPythonなどでプログラム処理する際にはコード中の記述(bus_number)に注意します。
dtoverlay=i2c-gpio,bus=3,i2c_gpio_sda=5,i2c_gpio_scl=6,i2c_gpio_delay_us=2
SPIの有効化
Raspberry Pi OSの SPI はデフォルトでは無効です。
苺メニューの「設定」 –>「Raspberry Pi の設定」 –> 「インターフェース」で「SPI」のスライドバーのスイッチで「有効」に変更します。
Python3:BME280測定データを2.8インチTFT液晶に表示
ライブラリのインストールとサンプルプログラムで動作確認
2.8インチTFT液晶(240×320)モジュールの液晶コントローラLSIはILI9341。Adafruitのライブラリ「Adafruit_CircuitPython_RGB_Display」を利用させていただきました。
https://github.com/adafruit/Adafruit_CircuitPython_RGB_Display
ライブラリのインストールにはpipを使います。pipは、Pythonパッケージのインストールなどを行うユーティリティで、Raspberry Pi OS with desktop(bullseye)にはPython3とともにインストールされています。
バージョンはPythonが3.9.2、pipが20.3.4。
構築したRaspberry Pi OSにはPython3のみなので、コマンドではpipとpip3を区別していません。
sudo pip install adafruit-circuitpython-rgb-display
Adafruitライブラリ付属のサンプルプログラム「rgb_display_pillow_stats.py」で2.8インチTFT液晶 (240×320)モジュールの画面描画を検証しました。サンプルコード内のGPIOピン番号の修正の必要も無く、実行すると動作します。
Python3:BME280から測定データを取得
BME280から測定データを取得するPythonコードは、i2c-gpio(ソフトウエアI2C)に合わせてbus_numberを「1」から「3」に変更する必要があったので、 スイッチサイエンスのPython2用のサンプルコード(bme280_sample.py)をPython3用に修正して使わせていただきました。
未インストールであればi2C通信を行うためにsmbusをインストールします。
sudo pip install smbus2
Thonny Python IDEで Python3用に修正したbme280_sample.pyを実行(Run)。 I2Cに接続したBME280センサから測定データを取得できることが確認できました。
Python3:BME280測定データを2.8インチTFT液晶に表示(2022/8/13更新)
Adafruitライブラリ付属のサンプルプログラム「rgb_display_pillow_stats.py」を参考にして、 スイッチサイエンスのPython2用のサンプルコード(bme280_sample.py)をPython3用に修正したコードに出力先として2.8インチTFT液晶モジュールを加えた「bme280_sample_TFT_csv.py」を作りました。
日本語を表示するために、独立行政法人情報処理推進機構 (IPA) が提供している日本語フォント「IPAex フォント」をインストールしました。「ipaexfont-gothic」を使っています。
sudo apt install fonts-ipaexfont
Raspberry Pi OS with desktop(bullseye)にプリインストール されているThonny Python IDEを使って動作確認しました。
Thonny Python IDEのshell部分にCSV形式で連続表示するとともに、2.8インチTFT液晶にも日時(曜日)、気温、気圧、湿度を1秒間隔で表示します。
背景がダークグレイの部分が修正・追加した箇所です。
8行目の bus_number = 3 は、i2c-gpio(ソフトウエアI2C)を使う場合(結線図(2))のバス番号です。SoC内蔵の I2C(ハードウエア I2C)を利用する場合(結線図(1))は、7行目の bus_number =1の#を8行目に付け替えます。
Thonny Python IDEのshell部分へのCSV表示が不要な場合は206行目を#でコメントアウトします。
256行目のtime.sleep(xx)で測定間隔を調整します。
bme280_sample_TFT_csv.py
※ここをクリックするとコード表示を開閉できます。
#bme280_sample_TFT_csv.py
#coding: utf-8
#---BME280 init----------bme280_sample.py--------------
import datetime
from smbus2 import SMBus
#bus_number = 1 #Soc I2C (hardware I2C)
bus_number = 3 #i2c-gpio (software I2C)
i2c_address = 0x76
bus = SMBus(bus_number)
digT = []
digP = []
digH = []
t_fine = 0.0
#---TFT-ili9341 init-----rgb_display_pillow_stats.py---
import time
import subprocess
import digitalio
import board
from PIL import Image, ImageDraw, ImageFont
from adafruit_rgb_display import ili9341
# Configuration for CS and DC pins (these are PiTFT defaults):
cs_pin = digitalio.DigitalInOut(board.CE0)
dc_pin = digitalio.DigitalInOut(board.D25)
reset_pin = digitalio.DigitalInOut(board.D24)
# Config for display baudrate (default max is 24mhz):
BAUDRATE = 24000000
# Setup SPI bus using hardware SPI:
spi = board.SPI()
# pylint: disable=line-too-long
# Create the display:
disp = ili9341.ILI9341(
spi,
rotation=90, # 2.2", 2.4", 2.8", 3.2" ILI9341
cs=cs_pin,
dc=dc_pin,
rst=reset_pin,
baudrate=BAUDRATE,
)
# pylint: enable=line-too-long
# Create blank image for drawing.
# Make sure to create image with mode 'RGB' for full color.
if disp.rotation % 180 == 90:
height = disp.width # we swap height/width to rotate it to landscape!
width = disp.height
else:
width = disp.width # we swap height/width to rotate it to landscape!
height = disp.height
image = Image.new("RGB", (width, height))
# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)
# Draw a black filled box to clear the image.
draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0))
disp.image(image)
# First define some constants to allow easy positioning of text.
padding = -2
x = 0
# Load a ipaexfont-gothic font.
font1 = ImageFont.truetype("/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf", 65)
font2 = ImageFont.truetype("/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf", 25)
font3 = ImageFont.truetype("/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf", 20)
#---BME280---------------------------------------------
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)
return [t,p,h]
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)
def tft_csv(t,p,h):
dtd = time.strftime('%Y年%m月%d日', time.localtime())
dtt = time.strftime('%H時%M分%S秒', time.localtime())
dta = time.strftime('%a', time.localtime())
P_csv = dtd + " " + dtt + "," + t + "," + p + "," + h
print(P_csv)
#---TFT-ili9341- Draw ---------------------------------
# Draw a black filled box to clear the image.
draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0))
draw.text((20, 6), "温度:", font=font2, fill="#ffffff")
draw.text((30, 32), "(℃)", font=font3, fill="#ffffff")
temp =format(float(t), '.1f')
draw.text((90, 0), temp, font=font1, fill="#FFFF00")
draw.text((20, 70), "気圧:", font=font2, fill="#ffffff")
draw.text((25, 93), "(hPa)", font=font3, fill="#ffffff")
pres =format(float(p), '.1f')
draw.text((90, 60), pres, font=font1, fill="#FFFF00")
draw.text((20, 130), "湿度:", font=font2, fill="#ffffff")
draw.text((30, 153), "(%)", font=font3, fill="#ffffff")
humi =format(float(h), '.1f')
draw.text((90, 120), humi, font=font1, fill="#FFFF00")
draw.text((50, 185), dtd, font=font2, fill="#00ff00")
draw.text((50, 210), dtt, font=font2, fill="#00ff00")
if dta == "Mon":
jdta = dta.replace("Mon", "(月)")
elif dta == "Tue":
jdta = dta.replace("Tue", "(火)")
elif dta == "Wed":
jdta = dta.replace("Wed", "(水)")
elif dta == "Thu":
jdta = dta.replace("Thu", "(木)")
elif dta == "Fri":
jdta = dta.replace("Fri", "(金)")
elif dta == "Sat":
jdta = dta.replace("Sat", "(土)")
elif dta == "Sun":
jdta = dta.replace("Sun", "(日)")
else:
jdta="ー"
draw.text((225, 210), jdta, font=font2, fill="#00ff00")
disp.image(image)
setup()
get_calib_param()
if __name__ == '__main__':
try:
while True:
[t,p,h] = readData()
tft_csv(t,p,h)
time.sleep(1)
except KeyboardInterrupt:
pass