Покупные двоичные часы у меня жили на полке два года: две колонки огоньков, левые — часы, правые — минуты, и каждый светодиод — степень двойки. Считывается такая штука медленнее стрелок, зато честнее любого цифрового дисплея показывает, что время — число. Когда собрал свою на Uno, упёрся в арифметику сразу: часы 0–23 просят пять бит, минуты 0–59 — шесть, вместе одиннадцать, а шкала из восьми светодиодов — это восемь бит. Показывать время целиком не на что, и это не тупик, а условие игры.
Соберём часы в упрощённом виде: шкала по очереди показывает либо часы (0–23, пять бит), либо минуты пятёрками (0–11, четыре бита) — как в классических двоичных часах-браслетах, где минуты делят на пятёрки, чтобы читалось быстрее. Кнопка D2 переключает показ, секунды тикают на millis(), а стартовое время заводится через Serial.parseInt — и если его не завести, часы стартуют с 00:00. Шкала — та же, что в уроке про бегущие огни, восемь светодиодов на цифровых пинах через резисторы 220 Ом.
Сколько бит нужно времени
Считаем по той же логике степеней двойки, что и в прошлом уроке. Часы живут в 0–23: четырёх бит хватает на 0–15, пяти — на 0–31, значит, пять бит. Минуты 0–59: пять бит дают только 0–31, шесть — 0–63, значит, шесть. Секундам тоже нужно шесть. Сумма 5 + 6 = 11 бит на часы и минуты — на восемь светодиодов не помещается, потому и поочерёдный показ.
Минуты пятёрками — это целочисленное деление: minuty / 5 даёт 0–11, четыре бита, а точность показа — та самая пятёрка минут. Для кухонных часов это нормально, для таймера варки яиц уже нет, и тут надо быть честным с собой: минуты целиком (шесть бит) тоже влезают в восемь светодиодов, просто по очереди с часами. Пятёрки я оставил из уважения к классике — они читаются быстрее, потому что битов меньше, — а минуты без округления останутся вам задачей на доработку: поменять одну строку.
| Показатель | Диапазон | Бит | На шкале |
|---|---|---|---|
| часы | 0-23 | 5 | огни 0-4, старшие три погашены |
| минуты целиком | 0-59 | 6 | влезает: задача на доработку |
| минуты пятёрками | 0-11 | 4 | огни 0-3, читается быстрее всех |
| секунды | 0-59 | 6 | тикают в скетче, на шкалу не выводятся |
Тик на millis() и завод через Serial
Время в этом скетче — три счётчика и один таймер на millis(): раз в 1000 мс секунда прибавляет единицу, набралось 60 — сброс и минута, набралось 60 минут — сброс и час, 24 часа — полночь. Паттерн тот же, что в уроке про millis() вместо delay: loop() не спит ни секунды, кнопка опрашивается каждый проход. Про то, как долго так часы держат ход и от чего уезжает кварц, у нас есть отдельный разбор — часы без модуля на millis: там же лежит завод времени делением и остатком, на который я опирался.
Завод времени берёт два числа из монитора: Serial.parseInt() читает часы, второй вызов — минуты. У parseInt есть два свойства, о которых стоит знать заранее. Первое: если данных нет, он ждёт до 1000 мс и возвращает 0 — не введёте время, часы честно стартуют с 00:00. Второе: нецифровые символы он пропускает, поэтому заводится даже строка 14:35 — двоеточие просто перепрыгивается. Числа обжимаются constrain-ом: 99 часов превратятся в 23, минус — в ноль; механика parseInt с примерами разбиралась в уроке про чтение чисел из Serial.
const int BTN = 2;const int BITS[8] = {5, 6, 7, 8, 9, 10, 11, 12}; // бит 0 -> пин 5 ... бит 7 -> пин 12 byte chasy = 0; // 0-23byte minuty = 0; // 0-59byte sekundy = 0; // 0-59int pokaz = 0; // 0 — часы, 1 — минуты пятёркамиunsigned long tSek = 0;int prevBtn = HIGH; void vyvesti(byte b) { for (int i = 7; i >= 0; i = i - 1) { digitalWrite(BITS[i], bitRead(b, i)); }} void pechat() { Serial.print("chas="); Serial.print(chasy); Serial.print(" min="); Serial.print(minuty); Serial.print(" pokaz="); Serial.println(pokaz == 0 ? "chasy" : "minuty/5");} void setup() { for (int i = 0; i < 8; i = i + 1) { pinMode(BITS[i], OUTPUT); } pinMode(BTN, INPUT_PULLUP); Serial.begin(9600); Serial.println("Vvedite chasy (0-23), potom minuty (0-59):"); chasy = constrain(Serial.parseInt(), 0, 23); minuty = constrain(Serial.parseInt(), 0, 59); Serial.print("Zavod: "); Serial.print(chasy); Serial.print(":"); Serial.println(minuty); vyvesti(chasy); pechat();} void loop() { int btn = digitalRead(BTN); if (btn == LOW && prevBtn == HIGH) { // фронт: одно нажатие — один показ pokaz = 1 - pokaz; if (pokaz == 0) { vyvesti(chasy); } else { vyvesti(minuty / 5); } pechat(); delay(30); } prevBtn = btn; if (millis() - tSek >= 1000) { // секундный тик tSek = millis(); sekundy = sekundy + 1; if (sekundy >= 60) { sekundy = 0; minuty = minuty + 1; if (minuty >= 60) { minuty = 0; chasy = chasy + 1; if (chasy >= 24) { chasy = 0; } } } if (pokaz == 0) { vyvesti(chasy); // 0-23 -> 5 бит } else { vyvesti(minuty / 5); // 0-11 -> 4 бита } pechat(); }}Два решения здесь стоит разобрать, потому что они повторятся в любом проекте с индикацией. Первое: vyvesti() перерисовывает все восемь бит целиком, а не «догорает» частично — на шкале за минуту меняются один-два огонька, но полная перерисовка стоит пары микросекунд и исключает рассинхрон, когда в байте число, а на светодиодах чужая картинка. Второе: кнопка читается по фронту HIGH-LOW с проверкой prevBtn и паузой 30 мс — иначе удержание листало бы показы десятки раз в секунду. Полный разбор дребезга и фронта — в уроке про дребезг кнопки.
Печать времени в журнале устроена тройкой chas/min/pokaz — секунд я не печатаю нарочно: строка раз в секунду читаема, а с секундами журнал превращается в шум. Тернарник в pechat() выбирает подпись показа; если такая строка непривычна, замените его обычным if — разницы в поведении нет. И обратите внимание на типы: chasy, minuty, sekundy — byte, потому что диапазоны 0–23 и 0–59 влезают в один байт с запасом, о чём шла речь в первом уроке кластера.
- Часы 0-23 — пять бит, минуты 0-59 — шесть: вместе 11 бит, на шкале из восьми показ по очереди, кнопка переключает
- Минуты пятёрками — minuty / 5, диапазон 0-11 и четыре бита; точность показа ±5 минут
- Тик — if (millis() - tSek >= 1000) с переносами 60-60-24; loop() остаётся живым для кнопки
- Serial.parseInt() ждёт до 1000 мс и возвращает 0 без ввода; строка 14:35 заводится одной посылкой
- Millis-часы не переживают выключение и уезжают на минуты в сутки; DS3231 помнит время и держит ±2 ppm
- 1. Сколько бит нужно, чтобы закодировать минуты 0-59?
- 2. Кнопку читают по уровню без проверки фронта. Что будет с показом часов?
- 3. Что вернёт Serial.parseInt(), если за секунду ожидания в монитор ничего не ввели?
- 4. Почему у настоящих RTC-модулей вроде DS3231 ход точнее, чем у millis-часов на Uno?
- 5. На шкале в режиме минут-пятёрок горят огни 0 и 3. Какое сейчас примерно время минут?
Частые вопросы по теме урока
- Сколько бит нужно, чтобы закодировать минуты 0-59?
- Пять бит дают максимум 31 — мало. Шесть бит покрывают 0-63, и 59 влезает с запасом. Секундам нужны те же шесть бит. 6
- Кнопку читают по уровню без проверки фронта. Что будет с показом часов?
- Уровень LOW держится всё нажатие, и каждый проход loop() засчитал бы новое переключение. Поэтому сравнивают с prevBtn по фронту и ставят паузу 30 мс. показ будет переключаться туда-сюда десятки раз в секунду, пока кнопка удерживается
- Что вернёт Serial.parseInt(), если за секунду ожидания в монитор ничего не ввели?
- Таймаут по умолчанию 1000 мс, после него parseInt возвращает 0. Часы честно стартуют с 00:00 — и это стоит учитывать при заводе. 0
- Почему у настоящих RTC-модулей вроде DS3231 ход точнее, чем у millis-часов на Uno?
- Керамический резонатор Uno уезжает на минуты в сутки. DS3231 держит ±2 ppm за счёт температурной компенсации — плюс батарейка, которая помнит время при выключенной плате. у DS3231 температурно-компенсированный кварц: ±2 ppm, около 0,2 секунды в сутки
- На шкале в режиме минут-пятёрок горят огни 0 и 3. Какое сейчас примерно время минут?
- 0b00001001 — это 9, значит minuty / 5 = 9, и минуты лежат между 45 и 49. Бит 0 весит 1, бит 3 весит 8 — степени двойки никто не отменял. около 9 минут: 1 + 8