Arduino Nanoのみで作る高精度時計 ─ 内部クロック誤差をソフト補正
概要
外部RTCモジュールなしで、Arduino Nanoの内蔵クロックのみを用いて高精度な時計を実現。内部クロックの系統誤差を実測し、ソフトウェアで毎時補正することで、5日間で誤差ゼロを達成した手法を解説します。16×2 LCD表示、3ボタンで時刻・日付・補正値設定可能。
核心アイデア:誤差の実測と定常補正
- Arduinoの
millis()とPCの高精度クロックを比較し、1時間あたりの誤差(ミリ秒)を測定 - 測定値を
speedCorrection変数に格納 - 自作ミリ秒カウンタで毎時
speedCorrectionを加減算し、誤差を打ち消し
必要な部品
- Arduino Nano R3(他のArduinoでも可) ×1
- 16×2 キャラクタLCD ×1
- タクトスイッチ ×3(モード、アップ、ダウン)
- 10kΩ 半固定抵抗(LCDコントラスト) ×1
- ジャンパーワイヤー
誤差測定用ツール
付属のArduinoスケッチ+Processingスクリプトで自動測定可能。Arduinoが2秒ごとに経過ミリ秒を送信、Processing側でPCクロックと比較し、1時間後の差分を算出。
独自ミリ秒カウンタ(Timer0割り込み)
millis()は約50日でオーバーフローし、書き換え不可。そこでTimer0のCTCモードで1ms割り込みを自作し、任意リセット可能なカウンタを実装。
// Timer0を1ms割り込みに設定(16MHz/64分周)
TCCR0A = (1 << WGM01); // CTCモード
OCR0A = 0xF9; // 249 = 1ms
TIMSK0 |= (1 << OCIE0A); // 比較一致割り込み有効
TCCR0B |= (1 << CS01) | (1 << CS00); // 分周 1/64
TCNT0 = 0;
sei(); // 全割り込み有効
volatile unsigned long currentTime = 0; // 0時からの経過ms
volatile unsigned long elapsed = 0; // 汎用経過カウンタ
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
日次・時間次処理
// 日付またぎ処理(24時間 = 86,400,000 ms)
if (currentMode == SHOW_TIME && currentTime > MILLIS_PER_DAY) {
noInterrupts();
currentTime -= MILLIS_PER_DAY;
interrupts();
currentDate++; // ユリウス日で管理
}
// 毎時の誤差補正
if (elapsed >= MILLIS_PER_HOUR) {
noInterrupts();
currentTime += speedCorrection; // 正なら遅れ補正、負なら進み補正
elapsed = 0;
interrupts();
}
日付管理:ユリウス日
日付をユリウス日(紀元前4713年1月1日からの日数)で保持。グレゴリオ暦⇔ユリウス日相互変換関数で表示・設定を実現。うるう年・世紀末スキップも自動計算。
操作モード(モードボタンで遷移)
| モード | 内容 | アップ/ダウン操作 |
|---|---|---|
| 時刻表示 | 通常動作 | - |
| 時刻設定 | 時・分設定 | 時/分 インクリメント |
| 年設定 | 年設定 | 年 ±1 |
| 日月設定 | 日・月設定 | 日/月 インクリメント(月末自動調整) |
| 補正値設定 | 速度補正調整 | ±5ms/ステップ |
実測結果
- 筆者のNanoでは1時間あたり約3,545ms遅れ(
speedCorrection = +3545) - 補正適用後、5日間連続動作で誤差なし
- 個体差があるため、必ず実測して補正値を決定すること
コード全文(主要部)
#include
LiquidCrystal lcd(12, 13, 6, 7, 8, 9);
#define MODE_BTN 2
#define UP_BTN 3
#define DOWN_BTN 4
enum { SHOW_TIME=1, SET_TIME, SET_YEAR, SET_DATE, SET_SPEED_ADJ };
int currentMode = SHOW_TIME;
int speedCorrection = 3545; // 実測値に書き換え
volatile unsigned long currentTime = 0, elapsed = 0;
unsigned long lastTime = -1000;
float currentDate = JulianDate(1,1,2021), lastDate = 0;
const unsigned long MILLIS_PER_DAY = 86400000UL;
const unsigned long MILLIS_PER_HOUR = 3600000UL;
void setup() {
// Timer0 1ms割り込み設定(上記参照)
pinMode(MODE_BTN, INPUT_PULLUP);
pinMode(UP_BTN, INPUT_PULLUP);
pinMode(DOWN_BTN, INPUT_PULLUP);
lcd.begin(16, 2);
}
void loop() {
// 日付またぎ・毎時補正(上記参照)
checkButtons(); // ボタン処理でモード遷移・値変更
switch (currentMode) {
case SHOW_TIME: showTime(currentTime); showDate(currentDate); break;
case SET_TIME: showTimeSet(currentTime); break;
case SET_YEAR: showYearSet(currentDate); break;
case SET_DATE: showDDMMSet(currentDate); break;
case SET_SPEED_ADJ: showSpeedSet(); break;
}
wait(150); // 自作delay
}
// ユリウス日変換、時刻分解、ボタン処理、LCD表示関数等は全文参照
メリット・デメリット
| メリット | デメリット |
|---|---|
| 外部部品不要(RTCモジュール・電池不要) | 電源断で時刻リセット(バックアップ不可) |
| 低コスト・シンプル | 個体ごとに校正必須、温度ドリフト未補償 |
| 50日オーバーフロー問題を解決 | 高精度用途にはDS3231/GPS推奨 |