Замок на тумбочку с ключами я собрал за вечер и к утру разобрал обратно. Сканер клавиатуры из прошлого урока честно печатал каждую нажатую клавишу в монитор порта — но печать и есть вся его польза: любой гость нажимает что угодно, плата рапортует в никуда, а прибор, который всем всё рассказывает, охраны не прибавляет. Не хватает двух этажей: памяти о том, что уже набрано, и решения «свой или чужой».
В этом уроке достраиваем оба. Нажатия копятся в буфер до клавиши решётки, затем набранное сверяется с эталоном — у меня это 2580, весь второй столбец мембранки сверху вниз, а у вас любой, ведь раскладка живёт в массиве. Совпало — зелёный светодиод, короткий радостный бип и пять секунд открытой двери, после которых замок сам возвращается в закрытое состояние. Не совпало — красный и три тревожных гудка зуммером. Сам сканер берём готовый из урока про матричную клавиатуру: здесь он станет тремя строками внутри большего скетча, и именно так выглядит переиспользование наработок в реальных проектах.
Замок — идеальный первый проект, где данные идут по полной цепочке: палец, матрица, сканер, буфер, сравнение, действие. Нажатая клавиша в этой цепи то же самое, что команда, пришедшая из монитора порта: снаружи приходит символ, и кто-то внутри должен его разобрать и решить, что он значит. Разница только в проводе — на выходе вы получите прибор, который понимает последовательности, а не отдельные нажатия.
Замок как автомат: ждём, копим, сверяем
Четыре состояния — закрыт, набор, открыто, тревога — ложатся на схему из урока про конечный автомат один в один, и это не красивость: без памяти состояния замок не собрать, потому что одно и то же нажатие решётки должно означать разное в зависимости от того, что было до него. Память держат три переменные: pos считает набранные клавиши, nabrano[8] хранит сами символы, флаг otkryto отличает открытую дверь от закрытой. Восемь ячеек в буфере — с запасом: цифры сверх восьми молча не пишутся, замок от длинного набора не переполняется и не сходит с ума, а просто ждёт решётку с тем, что успело набраться. Всё остальное — если и циклы, которые вы уже писали руками сотню раз.
Первая грабля — удержание. Пока клавиша зажата, сканер видит её на каждом проходе loop, и наивный скетч запишет пятёрку трижды за четверть секунды: я поймал на этом 555 вместо 5 и добрые полминуты подозревал клавиатуру в поломке, хотя виноват был собственный цикл. Лекарство — память derzhim: клавиша принимается только если она отличается от той, что сканер видел в прошлый раз. Отпустили — derzhim обнулился, и следующее нажатие снова считается новым. Приём тот же, что у фронтов кнопки из урока про дребезг, только порогом служит сама клавиша; после принятого нажатия я ещё ставлю паузу 30 мс — мембранка дребезжит ровно так же, как обычная кнопка, просто тише. И про печать: замок выводит в монитор не сами цифры, а счётчик Zvezdochek — банкоматный обычай маскировать ввод, чтобы код не читался через плечо ни из журнала, ни с экрана.
Вторая грабля — брошенный ввод. Набрали две цифры и ушли: буфер висит с половиной кода, и следующий человек молча допишет вторую половину поверх чужой. Замок, который помнит чужой набор, — это дыра в буквальном смысле, поэтому у всех замков, от офисных домофонов до банковских ячеек, есть таймаут ввода. Мой — десять секунд тишины, буфер чистится, замок ждёт с начала; заодно таймаут страхует и залипшую клавишу, которая иначе успела бы забить буфер мусором за пару минут.
Сравнение с эталоном начинается с длины: набрано не четыре клавиши — сверять нечего, это уже неверный код. Дальше цикл по четырём местам: nabrano[i] против ETALON[i], и флаг sovpalo сбрасывается при первом же несовпадении. Отдельный флаг вместо счётчика совпадений — не прихоть: счётчик хочется инкрементировать в двух местах и проверять в третьем, а флаг сбрасывается один раз и читается однозначно. Массивы в C++ не сравниваются знаком равенства — только поэлементно, мы это разбирали в уроке про String и сравнение; для буфера из восьми символов честный цикл из четырёх строк надёжнее любой обёртки.
Тревога и успех различаются звуком сознательно: три гудка по 200 мс на частоте 400 Гц с паузами 120 мс — примерно секунда резкого низкого гула, который не спутаешь ни с чем, против одного писка 1200 Гц на 120 мс — высокого и короткого, как подтверждение на кассе. Оба сигнала делает tone() с третьим аргументом-длительностью, разобранным в уроке про пьезозуммер. На плате tone() не задерживает loop: звук идёт сам по таймеру, программа уже печатает в Serial. В симуляторе длительность внутри tone() паузит скетч на те же миллисекунды — на логику замка это не влияет, а различие честнее знать заранее.
Открытое состояние держится не delay(5000), а меткой millis(): при верном коде otkrytoDo = millis() + 5000, и пока часы не дотикали, замок открыт. Разница проявится, как только захотите, чтобы во время открытой двери клавиатура жила: delay заморозил бы весь скетч на пять секунд, а таймер на millis оставляет loop свободным — ввод, тревога и печать продолжают работать. Полный разбор приёма с примерами — в уроке про millis вместо delay.
Полный скетч: пины, уровни и буфер
const int STROKI[4] = {2, 3, 4, 5};const int STOLBY[4] = {6, 7, 8, 9};const int LED_ZAKRYT = 12; // красный: замок закрытconst int LED_OTKRYT = 11; // зелёный: код принятconst int ZUMMER = 13;char klavishi[16] = {'1','2','3','A','4','5','6','B','7','8','9','C','*','0','#','D'};char ETALON[4] = {'2','5','8','0'}; // второй столбец сверху вниз char nabrano[8] = {'0','0','0','0','0','0','0','0'};int pos = 0;int derzhim = -1; // индекс зажатой клавиши, -1 — клавиш нетunsigned long posledneeNazhatie = 0;unsigned long otkrytoDo = 0;bool otkryto = false; void setup() { for (int r = 0; r < 4; r++) { pinMode(STROKI[r], OUTPUT); digitalWrite(STROKI[r], HIGH); // неактивная строка — HIGH } for (int c = 0; c < 4; c++) { pinMode(STOLBY[c], INPUT_PULLUP); } pinMode(LED_ZAKRYT, OUTPUT); pinMode(LED_OTKRYT, OUTPUT); pinMode(ZUMMER, OUTPUT); Serial.begin(9600); digitalWrite(LED_ZAKRYT, HIGH); Serial.println("Vvedite kod, zatem #");} int opros() { for (int r = 0; r < 4; r++) { digitalWrite(STROKI[r], LOW); // активная строка — LOW: плате нужен путь на землю for (int c = 0; c < 4; c++) { if (digitalRead(STOLBY[c]) == LOW) { digitalWrite(STROKI[r], HIGH); return r * 4 + c; // индекс клетки в раскладке } } digitalWrite(STROKI[r], HIGH); } return -1;} void proverka() { bool sovpalo = (pos == 4); for (int i = 0; i < 4; i++) { if (nabrano[i] != ETALON[i]) sovpalo = false; } pos = 0; if (sovpalo) { otkryto = true; otkrytoDo = millis() + 5000; digitalWrite(LED_ZAKRYT, LOW); digitalWrite(LED_OTKRYT, HIGH); tone(ZUMMER, 1200, 120); Serial.println("Kod vernyi: otkryto na 5 s"); } else { Serial.println("Kod nevernyi: trevoga"); for (int i = 0; i < 3; i++) { tone(ZUMMER, 400, 200); delay(120); } }} void loop() { int i = opros(); // индекс клавиши или -1 if (i >= 0 && i != derzhim) { // новое нажатие, а не удержание char k = klavishi[i]; posledneeNazhatie = millis(); if (k == '*') { pos = 0; Serial.println("Sbros: vvod s nachala"); delay(30); } else if (k == '#') { proverka(); } else if (pos < 8) { nabrano[pos] = k; pos = pos + 1; Serial.print("Zvezdochek: "); Serial.println(pos); delay(30); } } derzhim = i; if (pos > 0 && millis() - posledneeNazhatie > 10000) { pos = 0; // таймаут: буфер не должен ждать вечно Serial.println("Sbros po taymautu"); } if (otkryto && millis() > otkrytoDo) { otkryto = false; digitalWrite(LED_OTKRYT, LOW); digitalWrite(LED_ZAKRYT, HIGH); Serial.println("Zamok zakrylsya"); } delay(10);}| Событие | Что делает замок | Что вы видите |
|---|---|---|
| 2, 5, 8, 0 и # | сверяет буфер с эталоном поэлементно | зелёный, один бип 1200 Гц, через 5 с закрыто |
| четыре цифры мимо эталона | фиксирует несовпадение хоть в одном месте | красный, три гудка 400 Гц, буфер чист |
| клавиша зажата дольше прохода | принимает её один раз — за фронтом следит derzhim | одна цифра вместо очереди |
| 10 секунд тишины при набранной части | чистит буфер, ждёт ввод с начала | Sbros po taymautu в мониторе |
| * в любой момент набора | сбрасывает буфер, не трогая замок | Sbros: vvod s nachala |
Живая клавиатура и честные границы
Мембранка живёт на этих восьми пинах годами без обслуживания: ток через замкнутую клавишу — доли миллиампера, греться и изнашиваться тут нечему, единственное слабое место — шлейф в разъёме, который отгибается при частых перетыканиях. Сам шлейф — восемь жил на стандартном шаге 2,54 мм: втыкаются в макетку без обжимки и переходников, у меня один экземпляр кочует между проектами третий год. Раскладку в массиве klavishi[16] я менял трижды и ни разу не тронул провода: приезжает клавиатура с другой распиновкой — переставляешь символы в одной строке массива, и замок снова читает мысли. Пины строк и столбцов при этом остаются на месте: формула r умножить на 4 плюс c безразлична к тому, какой буквой вы назвали клетку.
Честное ограничение: замок из этого урока — учебный, и защита у него уровнем ниже, чем у амбарного. Эталон лежит во флеше открытым текстом — подключившийся к плате человек прочтёт его дампом памяти; три неверных попытки не блокируют ничего, перебирать можно хоть без паузы. Настоящие замки считают попытки и растят паузу после каждой, а код хранят не строкой, а хэшем. Прикинем масштаб: четыре цифры — десять тысяч комбинаций; пальцами такой перебор займёт недели, а скетчем по UART — секунды, поэтому у серьёзных замков пауза после неудачи растёт геометрически: пять, десять, двадцать секунд. Мы это сознательно отложили: сейчас ценность в цепочке сканер — буфер — сравнение — действие, а счётчик попыток с минутной блокировкой — готовое домашнее задание из списка ниже.
Куда это растёт дальше. Тот же буфер с таймаутом принимает команды по UART, коды ИК-пульта или последовательность с энкодера — меняется источник, логика стоит на месте. А если добавить дисплей, замок превращается в прибор с приглашением «Vvedite kod:» и звёздочками вместо цифр — интерфейсная надстройка над уже работающим сердцем. Ниже — живая клавиатура: нажмите клавишу и проследите, как сканер ведёт строку и находит столбец, — ровно то, что делает opros() в замке.
Опрос строк и столбцов: как подтяжки ловят нажатия и почему дребезг не страшен.
Подгружаем интерактив…
- Добавьте счётчик попыток: после трёх неверных кодов замок молчит минуту — таймер на millis() вы уже держите в руках.
- Смену кода сделайте из самого замка: удержание клавиши D переводит в режим записи нового эталона.
- Заведите мастер-код: второй эталон и второй флаг — он открывает дверь и сбрасывает счётчик попыток.
- Замените зелёный светодиод на сервопривод из одноимённого раздела: 90 градусов — засов выдвинут, 0 — убран.
- 1. Почему одна зажатая клавиша учитывается один раз?
- 2. Что первым делом проверяет сравнение с эталоном?
- 3. Зачем замку таймаут ввода в десять секунд?
- 4. Замок открылся по верному коду. Что вернёт его в закрытое состояние?
- 5. Где нельзя размещать строки клавиатуры?
- Замок — автомат с памятью: буфер nabrano[8] и счётчик pos копят набор, флаги otkryto и derzhim различают состояния.
- Одно нажатие — одно событие: клавиша принимается на фронте относительно derzhim, пауза 30 мс гасит дребезг мембранки.
- Сравнение с эталоном: сначала длина набора, затем поэлементный цикл с флагом sovpalo — массивы знаком равенства не сравнивают.
- Тревога и успех различаются звуком: три гудка 400 Гц по 200 мс против одного писка 1200 Гц; открытие — метка millis() + 5000.
- D0 и D1 не трогаем — это UART; в симуляторе строка активна HIGH, на плате LOW: одна схема с зеркальными уровнями.
Частые вопросы по теме урока
- Почему одна зажатая клавиша учитывается один раз?
- Пока клавиша зажата, сканер видит её на каждом проходе loop. Событием считается только смена клавиши относительно derzhim: отпустили — память обнулилась, следующее нажатие снова новое. Память derzhim ловит фронт: клавиша совпала с прошлой — событие не считается
- Что первым делом проверяет сравнение с эталоном?
- Набрано не четыре — сверять нечего, это уже неверный код. Затем идёт поэлементный цикл с флагом sovpalo, который сбрасывается при первом несовпадении. Длину набора — набраны ли все четыре клавиши
- Зачем замку таймаут ввода в десять секунд?
- Буфер с чужой половиной кода — прямая дыра: следующий человек допишет недостающее поверх. Таймаут чистит буфер и возвращает замок к началу; заодно он страхует залипшую клавишу. Брошенная половина кода не должна ждать следующего пользователя
- Замок открылся по верному коду. Что вернёт его в закрытое состояние?
- Открытие живёт на таймере: otkrytoDo = millis() + 5000, и как только часы дошли — светодиоды меняются, замок сам закрывается. Нажатия в это время продолжают обрабатываться. Дедлайн метки millis() + 5000 в loop
- Где нельзя размещать строки клавиатуры?
- Пины 0 и 1 заняты приёмопередатчиком USB: шлейф на них ломает и загрузку скетча, и монитор порта. Именно так устроен пример Custom Keypad из поставки библиотеки — не повторяйте. D0 и D1 — это аппаратный UART