ラズパイで温湿度気圧センサー(BME280)の値を小型ディスプレイに表示するプログラム | そう備忘録

ラズパイで温湿度気圧センサー(BME280)の値を小型ディスプレイに表示するプログラム

小型ディスプレイ

Raspberry Pi 3 B+で温湿度と気圧を測定できるセンサー(BME280)を使ったプログラムの記事の続き。

前回は測定した値をprint文でコンソールに表示をしただけだったが今回は測定した値を10秒ごとに小型のディスプレイに表示する回路とPythonのプログラムに関する記事とする。

温湿度・気圧をOLEDディスプレイに表示する回路とプログラム

スペック

ディスプレイは単体で購入したのでは無くて温湿度・気圧センサー(BME280)とOLEDディスプレイモジュール、そしてWi-Fiモジュールがセットになっている商品を購入した。

2021/07/29 追記

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に接続)

となっている。

表面

OLEDディスプレイのコネクター

裏面

裏面

温湿度気圧センサー

温湿度・気圧センサーは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のアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。

souichirou

やった事を忘れない為の備忘録 同じような事をやりたい人の参考になればと思ってブログにしてます。 主にレゴ、AWS(Amazon Web Services)、WordPress、Deep Learning、RaspberryPiに関するブログを書いています。 仕事では工場に協働ロボットの導入や中小企業へのAI/IoT導入のアドバイザーをやっています。 2019年7月にJDLA(一般社団法人 日本デイープラーニング協会)Deep Learning for GENERALに合格しました。 質問は記事一番下にあるコメントかメニュー上部の問い合わせからお願いします。

おすすめ

4件のフィードバック

  1. hide より:

    全く同じ構成で同じことやろうとしていたので、めっちゃ役に立ちました!
    測定値は出力OK、でもディスプレイ表示ができず、困っておりました。
    ラズパイで色々遊んでみたく、また参考にさせてくださいまし。
    ちなみに、私もG検定持ってます。@2020#2
    ありがとうございました!

    • souichirou より:

      hide さん
      こんばんは。コメントありがとうございます。
      ディスプレイ表示のロジックがお役に立てた様で何よりです。
      G検定取得者のコメントは初ですね?
      これからもよろしくおねがいします。

  1. 2022年2月13日

    […] 前回の記事にてこちらのサイトを参考に、環境センサーBME280、秋月の0.96インチのAdafruit SSD1306互換のディスプレイ(OLED)をブレッドボードに配線すると書いてみた。実際にやってみるとブレッドボードへの配線図がサイトには載っていず、さらにBlynkというツールの導入法がなかなか理解できず困ったあ、と思っていた。そんななか、もっと単純な似た環境がないかと探してみたらこちらにありましたね。環境センサーはサイトでしようしているのは4ピン、自分のは6ピンという違いがある。このため試行錯誤がかなり長くなり、6時間くらいして下の写真のように2つの部品を認識することに成功した。 […]

  2. 2022年10月26日

    […] 前回の記事ではこちらのサイトを参考に、RaspberryPi上に環境センサーBME280、秋月の0.96インチのAdafruit SSD1306互換のディスプレイ(OLED)を使いブレッドボードに配線すると書いてみた。実際にやってみるとブレッドボードへの配線図がサイトには載っていず、さらに計測データをクラウド経由で自分のスマホに表示させるBlynkというツールの導入法がなかなか理解できず困ったあ、と思っていた。そんななか、もっと単純な似た環境がないかと探してみたらこちらにありましたね。環境センサーはあちらのサイトで使用しているのは4ピン、自分のは6ピンという違いがある。 […]

質問やコメントや励ましの言葉などを残す

名前、メール、サイト欄は任意です。
またメールアドレスは公開されません。