Часто бывает, что в проекте, над которым мы работаем, необходимо иметь в распоряжении довольно много кнопок. Уже с обычной цифровой клавиатурой, подключив каждую кнопку к отдельному контакту, мы будем использовать 12 контактов ввода/вывода (цифры 0-9, #, *).
В сегодняшней статье мы узнаем, как подключить, подобную клавиатуру, используя как можно меньше контактов.
Подключение матричной клавиатуры 4×4 к Ардуино
Для экономии выводов микроконтроллера нам нужно правильно соединить кнопки между собой. Идея состоит в том, чтобы расположить их в виде матрицы, создавая строки и столбцы, как на схеме ниже:
Благодаря такому расположению мы можем подключить 16 кнопок, используя всего 8 контактов GPIO.
Принцип действия
Чтобы прочитать состояние каждой кнопки, все контакты, к которым подключены столбцы, должны быть установлены в состояние INPUT_PULLUP.
Затем установить контакты, к которым подключены строки как выходы с состоянием HIGH. Теперь все, что осталось, — это использовать несколько циклов, чтобы установить состояние LOW по одной строке за раз, а затем прочитать состояния входов столбца. Когда мы просканируем каждую строку, мы узнаем, какая кнопка нажата.
Реализация
Ниже приводим код который обрабатывает нажатие кнопки клавиатуры:
const int ROWS[] = {6,7,8,9}; const int COLS[] = {5,4,3,2}; const int NUM_ROWS = sizeof(ROWS)/sizeof(int); const int NUM_COLS = sizeof(COLS)/sizeof(int); const char KEYS[NUM_ROWS][NUM_COLS] = { {'A', 'B', 'C', 'D'}, {'E', 'F', 'G', 'H'}, {'I', 'J', 'K', 'L'}, {'M', 'N', 'O', 'P'} }; void setup() { Serial.begin(9600); for(int x = 0; x < NUM_ROWS; x++) { pinMode(ROWS[x], OUTPUT); digitalWrite(ROWS[x], LOW); } for(int x = 0; x < NUM_COLS; x++) { pinMode(COLS[x], INPUT_PULLUP); } Serial.println("Keypad initialized"); printKeypadLayout(); } void loop() { char key = readKey(); if(key) Serial.println(key); } void printKeypadLayout() { for(int x = 0; x < NUM_ROWS; x++) { for(int y = 0; y < NUM_COLS; y++) { Serial.print(KEYS[x][y]); Serial.print("\t"); } Serial.print("\n"); } Serial.print("\n"); } char readKey() { for(int x = 0; x < NUM_ROWS; x++) { for(int row = 0; row < NUM_ROWS; row++) digitalWrite(ROWS[row], x != row); for(int y = 0; y < NUM_COLS; y++) { if(!digitalRead(COLS[y])) return KEYS[x][y]; } } return 0; }
Рассмотрим код подробнее.
Для начала объявим несколько констант, чтобы код был легко читаемым:
const int ROWS[] = {6,7,8,9}; const int COLS[] = {5,4,3,2}; const int NUM_ROWS = sizeof(ROWS)/sizeof(int); const int NUM_COLS = sizeof(COLS)/sizeof(int); const char KEYS[NUM_ROWS][NUM_COLS] = { {'A', 'B', 'C', 'D'}, {'E', 'F', 'G', 'H'}, {'I', 'J', 'K', 'L'}, {'M', 'N', 'O', 'P'} };
ROWS и COLS — это выводы Ардуино, к которым подключены контакты клавиатуры (строки и столбцы соответственно).
NUM_ROWS и NUM_COLS вычисляются автоматически на основе ROWS и COLS и обозначают количество строк и столбцов.
Последняя константа — это KEYS, двумерный массив, содержащий односимвольное представление нажатой клавиши.
Пришло время инициализировать клавиатуру:
void setup() { Serial.begin(9600); for(int x = 0; x < NUM_ROWS; x++) { pinMode(ROWS[x], OUTPUT); digitalWrite(ROWS[x], LOW); } for(int x = 0; x < NUM_COLS; x++) { pinMode(COLS[x], INPUT_PULLUP); } Serial.println("Keypad initialized"); printKeypadLayout(); }
Первое, что мы делаем, — запускаем наш последовательный порт, чтобы увидеть позже результаты.
Затем мы устанавливаем все строки как выходы с состоянием LOW (хотя на этом этапе не имеет значения, высокое или низкое состояние), а столбцы как входы, подтянутые к источнику питания (INPUT_PULLUP). Наконец, мы просто печатаем в последовательный порт, что клавиатура была успешно инициализирована, и отображаем раскладку клавиатуры.
В основном цикле программы мы просто вызываем функцию readKey(), которая возвращает символ, соответствующий нажатой клавише:
void loop() { char key = readKey(); if(key) Serial.println(key); }
Итак, давайте посмотрим, что стоит за этой функцией:
char readKey() { for(int x = 0; x < NUM_ROWS; x++) { for(int row = 0; row < NUM_ROWS; row++) digitalWrite(ROWS[row], x != row); for(int y = 0; y < NUM_COLS; y++) { if(!digitalRead(COLS[y])) return KEYS[x][y]; } } return 0; }
Как видите ничего сложного. Мы перебираем все строки и на каждой итерации строки перебираем столбцы, считывая их состояния. Перед проверкой столбцов нам нужно убедиться, что только одна строка находится в состоянии LOW, а остальные — в состоянии HIGH. Цикл делает за нас всю работу.
for(int row = 0; row < NUM_ROWS; row++) digitalWrite(ROWS[row], x != row);
На каждой итерации x является неизменным, а переменная row изменяется с 0 на NUM_ROWS — 1. По этой причине после завершения всех итераций этого цикла логическое условие x! = Row будет ложным только один раз (когда x равно row), поэтому только одна строка перейдет в состояние LOW.
Теперь мы можем приступить к проверке столбцов. Как видите, сразу после обнаружения нажатой кнопки мы выходим из функции, возвращая первую нажатую кнопку.
Вы можете задать вопрос, зачем писать свой код, если имеется готовая библиотека для поддержки таких клавиатур. На то есть две простые причины:
- Стоит знать, как реализовывается такой функционал.
- Заглянув внутрь готовой библиотеки, мы можем увидеть, что есть много кода, который мы, вероятно, никогда не будем использовать — наш код будет занимать меньше места, что очень ценно при программировании микроконтроллеров.
Простой вопрос — что будет при одновременном нажатии двух и более кнопок? В приведенном примере есть защита от этого?
В готовой библиотеке скорее всего есть
Все хорошо в этой программе. А что с антидребезгом кнопок? При каждом нажатии кнопки может проскакивать до 50 импульсов, а то и больше, а значит и буква будет печататься столько же раз.