ESP8266でRTCを動かす

前回までの5回でようやくTWELITEでI2C通信を使って温湿度センサーのBME280を動かすことができ、I2Cで通信するめどが立ったので、今度はいよいよRTCを動かしてみることにしました。
alasixosaka.hatenablog.com

といっても、いきなりTWELITEで動かすのは何となく不安なので、まずはやりやすいESP8266で動かしてみることにします。どのみち、ESP8266でもRTCを動かす必要があるので、そんなに回り道にならないと思っています。とはいいながら、今回も地雷踏みまくりでいろいろとありました。備忘録代わりにブログを書いているので、いろいろとはまったところを書いていくことにします。
ESP8266はとりあえず、使いやすい開発ボード(NodeMCU)を使いました。

RTCモジュールは手持ちのRTC-8564NBを使いました。
秋月電子から購入したこちらのもの。
akizukidenshi.com
ESP8266はArduino IDEでプログラムできるし、ESP8266を使ったプログラムもいろいろと公開されている。しかし、専用のライブラリをインポートして使っているプログラムが多く、今後TWELITEでもプログラムすることを考えると、直接I2C通信でプログラムしている方が読みやすいので、こちらのサイトを参考にした。
www.petitmonte.com
こちらはArduinoのプログラムなので、ESP8266でそのまま動くとは限らないが、I2Cの部分さえ気を付ければ問題なく動くと思っていた。ところが、全然動かない。理由は

ESP8266のGPIOは入力に使えない端子が結構ある

ということです。プログラムは、リアルタイムクロックのクロック出力を使って1秒ごとに割り込みをかけて、割り込み処理を行っています。Arduinoではピン入力割り込みに使えるピンが限られていて、2番ピンを使っています。ところが、ESP8266では2番ピンは入力としては使えないのです。
プログラムは正常に書き込めるのですが、暴走しているらしくウォッチドックタイマーがかかって再起動するという動作を繰り返してしまいます。
Serial.println("xxxx")を適当に入れて、どこまで動くかを確認してようやく、ピン入力の設定の所で暴走しているのを見つけました。そのあと色々調べると、こちらのサイトに詳しく書いてありました。
kurobekoblog.com
何も考えすに入力に使えるのは、4,5,12,13,14の5つだけということです。上記のサイトで青や緑の印のついているピンは使わない方が良いということのようです。ですので、2番ピンを入力に設定してしまったがために暴走をしていたようです。
まあ、安直に他人のプログラムを使ってすぐに動かそうとしたためにかえってはまってしまうというよくあるパターンといえばそうなんですが。
なので、初心に帰って、データシートをちゃんと読んで、理解したうえでプログラムをし直すことにしました。幸いなことに、RTC-8564NBは日本のエプソン製なので、データシートも日本語で読みやすかったですが。

I2Cアドレスの謎

そして、次に引っかかったのがI2Cアドレスの設定でした。データシートには、I2Cアドレスは、readがA3でwriteがA2と書いてある。先のArduinoのプログラムだと、Arduinoでは右に1ビットシフトしたアドレスを使うと書いてある。温度センサーBME280を使ったときは何も意識せずアドレスは0x76を使ったが、1ビットシフトとはどういうこと??? となってしまった。そこで、こちらも初心に帰って、I2C通信の仕組みをもう一度調べてみた。
tool-lab.com
こちらのサイトを読んでようやく納得がいった。要するに7ビットのアドレスに1ビット追加して、それを1とするか0とするかで、Read、Writeを区別しているということで、それを込みで書くとエプソンのデータシートの表記になり、それはあらかじめ織り込み済みで7ビットで書くとBME280のような表記になるということのようだ。そんなことは知っている人には常識なのかもしれないが、いままでI2CアドレスでReadとWriteで2通りの表記をされているのを見なかったので戸惑ってしまった。ということで、7ビットで表記すると、RTC-8564NBのI2Cアドレスは、0x51ということになる。
レジスタの使い方はデータシートでなく、アプリケーションノートを参照した。どちらも、秋月電子のサイトからダウンロードできる。
配線はこんな感じになります。秋月のRTC-8564NBは、I2Cバス用のプルアップ抵抗がオンボードで実装されていますが、はんだジャンパーを繋ぐ必要があり、面倒なので外付けの抵抗を繋いでいます。また、ESP8266側のSDAは4番ピン、SCLは14番ピンとして、プログラム中で設定しています。

プログラムの全文はこちら。参考サイトのプログラムで使える部分は最大限使わせていただいた。特に、RTCからデータを読み込んでシリアル出力する部分はほとんどそのままになっている。プログラムの動作は、まず、RTCに適当な日時を書き込んで、50秒間、日時を表示し、ディープスリープするという動作をします。

#include <Wire.h>
 
// RTCのレジスタテーブル(16byte)
int RegTbl[16];  
 
// デバイスアドレス(スレーブ)
// ※Arduinoの仕様では8bitのアドレスを右に1bitシフトした「7bit」を使用する
// ※[8bit]Write : 0xA2 = 10100010 Read : 0xA3 = 10100011
byte DEVICE_ADDRESS= 0x51;  
 
// 2進化10進数(BCD)を10進数に変換
byte BCDtoDec(byte value){
  return ((value >> 4) * 10) + (value & 0x0F) ;
}
 
void init_RTC_8564(void){
  Serial.println("write start");
  Wire.beginTransmission(DEVICE_ADDRESS);
  Wire.write(0x00);  //Cotrol Reg1
  Wire.write(0x20);  //Stop RTC
  Wire.write(0x02);  //Control Reg2
  Wire.write(0x00);  //0 second
  Wire.write(0x00);  //0 min
  Wire.write(0x10);  //0 hour
  Wire.write(0x05);  //1st
  Wire.write(0x02);  //Tuesday
  Wire.write(0x11);  //November
  Wire.write(0x22);  //2022
  Wire.write(0x01);  //Alarm min
  Wire.write(0x80);  //Alarm hour not 
  Wire.write(0x80);  //Alarm day not
  Wire.write(0x80);  //Alarm weekday not
  Wire.write(0x00);  //Stop interval timer
  Wire.write(0x00);  //interval timer counter
  Wire.write(0x00);  //not clockout
  Wire.endTransmission();
  delay(50);
  Wire.beginTransmission(DEVICE_ADDRESS);
  Wire.write(0x00);  //control reg1
  Wire.write(0x00);  //start RTC
  Wire.endTransmission();
  //delay(50);  
}
 
void setup() {
  Serial.begin(115200);  
  Serial.println("start");
 
  // マスタとしてI2Cバスに接続する
  Wire.begin(4,14); 

  init_RTC_8564();
  Serial.println("start RTC");
  delay(500);
}
 
void loop(){
  int i;
  
  for (int j=0;j<50;j++){
    // レジスタのアドレスを先頭にする
    Wire.beginTransmission(DEVICE_ADDRESS);
    Wire.write(0x00);
    Wire.endTransmission(); 
 
    // I2Cスレーブに16byteのレジスタデータを要求する
    Wire.requestFrom(DEVICE_ADDRESS,16);
                
    // 16byteのデータを取得する
    for (i=0; i<16; i++){
      while (Wire.available() == 0 ){}
      RegTbl[i] = Wire.read();
    }
    
    // 現在日時
    Serial.print("現在:");
    Serial.print(String(BCDtoDec(RegTbl[8])+ 2000)  + "年");
    Serial.print(String(BCDtoDec(RegTbl[7] & 0x1F)) + "月");
    Serial.print(String(BCDtoDec(RegTbl[5] & 0x3F)) + "日");
     
    switch(RegTbl[6] & 0x07){
      case 0 :  Serial.print("(日)");break;
      case 1 :  Serial.print("(月)");break;
      case 2 :  Serial.print("(火)");break;
      case 3 :  Serial.print("(水)");break;
      case 4 :  Serial.print("(木)");break;
      case 5 :  Serial.print("(金)");break;
      case 6 :  Serial.print("(土)");break;
    }
    Serial.print(" ");
    Serial.print(String(BCDtoDec(RegTbl[4] & 0x3F)) + "時");
    Serial.print(String(BCDtoDec(RegTbl[3] & 0x7F)) + "分");
    Serial.print(String(BCDtoDec(RegTbl[2] & 0x7F)) + "秒");
    
    // アラーム日時
    Serial.print(" アラーム:");
    if((RegTbl[11] & 0x80) == 0){
      Serial.print(String(BCDtoDec(RegTbl[11] & 0x3F)) + "日");
    }
    
    if((RegTbl[12] & 0x80) == 0){
      switch(RegTbl[12] & 0x07){
        case 0 :  Serial.print("(日) ");break;
        case 1 :  Serial.print("(月) ");break;
        case 2 :  Serial.print("(火) ");break;
        case 3 :  Serial.print("(水) ");break;
        case 4 :  Serial.print("(木) ");break;
        case 5 :  Serial.print("(金) ");break;
        case 6 :  Serial.print("(土) ");break;
      }    
    }
    
    if((RegTbl[10] & 0x80) == 0){
      Serial.print(String(BCDtoDec(RegTbl[10] & 0x3F)) + "時");
    }
    
    if((RegTbl[9] & 0x80) == 0){
      Serial.print(String(BCDtoDec(RegTbl[9] & 0x7F)) + "分");
    }
    
    Serial.println("");
    state = !state;  
    delay(1000);
  }
  Serial.println("Go to Sleep");
  ESP.deepSleep(0);
  delay(1000); 
}

Setupの所で、Wire.begin(4,14); として、I2Cに使うピンを指定しています。また、init_RTC_8564();として、RTC-8564の初期化関数を呼び出しています。
init_RTC_8564()では、16あるレジスタを設定しています。各レジスタは下記のようになります。

コントロールレジスタ

レジスタはアドレス00がコントロールレジスタ1で、こいつには有効なビットが1つしかありません。第5ビットを1にするとクロックが停止します。また、テスト用の第3ビットと第7ビットは必ず0にする必要があります。まず、第5ビットを1にしてクロックを停止して各レジスタに書き込みを行いますので、0x20を書き込みます。

コントロールレジスタ

次がコントロールレジスタ2です。これは各種の割り込みイベントの設定をするところです。RTC-8564は定周期割り込みと指定した日時に発生するアラーム割り込みの2種類があります。まず第4ビットのTI/TPは定周期割り込みを繰り返すか1回かぎりにするかの設定で、1で繰り返し、0で1回限りになります。
定周期割り込み関連は他に第2ビットのTFと第0ビットのTIEがあり、TFは割り込み発生フラグで発生すると1になります。TIEはINT端子の制御用で、1にすると割り込み発生時にINT端子が"L"になります。アラーム割り込み関連のビットは第3ビットのAFと第1ビットのAIEで、それぞれTFとTIEと同じような働きをします。今回は、定周期割り込みは使わず、アラーム割り込みを使うので、0x02としました。

時、分、秒レジスタ

アドレス02が秒、03が分、04が時のそれぞれレジスタになります。レジスタに与えるデータはBCDコードで記述します。また、秒レジスタの第7ビットはVLで電圧低下を検知するとこのビットが1になります。10時0分0秒に設定したので、秒が0x00、分が0x00、時が0x10としました。

年、月、日レジスタ

アドレス08が年、07が月、05が日のレジスタになります。この3つのレジスタは連続していないので注意が必要です。なぜか、06が曜日のレジスタになっているからです。どうしてなのかはわかりませんが。はじめ、それに気づかず連続して日、月、年と書き込んだら月と年がおかしくてあれっとなってしまいました。
これらのレジスタBCDで書き込みます。2022年11月5日としたので、年が0x22、月が0x11、日が0x05としました。
また、月レジスタの第7ビットは"C"で、世紀が変わると1になるそうです。まあ、生きているうちになることはないですが。

曜日レジスタ

先ほど書いたように、アドレス06が曜日のレジスタで、0が日曜日で6が土曜日です。火曜日を設定したので、0x02を設定。

アラームレジスタ

アドレス09から4つがアラームレジスタで、順に、分、時、日、曜日となっています。各レジスタの第7ビットは”AE"でこれを1にするとこれらのアラームは機能しません。従って、全部のAEを1にするとアラームが機能しないということになります。また、一番短いアラーム間隔は分レジスタでしか指定できませんので、1時間に1回のアラームが最小間隔ということになります。それより短い間隔なら定周期タイマを使いなさいということでしょう。例えば、毎時0分にアラームを出すためには、分レジスタを0にして、他の3つのレジスタの第7ビットを立てます。アラームは分のみを有効にして0x01としました。残り3つは全部無効で0x80です。10時0分0秒からクロックがスタートするので、スタートから1分後にアラームが発生します。

定周期タイマレジスタ

アドレス0Eは定周期タイマ用のレジスタで、有効なビットは3つだけです。第7ビットが”TE"で1にすると定周期タイマON、0でOFFとなります。第0ビットと第1ビットは定周期にタイマのカウントに使うパルスの間隔を指定するもので、値に応じて、4096Hz、64Hz、1Hz、1/60Hzを選択できます。定周期タイマは使わないので、0x00としました。

定周期タイマ用カウンタレジスタ

アドレス0Fは定周期タイマ用のダウンカウントの値を入力するレジスタです。8ビットですので最大は255です。ここも使わないので0x00としました。

ロックアウト制御レジスタ

アドレス0Dは、クロックアウト端子から出力されるクロックの周期を制御します。第7ビットが”FE"で1の時クロックアウトが有効になります。第1ビットと第0ビットで指定した4通りの周期から選択でき、32768Hz、1024Hz、32Hz、1Hzから選択できます。ちなみに、参考にしたサイトのプログラムはクロックアウトを1Hzに設定して、そのクロックで1秒ごとに割り込みをかけていました。ここも使わないので、0x00としました。

自分のプログラムでは、まず、コントロールレジスタ1の”STOP"を1にして、コントロールレジスタ2の”AIE"を1にして、アラーム割り込みを有効にしています。
そして、11月5日火曜日の10時0分0秒に時刻を設定し、アラームは分レジスタのみ有効にして1を入れています。つまり、起動後1分でアラーム割り込みがかかるようにしています。定周期割り込み、クロックアウトは無効にしています。
レジスタを設定した後、コントロールレジスタ1に0を書き込んでRTCをスタートさせています。
そのあとループ関数に入って、RTCからデータを読んでシリアルに出力、1秒休止を50回繰り返した後、ディープスリープに入ります。ディープスリープコマンドの後のディレイはスリープに入る前の時間稼ぎです。PICでNOPを一つ書くのと同じような感じですね。
シリアルには1秒ごと年月日と時分秒、アラームの時刻が表示されます。

ESP8266のリセット失敗

最後の地雷は、これでした。RTC-8564はアラームが発報されるとINT端子が”L"になります。これで、ESP8266をディープスリープから起こそうと思ったのですが、ものの見事に失敗。いろいろなサイトには、RST端子を”L"にするとディープスリープから起動するように書いてあったのですが、実は一旦”L"にしておいて、”H"に戻さないと再起動しません。このことは、開発ボードなどRSTボタンのついている場合、ボタンを押してみるとわかりますが、押しただけでは動作が停止するだけで、離した瞬間に再起動がかかります。これは開発ボードの特性なのかと思って、他の手持ちのESP8266でも念のため調べてみましたが結局同じでした。
もう一つおまけの地雷として、手持ちのESP8266ボードを3つ試してみたのですが(ほかにもまだあったのですがとりあえず3つ)、そのうちちゃんと動いたのは一つだけでした。何故なのかはよくわかりません。昔のことなのでよく覚えていないのですが、確実に動かなかった一つも過去には動いていた記憶があるので、壊れてしまったのか、壊してしまったのか。そして、Amazonで買ったUSBシリアル変換器で動かそうとしましたが3.3Vの容量が足りないらしく、プログラムの書き込みまでは何とか出来るのですがそこからちゃんと起動してくれません。3.3VのACアダプタを繋ぐことで1つだけはちゃんと動いてくれました。ほかの2つもプログラムの書き込みはエラーなく終了するのですが、何故かちゃんと起動してくれませんでした。
それはさておき、ESP8266をスリープから起こすためには、消費電流は大きくなるが、立下りエッジを検出できるLight Sleepにするか、Deep Sleepのときに、エッジ検出してパルスを出す回路を工夫するかということになります。調べてみると、単安定マルチバイブレータというのがエッジ検出でパルス発生ができるとのこと。単安定マルチバイブレータトランジスタ2個と抵抗、コンデンサで構成できるみたいだが、ネットで調べた限り、トランジスタの回路では、立下りを検出して、”H"から”L"そして”H"へ戻る出力を出す回路は見つけられなかった。その逆なら簡単にできそうなのだけど。逆の論理も回路を工夫すればできるような気がするのだが、どうもアナログ回路(こういうのをアナログと言っていいのかちょっとよくわからないが)は苦手で自分で回路を組むとかできそうにない。製品として売っているものなら、エッジ検出も立ち上がり、立下り、両方に対応していて、出力パルスも逆極性を選択できるのがある。
例えば、こんなやつ。
www.marutsu.co.jp
確かにCMOSで消費電流も非常に低い、でも、わざわざ買うのもと思っていたら、別のことを思いついた。実際の回路には高級なチップであるTWELITEがもう一つ載っているということ。それなら、TWELITEでエッジ検出をしてやって、ESP8266をTWELITEから起こしてやればいいのでは。またTWELITEのプログラムが面倒になるが、まあ何とかなるでしょう。
さて次回は、ESP8266のWiFi機能を使ってNTPサーバーから時刻を取得してRTCに書き込むことをやってみます。