#1. はじめに
RTC(Real Time Clock) は、日付や時刻を高い精度で計数する LSI で構成されます。装置の電源が切断された状態でもバックアップ電池等により時刻を維持します。アラーム機能やタイマー機能もあります。IoT はインターネットが前提であり、日付や時刻を NTP で維持したりクラウド側で記録したりできます。しかし高速なネットワークや潤沢な電源から遠ざかるほど RTC の必要性が増すと思われます。
RTC の利用については NTP の様な標準的な方法がない様に思います。NTP と一体化されシームレスに利用できる環境があればよいのにと思います。GPS や標準電波なども同様です。「車輪の再発明」で似て非なるものが増えてしまいますが、趣味の電子工作をターゲットに利用方法を考えてみました。
NTP の時刻同期のタイミングで、RTC の時刻を合わせ込む方法については、**Qiita「ESP32 において NTP の時刻同期を捕まえて RTC を更新する」1**を参照ください。作成したコードや設計データは、GitHub2 にあります。
- LSI(Large-Scale Integration): 大規模集積回路
- IoT(Internet of Things)
- NTP(Network Time Protocol)
- GPS(Global Positioning System)
- 標準電波: JJY など
#2. M5Stack, M5Atom
M5Stack 社3は、多種多様の IoT デバイスや拡張モジュールを販売しています。コントローラ製品とカメラ製品の RTC 搭載状況を調べてみました。BM85634 または HYM8563 が使用され、内蔵デバイス用の I2C に接続されています。INT(Interrupt) も機種により活用されています。RTC を搭載していない M5Stack(Core) や M5Atom 向けに RTC 基板を作成したいと思います。その後、拡張ユニットもラインナップ5されました。
Controller | RTC | System SCL (GPIO) | System SDA (GPIO) | Interrupt (GPIO) |
---|---|---|---|---|
M5Core2 | BM8563 | 22 | 21 | Power On Key |
M5Core2 for AWS | BM8563 | 22 | 21 | Power On Key |
M5Stack Basic | - | 22 | 21 | - |
M5Stack Gray | - | 22 | 21 | - |
M5Stack Fire | - | 22 | 21 | - |
M5Go | - | 22 | 21 | |
M5CoreInk | BM8563 | 22 | 21 | - |
M5Paper | BM8563 | 22 | 21 | - |
M5StickC PLUS | BM8563 | 22 | 21 | 35 |
M5StickC | BM8563 | 22 | 21 | 35 |
M5StickT | - | 22 | 21 | - |
M5StickT White/Gray | - | 22 | 21 | - |
M5Atom Lite | - | - | - | - |
M5Atom Matrix | - | 21 | 25 | - |
M5Atom Echo | - | - | - | - |
Timer Camera F | BM8563 | 14 | 12 | Sleep/Wakeup |
Timer Camera X | BM8563 | 14 | 12 | Sleep/Wakeup |
Timer Camera | BM8563 | 14 | 12 | Sleep/Wakeup |
M5CameraF New | - | 23 | 22 | - |
M5CameraF | - | 23 | 22 | - |
M5CameraX | - | 23 | 22 | - |
M5Camera | - | 23 | 22 | - |
ESP32CAM | - | 4 | 13 | - |
- GPIO(General-Purpose Input/Output)
#3. PCF8563(BM8563, HYM8563)
BM8563, HYM8563 には M5Stack シリーズ用のソフトウェアを活用できます。M5Stack 社の提供する API6 のほかにライブラリマネージャーに登録されているもの(一例7)、GitHub 等に公開されているもの8など多種多様です。
BM8563 の製造元の Web ページ9にリファレンス PCF856310 とあります。BM8563 は PCF8563 の互換品と思われます。基板製造業者である JLCPCB11 の SMT ライブラリ12には、これらを含め複数部品が登録されています。PCF8563 はハンドリング費が不要の Basic Parts13 です。PCF8563 または BM8563 を選択します。水晶振動子との相性など特性上の非互換があるかもしれません。実際に製作して動作を確認します。
#4. I2C
PCF8563 は I2C でアクセスします。Arduino IDE ではライブラリ Wire14 を使用します。ESP32 の HAL で提供される I2C15 をクラス TwoWire で包み、そのインスタンスとして Wire, Wire1 の 2 つが予め宣言されています。
- I2C(Inter-Integrated Circuit): フィリップス社で開発されたシリアルバス
- HAL(Hardware Abstraction Layer): ハードウェア依存の処理を担い、抽象化するプログラム
##4.1. M5Stack シリーズの I2C
M5Stack.cpp16 および M5Atom.cpp17 では以下の設定です。Wire1 は、ユーザが GPIO を指定して使用できます。
Library | Wire: SCL(GPIO) | Wire: SDA(GPIO) | Wire: Frequency(Hz) | Wire1: |
---|---|---|---|---|
M5Stack.cpp | 22 | 21 | 指定なし(100000) | 未使用 |
M5Atom.cpp | 21 | 25 | 100000 | 未使用 |
I2C の動作スピード(周波数)について M5Stack.cpp には指定がなく、デフォルトの 100kHz に設定されます。M5Atom.cpp はデフォルトと同じ 100kHz の指定です。PCF8563 の最大 400kHz で動作を安定させるには、配線の長さ、プルアップ抵抗の値や配置などの条件が厳しくなります。
#include <M5Atom.h>
// for M5Atom
const bool serial_enable = true;
const bool i2c_enable = true; // SCL = GPIO21, SDA = GPIO25, Frequency = 100kHz
const bool display_enable = true;
void setup()
{
M5.begin(serial_enable, i2c_enable, display_enable);
}
##4.2. I2C バスの共有
I2C はバスであり、センサーなど RTC 以外のデバイスと並列に接続されます。1 つの I2C バスを共有する形でプログラミングする必要があります。PCF8563 を扱うクラスでは、クラス外で用意され、PCF8563 を接続している Wire または Wire1 をメンバ変数(ポインタ)に保持して使用します。初期設定のための Begin() を用意し、その引数で Wire を取り込みます。PCF8563 は、せいぜいシステムに 1 つです。インスタンスの名称 rtcx を事前に設定したいと考え、コンストラクタでの Wire の取り込みは諦めました。
#include <Arduino.h> // for Serial Monitor
#include <Wire.h>
class Pcf8563 {
public:
void Begin(TwoWire &wire);
private:
TwoWire *m_wire;
const int m_i2c_address = 0x51;
const int m_reg_size = 0x10;
uint8_t *m_reg;
int WriteReg(int reg_start, size_t write_length);
};
void Pcf8563::Begin(TwoWire &wire)
{
m_wire = &wire;
}
int Pcf8563::WriteReg(int reg_start, size_t write_length)
{
m_wire->beginTransmission(m_i2c_address);
m_wire->write(reg_start);
for (int i = 0; i < write_length; ++i )
m_wire->write(m_reg[reg_start + i]);
int return_code = m_wire->endTransmission();
if (return_code != 0)
Serial.print("[Pcf8563:WriteReg] ERROR write\n");
return return_code;
}
// an instance "rtc external"
Pcf8563 rtcx;
##4.3. PCF8563 の Read
PCF8563 から時刻等のデータを読み出す場合、PCF8563 のレジスタアドレスを Write で送ってから Read を行います。PCF8563 のデータシート18には、Write と Read の間に I2C バスを開放しない流れが記載されています。Arduino の Reference19 によると、Wire.endTransmission(stop) で、stop = false とするとバスを掴んだままとなります。デフォルトは stop = true です。
HYM8563
M5Stack Core2 では、BM8563 に代えて HYM8563 が実装される様になりました20。HYM8563 では、Write - Read の間に stop があると、Read のレジスタアドレスが 0 になってしまうとのことです21。stop なしとすることが必須となります。
#include <Arduino.h> // for Serial Monitor
#include <Wire.h>
class Pcf8563 {
private:
TwoWire *m_wire;
const int m_i2c_address = 0x51;
const int m_reg_size = 0x10;
uint8_t *m_reg;
size_t ReadReg(int reg_start, size_t read_length)
};
size_t Pcf8563::ReadReg(int reg_start, size_t read_length)
{
const bool send_stop(true);
m_wire->beginTransmission(m_i2c_address);
m_wire->write(reg_start);
if (m_wire->endTransmission(!send_stop) != 0) {
Serial.print("[Pcf8563:ReadReg] ERROR write\n");
return 0; // command error
}
m_wire->requestFrom(m_i2c_address, read_length);
size_t i = 0;
while (m_wire->available())
m_reg[reg_start + i++] = m_wire->read();
if (i != read_length)
Serial.print("[Pcf8563:ReadReg] ERROR read\n");
return i;
}
Wire.endTransmission(false) とした場合の波形を観測しました**(波形 1 )**。I2C デコード表示の青色の 1 番目が Write 指示でレジスタアドレス 02 を送っています。青色の 2 番目が Read 指示で 7 バイト分のデータ読み出しが続きます。
(波形 1 )I2C Write-Read, CH1: SCL, CH2: SDA, I2C デコード
Wire.endTransmission(true); とした場合の波形を観測しました**(波形 2 )**。Write と Read の間に、ほんの少し時間が空いています。
(波形 2 )I2C Write(stop)-Read, CH1: SCL, CH2: SDA, I2C デコード
Write と Read の間を拡大する**(波形 3 )**と、約 66μs でした。Write 後に stop( SCL = H の間に SDA = L → H )があるのが見えます。
(波形 3 )I2C Write(stop)-Read, CH1: SCL, CH2: SDA, I2C デコード
Write と Read の間で I2C バスが開放されても、処理スピードやバス効率が問題になるとは思えません。アクセス元(マスタ)が複数あって I2C デバイスを共有する場合、Write と Read の隙間は致命的な問題になります。マスタが 1 つの場合は stop = true でも問題はないと思われますが、HYM856322は正しく動作しない(次項参照)とのことです。マスタが 1 つでも複数プロセスが I2C デバイスを共有する状況も考えられますが、この場合はソフトウェアレベルで排他制御が必須です。
#5. RTC の機能
PCF8563 の機能を利用するソフトウェアを作成してみました。
##5.1. 日付・時刻
C 言語の時間の仕様を参照して、日付・時刻に分解した表現に構造体 struct tm を使用します。struct tm を使用することでシステムで使用される time_t 型との相互変換や strftime() による文字列表現ができます23。さらに Arduino IDE で標準的に使える getlocaltime()2425 で得た時刻をそのまま RTC に設定できます。
PCF8563 のレジスタに合わせて日付や時刻を BCD に変換します。
class Pcf8563 {
public:
int ReadTime(struct tm *tm_now);
int WriteTime(struct tm *tm_now);
private:
int Int2Bcd(int int_num);
int Bcd2Int(int bcd_num);
};
int Pcf8563::ReadTime(struct tm *tm_now)
{
if (ReadReg(0x02, 7) != 7) return 1; // ReadReg error
if (m_reg[0x02] & 0x80) return 2; // invalid time
tm_now->tm_sec = Bcd2Int(m_reg[0x02] & 0x7f);
tm_now->tm_min = Bcd2Int(m_reg[0x03] & 0x7f);
tm_now->tm_hour = Bcd2Int(m_reg[0x04] & 0x3f);
tm_now->tm_mday = Bcd2Int(m_reg[0x05] & 0x3f);
tm_now->tm_wday = Bcd2Int(m_reg[0x06] & 0x07); // 0:Sun, 1:Mon, .. 6:Sat
tm_now->tm_mon = Bcd2Int(m_reg[0x07] & 0x1f) - 1; // tm month: 0..11
tm_now->tm_year = Bcd2Int(m_reg[0x08] & 0xff);
if ((m_reg[0x07] & 0x80) != 0)
tm_now->tm_year += 100; // century bit
return 0;
}
int Pcf8563::WriteTime(struct tm *tm_now)
{
m_reg[0x02] = Int2Bcd(tm_now->tm_sec);
m_reg[0x03] = Int2Bcd(tm_now->tm_min);
m_reg[0x04] = Int2Bcd(tm_now->tm_hour);
m_reg[0x05] = Int2Bcd(tm_now->tm_mday);
m_reg[0x06] = Int2Bcd(tm_now->tm_wday); // 0:Sun, 1:Mon, .. 6:Sat
m_reg[0x07] = Int2Bcd(tm_now->tm_mon + 1); // rtc month: 1..12
m_reg[0x08] = Int2Bcd(tm_now->tm_year % 100);
if (tm_now->tm_year >= 100)
m_reg[0x07] |= 0x80; // century bit
return WriteReg(0x02, 7);
}
int Pcf8563::Int2Bcd(int int_num)
{
return int_num / 10 * 16 + int_num % 10;
}
int Pcf8563::Bcd2Int(int bcd_num)
{
return bcd_num / 16 * 10 + bcd_num % 16;
}
- time_t: 1970年1月1日0時0分0秒(UTC)からの経過秒数を表す型。UNIX 時間、エポック秒、POSIX 時間
- BCD(Binary Coded Decimal): 10 進数の 1 桁を 4bit で表現する
##5.2. アラーム
分、時、日、曜の各々について、一致条件を設定できます。条件が成立した瞬間にアラームが発生します。実用的な組み合わせは限られます。月~金といった範囲やパターンの設定はできません。
分 | 時 | 日 | 曜 | アラーム成立 |
---|---|---|---|---|
有効 | 有効 | - | - | 毎日の、設定した 時:分 |
有効 | 有効 | - | 有効 | 毎週の、設定した曜の 時:分 |
有効 | 有効 | 有効 | - | 毎月の、設定した日の 時:分 |
有効 | - | - | - | 毎時の、設定した 分 |
- | 有効 | - | - | 毎日の、設定した 時:00 |
- | - | 有効 | - | 毎月の、設定した日の 00:00 |
- | - | - | 有効 | 毎週の、設定した曜の 00:00 |
- | - | 有効 | 有効 | 日と曜の条件が合った日の 00:00 |
分、時、日、曜の各々について数値が有効な範囲にある場合にアラーム条件も有効となる様にしました。デフォルト値は無効となる値です。
PCF8563 のレジスタ上では "1" で無効、"0" で有効であり、enable とは反対の意味になっています。
class Pcf8563 {
public:
int SetAlarm(int miniute = -1, int hour = -1, int day = -1, int weekday = -1);
int DisableAlarm();
bool alarm_minute_enable; // 09h Minute_alarm bit 7 !AE_M
int alarm_minute; // 09h Minute_alarm 0..59
bool alarm_hour_enable; // 0Ah Hour_alarm bit 7 !AE_H
int alarm_hour; // 0Ah Hour_alarm 0..23
bool alarm_day_enable; // 0Bh Day_alarm bit 7 !AE_D
int alarm_day; // 0Bh Day_alarm 1..31
bool alarm_weekday_enable; // 0Ch Weekday_alarm bit 7 !AE_W
int alarm_weekday; // 0Ch Weekday_alarm 0..6
int ReadAlarm();
int WriteAlarm();
}
// to enable alarm, give valid value for arguments of minute, hour, day, weekday
int Pcf8563::SetAlarm(int minute, int hour, int day, int weekday)
{
alarm_minute_enable = false;
alarm_hour_enable = false;
alarm_day_enable = false;
alarm_weekday_enable = false;
if (minute >= 0 && minute <= 59) { alarm_minute_enable = true; alarm_minute = minute; }
if (hour >= 0 && hour <= 23) { alarm_hour_enable = true; alarm_hour = hour; }
if (day >= 1 && day <= 31) { alarm_day_enable = true; alarm_day = day; }
if (weekday >= 0 && weekday <= 6) { alarm_weekday_enable = true; alarm_weekday = weekday; }
return WriteAlarm();
}
int Pcf8563::DisableAlarm()
{
return SetAlarm();
}
int Pcf8563::ReadAlarm()
{
if (ReadReg(0x09, 4) != 4) return 1;
alarm_minute_enable = (m_reg[0x09] & 0x80) == 0;
alarm_hour_enable = (m_reg[0x0A] & 0x80) == 0;
alarm_day_enable = (m_reg[0x0B] & 0x80) == 0;
alarm_weekday_enable = (m_reg[0x0C] & 0x80) == 0;
alarm_minute = Bcd2Int(m_reg[0x09] & 0x7f);
alarm_hour = Bcd2Int(m_reg[0x0A] & 0x3f);
alarm_day = Bcd2Int(m_reg[0x0B] & 0x3f);
alarm_weekday = Bcd2Int(m_reg[0x0C] & 0x07);
return 0;
}
int Pcf8563::WriteAlarm()
{
m_reg[0x09] = Int2Bcd(alarm_minute );
m_reg[0x0A] = Int2Bcd(alarm_hour );
m_reg[0x0B] = Int2Bcd(alarm_day );
m_reg[0x0C] = Int2Bcd(alarm_weekday);
if (!alarm_minute_enable ) m_reg[0x09] |= 0x80;
if (!alarm_hour_enable ) m_reg[0x0A] |= 0x80;
if (!alarm_day_enable ) m_reg[0x0B] |= 0x80;
if (!alarm_weekday_enable) m_reg[0x0C] |= 0x80;
return WriteReg(0x09, 4);
}
##5.3. タイマー
クロック源 4 種と、カウントダウン初期値 (1-255) の組み合わせでタイマーを設定できます。タイマーを有効にするとクロック源に合わせてカウントダウンが進みます。"0" となった瞬間に再び初期値がロードされ、カウントダウン動作を繰り返します。
クロック源 | タイマー最小 | タイマー最大 |
---|---|---|
1/4096 s | 0.244ms | 62.256ms |
1/64 s | 15.625ms | 3.984s |
1 s | 1s | 255s |
60 s | 60s | 15,300s |
与えられたタイマー時間から適切なクロック源とカウントダウン初期値を計算します。クロック源の切替は、なるべく周期の短い方を選ぶことで精度が上げます。クロック源 1s から 60s への切替は、タイマー時間の逆転を防止するため 240s としています。実際に設定できたタイマー値を戻り値としています。
カウントダウン中に有効ビットをオフにするとカウント値を保持して停止します。有効ビットをオンにすると、その時のカウント値からカウントダウンを再開します。
class Pcf8563 {
public:
double SetTimer(double timer_sec);
int EnableTimer(bool enable_timer = true);
int DisableTimer();
enum FreqClockOut_t { // 0Dh Clock_out_control FD 0..3
fco_32768Hz,
fco_1024Hz,
fco_32Hz,
fco_1Hz,
};
bool clock_out_active; // 0Dh Clock_out_control bit 7 FE
FreqClockOut_t clock_out; // 0Dh Clock_out_control 0..3
int ReadTimer();
int WriteTimer();
}
// Set timer_source and timer to the argument timer_sec as close as possible
// returns actual timer value realized by timer_source and timer,
// return 0.0: timer_sec is out of range, the timer was not set
// return <>0.0: The timer is enabled.
double Pcf8563::SetTimer(double timer_sec)
{
if (timer_sec < 1.0 / 4096.0) return 0.0; // minimum 0.00024414s
if (timer_sec > 255.0 * 60.0) return 0.0; // maximum 15,300s
if (timer_sec >= 240.0) { // cross over at 240s not 255s
timer_source = fts_1_60th_Hz;
timer = timer_sec / 60.0 + 0.5; // +0.5 for rounding
}
else if (timer_sec > 255.0 / 64.0) {
timer_source = fts_1Hz;
timer = timer_sec + 0.5; // +0.5 for rounding
}
else if (timer_sec > 255.0 / 4096.0) {
timer_source = fts_64Hz;
timer = timer_sec * 64.0 + 0.5; // +0.5 for rounding
}
else {
timer_source = fts_4096Hz;
timer = timer_sec * 4096.0 + 0.5; // +0.5 for rounding
}
timer_enable = true;
if (WriteTimer() != 0) return 0.0;
switch (timer_source) {
case 0: return timer / 4096.0; break;
case 1: return timer / 64.0; break;
case 2: return timer / 1.0; break;
case 3: return timer * 60.0; break;
default: return 0.0; break;
}
}
// timer_en = true: enable, false: disable
int Pcf8563::EnableTimer(bool enable_timer)
{
if (ReadReg(0x0e, 1) != 1) return 1;
switch (m_reg[0x0e] & 0x03) {
case 0: timer_source = fts_4096Hz; break;
case 1: timer_source = fts_64Hz; break;
case 2: timer_source = fts_1Hz; break;
case 3: timer_source = fts_1_60th_Hz; break;
default: break;
}
timer_enable = enable_timer;
m_reg[0x0e] = timer_source;
if (timer_enable)
m_reg[0x0e] |= 0x80;
return WriteReg(0x0e, 1);
}
int Pcf8563::DisableTimer()
{
return EnableTimer(false);
}
int Pcf8563::ReadTimer()
{
if (ReadReg(0x0e, 2) != 2) return 1;
timer_enable = m_reg[0x0e] & 0x80;
switch (m_reg[0x0e] & 0x03) {
case 0: timer_source = fts_4096Hz; break;
case 1: timer_source = fts_64Hz; break;
case 2: timer_source = fts_1Hz; break;
case 3: timer_source = fts_1_60th_Hz; break;
default: break;
}
timer = m_reg[0x0f];
return 0;
}
int Pcf8563::WriteTimer()
{
m_reg[0x0e] = timer_source;
if (timer_enable)
m_reg[0x0e] |= 0x80;
m_reg[0x0f] = timer;
return WriteReg(0x0e, 2);
}
##5.4. 割込み
アラームの条件が整うか、またはタイマーのカウントが尽きると各々フラグがセットされます。アラーム、タイマーの各々に割込みを設定でき、PCF8563 の割込み (INT) 信号がアクティブ (Low) になります。INT を GPIO に取り込むなどして、CPU に割込みを発生させることができます。
- INT(Interrupt)
- CPU(Central Processing Unit)
ポーリング
割込みフラグをポーリングで監視することもできます。GetInterrupt() は、フラグを検出したことを伝え、そのフラグをクリアします。
class Pcf8563 {
public:
int GetInterrupt(); // check interrupt and clear
bool timer_interrupt_pulse_mode; // 01h Control_status_2 bit 4 TI_TP
bool alarm_flag_active; // 01h Control_status_2 bit 3 AF
bool timer_flag_active; // 01h Control_status_2 bit 2 TF
bool alarm_interrupt_enable; // 01h Control_status_2 bit 1 AIE
bool timer_interrupt_enable; // 01h Control_status_2 bit 0 TIE
int ReadInterrupt();
int WriteInterrupt();
}
int Pcf8563::GetInterrupt()
{
if (ReadInterrupt() != 0) return 4;
int flag_got(0);
if (alarm_flag_active) { flag_got |= 0x02; alarm_flag_active = false; }
if (timer_flag_active) { flag_got |= 0x01; timer_flag_active = false; }
if (flag_got != 0)
if (WriteInterrupt() != 0) return 4;
return flag_got;
}
int Pcf8563::ReadInterrupt()
{
if (ReadReg(0x01, 1) != 1) return 1;
timer_interrupt_pulse_mode = m_reg[0x01] & 0x10;
alarm_flag_active = m_reg[0x01] & 0x08;
timer_flag_active = m_reg[0x01] & 0x04;
alarm_interrupt_enable = m_reg[0x01] & 0x02;
timer_interrupt_enable = m_reg[0x01] & 0x01;
return 0;
}
int Pcf8563::WriteInterrupt()
{
m_reg[0x01] = 0x00;
if (timer_interrupt_pulse_mode) m_reg[0x01] |= 0x10;
if (alarm_flag_active ) m_reg[0x01] |= 0x08;
if (timer_flag_active ) m_reg[0x01] |= 0x04;
if (alarm_interrupt_enable ) m_reg[0x01] |= 0x02;
if (timer_interrupt_enable ) m_reg[0x01] |= 0x01;
return WriteReg(0x01, 1);
}
アラーム割込み
アラームによる割込みの可否を制御します。同時にフラグのクリアも指定できます。
class Pcf8563 {
public:
int EnableAlarmInterrupt(bool enable_interrupt = true, bool keep_flag = false);
int DisableAlarmInterrupt();
}
// interrupt_en = true: enable, false: disable
int Pcf8563::EnableAlarmInterrupt(bool enable_interrupt, bool flag_keep)
{
if (ReadInterrupt() != 0) return 1;
alarm_flag_active = flag_keep;
alarm_interrupt_enable = enable_interrupt;
return WriteInterrupt();
}
int Pcf8563::DisableAlarmInterrupt()
{
return EnableAlarmInterrupt(false); // disable and clear
}
タイマー割込み
タイマーによる割込みの可否を制御します。同時にフラグのクリアも指定できます。タイマーにはパルスモードを指定できます。パルスモードの場合、カウントダウンが尽きたときに短いパルスを送出します。初期値からのカウントダウンを繰り返すので、タイマー周期でパルスを連続生成できます。パルスモードでない場合、フラグはクリアされるまでオンを保ちます。
class Pcf8563 {
public:
int EnableTimerInterrupt(bool enable_interrupt = true, bool pulse_mode = false, bool keep_flag = false);
int DisableTimerInterrupt();
}
// interrupt_enable = true: enable, false: disable
int Pcf8563::EnableTimerInterrupt(bool enable_interrupt, bool pulse_mode, bool keep_flag)
{
if (ReadInterrupt() != 0) return 1;
timer_interrupt_pulse_mode = pulse_mode;
timer_flag_active = keep_flag;
timer_interrupt_enable = enable_interrupt;
if (WriteInterrupt() != 0) return 1;
return 0;
}
int Pcf8563::DisableTimerInterrupt()
{
return EnableTimerInterrupt(false); // disable and clear
}
##5.5. クロック出力
PCF8563 のクロック出力端子 (CLKO) への出力信号を指定します。32,768Hz, 1,024Hz, 32Hz, 1Hz およびオフを指定できます。PCF8563 の水晶振動子の発振周波数の微調整のため、1Hz を出力する関数 ClockOutForTrimmer() を用意しました。INT の LED はデモンストレーションで 5 秒毎に光ります。(その後、INT のデモは、ボタンでオンオフできる様にし、この関数からは削除しました。)
- CLKO(Clock Out)
class Pcf8563 {
public:
int ClockOutForTrimmer(); // clock out 1Hz
enum FreqClockOut_t { // 0Dh Clock_out_control FD 0..3
fco_32768Hz,
fco_1024Hz,
fco_32Hz,
fco_1Hz,
};
bool clock_out_active; // 0Dh Clock_out_control bit 7 FE
FreqClockOut_t clock_out; // 0Dh Clock_out_control 0..3
int ReadClockOut();
int WriteClockOut();
};
int Pcf8563::ClockOutForTrimmer() // clock out 1Hz
{
// CLKO(clock out) to adjust trimmer
clock_out = Pcf8563::fco_1Hz;
clock_out_active = true;
if (WriteClockOut() != 0) return 1;
// INT(interrupt) of pulse mode for demonstration
const bool enable_interrupt = true;
const bool enable_pulse_mode = true;
if (SetTimer(5.0) == 0.0) return 1; // 5.0sec
if (EnableTimerInterrupt(enable_interrupt, enable_pulse_mode) != 0) return 1;
return 0;
}
int Pcf8563::ReadClockOut()
{
if (ReadReg(0x0d, 1) != 1) return 1;
clock_out_active = m_reg[0x0d] & 0x80;
switch (m_reg[0x0d] & 0x03) {
case 0: clock_out = fco_32768Hz; break;
case 1: clock_out = fco_1024Hz; break;
case 2: clock_out = fco_32Hz; break;
case 3: clock_out = fco_1Hz; break;
default: break;
}
return 0;
}
int Pcf8563::WriteClockOut()
{
m_reg[0x0d] = clock_out;
if (clock_out_active)
m_reg[0x0d] |= 0x80;
return WriteReg(0x0d, 1);
}
#6. Grove
Grove は Seeed 社26 の製品群27です。製品の一覧表28があります。特徴の 1 つは独自の Grove コネクタです。基板側の Grove コネクタは単体でも販売29されています。Grove コネクタの詳しい情報30があります。
##6.1. ピンアサイン
Grove コネクタのピンアサインの確認にはベースシールド31の情報が便利です。製品写真と回路図32、基板実装図33があります。ピンアサインは以下です。
Pin | 1 | 2 | 3 | 4 |
---|---|---|---|---|
I2C | SCL | SDA | VCC | GND |
UART | RX | TX | VCC | GND |
Digital | Dx | Dy | VCC | GND |
Analog | Ax | Ay | VCC | GND |
##6.2. シンボル・フットプリント
プリント基板設計に必要な Grove コネクタのシンボルやフットプリントを Seeed が提供34しています。KiCad35 の場合、以下が Grove コネクタのデータです。
シンボル: OPL_Connector.lib
- GROVE-CONNECTOR-DIP-90D_4P-2.0_
- GROVE-CONNECTOR-DIP_4P-2.0_
- GROVE-CONNECTOR-SMD-90D_4+2P-2.0_
- GROVE-CONNECTOR-SMD_4+2P-2.0_
フットプリント: OPL_Connector.pretty
- HW4-2.0
- HW4-2.0-90D
- HW4-SMD-2.0
- HW4-SMD-2.0-90D
KiCad で使用してみると、OPL のシンボルはピンの属性に起因して DRC でエラーとなることがあり使いにくいです。シンボルには一般の 4p コネクタを使用して、OPL のフットプリントを割り当てました。
- OPL(Open Parts Library)
- DRC(Design Rule Check)
##6.3. 電源
一般的な Grove モジュールの電源は +5V または +3.3V であり、信号も電源に合わせたレベルになります。M5Stack シリーズの Grove コネクタの電圧は特殊です。M5Stack の信号レベルは +3.3V であり、I2C 信号も +3.3V にプルアップするのが妥当です。しかし電源には +5V が供給されています。M5Stack の Grove モジュールの回路図を見ると、+5V を +3.3V に変換する LDO が多く見つかります。一方で、例えばモジュール ENV36 では温湿度センサ DHT12 に +5V を、温湿度気圧センサ BME280 に 3.3V を供給し、+5V にプルアップした I2C を両方に接続する割り切った設計です。
PCF8563 基板では I2C の信号レベルに合わせ、電源を +3.3V に変換します。他のモジュールがバス接続されることを考慮し I2C のプルアップはありません。どこかで 3.3V にプルアップされることを期待します。
- LDO(Low DropOut) 少ない電圧差で使える電源用 IC(レギュレータ)
- ENV(Environment)
##7.1. Grove コネクタ
I2C バスの数珠繋ぎを考慮して、2 個並列 (J1, J2) にしました。
##7.2. LDO
LDO (U1) には、入力・出力に積層セラミックコンデンサを利用可能な XC620637 を選択しました。
##7.3. バックアップ電池と逆流防止ダイオード
Grove コネクタの +5V を LDO で変換した +3.3V と、バックアップ電池の +3.3V は逆流防止のためのダイオード (D1, D2) を用いて合流させます。ダイオードの順方向電圧 $V_F$ は、PCF8563 に供給する電源電圧 $V_{DD}$ を低下させます。逆方向のリーク電流 $I_R$ は、電源オフ時には電池を消耗させ、電源オン時には逆流が電池の劣化を早めます。ショットキーバリアダイオード( SBD )は $V_F$ は小さいが $I_R$ が大きく、今回の用途には向きません。接合型ダイオードは $I_R$ が非常に小さく好適です。$V_F$ が一般には 0.6 ~ 0.7V と大きいですが、実は PCF8563 の様に電流が少ない場合は 0.2 ~ 0.3V で済みます38。ダイオードには一般的な小信号スイッチングダイオード 1N4148W39 を使用します。
バックアップ年数
PCF8563 のデータシートによると消費電流は、インターフェース停止、CLKO 停止、-40 ~ +85 ℃、3V で最大 650nA とあります。CR2032 の共通仕様は 220mAh です。単純計算では 38 年以上になります。電池の寿命の 5 年間はバックアップ可能と考えられます。本来は、もっと小さな電池を選択できます。
\frac{220mAh}{650nA} = 338kh\\
338kh ✕ \frac {1}{24h/day} ✕ \frac{1}{365day/year} = 38.6year
##7.4. トリマコンデンサ
32,768Hz の水晶振動子の周波数の微調整は、PCF8563 の OSCI ピンに接続するコンデンサで行います。PCF8563 のデータシート18によると、容量固定であってもプリント基板の評価を行って平均的に適切な容量を選べば $\pm$ 5ppm 程度の誤差に収まる、とあります。月差 $\pm$ 15 秒程度、年差 $\pm$ 3 分程度に相当します。固定コンデンサをトリマコンデンサに置き換えて調整することで、さらなる精度が期待できます。基板上には固定コンデンサ (C4) とトリマコンデンサ (C5) の両方をパターンを用意し、どちらか一方を選択して実装することにしました。秋月電子通商で入手可能なトリマコンデンサ40を使用します。調整ドライバの影響を小さくするため、ローター側がマイナス極41となっており、これを GND に接続します。
- OSCI(OSCillator In)
- GND(Ground)
##7.5. LED とプルアップ
クロック出力 (CLKO) と 割込み信号出力 (INT) は、オープンドレインです。信号を使用する側で都合に合わせて処置すべきですが、便宜のため 3.3V へのプルアップ抵抗 (10kΩ) を基板上に載せました。邪魔になる場合には抵抗を外します。アマチュアらしく LED(緑・赤)で信号を見える様にしました。いずれも Low レベルで点灯します。クロック出力に 1Hz を出力すると LED(緑)の点滅がわかります。アラームまたはタイマーの割込み信号がオンの間 LED(赤)が点灯します。Pulse Mode では周期的に一瞬点灯します。邪魔になる場合には抵抗または LED を外します。プルアップ抵抗も LED も Grove 側からの電源 3.3V で動作し、バッテリの無駄な消耗を防ぎます。LED は順方向電圧 $V_F$ が大きく、信号電圧が不足します。LED とは別にプルアップ抵抗が必要です。
- LED(Light Emitting Diode)
RTC 基板
#8. 調整
32,768Hz の発振周波数をトリマコンデンサで合わせ込むためには基準となる測定器が必要です。本格的な測定器が使えるに越したことはありません。趣味の電子工作としては、安価な測定器でそれなりの精度改善を目指します。
秋月電子通商で販売されている周波数カウンターキット42を利用してみました。電圧で微調整可能な温度補償付きの高精度水晶振動子 (VCTCXO) 12.8MHz を使用し、周波数誤差 $\pm$ 1ppm、温度特性 $\pm$ 3ppm とあります。VCTCXO の微調整には GPS 受信機43の 1PPS 信号を使用する方法が示されています。1PPS は、原子時計に基づく 20ns (0.02ppm) レベルの精度が期待できる模様です44。1PPS の周期で VCTCXO の発振周波数をカウントし 12,800,000 になる様に所定の半固定抵抗を調整します。
これを応用し RTC の 32,768Hz の発振周波数を調整できます。PCF8563 のクロック出力 (CLKO) を 1Hz に設定し 1PPS の代わりに入力します。表示が 12,800,000 になる様にトリマコンデンサを調整します。最小桁は落ち着かない模様で、$10/12,800,000 = 0.78ppm$ と、その時点における誤差を 1ppm 程度で合わせることができます。温度条件などが良好であれば、月差 $\pm$ 3 秒程度、年差 $\pm$ 30 秒程度が期待できるかもしれません。
- VCTCXO(Voltage Controlled, Temperature Compensated Crystal Oscillator)
- 1PPS(One Pulse Per Second)
GPS モジュール(受信中)
8桁周波数カウンターキット(1PPS 信号で調整)
RTC のトリマーを調整 (M5Stack Core Basic)
RTC のトリマーを調整 (M5Atom + RGB LED 276 基板)
#9. おわりに
RTC は、古典的なデバイスですが、いざ自分で基板を作って使うとなると様々な要素で立ち止まることになりました。LED とプルアップ、バッテリバックアップのダイオードなどの極基本的なハードウェア事項、I2C の Stop などのインタフェース仕様、PCF8563 の実際の動作、クラスインスタンスのポインタ渡しなどの C++ プログラミングにいたるままで、広範囲に基本的な事項を学ぶことができました。また、以前に購入して温存していた秋月電子通商の周波数カウンタキットや GPS モジュールを有効利用できたときには感動しました。