i2c-gpio(ソフトウエアI2C)を割り当てたRaspbery Pi CM4とBME280センサで気圧、温度、湿度の測定、SPI接続2.8インチTFT液晶に表示

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

SPI接続2.8インチTFTに表示
目次

ピン配置図と結線図

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)にはピンヘッダが未実装なので手持ちのピンヘッダをはんだ付けしています。

基板裏面のJ1のランドパターン
基板裏面のJ1のランドパターン(黄色の点線枠)

参考:
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
(機能)
備考
1VCC13V3 power
2GND30Ground
3CS24GPIO8
(CE0)
SPI:マスタ(Raspberry Pi)が通信相手のスレーブ(2.8″TFT液晶等)を選択するための信号線。CS:Chip Select、又はCE: Chip Enable、SS:Slave Select
4RESET18GPIO24リセット信号
5DC22GPIO25伝送される情報のコマンドとデータを区別する信号線。DC: Data/Command、又はRS Register Select)。
6SDI
(MOSI)
19GPIO10
(MOSI)
SPI:マスタ(Raspberry Pi)からスレーブ(2.8″TFT液晶)に転送するデータ信号線。MOSI:Master Out Slave In
7SCK23GPIO11
(SCLK)
SPI:シリアルクロック。MISOやMOSIのデータが同期して転送。SCLK:Serial CLocK
8LED13V3 power
9SDO
(MISO)
21GPIO9
(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通信が使えるようになります。

結線図(1)
結線図(1)
SoC内蔵の 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Ω)を有効化(はんだパターンをショート)します。

結線図(2)
結線図(2)

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」のスライドバーのスイッチで「有効」に変更します。

SPIを有効化
SPIを有効化

Python3:BME280測定データを2.8インチTFT液晶に表示

ライブラリのインストールとサンプルプログラムで動作確認

2.8インチTFT液晶(240×320)モジュールの液晶コントローラLSIはILI9341。Adafruitのライブラリ「Adafruit_CircuitPython_RGB_Display」を利用させていただきました。

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ピン番号の修正の必要も無く、実行すると動作します。

サンプルプログラム「rgb_display_pillow_stats.py」の実行
サンプルプログラム「rgb_display_pillow_stats.py」の実行
サンプルプログラムを2.8インチTFT液晶に表示
サンプルプログラムを2.8インチTFT液晶に表示

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

あわせて読みたい
i2c-gpio (ソフトウエアI2C)を割り当てたRaspbery Pi CM4とBME280センサで気圧、温度、湿度の測定、CSV... i2c-gpio (ソフトウエアI2C)を割り当てたRaspberry Pi Compute Module 4(以下CM4)とBME280センサモジュールで気圧、温度、湿度を測定した際の作業メモです。LTC4331モ...

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秒間隔で表示します。

shell部分にCSV形式で連続表示
shell部分にCSV形式で連続表示
2.8インチTFT液晶に日本語表示
2.8インチTFT液晶に日本語表示

背景がダークグレイの部分が修正・追加した箇所です。
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
よかったらシェアしてね!
  • URLをコピーしました!
目次