Seed Studio XIAO ESP32C3(以下、XIAO ESP32C3)とI2C接続したBME280、WiFi接続で情報通信研究機構(NICT)のNTPサーバと時刻合わせして、LCDモジュール AQM1602Y-NLW-FBW に日時、気圧、気温、湿度を表示する小型の環境モニタを作りました。
電源起動(スケッチ実行)時にWiFi経由でNTPサーバに接続してXIAO ESP32C3の内蔵時計をJSTに時刻合わせします。加えて、1日1回、NTPサーバに接続して時刻合わせすることで精度を維持します。
LCDモジュールの表示領域は16文字2行と少ないので、曜日の表記は漢字1文字です。AQM1602YのCGRAMに漢字のキャラクタパターンを登録して表示します。
準備したパーツ
XIAO ESP32C3は、Espressif ESP32-C3 WiFi / Bluetoothデュアルモードチップをベースにした超小型のマイコンボード。当サイトでよく利用する ESP32-DevKitC とはピン数が異なるものの、Arduino IDE(arduino-esp32)を使って開発できます。
小型モジュールのためピン数が限られますが独立したI2C、SPIピンがあるので、これらの接続インターフェースを持ったセンサモジュールを使って小型化できます。
ネット通販(秋月電子通商 など)でパーツを集めてブレッドボードで組み立てました。
# | パーツ | 個数 | 備考 |
1 | Seed Studio XIAO ESP32C3 | 1 | 【M-17454】 |
2 | BME280 気圧、温湿度センサ モジュール ※今回使ったモジュールはI2C接続、電圧レギュレータとI2C電圧レベル変換回路を内蔵 | 1 | 手持ち |
3 | ミニブレッドボード BB-601(白) | 1 | 【M-15178】 |
4 | LCDモジュール 16X2行 白色バックライト付 白文字 黒背景 AQM1602Y-NLW-FBW | 1 | 【P-12486】 |
5 | 積層セラミックコンデンサー 0.1μF 50V X7R 2.54mm(10個入) ※利用するのは1個 | 1 | 【P-13582】 |
6 | 積層セラミックコンデンサー 1μF 50V Y5V 5mm(10個入) ※利用するのは2個 | 1 | 【P-03093】 |
7 | 抵抗 220Ω ※バックライトLED電流制限抵抗 | 1 | 手持ち |
8 | 丸ピンIC用ソケット (シングル 9P)1×9 ※AQM1602Yをブレッドボードに挿すときに利用 ※ユニバーサル基板で一体化時には不要 | 1 | 【P-01016】 |
9 | 細ピンヘッダ 1×40(黒) ※バックライトLED端子(A、K)のピン ※ユニバーサル基板で一体化した際の引出し用ピン ※XIAO ESP32C3用のピン | 1 | 【C-06631】 |
10 | 両面ユニバーサル基板 ※AQM1602Yとパーツの一体化実装用 | 適量 | 手持ち |
11 | ジャンパーワイヤ オス-オス 10cmセット | 適量 | 【C-05371】 |
組立と動作確認
結線図
動作確認できた現在の結線図です。XIAO ESP32C3のピンレイアウトは下記サイトを参照して結線します。XIAO ESP32C3には、シート状のWiFiアンテナ(裏面は3M 300LSE両面テープ)がつきます。
Getting Started with Seeed Studio XIAO ESP32C3
https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/
Pinout diagram
LCDモジュール(AQM1602Y-NLW-FBW)
I2C接続のAQM1602Y-NLW-FBWは、薄型の16文字×2行のキャラクタ液晶、コントラスト調整はコマンドで行います。
このLCDモジュールを3.3Vで使う際はVoltage booster circuit(昇圧回路)を働かせるためにキャパシタが必須。CAP1P(4番ピン)とCAP1N(5番ピン)端子間に外付けキャパシタ(C1)を、VOUT(2番ピン)とVIN(3.3V)間にキャパシタ(C2)をつけます。
C1、C2の値は、秋月電子通商サイトの下記資料を参考に選択しました。VDDとVSS間のC3はパスコンです。
コントローラICデータシート(PDF)p61
https://akizukidenshi.com/download/ds/sitronix/st7032.pdf
ST7032i under Glass, IIC interface, with booster and follower circuit on
C1 : 0.1μF~1μF (SMD)
C2 : 0.47μF~2.2μF (SMD)
バックライトのLED電流制限抵抗として手持ちの220Ωを使っているので電流は15mA程度です(LEDの最大電流の仕様は30mA)。その際のコントラスト設定(lcd.setContrast(); は 40(0~63の範囲で調整))でした。
黒背景なので視認性も良いです。
LCDモジュールのピン引出し加工
LCDモジュール(AQM1602Y-NLW-FBW)と外付けキャパシタ(3個)、バックライトLED電流制限抵抗(1個)、接続用4ピン(3.3V、GND、SDA、SCL)をミニブレッドボード BB-601(白)で結線して動作確認しました。
LCDモジュールのバックライトLED端子(A、K)は板状なのでブレッドボードに直接挿せるように細ピンヘッダを曲げてはんだ付けしています。1~9番ピンはピンが細く短いので、丸ピンIC用ソケットを間に挿して引出しています。
LCDモジュールとパーツをユニバーサル基板で一体化(2022/12/17追加)
LCDモジュール、外付けのキャパシタ、抵抗、接続用ピンヘッダー(3.3V、GND、SDA、SCL)をユニバーサル基板上で配線・はんだ付けして組み立てました。一体化する際にはブレッドボードに挿すために使った丸ピンIC用ソケットは使いません。バックライトLED端子はユニバーサル基板のスルーホールの穴径をドリルで2.5mmΦに拡げます。
LCDモジュールを小型化できたので、1枚のミニブレッドボード BB-601上に全てのパーツを実装できました。
XIAO ESP32C3のSCLピンとSDAピンの信号波形(2022/12/19追加)
今回使ったBME280モジュールにはI2C電圧レベル変換回路内にプルアップ抵抗が内蔵されていたので、 外部に I2Cのプルアップ抵抗は実装していません。
ブレッドボード上のXIAO ESP32C3のSCLピンとSDAピンの信号波形をデジタルオシロスコープで見ると、信号の立ち上がりが少しなまっている波形でした。
現状動作に支障はないですが、外部にI2Cプルアップ抵抗を設けた方がより安心です。
開発ツール arduino-esp32のインストールとライブラリの追加
ESP32の開発ツール arduino-esp32を準備してスケッチを作っていきます。
ESP32開発ツール arduino-esp32のインストール
XIAO ESP32C3サイトにarduino-esp32のインストール手順の記載があります。
Getting Started with Seeed Studio XIAO ESP32C3
https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/#software-setup
+ Software setup
当サイトにもESP32-DevKitCを例として、arduino-esp32 をArduino IDE 2.0.x版にインストールする手順メモを纏めています。
ボード(XIAO_ESP32C3を選択)とポート(当サイトのPCではCOM6)を切り替えることでXIAO ESP32C3でもそのまま使えます。
ライブラリのインクルード
先達の方々が開発されたライブラリをインクルードすることでスケッチ作成が容易になります。
LCDモジュール
LCDモジュールのライブラリには ST7032.h を利用させていただきました。
ライブラリのzipファイル( arduino_ST7032-master.zip )をPCにダウンロードして保存(「Code」ボタンをクリックして「Download ZIP」)。Arduino IDEのメニューの「スケッチ」→「ライブラリをインクルード」→「.ZIP形式の ライブラリを インストール」で ダウンロードしたZIPファイルを追加します。
https://github.com/tomozh/arduino_ST7032
BME280センサ
BME280センサ(0x76)から測定データを取得するライブラリとして adafruit/Adafruit_BME280_Library と adafruit/Adafruit_Sensor を利用させていただきました。
TimeLib.h
Time.h、TimeLib.hライブラリは PaulStoffregen/Time を利用させていただきました。サイトの「Code」プルダウンから「Download ZIP」でPCに「Time-master.zip」をダウンロード。Arduino IDEのメニュー「スケッチ」–>「ライブラリをインクルード」–>「zip形式のライブラリをインクルード」で「Time-master.zip」を指定します。
esp_sntp.h
「esp_sntp.h」は arduino-esp32同梱モジュールです。詳細は Official develooment framework for ESP32 (ESP-IDF) に記述があります。mulong.meサイトを参考に内蔵時計の時刻が NTPサーバから取得した時刻に一致しているかの判定に使っています。
スケッチ:LCDの動作確認、コントラスト調整
コントラスト確認用のスケッチです。1行目にLCDの型式を、2行目に数字をカウントアップ表示します。
今回バックライトのLED電流制限抵抗として手持ちの220Ωを使っています。その際のコントラスト設定 lcd.setContrast(); は、40(0~63の範囲で調整)でした。黒背景なので視認性も良いです。
XIAO_ESP32C3_AQM1602.ino
※ここをクリックするとコード表示を開閉できます。
#include <Wire.h>
#include <ST7032.h>
ST7032 lcd;
int count=0;
void setup(){
Wire.begin(); // I2C初期化
lcd.begin(16, 2); // LCDの文字数(16)と行数(2)
lcd.setContrast(40); // LCDのコントラスト調整(0~63)
lcd.setCursor(0, 0);
lcd.print("AQM1602Y-NLW-FBW");
}
void loop() {
// 液晶にcount値を表示
lcd.setCursor(0, 1);
lcd.print(count);
// カウントアップ
count++;
delay(1000);
}
スケッチ:LCDモジュールに時刻、気圧、気温、湿度を表示
XIAO ESP32C3とI2C接続したBME280センサ、WiFi接続でNTPサーバと時刻合わせして、LCDモジュールAQM1602Yに日時、時刻、気圧、気温、湿度を表示します。
表示レイアウト、曜日をカタカナで表記
LCDモジュールの表示領域は16文字2行、下記レイアウトで表示しています。年の表示、気圧、温度、湿度の単位表示(ppm、℃、%)は無しです。
上段: | 月/日、曜日(1文字)、時:分:秒 |
下段: | 気圧(4桁)、温度と湿度(整数2桁、小数点以下1桁) |
曜日は、表示領域が1文字しか取れなかったので、読みの先頭1文字をカタカナ(二、ケ、カ、ス、モ、キ、ト)とケ゛、ト゛のみ濁点付きの2文字(時刻との境のスペース部分に ゛を表示)で表現しています。
LCDモジュールの キャラクタ パターンは秋月電子通商サイトのAQM1602Y-NLW-FBW PDFデータシートのp12にあります。
スケッチには1文字の英字(S、M、T、W、T、F、S)の配列も残してます。
曜日 | d_wday | LCD 表示 | キャラクタ コード |
日 | 0 | 二 | 0xc6 |
月 | 1 | ケ゛ | 0xb9 + 0xde |
火 | 2 | カ | 0xb6 |
水 | 3 | ス | 0xbd |
木 | 4 | モ | 0xd3 |
金 | 5 | キ | 0xb7 |
土 | 6 | ト゛ | 0xc4 + 0xde |
//曜日(カタカナ表示)
char weekStr[7] = {0xc6,0xb9,0xb6,0xbd,0xd3,0xb7,0xc4}; //1文字カタカナ(二,ケ,カ,ス,モ,キ,ト)
char dakuten = 0xde; //1文字の濁点(゛)
・・・
// ----- 曜日(英字 or カタカナ表示)
lcd.print(weekStr[d_wday]);
//濁点追加(ケ゛、ト゛のみ)、英字の場合は91~95行は不要)
if ((String(d_wday) == "1") || (String(d_wday) == "6")) {
lcd.print(dakuten);
} else {
lcd.print(" "); // 残像消去@2022/12/18追加
}
曜日を漢字(キャラクタパターンをCGRAM登録)で表記(2022/12/22追記)
LCDモジュールのCGRAMに任意のキャラクタパターンを登録し、そのコードを呼び出して表示させます。ST7032.hライブラリには、lcd.createChar()、lcd.write() 関数が用意されており、キャラクタパターンによる漢字表示が容易にできます。
漢字のキャラクタパターンを2進数に変換、”木”の場合はキャラクタパターンの上段より00100、00100、11111・・・00000の8行分をbyte型の配列で宣言します。
最下段はスペースなので5×7ドットのフォントです。粗さ(特に水、金)が気になりますが、漢字だと直感的な曜日の判断ができます。
//曜日(漢字キャラクタパターン)
byte nichi[8] = { 0b11111, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b11111, 0b00000 }; //日
byte getsu[8] = { 0b01111, 0b01001, 0b01111, 0b01001, 0b01111, 0b01001, 0b10001, 0b00000 }; //月
byte kayou[8] = { 0b00100, 0b10101, 0b10101, 0b00100, 0b01010, 0b01010, 0b10001, 0b00000 }; //火
byte suiyo[8] = { 0b00100, 0b00101, 0b11110, 0b00110, 0b01101, 0b10100, 0b00100, 0b00000 }; //水
byte mokuy[8] = { 0b00100, 0b00100, 0b11111, 0b00100, 0b01110, 0b10101, 0b00100, 0b00000 }; //木
byte kinyo[8] = { 0b00100, 0b01010, 0b10001, 0b01110, 0b10101, 0b01110, 0b11111, 0b00000 }; //金
byte doyou[8] = { 0b00000, 0b00100, 0b00100, 0b01110, 0b00100, 0b00100, 0b11111, 0b00000 }; //土
NTPサーバに接続して時刻合わせ(2023/01/15スケッチ修正)
電源起動(スケッチ実行)時にWiFi接続して、情報通信研究機構(NICT)のNTPサーバに接続してXIAO ESP32C3の内蔵時計をJSTに時刻合わせします。加えて、1日1回(下記スケッチでは12時30分0秒)、NTPサーバに接続して時刻合わせを実行して精度を維持します。
時刻合わせ実行時には、WiFi接続、NTPサーバ接続、JST同期の進捗状況をLCDの上段に表示しています。
最大1310720バイトのフラッシュメモリのうち、スケッチが681084バイト(51%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が33556バイト(10%)を使っていて、ローカル変数で294124バイト使うことができます。
XIAO_ESP32C3_AQM1602_bme280.ino
※ここをクリックするとコード表示を開閉できます。
#include <Wire.h>
#include <WiFi.h>
#include <esp_sntp.h>
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time
#include <ST7032.h> // https://github.com/tomozh/arduino_ST7032
#include <Adafruit_Sensor.h> // https://github.com/adafruit/Adafruit_Sensor
#include <Adafruit_BME280.h> // https://github.com/adafruit/Adafruit_BME280_Library
ST7032 lcd;
Adafruit_BME280 bme;
bool status;
float pressure;
float temp;
float humid;
const char* ssid = "your ssid";
const char* password = "your password";
const char* ntpServer = "ntp.nict.jp";
const long gmtOffset_sec = 32400;
const int daylightOffset_sec = 0;
struct tm *tm;
//曜日(英字表示)
//static const char *weekStr[7] = {"S","M","T","W","T","F","S"}; //1文字の英字
//曜日(カタカナ表示)
//char weekStr[7] = {0xc6,0xb9,0xb6,0xbd,0xd3,0xb7,0xc4}; //1文字カタカナ(二,ケ,カ,ス,モ,キ,ト)
//char dakuten = 0xde; //1文字の濁点(゛)
//曜日(漢字キャラクタパターン)@2022/12/22追加
byte nichi[8] = { 0b11111, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b11111, 0b00000 }; //日
byte getsu[8] = { 0b01111, 0b01001, 0b01111, 0b01001, 0b01111, 0b01001, 0b10001, 0b00000 }; //月
byte kayou[8] = { 0b00100, 0b10101, 0b10101, 0b00100, 0b01010, 0b01010, 0b10001, 0b00000 }; //火
byte suiyo[8] = { 0b00100, 0b00101, 0b11110, 0b00110, 0b01101, 0b10100, 0b00100, 0b00000 }; //水
byte mokuy[8] = { 0b00100, 0b00100, 0b11111, 0b00100, 0b01110, 0b10101, 0b00100, 0b00000 }; //木
byte kinyo[8] = { 0b00100, 0b01010, 0b10001, 0b01110, 0b10101, 0b01110, 0b11111, 0b00000 }; //金
byte doyou[8] = { 0b00000, 0b00100, 0b00100, 0b01110, 0b00100, 0b00100, 0b11111, 0b00000 }; //土
int d_mon ;
int d_mday ;
int d_hour ;
int d_min ;
int d_sec ;
int d_wday ;
void setup(){
Wire.begin(); // I2C初期化
lcd.begin(16, 2); // ディスプレイの文字数(16)と行数(2)
lcd.setContrast(40); // ディスプレイのコントラスト調整(0~63の範囲で調整)
// BME280初期化
status = bme.begin(0x76);
while (!status) {
lcd.setCursor(0, 0);
lcd.clear();
lcd.print("BME280 failed");
}
lcd.setCursor(0, 0);
lcd.clear();
lcd.print("BME280 connected");
delay(1000);
//---------内蔵時計のJST同期(起動時)--------
wifisyncjst();
//---------漢字_CGRAM登録@2022/12/22追加
lcd.createChar(0, nichi); //日
lcd.createChar(1, getsu); //月
lcd.createChar(2, kayou); //火
lcd.createChar(3, suiyo); //水
lcd.createChar(4, mokuy); //木
lcd.createChar(5, kinyo); //金
lcd.createChar(6, doyou); //土
}
void loop() {
//---------内蔵時計の表示--------
time_t t = time(NULL);
tm = localtime(&t);
d_mon = tm->tm_mon+1;
d_mday = tm->tm_mday;
d_hour = tm->tm_hour;
d_min = tm->tm_min;
d_sec = tm->tm_sec;
d_wday = tm->tm_wday;
lcd.setCursor(0, 0);
lcdzeroSup(d_mon);
lcd.print("/");
lcdzeroSup(d_mday);
lcd.setCursor(6, 0);
// ----- 曜日(英字 or カタカナ表示)
//lcd.print(weekStr[d_wday]);
//濁点追加(ケ゛、ト゛のみ)、英字の場合は91~95行は不要)
//if ((String(d_wday) == "1") || (String(d_wday) == "6")) {
// lcd.print(dakuten);
//} else {
//lcd.print(" "); // 残像消去@2022/12/18追加
//}
// ----- 曜日(漢字表示)@2022/12/22追加
if (String(d_wday) == "0") {
lcd.write(0); //日
} else if (String(d_wday) == "1") {
lcd.write(1); //月
} else if (String(d_wday) == "2") {
lcd.write(2); //火
} else if (String(d_wday) == "3") {
lcd.write(3); //水
} else if (String(d_wday) == "4") {
lcd.write(4); //木
} else if (String(d_wday) == "5") {
lcd.write(5); //金
} else if (String(d_wday) == "6") {
lcd.write(6); //土
}
lcd.setCursor(8, 0);
lcdzeroSup(d_hour);
lcd.print(":");
lcdzeroSup(d_min);
lcd.print(":");
lcdzeroSup(d_sec);
// ----- BME280センサからデータ取得、測定値を10秒毎に表示 -----
if ((String(d_sec) == "0") || (String(d_sec) == "10") ||
(String(d_sec) == "20") || (String(d_sec) == "30") ||
(String(d_sec) == "40") || (String(d_sec) == "50")) {
pressure=bme.readPressure() / 100.0F;
temp=bme.readTemperature();
humid=bme.readHumidity();
}
lcd.setCursor(0, 1);
lcd.print(" "); //気圧が4桁→3桁に変わった時の残像消去
lcd.setCursor(0, 1);
lcd.print(String(pressure,0));
lcd.setCursor(6, 1);
lcd.print(String(temp,1));
lcd.setCursor(12, 1);
lcd.print(String(humid,1));
//---------内蔵時計のJST同期(1日1回、12時30分0秒に実行)--------
if ((String(d_hour) == "12") && (String(d_min) == "30") && (String(d_sec) == "0")) {
wifisyncjst();
}
delay(100);
}
void wifisyncjst() {
//---------内蔵時計のJST同期--------
// WiFi接続
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
lcd.setCursor(0, 0);
lcd.clear();
lcd.print("WiFi bigin");
}
delay(1000);
// WiFi接続の表示
lcd.setCursor(0, 0);
lcd.clear();
lcd.print("WiFi connected");
delay(1000);
lcd.clear();
lcd.print(WiFi.localIP()); //LCD画面にIPアドレス表示@2023/01/15追加
delay(2000);
// NTPサーバからJST取得
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
lcd.setCursor(0, 0);
lcd.clear();
lcd.print("JST synchronized");
delay(1000);
// 内蔵時計の時刻がNTP時刻に合うまで待機
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET) {
delay(1000);
}
//WiFi切断
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
lcd.clear();
}
void lcdzeroSup(int digit){
//---------月、日、時、分、秒が0~9の場合、1桁目を 空白 もしくは 0 に置換--------
if(digit < 10)
lcd.print(' '); // 現在「空白」
lcd.print(digit);
}