Raspberry Pi 3 B+, Adafruit RGB Matrix + RTC HAT, BME280搭載モジュールで温湿度計付き時計を作った

# 初めに
初めまして、この度Qiitaを始めた「Radian_N」（ラジアン）と申します。
初投稿となる今回の記事は、Raspberry Pi 3を用いてLEDマトリクスパネルを動かそうと試行錯誤し、製作した温湿度計付き時計が完成するまでの経緯について記載しようと思います。
完成したものは こちらです。

https://twitter.com/OUS_Radian/status/1334778452172361729?s=20

***
# ハードウェア（動作環境構築）
## 使用パーツ
* Raspberry Pi 3 model B+　（この記事では初期設定方法については触れません）
* [Adafruit RGB Matrix + RTC HAT](https://www.adafruit.com/product/2345)
* [Adafruit 64x32 RGB LED Matrix - 3mm Pitch](https://www.adafruit.com/product/2279)
* 3Vリチウムコイン電池 CR1220　（RTC:Real Time Clockを実装する場合に必要です）
* [スイッチサイエンス　BME280搭載 温湿度・気圧センサモジュール](https://www.switch-science.com/catalog/2236/)
* [ピンヘッダ](https://www.switch-science.com/catalog/92/)　（HATでI2Cと電源ポートを作成することなどに使用します）
* ACアダプタ 5V4A

Adafruit RGB Matrix + RTC HATについては、[マルツ電商](https://www.marutsu.co.jp/pc/i/574341/)からでも入手可能です

## LEDマトリクスパネルの購入
[Adafruit製のLEDマトリクスパネル][]の購入前に、AliExpressで格安のLEDパネルを2枚ほど購入しましたが、それらのHATにつないでの動作はなかなか確認できていません。詳細ページにも出力電圧や出力電力の表示がわかりづらい様子です。
もし確実にHATにつないで動作できるものを導入するのであれば、HATと同じメーカであるAdafruitのものを優先して買ったほうがいいと思います。

## はんだ付けや初期設定について
HATにピンヘッダなどをはんだ付けする必要があります。HATのはんだ付けの方法やRaspberry Pi 3用ライブラリのインストール方法については、 [こちら(英語)][HAT初期設定] を参照してください。
またBME280搭載モジュールとHATをI2Cでつなげられるように、別途ピンヘッダで対応ポートをはんだ付けしましょう。ピンヘッダはスイッチサイエンスで扱っています。
BME280搭載モジュールの接続と計測方法は、 [こちら][BME280参照] を参照および引用しました。
### 2021年8月追記
使用したBME280搭載モジュールについてですが、2021年7月時点でははんだ付け済みのものが在庫切れしている傾向にあるようです。
はんだ付けなしのものは十分に在庫があるようなので、そちらを代用し併せてはんだ付け作業が必要になることも念頭に置いておきましょう。

# ソフトウェア（Cソースコードなど）
## ソースコード・コンパイル・ビルド概要
実行ファイルの作成には、 [H.Zeller氏のHAT用Cライブラリ(英語解説)][HAT用ライブラリ] を使用しC言語で書きました（ `rpi-rgb-led-matrix/example-api-use/c-example.c` を参照しましょう）。前述でも触れた [HATの初期設定解説][HAT初期設定] でこのライブラリがインストールされるようです。
注意しておきたいこととして、BME280による温度と湿度の計測結果を入手するために `pigpio.h` のライブラリを使用します。その際に、 [HAT用ライブラリ][] でLEDマトリクスを設定（ `matrix = led_matrix_create_from_options(&options, &argc, &argv);` などの処理）を**する前**に `pigpio.h` の初期化処理（ `gpioInitialise()`関数 ）を行いましょう。
でないと 以下のような `initCheckPermitted` エラーが出てしまいます。

```
+---------------------------------------------------------+
|Sorry, you don't have permission to run this program. |
|Try running as root, e.g. precede the command with sudo. |
+---------------------------------------------------------+
```

またソースコードがあるディレクトリ内の `Makefile` に、 `pigpio.h` を使用することを以下のように指定しておきましょう。 `Makefile` の編集方法やコンパイル・ビルド方法については、[H.Zeller氏の解説][HAT用ライブラリ] を参照しましょう。


```Makefile:Makefile
LDFLAGS+=-L$(RGB_LIBDIR) -l$(RGB_LIBRARY_NAME) -lpigpio -lrt -lm -lpthread
```

## ソースコード
参考にしたサイトおよびソースコード引用先など

* [H.Zeller氏のHAT用ライブラリ][HAT用ライブラリ]　（前述以外にも`rpi-rgb-led-matrix/example-api-use/clock.cc` など）
* [BME280の計測値読み取り][BME280参照]
* [UTF-8形式の値に変換][文字を16進数に変換するサイト]　など

```c:c-source
// https://github.com/hzeller/rpi-rgb-led-matrix/tree/master/examples-api-use
// https://tomosoft.jp/design/?p=6963

#include "led-matrix-c.h"

#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include <pigpio.h>

// 前半は上記URLに記載してあるBME280の計測用関数を使用しています

#define BME280_ADDRESS 0x76
unsigned long int hum_raw,temp_raw,pres_raw;
signed long int t_fine;
 
unsigned int dig_T1;
int dig_T2;
int dig_T3;
unsigned int dig_P1;
int dig_P2;
int dig_P3;
int dig_P4;
int dig_P5;
int dig_P6;
int dig_P7;
int dig_P8;
int dig_P9;
char  dig_H1;
int dig_H2;
char  dig_H3;
int dig_H4;
int dig_H5;
char  dig_H6;
  
static int dev;
 
static int init_dev(void)
{
	if (gpioInitialise() < 0)
	{
		return 1;
	}
	
	dev = i2cOpen(1, BME280_ADDRESS, 0);
	if (dev < 0)
	{
		return 1;
	}
    return 0;
}
void getRegisters(char address, int numData, unsigned char *data) {
	i2cReadI2CBlockData (dev, address, data, numData);
}

void readTrim()
{
	unsigned char data[32],i=0;
	
	getRegisters(0x88, 24, &data[0]);
	i+=24;
	
	getRegisters(0xA1, 1, &data[i]);
	i+=1;
	
	getRegisters(0xE1, 7, &data[i]);
	i+=7;
	
	dig_T1 = (data[1] << 8) | data[0];
	dig_T2 = (data[3] << 8) | data[2];
	dig_T3 = (data[5] << 8) | data[4];
	dig_P1 = (data[7] << 8) | data[6];
	dig_P2 = (data[9] << 8) | data[8];
	dig_P3 = (data[11]<< 8) | data[10];
	dig_P4 = (data[13]<< 8) | data[12];
	dig_P5 = (data[15]<< 8) | data[14];
	dig_P6 = (data[17]<< 8) | data[16];
	dig_P7 = (data[19]<< 8) | data[18];
	dig_P8 = (data[21]<< 8) | data[20];
	dig_P9 = (data[23]<< 8) | data[22];
	dig_H1 = data[24];
	dig_H2 = (data[26]<< 8) | data[25];
	dig_H3 = data[27];
	dig_H4 = (data[28]<< 4) | (0x0F & data[29]);
	dig_H5 = (data[30] << 4) | ((data[29] >> 4) & 0x0F);
	dig_H6 = data[31];
}

void writeReg(unsigned char address, unsigned char data) {
	char adr;
	
	adr = address;
	i2cWriteByteData (dev, adr, data);
	// printf("adr:%x data:%x\n",adr, data);
}

void readData()
{
	unsigned char data[8];
	
	getRegisters(0xF7, 8, &data[0]);
	
	pres_raw = data[0];
	pres_raw = (pres_raw<<8) | data[1];
	pres_raw = (pres_raw<<4) | (data[2] >> 4);
	
	temp_raw = data[3];
	temp_raw = (temp_raw<<8) | data[4];
	temp_raw = (temp_raw<<4) | (data[5] >> 4);
	
	hum_raw  = data[6];
	hum_raw  = (hum_raw << 8) | data[7];
	// printf("TEMP :%x  DegC  PRESS :%x  hPa  HUM :%x \n",temp_raw,pres_raw,hum_raw);
}
 
 
signed long int calibration_T(signed long int adc_T)
{
	signed long int var1, var2, T;
	
	var1 = ((((adc_T >> 3) - ((signed long int)dig_T1<<1))) * ((signed long int)dig_T2)) >> 11;
	var2 = (((((adc_T >> 4) - ((signed long int)dig_T1)) * ((adc_T>>4) - ((signed long int)dig_T1))) >> 12) * ((signed long int)dig_T3)) >> 14;
	
	t_fine = var1 + var2;
	T = (t_fine * 5 + 128) >> 8;
	return T; 
}
 
unsigned long int calibration_P(signed long int adc_P)
{
	signed long int var1, var2;
	unsigned long int P;
	
	var1 = (((signed long int)t_fine)>>1) - (signed long int)64000;
	var2 = (((var1>>2) * (var1>>2)) >> 11) * ((signed long int)dig_P6);
	var2 = var2 + ((var1*((signed long int)dig_P5))<<1);
	var2 = (var2>>2)+(((signed long int)dig_P4)<<16);
	var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + ((((signed long int)dig_P2) * var1)>>1))>>18;
	var1 = ((((32768+var1))*((signed long int)dig_P1))>>15);
	if (var1 == 0)
	{
		return 0;
	}
	P = (((unsigned long int)(((signed long int)1048576)-adc_P)-(var2>>12)))*3125;
	if(P<0x80000000)
	{
		P = (P << 1) / ((unsigned long int) var1);
	}
	else
	{
		P = (P / (unsigned long int)var1) * 2;
	}
	var1 = (((signed long int)dig_P9) * ((signed long int)(((P>>3) * (P>>3))>>13)))>>12;
	var2 = (((signed long int)(P>>2)) * ((signed long int)dig_P8))>>13;
	P = (unsigned long int)((signed long int)P + ((var1 + var2 + dig_P7) >> 4));
	return P;
}
 
unsigned long int calibration_H(signed long int adc_H)
{
	signed long int v_x1;
	
	v_x1 = (t_fine - ((signed long int)76800));
	v_x1 = (((((adc_H << 14) -(((signed long int)dig_H4) << 20) - (((signed long int)dig_H5) * v_x1)) +
		((signed long int)16384)) >> 15) * (((((((v_x1 * ((signed long int)dig_H6)) >> 10) * 
		(((v_x1 * ((signed long int)dig_H3)) >> 11) + ((signed long int) 32768))) >> 10) + (( signed long int)2097152)) * 
		((signed long int) dig_H2) + 8192) >> 14));
	v_x1 = (v_x1 - (((((v_x1 >> 15) * (v_x1 >> 15)) >> 7) * ((signed long int)dig_H1)) >> 4));
	v_x1 = (v_x1 < 0 ? 0 : v_x1);
	v_x1 = (v_x1 > 419430400 ? 419430400 : v_x1);
	return (unsigned long int)(v_x1 >> 12);   
}

volatile bool interrupt_received = false;
static void InterruptHandler(int signo) {
	interrupt_received = true;
}

// ここからLEDマトリクスの処理になります

int main(int argc, char **argv) {
	// gpioInitialise関数を呼び出し（init_dev関数内）は、必ず
	// led_matrix_create_from_options関数（LEDマトリクスの初期設定）の前に
	// 実行しましょう
	if(init_dev()==1)  return 1;
	
	// LEDマトリクスを設定するための変数定義です
	struct RGBLedMatrixOptions options;
	struct RGBLedMatrix *matrix;
	struct LedCanvas *offscreen_canvas;
	int width, height;
	struct LedFont *font_main;
	struct LedFont *font_sub;

	memset(&options, 0, sizeof(options));
	options.rows = 32;
	options.cols = 64;
	options.chain_length = 1;
    
    // 表示するフォントの設定をします
	const char *bdf_font_main = "../fonts/10x20.bdf";
	const char *bdf_font_sub  = "../fonts/6x13.bdf";

	font_main= load_font(bdf_font_main);
	font_sub = load_font(bdf_font_sub);
	if (font_main == NULL) {
		fprintf(stderr, "Couldn't load font '%s'\n", bdf_font_main);
		return 1;
	}
	if (font_sub == NULL) {
		fprintf(stderr, "Couldn't load font '%s'\n", bdf_font_sub);
		return 1;
	}

    // LEDマトリクスの初期設定をします
	/* This supports all the led commandline options. Try --led-help */
	matrix = led_matrix_create_from_options(&options, &argc, &argv);
	if (matrix == NULL)
		return 1;

	/* Let's do an example with double-buffering. We create one extra
	* buffer onto which we draw, which is then swapped on each refresh.
	* This is typically a good aproach for animations and such.
	*/
	offscreen_canvas = led_matrix_create_offscreen_canvas(matrix);

	led_canvas_get_size(offscreen_canvas, &width, &height);

	fprintf(stderr, "Size: %dx%d. Hardware gpio mapping: %s\n",
		width, height, options.hardware_mapping);
	
	// 時間を表示するための変数設定です
	struct tm tm;
	struct timespec next_time;
	next_time.tv_sec = time(NULL);
	next_time.tv_nsec = 0;

	// 表示する文字列を格納する変数です（順に 時刻用, 気温用, 湿度用）
	char text_buff0[256], text_buff1[256], text_buff2[256];

	// この2行は不要かもしれません、必要に応じましょう
	signal(SIGTERM, InterruptHandler);
	signal(SIGINT, InterruptHandler);
	
	// 再びBME280用の処理です
	unsigned char osrs_t = 1; // Temperature oversampling x 1
	unsigned char osrs_p = 1; // Pressure oversampling x 1
	unsigned char osrs_h = 1; //　Humidity oversampling x 1
	unsigned char mode = 3;   //　Normal mode
	unsigned char t_sb = 5;   //　Tstandby 1000ms
	unsigned char filter = 0; //　Filter off 
	unsigned char spi3w_en = 0;	//　3-wire SPI Disable
    
	unsigned char ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
	unsigned char config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
	unsigned char ctrl_hum_reg  = osrs_h;
    
	double temp_act = 0.0, press_act = 0.0,hum_act=0.0;
	signed long int temp_cal;
	unsigned long int press_cal,hum_cal;
    
	writeReg(0xF2,ctrl_hum_reg);
	writeReg(0xF4,ctrl_meas_reg);
    	readTrim();
	
	
	while(!interrupt_received) { // 上記signal関数の2行を省略した場合：while(1)
		// 時刻を表示する文字列に変換します
		localtime_r(&next_time.tv_sec, &tm);
		strftime(text_buff0,sizeof(text_buff0),"%H:%M", &tm);

		// BME280の計測を行います		
		readData();
		temp_cal = calibration_T(temp_raw);
		press_cal = calibration_P(pres_raw);
		hum_cal = calibration_H(hum_raw);
		temp_act = (double)temp_cal / 100.0; // 気温[℃]
		press_act = (double)press_cal / 100.0; // 湿度[%]
		hum_act = (double)hum_cal / 1024.0; // 気圧[hPa](今回は使用しません)

		// 表示するための処理です
		led_canvas_fill(offscreen_canvas, 0,0,0); // 次フレームで前の文字が重ならないよう黒画面にする
		// 文字を描画します
		draw_text(offscreen_canvas, font_main,
			7, 1+baseline_font(font_main),
			127,255,191, text_buff0, 0); // 時刻を表示
		sprintf(text_buff1, "%4.1f\xE2\x84\x83", temp_act); // 気温を文字列に変換
		draw_text(offscreen_canvas, font_sub,
			3, 3+baseline_font(font_main)+baseline_font(font_sub),
			255,191,127, text_buff1, 0); // 気温を表示
		sprintf(text_buff2, "%3.0f%%", hum_act); // 湿度を文字列に変換
		draw_text(offscreen_canvas, font_sub,
			38, 3+baseline_font(font_main)+baseline_font(font_sub),
			255,255,255, text_buff2, 0); // 湿度を表示
		
        // クロックを更新します
		// Wait until we're ready to show it.
		clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &next_time, NULL);
		
		/* Now, we swap the canvas. We give swap_on_vsync the buffer we
		 * just have drawn into, and wait until the next vsync happens.
		 * we get back the unused buffer to which we'll draw in the next
		 * iteration.
		 */
		
		// 次のフレームの描画に移ります
		offscreen_canvas = led_matrix_swap_on_vsync(matrix, offscreen_canvas);
		
		next_time.tv_sec += 1; // 時刻の更新
	}

	// 終了処理です
	/*
	 * Make sure to always call led_matrix_delete() in the end to reset the
	 * display. Installing signal handlers for defined exit is a good idea.
	 */
	led_matrix_delete(matrix); // LEDマトリクスを閉じます

	gpioTerminate(); // GPIOを閉じます（2022年5月追記、無くても動くと思います）

	return 0;
}
```
### 2022年5月追記
コードの注釈等を追記・修正しました。
文字の16進数（UTF-8形式）への変換についてですが、今回は気温の単位「℃」を表示するために使用しています。しかしCJK(日中韓)対応フォントデータを使っている際に、日本語の文字列代入で16進数変換を使わなくても難なく動きましたので、場合によっては必要ないかもしれません。

***
# あとがき
LEDマトリクスパネルを動かすことやソースコードの作成など手探りだったので、もしこの投稿で少しでも参考になる人がいればいいなと思っております。

## 参考・引用URLまとめ

* [完成品][]
* [HAT初期設定(英語)][HAT初期設定]
* [HAT用ライブラリ(英語)][HAT用ライブラリ]
* [BME280との接続][BME280参照]
* [文字を16進数に変換するサイト][]　など

[完成品]:https://twitter.com/OUS_Radian/status/1334778452172361729?s=20 
[HAT初期設定]:https://learn.adafruit.com/adafruit-rgb-matrix-plus-real-time-clock-hat-for-raspberry-pi
[BME280参照]:https://tomosoft.jp/design/?p=6963
[HAT用ライブラリ]:https://github.com/hzeller/rpi-rgb-led-matrix/tree/master/examples-api-use
[文字を16進数に変換するサイト]:https://www.marbacka.net/msearch/tool.php#chr2enc
[Adafruit製のLEDマトリクスパネル]:https://www.adafruit.com/product/2345