Счётчик нажатий — самый частый запрос после первых кнопочных уроков: посчитать детали на входе, людей в комнате, циклы станка. Схема из прошлого урока готова, но скетч пугает: на каждую цифру семь вызовов digitalWrite, на десять цифр — семьдесят строк, и в одной из пар я умудрился перепутать пятёрку с шестёркой. Правишь сегмент в цифре «5» — забываешь тот же сегмент в цифре «6». Код растёт линейно, а места для проделок в нём — с каждой цифрой всё больше.
Здесь второй заход, и он про таблицу вместо повторов: цифра становится байтом-паттерном, вся таблица цифр — массивом из десяти байт, а рисование — функцией showDigit(n) в семь строк. Кнопки плюс/минус добавят счётчику управление: фронт HIGH-LOW, антидребезг на millis и кольцевую обёртку 0-9, после которой девятка прыгает на ноль без пауз. Массивы уже мелькали в уроке про массивы и паттерны, чтение кнопки разобрано в уроке про digitalRead — здесь обе механики работают в паре и прикрывают друг друга. Проверка у счётчика одна: кольцо обязано ходить от нуля до девятки в обе стороны без заездов за границу массива, сколько бы кнопок ни было зажато подряд.
Сегменты как биты одного байта
Идея в том, что семь сегментов — это семь бит одного байта. Договариваемся о порядке: бит 0 — сегмент a, бит 1 — b, и так до бита 6 — g. Тогда цифра описывается одним числом: ноль — горят сегменты a-f, это биты 0-5, получается 0b0111111. В двоичной записи паттерн читается прямо по сегментам — включён бит, горит палочка. А вот десятичная запись того же числа, 63, немая: из неё не видно ни верхней перекладины, ни спящей средней черты, только абстракция, которую приходится держать в голове.
Порядок бит — не стандарт, а договорённость конкретного скетча, главное — чтобы бит i всегда означал один и тот же сегмент и совпадал с массивом пинов. Приятный бонус: в даташитах готовых шрифтов, например у драйвера TM1637, сегменты лежат в тех же младших битах байта в порядке a-g — привычка, набитая на голом индикаторе, переносится на модули один в один. Точка dp в этой схеме — бит 7, и если она не подключена, паттерн её просто игнорирует.
Тип byte в объявлении массива выбран не случайно: это те же восемь бит без знака, 0-255, ровно один байт памяти. Массив из десяти цифр занимает 10 байт SRAM, а в типе int — уже 20: int тянет два байта. На Uno с её двумя килобайтами SRAM разница в два раза звучит громче, чем кажется: большие таблицы шрифтов для многоразрядных табло набирают десятки и сотни байт, и привычка хранить сегментные паттерны в byte выливается в свободную память для остального скетча. Соберёте когда-нибудь шрифт для многоразрядного табло — экономия вырастет кратно: по байту на каждый сегментный байт каждой цифры.
| Цифра | Байт | Двоичный вид | Что горит |
|---|---|---|---|
| 0 | 63 | 0b0111111 | a b c d e f — всё, кроме средней черты g |
| 3 | 79 | 0b1001111 | a b c d g — правая сторона и обе перекладины |
| 6 | 125 | 0b1111101 | a c d e f g — с хвостом слева, без правой верхней |
| 8 | 127 | 0b1111111 | все семь сегментов |
const int SEG[7] = {2, 3, 4, 5, 6, 7, 8}; // a, b, c, d, e, f, gconst int BTN_PLUS = 9;const int BTN_MINUS = 10;const int N = 7; byte CIF[10] = { 0b0111111, 0b0000110, 0b1011011, 0b1001111, 0b1100110, 0b1101101, 0b1111101, 0b0000111, 0b1111111, 0b1101111}; int cifra = 0;int prevPlus = HIGH;int prevMinus = HIGH;unsigned long tPlus = 0;unsigned long tMinus = 0; void showDigit(int n) { for (int i = 0; i < N; i++) { if ((CIF[n] >> i) & 1) { digitalWrite(SEG[i], HIGH); } else { digitalWrite(SEG[i], LOW); } }} void setup() { for (int i = 0; i < N; i++) pinMode(SEG[i], OUTPUT); pinMode(BTN_PLUS, INPUT_PULLUP); pinMode(BTN_MINUS, INPUT_PULLUP); Serial.begin(9600); showDigit(cifra); Serial.println("Schetchik 0-9: plus D9, minus D10");} void loop() { int plus = digitalRead(BTN_PLUS); int minus = digitalRead(BTN_MINUS); if (plus == LOW && prevPlus == HIGH && millis() - tPlus > 200) { tPlus = millis(); cifra = cifra + 1; if (cifra > 9) cifra = 0; showDigit(cifra); Serial.print("cifra="); Serial.println(cifra); } prevPlus = plus; if (minus == LOW && prevMinus == HIGH && millis() - tMinus > 200) { tMinus = millis(); cifra = cifra - 1; if (cifra < 0) cifra = 9; showDigit(cifra); Serial.print("cifra="); Serial.println(cifra); } prevMinus = minus;}Сердце скетча — функция showDigit, и в ней два приёма, которые стоит разобрать по буквам. Цикл идёт по сегментам i от 0 до 6, выражение (CIF[n] >> i) & 1 достаёт из байта бит номер i: сначала сдвигает байт вправо на i позиций, чтобы нужный бит встал на младшее место, потом маска & 1 обрезает всё остальное. Единица — сегменту HIGH, ноль — LOW. Своя функция уже встречалась в уроке про свои функции, здесь она приносит первую заметную экономию: десять цифр — десять чисел в массиве, а не семьдесят строк кода.
Кнопки опрашиваются по фронту: сравнение с prevPlus ловит момент перехода HIGH в LOW, и счётчик срабатывает один раз за нажатие, а не сто раз за секунду удержания. Окно антидребезга в 200 мс глушит повторные дребезги контактов — механику фронта и дребезга я подробно разбирал в уроке про антидребезг, здесь она размножена на две кнопки с отдельными таймерами tPlus и tMinus. Если зажать обе кнопки разом, ветки отработают в одном проходе loop() и цифра останется на месте — плюс и минус аннулируют друг друга, что честно.
Задержек delay в скетче нет: loop() оборачивается за миллисекунды, и ни одно нажатие не проскочит — тем же принципом в третьем уроке этого кластера оживёт таймер. В sim-версии в конце loop() стоит delay(10): это не пауза «после нажатия», а короткая передышка движку, и против окна нажатия в 100-150 мс она незаметна. Подробно о том, почему delay в больших скетчах вреден и что приходит на смену, написано в уроке про millis вместо delay.
Кольцо 0-9 и границы массива
Обёртка цикла — две строки, на которых держится вся навигация: после девятки плюс возвращает счётчик на ноль, после нуля минус отправляет на девятку. Так работают навигация по меню, выбор режима, перебор настроек — тот же принцип, что в меню на двух кнопках. Кольцо удобнее останова на границе: пользователю не надо возвращаться назад, чтобы перейти от нуля к девятке, — путь всегда короткий, а управлять можно одной рукой.
За обёрткой стоит не эстетика, а защита памяти. В C++ обращение к массиву за границей не вызывает никаких ругательств компилятора: CIF[10] молча читает соседнюю ячейку, и на индикатор вылезает случайный узор из битов чужой переменной. Я один раз ловил именно это: минус-кнопка на нуле уводила счётчик в -1, и на дисплее вспыхивала каша. Проверки if (cifra > 9) и if (cifra < 0) стоят две строки — дешевле, чем час с отладкой «самопроизвольной» живописи.
В симуляторной макетке одна кнопка — она играет роль плюса: счётчик крутится по кольцу 0-9 от каждого нажатия. Индикатор садится на пины D12-D6 в порядке a-g, кнопка — на D2, и sim-скетч ниже держит именно эти номера; на реальной плате берите раскладку из большого скетча выше — сегменты 2-8, кнопки 9 и 10. Паттерны в обоих скетчах идентичны байт в байт: массив CIF не знает, какие пины прячутся за массивом SEG, и в этом вся прелесть упаковки цифры в число. Перенос скетча между платами сводится к правке одной строки с пинами — вся логика остаётся нетронутой.
Кстати о шрифте: массив CIF — обычная таблица соответствий, и её можно расширять. Байт 0b1000000 зажигает только среднюю черту — получается знак минус, которым удобно показывать отрицательную температуру или паузу; 0b0110000 — заготовка для дробных значений вместе с точкой. Буквы A, b, C, d, E, F тоже рисуются семью сегментами и позволяют выводить шестнадцатеричные числа — до них дойдёте, когда счётчику понадобится больше десяти значений, а понадобится он вам раньше, чем кажется.
Зажигайте сегменты по одному биту и собирайте цифры 0–9.
Подгружаем интерактив…
- Цифра — байт-паттерн: бит 0 — сегмент a, бит 6 — g; ноль это 0b0111111, восьмёрка 0b1111111
- Десять паттернов живут в массиве CIF[10] типа byte — 10 байт SRAM против 20 у int; вся таблица цифр занимает десять строк вместо семидесяти
- showDigit(n) прогоняет (CIF[n] >> i) & 1 по семи сегментам: сдвиг ставит бит на младшее место, маска & 1 обрезает остальное
- Кнопки по фронту HIGH-LOW с окном антидребезга 200 мс на millis; loop() без delay — ни одно нажатие не теряется
- Кольцо 0-9 — это защита массива: без if (cifra < 0) и if (cifra > 9) обращение CIF[10] молча читает чужую память
- 1. Какой сегмент отвечает бит 2 в паттерне?
- 2. Что делает выражение (CIF[n] >> i) & 1 внутри showDigit?
- 3. Почему кнопка опрашивается по фронту HIGH-LOW, а не просто по LOW?
- 4. Счётчик стал равен 10, а проверки if (cifra > 9) нет. Что покажет showDigit(10)?
- 5. Зачем в скетче окно антидребезга на millis(), а не delay(200) после нажатия?
Частые вопросы по теме урока
- Какой сегмент отвечает бит 2 в паттерне?
- Порядок договорной, но в этом кластере бит 0 — a, бит 1 — b, бит 2 — c, и так по кругу до бита 6 — g. Единица в бите 2 зажигает правую нижнюю палочку. c — правая нижняя
- Что делает выражение (CIF[n] >> i) & 1 внутри showDigit?
- Сдвиг >> i опускает нужный бит в позицию 0, маска & 1 обнуляет всё остальное. Результат 0 или 1 превращается в LOW или HIGH на пине сегмента. достаёт бит номер i: сдвиг ставит его на младшее место, маска & 1 отрезает остальные
- Почему кнопка опрашивается по фронту HIGH-LOW, а не просто по LOW?
- digitalRead возвращает уровень, и пока кнопка зажата, условие btn == LOW верно каждый проход loop(). Сравнение с предыдущим состоянием ловит момент нажатия — один раз. по фронту нажатие считается один раз, а по уровню — сотни раз за удержание
- Счётчик стал равен 10, а проверки if (cifra > 9) нет. Что покажет showDigit(10)?
- C++ не проверяет границы массивов. CIF[10] — ячейка за пределами массива с чужими битами; на индикаторе вылезает мусор. Обёртка if (cifra > 9) cifra = 0 обязательна. случайный узор: CIF[10] молча читает соседнюю память
- Зачем в скетче окно антидребезга на millis(), а не delay(200) после нажатия?
- После delay(200) скетч 200 мс не делает ничего: минус-кнопка и монитор глухи. Гейт millis() - tPlus > 200 отсекает только повторные срабатывания, не трогая остальные дела loop(). delay заморозил бы loop(), и вторая кнопка с Serial не опрашивались бы 200 мс