TWELITEの消費電流を測定する

今回は電子工作の疑似電波時計制作の続きです。年内にはだいたいの所を完成させたいと考えていましたがちょっと遅れ気味です。まだ、年賀状も書いてないし、大掃除もあるしということで、年末の休みは電子工作に使える時間もあまりなさそうなのでちょっとあきらめの心境になってきました。本当は春節になる前に回路とソフトを完成させて基板発注をしたいと思っていたのですが、それも厳しくなりそうな気配です。でも、中途半端に未完成の状態で見切りで基板を発注してもそのまま動かなかったら無駄になるだけなので、きっちり動くことが確認できてからにしようと思っています。そうすると、基板完成が春節明けになってしまい、完成は春頃という感じでしょうか。まあ、のんびりやっていくことにします。
さて、前回は、時刻サーバーから送られてくるデータを、TWILITE STICKで受け取るということをやりました。
alasixosaka.hatenablog.com

しかし、loop関数内に、ウェイトループを置いてしまうと受信がうまくできないことが判明し、ウェイトループによる時間待ちを諦めることになりました。
TWILITEのHPの資料によると、loop関数はその中に居続けるわけでなく、処理が終わるとDOZEモードに入って、割り込み待ちをするとのことでした。DOZEモードの消費電流は資料によるとCPUクロックが最速の32MHzの時でも2mAということで、まあこれくらいなら丸一日くらい待機していても乾電池駆動で行けそうな気がします。そこで、今回は実際にどのくらいの消費電流なのかを調べてみました。
消費電流の測定は、以前購入したμアンペアオシロを使いました。
alasixosaka.hatenablog.com
測定するにあたって、STICKでは余分な回路がついている上に、LEDもあるので、ほぼ同じプログラムをDIPに書き込んで測定してみました。ほぼ同じというのは、STICK用のライブラリをインクルードしないということが違いです。冒頭のインクルードをコメントアウトします。

//#include <MONOSTICK>

また、Setupの中のLEDの設定もコメントアウトします。

//brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
//brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)

これで測定してみました。

DOZEモードに入らない

まず、時刻サーバーを起動しない状態、つまり、永久に受信待ちになる状態で測定しました。そうすると、だいたい17mA程度消費したままの状態が続きます。ちょっと話が違うぞと思いながら、時刻サーバーを起動すると、スリープに入って電流値がほぼ0になりました。そしてスリープから目覚めると電流値が跳ね上がります。電流値の差が大きいのでグラフではわかり難いのでログのデータを載せておきます。

"7826","-65.5174"
"7827","-65.6128"
"7828","719.8335"
"7829","6745.9114"
"7830","15136.2437"
"7831","17390.8254"
"7832","17293.5506"
"7833","17345.3352"
"7834","17409.3267"
"7835","17421.4384"
"7836","17425.1577"

数字の左が、測定開始からの時間、右が電流値です。スリープ中は値がマイナスになっています。単位はμAです。マイナスはあり得ないのですが、それくらい少ないということでしょう。そして、スリープから復帰して、電流値が17mAくらいになります。この状態で受信待ちをし、シグナルを受信したら電流値が20mAくらいまで上がって、またスリープに入るので電流値がマイナスになります。

"7919","17639.9252"
"7920","11461.7361"
"7921","20074.0838"
"7922","20089.4380"
"7923","20243.7425"
"7924","15929.4147"
"7925","15913.1069"
"7926","20079.2337"
"7927","20139.9827"
"7928","20246.2220"
"7929","17893.1258"
"7930","0.8583"
"7931","-61.7981"

電流値が一時的に増えるのは、シリアル通信をするからなのか、無線を受信するからなのかのどちらか(あるいは両方)だと思われます。
いずれにせよ、このプログラムではどうやらDOZEモードには入っていないらしいということがわかりました。DOZEモードに明示的に入れるコマンドがあるのかと思い探してみましたが、C/C++用のコマンドは見つけられたのですが、今回はプログラムしやすいMWXライブラリを使っていて、こちらでは見つけられませんでした。loop関数に何も処理が書いてないのが悪いのかと思って、何か処理を書いてみましたが、結果は同じで、DOZEモードに入る気配はありませんでした。
ということで、単なる待機状態で大量に電流を消費してしまいますので、このままではちょっとまずいことになりました。もちろん、トランジスタスイッチなどを使ってPIC側から強制的にTWELITEの電源を切ってしまうという手段もあるのですが、せっかくプログラムができて、スリープに入れることができるのに、なんかなあ、という感じで気が進みません。

PICとシリアル通信してスリープに入れる

そこで、どうせTWELITEからPICへは時刻データをシリアル通信で送るのだから、PIC側で時間を管理して、一定時間たっても時刻データが送られてこなかった場合は受信失敗とみて、PICからTWELITEにスリープするように命令を出すということを考えました。
まあ、うまくいくかどうかわからないですが、とりあえずやってみることにします。
その結果はまた次回。

久しぶりの柳谷観音

昨日(12/24)久しぶりに柳谷観音に行ってきました。
観音様に行ってきたというよりも、そこまで自転車で走ってきただけなんですが。ですので参拝はしてません。
職場の仲間と島本駅のロータリーで待ち合わせて、登ってきました。本当に久しぶりだったので道もすっかり忘れてしまっていました。いつもトレーニングで登る萩谷と同じくらいかと思っていましたが、もうちょっと登りごたえがありましたね。まあ、走りなれてないということもあってペースがつかめなかったというのもあったのかもしれませんが。
前日(12/23)は死ぬほど寒かったのでどうなることかと思っていましたが、幸い、少し寒さも緩み、風もあまりなく、日差しもあって、それほど寒さは感じませんでした。路面も1か所だけ凍っているところがありましたが、それ以外は濡れていても凍っているところはなく無事に走ることができました。
でも、基本的に寒いのは苦手なので、小径車で行って、帰りは島本駅から輪行して帰ってきました。
行きは西国街道をこれも久しぶりに走ったのですが、相変わらず狭いわりに車が結構通るので走りにくいですね。今、新名神を工事していて、関係あるのかどうかわかりませんが、JRの線路沿いに新しい道路ができつつあります。早くできないかなと思ってしまいました。
自転車は今年はこれで走りおさめです。
ランの方は年末にもう一回どこかに走りに行きたいと思っていますが、また寒波が来そうなのでどうなることやら。

時刻サーバーからのデータをTWELITEで受信する

前回は、RTC-8564NBからI2Cでデータを読み取って、TWELITEで無線送信するところまでやりました。
alasixosaka.hatenablog.com

今回は、無線の受け側(親機)の話です。
まずは、簡単にプログラムするために、TWELITE STICKを使って受信することをやりました。
親機用のプログラムも、BME280のテストの時使ったプログラムをベースに少し改良するだけでできます。
BME280のデータを解析してシリアル出力していた部分を下記のように変更します。
今回はアプリの固有4文字が”RTCC”なので、RTCCが送られてきますので、if分で”RTCC”を判定して、RTCCだったら以下の処理を行います。
パケットが秒、分、時、日、月、年、曜日の順に送られてきますので、その順に各変数で受け取ります。
それを所定の形式で出力するだけです。

if 	(!b_handled && 
		(  !strncmp((char*)fourchars, "RTCC", 4) // BMx280		
		)
	)
{
	b_handled = true;

	uint8_t u8sec;
	uint8_t u8min;
	uint8_t u8hour;
	uint8_t u8day;
	uint8_t u8month;
	uint8_t u8year;
	uint8_t u8wday;

	np = expand_bytes(np, rx.get_payload().end()
		, u8sec
		, u8min
		, u8hour
		, u8day
		, u8month
		, u8year
		, u8wday
	);

	if (np != nullptr) {
		Serial 	<< format("%02x:%02x:%02x", u8hour, u8min, u8sec)
				//<< " Temp:" << div100(int16_t(u16temp))
				<< crlf
				<< format("%02x/%02x/%02x/%01x", u8year, u8month, u8day, u8wday)
				<< crlf
				;
	} else {
		Serial << ".. error ..";
	}
}

ただ今回はここからが問題でした。

TWELITEをスリープする

基本的にTWELITEの親機はスリープしないように使用するのが一般的ですが、今回は時刻合わせのためだけにTWELITEを使うので、1回/日程度通信できれば良い。そして、時計は電池で駆動するので不要な時はスリープさせておきたい。ということで、スリープをさせることにします。
TWELIETの親機用のサンプルアクトの説明には、ウォッチドッグタイマが1分でかかるというように書いてあります。つまり、無限ループなどに1分以上いるとウォッチドッグタイマーにかかってしまってリセットされてしまいます。ただ、時刻サーバーからはだいたい2秒間隔でデータが送られてくるので、5秒待ってデータが来なかったらスリープするというようにしようと考えました。TWELIETにはmilles()という関数があり、起動からの時間をmsで16ビットでカウントする関数があります。これを読んで、次のように、5秒間ループし、その間にデータが来なければ5秒間スリープするということを考えました。

int now = int(millis()&0xFFFF);
while(int(millis()&0xFFFF) < (now + 5000)){

}
the_twelite.sleep(5000);

ところが、これを実行すると、スリープが実行されず、リセットがかかってプログラムが最初から走ってしまいます。リセットがかかる理由はウォッチドッグタイマー以外に考えにくいので、とりあえずオフにしてみました。

ウォッチドッグタイマーをオフにする

Stick用のプログラムの説明にはウォッチドッグタイマーをオフにするやり方が書いてあります。通常は、

pinMode(PIN_WDT_EN, OUTPUT_INIT_LOW);

のように、PIN_WDT_ENをLOWにしておくところを、次のようにHIGHにしておくと、ウォッチドッグタイマーが働かなくなります。

pinMode(PIN_WDT_EN, OUTPUT_INIT_HIGH);

PIN_WDT_ENとは何ぞやというところですが、とりあえずマニュアルに従ってやってみます。
ところが、これでもリセットがかかってしまいます。

待ち時間を短くする。

待ち時間を5000としていたのを、2000(つまり2秒)に書き換えると、今度はスリープまでたどり着くことができました。色々試してみましたが3.5秒くらいが限界でした。この3.5秒にどういう意味があるのかさっぱりわかりませんが、例えば、つぎのように2秒待ちを繰り返すことで4秒間の待機をすることができます。

int now = int(millis()&0xFFFF);
while(int(millis()&0xFFFF) < (now + 2000)){

}
now = int(millis()&0xFFFF);
while(int(millis()&0xFFFF) < (now + 2000)){

}

これで解決万事めでたし。と思いきやそうは問屋が卸さないようで、

ウェイトループがあると受信ができない。

という、致命的なことがわかりました。ループ関数内は本来は何も書かれていないのですが、ここにウェイト関数なんかを入れてやるとうまく受信ができなくなります。delay(4000); とやっても同じです。ディレイの方はPICとかでも割り込みがうまくかからなくなるので、なんとなくそうだろうと思っていましたが、たんなるウェイトループでもうまくいきませんでした。
TWELITEの資料を読むと、loop関数は、「アプリケーションのメインループです。ループ終了後はCPUがDOZEモードに遷移し低消費電流で次の割り込みを待ちます。」と書いてあります。ということは、動作はArduinoのloop関数などと違って、ずっとループ内にいるのではなく、処理が終わったらループを抜け出して割り込み待ちになるということのようです。これが、loop関数内でウェイトを入れてうまく受信できなかった理由のようです。つまり、loop関数を抜けないと割り込み待ちにならないということです。ですので、loop関数内で時間待ちをするということはTWELITEの仕様上無理のようです。

ウェイトループは諦めて受信したらスリープするようにする

結局、結論はこれしかなさそうです。
TWELITEの資料の色々なところを読んでみましたが、解決策が見つけられず、ループ関数は元のまま、何もしないという形に戻して、受信処理の最後にスリープを入れることにしました。これはうまくいきました。受信したら、所定の時間スリープして、目覚めたらまたスリープするということを繰り返します。
ただ、こうすると、時刻サーバー側が何らかの理由で止まってしまっていた場合、受信するまで永遠に待ち続けるということになってしまいます。この辺りはPIC側で管理して、データ受信ができなければエラー表示をするという処理をするしかないと思います。それでもTWELITEは動き続けるのでもしそういう事態になったら結構電池を消費してしまうと思いますが現状では他に手がなさそうなので仕方がありません。ただ、資料によるとTWELITEはDOZEモードに入るようなので、めいっぱい電流を消費するわけではなさそうです。そのあたり、本当にどのくらい消費するかは、実際に消費電流を測ってみようと思っています。

ESP8266でNTPサーバーから時刻を取得し、RTCに書き込み、TWELITEで送信する

前回、ESP8266をスリープさせてTWELITEで起こすことをやりました。
alasixosaka.hatenablog.com

前々回は、ESP8266をWiFiに繋いで、NTPサーバーから時刻を取得し、RTC-8564に書き込みました。
alasixosaka.hatenablog.com

今回は、ESP8266が書き込んだ時刻をTWELITEで読み取って、無線で送信することをやってみます。
これができれば、時刻サーバーの基本部分が完成です。
配線は、前回の配線に、TWELITEとRTC-8564のI2C接続を追加します。

心配なのは、この接続だとI2Cのマスターが2つになってしまうのでシグナルがかち合ったときにうまく動いてくれるかという問題です。
こればっかりはやってみないとわからないのでとりあえずやってみることにします。
ESP8266側のプログラムは、前々回のNTPサーバーから時刻を取得するプログラムをほぼそのまま使います。違っている点は、ディープスリープ時間を1時間としていたのを ESP.deepSleep(0); として、時間を無限にしておくことです。変更点はそれだけです。もっとも、動き出して1分後にはリセットがかかってしまうので、どちらにしても結果は同じことになりますが。

TWELITEのプログラム

まずは無線なしで

TWELITE側は、今回も、温湿度センサーBME280を使ったときのプログラムをベースにした、前回のプログラムを改良しますが、結構変える必要があります。
いきなり全部変えてしまってうまく動かす自信もないので、まずはRTC-8564の値を読み取ってシリアルに出力するところまでやります。
まず、RTCの読み取り部分ですが、struct SHTC3 {....}の中のbool read(... の所を前回はコメントアウトしていたのですが、下記のように書き換えます。
変数がいっぱい出てきますが、i8sec, i8min, i8hour, i8day, i8wday, i8month, i8yearの7つは読み取った値の戻り値です。
また、u8sec, u8min, u8hour, u8day, u8wday, u8month, u8yearの7つはRTC-8564のレジスタから読みだした値を一時的に格納する変数です。

bool read(uint8_t &i8sec, uint8_t &i8min, uint8_t&i8hour, uint8_t &i8day, uint8_t &i8wday, uint8_t &i8month, uint8_t &i8year) {
	// read result
	uint8_t u8sec, u8min, u8hour, u8day, u8wday, u8month, u8year;
	if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
		wrt << 0x02; // register address for temp
	} else {
		return false;
	}

	if (auto&& rdr = Wire.get_reader(I2C_ADDR, 7)) {
		rdr >> u8sec; // read temperature
		rdr >> u8min;
		rdr >> u8hour;
		rdr >> u8day;
		rdr >> u8wday;
		rdr >> u8month;
		rdr >> u8year;
	} else {
		return false;
	}
	i8sec = u8sec & 0x7F;
	i8min = u8min & 0x7F;
	i8hour = u8hour & 0x3F;
	i8day = u8day & 0x3F;
	i8wday = u8wday & 0x07;
	i8month = u8month & 0x1F;
	i8year = u8year;

	return true;
}

日時が格納されているレジスタは2番からの連番ですので、まずRTCに2を書き込んで、レジスタの番号を2番にします。

        if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
		wrt << 0x02; // register address for temp
	} else {
		return false;
	}

の部分がそうです。
そして、連続で7つのデータを読み出します。

if (auto&& rdr = Wire.get_reader(I2C_ADDR, 7)) {

以下の部分がそうです。
最後に、読み取ったデータを有効ビットにするためにそれぞれアンドを取っています。色々試してみましたが、今回はこの部分が肝で、なぜだかわからないのですが、無効なビットが立ったりクリアされたりと不可解な動作をします。ESP8266だとこんなことにはならないのですが、一応有効ビットの部分は安定して読み出せているので今のところはこの処理でよいのかなと思っています。
I2C通信用のWire関数はこのプログラムで使っているヘルパークラスと通常のArduinoなどのプログラムで使うのとほぼ同じメンバー関数があります。
メンバー関数は、Wire.beginTransmission(xx); で始まり、Wire.requestFrom(xx, y); で読み込みます(xxはI2Cアドレス、yは読み込むバイト数)。
RTC-8564から読みだした値が不安定だったので、メンバー関数の方も試してみましたが、メンバー関数を使うとなぜかうまくデータ読み出せませんでした。書き込みの方はエラーなしで帰ってくるのですが、読み込みを行うとエラーになり得られたデータも7つすべての値が同じという不可解なことになってしまいました。なので、諦めてヘルパークラスの方を使って、得られたデータの有効ビットだけを残すというこの方法を使います。
シリアルの出力はこんな感じです。

RTC-8564の値を読み取ってシリアル出力されたデータ

RTCC Alarmと出ているところが、RTCでアラームが起動したことを検知している部分です。これで、ESP8266にリセットシグナルを送ってディープスリープから起こしています。ESP8266はディップスリープから起きると再びWiFiに接続しに行くため、その間はI2CにアクセスしないようにTWELITEの方は10秒のディレイを入れています。

ESP8266ではシリアル接続が必要?

ある程度安定に動いたら 、ESP8266側はシリアルをモニタする必要がないので、シリアル変換器のついた開発ボード(NodeMCU)からノーマルのESP8266に変えてみました。すると、USBシリアル変換器(FT-232RL)につながっているPCのUSB端子を抜くとESP8266が止まってしまうという現象が起こりました。最初は何故なのかよくわからず、ネットで調べても類似の現象が見つけられなくて結構悩みました。結論から言うと、USBシリアル変換器がつながっているとだめのようです。
シリアル送信の所がネックになるのかと思い、シリアル送信のコマンドを全部コメントアウトしてもやっぱりだめで、結局USBシリアル変換器をブレッドボードから引っこ抜いたら動くようになりました。

ESP8266のリセットが安定しない。

これで、しばらく動かしていると、最初のうちは1分おきにちゃんとリセットがかかったのですが、そのうちにリセットに失敗することが発生することがわかりました。はじめは、RTC-8564のINT端子にプルアップ抵抗を繋いでいなかったので、TWELITEがINTの出力をちゃんととらえられていないのかと思い、プルアップ抵抗を入れてみましたが症状が改善しないので、ネットで調べてみました。ESP8266のリセット動作が安定しない問題は割と有名らしく、いろいろな記事が見つかりました。結論としては、EN端子とGNDの間にコンデンサを入れてやればよいようです。
ehbtj.com
ただ、テストに使ったESP8266はマイクロテクニカの変換ボードに載っているやつで、残念ながらEN端子が引き出されていません。こいつは、リセットとIO0のスイッチ、抵抗などがボード上に実装されているので、外付けの部品が不要でUSBシリアル変換器だけ繋げばよかったので便利だったのですが、仕方ありません。
deviceplus.jp
もう売ってないみたいですが。

なので、また別のESP8266を引っ張り出してきて、今度は秋月電子で買ったESP8266を使いました。これは本当に素のESP8266をブレッドボードに刺せるように変換しているだけなので、スイッチも抵抗も一切実装されていないので、全部ブレッドボードに実装する必要があります。
こちらのサイトを参考に配線しました。
lipoyang.hatenablog.com
ただし、EN端子の部分だけは、上記のサイトのように、リセット対策として10kΩの抵抗でプルアップして、1μFのコンデンサをGNDとの間に入れておきます。
これで、リセットが安定してかかるようになりました。パワーオンリセットではないのでコンデンサに意味があるような気がしないのですが、結果的によくなりました。ひょっとしたらESP8266の個体差なのかもしれません。

無線送信をする

TWELITEのプログラムに戻ります。
RTC-8564からデータが読み出せたら、あとはそれほど難しくありません。
loop関数の中のSTATE::TXの部分を下記のようにします。やっていることはBME280の時とほぼ同じです。pack_bytes(pkt.get_payload() 以下の部分で、送信するパケットを作っています。ここに、秒、分、時、日、月、年、曜日の各データを1バイトずつ加えていっているだけです。

case STATE::TX:
	step.next(STATE::GO_SLEEP); // set default next state (for error handling.)
			
	// get new packet instance.
	if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
		// set tx packet behavior
		pkt << tx_addr(0xFF)  // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
			<< tx_retry(0x1) // set retry (0x1 send two times in total)
			<< tx_packet_delay(0, 0, 2); // send packet w/ delay

		// prepare packet payload
		pack_bytes(pkt.get_payload() // set payload data objects.
			, make_pair(FOURCHARS, 4)  // just to see packet identification, you can design in any.
			, uint8_t(sensor.i8sec)
			, uint8_t(sensor.i8min)
			, uint8_t(sensor.i8hour)
			, uint8_t(sensor.i8day)
			, uint8_t(sensor.i8month)
			, uint8_t(sensor.i8year)
			, uint8_t(sensor.i8wday)
		);

		// do transmit
		MWX_APIRET ret = pkt.transmit();
		Serial << "..transmit request by id = " << int(ret.get_value()) << '.' << mwx::crlf << mwx::flush;

		if (ret) {
			step.clear_flag(); // waiting for flag is set.
			step.set_timeout(100); // set timeout
			step.next(STATE::TX_WAIT_COMP);
		}
	}
			
break;

これで送信ができるようになります。

TWELITEでESP8266を起こす

前回はESP8266でNTPサーバーから時刻を取得し、RTC-8564NBに現在時刻を書き込むとこまでやりました。
alasixosaka.hatenablog.com

今回は、RTC-8564のアラーム機能を使って、TWELITEからスリープ中のESP8266を起こしてみたいと思います。
何故そんな回りくどいことをするのかというと、前にも書いたようにRTC-8564のアラーム機能を使うと、INT端子がLOWになるのですが、LOWを維持したままになるのでESP8266のリセット端子に直接つないでもリセットがかけられないためです。
そこで、TWELITEでRTC-8564のINT端子を監視して、LOWになったら、TWELITEのデジタル出力を使ってESP8266をリセットしてやろうという試みです。
こんな感じで配線します。
かなり複雑になっていますが。ESP8266はRTC-8564にI2Cで接続します。また、RTC-8564のINT端子とTWELITEの13番ピンを接続し、ESP8266のRST端子とTWELITEの18番ピンを接続します。TWELITEのピン番号は少しややこしいのですが、TWELITEDIPの場合、基板にシルク印刷している番号です。13番ピンは通常のDIPの数え方だと16番ピンに相当します。また、18番ピンは通常のDIPの数え方だと5番ピンに相当します。
そしてTWELITEはFT232RLとシリアルで接続します。ESP8266とFT232RLをPCのUSB端子に接続します。

ESP8266側のプログラムは前々回のプログラムをそのまま使います。動作は、ESP8266が起動すると2022年11月5日10時0分0秒にRTCを設定。アラームはその1分後にかかるように設定しています。50秒間カウントするとESP8266はディープスリープします。
RTCは起動から1分後にアラームを出してINT端子をLowにします。
今回はTWELITE側のプログラムをいじってINT端子を監視して、LowになったらESP8266を再起動するシグナルを出します。RTCのINT端子はTWELITEのDIO13に接続しています。ESP8266のRST端子はTWELITEのDIO18に接続しています。
TWELITEのプログラムはBME280を動かしたときのプログラムをベースに書き換えました。
変更点だけを書きます。
まず、struct SHTC3{...の部分、つまりセンサーを制御するルーチンの一群ですが、今回はセンサーを繋いでいないので本当は全部不要なのですが、センサーにアクセスしに行く部分だけコメントアウトしています。
まず、bool setup(uint16_t &i15Temp, uint16_t &i15Humd) {...の所の下記の部分をコメントアウト

		/*
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF4; // register address
			wrt << ctrl_meas_reg; // register data
		} else {
			return false;
		}
		//return true;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF5; // register address
			wrt << config_reg; // register data
		} else {
			return false;
		}
		delay(1000);
		
		//get compensation data
		
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0x88; //register address
		} else {
			return false;
		}
		
		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 6)) {
			rdr >> dig_T1tempL; 
			rdr >> dig_T1tempH; 
			rdr >> dig_T2tempL;
			rdr >> dig_T2tempH;
			rdr >> dig_T3tempL;
			rdr >> dig_T3tempH;
		} else {
			return false;
		}
		
		dig_T1 = uint16_t(dig_T1tempH<<8)|dig_T1tempL;
		dig_T2 = uint16_t(dig_T2tempH<<8)|dig_T2tempL;
		dig_T3 = uint16_t(dig_T3tempH<<8)|dig_T3tempL;
		i15Temp = dig_T1;
		i15Humd = dig_T2;
		

		ctrl_meas_reg = ctrl_meas_reg | 1;  //set one shot mode
		*/

続いて、セットアップの部分、void setup(){...以下の所に、ピンモードの設定を行う2行を追加。

/*** setup procedure (run once at cold boot) */
void setup() {
	/*** SETUP section */
	pinMode(PIN_DIGITAL::DIO18, OUTPUT_INIT_HIGH);
	pinMode(PIN_DIGITAL::DIO13, INPUT_PULLUP);

18番ピンを出力にして、13番ピンを入力にしています。18番ピンの出力はESP8266のRST端子につながっていますので、デフォルトをHIGHにしています。また、13番ピンはRTC-8564のINT端子とつながっていますが、INT端子はオープンドレインのためTWELITE側でプルアップをしています。
ピン番号の指定ですが、ACT0のLチカのサンプルではピン番号の指定はpinMode(18, OUTPUT)のようにただの番号で指定していますが、これだとコンパイル時にエラーになるらしく、TweliteStageが落ちてしまうので、インタラクティブモードの判定に使っている部分の記述に倣って、PIN_DIGITAL::DIO18のようにしています。APIを読んでも数字で指定できるように読めるのですが、なぜだかよくわかりません。とりあえずこの記述で動いているのでまあ良しとしています。
それから、同じくvoid setup(){..中のセンサーのセットアップをする部分を下記のようにコメントアウト

	// setup sensor device
	/*
	sensor_device.setup(sensor.i15temp,sensor.i15humid);
	Serial 	<< format("     : T1temp= %04x"
					,sensor.i15temp)
					<< mwx::crlf;
	Serial	<< format("     : T2temp=%04x"
					,sensor.i15humid)
					<< mwx::crlf;
					
				//Serial.flush();
	*/

そして、TWELITEがスリープから目覚めたときに実行されるvoid wakeup(){...の部分に、RTCのINT端子の状態を判定して、LOWなら、ESP8266をリセットする処理を入れています。if(digitalRead(... 以下がその部分です。一応シリアルにメッセージを出力する文も追加しています。

// wakeup procedure
void wakeup() {
	Serial	<< mwx::crlf
			<< "--- TEMP&HUMID:" << FOURCHARS << " wake up ---"
			<< mwx::crlf
			<< "..start sensor capture again."
			<< mwx::crlf;
	if (digitalRead(PIN_DIGITAL::DIO13) == PIN_STATE::LOW) {
		digitalWrite(18, LOW);
		delay(100);
		digitalWrite(18, HIGH);
		Serial	<< mwx::crlf
			<< "--- RTCC Alarm ---"
			<< mwx::crlf;
	}
}

続いて、ループ関数の中ですが、
case STATE::TX、つまり無線でデータを送信する部分は、初めの一行のstep.next(STATE::GO_SLEEP); を除いてすべてコメントアウトして、無線送信をキャンセルしてすぐにスリープするようにしています。

case STATE::TX:
	step.next(STATE::GO_SLEEP); // set default next state (for error handling.)
	/*
	// get new packet instance.
	if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
		// set tx packet behavior
		pkt << tx_addr(0x00)  // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
			<< tx_retry(0x1) // set retry (0x1 send two times in total)
			<< tx_packet_delay(0, 0, 2); // send packet w/ delay

		// prepare packet payload
		pack_bytes(pkt.get_payload() // set payload data objects.
			, make_pair(FOURCHARS, 4)  // just to see packet identification, you can design in any.
			, uint16_t(sensor.i16temp)
			//, uint16_t(sensor.i16humid)
			);

		// do transmit
		MWX_APIRET ret = pkt.transmit();
		Serial << "..transmit request by id = " << int(ret.get_value()) << '.' << mwx::crlf << mwx::flush;

		if (ret) {
			step.clear_flag(); // waiting for flag is set.
			step.set_timeout(100); // set timeout
			step.next(STATE::TX_WAIT_COMP);
		}
	}
	*/

変更点は以上です。
これで、TWELITEがスリープから目覚めたときにINT端子がLOWになっていたらESP8266をリセットして再起動がかかります。

ESP8266でNTPサーバーから時刻を取得してRTCに書き込む

前回はESP8266でRTC-8564NBを動かすことをやりました。
alasixosaka.hatenablog.com

ただ、これでは時刻が適当に設定されるだけなので、ESP8266の真骨頂であるWiFi機能を使ってNTPサーバーから時刻を取得し、正確な時刻をRTCに書き込んでみます。ESP8266やESP32を使ってNTPサーバーから時刻を取得するプログラムはいろいろと公開されています。WiFiにさえ繋いでしまえば実はNTPサーバーから時刻を取得するのは非常に簡単です。
今回はこちらのサイトを参考にしました。
garretlab.web.fc2.com
ソースコードが自分的に読みやすかったからです。でも、実はこれはESP32用のプログラムでそれが原因でまたはまってしまいました。どうなったかは後で書きます。
まず、NTPサーバーから時刻を取得する部分ですが、たったの1行で、

configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");

で済みます。
そいつを

struct tm timeInfo;

で定義したtimeInfoを使って

getLocalTime(&timeInfo);

としてやればtimeInfoに年、月、日、曜日、時、分、秒が格納されます。
年なら、timeInfo.tm_year、月なら、timeInfo.tm_monthといった具合です。
ところが、このgetLocalTime()という関数がESP32の関数になっていて、ESP8266用のプログラムでは動きません。
検索すると、Arduinoのフォーラムに解決策が書いてありました。
arduino.stackexchange.com
ここに書いてあるサブルーチンをそのまま使ってやるとエラーが解消されてちゃんと動きました。
これで、現在時刻が取得できたので、それをBCDコードに直して、RTCに書き込んでやればよいということになります。
それだけではおもしろくないので、ディープスリープを使って、必要ない時はESP8266をスリープさせるようにしました。本当はRTCで目覚めるようにしたいのですが、前回書いたようにそのままつないでも起きてくれないので、今回はタイマーを使って1時間ごとに目覚めるようにしました。ディープスリープの関数は下記のようになります。

ESP.deepSleep(3600 * 1000 * 1000, WAKE_RF_DEFAULT);

また、電池駆動にしてAmbientというネットサービスを使ってVCCの電圧をウェイクアップするたびに送信するようにしてみました。
Ambientを使えば送信したデータをグラフ化してくれます。
ambidata.io

配線はこんな感じです。図では、LiPoを電池に使っていますが、実際は、NiMHのエネループを3本直列に繋いでいます。

電圧の取得は、

int v=ESP.getVcc();

で取得できます。
それを、ループ関数内で

ambient.set(1, v);
delay(100);
ambient.send();
delay(1000);

としてAmbientに送っています。delayはデータが確実に送られるように適当に入れています。
Ambientを使うには、HPで登録をして、チャンネルを作成し、チャンネルのNo と ライトキーをあらかじめ取得しておく必要があります。詳細な手順はAmbentのHPに書いてあるのでその通りにやれば問題なくできると思います。
また、AmbientをArduinoで使うにはライブラリをインクルードする必要があります。ライブラリは、Arduino IDEのライブラリを追加から、Ambientを検索することで見つけることができます。見つかったライブラリをクリックしてインストールしておきます。
プログラムの全文です。

#include <ESP8266WiFi.h>
#include <time.h>
#include <Wire.h>
#include <Ambient.h>

#define ssid "XXXXXXXX"      //your SSID
#define password "YYYYYYYY"    //your Password
#define channelId ZZZZZZ  // 生成したAmbientチャネルID
#define writeKey "WWWWWWWW"  // Ambient のライトキーをセット

ADC_MODE(ADC_VCC)
Ambient ambient;
WiFiClient client;
char s[32];
int year;
int month;
int day;
int hour;
int minute;
int sec;
int wday;
int RegTbl[16]; 
byte DEVICE_ADDRESS= 0x51; 

byte yearBCD;
byte monthBCD;
byte dayBCD;
byte hourBCD;
byte minuteBCD;
byte secBCD;

// 2進化10進数(BCD)を10進数に変換
byte BCDtoDec(byte value){
  return ((value >> 4) * 10) + (value & 0x0F) ;
}
//整数変数をBCDに変換する関数
byte DectoBCD(int value){
  int dec = (value / 10)*16;
  return ((value%10)+dec);
}

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(0x01);  //Control Reg2
  Wire.write(secBCD);  //0 second
  Wire.write(minuteBCD);  //0 min
  Wire.write(hourBCD);  //0 hour
  Wire.write(dayBCD);  //1st
  Wire.write(wday);  //Tuesday
  Wire.write(monthBCD);  //November
  Wire.write(yearBCD);  //2022
  Wire.write(minuteBCD+1);  //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);  
}

bool getLocalTime(struct tm * info, uint32_t ms = 5000)
{
    uint32_t start = millis();
    time_t now;
    while((millis()-start) <= ms) {
        time(&now);
        localtime_r(&now, info);
        if(info->tm_year > (2016 - 1900)){
            return true;
        }
        delay(10);
    }
    return false;
}

void setup(){
  Serial.begin(115200);
  delay(1000);
  Serial.print("\n\Start:\n");
  // マスタとしてI2Cバスに接続する
  Wire.begin(4,14); 
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println();
  Serial.printf("Connected, IP address: ");
  Serial.println(WiFi.localIP());
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");

  struct tm timeInfo;
   Serial.println("get time");
   getLocalTime(&timeInfo);
   
   year = timeInfo.tm_year + 1900;
   month = timeInfo.tm_mon + 1;
   day = timeInfo.tm_mday;
   hour = timeInfo.tm_hour;
   minute = timeInfo.tm_min;
   sec = timeInfo.tm_sec;
   wday = timeInfo.tm_wday;
   Serial.println("local time get");
   
   yearBCD = DectoBCD(year-2000);
   monthBCD = DectoBCD(month);
   dayBCD = DectoBCD(day);
   hourBCD = DectoBCD(hour);
   minuteBCD = DectoBCD(minute);
   secBCD = DectoBCD(sec);
   init_RTC_8564();
  Serial.println("start RTC");
  ambient.begin(channelId, writeKey, &client);  //  チャネルIDとライトキーを指定してAmbientの初期化
  delay(100);
}

void loop(){
   
  int v=ESP.getVcc();
  Serial.println("VCC="+String(v/1000.0)+"V");
  ambient.set(1, v);
  delay(100);
  ambient.send();
  delay(1000);
  Serial.println("Go to Sleep");
  ESP.deepSleep(3600 * 1000 * 1000, WAKE_RF_DEFAULT);
  delay(1000);
}

今回は、RTCのデータを読み取ってシリアル出力する部分は省略しています。
AmbientでVCCの電圧を見ると4Vを越えています。昔、Ni-MHの電池は1.2Vと習ったので3本で3.6VでESP8266などの3.3V系の動作にはちょうど良いと思ったのですが、ちょっと電圧が高すぎます。まあ、壊れずに動いているので一応セーフなのかもしれませんが、ちょっと怖い。
それと、状況をモニタするために、要所要所にシリアル出力が入っていますが、何時間もモニタするのも面倒だと思い、USBケーブルを外してPCをシャットダウンしたらプログラムが止まったのか、Ambientにデータが更新されなくなりました。

USBケーブルを外すとAmbientのデータ更新が止まった。

図のグラフは、外してしばらくほっておいて、データが更新されていないので、もう一度USBケーブルを繋いで、リセットしたデータで、本来1時間おきにデータがあるはずなのに、ずっとデータが抜け落ちている。ただほっておいて、Ambientにだけデータを送るということであればプログラム中のシリアル出力文を全部コメントアウトする必要がありそう。

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に書き込むことをやってみます。