Вы здесь

Матричная клавиатура

Матричная клавиатураНередко в устройстве, собранном с применением микроконтроллеров, предусмотрен ввод данных с использованием кнопок, переключателей или других контактных групп. Реализовать такое схемное решение очень просто учитывая то, что кроме собственно кнопки и подтягивающего резистора (и то, в некоторых случаях он не нужен) больше ничего не надо. Но простое подключение контактных групп к линиям ввода/вывода микроконтроллера может породить проблему нехватки этих самых линий, если таких контактных групп много. Решение проблемы довольно простое - использование клавиатурной матрицы.



Схема клавиатуры

Схема клавиатурной матрицы представлена на рисунке 1. Кнопки включены таким образом, что при нажатии кнопка замыкает строку на столбец. Из схемы видно, что часть линий контроллера используется в качестве сканирующих (столбцы), а часть в качестве считывающих (строки). Количество кнопок, подключенных таким образом, определяется как количество сканирующих линий умноженное на количество считывающих. Отсюда следует, что использование матричной клавиатуры для случая, когда кнопок меньше или равно четырем, не имеет смысла, так как понадобятся те же четыре линии, а схема и прошивка усложнятся.

Схема матричной клавиатуры

Рисунок 1. Схема матричной клавиатуры

Из внешних элементов понадобятся диоды (столько же, сколько сканирующих линий) и резисторы (столько же, сколько считывающих линий). Назначение этих элементов будет рассмотрено ниже.

Как это работает

Работает это довольно просто. Линии сканирующего порта (столбцы) по умолчанию находятся состоянии, когда на всех линиях, кроме одной, установлен высокий логический уровень. Линия, на которой установлен низкий логический уровень является опрашиваемой в текущий момент, то есть определяет опрашиваемый столбец. Если какая либо кнопка этого столбца будет нажата, на соответствующей линии считывающего порта (строке) так же будет низкий логический уровень. Как говорится, замкнутая кнопка подтянет строку к потенциалу столбца, то есть к земле. Зная номер опрашиваемого столбца и номера линий считывающего порта, на которых установлен логический ноль, можно однозначно определить какие кнопки этого столбца нажаты. Хотя на счет однозначности я переборщил, но об этом позже.

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

Для случая, когда одновременно нажато несколько кнопок одного столбца все понятно. Будет установлено в логический ноль несколько битов считывающего порта одновременно. Но что произойдет, если будут замкнуты контакты нескольких кнопок из разных столбцов одной строки? Ведь в разных столбцах могут оказаться (и ведь окажутся) разные напряжения (помните? на всех столбцах, кроме одного, логическая единица). Одновременное нажатие двух кнопок в одной строке привело бы к короткому замыканию и выжженным портам, если бы не диоды VD1-VD5. Именно они защищают порты от короткого замыкания.

Резисторы на схеме являются подтягивающими. Если используемый контроллер имеет в своем составе подтягивающие резисторы, от них можно отказаться. Микроконтроллеры серии AVR имеют в своем составе подтягивающие резисторы, а вот микроконтроллеры с архитектурой 8051 и 8052 таких резисторов не имеют. Внимательно изучайте документацию или просто оставьте эти резисторы для универсальности. Модифицированная схема без подтягивающих резисторов представлена на рисунке 2.

Схема матричной клавиатуры без подтягивающих резисторов

Рисунок 2. Схема матричной клавиатуры без подтягивающих резисторов

Описание исходного кода

Исходный код функций для работы с матричной клавиатурой представлен ниже в листингах. Код разбит на три файла:

  • MatrixKeyboard.h - этот файл содержит определения для конфигурирования используемых клавиатурой ресурсов и предварительные объявления функций.
  • MatrixKeyboard.c - в этом файле содержится основной код, работающий с клавиатурой. Если точнее, то файл содержит реализацию функций InitializeKeyboard и ReadFromKeyboard, а так же пачку макросов для формирования необходимых констант. Я вообще выступаю за минимизацию количества параметров, которые требуется подстроить под конкретный проект. Это снижает количество ошибок, допущенных по невнимательности или в результате неполного понимания того, зачем эти параметры нужны. Так вот, эта груда макросов позволила сократить количество настраиваемых параметров до шести, и те очевидны как божий день. Пусть вас не смущает большой объем кода, он работает только в процессе компиляции и не попадет в конечную прошивку. Если есть желание разобраться с этими макросами, вперед, они неплохо задокументированны.
  • main.c - здесь представлен пример работы с функциями InitializeKeyboard и ReadFromKeyboard. Пример чисто демонстрационный, так как опрос клавиатуры ведется в цикле основной подпрограммы. Правильнее сделать опрос клавиатуры по таймеру с периодом, позволяющим избежать эффекта дребезга контактов (об этом эффекте ниже). Результат выводится в порты PORTC и PORTD, зажигая соответствующие светодиоды.

Листинги всех трех файлов представлены в конце этой статьи.

Представленный код накладывает ряд ограничений, которые следует обязательно учитывать:

  1. Максимальное количество сканирующих линий - 8;
  2. Максимальное количество считывающих линий - 8;
  3. Все сканирующие линии должны принадлежать одному порту;
  4. Все считывающие линии должны принадлежать одному порту;
  5. Считывающие линии должны подключаться начиная с нулевой линии порта и идти подряд.

Ограничения 1 и 2 определяют максимальное количество кнопок клавиатуры. В нашем случае это 8*8, то есть 64 кнопки. В большинстве случаев хватит за глаза.

Ограничения 3 и 4 подразумевают, что сканирующие линии должны принадлежать одному порту. То же самое касается и считывающих линий. Однако считывающие и сканирующие линии могут принадлежать как одному порту, так и разным. Первый пример: нам нужна клавиатура из 16 кнопок. Наиболее рационально отвести под нее один порт. Четыре линии отводятся под считывающие линии, а четыре под сканирующие. Второй пример: нам понадобилась клавиатура из 25 кнопок. То есть требуется 5 считывающих и 5 сканирующих линий. Здесь нам понадобится два порта один порт отводим под считывающие, другой под сканирующие линии. Незадействованные линии не пропадут, их можно еще для чего нибудь использовать. Но нельзя сделать так: первые пять линий первого порта задействовать под считывающие линий, оставшиеся три линии первого порта и какие нибудь две линии второго порта отвести под сканирующие линии. Без переделки программы это не получится.

Пятое ограничение следует пояснить подробно. Например, если вы хотите использовать четыре считывающие линии, а считывающим портом выбрали порт PORTA, тогда под эти цели следует задействовать линии порта A с 0 по 3. Ни с 2 по 5, ни 0, 1, 3, 5 и ни какие другие. Именно с нулевой и подряд, до N-1, где N - количество считывающих линий. Порядок подключения считывающих линий не важен, но следует учитывать, что если порядок линии контроллера будет совпадать с порядком линий клавиатуры (нулевую строку клавиатуры к нулевой линии порта микроконтроллера, первую к первой и так далее), на выходе функции ReadFromKeyBoard получится значение, номера поднятых бит которого будут совпадать с номерами кнопок (теми номерами, что указаны на схеме). То есть если поднят нулевой, первый и пятый биты, значит нажата нулевая, первая и пятая кнопки. Очень удобно для дальнейшей обработки.

В общем пятое ограничение довольно просто снять (а если точнее, то все пять), подправив код прошивки. Пришлось бы побитово собирать результат сканирования. Это сделать не сложно, но тогда пострадала бы наглядность кода, да и размер прошивки увеличился бы. Пусть будет домашним заданием, если вдруг возникнет такая необходимость.

Теперь о конфигурировании кода. Здесь для каждого конкретного случая придется подправить некоторые определения. Все определения находятся в файле MatrixKeyboard.h. Итак:

  • KB_READ_PORT - имя порта, к которому подключены считывающие линии. Внимание! Следует указать только имя, например A, B, C и тому подобное. У меня это A. По этому имени далее будут сформированы имена необходимых регистров.
  • KB_READ_LINES_COUNT - количество считывающих линий. У меня равно четырем.
  • KB_READ_LINES_MASK - маска считывающих линий. Определяет, к каким линиям порта подключены считывающие линии. У меня маска стоит 0b00001111. Для проверки: единиц должно быть столько, сколько указано считывающих линий в определении KB_READ_LINES_COUNT. Помня о пятом ограничении, единицы должны идти подряд начиная с самого младшего разряда.
  • KB_SCAN_PORT - имя порта, к которому подключены сканирующие линии. У меня это все тот же A.
  • KB_SCAN_LINES_COUNT - количество сканирующих линий. У меня равно четырем.
  • KB_SCAN_LINES_MASK - маска сканирующих линий. Биты, установленные в 1, указывают на то, что соответствующая линия порта является сканирующей. Например, если установлен третий бит, значит третья линия порта KB_SCAN_PORT является сканирующей. И так далее. У меня сканирующими будут линии с 4 до 7 включительно. Если KB_READ_PORT и KB_SCAN_PORT одинаковы, биты KB_READ_LINES_MASK и KB_SCAN_LINES_MASKS не должны пересекаться.

Все. На этом настройка исходного кода закончена.

Осталось дело за малым. Вызвать где нибудь в начале функцию InitializeKeyboard, а потом периодически вызывать функцию ReadFromKeyboard. Периодичность вызова функции ReadFromKeyboard зависит от того, кто или что замыкает контакты. Если это делает человек, то как правило достаточно опрашивать клавиатуру не менее 10 раз в секунду. Если же контакты подключены к чему то иному (например, механический датчик), тогда период опроса выбирается индивидуально.

Эффект фантомного нажатия кнопки

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

Рассмотрим рисунок 3. На нем синими квадратами обозначены линии, на которых присутствует низкий логический уровень. Соответственно, красными квадратами обозначены линии с высоким логическим уровнем. В замкнутом состоянии находятся кнопки PB1, PB5 и PB6. В текущий момент сканируется столбец с номером 0, так как на выводе 36 микроконтроллера присутствует низкий логический уровень, а на всех остальных сканирующих линиях высокий. В нулевом столбце в замкнутом состоянии находится только одна кнопка - PB1, и она подтягивает всю строку с номером 1 (нумерация начинается с нуля) к земле. Прочитав низкий логический уровень на первой считывающей линии микроконтроллер определит, что кнопка PB1 нажата, и это нормально. Однако в строке с номером 1 есть еще одна нажатая кнопка - PB5. Она расположена в первом столбце, а это значит, что и весь первый столбец окажется подтянутым к нулю. Сам по себе этот факт был бы не страшен, если бы в первом столбце не оказалась нажатой еще одна кнопка - PB6. Вот тут то и начинаются неприятности. Эта кнопка подтянет к земле строку с номером 2. Микроконтроллер, прочитав со второй строки низкий логический уровень распознает кнопку PB2 нажатой (напомню, идет сканирование нулевого столбца), а ее никто не нажимал. Вот так вот.

Эффект фантомного нажатия кнопки

Рисунок 3. Эффект фантомного нажатия кнопки

Итак, проблема проявляется при следующем условии: нажатыми должны быть минимум три кнопки, первая из которых находится в сканируемом столбце, вторая в том же ряду, что и первая, а третья в том же столбце, что и вторая.

Основным решением проблемы мне представляется ограничение на количество одновременно нажатых кнопок. Безопасным количеством, как Вы уже наверное догадались, будет две кнопки. Если нажато более двух кнопок, всячески предупреждаем пользователя об этом (моргаем, пищим, пишем на LCD, возможны еще варианты).

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

Дребезг контактов

Этот неприятный эффект свойственен в различной степени всем механическим контактам. Представляет из себя следующее: при нажатии на кнопку происходит многократное замыкание/размыкание контактов, вызванное тем, что контакты пружинят, обгорают и тому подобное. Графически этот эффект представлен на рисунке 4. Длительность периода дребезга зависит от многих факторов и составляет от 10 до 100 мс.

Эффект дребезга контактов

Рисунок 4. Эффект дребезга контактов

Борются с этим эффектом, как правило, программно. Способ борьбы зависит от алгоритма работы прошивки. Если по нажатию на кнопку генерируется прерывание, выполняющее роль сигнала для начала опроса клавиатуры, то следует просто вставить задержку между моментом начала обработки прерывания и опросом клавиатуры. Эта задержка позволит устояться контактам до момента сканирования. Значение задержки выбирается большим чем период дребезга контактов. Если же опрос клавиатуры выполняется циклически (в цикле или по таймеру), период опроса следует выбрать большим чем период дребезга контактов. Это позволит избежать ложных срабатываний.

Дальнейшие планы

Эту статью я считаю не законченной. Не рассмотрен вариант прошивки, выводящей микроконтроллер из энергосберегающего режима. Не рассмотрены схемы с применением двоично-десятичных шифраторов/дешифраторов, что позволило бы еще сэкономить линии микроконтроллера (или увеличить максимальное число кнопок).

Кроме того, если статья Вам показалась полезной, жду от Вас комментарии с предложениями и замечаниями.

Исходный код

Листинг 1. Файл MatrixKeyboard.h

#ifndef _MATRIXKEYBOARD_H_
#define _MATRIXKEYBOARD_H_
 
// Имя считывающего порта. Внимание, указывать только букву,
// например A, B  и тому подобное. По этому имени далее будут
// сформированы имена необходимых регистров.
#define KB_READ_PORT  A
 
// Количество считывающих линий.
#define KB_READ_LINES_COUNT  (4)
 
// Маска считывающих линий. Биты, установленные в 1, указывают на то,
// что соответствующая линия порта является считывающей. Например,
// если установлен третий бит, значит третья линия порта KB_READ_PORT
// является считывающей. И так далее. У меня считывающими будут линии
// с 0 до 3 включительно. Если KB_READ_PORT и KB_SCAN_PORT одинаковы,
// биты KB_READ_LINES_MASK и KB_SCAN_LINES_MASKS не должны пересекаться.
#define KB_READ_LINES_MASK  0b00001111
 
// Имя сканирующего порта. Внимание, указывать только букву,
// например A, B  и тому подобное. По этому имени далее будут
// сформированы имена необходимых регистров.
#define KB_SCAN_PORT  A
 
// Количество сканирующих линий.
#define KB_SCAN_LINES_COUNT  (4)
 
// Маска сканирующих линий. Биты, установленные в 1, указывают на то,
// что соответствующая линия порта является сканирующей. Например,
// если установлен третий бит, значит третья линия порта KB_SCAN_PORT
// является сканирующей. И так далее. У меня сканирующими будут линии
// с 4 до 7 включительно. Если KB_READ_PORT и KB_SCAN_PORT одинаковы,
// биты KB_READ_LINES_MASK и KB_SCAN_LINES_MASKS не должны пересекаться.
#define KB_SCAN_LINES_MASK  0b11110000
 
 
// Этот макрос позволяет выбрать оптимальный тип данных, возвращаемыц
// функцией ReadFromKeyboard. Это необходимо для оптимизации.
#if (KB_READ_LINES_COUNT * KB_SCAN_LINES_COUNT) 

 

Листинг 2. Файл MatrixKeyboard.с

#include 
#include "MatrixKeyboard.h"
 
 
// Ниже идут макросы полуавтоматического конфигурирования кода.
// Предполагается, что их для конфигурирования кода трогать не
// придется. Однако комменты к ним я на всякий случай накидаю.
// Так сказать, для лучшего понимания.
 
 
// Эти два макроса позволяют из буквы, определяющей имя порта
// (например A, B и т.п.) сформировать имя порта вида PORTA.
#define PRECOMP2_PORT(x)    PORT ## x
#define PRECOMP_PORT(x)      PRECOMP2_PORT(x)
 
// Эти два макроса позволяют из буквы, определяющей имя порта
// (например A, B и т.п.) сформировать имя регистра, управляющего
// направлением данных порта вида DDRA.
#define PRECOMP2_DDR(x)      DDR ## x
#define PRECOMP_DDR(x)      PRECOMP2_DDR(x)
 
// Эти два макроса позволяют из буквы, определяющей имя порта
// (например A, B и т.п.) сформировать имя регистра, из которого
// будут считываться сотояния входных линий, имеющего вид PINA.
#define PRECOMP2_PIN(x)      PIN ## x
#define PRECOMP_PIN(x)      PRECOMP2_PIN(x)
 
 
// Определяем имена регистров для считывающего порта.
#define KBI_READ_PORT      PRECOMP_PORT(KB_READ_PORT)
#define KBI_READ_DDR      PRECOMP_DDR(KB_READ_PORT)
#define KBI_READ_PIN      PRECOMP_PIN(KB_READ_PORT)
 
// Определяем имена регистров для сканирующего порта.
#define KBI_SCAN_PORT      PRECOMP_PORT(KB_SCAN_PORT)
#define KBI_SCAN_DDR      PRECOMP_DDR(KB_SCAN_PORT)
 
 
// Это мега извратный способ сформировать таблицу масок для
// сканирующего порта. Предположим, что маска KB_SCAN_LINES_MASK
// равна 0b11110000. Этот макрос сформирует из нее массив из
// следующих значений:
//    0b11100000
//    0b11010000
//    0b10110000
//    0b01110000
// Глаз режет, зато все формируется автоматом, что исключает
// ошибку со стороны замученного программиста.
unsigned char KBI_SCAN_LINES_MASKS[KB_SCAN_LINES_COUNT] = {
 
#if KB_SCAN_LINES_MASK & 0x01
  KB_SCAN_LINES_MASK - 0x01,
#endif
 
#if KB_SCAN_LINES_MASK & 0x02
  KB_SCAN_LINES_MASK - 0x02,
#endif
 
#if KB_SCAN_LINES_MASK & 0x04
  KB_SCAN_LINES_MASK - 0x04,
#endif
 
#if KB_SCAN_LINES_MASK & 0x08
  KB_SCAN_LINES_MASK - 0x08,
#endif
 
#if KB_SCAN_LINES_MASK & 0x10
  KB_SCAN_LINES_MASK - 0x10,
#endif
 
#if KB_SCAN_LINES_MASK & 0x20
  KB_SCAN_LINES_MASK - 0x20,
#endif
 
#if KB_SCAN_LINES_MASK & 0x40
  KB_SCAN_LINES_MASK - 0x40,
#endif
 
#if KB_SCAN_LINES_MASK & 0x80
  KB_SCAN_LINES_MASK - 0x80,
#endif
 
};
 
 
 
// Все. Прилюдия окончена. Теперь несколько коротких функций,
// ради которых мы стока всего подготовили.
 
// Эта функция выполняет инициализацию регистров клавиатуры.
void InitializeKeyboard()
{
  // Считывающие линии конфигурируем как вход.
  KBI_READ_DDR &= ~KB_READ_LINES_MASK;
 
  // Сканирующие линии конфигурируем как выход.
  KBI_SCAN_DDR |= KB_SCAN_LINES_MASK;
 
  // Подключаем подтягивающие резисторы к считывающим линиям.
  KBI_READ_PORT |= KB_READ_LINES_MASK;
}
 
 
// Эта функция опрашивает клавиатуру и возвращает маску нажатых
// клавиш. То есть если в возвращенном значении установлен бит 0,
// значит была нажата клавиша с номером 0. И так далее.
KEYS ReadFromKeyboard()
{
  KEYS result = 0;
 
  for (int i = 0; i 

 

Листинг 3. Файл main.c

#include 
#include "MatrixKeyboard.h"
 
int main()
{
  InitializeKeyboard();
 
  DDRC = 0xFF;
  DDRD = 0xFF;
  PORTC = 0x00;
  PORTD = 0x00;
 
  while(1)
  {
    KEYS res = ReadFromKeyboard();
 
    PORTC = ~((char)res);
    PORTD = ~((char)(res >> 8));
  }
  return 0;
}
Качественный письменный перевод и перевод документов от Native Speaker Translation.