Подключение матричной клавиатуры к микроконтроллерам AVR. AVR
В этой статье мне бы хотелось познакомить читателей с подключением к микроконтроллерам клавиатуры. Дело в том, что обычно большинству схем на микроконтроллерах для ввода данных требуется одна или несколько кнопок. Но, когда проекты становятся сложнее, может возникнуть потребность в применении небольшой клавиатуры. Встречаются варианты клавиатур 3х4 или 4х4, и почти всегда клавиши в них соединены по схеме матрицы. Применение матрицы необходимо из -за того, что для её подключения нужно минимальное число линий ввода - вывода. Например, для клавиатуры 4х4 необходимо, состоящей из 16 кнопок необходимо 16 линий ввода, поэтому рациональнее организовать её в виде матрицы, т.е. расположить 4 кнопки в 4 строки, использовав 8 линий ввода - вывода (один порт микроконтроллера). Самое распространённое решение подключения матрицы на один порт - это подключить строки к старшим разрядам, а столбцы к младшим. Однако, здесь возникает проблема - считывание состояния клавиатуры происходит при возникновении прерывания, но в микроконтроллере ATtiny2313 мы можем использовать два внешних прерывания (остальные выводы заняты). Эта проблема решается подключением четырёх диодов, образующих с подтягивающим резистором на входе INT0 элемент «ИЛИ».
Схема подключения клавиатуры 4х4 матричного типа к микроконтроллеру приведена на Рис.1. Она имеет разъём DB-9F, преобразователь уровней MAX3232, которые необходимы для взаимодействия UART микроконтроллера с RS-232, и работа которых была описана в предыдущей статье. Резисторы R3 - R6 защищают микроконтроллер от короткого замыкания питания на землю. На входе INT0 имеется подтягивающий резистор R7 . Четыре диода VD1 - VD4 подключены к линиям ввода- вывода клавиатуры (катодами) и к выводу INT0 (анодами). Теперь при нажатии любой кнопки, если на столбцы подать нули, на входе INT0 появится низкий уровень.
Как показано на Рис.1 клавиатура подключена к порту В микроконтроллера. Её схема представлена на Рис.2.
Предположим, что весь порт В настроен на вход, и все входа имеют подтяжку:
DDRB = &B00000000
PORTB = &B11111111
Пусть кнопки (Рис.2) каким -то образом соединены ещё и «землёй» (GND), тогда при нажатии, например, кнопки «1» на контактах порта В РВ3 и РВ4 будет низкий уровень, т.е. порт примет значение PORTB = &B11100111, которое и является кодом кнопки «1». Аналогично и в отношении остальных кнопок (код кнопки «В» - 01111011, «5» - 11011011 и т.д.). Но, поскольку кнопки не соединены с GND, необходимо ввести раздельное определение строк и столбцов с последующим суммированием и идентификацией результата с названием кнопки.
Сделаем строки РВ0 - РВ3 входами и включим их подтяжку, а столбцы РВ3 - РВ7 - выходами:
DDRB = &B11110000
PORTB = &B00001111
При нажатии кнопки определённая строка будет иметь низкий уровень. Например, при нажатии кнопки «1» порт В примет значение 00000111, которое является кодом строки. Одновременно с этим включится прерывание, при обработке которого необходимо считать данный код в переменную Stro:
Инвертируем настройки порта - сделаем столбцы РВ3 - РВ7 входами и включим их подтяжку, а строки РВ0 - РВ3 - выходами:
DDRB = &B00001111
PORTB = &B11110000
Теперь при нажатой кнопке «1» порт будет иметь значение 11100000, которое является кодом столбца. Считаем этот код в переменную Col.
Резисторы R2 – R4, R8 – R11 предназначены для ограничения входного/выходного тока в случае неправильной настройки портов или одновременного нажатия нескольких кнопок. Выводы PD0(RXD), PD1(TXD) подключены к преобразователю UART-RS232, который на схеме не отображен. Обмен по USART`у используется для отладки программы.
Алгоритм опроса матричной клавиатуры
Строки клавиатуры подключены к выводам PD4, PD5, PD6, PD7. Они настроены на выход и в начальном состоянии на этих выводах напряжение логического нуля. Столбцы подключены к выводам PC0, PC1, PC2. Они настроены на вход, внутренние подтягивающие резисторы отключены и эти линии “придавлены к нулю питания” с помощью внешних резисторов номиналом в 10 КОм.
Процедура сканирования клавиатуры выглядит следующим образом. Выставляем 1 на выводе PD4 и проверяем состояние выводов PC0, PC1, PC2 (то есть считываем содержимое регистра PINC). Если на каком-то из выводов установлена 1, значит, на клавиатуре в данный момент нажата кнопка, подключенная к первой строке. Сохраняем биты PD4, PD5, PD6, PD7 и PC0, PC1, PC2 в одной переменной – по этому коды мы будем определять номер нажатой кнопки. Если ни одна из кнопок не нажата, продолжаем процедуру сканирования.
Сбрасываем 1 на выводе PD4 и устанавливаем 1 на выводе PD5. Снова проверяем состояние выводов PC0, PC1, PC2, и в случае нажатия кнопки сохраняем биты PD4, PD5, PD6, PD7 и PC0, PC1, PC2 в переменной.
Повторяем описанную последовательность для двух оставшихся строк.
Переключения кнопок клавиатуры сопровождаются дребезгом контактов, который микроконтроллер может “воспринимать” как многократные нажатия. В приложениях, использующих клавиатуры, это нежелательное явление, поэтому в программе опроса клавиатуры обязательно должна быть какая-нибудь защита. Обычно делают так - факт нажатия кнопки регистрируется, если она удерживается в течение нескольких циклов опроса.
Код, получаемый в процессе сканирования клавиатуры, часто требуется преобразовать в символьное значение номера/буквы кнопки (например, для передачи по USART`у). Для этого можно создать таблицу перекодировки - двумерный массив. В первом столбце таблицы будут храниться коды кнопок, а во втором соответствующие символьные значения. Методом последовательного перебора в таблице можно находить нужное значение.
Алгоритм опроса матричной клавиатуры можно реализовать в виде конечного автомата (State Machine) – функции, которая в зависимости от своего состояния (значения определенной переменной) и входного воздействия, выполняет разную работу. На рисунке ниже представлена диаграмма подобного автомат.
Начальное состояние автомата - 0. В этом состоянии автомат находится, пока не будет нажата какая-нибудь кнопка. Когда зафиксировано нажатие на кнопку, запускается функция сканирования клавиатуры ScanKey(), запоминается код нажатой кнопки и автомат переходит в состояние 1.
В состоянии 1 автомат проверяет, нажата ли в данный момент та же кнопка, что и в состоянии 0 или нет. Если коды кнопок не совпадают, автомат возвращается в состояние 0, если совпадают, запускается функция FindKey(), которая находит символьное значение номера кнопки и устанавливает флаги, сигнализирующие системе о нажатой кнопке. По завершению функции автомат переходит в состояние 2.
Пока удерживается одна и та же кнопка, автомат находится в состоянии 2. Если произошли какие-то изменения, он переходит в состояние 3.
Если изменения было случайным, автомат возвращается в состояние 2, а если нет, переходит в начальное состояние, чтобы снова запустить функцию сканирования клавиатуры.
Программная реализация автомата
Представленную диаграмму конечного автомата легко преобразовать в программный код.//хранит текущее состояние автомата
unsigned char
keyState;
//прототипы функций используемых автоматом
unsigned char
AnyKey(void
);
unsigned char
SameKey(void
);
void
ScanKey(void
);
unsigned char
FindKey(void
);
void
ClearKey(void
);
void
ScanKeyboard(void
)
{
switch
(keyState){
case
0:
if
(AnyKey()) {
ScanKey();
keyState = 1;
}
break;
case
1:
if
(SameKey()) {
FindKey();
keyState = 2;
}
else
keyState = 0;
break;
case
2:
if
(SameKey()){}
else
keyState = 3;
break;
case
3:
if
(SameKey()) {
keyState = 2;
}
else
{
ClearKey();
keyState = 0;
}
break;
Default:
break;
}
Довольно наглядная запись. Теперь все, что нам остается, это написать/разобрать реализации недостающих функций. Чтобы код программы был максимально понятным, давайте обойдемся без макроопределений. Итак, по порядку.
unsigned char AnyKey(void )
{
PORTD |= 0xf0;
return (PINC & 0x07);
}
Устанавливаем на выводах PD7 – PD4 единицы, возвращаем состояния выводов PC2 – PC0. Если какая-нибудь из кнопок клавиатуры в этот момент нажата, функция вернет значение отличное от нуля, то есть true.
//хранит код нажатой кнопкиunsigned char keyCode;
void
ScanKey(void
)
{
unsigned char
activeRow = (1<<4);
while
(activeRow) {
PORTD = (PORTD & 0x0f)|activeRow;
if
(PINC & 0x07) {
keyCode = (PINC & 0x07);
keyCode |= (PORTD & 0xf0);
}
activeRow <<= 1;
}
}
Устанавливаем в 1 четвертый бит переменной activeRow (активизируем первую строку). Обнулив биты PD7 – PD4, записываем переменную в PORTD. Если какой-нибудь из трех младших разрядов регистра PINC установлен в единицу, значит в данный момент нажата кнопка, относящаяся к первой строке. Сохраняем биты PD7 – PD4 и PC2 - PC0 в переменную keyCode. Сдвигаем влево значение переменной activeRow на один разряд и повторяем цикл еще три раза.
unsigned char SameKey(void )
{
PORTD = (PORTD & 0x0f) | (keyCode & 0xf0);
return ((PINC & keyCode) & 0x07);
}
Функция проверяет, совпадает ли код нажатой в данный момент кнопки с кодом, полученным в предыдущем цикле опроса. Для этого на нужной строке устанавливается 1 – в PORTD записываются старшие 4 бита keyCode. Затем считывается регистр PINC, на него накладывается маска в виде переменной keyCode и выделяются 3 младших разряда. Полученное значение возвращается. Если коды кнопок совпадут, значение будет отлично от нуля, то есть true.
//хранит символьное значение нажатой кнопки
unsigned char keyValue;
//флаговая переменная - устанавливается, если кнопка удерживается
unsigned char keyDown;
//флаговая переменная - устанавливается, когда нажата новая кнопка
unsigned char keyNew;
//таблица перекодировки
__flash unsigned cha
r
keyTable = {
{ 0x11, "1"},
{ 0x12, "2"},
{ 0x14, "3"},
{ 0x21, "4"},
{ 0x22, "5"},
{ 0x24, "6"},
{ 0x41, "7"},
{ 0x42, "8"},
{ 0x44, "9"},
{ 0x81, "*"},
{ 0x82, "0"},
{ 0x84, "#"}
};
unsigned char
FindKey(void
)
{
unsigned char
index;
for
(index = 0; index < 12; index++) {
if
(keyTable == keyCode) {
keyValue = keyTable ;
keyDown = 1;
keyNew = 1;
return
1;
}
}
return
0;
На страницах этого сайта уже была представлена работа по созданию и использованию клавиатуры, но она была сенсорная . Те, кому нужен вариант попроще, чаще выбирают стандартные кнопочные клавиатуры, в которых кнопки соединены матрицей. Такое соединение кнопок позволяет сократить число выводов необходимых для подключения к микроконтроллеру.
Чаще всего встречаются клавиатуры 3х4 и 4х4 с 12-ю и 16-ю кнопками, соответственно. У меня в руках оказалась клавиатура 4х4. Работу с ней сейчас и рассмотрим.
У меня оказалась вот такая клавиатура с мембранными контактами. Хороша тем, что в толщину составляет всего 1 мм и легко может приклеиваться к нужным поверхностям.
Потыкав по кнопкам, с помощью мультиметра установил как соединены кнопки внутри клавиатуры.
Для сканирования матричных клавиатур в Bascom-AVR существует специальная функция Getkbd () . Эта команда заточена под использование клавиатур 4х4, поэтому она использует все 8 выводов одного порта микроконтроллера. И в случае использования клавиатур c меньшим числом кнопок стоит это учитывать.
Конфигурирование команды заключается в назначении порта, к которому будет подключена клавиатура, время прохождения дребезга контактов и задержка после определения нажатия
к примеру, в следующей строке:
Config Kbd = Portd , Debounce = 20 , Delay = 100
сконфигурировано подключение клавиатуры к PortD, время дребезга установлено 20 мс, время задержки после нажатия 100 мс
Эта команда берет на себя всю работу по сканированию строк клавиатуры и возвращает переменной номер нажатой клавиши от 0 до 15. Если нажатия на кнопку не зафиксировано, тогда функция вернет число 16. Дальше, в зависимости от того как соединены кнопки, может потребоваться узнать символ нажатой кнопки.
В приведенном ниже примере, сканирование клавиатуры происходит с частотой 10 Гц и находится в главном цикле программы. Результат нажатий выводится на ЖК дисплей.
$regfile
=
"m8def.dat"
$crystal
=
1000000
"конфигурация дисплея
Config
Lcdpin
=
Pin
,
Rs
=
Portc
.
0
,
E
=
Portc
.
1
,
Db4
=
Portc
.
2
,
Db5
=
Portc
.
3
,
Db6
=
Portc
.
4
,
Db7
=
Portc
.
5
Config
Lcd
=
20
*
4
Cursor
Off
Cls
"конфигурация клавиатуры
Config
Kbd
=
Portd
,
Debounce
=
20
,
Delay
=
100
"переменные
Dim
Key_char
As
Byte
"номер нажатой клавиши
Dim
Key_str
As
String
*
1
"символ нажатой клавиши на клавиатуре
Dim
Result
As
String
*
20
"результат нажатий на клавиатуру
Result
=
""
"Главный цикл программы
Do
Key_char
=
Getkbd
()
"клавиша не нажата и функция возвращает переменной значение 16
If
Key_char
<>
16
Then
"если переменная не равна 16, значит была нажата кнопка
Key_str
=
Lookupstr
(key_char
,
Keyboard_data
)
"вытаскиваем символ нажатой клавиши
Result
=
Result
+
Key_str
End
If
Locate
1
,
1
Lcd
Result
"выводим на дисплей результат нажатий
Waitms
100
Loop
End
Keyboard_data
:
Data
"1"
,
"4"
,
"7"
,
"*"
,
"2"
,
"5"
,
"8"
,
"0"
Data
"3"
,
"6"
,
"9"
,
"#"
,
"A"
,
"B"
,
"C"
,
"D"
А вот тестовая схемка, по которой подключаются дисплей и клавиатура:
Еще в схему не мешало бы добавить токоограничительные резисторы на линию клавиатуры, номиналом 100-150 Ом. На всякий случай, но для пробы сойдет и так (главное не нажимать сразу несколько кнопок)
Подключаем, прошиваем, тыкаем на кнопки и видим результат - на дисплей выводятся символы нажатых клавиш:
Позже добавлю демонстрационное видео, как только найду чем снять в приличном качестве.
А пока можно скачать архив в котором находится файл для симуляции в Proteus и прошивка.
Пора бы рассказать как организовать опрос такой клавы. Напомню, что клава представляет из себя строки, висящие на портах и столбцы, которые сканируются другим портом. Код написан для контроллера ATMega8535 , но благодаря тому, что все там указано в виде макросов его можно быстро портировать под любой другой контроллер класса Mega , а также под большую часть современных Tiny . Хотя в случае с Tiny может быть некоторый затык ввиду неполного набора команд у них. Придется чуток дорабатывать напильником.
Короче, ближе к коду. Сразу оговорюсь, что я взял моду крошить один проект на десяток мелких файлов, а потом подключать их по мере необходимости. Во-первых, это резко структурирует код, позволяя легче в нем ориентироваться, а во-вторых, код становится модульным и его куски можно использовать как готовые библиотеки в других программах. Только подправить чуток. По этой же причине я все определения делаю через макросы, чтобы не пришлось править весь код, а достаточно было только пару строк изменить в файле конфигурации.
Теперь коротко о файлах:
keyboard_define.inc
— файл конфигурации клавиатуры.
В этом файле хранятся все макроопределения используемые клавиатурой. Здесь мы задаем какие ножки микроконтроллера к какой линии подключены. Одна тонкость — выводы на столбцы (сканирующий порт
) должны быть последовательным набором линий одного порта. То есть, например, ножки 0,1,2,3
или 4,5,6,7
, или 3,4,5,6
. Неважно какого порта, главное чтобы последовательно.
С определением ножек, думаю проблем не возникнет, а вот по поводу параметра KEYMASK
я хочу рассказать особо.
Это маска по которой будет выделяться сканируемый порт. В ней должны быть 6 единиц и один 0. Ноль выставляется в крайне правую позицию сканирующего порта.
Пример:
У меня сканирующий порт висит на битах 7,6,5,4 крайне правый бит сканирующего порта это бит 4, следовательно маска равна 0b11101111 — ноль стоит на 4й позиции. Если сканирующие линии будут висеть на ножках 5,4,3,2, то маска уже будет 0b11111011 — ноль на второй позиции. Зачем это все будет объяснено ниже.
Также есть маска активных линий сканирующего порта — SCANMSK . В ней единицы стоят только напротив линий столбцов. У меня столбцы заведены на старшую тетраду порта, поэтому сканирующая маска имеет вид 0b11110000 .
В разделе инициализации нужно не забыть настроить ножки сканирующего порта на выход, а ноги считывающего на вход с подтяжкой. А потом вставить код обработчика клавиатуры куда-нибудь в виде обычной подпрограммы. Пользоваться просто — вызываем подпрограмму чтения с клавы, а когда возвращаемся у нас в регистре R16 находится скан код клавиши.
Вот так у меня выглядел тестовый код:
Main: SEI ; Разрешаем прерывания.
RCALL KeyScan ; Сканируем клавиатуру
CPI R16,0 ; Если вернулся 0 значит нажатия не было
BREQ Main ; В этом случае переход на начало
RCALL CodeGen ; Если вернулся скан код, то переводим его в
; ASCII код.
MOV R17,R16 ; Загружаем в приемный регистр LCD обработчика
RCALL DATA_WR ; Выводим на дисплей.
RJMP Main ; Зацикливаем все нафиг.
Про LCD дисплей я пока ничего не скажу, так как процедуры еще не доведены до ума, но будут выложены и разжеваны в ближайшее время.
Теперь расскажу как работает процедура KeyScan
Def COUNT = R18
KeyScan: LDI COUNT,4 ; Сканим 4 колонки
LDI R16,KEYMASK ; Загружаем маску на скан 0 колонки.
Вначале мы подготавливаем сканирующую маску. Дело в том, что мы не можем вот так взять и гнать данные в порт. Ведь строки висят только на последних четырех битах, а на первых может быть что угодно, поэтому нам главное ни при каких условиях не изменить состояние битов младшей тетрады порта.
KeyLoop: IN R17,COL_PORT ; Берем из порта прежнее значение
ORI R17,SCANMSK ; Выставляем в 1 биты сканируемой части.
Вначале загружаем данные из регистра порта , чтобы иметь на руках первоначальную конфигурацию порта. Также нам нужно выставить все сканирующие биты порта в 1, это делается посредством операции ИЛИ по сканирующей маске. В той части где стояли единицы после операции ИЛИ по маске 11110000 (мое значение SCANMASK ) все биты станут единицами, а где был ноль останутся без изменений.
AND R17,R16 ; Сбрасываем бит сканируемого столбца
OUT COL_PORT,R17 ; Выводим сформированный байт из порта.
Теперь мы на сформированный байт накладываем маску активного столбца . В ней вначале ноль на первой позиции, а все остальные единицы. В результате, другие значения порта не изменятся, а вот в первом столбце возникнет 0. Потом маска сдвинется, а вся операция повторится снова. В результате ноль будет уже в следующем столбце и так далее. Таким образом, мы организуем «бегающий» нолик в сканирующем порте, при неизменности других, посторонних, битов порта. А дальше сформированное число загружается в регистр порта и ножки принимают соответствующие уровни напряжений.
NOP ; Задержка на переключение ноги.
NOP
NOP
NOP
SBIS ROW0_PIN,ROW0 ; Проверяем на какой строке нажата
RJMP bt0
SBIS ROW1_PIN,ROW1
RJMP bt1
SBIS ROW2_PIN,ROW2
RJMP bt2
SBIS ROW3_PIN,ROW3
RJMP bt3
Серия NOP нужна для того, чтобы перед проверкой дать ножке время на то, чтобы занять нужный уровень. Дело в том, что реальная цепь имеет некоторое значение емкости и индуктивности, которое делает невозможным мгновенное изменение уровня , небольшая задержка все же есть. А на скорости в 8Мгц и выше процессор щелкает команды с такой скоростью, что напряжение на ноге еще не спало, а мы уже проверяем состояние вывода. Вот я и влепил несколько пустых операций. На 8Мгц все работает отлично. На большую частоту, наверное, надо будет поставить еще штук пять шесть NOP или влепить простенький цикл. Впрочем, тут надо поглядеть на то, что по байтам будет экономичней.
После циклов идет четыре проверки на строки. И переход на соответствующую обработку события.
ROL R16 ; Сдвигаем маску сканирования
DEC COUNT ; Уменьшаем счетчик столбцов
BRNE KeyLoop ; Если еще не все перебрали делаем еще одну итерацию
CLR R16 ; Если нажатий не было возвращаем 0
RET
.undef COUNT
Вот тут происходит сдвиг маски влево командой циклического сдвига ROL
. После чего мы уменьшаем счетчик итераций (изначально равен четырем, так как у нас четыре столбца). Если нажатий не было, то по окончании всех четырех итераций мы вываливаемся из цикла, обнуляем регистр R16
и возвращаемся.
bt0: ANDI R16,SCANMSK ; Формируем скан код
ORI R16,0x01 ; Возвращаем его в регистре 16
RET
А вот один из возможных концов при нажатии. Тут формируется скан код который вернется в регистре R16.
Я решил не заморачиваться, а как всегда зажать десяток байт и сделать как можно быстрей и короче. Итак, что мы имеем по приходу в этот кусок кода. А имеем мы один из вариантов сканирующего порта (1110,1101,1011,0111
), а также знаем номер строки по которой мы попали сюда. Конкретно в этот кусок можно попасть только из первой строки по команде RJMP bt0.
Так давай сделаем скан код из сканирующей комбинации и номера строки! Сказано — сделано! Сначала нам надо выделить из значения порта сканирующую комбинацию — она у нас хранится в регистре R16
, поэтому выковыривать из порта ее нет нужды. Продавливаем операцией И значение R16
через SCANMASK
и все что было под единичками прошло без изменений, а где были нули — занулилось. Опа, и у нас выведен сканирующий кусок — старший полубайт. Теперь вклеим туда номер строки — операцией ИЛИ
. Раз, и получили конструкцию вида [скан][строка]
Вот ее и оставляем в регистре R16
, а сами выходим прочь! Также и с остальными строками. Погляди в исходнике, я их не буду тут дублировать.
Декодирование скан кода.
Отлично, скан код есть, но что с ним делать? Его же никуда не приткнуть. Мы то знаем, что вот эта шняга вида 01110001
это код единички, а какой нибудь LCD
экран или стандартная терминалка скорчит нам жуткую кракозябру и скажет, нам все что она думает о нашей системе обозначений — ей видите ли ASCII
подавай. Ладно, будет ей ASCII.
Как быть? Прогнать всю конструкцию по CASE
где на каждый скан код присвоить по ASCII
коду меня давит жаба — это же сколько надо проверок сделать! Это же сколько байт уйдет на всю эту тряхомудию? А память у нас не резиновая, жалкие восемь килобайт, да по два байта на команду, это в лучшем случае. Я мог все это сделать прям в обработчике клавиатуры. НЕТ!!! В ТОПКУ!!! Мы пойдем своим путем.
Ок, а что у нас есть в запасе? Метод таблиц перехода не катит, по причине жуткой неупорядоченности скан кодов. Почесал я тыковку, пошарился по квартире… и тут меня осенило. Конечно же!!! Брутфорс!!!
Брутфорсим скан код.
Итак, у нас есть жутко несваримый скан код, а также стройная таблица ASCII
символов. Как скрестить ужа с ежом? Да все просто! Разместим в памяти таблицу символов в связке [скан код]:
, а потом каждый нужный скан код будем прогонять через эту таблицу и при совпадении подставлять на выходе нужный ASCII
из связки. Классический пример программизма — потеряли во времени, зато выиграли в памяти.
Вот так это выглядит:
CodeGen:LDI ZH,High(Code_Table*2) ; Загрузил адрес кодовой таблицы
LDI ZL,Low(Code_Table*2) ; Старший и младший байты
Тут мы загрузили в индексный регистр адрес нашей таблицы. Умножение на два для того, чтобы адрес был в байтах, т.к. в среде компилятора пространство кода адресуется в словах.
Brute: LPM R17,Z+ ; Взял из таблицы первый символ — скан код
CPI R17,0xFF ; Если конец таблицы
BREQ CG_Exit ; То выходим
CPI R16,0 ; Если ноль,
BREQ CG_Exit ; то выходим
CP R16,R17 ; Сравнил его со скан кодом клавиши.
BREQ Equal ; Если равен, то идем подставлять ascii код
Загружаем из таблицы первый скан код и нычим его в регистр R17 , попутно увеличиваем адрес в регистре Z (выбор следующей ячейки таблицы) и первым делом сравниваем его с FF — это код конца таблицы. Если таблица закончилась, то выходим отсюда. Если мы не всю таблицу перебрали, то начинаем сравнивать входное значение (в регистре R16 ) вначале с нулем (нет нажатия), если ноль тоже выходим. И со скан кодом из таблицы. Если скан таблицы совпадает со сканом на входе, то переходим на Equal .
LPM R17,Z+ ; Увеличиваем Z на 1
RJMP Brute ; Повтор цикла
А в случае если ничо не обнаружено, то мы повторно вызываем команду LPM R17,Z+ лишь для того, чтобы она увеличила Z на единичку — нам же надо перешагнуть через ASCII код и взять следующий скан код из таблицы. Просто INC Z не прокатит, так как Z у нас двубайтный . ZL и ZH . В некторых случаях достаточно INC ZL , но это в случае когда мы точно уверены в том, что адрес находится недалеко от начала и переполнения младшего байта не произойдет (иначе мы вместо адреса 00000001:00000000 получим просто 00000000:0000000, что в корне неверно), а команда LPM все сделает за нас, так что тут мы сэкономили еще пару байт. Потом мы вернемся в начало цикла, а там будет опять LPM которая загрузит уже следующий скан код.
Equal: LPM R16,Z ; Загружаем из памяти ASCII код.
RET ; Возвращаемся
Если же было совпадение, то в результате LPM Z+
у нас Z
указывает на следующую ячейку — с ASCII
кодом. Ее мы и загружаем в регистр R16
и выходим наружу.
CG_Exit: CLR R16 ; Сбрасываем 0 = возвращаем 0
RET ; Возвращаемся
А в случае нулевого исхода, когда либо таблица кончилась, а скан код так и не подобрался, либо ноль был в регистре R16 на входе — возвращаемся с тем же нулем на выходе. Вот так вот.
; STATIC DATA
;========================================
Code_Table: .db 0x71,0x31 ;1
.db 0xB1,0x32 ;2
.db 0xD1,0x33 ;3
.db 0x72,0x34 ;4
.db 0xB2,0x35 ;5
.db 0xD2,0x36 ;6
.db 0x73,0x37 ;7
.db 0xB3,0x38 ;8
.db 0xD3,0x39 ;9
.db 0x74,0x30 ;0
.db 0xFF,0 ;END
Тут просто табличка статичных данных, на границе памяти. Как видишь данные сгруппированы по два байта — сканкод/ASCII
Вот посредством таких извратов вся программа, с обработкой клавиатуры, декодированием скан кода, чтением/записью в LCD индикатор и обнулением оперативки (нужно для того, чтобы точно быть увереным, что память равна нулю) заняло всего 354 байта . Кто сможет меньше?
В очередном уроке, я поведаю читателю о том, как пишутся статьи на данном сайте, чтобы можно было не только скопировать готовый код, но и проследить за ходом мыслей. Поэтому урок будет в другом формате.
Первым делом выбирается тема изучения. Эта статья — про матричную клавиатуру. Далее нужно разобраться, что это такое, основной источник — википедия. Если там нет информации — гугл. После прочтения 3-5 ссылок, главное понять что оно из себя представляет и зачем вообще нужно. Ответ на первый вопрос должен быть примерно таким — это набор кнопок, включенных и опрашиваемых особым образом. Так почему не использовать обычное включение кнопок? Это ответ на второй вопрос — подобное включение, при использовании 16 и более кнопок, позволяет сэкономить ножки микроконтроллера. Все очень просто, на первом этапе этой информации более чем достаточно.
Теперь следующий шаг, изучение схемы. Во время изучения схемы микроконтроллер представляем черным ящиком, который может либо дрыгать ножками, либо читать информацию с ножек. Смотрим на особенности схемы, на одну ножку приходится 4 кнопки по горизонтали (строки), аналогично 4 кнопки приходится на одну ножку по вертикали (столбцы). Кнопки в пределах одной строки/столбца объединены общим проводом. Получается матрица 4х4.
Теперь нужно понять принцип работы более детально. Ножки, подключенные к общему проводу строк, настраиваются как вход, те что подключены к столбцам настроены как выход. Когда активен только первый столбец, то мы однозначно знаем, что нажаты могут быть только кнопки с 1 по 4. Далее, переключаемся на второй столбец, сканируем кнопки с 5 по 8 и т.д. Остается лишь читать состояния входов. Диоды нужны, чтобы защитить входы микроконтроллера, если нажато несколько кнопок одновременно. Этой информации должно быть достаточно для того, чтобы написать собственную прошивку. Написать свою прошивку это интересно и главное, ты знаешь как она работает. Поэтому я стараюсь все прошивки писать с нуля. Перейдем к программной части.
Начнем с того, что опрос кнопок, сам по себе, не является целью. Возможно в основном цикле будет программа, использующая клавиатуру, поэтому логично производить опрос независимо от основного цикла, через равные промежутки времени. Поможет нам в этом прерывание таймера по совпадению. Его суть в том, что когда таймер сделает заданное количество тиков основная программа встанет на «паузу» и выполнится код записанный отдельно от основного цикла. Этот код и будет нашим опросом.
Можно использовать Таймер1, но он имеет множество полезных функций и может пригодиться для других вещей, поэтому задействуем таймер2. С какой периодичностью производить опрос? С такой, чтобы не пропустить самые быстрые нажатия. Для надежности, решил взять 20 раз в секунду, т.е. прерывание должно происходить раз в 50мс. Таймер2 не очень годится для длинных отсчетов, потому что максимальное число, которое можно записать в регистр сравнения 0xFF = 255. При минимальной частоте таймера 7813Гц, один тик будет длиться 1/7813=0,000128сек, т.е. максимальный промежуток между прерываниями 0,000128*255=32,6мс. В данном случае не принципиально 50 или 32, поэтому этот результат меня устроил.
За одно прерывание нужно опросить 4 столбца, для этого сделаем цикл for, переменная i хранит номер опрашиваемого столбца.
| int i= 0 ; < 4 ; i++ ) { } } |
int i=0; // Timer 2 output compare interrupt service routine interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { } }
Прерывание работает, цикл крутится, нужно добавить переключения столбцов. Для этого, проще всего, создать массив, в котором будут заранее сконфигурированы состояния PORTD, эти состояния будут по очереди записываться в порт.
| int i= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; // Timer 2 output compare interrupt service routine interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; } } |
int i=0; char portState= {0xEF,0xDF,0xBF,0x7F}; // Timer 2 output compare interrupt service routine interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; } }
С переключениями порта разобрались, теперь нужно в течение каждого переключения, проверить каждую строку — не замкнута ли кнопка, т.е. произвести опрос PIND.0-PIND.3. Для этого сделаем в одном цикле еще один цикл, используем for, переменная j хранит номер строки.
| int i= 0 , j= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; for (j= 0 ; j< 4 ; j++ ) { } } } |
int i=0,j=0; char portState= {0xEF,0xDF,0xBF,0x7F}; interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; for(j=0; j<4; j++) { } } }
Теперь нужно добавить сам опрос входа, но если писать if(PIND.0==0), то получится много условий, код будет не компактным, поэтому, вспоминаем про «логическое и»: если число 1 и число 2 равны единице, то результат будет равен единице, во всех остальных случаях результат 0. Добавляем массив, в котором записаны номера пинов от 0 до 3. Выделяем из всего PIND, при помощи «логического и», нужный пин и проверяем равен ли он нулю.
| int i= 0 , j= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; char inputState[ 4 ] = { 0x01 , 0x02 , 0x04 , 0x08 } ; interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; for (j= 0 ; j< 4 ; j++ ) { if (((PIND& inputState[ j] ) == 0 ) ) { } } } } |
int i=0,j=0; char portState= {0xEF,0xDF,0xBF,0x7F}; char inputState={0x01,0x02,0x04,0x08}; interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; for(j=0; j<4; j++) { if(((PIND&inputState[j])==0)) { } } } }
Неплохо еще вывести на экран какой нибудь символ. Для этого можно создать двухмерный массив, выбор символа, будет зависеть от номера столбца и строки. Символы до 9 соответствуют номерам кнопок, на остальные задействованы буквы. Символ выводится в момент сканирования строки, что очень удобно, так как мы знаем номер столбца и строки.
| int i= 0 , j= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; char inputState[ 4 ] = { 0x01 , 0x02 , 0x04 , 0x08 } ; char symbol[ 4 ] [ 4 ] = { { "1" , "2" , "3" , "4" } , { "5" , "6" , "7" , "8" } , { "9" , "A" , "B" , "C" } , { "D" , "E" , "F" , "D" } } ; // Timer 2 output compare interrupt service routine interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; for (j= 0 ; j< 4 ; j++ ) { if (((PIND& inputState[ j] ) == 0 ) ) { lcd_putchar(symbol[ i] [ j] ) ; } } } } |
int i=0,j=0; char portState= {0xEF,0xDF,0xBF,0x7F}; char inputState={0x01,0x02,0x04,0x08}; char symbol={{"1","2","3","4"}, {"5","6","7","8"}, {"9","A","B","C"}, {"D","E","F","D"}}; // Timer 2 output compare interrupt service routine interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; for(j=0; j<4; j++) { if(((PIND&inputState[j])==0)) { lcd_putchar(symbol[i][j]); } } } }
Все супер, все работает, но… когда нажимаешь на кнопку один раз выводится сразу несколько символов. Поэтому введем проверку. После того, как кнопка была нажата, программа крутится в пустом бесконечном цикле, до тех пор, пока кнопка не будет отжата. Это позволит выводить символы по одному. В итоговую программу добавлена кнопка для очистки экрана.
| #include |
#include
Примерно в такой последовательности я рассуждал, когда писал данную прошивку. Дальше должна следовать еще отладка на железе. Но в пределах данной статьи ограничусь симулятором.
Все что я хотел сказать данной статьей, что собирать из маленьких готовых кирпичиков гораздо проще, чем пытаться сразу написать все целиком. Разбейте большую задачу на множество маленьких задач и постепенно их решайте. Главное, разобраться с принципом работы устройства и заставить прошивку работать, оптимизировать ее можно после. Прошивка и схема доступны .
Update: Зачем нужны диоды? Допустим некий момент времени, когда опрашиваем первый столбец, соответственно на остальных столбцах логическая единица. Если диодов нет и нажать кнопку 1 и 5, то будет короткое замыкание.
Популярное
- Что такое синий экран смерти?
- Как пользоваться яндекс диском и что это такое Как работает яндекс диск на компьютере
- Объединяем разделы жёсткого диска
- Ускорение подтверждения транзакции биткоин как это сделать
- Почему BIOS не видит диск (CD-DVD) и что делать?
- Как делать скриншоты на Mac?
- Как в фотошопе вырезать и вставить объект Наложение полупрозрачных изображений на фотографию в фотошопе
- Браузер теперь и для Linux
- Непрочитанные сообщения вконтакте
- Что значит «добавить на Яндекс» виджет





