1.システムクロックの所要機能
次の要件を満たすクロックを製作します。
①バッテリー・バックアップ機能
電源OFFの状態でもバッテリーによって日時を刻み続けること。
②日付・曜日設定機能
初期起動時または必要に応じて、日付と曜日の設定ができること。
③時刻の自動補正機能
所定の時間間隔で公開NTP(Network Time Protocol)サービスに接続して時刻を取得し、正しい時刻を
維持すること。
④その他の付帯要件
・公開NTPサービスへの接続にはWi-Fi利用を前提とする。
・複数のセンサー類の接続に配慮してI2Cプロトコルが利用できること。
2.使用する部品
(1)部品の規格・仕様
上記の要件を満たすクロックとして、秋月電子通商で販売されている「DS1307 I2Cリアルタイム・クロック・モジュール」を使用します。価格はバックアップ用電池込みで750円です。
〔主な仕様〕
DS1307を利用したリアルタイム・クロック(RTC)モジュールです。
バックアップ用リチウム充電池LIR2032を搭載済みです。
・電源電圧 | : 5V |
・I/O | : I2C |
・時計/カレンダ(8byte) | : 時分秒、年月日、曜日(閏年補正 |
2100年までのカレンダー) | |
・時刻表現 | : 12/24時制 |
・ユーザーメモリー(56byte) | : バッテリーバックアップRAM領域 |
・基板サイズ | : 28mm × 25mm × 8mm |
(2)RTCモジュールの内部構造
DS1307は64バイトのメモリー・レジスターをもっていて、その上位00~07の8バイトに日時情報を保持しています。次の表はメーカーの「DS1307データシート」から抜粋したレジスターの詳細です。これに沿ってレジスターの内容を参照したり書き換えることでRTCを操作することができます。
3.組み立てと配線
(1)RTCモジュールの端子と配線
RTCモジュールの端子と接続先は図のとおりです。ESP-WROOM-02側はI2C通信のために、デフォルト・ピンIO4(SDA)とIO5(SCL)を使用します。
RTCモジュールを設置するために新たなブレッドボードを準備して「計測ボード」とします。このボードには、計測用の各種センサーを組み付ける予定です。写真・左のようにその一角にRTCモジュールを取り付けます。配線は、VccとGndをそれぞれブレッドボード上側の+-ラインにつなぐだけです。
MCPボードは、写真・右のようにIO5ピンからオレンジ色のジャンパーワイヤで右へ配線します。これがクロック線になり、同色のジャンパーコードでRTCのSclへ接続します。データ線はIO4ピンの位置から黄色のジャンパーコードでRTCのSdaに接続します。本来はI2CのSDAとSCLピンには動作を安定させるためにプルアップ抵抗を接続すべきですが、ここでは省略しています。
(2)ボード間の配線
次に、計測ボードの配線とMCUボード・専用電源との接続を示します。MCUボードには3.3V、計測ボードには5Vを、間違えないようにジャンパーコードで接続してください。
〔回路図〕
4.ソフト開発のポイント
システムクロックに必要な機能を実現するために、ソフト面では次のようなテーマと取り組むことになります。
(1)I2Cプロトコルの利用
I2CはInter-Integrated Circuitの略で、アイ・スケア・シーとも呼ばれます。2線式のシリアル通信方式で、電源とGNDを確保すれば、SDA、SCLの2本に複数のI/Oを接続できるのが特長です。GPIOの数が少ないESP-WROOM-02では重宝な機能です。
I2Cに接続されるデバイスはマスターとスレーブに分かれ、送受信は1本の信号線(SDA)で交互に通信します。これとは別にクロック・ライン(SLC)があり、クロックはマスターが出力します。スレーブのデバイスにはそれぞれ1つまたは複数の固有アドレスが割り振られており、マスターはデバイス・アドレスを指定してスレーブと通信します。
ArduinoではTWI(Two Wire Interface)と呼び、標準ライブラリーWireを利用すると細かい手順を知らなくても通信処理ができます。
以下では、シリアルモニターの入力エリアに'hh/mm/dd/w'形式で年月日と曜日コード(日曜日が1から始まり7までのコード)をキー入力して[Enter]キーを押すと、DS1307の日付レジスターにその内容を設定するスケッチを例示します。
〔ポイント説明〕
・1行目: I2Cを利用するために、WireライブラリのヘッダーファイルWire.hをインクルードします。
・4行目: DS1307のスレーブアドレスはこのように定義します。
・7行目: 初期化処理で、通信速度bps(ビット/秒)を指定してシリアルモニターを初期化します。
・10行目: あわせて、Wireライブラリを初期化して、I2Cバスにマスターとして参加するよう指示します。
Wire.begin(address)の引数addressを省略するとマスターモードになります。
・15行目: シリアルモニターの入力を監視し、[Enter]キー押下で所定のデータ長なら次行を実行します。
・16行目: キー入力された文字列を読み込みます。
・17行目: 入力された文字列を伴ってsetDateメソッドを呼びます。
・25行目: ここからsetDateメソッドのコードが始まります(細かい内容は省略します)。
・40行目: WireライブラリのbeginTransmissionメソッドで、指定アドレスに対するI2C通信を開始します。
・41~45行: マスターからの要求に応じて、レジスター位置0x03から曜日、日、月、年の設定データを送信、
通常はendTransmissionまでキューイングされます。
・46行目: 通信を終了し、write()によってキューイングされたデータを実際に送信します。
#include <Wire.h>
// I2C Address
#define DS1307_ADDRESS 0x68 // Realtime clock
void setup() {
Serial.begin(115200);
// Prepare I2C protocol.
Wire.begin();
}
void loop() {
// Check date input ('hh/mm/dd/w') from serial buffer.
if (Serial.available() >= 8) {
String date = Serial.readString();
setDate(date);
}
}
/*
* Set DS1307 date register.
* Argument: (String)Time string 'yy/mm/dd/w'.
*/
void setDate(String sDate)
{
byte bValue = 0x03; //Top Address
String buf = sDate;
buf.replace("/", ""); buf.trim();
if (buf.length() >= 6) {
byte year = (buf.charAt(0) << 4) + (buf.charAt(1) & 0x0f);
byte month = (buf.charAt(2) << 4) + (buf.charAt(3) & 0x0f);
byte day = (buf.charAt(4) << 4) + (buf.charAt(5) & 0x0f);
byte week;
if (buf.length() == 7) {
week = (buf.charAt(6) & 0x07);
if (week < 0x01 || week > 0x07)
week = 0x01;
}
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write(bValue);
Wire.write(week);
Wire.write(day);
Wire.write(month);
Wire.write(year);
Wire.endTransmission();
}
}
(2)RTCレジスターの操作
DS1307のメモリー・レジスターに日付や時刻を設定したり、現在の日時を取得するために、ひとつのデータ型と次の7つのメソッドを作成します。
①構造体ClockData型の定義
struct ClockData {
byte year;
byte month;
byte day;
byte week;
byte hour;
byte minute;
byte sec;
byte ctrl;
};
②メソッドの作成
・void getDateTime(ClockData *dt)
ClockData構造体にDS1307の日時情報を取り出す。
・String tellDayOfWeek(byte num)
日曜が1から始まる曜日コードから英文字の省略曜日名を通知する。
・void setTime(String sTime)
'hh:mm:ss'形式の時刻文字列からDS1307レジスターに時刻情報を設定する。
・void setDate(String sDate)
'yy/mm/dd/w'形式の文字列からDS1307レジスターに日付・曜日情報を設定する。
ここでwには日曜が1から始まる曜日コードを指定する。
・String editDateTime(ClockData dt)
ClockData構造体に収納されている日時情報から、メソッドeditDateとeditTimeを使用して、
'yyyy.mm.dd hh:mm:ss'形式の文字列に編集する。
・String editTime(ClockData dt)
ClockData構造体に記録された時刻情報を 'hh:mm:ss'形式の文字列に編集する。
・String editDate(ClockData dt)
ClockData構造体に記録された日付情報を 'yyyy:mm:dd'形式の文字列に編集する。
ここではgetDateTime,、setTime、setDateの3つのメソッドを簡単に見ておきましょう。
〔ポイント説明〕
・4行目: ここから引数にClockData型のポインターを与えられたgetDateTimeメソッドが始まります。
ポインターなので、これを通して取得データを呼び出し元に通知することができます。
・9行目: beginTransmissionとendTransmissionに挟んで通信位置、DS1307_ADDRESSからのオフセット
を指定します。ここではゼロ、つまりDS1307_ADDRESSの直後から読み込むよう指示しています。
・11行目: DS1307_ADDRESSのスレーブから7バイトのデータを読み込むよう指示しています。
・12行目: ClockData型データのsecに、スレーブから読み込んだ1バイト、つまり「秒」を格納します。
以降では引き続き「分」「時」「曜日コード」「日」「月」「年」を読み出しています。
・25行目: ここから'hhmmss'形式で指定されたデータを伴ったsetTimeメソッドが始まります。
・31~33行: 第1文字目の数字を4ビット左シフトして、第2文字目の4ビットと合成して「時」データと
します。第2文字目と第3文字目も同様にして「分」データとします。「秒」も同様。
・35行目: DS1307_ADDRESSに書き込むレジスターの先頭を指定し、続いて「秒」~「時」を書き込む。
・47行目: ここから'yymmddw'形式で入力されたデータを伴ってsetDateメソッドが始まります。
メソッド内では、先と同様にしてDS1307_ADDRESSの3バイト目から順番に日付を書き込みます。
/*
* Read data from DS1307 Register
*/
void getDateTime(ClockData *dt)
{
int iValue = 0;
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write(iValue);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDRESS, 7);
dt->sec = Wire.read();
dt->minute = Wire.read();
dt->hour = Wire.read();
dt->week = Wire.read();
dt->day = Wire.read();
dt->month = Wire.read();
dt->year = Wire.read();
}
/*
* Set DS1307 time register.
* Argument: (String)Time string 'hh:mm:ss'.
*/
void setTime(String sTime)
{
byte bValue = 0x00; //Top Address
String buf = sTime;
buf.replace(":", ""); buf.trim();
if (buf.length() == 6) {
byte hour = (buf.charAt(0) << 4) + (buf.charAt(1) & 0x0f);
byte minute = (buf.charAt(2) << 4) + (buf.charAt(3) & 0x0f);
byte sec = (buf.charAt(4) << 4) + (buf.charAt(5) & 0x0f);
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write(bValue);
Wire.write(sec);
Wire.write(minute);
Wire.write(hour);
Wire.endTransmission();
}
}
/*
* Set DS1307 date register.
* Argument: (String)Time string 'yy/mm/dd/w'.
*/
void setDate(String sDate)
{
byte bValue = 0x03; //Top Address
String buf = sDate;
buf.replace("/", ""); buf.trim();
if (buf.length() >= 6) {
byte year = (buf.charAt(0) << 4) + (buf.charAt(1) & 0x0f);
byte month = (buf.charAt(2) << 4) + (buf.charAt(3) & 0x0f);
byte day = (buf.charAt(4) << 4) + (buf.charAt(5) & 0x0f);
byte week;
if (buf.length() == 7) {
week = (buf.charAt(6) & 0x07);
if (week < 0x01 || week > 0x07)
week = 0x01;
}
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write(bValue);
Wire.write(week);
Wire.write(day);
Wire.write(month);
Wire.write(year);
Wire.endTransmission();
}
}
(3)Wi-Fiの利用
そもそもWi-Fiモジュールだから、Wi-Fi、つまり無線LANを存分に利用することができます。
以下ではそのポイントを例示します。
〔ポイント説明〕
・1行目: Wi-Fi機能を利用するために、ヘッダーファイルESP8266WiFi.hをインクルードします。
・4~5行: Wi-Fiルーターに接続するためのSSIDとパスワード(暗号化キー)を指定します。
・9行目: SSIDと暗号化キーによりWiFiに接続します。
・10行目: Wi-Fi接続が完了するまで待ちます。
・19行目: Wi-Fiは時刻自動調整ロジックの中で間接的に使用されます。
#include <ESP8266WiFi.h>
// WiFi connection
const char* ssid = "your ssid";
const char* password = "your password";
void setup() {
// Prepare WiFi system.
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\r\nWiFi connected!");
}
void loop() {
// Adjust the time automatically at specified intervals.
}
(4)NTPサービスの利用
NTP(Network Time Protocol)は、時計を正しい時刻に同期するための通信プロトコルです。日本でのNTPサービスは国立研究開発法人「NICT」が行っていて、公開サーバーにより日本標準時を配信しています。
NTPサービスを利用する方法はいろいろありますが、ここでは、 SG Labs『ESP-WROOM-02 の時刻合わせ』を参考にさせていただきました。
次のサイトにアクセスしてzip形式ファイルをダウンロードし、IDEにインストールします。
https://github.com/arduino-libraries/NTPClient
Arduino IDEを起動して[スケッチ]をクリックし、表示されたメニューから「ライブラリをインクルード」をポイントします。さらに表示されるサブメニューの「zip形式のライブラリをインクルード」をクリックします。ファイルの選択ダイアログが開くので、先にダウンロードしたzipファイルを指定して[開く]ボタンをクリックします。
以上の手順でIDEにライブラリーが組み込まれて、もう一度「ライブラリをインクルード」をポイントすると、サブメニューにNTPClientが登録されているのを確認できます。
NTPサービスを利用した時刻調整ロジックを例示します。
反復処理loopの中で調整時刻が到来しているかを常時監視していて、到来すれば時刻合わせを行って次の調整時刻を更新する仕組みです。
〔ポイント説明〕
・2~3行: NTPClientとWi-Fi上でのUDP(User Datagram Protocol)を利用するためのインクルードです。
・10~11行: WiFiUDPのインスタンスを作成し、これを使ってNTPClientのインスタンスを作成します。
・12行目: これがNTPサーバーのURLです。
・13行目: UTC協定世界時(Universal Time, Coordinated)から日本標準時JST(Japan Standard Time)
への変換用です。「UTC+9」がJSTになるので、秒数に換算した値を設定します。
・14行目: テスト用なので時刻調整間隔を2分に設定しています。実際は1日に1~2回で十分でしょう。
・43行目: 初期化処理でUDPのインスタンス、NTPサーバーのURL、UTC→JST変換秒数を引数
にしてNTPClientを初期化します。
・49~52行: getTransitTimeメソッドで零時からの経過分数を求めて、調整時刻が到来していれば
NTPサーバーに時刻を問い合わせてJSTを取得します。
・54行目: ここでDS1307の時刻情報を更新しています。
・55行目: 零時からの経過分数に時刻調整間隔を加えて、次の調整時刻とします。
・67行目: 零時からの経過分数を求めるgetTransitTimeメソッドです。
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
// WiFi connection
const char* ssid = "your ssid";
const char* password = "your password";
// Time adjust
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
const char* sNtpUrl = "ntp.nict.jp";
int iNtpOffset = 32400; // UTC + 9h (3600sec * 9h)sec.
int iNtpInterval = 2; // minutes
int iAdjustTime = 0;
const int iMaxmin = 1440; // Minutes of day.
// DS1307 Clock data
struct ClockData {
byte year;
byte month;
byte day;
byte week;
byte hour;
byte minute;
byte sec;
byte ctrl;
};
ClockData dtClock;
void setup() {
Serial.begin(115200);
// Prepare WiFi system.
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\r\nWiFi connected!");
// Recreate NTPClient object.
timeClient = NTPClient(ntpUDP, sNtpUrl, iNtpOffset);
}
void loop() {
// Adjust the time automatically at specified intervals.
getDateTime(&dtClock);
int now = getTransitTime(editTime(dtClock));
if (now >= iAdjustTime) {
if (timeClient.update()) {
String strJST = timeClient.getFormattedTime();
Serial.print("JST time = "); Serial.println(strJST);
setTime(strJST);
iAdjustTime = now + iNtpInterval;
if (iAdjustTime >= iMaxmin)
iAdjustTime -= iMaxmin;
}
}
}
/*
* Get elapsed time(minutes) based on zero hour.
* Argument: (String)Time string 'hh:mm:ss'
* Return: (Integer)Transit time (minutes)
*/
int getTransitTime(String strTime)
{
return strTime.substring(0, 2).toInt() * 60 + strTime.substring(3, 5).toInt();
}
5.スケッチの実行
(1)実行結果と評価
シリアルモニターを表示させた状態でスケッチを実行すると、少し待ってWi-Fi接続が完了したメッセージ「WiFi connected!」が表示されます。すぐに時刻調整が始まり、ここでは11時10分53秒に調整されています。
それから2分経過後の11時12分ちょうどに、次回の時刻調整が実行されていることがわかります。DS1307のクロックはかなり正確なので、実際は頻繁に調整する必要はありません。NTPサーバーに過度な負担をかけないためにも、1日1回程度で十分ではないかと思われます。
これで、最初にかかげた所要機能すべてを満足するシステムクロックが完成しました。ライブラリー化すれば、クロックのインスタンスを生成してgetDateTimeと命じるだけで正確な日時情報を取得するようにできるのですが、今回はこのままにして先を急ぐことにします。
(2)スケッチ全景