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

リアルタイムクロック DS3231 のエージングレジスタを調整して精度アップ

リアルタイムクロックの DS3231 を Arduino から動かせるようにして約20日経ちました。ブレッドボードはそのまま残っているので、久しぶりに電源を入れて動かしてみました。

▼DS3231の動作テスト
DS3231動作テスト中
動かした目的は、どの程度の精度で時計が動いているか確認したかったからです。ちなみにバッテリーバックアップされているので、外部から電源を供給しなくても、基板単体で動き続けています。

DS3231 のモジュールは3枚ありますが、その誤差(時刻のずれ)は、A基板: -2秒、B:基板: -13秒、C基板: -6秒でした。なお、マイナスの誤差は遅れを表しています。これは約20日間経過後のずれ量なので、A基板についてはまずまずの結果だと思います。

ちなみに、 B と C の基板に載っているチップは偽物で、仕様書通りにすら動かない物なので、精度を語ってもあまり意味が無いです。まあ、そうは言ってもそこそこの精度では動いているようです。

20日で2秒の誤差なので、その精度は約 1.1ppm で、流石は TCXO です。こうなると欲が出て、もう少し精度を上げてみたくなります。そんなことで調べてみると、ありました。エージングレジスタを調整することで、オシレーターの周波数の微調整が出来るようです。

▼エージングレジスタの効果(DS3231のデーターシートより抜粋)
エージング調整特性
このレジスタは、水晶の経時変化 (Aging) を補正するために用意されているようですが、周波数の微調整のために使うことも出来ます。ということで、エージングレジスタの値を OLED の画面を見ながら操作する機能をプログラムに追加しました。

プログラムはこちら。20190731DS3231_OLED_Clock.txt (shift-JISエンコードです)
なお、エージングレジスタなどを設定する機能が、使用したライブラリでサポートされていなかったので、別途作り込んでいいます。また、設定変更がすぐに反映されるようにControl Redgister の CONVビットの操作も行っています。

エージング調整画面
使い方は、Inc.(+)ボタンを押しながら起動(リセット)すると、上の写真のようなエージング値の調整画面に入るので、+/-ボタンで値を調整。最後に Ent .ボタンを押すことで、エージング値がセットされます。なお、エージング値はバッテリーバックアップされるので、電源を切っても消えません。

ちなみに、エージングレジスタの値を変更しても、実際に反映されるのは、64 秒毎に行われる温度補正のタイミングになります。これでは調整がやりづらいので、強制的に値を反映させるように CONV ビットを操作しています。

▼調整の様子(32.768kHzの測定)
32.768kHz
周波数カウンタで水晶の周波数をモニタすると簡単に調整することが出来ます。この写真はぴったり 32.768000kHz に合わせた状態です。

▼エージングオフセットと周波数の測定結果
エージングオフセット値と周波数変化測定結果

▼エージングオフセットと周波数誤差のグラフ
エージングオフセット値と周波数変化グラフ
オフセット値は±120 の範囲で入力可能で、水晶の周波数は±4ppm の範囲で変化しました。ちなみに、オフセットの 1LSB で周波数は -0.035ppm 変化することになります。(あくまでもこの個体の値です)

これまでのオフセットはゼロだったので、約0.46pm の誤差があったことになります。なお、この記事の初めの方で誤差の実績は1.1ppm だったので、少し値が違っています。これはたぶん、温度の違いなどが原因では無いかと思います。まあ、目視で1秒以下のタイミングの違いをきちっと測定するには、何か工夫しないと難しいです。

◆エージングオフセットの調整
エージングオフセットに -12 を設定してとりあえず周波数をぴったり合わせました。なお、真夏で気温が高いのでたぶん年間を通した最適値は少し違ってくる気がします。

▼感度の高い測定方法
位相差測定
周波数を測定する以外に、ユニバーサルカウンタのタイムインタバル測定機能を使った測定方法も使えます。

正確な 1PPS 信号と、DS3231 から出ている1秒信号との位相差(位相変化)をユニバサルカウンタで測定することで、両者の周波数の差を高感度で検出することが出来ます。上の写真は位相差が 584.4543ms ですが、時間を置いて位相の変化を測定すると、極めて高感度な周波数の比較が可能になります。

ちなみに、BとCの基板の1秒信号には大きなジッタがあって、パルスの周波数測定では精密な測定は難しかったのですが、位相差測定を使うと、ジッタが平均化されるので、測定し易くなりました。

◆まとめ
リアルタイムクロックの DS3231 の精度は高いですが、エージングレジスタを調整することで、更に高い精度まで追い込むことが出来ました。

ここで問題は、こういう調整を行うためには絶対精度の高い測定器、あるいは信号源が必要になる点です。GPSの1PPS信号を使うことが出来れば、それが一番確実だと思います。それ以外にいろいろ方法があるので、考えてやって実際にやってみると面白いと思います。

◆おまけ
以前の記事に、挙動から偽物らしい DS3231 があると書きました。そのチップの拡大写真は以下の通りです。

▼Aの基板のDS3231
Aのチップ(本物と同じ挙動)
これはデーターシート通りの挙動だったチップです。ただ本物かどうかは判りません。鑑定できる人いませんか?

▼Bの基板のDS3231
Bのチップ(明らかな偽物)
これはおかしな挙動だったチップで、明らかに偽物だと思います。マーキングも違っています。

アナログ時計をArduinoを使って正転/逆転させる

youtube に Arduinoを使ってアナログ時計を正転・逆転させる話 の動画を登録しました。この記事はその動画のサポートページとして、回路図とプログラムを公開するために作成したものです。内容的には以前作成した記事とほぼ同じものです。
(I have registered a video of the story of using the Arduino on youtube to forward and reverse the analog clock. This article is created to publish schematics and programs as the video support page. it is almost the same as the previously created article)

Over view
デモ運転

Circuit schem
schem

program
20190716ClockDriveDemo.txt (Ascii file with english comment)

CW wave form parameters
CWの駆動波形
My example value : cwPulse = 25.5ms, cwPeriod = 12ms

CCW waveform parameters
CCW駆動波形
My example value : ccwPulse1 = 4ms, ccwWait = 2ms, ccwPulse2 = 15.5ms, ccwPeriod = 80ms

Optimum waveform parameters are greatly depend on your clock mechanism. so many many times of tuning work will be reqyired.

Actual waveform CW
CW駆動パルス

Actual waveform CCW
CCW駆動パルス

note
this is the test of collaboration between this web and youtube

Make bi-directionaly moving clock by Arduino (part-1)


Make bi-directionaly moving clock by Arduino (part-2)

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 は性能が怪しい物もありますが、格安なのでこれを使わない手は無いと思います。値段が高いから使うのをためらうようだと、せっかくの技術習得のチャンスを逃すことになって、もったいないと思います。そうは言っても、一方で国内メーカーや販売店が心配になりますが、それは別次元の話でしょう。
カレンダー
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コード