google-site-verification: google3bd66dd162ef54c7.html
FC2ブログ

DS3231と0.96インチOLEDを使ったデジタル時計

このところ取り組んでいる RTC いじりのたぶん最終回。前回の記事では DS1302 を取り上げましたが、今回はもう一度 DS3231 に戻って、もっと完成度の高い時計に仕上げてみます。
ちなみに、以前のDS1302の記事はこちらDS3231の記事はこちら、とこちらです。

時計の表示を OLED(0.96インチ)に表示するのは以前の記事と同じで、回路図は次の通りです。

▼回路図
DS3231を使った時計の回路図

I2C バスのレベルが 3.3V と 5V の二種類あるので、その間に FET のレベルコンバーターを入れました。時刻合わせのためのスイッチと、割り込みのための配線があるのは以前の記事と同じです。なお、このFETを省略してバスを直結しても、ちょっと試すくらいならたぶん大丈夫です。

▼ブレッドボード
DS3231の動作テスト、ブレッドボード
以前の記事に出した写真と見掛けはほとんど変わっていませんが、FET のレベルコンバーターが増えています。

◆プログラム
年月日・曜日・時刻・温度表示機能と、時計の時刻合わせ機能が入っています。また、DS3231のSQW信号でCPUに割り込みを入れることで、待機時の消費電流を削減しています。ちょっと長いですが、全ソースをここに記載します。
/* RTC DS3231 のテスト
RTCから時刻情報を読んで0.96インチOLED(128x64pix)に表示
動作はピン2からの割り込みで行い、割り込み待ちの時はパワーダウンモードで省電流化
2019/7/4 ラジオペンチ http://radiopench.blog96.fc2.com/
*/
#include <DS3231.h> //
#include <Adafruit_SSD1306.h> // OLED表示ライブラリ
#include <avr/sleep.h> // 割込み待ち期間はスリープさせるために使用

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define xS 8 // 画面のX方向シフト量
#define timeIrqPin 2
#define incPin 5 // (+) Inc. button
#define decPin 6 // (-) Dec. button
#define entPin 7 // Enter button
#define ledPin 13 // LED pin

Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
DS3231 rtc(SDA, SCL); // DS3231の設定

Time t; // Timeクラス構造体
char buff[10]; // 文字列操作バッファ
String ymd = "yyyy/mm/dd";
String hms = "hh:MM:ss";
String dayOfWeek = "sun";
float rtcTemp; // RTCの温度センサ

void setup() {
pinMode(timeIrqPin, INPUT_PULLUP); // RTC割り込み入力(宣言不要だが明示のために定義)
pinMode(incPin, INPUT_PULLUP); // +
pinMode(decPin, INPUT_PULLUP); // -
pinMode(entPin, INPUT_PULLUP); // enter
pinMode(ledPin, OUTPUT);

Serial.begin(115200);
rtc.begin();
rtc.setSQWRate(SQW_RATE_1); // DS3231から1秒パルスを、
rtc.setOutput(OUTPUT_SQW); // SQWピンに出力
rtc.enable32KHz(true); // 32kHzも出力

oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); // アドレス 0x3C (0x78)
oled.setTextColor(WHITE); // 白文字で描く
oled.setTextSize(2); // 2倍角文字で表示

if (digitalRead(entPin) == LOW) { // 起動時にEnt.ボタンが押されていたら
clockAdjust(); // OLED画面と+, -, Ent.ボタンで時刻合わせ。
}
// adjClock(); // 決め打ちで時刻合わせする場合はこっちを使う

set_sleep_mode(SLEEP_MODE_PWR_DOWN); // スリープはパワーダウンモードで行う
}

void loop() {
waitExtIRQ(); // RTCからの1秒パルスをスリープ(パワーダウンモード)で待つ

digitalWrite(ledPin, HIGH);
t = rtc.getTime(); // Time変数の値を取得(注:下記と別関数なので内容の同一性が保証出来ていない)
ymd = rtc.getDateStr(FORMAT_LONG, FORMAT_BIGENDIAN, '/'); // 年月日を文字列(yyyy/mm/dd)で取得
hms = rtc.getTimeStr(FORMAT_LONG); // 時刻を文字列(hh:mm:dd 形式)で取得
dayOfWeek = rtc.getDOWStr(FORMAT_SHORT);
rtcTemp = rtc.getTemp(); // 温度を取得
Serial.print(ymd); Serial.print(", "); Serial.print(hms); Serial.print(", "); Serial.println(rtcTemp);

oled.clearDisplay(); // 0.96インチOLEDに表示
oled.setCursor(0 + xS, 0);
oled.print(ymd); // 年月日表示

oled.setCursor(24 + xS, 16);
oled.print("("); oled.print(dayOfWeek); oled.print(")"); // 曜日を表示

oled.setCursor(12 + xS, 32);
oled.println(hms); // 時刻表示

oled.setCursor(24 + xS, 48);
oled.print(rtcTemp, 1); // 温度表示
oled.print("C");

oled.display(); // OLEDに表示(データーを転送)
Serial.flush();
digitalWrite(ledPin, LOW);
}

void clockAdjust() { // OLEDとボタンスイッチで時刻を合わせる
oled.clearDisplay(); oled.setCursor(0, 0);
oled.println("Time adj."); // 時刻合わせ開始表示
oled.display();
while (digitalRead(entPin) == LOW) { // entボタンが離されるまで待つ
}
t = rtc.getTime(); // Time変数の値((year,mon,date,hour,min,sec)を取得
ymd = rtc.getDateStr(FORMAT_LONG, FORMAT_BIGENDIAN, '/'); // 年月日を文字列(yyyy/mm/dd)で取得
hms = rtc.getTimeStr(FORMAT_LONG); // 時刻を文字列(hh:mm:dd 形式)で取得

oled.clearDisplay(); // 画面を消して
oled.setCursor(0 + xS, 0);
oled.println(ymd); // 現在の年月日を表示
Serial.println(ymd);
hms[6] = '-'; // 秒の桁を--に差し替え
hms[7] = '-';
oled.setCursor(12 + xS , 24); // 2行目に
oled.println(hms); // 時刻表示
oled.display();

// x, y座標, 値, ステップ, 下限, 上限を指定して時計の設定値を入力
t.year = oledRW(24 + xS, 0, t.year - 2000, 1, 10, 49) + 2000; // 年の値を入力
t.mon = oledRW(60 + xS, 0, t.mon, 1, 1, 12); // 月の入力
if (t.mon == 2) { // 2月で
if ((t.year % 4) == 0 ) {
t.date = oledRW(96 + xS, 0, t.date, 1, 1, 29); // 閏年なら29日まで
} else {
t.date = oledRW(96 + xS, 0, t.date, 1, 1, 28); // 閏年でなければ28日まで
}
} else if ((t.mon == 4) || (t.mon == 6) || (t.mon == 9) || (t.mon == 11)) {
t.date = oledRW(96 + xS, 0, t.date, 1, 1, 30); // 4,6,9,11月なら30日まで
} else {
t.date = oledRW(96 + xS, 0, t.date, 1, 1, 31); // それ以外なら31日まで
}
t.hour = oledRW(12 + xS, 24, t.hour, 1, 0, 23); // 時
t.min = oledRW(48 + xS, 24, t.min, 1, 0, 59); // 分

rtc.setDate(t.date, t.mon, t.year); // 年月日を書き込み
rtc.setTime(t.hour, t.min, 0); // 時分秒を書き込み
rtc.setDOW(zeller(t.year, t.mon, t.date)); // ツェラーの式で曜日を書き込み
delay(10);
}

int oledRW(int x, int y, int d, int stepD, int minD, int maxD) { // OLEから値を入力
// OLEDの指定位置に2桁右詰めで変数の値を表示。ボタン操作で値を増減し、
// Ent入力で値を確定し戻り値として返す。表示位置の左上をx, y 座標で指定
// 操作位置は下線で表示。値は上下限の範囲でサーキュレート。文字サイズは2倍角(12x16画素)
// 引数:x座標、y座標、変更したい変数、変更ステップ量、下限値、上限値

oledDisp2Chr(x, y, d); // 画面の指定位置に数値を下線付きで2桁表示する
while (digitalRead(entPin) == LOW) { // enterボタンが押されていたら離されるまで待つ
}
delay(30);
while (digitalRead(entPin) == HIGH) { // enterボタンが押されるまで以下を実行

if (digitalRead(incPin) == 0) { // + ボタンが押されていたら
d = d + stepD; // x を指定ステップ増加
if (d > maxD) { // 上限超えたら下限へサキュレート
d = minD;
}
oledDisp2Chr(x, y, d); // 画面の指定位置に数値を2桁表示(下線付き)
while (digitalRead(incPin) == 0) { // + ボタンが離されるまで待つ
}
delay(30);
}
if (digitalRead(decPin) == 0) { // - ボタンが押されていたら
d = d - stepD; // x を指定ステップ減らす
if (d < minD) { // 下限以下なら上限へサーキュレート
d = maxD;
}
oledDisp2Chr(x, y, d); // 画面の指定位置に数値を2桁表示(下線付き)
while (digitalRead(decPin) == 0) { // - ボタンが離されるまで待つ
}
delay(30);
}
}
oled.drawFastHLine(x, y + 15, 24, BLACK); // アンダーラインを消す(画面への反映は次の表示)
delay(30);
return d; // 戻り値
}

void oledDisp2Chr(int x, int y, int val) { // OLEDの指定場所に2桁の値を表示
oled.fillRect(x, y, 24, 16, BLACK); // 指定座標から2文字分消す(黒塗り)
oled.drawFastHLine(x, y + 15, 24, WHITE); // 下線を引く
sprintf(buff, "%02d", val); // データーを10進2桁0フィル文字列に変換
oled.setCursor(x, y); // カーソルを指定位置に
oled.print(buff); // 数値を書き込み
oled.display(); // 画面に表示
}

int zeller(int y, int m, int d) { // ツェラーの式で曜日を計算
if (m <= 2) {
m += 12;
y--;
}
// DS3231の仕様(月曜日=1、日曜日=7)に合わせるために補正している
return 1 + (y + y / 4 - y / 100 + y / 400 + (13 * m + 8 ) / 5 + d - 1) % 7;
}

void adjClock() { // 時刻の値を決め打ちで設定したい時に使う
// rtc.setDOW(WEDNESDAY); // 曜日の設定
rtc.setDate(26, 6, 2019); // 日、月、年の設定
rtc.setTime(20, 19, 0); // 時、分、秒の設定
}

void waitExtIRQ() {
ADCSRA &= ~(1 << ADEN); // ADENビットをクリアしてADCを停止(120μA節約)
attachInterrupt(0, rtcIRQ, FALLING); // Pin2のネガエッジで割込み
sleep_mode(); // 指定したモードでスリープ
detachInterrupt(0); // ここでリープから復活
ADCSRA |= (1 << ADEN); // ADC動作再開
}

void rtcIRQ() { // IRQ0(pin2)割込み処理
}
プログラムのポイントは以下の通りです。

・ライブラリとしては画面表示に Adafruits_SSD1306.h を、RTC の制御に RTC3231.h を使用。

・時刻情報の取扱いには Time 構造体を使用。但し時計を Arduino で動かす訳では無いので、Time ライブラリは使っていません。

・時刻合わせは、リセット時に Enter ボタンを押しておくと起動する方式で、中身は85行目の clockAdjust 関数で実行。

・SQW の信号を I/0 の Pin2 に入力し、Arduino の標準機能の attachiInterrupt で検出しています。また、割込み待ちの間はスリープモードにすることで省電力化を図っています。

・年月日の入力で、日の値は月の大小と閏年を考慮した範囲の値しか入力出来ないようにしました。

・年月日から曜日を計算するためにツェラーの式を使って、RTC へ曜日を設定しています。175行目の zeller 関数がその部分です。

ともかく、せっかく作るなら、ということで想定される機能は出来るだけ入れてみました。その副作用で表示が見辛くなっていますが、そのあたりは(もし)実用機を作るなら、考えたいと思います。

あと、プログラムの59行目のコメントに書いておきましたが、時刻情報の入手のために、複数回 RTC にアクセスしているので、プログラムの構造によっては途中で内容が違ってくる可能性があります。本来は一発で情報を取得して、フォーマットの変換などは後でプログラムでやるべきです。ともかくそういう ズル をやっているので、少し後ろめたいところがあります。ちなみにこのプログラムのように割込みで同期させていれば、タイミングが固定されるので問題は起きないはずです。

▼動作中の画面
時計の表示
情報を全部表示するようにしたので窮屈ですが、動作確認用なのでまあいいかと。

▼時刻合わせの画面
時刻合わせの様子
以前の記事で解説したのと同じ動作です。

▼割込みでCPUのクロックが起動・停止する様子
クロックピンの波形
これは CPU の X1 ピンの波形です。割り込みで CPU のクロックが起動して処理を行い、処理が終わったらスリープに入る様子が見えています。処理時間は約60mSでこれが loop() を通過する時間。これを1秒間隔で繰り返します。

なお、処理中の CPU の消費電流は約16mAですが、スリープ中は 50μA 程度まで減るので大きな省電流効果になります。但し、表示に使っている OLED は連続で 12mA の消費電流があるので CPU 側でがんばってもあまり報われないのが残念です。

◆まとめ
このところしつこくRTCを調べてきましたが、とりあえず今回の記事でおしまいです。最終的には、かなり実用的なところまでで追い込めたのではないかと思います。ここまでやっておけば後で何かやる時に楽になるずです。

中華な RTC は性能が怪しい物もありますが、格安なのでこれを使わない手は無いと思います。値段が高いから使うのをためらうようだと、せっかくの技術習得のチャンスを逃すことになって、もったいないと思います。そうは言っても、一方で国内メーカーや販売店が心配になりますが、それは別次元の話でしょう。
関連記事

コメントの投稿

管理者にだけ表示を許可する

No title

すばやくて、お見事な完成形ですね。

OLEDは、やっぱり電気食うみたいですね。
LCDならもっと長持ちするのかな?と思ってNokia5110とか調べなおしてみたら、

https://blogs.yahoo.co.jp/le_nozze_di_figaro_2001/38816523.html

5110よりも、秋月液晶モジュールの方が電気食わないらしく、

http://akizukidenshi.com/catalog/g/gK-07007/

データシートでは1mAになってました。ただ、バックライトは無いので、必要に応じてバックライト代わりのLEDを点灯するといった必要はありそうです。

あと、I2Cではなくなってしまうので、配線やライブラリ周りなんかも変わってきてしまいますが、コントローラはST7565Rなので、ライブラリは汎用の「U8g2」あたりでよさそうです。

1mAなら、単3電池2本でも2ヶ月くらいは動かしっぱなしでも大丈夫ですかねぇ。

nekosan0さん、どうも

そうなんですよね、実はOLEDの消費電流はもう少し少ないと思っていました。光っている画素が増えると消費電流も増える、ということなんでしょうね。

もっと消費電流が小さければ、いろいろ面白いことに使えるのになー、というのはnekosan さんもお考えだったようですね。貼っていただいたリンクとか参考になります。

CR2032で1週間くらい動く物が出来れば、ネームバッチみたいな物に仕上げて、MAKEの会場で見せびらかしたりできるんですけどね。

No title

普段は時間だけ表示しておいて、なにかボタン操作したときだけ、日付や曜日などを表示する…なんて動作にしたら、OLEDでも光っている画素数の平均値は少なく出来そうなので、場合によってはほぼ無点灯に近い電流に持っていけるかもしれないなぁ…などと思いました。

以前実験してみたときの数値を掘り起こしてみました。

https://brown.ap.teacup.com/nekosan0/3702.html

無点灯の状態ならLCDと同レベルの1~2mAくらいだったみたいなので、やはり「時刻だけ」など最小限表示にすると、3~4mAくらいまでは落とすことができるんではないかなぁ?という気がします。(データは取ってないですが)

ただ、1秒おきにスリープしているので、スイッチ入力とかで表示を切替えるのはちょっと難しい感じもありますが。

nekosan0さんへ、

なるほど、無点灯なら1-2mAですか、そういうの知りたかったです。ありがとうございます。

消費電流を減らす手として他に、パネルの明るさ(コントラスト)をSSD1306のコマンドで設定出来るので、待機時は明るさを落としておく手もありそうですね。ライブラリから出来るようになっているかは判らないのですが、レジスタを直接叩けば出来そうです。

カレンダー
07 | 2019/08 | 09
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
プロフィール

ラジオペンチ

Author:ラジオペンチ
電子工作を中心としたブログです。たまに近所(東京都稲城市)の話題など。60過ぎて視力や器用さの衰えを感じつつ日々挑戦!
コメントを入れる時にメールアドレスの記入は不要です。なお、非公開コメントは受け付けていません。

記事が気に入ったらクリックを!
最新記事
カテゴリ
最新コメント
リンク
FC2カウンター
検索フォーム
月別アーカイブ
RSSリンクの表示
QRコード
QRコード