こんにちは、ENGかぴです。
ESP32-WROOM-32EのWiFi通信でブラウザーから時刻を送信しRTCモジュールに時刻を設定することができます。NTPサーバーにアクセスする方法など時刻管理の方法は様々ですが、外部の回線に接続しない場合に有効な方法です。
ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。
RTCモジュールはRX8900を搭載したAE-RX8900(秋月電子)使用しています。温度補償発振器(DTCXO)を内蔵しており高精度で最大で月差13秒で時刻を管理できます。時刻データはI2C通信(Wireライブラリ)を使用することで取得することができます。RX8900を以下ではRTCと表記します。
ESP32-WROOM-32Eで動作確認したことについてリンクをまとめています。
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
RTCの時刻データの更新
RX8900モジュールを使用した別の例を下記記事にまとめています。
Seeeduino XIAOでRTCモジュールの情報を更新する
ArduinoでRTCを使用してファイル管理とAPIの実装する
今回は上記2記事とは少し内容を変更して動作確認を行っています。
RTC専用のファイルを作成する
Arduino IDEのプロジェクトファイル表示欄横の▼マークから新規タブを押すと新規ファイル追加することができますが、ここではプロジェクトのフォルダ内にファイルを直接追加する方法を説明しています。
Arduinoプロジェクトとしてメインファイルである「RTC.ino」を新規作成するとフォルダが生成されます。このフォルダの中に以下のファイルを追加します。
- rx8900.ico:RX8900に関する処理を行うファイル
- rx8900.h:RX8900処理に使用する定義をまとめたファイル
これらのファイルをArduinoプロジェクト(RTC.ino)内で作成しArduino IDEを再起動するかArduino IDE内のタグ部分にドロップすることで追加します。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
ファイルの管理の方法
追加したrx8900.icoとrx8900.hの構成を示します。基本的にRTCの操作に関する処理はこれら専用のファイル内で完結するように作ります。
//rx8900.ico
#define RTC_C
#include "rx8900.h"
#undef RTC_C
rx8900.icoファイルでは専用のヘッダーファイルである「rx8900.h」をインクルードする前にRTC_Cを定義しています。コンパイルする手順はコンパイラーに依存するため他のファイルで「RX8900.h」をインクルードしていた場合に重複して変数の定義をしないようにするために実装します。
//-------rx8900.h----------------
#ifndef RX8900_H
#define RX8900_H
//この間に専用の定義を入れる
#endif
#ifdef RTC_C
#define GLOBAL //rx8900.icoの最初でRTC_Cを定義している場合
#else
#define GLOBAL extern //RTC_Cが定義されていなければ外部参照になる
#endif
GLOBAL uint16_t data; //他のファイルでも共通で使用できる変数
#undef GLOBAL
rx8900.hでは一度でも読み込まれているかの確認を行うため#ifndef RX8900_Hの定義の確認を行っています。初めて読み込まれるときはRX8900_Hが定義されていないためRX8900_Hを定義して以降の専用の定義(#endifまで)を行います。
2度目に読み込まれた時はRX8900_Hが定義されているため無視されます。これにより重複した読み込みが発生しません。
次にRTC_Cについてですが、rx8900.icoが読み込まれた時にRTC_Cが定義されるため#define GLOBALが有効になります。GLOBALの後には何も記述がないため空欄と同じ扱いになるので変数の宣言となります。
一方rx8900.ico以外のファイルからrx8900.hが参照された場合はRTC_Hが定義されていないことから#define GLOBAL externが有効になるため外部参照の変数として宣言することになります。
RTCを操作する関数を実装
RTCにデータの読み書きを行うために以下の関数を実装しています。
関 数 | 説 明 |
---|---|
Rx8900begin() | RX8900の初期化を行います。 バックアップ電源が有効であるか判断して時刻の初期化を行います。 |
Rx8900setUint(引数1,引数2) | 引数1に時刻データをセットすると時刻を書き込みます。 引数2に曜日の番号を指定します。 |
Rx8900getDateTime() | 時刻データを引数に指定した変数にセットします。 |
getTemp(引数) | 温度データを引数に指定した変数にセットします。 温度表示するときは換算が必要です。 |
setRegisters(引数,引数2,引数3) | レジスタの設定を変更するときに使用します。 引数には変更するレジスタのアドレスを指定します。 引数2は書き込みするバイト数です。 引数3はデータのアドレスです。 |
getRegisters(引数,引数2,引数3) | レジスタの設定を読み込むときに使用します。 引数には読み込むレジスタのアドレスを指定します。 引数2は読み込むバイト数です。 引数3はデータのアドレスです。 |
実装した関数の使用例は以下の通りとなります。
void setup() {
Rx8900begin();
}
void loop() {
Serial.print(Rx8900getDateTime()); //日時を表示
Serial.print(" ");
Serial.print(Rx8900getTemp());
Serial.println("deg"); //温度を表示
delay(1000);
}
setup()内でRx8900begin()関数をコールすることでRTCの初期化を行います。初期化は各種レジスタのフラグを初期化し時刻更新を通知する割り込みの許可やバックアップ電源が有効であるかの確認を行います。
Rx8900getDataTime()はRX8900から読み込んだ時刻(例:2021-12-16/23:16:30-SAT)を表示します。Rx8900getTemp()ではRX8900が測定している温度を表示します。
関数内の処理の詳細はソースコード全体を確認してください。
RTCの時刻データの指定方法
void Rx8900setUint(uint8_t *dt, uint8_t wk){
dateTime buf;
buf.year = (dt[2] << 4 ) + dt[3];
buf.month = (dt[5] << 4 ) + dt[6];
//省略
switch (wk)
{
case 0:
buf.week = SUN;
break;
case 1:
buf.week = MON;
break;
//省略
}
Rx8900setRegisters(SEC_REG,sizeof(buf),(uint8_t*)&buf);
}
Rx8900setUint()はRTCに指定した時刻を書き込みますが、レジスタはBCDデータで管理されているため10進数をBCDデータに変換する必要があります。
例)引数が10進数で1文字ずつ指定されている場合(2021年の下2桁の21)
dat[0]=2 dat[1] = 1とするとBCDで値を表現すると0x21になります。10進数で表現する場合は33になるため分かりにくくなってしまいます。そのため 時間データを10進数で1桁ずつに分割してBCDデータに変換する方法を採用しています。
曜日は曜日に対するビットをセットすることで判断する使用になっているため取得した曜日データに相当するビットを選択しています。
RTCから時刻データを読み出す際は逆にBCD値を10進数に変換します。
Webサーバーを使用する
Webサーバーライブラリの使い方は下記記事まとめています。ここでは要点のみまとめています。
ESP32-WROOM-32EでWebServerを実装する
#include <WebServer.h>
void setup() {
Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
Wserver.on("/", HTTP_POST, HtmlPost); //URLを指定して処理する関数を指定
Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
Wserver.begin(); //Webサーバーの開始
}
void loop(){
Wserver.handleClient();
}
WebServerライブラリを使用するためWebServer.hをインクルードします。on()関数を使用するとブラウザーで指定したURLで表示するページを分岐させることができます。
on()関数では第1引数にURLアドレスの階層を示す文字、第2引数にHTTPの種類を指定、第3引数に接続要求の処理を行う関数を指定します。
onNotFound()関数はページが存在しない時に表示する関数を指定します。on()関数やonNotFound()関数の指定後はbegin()関数でWebサーバーを開始します。
handleClient()関数でクライアントからのリクエストを常時監視します。クライアントからリクエストがあれば登録したURLに対応する処理が呼び出されます。
void HtmlPost(void){
String str = "";
uint8_t wk;
dateTime tim;
NowTime = Wserver.arg("NowTime"); //時間データを取得
weekday = Wserver.arg("weekday"); //曜日データを取得
wk = weekday.toInt();
if( NowTime.length() > 1 ){ //書き込み
for(uint8_t i=0; i< NowTime.length();i++){
stimData[i] = *NowTime.substring(i).c_str(); //char型に変換
stimData[i] = strtol((char*)&stimData[i],NULL,10); //char→bin変換
}
Rx8900setUint(&stimData[0],wk);
}
//ブラウザーに表示するhtml
}
HtmlPost()はブラウザーから送信したデータを取得したときに呼び出されるようにしています。Webサーバーから取得したNowTimeとweekdayをarg()関数で取得します。NowTimeはString型なのchar型に変換してRTC設定用のデータを生成しています。
PR:アクセンチュアの転職なら【コンサルアクシスコンサルティング】
動作確認
ESP32-WROOM-32EとAE-RX8900(RTC)を組み合わせた配線例を示しています。I2CのSCLとSDAはプルアップする必要がありますがAE-RX8900モジュール内でプルアップ抵抗を有効にすると10kΩでプルアップすることができます。
RX8900モジュールは1秒経過(変更可能)する毎に/INTを出力するため時刻が更新されたタイミングを取得することができます。
RX8900モジュールの7ピン(VBAT)は時刻情報をバックアップしたい場合に電気二重層コンデンサなどのバックアップ電源を接続します。
電源を入れると初期の日付がシリアルモニタに表示されて1秒ごとに更新されていますが、スマホの通信開始ボタンを押すと日付が変更されシリアルモニタの表示が指定した時刻になっていることが確認できました。
更新した日時は電源のUSBを抜いて電源をOFFした後に再び電源をONしてシリアルモニタでRTCの時刻を確認すると電気二重層コンデンサによるバックアップによりデータが保持されていました。
iOSの場合はカレンダー上で一度何か変更して元に戻すことで値が変更されセットされます。例えば19日→20日→19日に戻すなど一度でも操作すると動作するようです。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
メインのソースコード:
#include <Wire.h>
#include <WebServer.h>
#include "rx8900.h"
#define PIN_DI_INT 33
const char *ssid = "EngKapi1"; //SSID
const char *pass = "22223333"; //password
const IPAddress ip(192,168,11,1); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク
/* 変数宣言 */
String NowTime;
String weekday;
WebServer Wserver(80);
/* プロトタイプ宣言 */
void mainApp(void);
void HtmlSet(void);
void HtmlPost(void);
void handleNotFound(void);
void setup() {
pinMode(PIN_DI_INT, INPUT_PULLUP); //RX8900の時刻更新割り込みを受けるDI
Serial.begin(115200);
Rx8900begin();
WiFi.softAP(ssid, pass); //WiFiのアクセスポイントの設定
WiFi.softAPConfig(ip, ip, subnet); //アクセスポイントのIP及びサブネットマスク
Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
Wserver.on("/", HTTP_POST, HtmlPost); //URLを指定して処理する関数を指定
Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
Wserver.begin(); //Webサーバーの開始
}
void loop() {
Wserver.handleClient();
if (digitalRead(PIN_DI_INT)) {
if(rtcmode == RTC_MODE_TYP::RTC_RX_END ){
rtcmode = RTC_MODE_TYP::RTC_IDLE;
}
}else{
if(rtcmode == RTC_MODE_TYP::RTC_IDLE){
rtcmode = RTC_MODE_TYP::RTC_RX;
}
}
switch(rtcmode ){
case RTC_MODE_TYP::RTC_IDLE:
//処理なし
break;
case RTC_MODE_TYP::RTC_RX:
Serial.print(Rx8900getDateTime());
Serial.print(" ");
Serial.print(Rx8900getTemp());
Serial.println("deg");
rtcmode = RTC_MODE_TYP::RTC_RX_END;
break;
case RTC_MODE_TYP::RTC_RX_END:
//タイムアップで異常を検出する場合の処理
break;
}
}
/* クライアントに返信するhtmlデータを生成 */
void HtmlSet(void){
String str = "";
str += "<html lang=\"ja\">";
str += "<head>";
str += "<meta charset=\"UTF-8\">";
str += "<title>RTC8900動作確認</title>";
str += "</head>";
str += "<body>";
str += "<h1>RTC8900動作確認</h1>";
str += "<form method='post'>";
str += "<label>時刻をセットします。</label>";
str += "<input type='datetime-local' id='real_time' name='NowTime'";
str += "style='font-size:1rem; padding:16px;'>";
str += "曜日設定:";
str += "<select size='1' name='weekday'";
str += "style='font-size:1rem; padding:16px;'>";
str += "<option value='0' selected>日</option>";
str += "<option value='1'>月</option>";
str += "<option value='2'>火</option>";
str += "<option value='3'>水</option>";
str += "<option value='4'>木</option>";
str += "<option value='5'>金</option>";
str += "<option value='6'>土</option>";
str += "</select>";
str += "<br><br>";
str += "<input type='submit' value='通信開始'";
str += "style='padding: 16px; min-width: 100px;border-radius: 5px;";
str += "font-family: inherit; background: lightgreen; font-size: 1rem;'>";
str += "</form>";
str += "</body>";
str += "</html>";
Wserver.send(200,"text/html", str);
//HTTPレスポンス200でhtmlデータとして送信
}
/* 送信を押したときに遷移する */
void HtmlPost(void){
String str = "";
uint8_t wk;
dateTime tim;
NowTime = Wserver.arg("NowTime");
weekday = Wserver.arg("weekday");
wk = weekday.toInt();
if( NowTime.length() > 1 ){ //書き込み
for(uint8_t i=0; i< NowTime.length();i++){
stimData[i] = *NowTime.substring(i).c_str();
stimData[i] = strtol((char*)&stimData[i],NULL,10);
}
Rx8900setUint(&stimData[0],wk);
}
str += "<html lang=\"ja\">";
str += "<head>";
str += "<meta http-equiv='refresh' content='2; URL=http://192.168.11.1/'>";
str += "<meta charset=\"UTF-8\">";
str += "<title>Sensor graph</title>";
str += "</head>";
str += "<body>";
str += "<h1>RTCに書き込み中</h1>";
str += "</body>";
str += "</html>";
Wserver.send(200, "text/html", str);
}
/* URLが存在しない場合の処理 */
void handleNotFound(void) {
String message = "File Not Found\n\n";
message += "URI: ";
message += Wserver.uri();
message += "\nMethod: ";
message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += Wserver.args();
message += "\n";
for (uint8_t i = 0; i < Wserver.args(); i++) {
message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
}
Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}
rx8900.icoのソースコード:
#include <Wire.h>
#define RTC_C
#include "rx8900.h"
#undef RTC_C
#define RX8900A_ADRS 0x32
#define SEC_REG 0x00
#define MIN_REG 0x01
#define HOUR_REG 0x02
#define WEEK_REG 0x03
#define DAY_REG 0x04
#define MONTH_REG 0x05
#define YEAR_REG 0x06
#define RAM_REG 0x07
#define MIN_ALARM_REG 0x08
#define HOUR_ALARM_REG 0x09
#define WEEK_DAY_ALARM_REG 0x0A
#define Timer_CNT0_REG 0x0B
#define Timer_CNT1_REG 0x0C
#define EXTENSION_REG 0x0D
#define FLAG_REG 0x0E
#define CONTROL_REG 0x0F
#define TEMP_REG 0x17
#define BACKUP_FUNC_REG 0x18
#define NO_WEEK 0x00
#define SUN 0x01
#define MON 0x02
#define TUE 0x04
#define WED 0x08
#define THU 0x10
#define FRI 0x20
#define SAT 0x40
uint8_t decimalToBCD(uint8_t decimal);
uint8_t BCDToDecimal(uint8_t bcd);
void Rx8900begin(void){
uint8_t extreg;
uint8_t flgreg;
uint8_t flgreg_init;
uint8_t conreg;
dateTime dt = {0, 0, 0, TUE, 1, 10, 20};
extreg = 0x08;
flgreg = 0x00;
conreg = 0x01;
Wire.begin();
delay(1000);
Rx8900setRegisters(EXTENSION_REG, 1, &extreg);//set WEEK ALARM , 1Hz to FOUT
Rx8900getRegisters( FLAG_REG, 1, &flgreg_init );
if( flgreg_init & 0x02 ){ //VLFの確認(バックアップが有効でない)
Rx8900setDateTime(&dt);
}
flgreg = 0x00;
Rx8900setRegisters(FLAG_REG, 1, &flgreg);//reset all flag
conreg = 0x21;
Rx8900setRegisters(CONTROL_REG, 1, &conreg);//reset all flag
}
void Rx8900setRegisters(uint8_t address, int sz, uint8_t *data) {
Wire.beginTransmission(RX8900A_ADRS);
Wire.write(address);
Wire.write(data, sz);
Wire.endTransmission();
}
void Rx8900getRegisters(uint8_t address, int sz, uint8_t *data) {
Wire.beginTransmission(RX8900A_ADRS);
Wire.write(address);
Wire.endTransmission();
Wire.requestFrom(RX8900A_ADRS, sz);
for (int i = 0; i < sz; i++) {
data[i] = Wire.read();
}
}
void Rx8900setDateTime(dateTime *dt){
uint8_t data[7];
data[0] = decimalToBCD(dt->second);
data[1] = decimalToBCD(dt->minute);
data[2] = decimalToBCD(dt->hour);
data[3] = dt->week; //Weekのみ各ビットを立てる
data[4] = decimalToBCD(dt->day);
data[5] = decimalToBCD(dt->month);
data[6] = decimalToBCD(dt->year);
Rx8900setRegisters(SEC_REG,7,data);
}
void Rx8900setUint(uint8_t *dt, uint8_t wk){
dateTime buf;
buf.year = (dt[2] << 4 ) + dt[3];
buf.month = (dt[5] << 4 ) + dt[6];
buf.day = (dt[8] << 4 ) + dt[9];
buf.hour = (dt[11] << 4 ) + dt[12];
buf.minute = (dt[14] << 4 ) + dt[15];
buf.second = 0;
switch (wk)
{
case 0:
buf.week = SUN;
break;
case 1:
buf.week = MON;
break;
case 2:
buf.week = TUE;
break;
case 3:
buf.week = WED;
break;
case 4:
buf.week = THU;
break;
case 5:
buf.week = FRI;
break;
case 6:
buf.week = SAT;
break;
default:
break;
}
Rx8900setRegisters(SEC_REG,sizeof(buf),(uint8_t*)&buf);
}
void Rx8900getDateTime(struct dateTime *dt) {
uint8_t data[7];
Rx8900getRegisters(SEC_REG, 7, data);
dt->second = BCDToDecimal(data[0] & 0x7f);
dt->minute = BCDToDecimal(data[1] & 0x7f);
dt->hour = BCDToDecimal(data[2] & 0x3f);
dt->week = data[3];
dt->day = BCDToDecimal(data[4] & 0x3f);
dt->month = BCDToDecimal(data[5] & 0x1f);
dt->year = BCDToDecimal(data[6]);
}
String Rx8900getDateTime(void) {
String ret="";
uint8_t data[7];
char str[7];
Rx8900getRegisters(SEC_REG, 7, data);
sprintf(str,"20%02d-",BCDToDecimal(data[6] & 0x7f)); //year
ret.concat(str);
sprintf(str,"%02d-",BCDToDecimal(data[5] & 0x1f)); //month
ret.concat(str);
sprintf(str,"%02d/",BCDToDecimal(data[4] & 0x3f)); //day
ret.concat(str);
sprintf(str,"%02d:",BCDToDecimal(data[2] & 0x3f)); //hour
ret.concat(str);
sprintf(str,"%02d:",BCDToDecimal(data[1] & 0x7f)); //minite
ret.concat(str);
sprintf(str,"%02d-",BCDToDecimal(data[0] & 0x7f)); //second
ret.concat(str);
switch (data[3])
{
case SUN:
ret.concat("SUN");
break;
case MON:
ret.concat("MON");
break;
case TUE:
ret.concat("TUE");
break;
case WED:
ret.concat("WED");
break;
case THU:
ret.concat("THU");
break;
case FRI:
ret.concat("FRI");
break;
case SAT:
ret.concat("SAT");
break;
default:
Serial.print("NG");
break;
}
return ret;
}
float Rx8900getTemp(void) {
float ret;
uint8_t temp;
Rx8900getRegisters( TEMP_REG, 1, &temp);
ret = ((float)temp * 2 - 187.19)/ 3.218;
return ret;
}
uint8_t decimalToBCD(uint8_t decimal) {
return (((decimal / 10) << 4) | (decimal % 10));
}
uint8_t BCDToDecimal(uint8_t bcd) {
return ((bcd >> 4) * 10 + (bcd & 0x0f));
}
rx8900.hのソースコード:
#ifndef RX8900_H
#define RX8900_H
struct dateTime {
uint8_t second; // 0-59
uint8_t minute; // 0-59
uint8_t hour; // 0-23
uint8_t week; // 0(Sun)-6(Sat)
uint8_t day; // 1-31
uint8_t month; // 1-12
uint8_t year; // 00-99
};
enum RTC_MODE_TYP{
RTC_IDLE = 0,
RTC_RX,
RTC_RX_END,
RTC_WRITE,
RTC_MODE_MAX
};
#endif
#ifdef RTC_C
#define GLOBAL
#else
#define GLOBAL extern
#endif
GLOBAL uint8_t stimData[20];
GLOBAL RTC_MODE_TYP rtcmode;
GLOBAL void Rx8900begin(void);
GLOBAL void Rx8900setRegisters(uint8_t address, int sz, uint8_t *data);
GLOBAL void Rx8900getRegisters(uint8_t address, int sz, uint8_t *data);
GLOBAL void Rx8900setDateTime(dateTime *dt);
GLOBAL void Rx8900setUint(uint8_t *dt, uint8_t wk);
GLOBAL void Rx8900getDateTime(struct dateTime *dt);
GLOBAL String Rx8900getDateTime(void);
GLOBAL float Rx8900getTemp(void);
#undef GLOBAL
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR: わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジムPython入門講座の申込
最後まで、読んでいただきありがとうございました。