第3回 Raspberry Piで作る温度計 実装編

前回に引き続き「Raspberry Piで作る温度計」を作っていきます。
本テーマでは、Raspberry Piと温度センサ(BME280)、OLEDディスプレイ(SSD1306)を使って温度、湿度、気圧の表示をしてみます。

前回、前々回と温度計のシステム・ソフトウェア設計を行いました。まだ見ていない方はこちらからどうぞ。

あわせて読みたい
第1回 Raspberry Piで作る温度計 システム設計編[UML] 今回のテーマは「Raspberry Piで作る温度計」です。Raspberry Piと温度センサ(BME280)、OLEDディスプレイ(SSD1306)を使って温度、湿度、気圧の表示をしてみます。 本シ...
あわせて読みたい
第2回 Raspberry Piで作る温度計 ソフトウェア設計編[UML] 前回に引き続き「Raspberry Piで作る温度計」を作っていきます。本テーマでは、Raspberry Piと温度センサ(BME280)、OLEDディスプレイ(SSD1306)を使って温度、湿度、気圧...

今回は実装編になります。前回の設計結果に基づいてRaspberry PiにC言語でソフトウェアを実装し、動作させていきます。

ソースコードはgithubにアップロードしていますので自由にご利用ください。
https://github.com/tekutak/ThermoMeter

それでは実装編いってみましょう。

目次

部品のセットアップ(配線)

まずは部品の配線から押さえていきます。
前回の記事でも掲載しましたが改めて説明します。

Raspberry Piと温度センサ(BME280)、ディスプレイ(SSD1306)はI2Cで接続します。


使用するRaspberry Piのピンは①3.3V, ③SDA, ⑤SCL, ⑥GNDになります。
それぞれ、各部品のVin(Vcc), SDA, SCL, GNDと接続すればOKです。

https://www.raspberrypi.org/documentation/computers/os.html

完成図はこちらです。

開発環境の構築

Raspberry Piの開発をする際、Raspberry Piから直接HDMIでモニターにつないで開発してもよいのですが、キーボードとマウスもRaspberry Piに接続する必要があり、PCも併用している場合は結構ごちゃごちゃします。

そこで、Raspberry Piはリモートで接続して開発することをオススメします。
リモートで接続できれば、開発は普段のPCから行うことができるので効率アップ間違いなしです。

具体的には、SSH&WinSCPの環境を構築します。
SSHはリモート(PC↔Raspberry Piなど)で通信することができる仕組みです。
WinSCPはPC, Raspberry Pi間のファイル転送が簡単に行えるツールです。

下記ページを参考に設定してください。

SSHの設定
https://qiita.com/c60evaporator/items/2384416f1122ae124f50

WinSCPの使い方

日記というほどでも | IT系覚書 と...
WindowsとRaspberry Piの間で簡単にファイル転送・フォルダ同期するには | 日記というほどでも RaspbianやUbuntu、Fedora等、多くのRaspberry Pi用のディストリビューションは、SSHサーバ機能を標準で搭載しています。SSHは仮想ターミナルへのログイン機能の他に、暗号...

Raspberry Piのセットアップ

Raspberry Piの設定をしていきます。
Raspberry PiはカーネルにI2Cドライバが用意されているので、configから有効にしてあげます。

以下のコマンドを実行してください。

$ sudo raspi-config

config画面が立ち上がるので、「3 Interface Options」を選択してエンターを押してください。

「P5 I2C」を選択してエンター。

<はい>を選択してエンターを押せば完了です。
リブートするか聞かれたら<はい>でリブートしてください。

リブート後、I2Cが有効になっているか確認するため下記コマンドを実行してください。

$ ls /dev/*i2c*

下記が表示されれば問題なくI2Cが認識されています。(末尾の数字は異なる可能性があります)

/dev/i2c-1

追加でツールをインストールしておきます。下記コマンドを実行してください。

$ sudo apt install -y i2c-tools

i2cdetectでBME280とSSD1306が接続できているか確認します。
下記コマンドを実行してください。

$ i2cdetect -y 1

正しく接続できていれば下記のように3cと76がアクティブになって表示されます。
BME280が0x76、SSDが0x3cに該当します。

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- 76 --

ここまで確認できれば部品との接続はOKです。準備は整いました。
以降はプログラムを実装していきます。

実機動作

ソースコードはgithubにアップしていますので、ダウンロードしておいてください。
https://github.com/tekutak/ThermoMeter

早速動かしてみましょう。

ディレクトリはどこでも大丈夫ですが、Thermoという名前でディレクトリを作り、そこに移動しておきます。

$ mkdir Thermo
$ cd Thermo

githubからクローンしたファイル一式を上記のフォルダにコピーします。(Raspberry Pi上でgit cloneしてもOKです)
WinSCPから行うと簡単です。

ファイルの転送が完了したらlsで確認します。Makefileとsrcが表示されていればOKです。

$ ls
Makefile  src

makeコマンドでコンパイルします。

$ make
gcc -Wall -Wextra -I./src -I./src/DriverCtl -I./src/CompoCtl -I./src/SysFunc/Paint -I./src/SysFunc/Thermo -o obj/./src/main.o -c src/main.c
gcc -Wall -Wextra -I./src -I./src/DriverCtl -I./src/CompoCtl -I./src/SysFunc/Paint -I./src/SysFunc/Thermo -o obj/./src/CompoCtl/Bme280.o -c src/CompoCtl/Bme280.c
gcc -Wall -Wextra -I./src -I./src/DriverCtl -I./src/CompoCtl -I./src/SysFunc/Paint -I./src/SysFunc/Thermo -o obj/./src/CompoCtl/Ssd1306.o -c src/CompoCtl/Ssd1306.c
gcc -Wall -Wextra -I./src -I./src/DriverCtl -I./src/CompoCtl -I./src/SysFunc/Paint -I./src/SysFunc/Thermo -o obj/./src/DriverCtl/I2cCtl.o -c src/DriverCtl/I2cCtl.c
gcc -Wall -Wextra -I./src -I./src/DriverCtl -I./src/CompoCtl -I./src/SysFunc/Paint -I./src/SysFunc/Thermo -o obj/./src/SysFunc/Thermo/Thermo.o -c src/SysFunc/Thermo/Thermo.c
gcc -Wall -Wextra -I./src -I./src/DriverCtl -I./src/CompoCtl -I./src/SysFunc/Paint -I./src/SysFunc/Thermo -o obj/./src/SysFunc/Paint/Paint.o -c src/SysFunc/Paint/Paint.c
gcc -Wall -Wextra -I./src -I./src/DriverCtl -I./src/CompoCtl -I./src/SysFunc/Paint -I./src/SysFunc/Thermo -o obj/./src/SysFunc/Paint/Font.o -c src/SysFunc/Paint/Font.c
gcc -o ./a.out obj/./src/main.o obj/./src/CompoCtl/Bme280.o obj/./src/CompoCtl/Ssd1306.o obj/./src/DriverCtl/I2cCtl.o obj/./src/SysFunc/Thermo/Thermo.o obj/./src/SysFunc/Paint/Paint.o obj/./src/SysFunc/Paint/Font.o

コンパイルが完了すると、objフォルダとa.outが生成されているはずです。

$ ls
Makefile  a.out  obj  src

では早速動かしてみましょう。

$ ./a.out
[BME280]device id : 0x00000060
Temperature:26.5
Humidity:93.3
Pressure:963.9
...

上記のように温度・湿度・気圧が表示されれば動作できています。
ディスプレイにも表示されているのではないでしょうか。

停止したいときはctrl+cで止めます。

モジュールの実装

実機動作できたところで各モジュールの実装にフォーカスしていきましょう。
本セクションでは、第一回の設計と、実際のソースコードの実装がどうリンクするかを具体的にイメージできることを目的としています。

第一回で設計したモジュールの構成(依存関係)とシーケンスを再掲します。

モジュール構成

温度計測のシーケンスと、画面描画のシーケンスに分けて設計と実装がどうリンクするかを見ていきたいと思います。

温度計測

温度管理モジュール「Thermo」のMeasure関数がコールされると、Bme280制御モジュール「Bme280」にMeasure指示を出します。
計測が完了したら最新の温度・湿度・気圧を取得してThermoモジュールの内部変数に格納しておきます。

/* -----------------------------------------------------------------------------
 Function  : Thermo_Measure
 Memo      : 計測実施
 Date      : 2021.09.01
------------------------------------------------------------------------------*/
void Thermo_Measure()
{
    Bme280_Measure();
    F32_Temperature = Bme280_Get_Temperature();
    F32_Humidity = Bme280_Get_Humidity();
    F32_Pressure = Bme280_Get_Pressure();
}

Bme280では測定指示を発行し、113ms待ってから測定結果を読み出しています。
コマンドは自モジュールのI2Cドライバ制御ラッパーを経由してI2C経由で送信されます。詳細はソースコードをご参照ください。
読みだしたデータは補償関数を経由して温度・湿度・気圧の値に変換しています。

/* -----------------------------------------------------------------------------
 Function	: Bme280_Measure
 Memo		: 温度・湿度・気圧の計測実施
 Date		: 2021.08.28
------------------------------------------------------------------------------*/
void Bme280_Measure()
{
	U08 buf;
	BOOL status = OK;
	U08 read_data[BME280_READ_DATA_NUM] = {0};

	/* 測定開始 */
	buf = BME280_SETTING_REG_VAL_CTRL_MEAS;
	status = Bme280_Write1byte(BME280_REG_ADDRESS_CTRL_MEAS, buf);
	if (status == NG)
	{
		printf("[BME280]Forced Measure failed.\n");
		return;
	}

	//113ms sleep
	struct timespec req = {0, BME280_MEASURE_TIME * 1000 * 1000};
	struct timespec rem;
	nanosleep(&req, &rem);

	/* 読み出し */
	status = Bme280_Read(BME280_REG_ADDRESS_PRESS_MSB, read_data, BME280_READ_DATA_NUM);
	if (status == NG)
	{
		printf("[BME280]Data readout failed.\n");
		return;
	}

	/* 変換 */
	S32 adc_t = (S32)((((U32)read_data[e_BME280_READ_DATA_TEMP_MSB] << 16) | ((U32)read_data[e_BME280_READ_DATA_TEMP_LSB] << 8) | (U32)read_data[e_BME280_READ_DATA_TEMP_XLSB]) >> 4);
	F32_Temperature = (F32)Bme280_CompensateT(adc_t) * 0.01;
	S32 adc_p = (S32)((((U32)read_data[e_BME280_READ_DATA_PRESS_MSB] << 16) | ((U32)read_data[e_BME280_READ_DATA_PRESS_LSB] << 8) | (U32)read_data[e_BME280_READ_DATA_PRESS_XLSB]) >> 4);
	F32_Pressure = (F32)Bme280_CompensateP(adc_p) / 256.0 * 0.01;
	S32 adc_h = (S32)(((U32)read_data[e_BME280_READ_DATA_HUM_MSB] << 8) | (U32)read_data[e_BME280_READ_DATA_HUM_LSB]);
	F32_Humidity = (F32)Bme280_CompensateH(adc_h) / 1024.0;
}

画面描画

「Thermo」のGet関数がコールされると、「Thermo」は自身の内部変数の値を返してやります。こちらは先程Measure関数で更新した値が入っている変数になります。

/* -----------------------------------------------------------------------------
 Function  : Thermo_Get
 Memo      : 最新の温度・湿度・気圧の取得
 Date      : 2021.09.01
------------------------------------------------------------------------------*/
F32 Thermo_Get_Temperature()
{
    return F32_Temperature;
}
F32 Thermo_Get_Humidity()
{
    return F32_Humidity;
}
F32 Thermo_Get_Pressure()
{
    return F32_Pressure;
}

ディスプレイ描画モジュール「Paint」のDraw_ThermoMeter関数がコールされると、まず「Paint」自身が保持しているキャンバスのクリアを行い、温度・湿度・気圧の描画を実施します。この状態でキャンバスバッファには絵が描かれている状態になりますが、まだバッファは送信されていないので画面は更新されていません、

/* -----------------------------------------------------------------------------
 Function   : Paint_Draw_ThermoMeter
 Memo       : 温度・湿度・気圧の画面描画
 Date       : 2021.08.29
------------------------------------------------------------------------------*/
void Paint_Draw_ThermoMeter(F32 temperature, F32 humidity, F32 pressure)
{
    Paint_ClearCanvas();
    Paint_Draw_Temperature(temperature);
    Paint_Draw_Humidity(humidity);
    Paint_Draw_Pressure(pressure);
}

Flush関数がコールされたら「Paint」はキャンバスバッファの送信を行います。
まず、Ssd1306制御モジュール「Ssd1306」のキャンバスポインタGet関数をコールし、内部キャンバスバッファの値をコピーしてやります。
バッファのコピーが完了した後、「Ssd1306」のUpdate_Frame関数を実行するとI2C経由でSSD1306にバッファが送信され、画面が更新されます。

/* -----------------------------------------------------------------------------
 Function   : Paint_Flush_Canvas
 Memo       : キャンバスをSSD1306モジュールに流す
 Date       : 2021.09.01
------------------------------------------------------------------------------*/
void Paint_Flush_Canvas()
{
    U08 *p_buf = Ssd1306_Get_Draw_Canvas();
    memcpy(p_buf, U08_Canvas, SSD1306_CANVAS_SIZE);
    Ssd1306_Update_Frame();
}
/* -----------------------------------------------------------------------------
 Function   : Ssd1306_Update_Frame
 Memo       : 描画バッファをSSD1306に送信して画面更新
 Date       : 2021.08.28
------------------------------------------------------------------------------*/
void Ssd1306_Update_Frame()
{
    U08_Draw_Canvas_Payload[0] = SSD1306_CTRL_BYTE_DATA_SINGLE;
    Ssd1306_Write(U08_Draw_Canvas_Payload, SSD1306_DRAW_CANVAS_PAYLOAD_SIZE);
}

最後に

いかがでしたでしょうか。

実装編ではあらかじめソースコードを用意する形で構成しました。本シリーズでは、設計と実装が具体的にどうリンクするかをイメージしてもらうことを狙っているため、最初に完成図を提示する手段を取らせて頂きました。
ステップ・バイ・ステップで実装するやり方では、どうしても部品やドライバの細かい制御方法にフォーカスされやすく、主題から外れてしまうと考えたからです。

温度センサ(BME280)やディスプレイ(SSD1306)、I2Cドライバの使い方に関しては別記事で紹介予定ですので、そちらをご参照して頂けたらと思います。

第一回の設計編と今回の実装編を通して、設計と実装の関係が少しでも具体的にイメージできるようになったでしょうか。

ちまたに点在する設計情報やUMLの情報は抽象的なものが多く、実際のモノづくりではどう使えばいいのか、実装とどうリンクするのか、という疑問が浮かぶことが多いのではないでしょうか。

MonoEdgeではその悩みを解消できればと思い具体的なサンプルを作ろうとしています。
今後も本シリーズの掲載を増やしていきますので、少しでも皆さんのエンジニアリングに貢献できればと考えています。

もしこの記事を気に入って頂けましたらコメント・シェアしていただけると大変嬉しく思います。

それでは。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次