матричная клавиатура ардуино

Матричная клавиатура на arduino. Опрос кнопок

от автора

в

Подумал, что интересно будет не только делать уроки, а сразу делать реальное изделие, которое можно применить в жизни. По основной работе я занимаюсь системами безопасности, и очень часто работаю с системами контроля доступа. Я работал с Болид, z5r, z5r web, и т.д. Какие-то системы невероятно сложные в установке и настройке (Болид привет), каким-то не хватает стабильности и функционала.

Решил попробовать сделать открытую систему прохода, с возможностью настройки и контроля по телефону. Начнём с ардуино, потом возможно перейдём на более мощный кристалл, а затем перенесём всё на свою плату, сделаем дизайн корпусов, напечатаем на 3д принтере. В первых уроках реализуем следующий функционал:

  • Управление замком – нужна поддержка электромагнитного замка и электромеханического
  • Управление временем открытия замка – в случае применения электромагнитного замка, пользователь не успеет выйти за 1 секунду, а где-то нужно и 5 секунд
  • Доступ с внешней стороны по 4-значному коду, изменение кода доступа также производится с клавиатуры
  • С внутренней стороны – выход по кнопке
  • Buzzer – небольшой динамик, который сигнализирует, и понятен на слух, разрешён ли проход или нет
  • Джампер аппаратного сброса

Нарисуем схему в Proteus, на которой будем проверять код, перед тем как собирать в реальном железе.

В первой статье попробуем считывать значения клавиатуры. Конечно, можно подключить каждую кнопку отдельно, но для этого нам понадобится 12 контактов, а можно очень просто сократить их до 7, путём динамического считывания клавиш по горизонтали и вертикали. Такие клавиатуры называются матричными.

Принцип работы похож на динамическую индикацию светодиодных сегментных индикаторов, только делаем всё наоборот – не выводим информацию, а поочерёдно читаем. Рассмотрим схему матричной клавиатуры

У кнопки мгновенного действия минимум два контакта, которые замыкаются при нажатии. Следовательно ряды кнопок объединяем в массив одним контактом, и подписываем каждый ряд, а оставшийся контакт кнопок объединяем в ряды. Например, при нажатии кнопки 3, мы замкнём контакт ряда “A” и контакт столбца “3”

Выберем какие пины мы будем использовать у контроллера и дадим им для удобства осмысленные имена, будем использовать имя row – для строки, и column – для столбца.

#define row1  12                          // Строки клавиатуры
#define row2  2
#define row3  3
#define row4  4
#define column1 5                        // Колонки клавиатуры
#define column2 6
#define column3 7

Директива #define просто заменяет при трансляции одну последовательность символов на другую, т.е. строка #define row1 12 означает, что везде где в тексте программы встретится имя row1 компилятор заменит его на символы 12.

Для более комфортной работы с кодом программы, нужно создать массивы нашей клавиатуры, так будет проще разобраться, и сам код будет короче. Создадим отдельный массив для строк, и для столбцов, для более простого обращения к ним.

byte keypadOut[4] {row1, row2, row3, row4};    // пины строк - на них будем подавать напряжение
byte keypadIn[3] {column1, column2, column3};  // пины колонок - отсюда будем считывать напряжение

Сейчас мы создали одномерный массив. Также нам понадобится двумерный массив, который будет определять значения, полученные путём считывания строк и столбцов. Зададим значения двумерного массива

const char keyboardValue[4][3]          //Создаём двумерный массив
{ {'1', '2', '3'},                      //структура клавиатуры
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};

Двумерный массив даёт понятное представление данных в нём, визуально это полная копия клавиатуры. Перейдём непосредственно, к самому коду и алгоритму считывания данных, но сначала разберёмся, как мы будем определять нажатую клавишу. На ряды мы будем поочерёдно подавать низкий логический уровень, а во вложенном цикле на столбцах будем пытаться по очереди считать этот низкий уровень. Если в момент опроса увидим низкий уровень, то в алгоритме цикла будет легко понять, какой ряд и столбец мы сейчас опрашиваем. Поэтому не забудем задать режим работы i/o пинов микроконтроллера. Ряды – на выход, столбцы – на вход.

// инициализируем порты на выход (подают нули на столбцы)  
  pinMode(row1, OUTPUT);                  
  pinMode(row2, OUTPUT);
  pinMode(row3, OUTPUT);
  pinMode(row4, OUTPUT);
// входные порты колонок клавиатуры настроены на вход
  pinMode (column1, INPUT);               
  pinMode (column2, INPUT);
  pinMode (column3, INPUT);

Я не задаю программную Pull-up подтяжку для входных строк, потому-что в данном случае, использую резисторы на самой плате микроконтроллера. Выходы же можно не назначать в логическую 1, потому-что при старте контроллера, в первом цикле for, они и так примут нужные значения. Алгоритм считывания состоит из двух циклов for, один у нас перебирает строки по очереди, а второй цикл for перебирает ряды внутри каждой строки. Рассмотрим цикл for Для перебора строк.

for (int r = 1; r <= 4; r++) // цикл, передающий 0 по всем столбцам {
  digitalWrite(keypadOut[r - 1], LOW); // Подаём низкий уровень
    {
    здесь будем перебирать столбцы
    }
  digitalWrite(keypadOut[r - 1], HIGH); // подаём обратно высокий уровень
}

В первом цикле for мы указываем переменную r, которую будем использовать в самом теле цикла. Помним, что выводы контроллера, которые подсоединены к рядам, настроены на выход, причём в логической “1”. Чтобы сделать опрос конкретного ряда, нам нужно понизить логический уровень до “0”, что мы и сделаем командой

digitalWrite(keypadOut[r - 1], LOW); // Подаём низкий уровень

В первом цикле, когда r=1, у нас будет keypadOut[0], что равно row1. Так получилось, потому-что 1 – 1 = 0 . Тут нужно не забывать, что ардуино это с++, а значит нумерация массива начинается с 0. Понятнее будет на картинке.

Если мы не будем использовать формулу [r – 1], то дойдя до 4 цикла, значение массива станет keypadOut = 4, что приведёт к ошибке, потому-что это будет уже 5 элемент массива, а размерность его мы задали [4].

Затем нам нужно будет сделать 3 опроса столбцов в следующем цикле, разберём этот кусок кода чуть дальше. После опросов столбцов, чтобы перейти к следующему ряду, нам нужно будет обратно вернуть логическую “1” на ряде, который мы сканировали, командой

digitalWrite(keypadOut[r - 1], HIGH);

Перейдём непосредственно к циклу опроса столбца. Это аналогичный цикл for, только уже для 3 переменных, из-за структуры клавиатуры.

    for (int c = 1; c <= 3; c++) // 
    {
      if (digitalRead(keypadIn[c - 1]) == LOW) // 
      {     
        Serial.print(keyboardValue[r-1][c-1]); 
      }
    }

Здесь мы наоборот, ничего не подаём на выход, а считываем значения из массива keypadIn[3], который может принимать значения column1, column2 или column3. Если вдруг в какой-то момент мы замечаем низкий уровень на входе столбца, то в этот момент мы можем определить нажатую кнопку, так как мы знаем в какой итерации циклов FOR мы находимся, и знаем переменные r и c. Определить это можно следующей конструкцией

keyboardValue[r-1][c-1]

Помним про свойство массивов, что нумерация начинается с 0, и в итоге считываем нажатую клавишу из этого массива\

const char keyboardValue[4][3]          //Создаём двумерный массив
{ {'1', '2', '3'},                      //структура клавиатуры
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};

Например, если сработка произошла при r=3 и c=2, то keyboardValue будет со значением [2][1], т.е. пример значение “8”. Для наглядности работы цикла я сделал небольшое видео, которое точно даст понимание, как действует алгоритм.

Попробуем запустить всё это в proteus, и нажмём кнопку 3. Для начала посмотрим на графики осциллографа, и увидим, что наш код выполняется правильно. row1 падает до низкого уровня, затем row2, затем row3. На 4 дорожку я подключил столбец 1, и тут мы замечаем первую проблему – дребезг контактов и длительность нажатия кнопки, приводит к нескольким срабатываниям

Один раз нажав на “1” мы получаем 7-8 сообщений в терминале, значит нужно эту проблему решить. В реальной жизни, мы ещё столкнёмся с диким дребезгом контактов, что можно поймать и 100 сработок в момент нажатия кнопки. В целом, сейчас мы опрашиваем кнопки в цикле Loop, и другого кода у нас нет, следовательно опрос идёт слишком часто. Попробуем добавить самую простую задержку delay, если мы заметили срабатывание кнопки, а затем повторно проверить состояние кнопки

      if (digitalRead(keypadIn[c - 1]) == LOW) { //
        delay(50); // вводим дополнительную задержку, перед повторной проверкой
        if (digitalRead(keypadIn[c - 1]) == LOW) {
          {
            Serial.print(keyboardValue[r-1][c-1]); 
          }
        }
      }

Результат уже намного лучше – но всё равно присутствуют повторные нажатия

В какой-то момент, удаётся поймать однократное нажатие, и легко понять по виртуальному осциллографу почему. Длительности импульса от нажатия хватило, чтобы код сработал, он был более 60ms, оставшийся всплеск составил чуть менее 40ms, и программа не сделала условие if.

В разных устройствах работа с кнопками нуждается в разных алгоритмах, где-то нужно фиксировать двойные нажатия, где-то удерживание, а где-то и вовсе следить за комбинированным нажатием кнопок. В кодовой клавиатуре повторные срабатывания нам вообще ни к чему, и мы можем после игнорирования дребезга контактов, просто следить отпустили клавишу или нет. Представьте, человек вводит пароль, кто-то быстрее, кто-то медленнее, каждое двойное срабатывание заставит человека ошибаться в коде, поэтому всё, что нам нужно мониторить – это состояние клавиши, пока она ещё нажата, не будем переходить к действию, например так.

while (digitalRead(keypadIn[c - 1]) == LOW) {} //пустой цикл, пока клавиша будет нажата

Теперь пока кнопка нажата, в нашем цикле ничего не происходит, как можно заметить, и все клавиши чётко опознаются при нажатии. Пока наша программа не насыщена другими обработчиками событий, это не очень плохо, но вообще это не самый лучший путь решения проблемы. Далее, мы задействуем прерывания, и реализуем нормальную задержку по мере появления новых задач.

В заключение хотелось бы поговорить об аппаратных проблемах, которые нас могут ждать, если мы используем готовую матрицу клавиатуры. Что, например произойдёт, если одновременно нажать пару кнопок? Рассмотрим плохой вариант, если кнопки попадут на один столбец

В момент опроса, у нас низкий уровень поочередно меняется с первого ряда на 4-ый, но остальные при этом имеют выход 5В. Произойдёт КЗ, что очень плохо для МК. Чтобы этого не произошло нужно добавить диоды, и даже при нажатии двух таких кнопок, отрицательный потенциал не сможет пройти через анод.

Верхние же резисторы используются в качестве pull-up подтяжки, чтобы создать логическую 1 на входе ног, отвечающих за столбцы, но можно конечно задействовать и программную подтяжку.

Ссылки на скетч и проект протеуса здесь