Сторонние точные часы на Arduino Nano и 16×2 LCD без RTC-модуля
Компоненты и материалы
- Arduino Nano R3 — 1 шт. (подойдет любой Arduino)
- Символьный LCD 16×2 — 1 шт.
- Тактильные кнопки — 3 шт.
- Подстроечный резистор 10 кОм — 1 шт. (для контраста LCD)
- Перемычки — 1 комплект
О проекте
Начал как учебное упражнение, но получились очень точные часы. После 5 дней работы не потеряли и не набрали ни секунды.
Проблема использования только Arduino — внутренний генератор не идеально точен. Функция millis() накапливает ошибку, и часы либо отстают, либо спешат. Мой подход: измерить неточность конкретного Arduino (сколько миллисекунд теряет/набирает в час) и программно компенсировать её каждый час.
Также внутренний счетчик millis() переполняется каждые ~50 дней и его нельзя изменить. Решение — заменить прерывание millis() собственным счетчиком миллисекунд от полуночи, сбрасываемым ежедневно.
Оценка неточности
Предположил, что системные часы компьютера (и millis() в Processing) точны. Написал программу для Arduino, отправляющую прошедшие миллисекунды в Processing каждые 2 секунды. Скрипт на Processing сравнивал их со своими миллисекундами, показывая разницу в реальном времени и итоговую за час. Это дало значение коррекции для программы часов.
Код Arduino и скрипт Processing приведены ниже. Если Processing не установлен: https://processing.org
Код часов
Ключевые моменты: настройка прерывания, работа с временем, хранение даты.
Прерывание таймера
Настраиваем прерывание каждую миллисекунду. Это отключает стандартный millis() и delay().
// Настройка прерывания по таймеру 0
TCCR0A = (1 << WGM01); // CTC mode
OCR0A = 0xF9; // 249 = 1 мс при 16 МГц / 64
TIMSK0 |= (1 << OCIE0A); // Enable compare match interrupt
TCCR0B |= (1 << CS01) | (1 << CS00); // Prescaler 64
TCNT0 = 0; // Сброс счетчика
sei(); // Глобальное разрешение прерываний
Обработчик прерывания (вызывается каждую мс):
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
Переменные currentTime и elapsed объявлены как volatile.
currentTime — миллисекунды от полуночи. Есть функции преобразования в ЧЧ:ММ:СС и сброса при настройке.
Каждые 24 часа вычитаем суточные миллисекунды и увеличиваем дату на 1 день. Переполнение переменной не страшно.
if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
noInterrupts();
currentTime -= millisecondsInADay;
interrupts();
currentDate++;
}
Каждый час корректируем время на измеренную погрешность:
if (elapsed >= millisecondsInHour) {
noInterrupts();
currentTime += speedCorrection; // поправка на неточность генератора
elapsed = 0;
interrupts();
}
Хранение и расчет даты
Дата хранится как Юлианская дата (дни с 1 января 4713 г. до н.э.). Есть функции перевода в григорианский календарь и обратно.
float JulianDate(int iday, int imonth, int iyear) {
unsigned long d = iday, m = imonth, y = iyear;
if (m < 3) { m += 12; y -= 1; }
unsigned long t1 = (153 * m - 457) / 5;
unsigned long t2 = 365 * y + (y / 4) - (y / 100) + (y / 400);
return 1721118.5 + d + t1 + t2;
}
void GregorianDate(float jd, int &iday, int &imonth, int &iyear) {
unsigned long f = jd + 68569.5;
unsigned long e = (4.0 * f) / 146097;
unsigned long g = f - (146097 * e + 3) / 4;
unsigned long h = 4000ul * (g + 1) / 1461001;
unsigned long t = g - (1461 * h / 4) + 31;
unsigned long u = (80ul * t) / 2447;
unsigned long v = u / 11;
iyear = 100 * (e - 49) + h + v;
imonth = u + 2 - 12 * v;
iday = t - 2447 * u / 80;
}
Кнопки настройки
Кнопка Mode переключает режимы: Показ времени → Установка времени → Установка года → Установка даты → Коррекция скорости → Показ времени. Две другие кнопки меняют значения в текущем режиме.
Если часы идут неточно, в режиме «Коррекция скорости» можно изменить поправку шагами по 5 мс.
Исходный код
Программа часов (Arduino)
// Paul Brace - Feb 2021
// Simple Clock with Date using only Arduino - no RTC module
// Incorporates time correction adjustment for internal clock inaccuracy.
// Once correct speed adjustment set the clock is surprisingly accurate.
// In my test it did not lose or gain any time over a 5 day period.
// Displays time on a 16x2 LCD display
// Buttons to set time
// Mode button (pin 2) toggles set time, set date and run
// Button 1 (pin 3) Increments Minutes and Month and decreases Year/speed adj
// Button 2 (pin 4) Increments Hour and Day and increases Year/speed adj
// 24 Hour display
#include
LiquidCrystal lcd(12, 13, 6, 7, 8, 9);
#define MODE_BUTTON 2
#define HOUR_BUTTON 3
#define UP_BUTTON 3
#define DAY_BUTTON 3
#define MINUTE_BUTTON 4
#define DOWN_BUTTON 4
#define MONTH_BUTTON 4
#define SHOW_TIME 1
#define SET_TIME 2
#define SET_YEAR 3
#define SET_DATE 4
#define SET_SPEED_ADJ 5
int speedCorrection = 3545; // мс/час, которые мой Nano отстает (отрицательное если спешит)
volatile unsigned long currentTime;
unsigned long lastTime = -1000;
volatile unsigned long elapsed;
unsigned long millisecondsInADay;
unsigned long millisecondsInHour;
int currentMode;
float currentDate;
float lastDate = 0.0;
int currentDay, currentMonth, currentYear;
char *dayArray[] = { "Tue. ", "Wed. ", "Thur. ", "Fri. ", "Sat. ", "Sun. ", "Mon. " };
void setup() {
TCCR0A = (1 << WGM01);
OCR0A = 0xF9;
TIMSK0 |= (1 << OCIE0A);
TCCR0B |= (1 << CS01) | (1 << CS00);
TCNT0 = 0;
sei();
pinMode(MINUTE_BUTTON, INPUT_PULLUP);
pinMode(HOUR_BUTTON, INPUT_PULLUP);
pinMode(MODE_BUTTON, INPUT_PULLUP);
currentTime = 0;
currentDate = JulianDate(1, 1, 2021);
elapsed = 0;
millisecondsInADay = 24ul * 60 * 60 * 1000;
millisecondsInHour = 60ul * 60 * 1000;
currentMode = SHOW_TIME;
lcd.begin(16, 2);
lcd.noAutoscroll();
lcd.display();
lcd.clear();
ShowTime(currentTime);
}
void loop() {
if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
noInterrupts();
currentTime -= millisecondsInADay;
interrupts();
currentDate++;
}
if (elapsed >= millisecondsInHour) {
noInterrupts();
currentTime += speedCorrection;
elapsed = 0;
interrupts();
}
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);
}
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
float JulianDate(int iday, int imonth, int iyear) { /* ... */ }
void GregorianDate(float jd, int &iday, int &imonth, int &iyear) { /* ... */ }
void SplitTime(unsigned long curr, unsigned long &ulHour, unsigned long &ulMin, unsigned long &ulSec) { /* ... */ }
unsigned long SetTime(unsigned long ulHour, unsigned long ulMin, unsigned long ulSec) { /* ... */ }
void Wait(unsigned long value) {
unsigned long startTime = elapsed;
while ((elapsed - startTime) < value) {}
}
void CheckButtons() { /* ... */ }
String FormatNumber(int value) { /* ... */ }
void ShowTime(unsigned long value) { /* ... */ }
void ShowDate(float value) { /* ... */ }
void ShowDDMMSet(float value) { /* ... */ }
void ShowYearSet(float jd) { /* ... */ }
void ShowTimeSet(unsigned long value) { /* ... */ }
void ShowSpeedSet() { /* ... */ }
Arduino-скетч для измерения погрешности
// Paul Brace Feb 2021
// For use with corresponding Processing script
// to compare millis() from here to millis() in Processing
int inByte = 0;
unsigned long firstReading = 100000;
void setup() {
Serial.begin(9600);
sayHello();
}
void loop() {
if (Serial.available() > 0) {
inByte = Serial.read();
Serial.print(millis() - firstReading);
Serial.print('E');
delay(2000);
}
}
void sayHello() {
while (Serial.available() <= 0) {
Serial.print('Z');
delay(200);
}
firstReading = millis();
}
Скрипт Processing для тестирования
// Paul Brace Feb 2021
// Script to accept millis() from Arduino and compare to internal millis()
// Assumes computer clock is accurate
// -ve = Arduino slow -> enter as +ve adjustment in clock
// +ve = Arduino fast -> enter as -ve adjustment
import processing.serial.*;
Serial theSerialPort;
int[] serialBytesArray = new int[15];
int bytesCount = 0;
boolean init = false;
int fillColor = 255;
long mills = 0, first = 0, now, firstReading = 100000, DiffPerHour = 0;
int inByte;
void setup() {
size(500, 500);
background(70);
noStroke();
printArray(Serial.list());
String thePortName = Serial.list()[1];
theSerialPort = new Serial(this, thePortName, 9600);
}
void draw() {
background(70);
fill(fillColor);
textSize(25);
text(hour() + ":" + minute() + ":" + second(), 50, 50);
text("Incoming elapsed: " + mills, 50, 100);
text("Local elapsed: " + (now - firstReading), 50, 150);
text("Diff: " + (mills - (now - firstReading)), 50, 200);
if (((now - firstReading) >= 3600000) && (DiffPerHour == 0)) {
DiffPerHour = mills - (now - firstReading);
}
text("Diff after 1 hour: " + DiffPerHour, 50, 300);
}
void serialEvent(Serial myPort) {
inByte = myPort.read();
if (!init) {
if (inByte == 'Z') {
myPort.clear();
init = true;
myPort.write('Z');
if (first == 0) first = millis();
}
} else {
if (inByte != 69) {
if (bytesCount < 14) {
serialBytesArray[bytesCount] = inByte;
bytesCount++;
}
}
if (inByte == 69) {
now = millis();
mills = 0;
for (int i = 1; i <= bytesCount; i++) {
mills += (serialBytesArray[i-1] - 48) * pow(10, (bytesCount - i));
}
if (firstReading == 100000) firstReading = now;
myPort.write('Z');
bytesCount = 0;
}
}
}
Схемы


Производственный процесс
- Arduino POV-часы: сборка дисплея на эффекте сохранения образа
- Создание DTMF-декодера на Arduino без внешних микросхем
- Создание потрясающей системы Ambilight для монитора на Arduino
- Стильные настенные часы на четырех кольцах Adafruit Neopixel 60 LED и Arduino
- Слово-часы (Word Clock) на Arduino — DIY-часы с текстовой индикацией времени
- Часы реального времени на Arduino с расчётом времени намаза
- Создание пользовательских LED-часов с будильником — видно с 12 метров
- Умный вольтметр на Arduino и смартфоне — простое DIY-проект
- IoT-монитор пульса в реальном времени на Arduino и MAX30100
- Веб-сервер на Arduino Uno WiFi: управление LED через браузер