Таймер для варки кофе я собрал за вечер и похоронил за два дня. Часы шли через delay(1000): секунда тик, кадр перерисовался, секунда тик — и первые сутки меня всё устраивало. Ломалось всё в момент, когда я прикручивал кнопку подстройки: она срабатывала через раз, потом через три, и я добился стабильности только удержанием кнопки почти секунду — в реальной жизни таймер был бесполезен. Финальный удар: утром таймер ушёл на шесть минут за сутки. Каждый цикл длился delay(1000) плюс пара-тройка миллисекунд на перерисовку, и эти миллисекунды складывались в минуты, которые я честно набегал неделю, не замечая. Кнопку я тогда поменял дважды и плату перепрошил трижды, а только на четвёртый вечер сел считать, сколько времени loop() проводит внутри паузы, — считать оказалось полезнее, чем подбирать кнопку.
Часы на TM1637 собираются из трёх простых частей: счёт секунд из millis(), кадр с двоеточием, который перерисовывается целиком, и кнопки подстройки, которые опрашиваются без пауз. В этом уроке соберём все три и разберём, почему delay в цикле часов ломает не только кнопки, но и само время. Про замену пауз на таймеры у меня есть общий разбор — millis() вместо delay(); устройство индикатора и библиотеки — в уроке про сам TM1637, а схема подключения с проводами — на лендинге про подключение TM1637.
Скелет часов: секунды из millis
Ядро часов — три строки арифметики. long s = millis() / 1000 — секунды с запуска. int mm = (s / 60) % 100 — минуты с потолком в 99: остаток от деления съедает сотню, и после 99:59 счёт пойдёт с нуля. int ss = s % 60 — секунды внутри минуты. На экран число уезжает склеенным: showNumberDecEx(mm * 100 + ss, 0x40, true) — третий аргумент true обязателен, без него 00:05 выглядит одинокой пятёркой у правого края. Хотите часы с минутами — считайте int ch = (s / 3600) % 24 и рисуйте ch * 100 + mm: формат тот же, меняется только множитель.
У арифметики есть два честных потолка. Первый — переполнение millis на 49-е сутки: часам оно не страшно, после перехода через ноль остаток от деления съест скачок, на экране мелькнёт 00:00, и счёт пойдёт дальше. Второй — сам ход: на Uno стоит керамический резонатор, и уход доходит до полпроцента — до семи минут в сутки, сколько именно у вашей платы, покажут сутки на столе. Для таймера заварки это неважно, для будильника — уже нет, и там в проект приезжает модуль DS3231 с настоящим кварцем.
Формат выбирайте под задачу сразу: ммсс с потолком 99 минут — для таймеров и секундомеров, ччмм с модулем 24 — для настенных часов. Переключить формат потом несложно — меняется один множитель, — но раскладка разрядов и ведущие нули остаются теми же, поэтому привычку писать арифметику тремя строками я не меняю годами.
Двоеточие не переключается отдельной командой — оно часть кадра. Правило: чётная секунда — рисуем с 0x40, нечётная — с 0x00, и каждый кадр уезжает на дисплей целиком через showNumberDecEx. Получается мигание с периодом в секунду: полсекунды горит, полсекунды гаснет, ровно как у настенных часов. Бит двоеточия лежит в байте разряда, и любой вывод числа его перезаписывает — поэтому зажечь один раз в setup не работает: первый же кадр без 0x40 его сотрёт. Всё состояние двоеточия живёт в байтах, которые вы отправили последними, и ни в чём больше.
Вторая привычка — рисовать кадр только когда он сменился. Заводим lastKadr и перерисовываем не чаще, чем раз в 250 мс: обмен с TM1637 короткий, но гонять одни и те же байты по CLK и DIO тысячи раз в секунду — трата ради траты. Это же условие спасает от мерцания: перерисовка по смене кадра всегда выглядит ровнее, чем перерисовка на каждом проходе loop(). Как раскидать по скетчу несколько независимых интервалов, я расписывал в планировщике задач на millis, а тот же тик для экрана LCD — в уроке про обновление по таймеру.
Отдельный плюс тика в 250 мс — запас на будущее: заведёте вторую строку данных или показ температуры по кнопке — кадр уже умеет обновляться вчетверо чаще, чем нужно, и расширение скетча не потребует пересмотра всего расписания.
Где delay ломает кнопки
Вернёмся к моему таймеру и посчитаем честно. В скетче с delay(1000) кнопка опрашивается один раз в секунду — между паузами. Нажатие пальца живёт 80-150 мс; чтобы digitalRead его увидел, мгновение нажатия должно совпасть с мгновением опроса, и совпадает это примерно в каждом шестом-десятом случае. Кнопка исправна — скетч слеп в 90 процентов времени. Хуже того, подстройка через удержание, к которой я пришёл вслепую, означала: пользователь держит кнопку почти секунду, чтобы гарантированно попасть в окно опроса. Это не интерфейс, это испытание терпения.
В millis-версии кнопки опрашиваются каждый проход loop(), а проход занимает единицы миллисекунд — окно нажатия перекрыто в сотни раз, и любое касание ловится с первого раза. Дебаунс держим классический: срабатывание по фронту HIGH-LOW и пауза 40 мс на нажатии — почему именно фронт, а не уровень, я разбирал в уроке про дребезг. Если захочется длинных нажатий — удержание набирает минуты быстрее — это уже автомат состояний, у него есть отдельный разбор с кликом, удержанием и двойным кликом; здесь возьмём из него только фронт. Заметьте, что кнопки и двоеточие в правильном скетче не знают друг о друге: у каждой пары свой last-счётчик, и дела не мешают друг другу даже в один проход.
// Антипаттерн: delay(1000) душит и кнопки, и точность#include <TM1637Display.h> TM1637Display disp(4, 5); const int btnPin = 2;long sekundy = 0; void setup() { pinMode(btnPin, INPUT_PULLUP); disp.setBrightness(5);} void loop() { // 1) кадр int mm = (sekundy / 60) % 100; int ss = sekundy % 60; disp.showNumberDecEx(mm * 100 + ss, 0x40, true); // 2) опрос кнопки — один раз за цикл if (digitalRead(btnPin) == LOW) { sekundy = sekundy + 60; // подстройка на минуту } delay(1000); // всё это время плата стоит sekundy = sekundy + 1; // отсюда и набегают минуты}| Дело в часах | Как часто | Почему так |
|---|---|---|
| Опрос кнопок | каждый проход loop() | нажатие живёт 80-150 мс — редкий опрос теряет касания |
| Перерисовка кадра | раз в 250 мс, по смене кадра | цифры меняются раз в секунду, чаще гонять байты незачем |
| Мигание двоеточия | каждые 500 мс | полсекунды горит, полсекунды гаснет — как у настенных часов |
| Подстройка удержанием | автоповтор каждые 150 мс после 600 мс | темп набора минут, при котором не промахнёшься |
Две кнопки: подстройка на лету
Подстройка строится на переменной-смещении, а не на перезаписи времени. Заводим long offset и считаем секунды как millis() / 1000 + offset. Кнопка на D2 прибавляет к offset 60 — минуту, кнопка на D3 — 3600, час. Время не устанавливается, а сдвигается, и часы при этом продолжают идти: ни один тик не теряется, пауз нет, ожиданий нет. Переполнение тут не грозит: long держит порядка двух миллиардов секунд — вам не хватит жизни платы, чтобы выгнать offset за край.
Границы держим руками. Offset меньше нуля я не даю — иначе часы однажды покажут знак минус, о котором отдельным пунктом ниже. И помните про формат: у ммсс потолок в 99 минут, прибавленный час просто крутит счёт по модулю — для таймера это нормально, но не для будильника, где время должно переживать перезагрузку и идти ровно; там настоящий RTC и своя история.
И про пины в конце: две кнопки занимают D2 и D3, дисплей — D4 и D5, и это вся проводка проекта. Если D2 и D3 у вас заняты другими делами, кнопки переезжают на любые свободные цифровые пины — в скетче меняются две константы и больше ничего.
- Секунды берутся из millis()/1000, минуты и секунды раскладываются делением, число ммсс уезжает с ведущими нулями
- Двоеточие — часть кадра: чётная секунда с 0x40, нечётная с 0x00, отдельного переключателя у модуля нет
- Кнопки опрашиваются каждый проход loop(): delay(1000) оставляет кнопке одно окно в секунду — отсюда срабатывания через раз
- Подстройка — через offset к millis(), а не перезапись времени: часы продолжают идти без потери тиков
- offset не даём уходить в минус, иначе showNumberDec нарисует знак и разрядов не хватит
- 1. Почему в delay-версии часов кнопка срабатывает через раз?
- 2. Что покажет showNumberDecEx(mm * 100 + ss, 0x40, true) при 12 минутах и 5 секундах?
- 3. Как устроена подстройка времени кнопками в millis-версии?
- 4. Как часто стоит перерисовывать кадр часов на TM1637?
- 5. Что произойдёт, если offset уйдёт в минус?
Частые вопросы по теме урока
- Почему в delay-версии часов кнопка срабатывает через раз?
- Внутри delay() код стоит: digitalRead выполняется раз в секунду, и совпадение с коротким нажатием — лотерея примерно один к шести-десяти. опрос происходит раз в секунду, а нажатие живёт 80-150 мс
- Что покажет showNumberDecEx(mm * 100 + ss, 0x40, true) при 12 минутах и 5 секундах?
- Число склеивается как 12 x 100 + 5 = 1205, а true дорисовывает ведущие нули — без них разряды слева погасли бы. 12:05
- Как устроена подстройка времени кнопками в millis-версии?
- Время считается как millis()/1000 + offset: кнопки двигают offset, и ни один тик не теряется — пауз и перезапусков нет. меняется offset, часы продолжают идти
- Как часто стоит перерисовывать кадр часов на TM1637?
- Цифры меняются раз в секунду, двоеточие — раз в полсекунды: тик в 250 мс покрывает оба ритма и не гоняет байты зря. раз в 250 мс по тику, только при смене кадра
- Что произойдёт, если offset уйдёт в минус?
- Отрицательное время честно выводится со знаком — и окно из четырёх разрядов перестаёт вмещать значение. Ограничивайте offset снизу. showNumberDec нарисует минус, и разрядов не хватит