База знаний · Датчики и модули
Энкодер, джойстик и клавиатура для Arduino
Энкодер считает через раз не от дребезга в проводах, а от способа чтения: прерывания тут делают хуже, а не лучше.
Светодиод помигали, датчик прочитали — дальше устройству нужен человек. Ручка, палка, клавиши: потенциометр, энкодер, джойстик и матричная клавиатура. Все четыре стоят от ста рублей, все четыре подключаются за минуту — и все четыре ведут себя не так, как ожидается. Энкодер считает щелчки через раз, джойстик едет сам, клавиатура выдумывает кнопки. Разбираем, почему так, и что написать, чтобы этого не было.
Что выбрать под задачу
Четыре устройства решают четыре разные задачи, и подменять одно другим обычно неудобно. Потенциометр отвечает на вопрос «насколько сильно», энкодер — «на сколько щелчков изменили», джойстик — «куда толкают», клавиатура — «что ввели».
| Устройство | Что сообщает | Выводов | Чем хорошо | Чем неудобно |
|---|---|---|---|---|
| Потенциометр | положение ручки от края до края | 1 аналоговый | проще некуда, помнит положение сам | крутится не бесконечно, шкала всего 1024 деления |
| Энкодер | щелчки и направление | 2 цифровых + 1 на кнопку | крутится бесконечно, есть кнопка внутри вала | не знает своего положения и дребезжит |
| Джойстик | две оси сразу | 2 аналоговых | привычное управление, сам возвращается в центр | центр плавает, нужна мёртвая зона |
| Клавиатура 4×4 | какую из 16 кнопок нажали | 8 цифровых | ввод цифр и кодов | трёх одновременных нажатий не понимает |
| Магнитный энкодер | точный угол, 4096 положений | шина I2C | без контактов, значит без дребезга и износа | дороже и нужен магнит на валу |
Потенциометр: самый честный ввод
Потенциометр — это делитель напряжения: крайние выводы на питание и землю, средний отдаёт что-то между ними. Плата измеряет это напряжение и выдаёт число от 0 до 1023. Никакой логики, никаких библиотек, положение ручки видно даже после выключения.
Одна тонкость всё же есть. Измерение на Uno занимает около 0,1 мс: ядро Arduino ставит делитель 128, то есть частоту преобразователя 125 кГц, а само измерение стоит 13 тактов — получается 104 мкс. Восемь измерений подряд, как в скетче ниже, — это почти миллисекунда. Для ручки это незаметно, но если в цикле опрашиваются шесть датчиков, счёт уже идёт на миллисекунды.
Вторая тонкость видна сразу: даже неподвижная ручка даёт соседние числа. Это шум питания и самого преобразователя, и лечится он двумя строчками — усреднением и порогом.
// Потенциометр без дрожания. Даже неподвижная ручка даёт соседние
// числа: это шум питания и самого преобразователя.
const int PIN_POT = A0;
// Складываем несколько замеров и берём среднее — мелкий шум усредняется.
const int SAMPLES = 8;
// Порог: пока значение изменилось меньше чем на два деления,
// считаем, что ручку не трогали.
const int THRESHOLD = 2;
int shown = -1;
int readSmoothed(int pin) {
long sum = 0;
for (int i = 0; i < SAMPLES; i++) {
sum += analogRead(pin);
}
return sum / SAMPLES;
}
void setup() {
Serial.begin(9600);
}
void loop() {
int value = readSmoothed(PIN_POT);
if (shown < 0 || abs(value - shown) >= THRESHOLD) {
shown = value;
// Переводим 0...1023 в проценты — так понятнее человеку.
Serial.println("Ручка: " + String(map(value, 0, 1023, 0, 100)) + "%");
}
delay(50);
}
Скетч занимает 214 байт ОЗУ и 3 638 байт памяти программ. Порог в два деления убирает мельтешение полностью, а разрешение при этом почти не страдает: 1024 деления превращаются в 512 различимых положений — для регулятора яркости или скорости этого с запасом.
Джойстик: центр не 512
Джойстик — это два потенциометра под прямым углом и пружина, которая возвращает ручку в центр. Значит, и читается он так же: два аналоговых входа. Отдельная кнопка под ручкой есть не у всех модулей: у нашего Trema 3D-джойстика (арт. 888, 160 ₽) её нет — только оси X и Y, о чём карточка честно предупреждает.
Главная неожиданность: в центре получается не 512. Пружина ставит ручку примерно посередине, а «примерно» — это отклонение в десятки делений, у каждого экземпляра своё. Если сравнивать с числом 512, машинка будет медленно ехать сама. Лечение — запомнить центр при включении и завести вокруг него мёртвую зону.
// Джойстик — это два потенциометра и, если повезёт, кнопка.
// Ось X подключена к A0, ось Y к A1.
const int PIN_X = A0;
const int PIN_Y = A1;
// Мёртвая зона вокруг центра. Без неё значение дрожит само по себе,
// и машинка едет, хотя ручку никто не трогал.
const int DEAD_ZONE = 20;
int centerX = 512;
int centerY = 512;
int readCenter(int pin) {
long sum = 0;
for (int i = 0; i < 16; i++) {
sum += analogRead(pin);
delay(2);
}
return sum / 16;
}
int axisValue(int pin, int center) {
int raw = analogRead(pin);
int delta = raw - center;
if (delta > -DEAD_ZONE && delta < DEAD_ZONE) {
return 0;
}
if (delta > 0) {
return map(delta, DEAD_ZONE, 1023 - center, 0, 100);
}
return map(delta, -center, -DEAD_ZONE, -100, 0);
}
void setup() {
Serial.begin(9600);
// Центр у каждого джойстика свой, ровно 512 не бывает почти никогда.
// Поэтому запоминаем его при включении — ручку в этот момент не трогать.
centerX = readCenter(PIN_X);
centerY = readCenter(PIN_Y);
Serial.println("Центр X: " + String(centerX) + ", Y: " + String(centerY));
}
void loop() {
int x = axisValue(PIN_X, centerX);
int y = axisValue(PIN_Y, centerY);
if (x != 0 || y != 0) {
Serial.println("X: " + String(x) + ", Y: " + String(y));
}
delay(100);
}
Скетч занимает 222 байта ОЗУ и 4 216 байт памяти программ. Обратите внимание на две вещи. Первая: центр измеряется усреднением шестнадцати замеров — один замер поймал бы случайный выброс. Вторая: перевод в проценты делается по разным сторонам отдельно, потому что ход ручки вправо и влево от найденного центра неодинаков.
Если возиться с калибровкой не хочется, есть джойстик с шиной I2C (арт. 1235, 351 ₽): он меряет оси своим 12-битным преобразователем и отдаёт уже готовые числа от −100 до +100, а на плате занимает две линии шины вместо двух аналоговых входов. К одной шине их можно повесить больше сотни, потому что адрес меняется программно.
Энкодер: он не знает угол
Самое частое заблуждение: «энкодер показывает угол поворота». Инкрементальный энкодер не показывает ничего похожего. Внутри — два контакта, которые замыкаются на общий провод со сдвигом. При повороте вправо первым срабатывает один, при повороте влево — другой. Всё, что сообщает энкодер: «был щелчок, и он был в такую-то сторону». Сколько щелчков накопилось, считает программа, и после выключения этот счёт пропадает.
У Trema-модуля «Энкодер» (арт. 783, 216 ₽) полный оборот разбит на 20 щелчков по 18°. В покое на выводах A и B стоит единица, а на каждый щелчок приходится четыре перехода — вот почему счётчик, считающий каждое изменение, растёт вчетверо быстрее ожидаемого, и его делят на четыре.
Кнопка внутри вала у этого модуля работает наоборот привычного: в покое на выводе S ноль, при нажатии — единица. У китайских плат KY-040 всё как обычно: подтяжка к питанию и ноль при нажатии. Это первое, обо что спотыкаются, когда переносят чужой пример на наш модуль.
Почему энкодер прыгает: 1000 щелчков через модель
«Энкодер считает через раз» — самая частая жалоба. Причина — дребезг: механический контакт замыкается не мгновенно, а несколько раз подряд за пару миллисекунд. Увидеть это без осциллографа нельзя, поэтому мы поставили опыт иначе: написали программу, которая рисует сигнал энкодера вместе с дребезгом и прогоняет его через три способа расшифровки. Тысяча щелчков в одну сторону, быстрый поворот (50 щелчков в секунду), зерно случайных чисел закреплено — опыт повторяется байт в байт. Правильный ответ в каждой клетке — 1000.
Способы такие:
- наивный — «поймали фронт на A, посмотрели, что на B»: так написано в половине примеров в интернете;
- таблица переходов — смотрим на пару «было-стало» и берём ответ из таблицы на 16 значений;
- строгая цепочка — засчитываем щелчок, только если все четыре состояния пришли в правильном порядке; так устроена наша библиотека iarduino_Encoder_tmr.
| Как читаем | Дребезг | Наивный | Таблица | Строгая цепочка |
|---|---|---|---|---|
| прерывание на каждое изменение | нет | 1000 | 1000 | 1000 |
| прерывание на каждое изменение | до 1 мс | 1004 | 1000 | 416 |
| прерывание на каждое изменение | до 5 мс | 1047 | 1000 | 274 |
| опрос каждые 0,25 мс | до 5 мс | 1021 | 999 | 326 |
| опрос каждую 1 мс | до 1 мс | 1000 | 1000 | 1000 |
| опрос каждую 1 мс | до 5 мс | 1036 | 999 | 583 |
| опрос каждые 5 мс | до 5 мс | 484 | 502 | 193 |
| опрос каждые 20 мс | нет | 0 | 0 | 0 |
Из таблицы следуют три вывода, и два из них противоречат общему совету «вешайте энкодер на прерывания».
Первое. Прерывания — худший режим, а не лучший. Обработчик срабатывает на каждое изменение, включая каждое подрагивание контакта, и получает весь дребезг целиком. Опрос раз в миллисекунду, наоборот, дребезг просто не замечает: контакт успевает успокоиться между замерами. При дребезге до 1 мс опрос раз в миллисекунду дал ровно 1000 всем трём способам — а прерывания уронили строгую цепочку до 416.
Второе. Таблица переходов почти неуязвима. Во всех разумных режимах она даёт 999 или 1000. Ей не нужны ни прерывания, ни конденсаторы, ни задержки — нужен только цикл без длинных пауз.
Третье. Наивный способ ошибается непредсказуемо — то насчитает 1047 вместо 1000, то потеряет двадцать щелчков, а иногда случайно попадёт в точку. Именно поэтому «у меня работает» здесь ничего не доказывает: на другом экземпляре энкодера то же самое начнёт врать.
И отдельно про паузы: строка delay(20) в цикле убивает энкодер вернее любого дребезга. При быстром повороте опрос раз в 5 мс теряет половину щелчков, а раз в 20 мс — вообще всё, потому что за это время проходит целый щелчок и сигнал успевает вернуться в исходное состояние.
Скетч без библиотеки: таблица из шестнадцати чисел
Вся расшифровка помещается в один массив. Индекс складывается из четырёх бит: два старых уровня на A и B и два новых. Значение — куда повернули: плюс единица, минус единица или ноль, если такого перехода в природе быть не может.
// Энкодер без библиотеки: опрос в цикле и таблица переходов.
// A и B — сигнальные выводы энкодера, S — кнопка внутри вала.
const int PIN_A = 2;
const int PIN_B = 3;
const int PIN_BUTTON = 4;
// У Trema-модуля кнопка даёт единицу, когда её нажали, и ноль в покое.
// У китайских плат KY-040 наоборот: там нужны INPUT_PULLUP и сравнение с LOW.
const int BUTTON_PRESSED = HIGH;
// Таблица переходов. Индекс — четыре бита: сначала два старых уровня
// на A и B, потом два новых. Значение — куда повернули. Ноль стоит там,
// где такого перехода быть не может: это дребезг, его молча пропускаем.
const int8_t STEPS[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
uint8_t previous = 0;
long quarters = 0;
long shown = 0;
void setup() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_BUTTON, INPUT);
Serial.begin(9600);
previous = (digitalRead(PIN_A) << 1) | digitalRead(PIN_B);
}
void loop() {
uint8_t now = (digitalRead(PIN_A) << 1) | digitalRead(PIN_B);
if (now != previous) {
quarters += STEPS[(previous << 2) | now];
previous = now;
}
// На один щелчок приходится четыре перехода, поэтому делим на четыре.
long clicks = quarters / 4;
if (clicks != shown) {
shown = clicks;
Serial.println("Щелчков: " + String(clicks));
}
if (digitalRead(PIN_BUTTON) == BUTTON_PRESSED) {
quarters = 0;
shown = 0;
Serial.println("Сброшено");
delay(200);
}
}
Скетч занимает 257 байт ОЗУ и 4 086 байт памяти программ. Ноль в таблице — это и есть встроенная защита от дребезга: невозможный переход просто не считается. Отдельных задержек и «подождать 5 мс» тут нет и не нужно.
Когда всё-таки нужны прерывания
Прерывания нужны в одном случае: цикл занят чем-то долгим — выводом на дисплей, ожиданием датчика, отправкой в сеть. Тогда плата заметит поворот сама. Цена — дребезг придёт в обработчик целиком, и таблица переходов становится обязательной.
Второе ограничение — выводы. На Uno и Nano внешние прерывания есть только на выводах 2 и 3, на Mega — на 2, 3, 18, 19, 20 и 21, на Leonardo, Micro и Pro Micro — на 0, 1, 2, 3 и 7, а на ESP32 и ESP8266 годится почти любой вывод. Проверено по файлам описания плат в ядре Arduino.
// Тот же энкодер, но на прерываниях: плата замечает поворот сама,
// даже когда программа занята другим делом. На Uno и Nano прерывания
// есть только на выводах 2 и 3 — энкодер придётся подключить к ним.
const int PIN_A = 2;
const int PIN_B = 3;
const int8_t STEPS[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
// Переменные, которые меняются в прерывании, помечают словом volatile,
// иначе компилятор решит, что меняться им неоткуда, и выбросит проверки.
volatile long quarters = 0;
volatile uint8_t previous = 0;
void onEncoderChange() {
uint8_t now = (digitalRead(PIN_A) << 1) | digitalRead(PIN_B);
quarters += STEPS[(previous << 2) | now];
previous = now;
}
void setup() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
previous = (digitalRead(PIN_A) << 1) | digitalRead(PIN_B);
attachInterrupt(digitalPinToInterrupt(PIN_A), onEncoderChange, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_B), onEncoderChange, CHANGE);
Serial.begin(9600);
}
void loop() {
// Число длиннее одного байта нельзя читать на ходу: прерывание может
// случиться на середине и подсунуть половину старого значения.
noInterrupts();
long value = quarters;
interrupts();
Serial.println("Щелчков: " + String(value / 4));
// Полсекунды сна — и ни один щелчок при этом не потерян.
delay(500);
}
Скетч занимает 239 байт ОЗУ и 4 134 байта памяти программ. В нём две вещи, которые обычно забывают. Слово volatile у переменных, которые меняет обработчик, — без него компилятор решит, что менять их некому, и выбросит проверку из цикла. И запрет прерываний на время чтения длинного числа: если прерывание случится на середине, программа увидит половину старого значения и половину нового.
Библиотеки для энкодера: чем платит каждая
В каталоге libraries.trema.ru лежат все ходовые библиотеки для энкодеров. Мы собрали с каждой из них одну и ту же программу — «считать щелчки и печатать при изменении» — под Arduino Uno.
| Библиотека | ОЗУ, байт | Флеш, байт | Выводы | Чем платите |
|---|---|---|---|---|
| своя таблица, опрос в цикле | 213 | 2 336 | любые | ничего не занимает, но цикл должен быть быстрым |
| своя таблица, прерывания | 217 | 2 534 | только с прерыванием | не боится медленного цикла, боится дребезга |
| Encoder 1.4.4 | 212 | 2 614 | лучше с прерыванием | сама вешает обработчик — свой на те же выводы уже не поставить |
| RotaryEncoder 1.6.0 | 235 | 2 502 | любые | надо самому звать tick() почаще |
| EncButton 3.7.6 | 200 | 2 426 | любые | самая экономная, умеет ещё и кнопку с удержанием |
| iarduino_Encoder_tmr 1.0.3 | 285 | 2 582 | любые | опрашивает сама 4000 раз в секунду, но занимает таймер 2 |
Разброс небольшой: между самой лёгкой и самой тяжёлой — 85 байт ОЗУ. На Uno с его двумя килобайтами это четыре процента, так что выбирать стоит не по размеру, а по повадкам.
Наша iarduino_Encoder_tmr устроена необычно: она заводит таймер и опрашивает энкодеры сама, 4000 раз в секунду. Отсюда её главное удобство — выводы годятся любые, и цикл может быть каким угодно медленным. Отсюда же и плата: занят таймер 2. Производитель предупреждает, что после этого нельзя выводить ШИМ на выводы 3 и 11, и это правда — команда analogWrite(11, ...) пишет значение прямо в регистр периода этого таймера. Но есть и то, о чём не предупреждают: тот же таймер 2 нужен команде tone(). Скетч с обеими не соберётся, и сообщение будет неочевидное:
multiple definition of `__vector_7'__vector_7 — это и есть прерывание таймера 2. Такое же сообщение выдаёт линковщик, когда одна и та же функция попала в программу дважды: подробный разбор — в статье про файлы .h и .cpp.
#include <iarduino_Encoder_tmr.h>
// Библиотека опрашивает энкодер сама, по таймеру, четыре тысячи раз
// в секунду. Поэтому выводы подойдут любые, а не только 2 и 3.
// Плата за это — занятый таймер 2: перестанут работать tone()
// и плавная яркость на выводах 3 и 11.
iarduino_Encoder_tmr encoder(4, 5);
const int PIN_LED = 9;
int brightness = 128;
void setup() {
encoder.begin();
pinMode(PIN_LED, OUTPUT);
analogWrite(PIN_LED, brightness);
Serial.begin(9600);
}
void loop() {
int8_t step = encoder.read();
if (step != 0) {
brightness += step * 8;
brightness = constrain(brightness, 0, 255);
analogWrite(PIN_LED, brightness);
Serial.println("Яркость: " + String(brightness));
}
}
Скетч занимает 309 байт ОЗУ и 4 374 байта памяти программ. Светодиод здесь на выводе 9 не случайно: 3 и 11 заняты таймером, который забрала библиотека.
Библиотека Encoder вешает обработчик прерывания сама, прямо при создании объекта. Если после этого вызвать attachInterrupt на тот же вывод, скетч соберётся без единого слова, а энкодер начнёт врать: ваш обработчик заменит собой библиотечный. Проверено сборкой — ошибки нет, есть тихая поломка.
Клавиатура: 16 кнопок на 8 выводах
Шестнадцать отдельных кнопок заняли бы шестнадцать выводов — на Uno столько просто нет. Поэтому кнопки соединяют в решётку: четыре строки, четыре столбца, кнопка стоит на пересечении. Плёночная клавиатура 4×4 (арт. 38, 113 ₽) — это ровно такая решётка на самоклеящейся плёнке толщиной в миллиметр, и питание ей не нужно: внутри только шестнадцать перемычек.
Читают её развёрткой: одну строку прижимают к земле, остальные отпускают, а столбцы читают с внутренней подтяжкой. Нажатая кнопка соединяет свою строку со своим столбцом — и этот столбец показывает ноль.
Здесь прячется ошибка, которая портит платы. Кажется естественным сделать все строки выходами и держать непроверяемые в единице. Но если нажать две кнопки в одном столбце, вывод с единицей окажется соединён с выводом, прижатым к нулю, через два замкнутых контакта сопротивлением в единицы ом. Получается короткое замыкание между двумя выводами платы, а предельный ток вывода ATmega328P по паспорту — 40 мА. Правильно — сразу после проверки отпускать строку обратно во вход: вход ни к чему не привязан и замкнуть ничего не может.
// Та же клавиатура 4 на 4, но без библиотеки. Строки по очереди
// прижимаются к земле, столбцы читаются с внутренней подтяжкой.
// Нажатая кнопка соединяет свою строку со своим столбцом — и столбец
// в этот момент показывает ноль.
const byte ROWS = 4;
const byte COLS = 4;
const byte rowPins[ROWS] = {9, 8, 7, 6};
const byte colPins[COLS] = {5, 4, 3, 2};
const char KEYS[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
char previous = 0;
char scanKeypad() {
char found = 0;
for (byte r = 0; r < ROWS; r++) {
// Проверяемую строку делаем выходом и прижимаем к земле.
pinMode(rowPins[r], OUTPUT);
digitalWrite(rowPins[r], LOW);
for (byte c = 0; c < COLS; c++) {
if (digitalRead(colPins[c]) == LOW) {
found = KEYS[r][c];
}
}
// И сразу отпускаем: строка снова вход, то есть ни к чему не привязана.
// Держать её выходом с единицей нельзя — две нажатые кнопки в одном
// столбце замкнут два вывода платы друг на друга.
pinMode(rowPins[r], INPUT);
if (found != 0) {
return found;
}
}
return 0;
}
void setup() {
for (byte r = 0; r < ROWS; r++) {
pinMode(rowPins[r], INPUT);
}
for (byte c = 0; c < COLS; c++) {
pinMode(colPins[c], INPUT_PULLUP);
}
Serial.begin(9600);
}
void loop() {
char key = scanKeypad();
// Сообщаем только о новом нажатии, пока кнопку держат — молчим.
if (key != 0 && key != previous) {
Serial.println("Нажато: " + String(key));
delay(20);
}
previous = key;
}
Скетч занимает 237 байт ОЗУ и 3 746 байт памяти программ — и это вся клавиатура целиком, без единой библиотеки.
Кнопки-призраки: 144 случая из 560
У решётки есть врождённый недостаток. Три нажатые кнопки, стоящие углом, соединяют строку со столбцом в обход — и плата видит четвёртую кнопку, которой никто не касался. Мы перебрали программой все сочетания нажатий на матрице 4×4:
- две кнопки — 120 сочетаний, призраков ноль. Любую пару можно нажимать спокойно;
- три кнопки — 560 сочетаний, призрак появляется в 144 случаях, то есть в каждом четвёртом;
- те же три кнопки, но с диодами — призраков ноль.
Диод в каждой кнопке пропускает ток только в одну сторону, поэтому обходного пути не остаётся. Шестнадцать диодов — цена полного многопальцевого ввода; так сделаны настоящие компьютерные клавиатуры. Для кодового замка и меню это не нужно: там всё равно нажимают по одной кнопке.
Клавиатура с библиотекой
Библиотека нужна, когда хочется не «какая кнопка нажата сейчас», а события: нажали, отпустили, удерживают. В каталоге лежит классическая Keypad 3.1.1 — она умеет видеть до десяти нажатий одновременно и сама подавляет дребезг.
#include <Keypad.h>
// Клавиатура 4 на 4: шестнадцать кнопок на восьми выводах.
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
const char SECRET[] = "1234";
char entered[5] = "";
byte length = 0;
void setup() {
Serial.begin(9600);
Serial.println("Введите код и нажмите решётку");
}
void loop() {
char key = keypad.getKey();
if (key == NO_KEY) {
return;
}
if (key == '#') {
if (strcmp(entered, SECRET) == 0) {
Serial.println("Код верный");
} else {
Serial.println("Код неверный");
}
entered[0] = 0;
length = 0;
} else if (key == '*') {
entered[0] = 0;
length = 0;
Serial.println("Стёрто");
} else if (length < 4) {
entered[length] = key;
length++;
entered[length] = 0;
Serial.println("Введено знаков: " + String(length));
}
}
Скетч занимает 495 байт ОЗУ и 5 040 байт памяти программ. Это уже почти четверть памяти Uno, и вот на что она уходит:
| Как читаем клавиатуру | ОЗУ, байт | Флеш, байт | Что получаем |
|---|---|---|---|
| своя развёртка, 20 строк | 213 | 2 114 | одна кнопка за раз, зато даром |
| Keypad 3.1.1 | 333 | 3 120 | видит до 10 нажатий сразу, есть события |
| iarduino_KB 1.0.2 | 396 | 2 742 | четыре готовых раскладки, но 192 байта уходят на таблицы |
Разница уже заметная: библиотека Keypad стоит 120 байт ОЗУ сверх своей развёртки, наша iarduino_KB — 183 байта. Если проект упёрся в память, двадцать строк своего кода вернут четверть килобайта.
Что мы нашли в своих библиотеках
Раз уж мы всё это собирали и читали, три находки касаются наших собственных библиотек. Пишем как есть.
iarduino_Encoder_tmr 1.0.3 не тянет заявленные восемь энкодеров. И карточка товара, и описание библиотеки обещают «до 8 модулей». В коде счётчик созданных объектов сравнивается с восемью, и на восьмом энкодере проверка перестаёт проходить: метод begin() возвращает false, таймер не запускается — и замолкают все восемь сразу. Семь работают, восемь — нет.
У метода read() той же библиотеки есть ветка без возврата значения. Компилятор сообщает об этом при каждой сборке: warning: control reaches end of non-void function. Пока энкодеров меньше восьми, эта ветка не выполняется, но само предупреждение в консоли пугает — и не зря.
iarduino_KB 1.0.2 держит в ОЗУ 192 байта таблиц. Две таблицы раскладок — это константы, их место в памяти программ. Мы перенесли их туда и пересобрали ту же программу: ОЗУ 396 → 204 байта, флеш при этом даже уменьшился на 40 байт. Девять процентов памяти Uno — заметная плата за то, что никогда не меняется.
Все три находки записаны, и мы их поправим — как поправили ошибки в своей библиотеке для радиомодулей 433 МГц, о которых написано в статье про радио.
Не работает — что проверить
| Что видно | Почему | Что делать |
|---|---|---|
| Энкодер считает то два, то ноль | дребезг контактов попадает в обработчик | считать по таблице переходов и опрашивать раз в миллисекунду, а не по каждому фронту |
| Энкодер крутится, а число не меняется | в цикле стоит delay | убрать задержки или перейти на прерывания либо на нашу библиотеку с таймером |
| Крутишь вправо, а счёт идёт влево | перепутаны выводы A и B | поменять их местами в программе или в разъёме |
| multiple definition of `__vector_7' | iarduino_Encoder_tmr и tone() просят один и тот же таймер 2 | звук вывести на другую библиотеку или взять энкодер без таймера |
| control reaches end of non-void function | предупреждение из самой iarduino_Encoder_tmr, сборке не мешает | ждать исправления библиотеки, на работу скетча не влияет |
| Джойстик едет сам, хотя его не трогают | центр не совпадает с 512, мёртвой зоны нет | запомнить центр при включении и не реагировать на отклонения меньше 20 делений |
| Клавиатура выдаёт не ту кнопку | перепутаны строки и столбцы | поменять местами две четвёрки выводов: раскладка перевернётся правильно |
| При двух нажатых кнопках плата греется или перезагружается | непроверяемые строки оставлены выходами с единицей | отпускать строку во вход сразу после проверки — как в скетче ниже |
Что купить
Начать проще всего с потенциометра (арт. 611, 140 ₽) — три провода и никаких сюрпризов. Если нужен не «поворот ручки», а ползунок, как на микшерном пульте, есть ползунковый вариант (арт. 947, 414 ₽).
Для меню и настроек берут энкодер (арт. 783, 216 ₽) — крутится бесконечно, внутри есть кнопка. Если выводов не осталось, тот же энкодер бывает с шиной I2C (арт. 1190, 441 ₽): он же умеет работать потенциометром и кнопкой, а адрес на шине меняется программно, так что таких модулей можно повесить больше сотни.
Когда дребезг и износ контактов недопустимы вовсе — например, энкодер стоит на валу мотора и крутится часами, — берут магнитный AS5600 (арт. 1847, 370 ₽). Он вообще без контактов: на валу магнит, микросхема измеряет направление поля и отдаёт по шине готовый угол с точностью 0,088° — 4096 положений на оборот. Библиотека AS5600 в каталоге есть. Одно «но»: адрес 0x36 у него не меняется, поэтому на шину такой модуль встанет только один.
Для управления моделью или роботом — джойстик: обычный на два аналоговых входа (арт. 888, 160 ₽) или I2C-версия с готовыми числами −100…+100 (арт. 1235, 351 ₽). Если хочется управлять по-настоящему удобно, в магазине есть даже беспроводной геймпад DualShock от PS2 (арт. 1130, 891 ₽) — работает на 2,4 ГГц до 20 метров, для него нужны библиотеки PsxNewLib и DigitalIO (обе есть в каталоге) и переходник.
Для ввода цифр — плёночная клавиатура 4×4 (арт. 38, 113 ₽), а если шестнадцать кнопок избыточны — вариант 3×4 (арт. 675, 126 ₽), который занимает семь выводов вместо восьми.
Частые вопросы
Почему энкодер считает щелчки через раз?
Из-за дребезга контактов: за один щелчок контакт замыкается несколько раз подряд. Помогает не задержка, а таблица переходов — она просто не засчитывает невозможные переходы. Мы прогнали через модель тысячу щелчков: таблица дала ровно 1000, а популярный способ через фронт на выводе A ошибался то в плюс, то в минус.
Обязательно ли подключать энкодер к выводам 2 и 3?
Нет. Выводы 2 и 3 нужны только для прерываний: на Uno и Nano внешние прерывания есть лишь на них. Если опрашивать энкодер в цикле или взять библиотеку с таймером, подойдут любые выводы. При частом опросе прерывания даже вредят: обработчик ловит весь дребезг целиком.
Почему джойстик в центре показывает не 512?
Потому что пружина ставит ручку лишь примерно посередине, и у каждого экземпляра центр свой. Запомните его при включении, усреднив десяток замеров, и заведите вокруг мёртвую зону в 20 делений — иначе устройство будет реагировать на нетронутый джойстик.
Можно ли нажимать несколько кнопок клавиатуры сразу?
Две — можно всегда. Три нажатые кнопки в четверти случаев рисуют кнопку-призрак: мы перебрали все 560 сочетаний по три кнопки на матрице 4 на 4, призрак появился в 144. Лечится диодом в каждой кнопке.
Сколько выводов занимает клавиатура 4 на 4?
Восемь цифровых: четыре строки и четыре столбца. Питание ей не нужно, внутри только перемычки. Если выводов не хватает, берите клавиатуру 3 на 4 (семь выводов) или модуль с шиной I2C — он займёт две линии.
Какую библиотеку выбрать для энкодера?
Если цикл быстрый — можно вообще без библиотеки, таблица переходов занимает шестнадцать чисел. Из готовых самая экономная EncButton, самая привычная Encoder, а наша iarduino_Encoder_tmr удобна тем, что опрашивает энкодер по таймеру и не требует особых выводов.
Почему после подключения энкодера пропал звук?
Библиотека iarduino_Encoder_tmr занимает таймер 2, а команда tone() на Uno работает на нём же. Скетч с обеими не соберётся: линковщик напишет multiple definition of __vector_7. Заодно перестаёт работать плавная яркость на выводах 3 и 11.
Читайте также
Обновлено: 25 августа 2026
