マイコンでカレンダーや時刻を参照するには、リアルタイムクロック(RTC)を使います。上の写真でボタン電池が見えるボードがそれです。またブレッドボードの右半分は3V電源から5Vへの昇圧器で、本体は左半分だけです。RTCは今や非常に正確で、1年間放置しても数秒しか狂いません。RTCのICそのものはあらゆる機器で大量に使われるため、値段は非常に安いものです。DS3231などのICのブレークアウトモジュールは、海外ネット通販では百円程度です。
接続はI2Cインターフェイスなのでマイコンのピンは2つしか使いませんし、そのピンを他のI2Cデバイスと共有します。
しかし、最初はその中には時刻もカレンダーも入っていませんから、自分で設定が必要です。一度設定すればバックアップ電池が切れない限り、数年は動き続けます。そういうわけで、書き込みをする際にだけ、ブレッドボードを使って泥縄式に回路を作る方も多いかもしれません。
短時間とはいえ、いちいち作る回数が重なると無駄な時間となりますから作っておくほうが良いでしょうね。筆者はだいぶ前にPICマイコンで作り、それをずっと使っています。しかし、人にやり方を聞かれて説明するのは大変です。なにせアセンブラーですし、I2Cインターフェイスも手作りコーディングしています(ピン割当てを自由にすることが目的です)から。そこで、今回はArduinoでの作り方を書いておきます。
Arduinoを使って、さきほどブレッドボードに簡単に作ったのが最初の写真。このような試作は何度も行なっているわけなのですが、最後は邪魔になるのでほぼ毎度ブレッドボードから外して片づけてしまいます。そうこうするうち、コーディングを見失ったりして時間を無駄にすることがしばしばばあります^^;
次に回路を示します。殴り書きで大変きたなくてすみませんが。
写真ではATmega328P単体で使っていますが、スペースや電流節約のために最近は毎度このようにします。といってもArduino-UNO、あるいは互換機(SMTではなくDIPが使われているもの)でプログラミングして、AVRマイコンだけを抜き取ればよいもの。そのためにArduino-UNOに無圧ソケットの下駄をはかせています。とりだしたATmega328Pのピン配置は、例えば秋月電子のArduinoブートローダー入りATmega328Pに対応図が張り付けられていたりしますから、参考にされてください。Googleで検索しても画像が簡単にでてくるでしょう。
ブレッドボードにタクトスイッチ(プッシュボタン)をつけるのはとても不安定ですが、最近では4ピンではなく長い2ピンのブレッドボードにも使えるタクトスイッチがあります。それを使えば安定して動かせます。下の写真の右側です。
/*******************************************************************************
RTC-Writer-LCD-V01-01s (c) 2018 Akira Tominaga. All rights reserved.
Version 01.01s Sept.12, 2018
Function
Read Real-time-clock and display date and time.
Update RTC settings with 4 buttons; Mode, -, +, and Set
Revisions
V01-01s: Year extended up to 2099 (former < 2080)
Input/Output
1) IIC Interface for RTC and LCD
Addr 0x68 (H'D0'for 8bits) = Real time clock DS3231
Addr 0x27 (H'4E'for 8bits) = Blue LCD 16x2
2) Four Buttons
Mode, -, +,and Set buttons at D5-D8 for easy wiring
* *****************************************************************************/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// define Blue LCD for adr,en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
#define RTC_addr 0x68
#define btMode 7 // button Mode
#define btMinus 5 // button Minus(-)
#define btPlus 6 // button Plus(+)
#define btSet 8 // button Set
//
#define mdI 0 // Initial mode
#define mds 1 // ss
#define mdm 2 // mm
#define mdh 3 // hh
#define mdW 4 // WW=Day of Week
#define mdD 5 // DD
#define mdM 6 // MM
#define mdY 7 // YY
char modeChr[9] = {'I', 's', 'm', 'h', 'W', 'D', 'M', 'Y'};
byte curMode = mdI; // current mode indicating from mdI to mdY
//
char strYMDHMS[17]; // work area to diplay calendar and clock
byte vR[8]; // values of RTC registers
int vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0};
int maxvI[8] = { 0, 59, 59, 23, 8, 31, 12, 2099};
int minvI[8] = {0, 0, 0, 0, 0, 1, 1, 0};
byte dspTime = 1; // display time at 1st or btSet used
/********************************************************
User Functions
********************************************************/
// read Real-Time-Clock
void rRTC(void) {
Wire.beginTransmission(RTC_addr);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(RTC_addr, 7);
while (Wire.available() < 7) {} // wait for data ready
for (int i = 1; i < 8; i++) {
vR[i] = Wire.read();
}
Wire.endTransmission();
}
// write-Real-Time-Clock
void wRTC(void) {
Wire.beginTransmission(RTC_addr);
Wire.write(0x00);
for (int i = 1; i < 8; i++) {
Wire.write(vR[i]);
}
Wire.endTransmission();
Serial.println("wrote to RTC");
}
// display "mode=vI", vI at Column10 line0
void dspMd(void) {
lcd.setCursor(0, 0); // position at Col 0,Line 0
lcd.print(" "); // position at Col 5,Line 0
lcd.print(modeChr[curMode]);
lcd.print(modeChr[curMode]); //
lcd.print(" = "); // positioned at Col.10
lcd.print(vI[curMode]); // display vI
lcd.print(" "); // clear next 3 chars
lcd.setCursor(15, 0); // in case prior mode was year
lcd.print(" "); // clear last two chars
}
// convert RTC-format to Integers
void cR2I(void) {
vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
vI[mdW] = vR[mdW];
vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111) + 2000; // changed @V01-01s
}
// convert Integers to RTC-format
void cI2R(void) {
vR[mds] = (vI[mds] / 10) * 16 + vI[mds] % 10;
vR[mdm] = (vI[mdm] / 10) * 16 + vI[mdm] % 10;
vR[mdh] = (vI[mdh] / 20) * 32 + ((vI[mdh] % 20) / 10) * 16 + vI[mdh] % 10;
vR[mdW] = vI[mdW];
vR[mdD] = (vI[mdD] / 10) * 16 + vI[mdD] % 10;
vR[mdM] = (vI[mdM] / 10) * 16 + vI[mdM] % 10;
vR[mdY] = ((vI[mdY] - 2000) / 10) * 16 + vI[mdY] % 10;
}
// display current Integer contents
void dspInt(void) {
sprintf(strYMDHMS, "%02d-%02d%02d %02d:%02d:%02d", vI[mdY] - 2000, vI[mdM], vI[mdD], vI[mdh], vI[mdm], vI[mds]);
// Display line 1
lcd.setCursor(0, 1); // position at Col 0,line 1
lcd.print(strYMDHMS);
}
/********************************************************
Setup
********************************************************/
void setup() {
pinMode(btMode, INPUT_PULLUP);
pinMode(btMinus, INPUT_PULLUP);
pinMode(btPlus, INPUT_PULLUP);
pinMode(btSet, INPUT_PULLUP);
Serial.begin(9600);
Wire.begin();
delay(10);
lcd.begin(16, 2); // Initialize LCD including Wire.begin()
lcd.backlight(); // Set LCD backlight on
//
lcd.setCursor(0, 0); // position at Col 0,line 0
lcd.print("YY-MMDD hh:mm:ss");
rRTC(); // read RTC device
cR2I(); // convert RTC-format to Integers
dspInt(); // display values of the Integers
}
/********************************************************
Loop
********************************************************/
void loop() {
// Display line 0
// *** check Mode switch ***
if (digitalRead(btMode) == LOW) {
while (digitalRead(btMode) == LOW) { // avoid chattering
delay(1);
}
if (curMode > mdI) { // decrease curMode
curMode = curMode - 1; // when curMode>=1
}
if (curMode <= mdI) { // if curMode<=0 then set mdY(=8)
curMode = mdY;
}
dspMd(); // display current mode "XX="
dspTime = 0; // no RTC time,but display vI time
// followed by +,-,Set,or Mode in the next loop
}
// *** ceck Set button ***
if (digitalRead(btSet) == LOW) {
while (digitalRead(btSet) == LOW) { // avoid chattering
delay(1);
}
cI2R(); // convert integer vIs[] to RTC vRs[]
wRTC(); // write RTC
rRTC(); // read RTC data
cR2I(); // convert from RTC format to Integer
curMode = mdI; // reset current mode to mode-initial
dspTime = 1; // Set was used, hence display time
lcd.setCursor(0, 0); // position at Col 0,line 0
lcd.print("YY-MMDD hh:mm:ss");
}
// *** check + button ***
if (digitalRead(btPlus) == LOW) {
delay(200); // avoid chattering and accept long-push
if (vI[curMode] < maxvI[curMode]) {
vI[curMode] = vI[curMode] + 1; // increase one
}
dspMd(); // and display it
}
// *** check - button ***
if (digitalRead(btMinus) == LOW) {
delay(200); // avoid chattering and accept long-push
if (vI[curMode] > minvI[curMode]) {
vI[curMode] = vI[curMode] - 1; // decrease one
}
dspMd(); // and display it
}
Serial.print ("Loop end. Mode was ");
Serial.println (modeChr[curMode]);
if (dspTime == 1) { // when once Set used
rRTC(); // then read RTC
cR2I(); // and convert vR to vI[]
}
dspInt(); // display integer value vI[]
delay(100);
}
/* End of Sketch */
RTCに収容されるデータのフォーマットは特殊ですが、どれも1命令で整数に加工できる簡単なものです。逆も同じです。上のコーディングではそれを関数にしています。cR2I(RTCフォーマットから整数)、cI2R(整数からRTCフォーマット)がそれです。
上のコーディングでは入力データのチェックは完全には行っていません。必要な方はお好きなように加工してください。
曜日設定はLCDには常時は表示していませんが、操作は行います。内容としては日曜から土曜を1~7とします。データシートを詳しく読むと1週間の中で毎日1が加算されても数値が0~9に収まるような値を設定すればよいものです。例えばその日が日曜なら0か1などをいれておけば土曜日に6か7になって、日曜にまた元に戻ります。途中の日ならそれに合わせ入れます。
今回はアラームを使わないので、次の表のデータだけがセットされれば良いものです。
表中のCenturyには、0をセットしておきます。このデバイスでは年が2桁で扱われるため、99年が桁上がりした際にデバイスがここへ1を入れてくれます。
うるう年の2月29日は自動調整されます。ただし4で割れる年でも2100年、2200年、2300年はうるう年ではありませんが、おそらくそこまでサポートされていないのではないでしょうか。製品仕様では「2100年に至るまで(up to 2100)」大丈夫と書いてありますが、up toの意味が微妙なところですね。
試すには2100年2月28日23時59分を入れて、1分後の日付けがどうなるかをみればすぐにわかることですが、私はまだ試していません。それをためす場合には、上記のコーディングでは放置してある(2099年までしか扱っていない)ため、世紀を考慮する必要があります。そのためには、共通ルーチンのcR2Iでの最後の行、年の計算を
vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111) + 2000 + ((vR[mdM] & B10000000) / 128) * 100;
として、さらに共通ルーチンI2Rの最後にある、年の逆計算を
vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111) + 2000 + ((vR[mdM] & B10000000) / 128) * 100;
に直しておく必要があります(これらはテストしておりません。もしも間違いがありましたらご容赦をお願いします)。
なお、最後はおまけの余談です。
うるう年の計算がすぐわかるために:
現在のルールは1年が地球の公転周期÷自転周期=365.2422日であることから決まっています。
①まず4で割れる年をうるう年とします。 そうすると365日に0.25日が追加されますから、1年あたり0.078日足しすぎです。
②それを引くため100で割れる年を除外します。そうすると0.01日除外されますから、1年あたり0.0022日が不足します。
③それを足すために400で割れる年を再度加えます。そうすると0.0025日が追加されますから、0.0003日(1万年に3日)が足しすぎです。
そこまで行くと365.2422自体も少し変化しているかもしれませんね。
というわけで、ルール自体を覚えるより365.2422日を覚えておく方が確実かもしれません。
以上、簡単ですがなんらかのお役に立てば幸いです。掲載したプログラムはもちろんテスト済のつもりですが、なにせ半日の作業のみです。もし間違い等があればレポートいただくとたいへん助かります。
(c)2018 Akira Tominaga, All rights reserved.