Будильник на тумбочке собран из того, что нашлось в коробке: LCD 1602 по четырём проводам (схема подключения), тактовая кнопка, зуммер. Модуля RTC нет, а часы нужны сегодня. Форматирование времени на экране я разбирал в уроке про часы на millis и формат — там про двоеточия и ведущие нули. Здесь вопрос другой: сколько такие часы врут и что с этим делать. Спойлер: за сутки — от долей секунды до десятков секунд, и разница целиком зависит от кусочка керамики на вашей плате.
Механика простая до обидного. millis() тикает от того же кварца, что и весь контроллер, и сам по себе отставать не умеет — весь уход часов на millis это уход кварца. У Uno кварц с допуском порядка ±50 ppm: пятьдесят секунд на миллион, в сутках 86400 секунд — выходит около 4,3 секунды за сутки. Дешёвые клоны с керамическим резонатором вместо кварца живут хуже: в замерах, которые я встречал, уход доходил до десятков секунд в сутки. В пересчёте на месяц мой будильник соврал бы минуты на четыре — для полива цветов терпимо, для будильника издевательство.
План такой. Сначала цена бесплатного времени: ppm, формула пересчёта и чем кварц отличается от резонатора. Потом компенсация — как превратить замеренный уход в поправку раз в час. Дальше сверка кнопкой: короткое нажатие подстраивает минуты, длинное входит и выходит из режима. И в конце будильник с отбоем и сном. Рабочий скетч — на параллельном LCD с раскладкой шилда: движок симулятора исполняет его живьём, а на макетке разводка та же.
Сколько врёт кварц: ppm на пальцах
Уход принято мерить в ppm — миллионных долях. Формула одна: ppm = (расхождение в секундах / длина интервала в секундах) × 1 000 000. Порядок замера: выставили время по телефону, сутки спустя сверили — отстали на 5 секунд при интервале 86400. Делим 5 на 86400, умножаем на миллион — около 58 ppm. Это паспорт вашего конкретного экземпляра, а не даташитная абстракция: два одинаковых Uno дают разный уход, у меня на столе разница между двумя платами оказалась вдвое.
Числа для ориентира такие: кварц в пределах допуска ±50 ppm — до 4-5 секунд за сутки; керамический резонатор — на порядок хуже, десятки секунд. Температура добавляет свою дольку: кварц на холодном подоконнике и в тёплой комнате уходят по-разному, так что замер, снятый зимой, к лету уплывёт. Отсюда практический вывод: коэффициент компенсации — не раз и навсегда, его полезно перепроверять раз в сезон.
Прежде чем компенсировать, зафиксируем границы метода. millis() переполняется через 49,7 суток, но разность now - tick от этого не страдает: беззнаковое вычитание переживает переполнение честно, мы это проходили в уроке про millis вместо delay. Само время на millis я строил в уроке про millis против настоящих часов и в часах без модуля — там же про то, почему DS3231 честнее. Здесь остаёмся на millis и правим именно его слабое место — уход кварца.
| Источник времени | Уход за сутки | Что это значит для проекта |
|---|---|---|
| Кварц Uno, удачный экземпляр | доли секунды — 2 с | сверка раз в неделю, компенсация не нужна |
| Кварц с допуском ±50 ppm | до 4-5 с | компенсация раз в час плюс сверка раз в день |
| Керамический резонатор клона | десятки секунд | сверка кнопкой обязательна, коэффициент под сезон |
| DS3231 по I2C | порядка 2 ppm, секунды за месяц | модуль за копейки, если уход критичен |
Сверка кнопкой и звонок
Сверка кнопкой — это признание, что любой коэффициент врёт. Схема жестов: короткое нажатие — минута вперёд, секунды при этом сбрасываются в ноль, как на наручных часах; длинное от секунды — вход и выход из режима подстройки. В сверке двоеточие мигает, чтобы было видно, где вы; восемь секунд бездействия — и часы сами возвращаются в обычный режим. Механику коротких и длинных нажатий я разбирал в отдельном уроке — здесь она нужна как готовый инструмент.
Компенсация в коде выглядит скромно: раз в час от базы t0 отнимается порция миллисекунд. Измеренный уход — 4 секунды отставания в сутки, значит в час часы отстают на 4000/24 ≈ 166 мс; отнимаем 166 от базы — elapsed растёт быстрее, часы идут вперёд. Знак важен: если ваши часы спешат, порцию прибавляют, а не отнимают. Всё в целых long, никаких float: накопления за месяц точность не съедят.
Будильник сравнивает часы и минуты: совпало — зуммер пищит, светодиод мигает, на второй строке ZVONOK. Короткое нажатие во время звонка — отбой со сном на десять минут, длинное — выключить совсем. Часы будильника в скетче зашиты константами: их настройку делает меню из следующего урока — так кластер и собирается в прибор. Про тон и корпус зуммера — в уроке про зуммер и tone.
#include <LiquidCrystal.h> LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // раскладка параллельного LCD шилда const int btn = 2; // кнопка сверки и отбояconst int led = 12; // мигает при звонкеconst int zum = 11; // зуммер: tone/noTone long uhodMsSut = 4000; // измеренный уход: отстаёт на 4 с за сутки unsigned long t0 = 0; // millis() в момент выставления времениlong popravka = 0; // накопленная сверка, секундыunsigned long lastComp = 0;unsigned long lastSec = 0; int budH = 7; // будильник зашит: настройку сделает меню из урока 2int budM = 30;int zvon = 0;unsigned long zvonT = 0;long snuSek = -1; // отбой до этого момента времениint ledSv = 0;int sverka = 0; // 0 - часы, 1 - подстройка минутunsigned long bezdeistvie = 0; int prevBtn = HIGH;unsigned long pressT = 0;int longDone = 0; long vremyaSek() { return (long)((millis() - t0) / 1000) + popravka;} void pechatChasy() { long t = vremyaSek(); int h = (int)((t / 3600) % 24); int m = (int)((t / 60) % 60); int s = (int)(t % 60); lcd.setCursor(0, 0); if (h < 10) { lcd.print("0"); } lcd.print(h); lcd.print(":"); if (sverka == 1 && (millis() / 500) % 2 == 0) { lcd.print(" :"); // минуты мигают в режиме сверки } else { if (m < 10) { lcd.print("0"); } lcd.print(m); lcd.print(":"); } if (s < 10) { lcd.print("0"); } lcd.print(s); lcd.print(" ");} void setup() { Serial.begin(9600); pinMode(btn, INPUT_PULLUP); pinMode(led, OUTPUT); lcd.begin(16, 2); pechatChasy(); lcd.setCursor(0, 1); lcd.print("BUD 07:30 KN D2 "); t0 = millis(); Serial.println("CHASY BEZ RTC: DLINNOE - SVERKA");} void loop() { unsigned long now = millis(); if (now - lastComp >= 3600000UL) { // компенсация раз в час lastComp = now; t0 -= (unsigned long)(uhodMsSut / 24); // отстаёт - ускоряем Serial.println("KOMP +166ms"); } if (now - lastSec >= 1000) { lastSec = now; pechatChasy(); } long t = vremyaSek(); if (zvon == 0) { int h = (int)((t / 3600) % 24); int m = (int)((t / 60) % 60); if ((h == budH && m == budM) || (snuSek > 0 && t >= snuSek)) { zvon = 1; zvonT = now; tone(zum, 2400); Serial.println("ZVONOK"); } } else { if (now - zvonT >= 300) { zvonT = now; ledSv = 1 - ledSv; digitalWrite(led, ledSv); } lcd.setCursor(0, 1); lcd.print("ZVONOK: KOROTKOE "); } int nowBtn = digitalRead(btn); if (prevBtn == HIGH && nowBtn == LOW) { pressT = now; longDone = 0; } if (nowBtn == LOW && longDone == 0 && now - pressT >= 1000) { longDone = 1; if (zvon == 1) { // длинное в звонке: выключить zvon = 0; noTone(zum); digitalWrite(led, LOW); snuSek = -1; lcd.setCursor(0, 1); lcd.print("BUD 07:30 KN D2 "); Serial.println("OTBOJ POLNYJ"); } else { sverka = 1 - sverka; bezdeistvie = now; Serial.println("SVERKA"); } } if (prevBtn == LOW && nowBtn == HIGH) { // отпускание: короткое if (longDone == 0 && now - pressT < 1000) { if (zvon == 1) { // сон на десять минут zvon = 0; noTone(zum); digitalWrite(led, LOW); snuSek = t + 600; lcd.setCursor(0, 1); lcd.print("SON 10 MIN "); Serial.println("SON 10 MIN"); } else if (sverka == 1) { // минута вперёд, сек в ноль popravka += 60 - t % 60; bezdeistvie = now; pechatChasy(); } } } prevBtn = nowBtn; if (sverka == 1 && zvon == 0 && now - bezdeistvie >= 8000) { sverka = 0; // таймаут сверки Serial.println("SVERKA TIMEOUT"); }}Честная оговорка про симулятор: сутки там сжаты до секунд, кварц идеальный, и уход не увидишь в принципе. Зато вся механика нажимается руками: короткое подстраивает минуты, длинное ставит будильник с потенциометра, при звонке короткое даёт сон, длинное выключает. На плате первый замер ухода делайте за двое суток: расхождение в 8-10 секунд видно без секундомера, а формула пересчёта в ppm уже у вас.
Дальше с этим временем делают две вещи: сохраняют будильник в EEPROM, чтобы перезагрузка не стирала настройки, и подключают DS3231, если уход критичен. Дисциплина экрана для этих часов — без мерцания, полями фиксированной ширины — отдельная тема этого кластера. Начните с замера ppm своего экземпляра: без этой цифры любая компенсация — гадание на кварце.
Прикинем бюджет правды на реальном примере. Уход четыре секунды в сутки — это две минуты за месяц, и изредка просыпающийся человек их не заметит; но будильник, который молчит две лишние минуты, — это уже опоздание на автобус. Компенсация раз в час сжимает те же две минуты до пары секунд, а сверка кнопкой раз в неделю ставит точку: расхождение не успевает накапливаться. Три слоя — коэффициент, сверка, заранее названный допуск — закрывают вопрос без единого дополнительного модуля на плате.
Питание и место установки влияют сильнее, чем версия библиотеки. Плата в холодном коридоре и на тёплой полке идут по-разному: температурный ход кварца — единицы ppm на градус, и за двадцать градусов разницы набегает заметная доля. Мой стенд показывал ровно это: зимой часы за сутки отставали на шесть секунд, летом на три с половиной. Отсюда совет замерять уход в тех условиях, где прибор будет жить, а не на столе у окна.
Чек-лист проекта выходит такой: замерить ppm своего экземпляра за двое суток, вписать коэффициент раз в час, вынести сверку на длинное нажатие и дать будильнику сон по короткому нажатию. Зуммер — через транзистор, если его ток больше двадцати миллиампер; светодиод для звонка — с резистором на 220 Ом. Вся электроника уже была в этой серии: экран по четырём проводам, кнопка с подтяжкой, зуммер с tone. А если допуск не устроит — DS3231 встанет на ту же шину за пять минут.
- 1. Кварц платы имеет допуск 50 ppm. Сколько времени часы на millis наберут за сутки?
- 2. Откуда берётся уход часов, собранных на millis()?
- 3. Плата проработала 50 суток, millis() переполнился. Что станет с разностью now - tick?
- 4. Замерили уход: 4 секунды отставания за сутки. Какая компенсация разумна?
- 5. Зачем сверка кнопкой, если компенсация уже вшита в код?
- millis() не отстаёт сам: весь уход часов — допуск кварца, до 4-5 с за сутки при ±50 ppm
- Резонатор клона хуже кварца на порядок: десятки секунд за сутки — замерьте свой ppm
- Компенсация — порция миллисекунд базе раз в час: 4000/24 ≈ 166 мс при уходе 4 с/сутки
- Сверка кнопкой правит точку отсчёта: короткое — минута, длинное — вход в режим, таймаут 8 с
- Разность now - tick переживает переполнение millis на 49,7 сутках — перезагрузки не нужны
Частые вопросы по теме урока
- Кварц платы имеет допуск 50 ppm. Сколько времени часы на millis наберут за сутки?
- 50 ppm — это 50 секунд на миллион секунд; в сутках 86400 с, получается 86400 × 50 / 1000000 ≈ 4,3 с. Половина допуска уходит в плюс, половина в минус. Около 4,3 секунды
- Откуда берётся уход часов, собранных на millis()?
- millis() считает по тому же кварцу, что и вся плата: сама функция не отстаёт. Отклонение вносит кварц — его допуск и температурный ход. Темп задаёт кварц, и всё отклонение — его допуск и температура
- Плата проработала 50 суток, millis() переполнился. Что станет с разностью now - tick?
- Беззнаковая арифметика в C устроена так, что now - tick даёт верный интервал даже через границу переполнения. Перезагрузки по расписанию — лишний костыль. Она останется верной: беззнаковое вычитание переживает переполнение
- Замерили уход: 4 секунды отставания за сутки. Какая компенсация разумна?
- 4000 мс / 24 часа ≈ 166 мс в час: порция мелкая, ход остаётся ровным. Разовая добавка в полночь даёт скачок, а delay здесь ни при чём. Раз в час сдвигать базу времени на 166 миллисекунд
- Зачем сверка кнопкой, если компенсация уже вшита в код?
- Коэффициент делает ход равномерным, но стартовые секунды и сезонный уход он не отменит. Сверка кнопкой — секундное дело, и точность возвращается на место. Компенсация правит темп, а не точку отсчёта: перенос платы и перемена температуры требуют сверки