Содержание
- Назначение и принцип работы
- Подключение к Arduino
- Пример использования
- Часто задаваемые вопросы
Назначение и принцип работы матричный клавиатуры
Кнопка - простейшее устройство передачи информации от человека контроллеру. С ее помощью мы можем осмысленно установить на конкретном пине ноль или единицу, тем самым сообщив программе команду на то или иное действие. Иногда для проекта хватает всего одной кнопки, чаще двух-трех, но бывают случаи, когда кнопок надо много, например для ввода пароля из цифр или букв, или комбинации того и другого. Или мы создаем прибор, требующий большого количества настроек, ввода параметров, набора порядка действий и так далее. Для таких задач мало и дюжины кнопок, а ведь каждой требуется отдельный пин, которых на Ардуино не так уж много. Когда мы понимаем, что для одной отдельной кнопки слишком расточительно выделять аж целый пин, нам на помощь приходят иные способы организации массива этих самых кнопок. В данной статье мы рассмотрим один из них, простой, надежный и потому самый популярный. Называется “матрицирование”, а клавиатуры, собранные под него - “матричными”. Устроено все просто, несколько кнопок соединяются в прямоугольную матрицу, располагаясь в узлах рядов и столбцов из проводников.
Каждая кнопка, при нажатии на нее, замыкает контакты между конкретным столбцом и конкретным рядом, создавая цепь, которую можно программно обнаружить. Например, если мы нажмем верхнюю левую кнопку на схеме, изображенной выше, мы замкнем контакты A и 1. И так, как легко убедиться, любой кнопке соответствует своя индивидуальная пара контактов. При этом контактов всего 8, а кнопок аж целых 16! Уже заметна неплохая экономия в 8 пинов. Нетрудно подсчитать, что добавляя ряды и/или столбцы, мы будем еще больше увеличивать отношение между кнопками и пинами. Так, матрица 5х5 сэкономит нам уже 15 ножек контроллера.
Однако не все так просто. Мы не можем подать одинаковый сигнал на ряды и считывать их на столбцах, в этом случае мы могли бы определять только вертикаль, в которой нажата кнопка. Но если мы подадим сигнал, к примеру, только на ряд A, то все кнопки верхнего ряда будут отслеживаться на контактах 1, 2, 3 и 4, кнопки же в остальных рядах в этот момент никакого влияния на результат не окажут, ни нажатые, ни отжатые. Если подадим на ряд B, то отследим все кнопки только второго сверху ряда и так далее. Таким образом, для того чтобы составить общую картину, нам придется пройтись по всем рядам и всем столбцам, причем не важно в каком порядке, это дело вкуса, главное, чтобы сделать это как можно быстрее, пока с кнопки не убрали палец, и ни одной не пропустить. Этот полезный способ опроса называется “динамическим сканированием” и в этой статье мы научимся им пользоваться.
Подключение к плате Arduino
Матричных клавиатур существует довольно много, разного размера, количества кнопок, формы и технического исполнения. Хороший пример механические, они надежны, долговечны, приятны глазу и руке, и, что немаловажно, ремонтопригодны.
Но наибольшее распространение в DIY-индустрии получили пленочные клавиатуры, они подкупают своей невысокой ценой, неприхотливостью, ярким внешним видом и большим разнообразием.
Благодаря небольшой толщине, пленочные клавиатуры легко и быстро размещаются на плоских поверхностях, достаточно просто приклеить их на двухсторонний скотч. Частично герметичное исполнение защищает от брызг жидкостей, а поверхность легко отмывается от внешних загрязнений. Ресурс относительно небольшой, но компенсируется быстрой заменой без удара по бюджету.
Мы соберем схему на одной из таких клавиатур, но это не помешает использовать на ее месте любую, с другим количеством кнопок, но с тем же матричным принципом работы. Возьмем горячо и заслуженно любимую DIY-мастерами клавиатуру 4х4, очень похожую на те, что использовались на кнопочных телефонах: 10 цифровых кнопок, четыре буквенные, звездочка и решетка. Такого набора с запасом хватает на большинство реальных проектов.
Подключаем ее к Ардуино, как показано на схеме.
Схема требует небольших пояснений. Пины 11, 10, 9 и 8 подключаются к рядам, 7, 6, 5 и 4 - к столбцам. Разумеется, это для примера, использовать можно любые другие и те же самые в другом порядке, главное, правильно указать их в программе. На ряды мы будем поочередно подавать сигналы, на столбцах считывать наличие контактов с этими рядами. В нашем примере при отжатых кнопках на пинах столбцов будет “плюс”, поэтому мы притягиваем их к +5 В резисторами на 1 КОм. “Гуляющим” сигналом на рядах таким образом будет “земля”, она же “ноль”. Внимательный глаз заметит, что выходящие пины соединяются с клавиатурой через диоды, они нужны для того, чтобы уберечь нашу Ардуину от повреждений на случай, если плохо проинструктированный пользователь нажмёт несколько кнопок в столбце одновременно. Если еще раз взглянуть на принципиальную схему клавиатуры, станет очевидно, что в таком случае накоротко замкнутся два и более сигнальных пина, которые обязательно за это время получат разнонаправленное состояние. В лучшем случае Ардуино перезагрузится, в худшем лишится одного из портов. Недорогие диоды уверенно устранят возможность короткого замыкания.
Заливаем и запускаем программу:
#define ALONE // режим работы, если не закаменчено выводит кнопку однократно до ее отпускания, иначе выводит постоянно пока нажато const int P[] = {11, 10, 9, 8}; // пины строк const int M[] = {7, 6, 5, 4}; // пины столбцов const char k4x4 [4][4] = { // символы на клавиатуре {'1', '2', '3', 'A'}, {'4', '5', '6', 'B'}, {'7', '8', '9', 'C'}, {'*', '0', '#', 'D'} }; void setup() { for (int i = 0; i <= 3; i++) { // выставляем пины строк на выход, столбцов на вход pinMode(P[i], OUTPUT); pinMode(M[i], INPUT_PULLUP); digitalWrite(P[i], HIGH); } Serial.begin(9600); Serial.println("begin"); } void loop() { char a = GetKey4x4(); // опрашиваем клавиатуру if (a != 0) { // если кнопка была нажата выводим ее в порт Serial.print(a); } } char GetKey4x4() { static unsigned long timer; static char olda; char a = 0; if ((timer + 50) > millis()) return 0; // пауза для опроса передает 1, сам опрос возврвщает 0 если не нажато или символ, если нажато for (byte p = 0; p <= 3; p++) { // последовательно выставляем по одной строке в LOW digitalWrite(P[p], LOW); for (byte m = 0; m <= 3; m++) { // и считываем столбцы вылавнивая где LOW происходит if (!digitalRead(M[m])) { a = k4x4[p][m]; // считываем соотвествующий символ для комбинации столбца и строки } } digitalWrite(P[p], HIGH); // возвращем строку в HIGH и крутим дальше } timer = millis(); #ifdef ALONE // отсекаем удержание (если нужно) if (a == olda) return 0; olda = a; #endif return a; }
В примере сканирование выполняется раз в 50 мс, это оптимальное значение, если опрашивать реже, можно пропустить быстрое нажатие, если чаще, можно принять за нажатие дребезг, особенно если подключена побитая жизнью механическая клавиатура.
Главная логическая часть программы состоит из двух вложенных циклов. В первом мы поочередно устанавливаем в “ноль” один ряд и в “плюс” остальные, во втором пробегаемся по столбцам в поисках замкнутой кнопки для каждого ряда. Внешний цикл за одно сканирование выполняется 4 раза, вложенный - 16. Таким образом, если одна из кнопок нажата, мы точно установим, в каком она ряду и в каком столбце. Далее останется лишь взять по этим координатам соответствующий символ из заранее подготовленного двумерного массива k4x4, который представляет из себя внешнюю копию клавиатуры.
Интересно взглянуть на то, как реально выглядит сканирование на выходящих пинах, а заодно узнать, с какой скоростью это происходит.
Вот они поочередно падают в ноль, за время каждого падения разок опрашиваются все входящие пины. Весь цикл сканирования занимает 107 мкс, что, согласитесь, довольно быстро. Разумеется, можно ускорить этот процесс раз в 10, используя вместо ардуиновских функций ввода-вывода прямое обращение к портам, но на фоне паузы в 50 мс, это практически не заметно, разве что размер машинного кода несколько сократится, что может быть важно для контроллеров с небольшим объемом флеш-памяти или очень больших программ.
В нашем примере присутствует два варианта компиляции, реализованных с помощью директив препроцессора: #define, #ifdef и #endif. Если первую строку закомментировать, все что находится между двумя другими, не будет скомпилировано, а значит программа будет выполняться чуть иначе. У нас это варианты работы клавиатуры: одиночный (раскомментировано), при котором нажатие и удержание клавиши будет воспринято как одно действие, и повторяющийся (закомментировано), при котором кнопка будет считаться нажатой каждый раз при сканировании все время, пока палец жмет на кнопку. Выбор варианта использования зависит от контекста проекта, набирать код от сейфа - это одно, управлять движением робота - это другое.
Описанного способа динамического сканирования клавиатуры будет более чем достаточно для большинства проектов. Но мы, больше из академических чем из практических соображений, попробуем расширить возможности, уменьшив потребности. Давайте подключим вдвое больше клавиатур, используя вдвое меньше пинов! Да, схема получится несколько сложнее и компонентов потребуется чуть больше, но, когда выбора нет, результат того стоит.
Приглашаем на сцену сдвиговые регистры: выходящий 74HC595 и входящий 74HC165. У первого 8 выходов, у второго 8 входов, есть всё, чтобы держать под контролем две клавиатуры. Да, каждый из регистров требует по три пина от Ардуино, но, зная, что процесс установки выходящих сигналов и опрос входящих происходит в разное время, можем немного схитрить и использовать некоторые линии для обоих процессов. Ими будут канал данных DATA и синхроимпульса CLOCK, а вот каналы LATCH разведем по отдельным пинам, каждому свой, иначе как регистры узнают, к кому в данный момент обращается контроллер?
Программа для этой схемы тоже получится слегка сложнее и длиннее:
#define LATCH_IN_PIN 9 // пин защелки выходящего регистра #define DATA_PIN 10 // пин данных #define LATCH_OUT_PIN 11 // пин защелки входящего регистра #define CLOCK_PIN 12 // пин тактов синхронизации byte scan_OUT[5] = { // кадры сканирования 0b01110111, 0b10111011, 0b11011101, 0b11101110, 0b11111111 }; const char k4x4 [4][4] = { // символы на клавиатуре {'1', '2', '3', 'A'}, {'4', '5', '6', 'B'}, {'7', '8', '9', 'C'}, {'*', '0', '#', 'D'} }; char a1; char a2; // символы притяные с клавиатуры 1 и 2 void setup() { pinMode(LATCH_IN_PIN, OUTPUT); pinMode(LATCH_OUT_PIN, OUTPUT); pinMode(CLOCK_PIN, OUTPUT); pinMode(DATA_PIN, OUTPUT); // будет меняться каждый раз при записи и чтении Serial.begin(9600); } void loop() { GetKey4x4x2(); if (a1 != 0) { // если кнопка на клавиатуре 1 была нажата выводим ее в порт Serial.print("a1 = "); Serial.println(a1); } if (a2 != 0) { // если кнопка на клавиатуре 2 была нажата выводим ее в порт Serial.print("a2 = "); Serial.println(a2); } } void GetKey4x4x2() { static unsigned long timer; //static byte s_kadr = 0; a1 = 0; a2 = 0; if ((timer + 50) > millis()) return; // опрос раз в 50 мс for (byte s_kadr = 0; s_kadr < 4; s_kadr++) { set_kadr(s_kadr); // установка очередного кадра // s_kadr = s_kadr == 3 ? 0 : s_kadr + 1; // сдвиг кадра в цикле byte key = get_key(); // считывание состояние клавиатуры (байт) for (byte i = 0; i < 4; i++) { // клавиатура 1 if (~key & (1 << i)) { // поймали нолик a1 = k4x4[s_kadr][i]; } } for (byte i = 4; i < 7; i++) { // клавиатура 2 if (~key & (1 << i)) { // поймали нолик a2 = k4x4[s_kadr][i]; } } } set_kadr(4); // сброс всех пинов в 1, не обязательно на красиво на анализаторе timer = millis(); } void set_kadr(byte k) { // установка кадра сканирования в регистр pinMode(DATA_PIN, OUTPUT); digitalWrite(LATCH_OUT_PIN, LOW); // защелка открылась for (byte i = 0; i < 8; i++) { digitalWrite(DATA_PIN, bitRead(scan_OUT[k], i)); digitalWrite(CLOCK_PIN, HIGH); digitalWrite(CLOCK_PIN, LOW); } digitalWrite(LATCH_OUT_PIN, HIGH); // защелка закрылась } byte get_key() { pinMode(DATA_PIN, INPUT); byte byteR = 0; digitalWrite(LATCH_IN_PIN, LOW); // защелка щелкнула, отправляем байт digitalWrite(LATCH_IN_PIN, HIGH); for (int i = 7; i >= 0; i--) { // считываем побитно 8 раз if (digitalRead(DATA_PIN)) bitSet(byteR, i);// cчитываем бит, вставляем в байт digitalWrite(CLOCK_PIN, HIGH); // синхроимпульс digitalWrite(CLOCK_PIN, LOW); } return byteR; }
Грубо говоря, программа разделена на две части. Первая: вывод на “595-ый” регистр симметричных установок нулей для каждой клавиатуры, что можно наглядно видеть в массиве scan_OUT. Для четырех состояний это проще сделать, подставляя “кадры” в виде байтов массива, чем вычислениями бинарной логики. Вторая часть: принятие данных о состоянии ножек “165-ого” регистра и анализ результата. Принцип работы тот же, что в первом примере, два вложенных цикла, внешний выставляет сигналы на ряды, вложенный опрашивает столбцы и просматривает их состояние в поисках нажатых кнопок. В нашем случае клавиатуры две и поиск контактов тоже разбит на две половины: левые четыре бита принятого байта для клавиатуры 1 и правые четыре для клавиатуры 2.
Обратите внимание, что канал данных DATA используется в обоих направлениях, при установке от контроллера к регистру, при считывании от регистра к контроллеру, поэтому на каждом этапе он меняет свое состояние из
OUTPUT в INPUT и обратно. Довольно редкий случай.
По традиции подключаем логический анализатор к тем пинам, на которых можно что-то посмотреть, а именно к выходным регистра 74HC595 и посмотрим, что получится:
Одно сканирование за 1,2 мc на обе клавиатуры, в принципе приемлемо, учитывая свободную паузу 50 мс. Конечно, этот процесс тоже можно ускорить на порядок, но, как и в первом примере, если сильно мешается, 3% не так много даже для для великого проекта.
Как и планировалось, ножки 4-7 полностью повторяют то, что происходит на ножках 0-3. Теоретически это означает, что подключать столбцы обеих клавиатур можно параллельно, но практически не даст никакого преимущества в скорости, потому что байт на регистр все равно надо отправлять весь.
Самостоятельно повторив подключение по схеме с регистрами, вы наверняка заметите, что программа работает только по варианту повторного нажатия кнопки при каждом опросе. Если нужен вариант одиночного нажатия, дорабатываем его, опираясь на первый пример. Только не забываем, что клавиатуры две, и нажатых кнопок нужно запоминать, соответственно, тоже две. В остальном все аналогично.
Пример использования (делаем калькулятор)
Создадим проект простого калькулятора с использованием матричной клавиатуры и дисплея:
- Плата Arduino Uno – 1;
- Плата прототипирования – 1;
- Матричная клавиатура 4х4 – 1;
- Дисплей WH1602 I2C – 1;
- Блок питания 5В – 1;
- Провода.
Схема соединения элементов показана на рис.
Сначала изменим названия на некоторых клавишах клавиатуры для действий:
- cложения +
- вычитания –
- умножения *
- деления /
- итог =
- забой ←
При создании скетча будем использовать библиотеки Keypad и LiquidCryctal_I2C. Содержимое скетча показано в листинге 2.
Листинг 2
#include #include #include LiquidCrystal_I2C lcd(0x27,16,2); const char keys[4][4]={{'1','2','3','+'}, {'4','5','6','-'}, {'7','8','9','*'}, {'=','0',' ','/'} }; byte rows[] = {11, 10, 9, 8}; byte cols[] = {7, 6, 5, 4}; Keypad keypad1 = Keypad( makeKeymap(keys), rows, cols, 4, 4); int pos=0; int endkey=0; // 1 - цифра, 2 - действиеб 3 - результат char buf[32]; void setup() { Serial.begin(9600); lcd.init(); lcd.backlight(); lcd.cursor(); lcd.setCursor(3,0); lcd.print("Calculator"); lcd.setCursor(2,1); lcd.print("makerplus.ru"); delay(5000); lcd.clear(); for(int i=0;i<32;i++) buf[i]=0; } void loop() { char key = keypad1.getKey(); if (key){ Serial.print(key,HEX);Serial.println(); addkey(key); } } // при нажатии в буфер и на экран void addkey(char k) { switch(k) { case '+': if(endkey==1) { buf[pos]=k;tolcd(k);endkey=2; } break; case '-': if(endkey==1) { buf[pos]=k;tolcd(k);endkey=2; } break; case '*': if(endkey==1) { buf[pos]=k;tolcd(k);endkey=2; } break; case '/': if(endkey==1) { buf[pos]=k;tolcd(k);endkey=2; } break; case ' ': pos=max(0,pos-1);tolcd(k); pos=pos-1;lcd.setCursor(pos%15,pos/15); if(pos==0) endkey=0; else if(buf[pos-1]>=0x30 && buf[pos-1]<=0x39) endkey=1; else endkey=2; break; case '=': if(endkey==1) { buf[pos]=k;tolcd(k); getsumma(); endkey=3; } break; // 0-9 default : if(endkey==3) { startover();pos=0;} buf[pos]=k;tolcd(k);endkey=1; break; } } // вывод на дисплей void tolcd(char k) { lcd.setCursor(pos%15,pos/15); lcd.print(k); pos=pos+1; } // получить и вывести результат void getsumma() { String number1=""; String number2=""; char d; int i; int summa; // получить первое число for(i=0;i=0x30 && buf[i]<=0x39) number1+=buf[i]; else break; } Serial.print("number1=");Serial.print(number1.toInt()); // действие d=buf[i]; Serial.print(" d=");Serial.println(buf[i]); // получить второе число for(i=i+1;i=0x30 && buf[i]<=0x39) number2+=buf[i]; else break; } switch(d) { case '+': summa=number1.toInt()+number2.toInt(); break; case '-': summa=number1.toInt()-number2.toInt(); break; case '*': summa=number1.toInt()*number2.toInt(); break; case '/': summa=number1.toInt()/number2.toInt(); break; default: break; } lcd.setCursor(pos%15,pos/15); lcd.print(summa); } // очистка при начале нового набора void startover() { for(int i=0;i<=pos;i++) { buf[i]=0; } lcd.clear(); }
Загружаем скетч на плату Arduino и проверяем работу калькулятора.
Выводы
Матричные клавиатуры - крайне полезное изобретение, позволяющее очень надёжно и довольно легко работать с большим количеством кнопок, задействуя при этом минимум драгоценных пинов. Благодаря большому разнообразию видов, форм, цветов и размеров, найти подходящую для своего проекта клавиатуру не составит труда. К применению однозначно рекомендуется. FAQ (Часто задаваемые вопросы)
1. Если нет подходящей готовой клавиатуры экзотической формы, можно ли сделать собственную матрицу? Конечно, достаточно взять нужное количество кнопок и соединить их по известной схеме, при этом они не обязаны физически располагаться прямоугольником, рядами и колонками, расставляйте их хоть в прямую линию, хоть в кольцо, главное - логически правильный порядок соединения.
Нет, у сенсорных клавиатур количество выходов равно количеству кнопок. При необходимости экономии портов, придется придумывать что-то другое, например использовать упомянутые здесь сдвиговые регистры.
3. Какая матричная клавиатура лучше, механическая или пленочная?Зависит от задачи, дизайна изделия и вкуса. Для большинства случаев удобнее пленочная, т.к. не боится брызг, пыли и мелких предметов, ее контакты не окисляются, но она быстрее изнашивается при частом интенсивном использовании.
4. Как вызывать сканирование клавиатуры по прерыванию?По рассмотренной в статье схеме, только по прерыванию таймера. Задаём период 50 мс, полученный символ отправляем в глобальную переменную, возможно, поднимаем флаг, если надо. Но в таком случае лучше действительно переписать функцию на обращение к портам, потому что все, что вызывается прерываниями, должно выполняться как можно быстрее.
5. Как составить из принятых символов последовательность, например, для проверки кода к сейфу?Организовать буфер соответствующего размера, возможно кольцевой, и заполнять его по мере поступления данных, сравнивая их с правильным паролем.