ラズパイで温湿度気圧センサー(BME280)の値を小型ディスプレイに表示するプログラム
Contents
小型ディスプレイ
Raspberry Pi 3 B+で温湿度と気圧を測定できるセンサー(BME280)を使ったプログラムの記事の続き。
前回は測定した値をprint文でコンソールに表示をしただけだったが今回は測定した値を10秒ごとに小型のディスプレイに表示する回路とPythonのプログラムに関する記事とする。
スペック
ディスプレイは単体で購入したのでは無くて温湿度・気圧センサー(BME280)とOLEDディスプレイモジュール、そしてWi-Fiモジュールがセットになっている商品を購入した。
Wi-Fiとディスプレイモジュールが一緒になったセット商品は品切れになっていた。
ディスプレイは自分が購入した製品は品切れになっていたので互換と思われる製品。
小型ディスプレイのスペック
ディスプレイ | OLED(Organic Light Emitting Diode) 有機発光ダイオード |
互換性 | SSD1306ベースと記述されていた SSD1306はニューヨークに拠点があるAdafruit Industries社の製品 Githubに公開されているAdafruitのSSD1306のサンプルプログラムで恐らく動作すると予想をつけた |
サイズ | 0.96 インチ |
解像度 | 128 × 64 |
インターフェイス | I2C(Inter-Integrated Circuit) マスター(ラズパイ)からのSCL(クロック)信号に同期させてSDAでシリアルデータ通信を行う マスターに対して複数のスレーブが接続ができるのでBME280とディスプレイをスレーブとして接続する |
電圧 | 3.3V ~ 5V |
コネクタ
小型ディスプレイのコネクターは正面左から、
- GND(ラズパイGNDに接続)
- VCC(ラズパイ3.3Vに接続)
- SCL(BME280と同じSCLに接続)
- SDA(BME280と同じSDAに接続)
となっている。
表面
裏面
温湿度気圧センサー
温湿度・気圧センサーはBME280を使用した。
OLEDディスプレイと同様にI2C接続が可能で3.3V~5.0Vで動作する。
測定範囲、測定誤差等の詳細情報は以前の記事を参照して欲しい。
回路図
回路図は以下の通り。
BME280もOLEDディスプレイもI2C接続をしている。
事前準備
I2Cを有効にする
Raspberry Piの設定よりインターフェイスタブでI2Cを有効にする。
詳細はこちらの記事を参照。
プログラム用ディレクトリ
Raspberry Pi 3 B+の /opt/~配下にプログラム格納用のディレクトリ bme280を作成した。
cd /opt
sudo mkdir bme280
sudo chown -R pi:pi bme280/
bme280ディレクトリ配下に今回作成するプログラム(bme280_lcd.py)を格納する。
モジュールインストール
Pythonプログラムで使用するモジュールをインストールする。
smbus関連
温湿度気圧モジュール(BME280)で使用するI2C制御用のモジュールpython-smbusおよびsmbus2を以下のコマンドでインストールする。
sudo apt-get update
sudo apt install -y python-smbus
sudo pip install smbus2
Raspberry PiからI2CをPythonで制御するモジュールはpython-smbusが多い様だがBME280ではsmbus2を使用していたのでインストールしている。
ディスプレイ関連
ディスプレイはAdafruit社のSSD1306互換なのでGithubで公開されているAdafruit_Python_SSD1306のライブラリーで動作すると予測をつけて以下のコマンドでインストールした。
git clone https://github.com/adafruit/Adafruit_Python_SSD1306.git
cd Adafruit_Python_SSD1306
sudo python3 setup.py install
接続アドレスの確認
Raspberry PiのLXTerminalから以下のコマンドで接続アドレスを確認する。
sudo i2cdetect -y 1
以前の記事でBME280を接続した時の接続アドレスが0x76だったので、今回追加で接続したOLEDディスプレイのアドレスは0x3cであることが分かる。
- 76:BME280の接続アドレス
- 3c:OLEDディスプレイの接続アドレス
プログラム
BME280で温湿度気圧を測定して10秒毎にSSD1306互換のOLEDディスプレイに表示するPythonのプログラムは下記の通り。
尚、温湿度気圧を取得するBME280のロジックはgithubに公開しているSWITCH SCIENCEのサンプルプログラム(bme280_sample.py)をベースにクラス化と変数等のネーミングの変更などをしている。
またOLEDディスプレイに関してもAdafruitのSSD1036のGithub上のサンプルプログラムをベースにクラス化、変数等のネーミングの変更をすると同時に極力コメントを入れている。
ソースコード
# -*- coding: utf-8 -*-
"""
Created on Wed Sep 2 15:08:07 2020
@author: Souichirou Kikuchi.
"""
from smbus2 import SMBus
import time
import Adafruit_GPIO.SPI as SPI
import Adafruit_SSD1306
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
class Bme280Class: # 温湿度気圧センサー(BME280)クラス
def __init__(self, bus_number, i2c_address): # コンストラクタ(初期処理)
self.bus = SMBus(bus_number)
self.dig_temp = []
self.dig_pres = []
self.dig_humi = []
self.t_fine = 0.0
__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
self.write_reg(i2c_address, 0xF2, __ctrl_hum_reg)
self.write_reg(i2c_address, 0xF4, __ctrl_meas_reg)
self.write_reg(i2c_address, 0xF5, __config_reg)
self.get_calib_param(i2c_address)
def get_calib_param(self, i2c_address):
__calib = []
for i in range (0x88, 0x88+24):
__calib.append(self.bus.read_byte_data(i2c_address, i))
__calib.append(self.bus.read_byte_data(i2c_address, 0xA1))
for i in range (0xE1, 0xE1+7):
__calib.append(self.bus.read_byte_data(i2c_address, i))
self.dig_temp.append((__calib[1] << 8) | __calib[0])
self.dig_temp.append((__calib[3] << 8) | __calib[2])
self.dig_temp.append((__calib[5] << 8) | __calib[4])
self.dig_pres.append((__calib[7] << 8) | __calib[6])
self.dig_pres.append((__calib[9] << 8) | __calib[8])
self.dig_pres.append((__calib[11]<< 8) | __calib[10])
self.dig_pres.append((__calib[13]<< 8) | __calib[12])
self.dig_pres.append((__calib[15]<< 8) | __calib[14])
self.dig_pres.append((__calib[17]<< 8) | __calib[16])
self.dig_pres.append((__calib[19]<< 8) | __calib[18])
self.dig_pres.append((__calib[21]<< 8) | __calib[20])
self.dig_pres.append((__calib[23]<< 8) | __calib[22])
self.dig_humi.append( __calib[24] )
self.dig_humi.append((__calib[26]<< 8) | __calib[25])
self.dig_humi.append( __calib[27] )
self.dig_humi.append((__calib[28]<< 4) | (0x0F & __calib[29]))
self.dig_humi.append((__calib[30]<< 4) | ((__calib[29] >> 4) & 0x0F))
self.dig_humi.append( __calib[31] )
for i in range(1, 2):
if self.dig_temp[i] & 0x8000:
self.dig_temp[i] = (-self.dig_temp[i] ^ 0xFFFF) + 1
for i in range(1, 8):
if self.dig_pres[i] & 0x8000:
self.dig_pres[i] = (-self.dig_pres[i] ^ 0xFFFF) + 1
for i in range(0, 6):
if self.dig_humi[i] & 0x8000:
(-self.dig_humi[i] ^ 0xFFFF) + 1
def read_data(self, i2c_address):
__data = []
for i in range (0xF7, 0xF7+8):
__data.append(self.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)
__humi_raw = (__data[6] << 8) | __data[7]
__temp = self.compensate_temp(__temp_raw) # 温度を計算
__pres = self.compensate_pres(__pres_raw) # 気圧を計算
__humi = self.compensate_humi(__humi_raw) # 湿度を計算
return __temp, __pres, __humi
def write_reg(self, i2c_address, reg_address, data):
self.bus.write_byte_data(i2c_address, reg_address, data)
def compensate_pres(self, adc_pres):
__pressure = 0.0
__v1 = (self.t_fine / 2.0) - 64000.0
__v2 = (((__v1 / 4.0) * (__v1 / 4.0)) / 2048) * self.dig_pres[5]
__v2 = __v2 + ((__v1 * self.dig_pres[4]) * 2.0)
__v2 = (__v2 / 4.0) + (self.dig_pres[3] * 65536.0)
__v1 = (((self.dig_pres[2] * (((__v1 / 4.0) * (__v1 / 4.0)) / 8192)) / 8) + ((self.dig_pres[1] * __v1) / 2.0)) / 262144
__v1 = ((32768 + __v1) * self.dig_pres[0]) / 32768
if __v1 == 0:
return 0
__pressure = ((1048576 - adc_pres) - (__v2 / 4096)) * 3125
if __pressure < 0x80000000:
__pressure = (__pressure * 2.0) / __v1
else:
__pressure = (__pressure / __v1) * 2
__v1 = (self.dig_pres[8] * (((__pressure / 8.0) * (__pressure / 8.0)) / 8192.0)) / 4096
__v2 = ((__pressure / 4.0) * self.dig_pres[7]) / 8192.0
__pressure = __pressure + ((__v1 + __v2 + self.dig_pres[6]) / 16.0)
return __pressure / 100
def compensate_temp(self, adc_temp):
__v1 = (adc_temp / 16384.0 - self.dig_temp[0] / 1024.0) * self.dig_temp[1]
__v2 = (adc_temp / 131072.0 - self.dig_temp[0] / 8192.0) * (adc_temp / 131072.0 - self.dig_temp[0] / 8192.0) * self.dig_temp[2]
self.t_fine = __v1 + __v2
__temperature = self.t_fine / 5120.0
return __temperature
def compensate_humi(self, adc_humi):
__var_h = self.t_fine - 76800.0
if __var_h != 0:
__var_h = (adc_humi - (self.dig_humi[3] * 64.0 + self.dig_humi[4]/16384.0 * __var_h)) * (self.dig_humi[1] / 65536.0 * (1.0 + self.dig_humi[5] / 67108864.0 * __var_h * (1.0 + self.dig_humi[2] / 67108864.0 * __var_h)))
else:
return 0
__var_h = __var_h * (1.0 - self.dig_humi[0] * __var_h / 524288.0)
if __var_h > 100.0:
__var_h = 100.0
elif __var_h < 0.0:
__var_h = 0.0
return __var_h
class LCD1306class: # LCD1306クラス
"""
I2C:i2c_or_spi='i2c',rst, display_with, i2c_address, i2c_bus を指定する
SPI:i2c_or_spi='spi',rst, display_with, dc, spi_port, spi_device を指定する
SoftwareSPI:i2c_or_spi='softspi',rst, display_with, dc, sclk, din, cs を指定する
"""
def __init__(self, i2c_or_spi, rst, display_with, i2c_address, i2c_bus, dc=23, spi_port=0, spi_device=0, sclk=18, din=25, cs=22): # コンストラクタ(初期処理)
if i2c_or_spi == 'i2c':
if display_with == '128x32':
self.disp = Adafruit_SSD1306.SSD1306_128_32(rst=rst, i2c_address=i2c_address, i2c_bus=i2c_bus)
elif display_with == '128x64':
self.disp = Adafruit_SSD1306.SSD1306_128_64(rst=rst, i2c_address=i2c_address, i2c_bus=i2c_bus)
elif i2c_or_spi == 'spi':
if display_with == '128x32':
self.disp = Adafruit_SSD1306.SSD1306_128_32(rst=rst, dc=dc, spi=SPI.SpiDev(spi_port, spi_device, max_speed_hz=8000000))
elif display_with == '128x64':
self.isp = Adafruit_SSD1306.SSD1306_128_64(rst=rst, dc=dc, spi=SPI.SpiDev(spi_port, spi_device, max_speed_hz=8000000))
elif i2c_or_spi == 'softspi':
if display_with == '128x32':
self.disp = Adafruit_SSD1306.SSD1306_128_32(rst=rst, dc=dc, sclk=sclk, din=din, cs=cs)
elif display_with == '128x64':
self.disp = Adafruit_SSD1306.SSD1306_128_64(rst=rst, dc=dc, sclk=sclk, din=din, cs=cs)
self.disp.begin() # 初期化
self.disp_clear() # ディスプレイクリア
self.width = self.disp.width # ディスプレイの幅と高さを取得する
self.height = self.disp.height
self.image = Image.new('1', (self.width, self.height)) # 1: 白黒モード指定
self.draw = ImageDraw.Draw(self.image) # draw
self.font = ImageFont.load_default() # デフォルトフォント
def draw_background(self, outline): # 背景を黒く塗りつぶし(0:黒、255:白)
if outline == True: # 外枠を表示するかどうか
__outline = 255
else:
__outline = 0
self.draw.rectangle((0, 0, self.width-1, self.height-1), outline=__outline, fill=0)
def draw_text(self, x_axis, y_axis, draw_text): # 指定座標に文字を白(255)でdraw
self.draw.text((x_axis, y_axis), draw_text, font=self.font, fill=255)
def disp_display(self): # ディスプレイ表示
self.disp.image(self.image)
self.disp.display()
def disp_clear(self): # ディスプレイクリア
self.disp.clear()
self.disp.display()
BME280_BUS_NUMBER = 1
BME280_I2C_ADDRESS = 0x76 # BME280のI2Cのアドレス
I2C_SPI = 'i2c' # LCDをI2Cで接続する
LCD_RST = None # リセットしたいGPIOのピン番号を指定する
DISPLAY_WITH = '128x64' # LCDのディスプレイの解像度
LCD_I2C_ADDRESS =0x3C # LCDのI2Cのアドレス
LCD_I2C_BUS_NUMBER = 1 # LCDのI2CのBUS番号
if __name__ == '__main__':
try:
print('--- program start ---')
bme280 = Bme280Class(BME280_BUS_NUMBER, BME280_I2C_ADDRESS)
lcd1306 = LCD1306class(I2C_SPI, LCD_RST, DISPLAY_WITH, LCD_I2C_ADDRESS, LCD_I2C_BUS_NUMBER)
while True:
temp, pres, humi = bme280.read_data(BME280_I2C_ADDRESS) # 温湿度、気圧を取得
# print ('temp: {:-6.2f} c'.format(temp))
# print ('humi : {:6.2f} %'.format(humi))
# print ('pres : {:7.2f} hPa'.format(pres))
__outline = True # 外枠表示
lcd1306.draw_background(__outline) # 背景描画
x_axis = 5
y_axis = 5
draw_text = 'temp: {:-6.2f} c'.format(temp)
lcd1306.draw_text(x_axis, y_axis, draw_text) # 温度を表示
y_axis = y_axis + 16
draw_text = 'humi : {:6.2f} %'.format(humi)
lcd1306.draw_text(x_axis, y_axis, draw_text) # 湿度を表示
y_axis = y_axis + 16
draw_text = 'pres : {:7.2f} hPa'.format(pres)
lcd1306.draw_text(x_axis, y_axis, draw_text) # 気圧を表示
lcd1306.disp_display() # Displayに表示する
time.sleep(10)
except KeyboardInterrupt:
pass
finally:
lcd1306.disp_clear()
print('--- program end ---')
補足説明
モジュールのインポート
8~14行目でプログラムに必要なモジュールをインポートしている。
BME280クラス
16~129行目では温湿度気圧センサー(BME280)に対する処理をSWITCH SCIENCEのサンプルプログラムをベースにクラス化している。
基本的な処理はそのままでread_dataを呼べば温湿度気圧を返すメソッドを持ったクラスとした。
init
18~38行目はBME280クラスのコンストラクタ。
クラスが作成された時(190行目)に呼び出される。
変数の初期化とget_calib_paramメソッドを読み出している。
get_calib_param
調整パラメーターを取得している。
調整パラメーターは製造中にデバイスの不揮発性メモリーに書き込まれている値で温湿度、気圧の取得値を補正するためのパラメーター値が格納されている。
read_data
75~85行目のread_dataでF7h~FEhのアドレスから温度、圧力、湿度のデータをread_byte_dataで読み取って計算した後、温度、気圧、湿度の順番で返している。
メインからこのメソッドを読み出して温湿度気圧を取得する為のメソッド。
補正計算
90~129行目はget_calib_paramで取得した値を元に温湿度気圧の補正計算を行っている。
正直、処理内容の詳細は把握していないのだがこちらのデータシートの23ページ目に補正の計算式が載っているので同様の処理を行っている。
ディスプレイクラス(LCD1306class)
131~177行目はOLEDディスプレイのクラス。
ディスプレイに文字を表示するdraw_textメソッド、背景を黒く塗りつぶすdraw_backgroundメソッド、ディスプレイに表示するdisp_displayメソッドなどがある。
init
137~159行目はコンストラクタ。
- I2C
- SPI
- ソフトウェアSPI
の接続モードと解像度別に呼び出すAdafruitの関数を変更している。
ただI2C接続のAdafruit_SSD1306.SSD1306_128_64しか試していないので他の関数を呼び出した時に正常に動作するかは検証していない。
157行目のImage.newで指定している1は白黒モードの意味。
1(白黒)の他にはRGB(フルカラー)等の指定があるが今回のディスプレイは白黒なので1を指定している。
draw_background
背景を黒く塗りつぶす為のメソッド。
その際にoutlineにTrueが指定された時は外枠を描画する。
文字列を描画する前の処理でこのメソッドを呼び出して一旦背景を消すために使用する。
draw_text
x_axis(X軸)、 y_axis(Y軸)で指定された位置にdraw_textで指定された文字列を描画する。
disp_display
ディスプレイに表示する為のメソッド。
disp_clear
ディスプレイをクリアするメソッド。
プログラムの最初や終了時などに呼び出す。
定数の定義
179~185行目で定数を定義している。
事前に確認したI2Cの接続アドレスやディスプレイの解像度などを定義している。
メイン処理
187行目以降がメインのルーチン。
190行目でBme280Classのインスタンスの作成、192行目でLCD1306classのインスタンスを作成している。
またwhileループを10秒間隔で回してながら、193行目のbme280.read_dataで温湿度気圧を取得している。
後はdraw_textメソッドでそれぞれ描画した後、disp_displayでディスプレイに表示している。
実行結果
Raspberry Pi 3 B+からThonnyでプログラムを実行した結果は以下の通り。
動画
プログラムの実行の動画。
組み立てまでの様子。
以上で今回の記事を終了とする。
この記事が何処かで誰かの役に立つことを願っている。
尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。
全く同じ構成で同じことやろうとしていたので、めっちゃ役に立ちました!
測定値は出力OK、でもディスプレイ表示ができず、困っておりました。
ラズパイで色々遊んでみたく、また参考にさせてくださいまし。
ちなみに、私もG検定持ってます。@2020#2
ありがとうございました!
hide さん
こんばんは。コメントありがとうございます。
ディスプレイ表示のロジックがお役に立てた様で何よりです。
G検定取得者のコメントは初ですね?
これからもよろしくおねがいします。