ESP32-WROOM-32Eを使用してRTCの時間を設定する

組み込みエンジニア

こんにちは、ENGかぴです。

ESP32-WROOM-32EのWiFi通信でブラウザーから時刻を送信しRTCモジュールに時刻を設定することができます。NTPサーバーにアクセスする方法など時刻管理の方法は様々ですが、外部の回線に接続しない場合に有効な方法です。

RTCモジュールはRX8900を搭載したAE-RX8900(秋月電子)使用しています。温度補償発振器(DTCXO)を内蔵しており高精度で最大で月差13秒で時刻を管理できます。時刻データはI2C通信(Wireライブラリ)を使用することで取得することができます。RX8900を以下ではRTCと表記します。

RTCの時刻データの更新

RX8900モジュールを使用した別の例を下記記事にまとめています。

Seeeduino XIAOでRTCモジュールの情報を更新する

ArduinoでRTCを使用してファイル管理とAPIの実装する

今回は上記2記事とは少し内容を変更して動作確認を行っています。

RTC専用のファイルを作成する

Arduino IDEのプロジェクトファイル表示欄横の▼マークから新規タブを押すと新規ファイル追加することができますが、ここではプロジェクトのフォルダ内にファイルを直接追加する方法を説明しています。

プロジェクトファイルにファイルを追加
プロジェクトファイルにファイルを追加

Arduinoプロジェクトとしてメインファイルである「RTC.ino」を新規作成するとフォルダが生成されます。このフォルダの中に以下のファイルを追加します。

  1. rx8900.ico:RX8900に関する処理を行うファイル
  2. rx8900.h:RX8900処理に使用する定義をまとめたファイル

これらのファイルをArduinoプロジェクト(RTC.ino)内で作成しArduino IDEを再起動するかArduino IDE内のタグ部分にドロップすることで追加します。

ファイル名の後にファイルの種別である.cpp(もしくは.c)や.hを入力する際「.」の前にスペースがあるとArduinoプロジェクトをリロードした際に読み込みが無視されることがあります。

ファイルの管理の方法

追加した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で表示するページを分岐することができます。

第1引数にURLアドレスの階層を示す文字、第2引数にHTTPの種類を指定、第3引数に接続要求の処理を行う関数を指定します。

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設定用のデータを生成しています。

Code Camp完全オンラインのプログラミング個人レッスン【無料体験】

動作確認

動作確認用の回路図
動作確認用の回路図

ESP32-WROOM-32EとAE-RX8900(RTC)を組み合わせた配線例を示しています。I2CのSCLとSDAはプルアップする必要がありますがAE-RX8900モジュール内でプルアップ抵抗を有効にすると10kΩでプルアップすることができます。

RX8900モジュールは1秒経過(変更可能)する毎に/INTを出力するため時刻が更新されたタイミングを取得することができます。

RX8900モジュールの7ピン(VBAT)は時刻情報をバックアップしたい場合に電気二重層コンデンサなどのバックアップ電源を接続します。

動作確認(スマホの表示)
動作確認(スマホの表示)
シリアルモニタでの動作確認
シリアルモニタでの動作確認

電源を入れると初期の日付がシリアルモニタに表示されて1秒ごとに更新されていますが、スマホの通信開始ボタンを押すと日付が変更されシリアルモニタの表示が指定した時刻になっていることが確認できました。

更新した日時は電源のUSBを抜いて電源をOFFした後に再び電源をONしてシリアルモニタでRTCの時刻を確認すると電気二重層コンデンサによるバックアップによりデータが保持されていました。

Androidスマホの場合はhtml内のdatetime-localが正常に動作するため時刻データがセットされますが、iOSの場合動作が不完全で時刻をカレンダー上で設定しなければ「有効な値ではありません」となり時刻をセットすることができません。

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で学べるソフト開発と標準ライブラリの使い方

テックジム|ゼロからはじめるPython入門講座の申込

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました