Site Loader

Библиотека для опроса матричной клавиатуры 4×4 и 3х4

Написал программный модуль для опроса матричной клавиатуры.

Особенности модуля:

— простая интеграция с готовым проектом

— поддержка всех микроконтроллеров семейства mega

— возможность использования с любым из трех компиляторов CodeVision, IAR, GCC

— поддержка клавиатур 4х4 и 3х4

— поддержка работы на общей шине

— возможность подключения к одному или двумя портами

— программная антидребезговая защита

— возможность установки произвольных кодов кнопок

— скачиваем архив с модулем
— переписываем файлы keyboard.h и keyboard.c в папку проекта

— подключаем keyboard.c к проекту внутри среды разработки

— включаем keyboard.h в требуемый файл проекта, например main.c

— настраиваем конфигурацию в файле keyboard.h

— прописываем в свой код вызов пользовательских функций

 

Настройка конфигурации включает в себя несколько шагов.

 

Установка тактовой частоты микроконтроллера

 

#define F_CPU 16000000

 

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

Установка типа матричной клавиатуры – 4х4 или 3х4

 

#define KEYBOARD_4X4

 

   Если закомментировать это макроопределение будет установлена клавиатура 3х4. 

Тип заданной клавиатуры сообщается пользователю в процессе сборки проекта. Поэтому не удивляйтесь предупреждению в окне Messages. 

 

Установка типа подключения матричной клавиатуры 

 

#define COMMON_BUS

 

   Для подключения клавиатуры и индикаторов часто используют общую шину, что позволяет экономить выводы микроконтроллера. Если это макроопределение не закомментировано, значит, клавиатура подключена именно таким способом.

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

   Закомментировав макроопределение, можно сэкономить несколько байт флеш памяти.

 

Установка  портов, к которым подключены строки и столбцы клавиатуры

 

//порт, к которому подключены строки

#define PORTX_ROW PORTA

#define PINX_ROW  PINA

#define DDRX_ROW  DDRA

 

//порт, к которому подключены столбцы

#define PORTX_COL PORTB

#define PINX_COL  PINB

#define DDRX_COL  DDRB

 

   Все строки клавиатуры должны быть подключены к одному порту микроконтроллера. Данный модуль не позволяет подключить, например, одну строку к порту B, а остальные к порту С. Это же относится и к столбцам клавиатуры. 

   При этом группа строк или столбцов клавиатуры может быть подключена как к одному, так и к разным портам микроконтроллера. 

 

Установка  выводов, к которым подключены строки и столбцы клавиатуры

 

//выводы, к которым подключены строки 

#define PIN_ROW1 4

#define PIN_ROW2 5

#define PIN_ROW3 6

#define PIN_ROW4 7

 

//выводы, к которым подключены столбцы

#define PIN_COL1 0

#define PIN_COL2 1

#define PIN_COL3 2

#define PIN_COL4 3

 

   Выводы микроконтроллера, подключенные к строкам и столбцам матричной клавиатуры, не должны совпадать между собой. Это требования должно выполняться, когда клавиатура подключена к двум портам микроконтроллера.

 

Установка кодов кнопок

 

//коды кнопок

#define EVENT_NULL 0

#define EVENT_KEY0 ‘0’

#define EVENT_KEY1 ‘1’

#define EVENT_KEY2 ‘2’

#define EVENT_KEY3 ‘3’

#define EVENT_KEY4 ‘4’

#define EVENT_KEY5 ‘5’

#define EVENT_KEY6 ‘6’

#define EVENT_KEY7 ‘7’

#define EVENT_KEY8 ‘8’

#define EVENT_KEY9 ‘9’

#define EVENT_KEYA ‘A’

#define EVENT_KEYB ‘B’

#define EVENT_KEYC ‘C’

#define EVENT_KEYD ‘D’

#define EVENT_KEYZ ‘*’

#define EVENT_KEYR ‘#’

 

   Здесь никаких требований нет. Коды кнопок совершенно произвольные. Можно задать символьные значения, соответствующие кнопкам клавиатуры, как это сделано по умолчанию. А можно задать коды, которые будут использоваться в событийной системе. 

   В модуле реализовано три пользовательские функции.

 

void KEYB_Init(void) – функция инициализации

void KEYB_ScanKeyboard(void) – функция сканирования клавиатуры

unsigned char KEYB_GetKey(void) – функция проверки буфера

 

KEYB_Init() нужно запускать перед использованием двух других функций. Обычно это делается  в начале main`a.

 

KEYB_ScanKeyboard() – это основная функция, реализующая всю работу модуля. Она требует периодического запуска. Можно вставить ее в обработчик прерывания таймера, а можно вызывать ее из основного цикла программы по его сигналу. Первый вариант более расточителен в плане ресурсов микроконтроллера. 

 

KEYB_GetKey() – эта функция возвращает код кнопки, заданный в заголовочном файле keyboard.h. Если кнопочный буфер пуст – функция возвращает код  EVENT_NULL. Вызываем эту функцию там, где собираемся обрабатывать нажатия кнопок. 

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

 

Си библиотека для работы с 1-Wire устройствами

   Для работы с устройствами, поддерживающими 1-Wire протокол уже давным-давно написаны библиотеки. Поэтому нет смысла изобретать велосипед (лично я это уже делал, когда программировал  на ассемблере) и писать что-то свое. На сайте фирмы ATMEL есть замечательный application note AVR318: Dallas 1-Wire, в котором рассмотрены два варианта реализации 1-Wire протокола на микроконтроллерах AVR – программная и аппаратная. Программная реализация позволяет использовать однопроводный протокол на любых микроконтроллерах. Аппаратная – только на тех, на которых есть модуль UART. Аппаратной поддержки 1-Wire протокола «в чистом виде» микроконтроллеры AVR не имеют, однако, используя модуль UART неким хитрым образом, эту поддержку можно организовать. К application note идет проект. Я взял из этого проекта исходные файлы библиотеки, добавил, изменил несколько функций и написал файл compilers.h, чтобы можно было использовать эту либу с любым из трех компиляторов – IAR AVR, GNU GCC (WINAVR), CodeVision.

   Библиотека состоит из следующих файлов

 

OWISWBitFunction.h

OWISWBitFunction.c

OWIUARTBitFunction.c

 

OWIHighLevelFunction.h

OWIHighLevelFunction.c

 

OWIPolled.h

compilers.h

 

OWIdefs.h

OWIdevicespecific.h

OWIcrc.h

OWIcrc.c

Процесс интеграции 1-Wire библиотеки с проектом заключается в следующем: — переписываем файлы библиотеки в папку проекта

— подключаем сишные файлы к проекту

OWIHighFunction.c

OWISWBitFunction.c

OWIUARTBitFunction.c

— добавляем заголовочные файлы в main.c

#include «OWIPolled.h»

#include «OWIHighLevelFunctions.h»

#include «OWIBitFunctions.h»

#include «common_files\OWIcrc.h» — настраиваем файл OWIPolled.h

— выбираем реализацию OneWire интерфейса – программную 

       #define     OWI_SOFTWARE_DRIVER    

       //#define     OWI_UART_DRIVER

— задаем тактовую частоту микроконтроллера

       #define     CPU_FREQUENCY   16.000

— задаем порт, к которому подключена OneWire шина

       #define     OWI_PORT      PORTD   //!< 1-Wire PORT Data register.

       #define     OWI_PIN         PIND    //!< 1-Wire Input pin register.

       #define     OWI_DDR        DDRD    //!< 1-Wire Data direction register.

— в main.c задаем вывод, к которому подключена OneWire шина

      #define BUS   OWI_PIN_7

 

Для GCC проектов в makefile нужно будет добавить все сишные файлы. 

Например, так:

 

SRC = $(TARGET).c bcd.c lcd_lib.c OWISWBitFunctions.c OWIHighLevelFunctions.c OWIUARTBitFunctions.c  common_files/OWIcrc.c

Application note AVR318: Dallas 1-Wire

Проекты проверялись в железе и никаких нареканий не вызывали. В Proteus`е я их тоже запустил, но не сразу. Оказывается по умолчанию датчикам DS18B20 присваиваются одинаковые адреса. При использовании нескольких датчиков адреса нужно подправить ручками.

Подробное описание проектов будет уже в новом году…

AVR. Учебный курс. Процедура сканирования клавиатуры

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

Теперь коротко о файлах:
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 код] , а потом каждый нужный скан код будем прогонять через эту таблицу и при совпадении подставлять на выходе нужный 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 байта. Кто сможет меньше?

Подключение клавиатуры к AVR | avr

На этой страничке рассказывается, как подключать 12-кнопочную клавиатуру к микроконтроллеру AVR, и как считывать клавиатурные нажатия на языке ассемблера. Это перевод статьи «Connecting a keypad to an AVR» [1]. Как работать с матрицей клавиатуры на языке C, см. [2].

[1. Как работает клавиатура]

Клавиатура представляет собой просто набор замыкателей. Нажали кнопку клавиатуры — соответствующий контакт замкнулся. Для того, чтобы уменьшить количество проводов, которые идут от клавиатуры, и тем самым упростить схему подключения, используют соединение замыкателей в матрицу. В нашем примере с 12-кнопочной клавиатурой матрица имеет 3 столбца (Column, провода столбцов пронумерованы как Col3..Col1) и 4 строки (Row, провода строк пронумерованы как Row4..Row1).

К примеру, если нажата кнопка «1», то замыкаются друг на друга Col1 и Row1, если нажата кнопка «2», то замыкаются Col2 и Row1, и так далее.

Чтобы определить, что нажата хотя бы одна кнопка из 12, можно соединить все столбцы на землю, а все строки подключить через нагрузочный резистор (pull-up) к + питания. Тогда на выходе Output появится лог. 0, если будет нажата любая кнопка.

Однако обычно в реальной жизни нужно не только знать, что нажата любая кнопка, нужно определить, какая именно кнопка из 12 была нажата. Для этого не все сразу столбцы подключаются к лог. 0, они подключаются к 0 поочередно, друг за другом. Такая процедура пробегания лог. 0 по столбцам называется сканированием клавиатуры. Один цикл сканирования происходит очень быстро, за время порядка 1..10 миллисекунд. Кроме того, для каждой строки используют отдельный нагрузочный резистор.

Событие нажатия определяется по чтению состояния всех строк (Row4..Row1). Если на всех RowX уровень лог. 1, то это что ни одна из клавиш не нажата. Если на одной из строк RowX появился уровень лог. 0, то сканирование прекращается, и код нажатой клавиши определяется по таблице.

Столбцы Строки Код кнопки
Col1 Col2 Col3 Row1 Row2 Row3 Row4 Символ Двоичный код
0 0 0 1 1 1 1 нет нажатия 1111
0 1 1 0 1 1 1 1 0001
1 0 1 0 1 1 1 2 0010
1 1 0 0 1 1 1 3 0011
0 1 1 1 0 1 1 4 0100
1 0 1 1 0 1 1 5 0101
1 1 0 1 0 1 1 6 0110
0 1 1 1 1 0 1 7 0111
1 0 1 1 1 0 1 8 1000
1 1 0 1 1 0 1 9 1001
0 1 1 1 1 1 0 * 1010
1 0 1 1 1 1 0 0 0000
1 1 0 1 1 1 0 # 1011

Чтобы читать такую клавиатуру с использованием цифровой логики, то нужно иметь как минимум следующее:

• генератор, регистр сдвига и декодер запуска/останова для генерации сигналов столбцов,
• детектор появления нуля на сигналах строк,
• декодер для преобразования 7 цифровых сигналов в код нажатой клавиши.

Лучше всего с такой задачей справится микроконтроллер, например AVR.

[2. Подключение AVR к матрице клавиатуры]

Клавиатурная матрица может быть подключена к AVR напрямую, без каких-либо дополнительных компонентов.

На этой картинке в качестве примера показано подключение матрицы к 7 младшим разрядам порта P микроконтроллера AVR. Можно конечно также использовать любые другие порты в любой комбинации. В нашем примере GPIO PB4..PB6 работают как выходы, они обеспечивают сигналы для столбцов (сканирующий бегущий лог. 0). GPIO PB0..PB3 используются как входы, через них читается состояние строк. На этих входах программно включены внутренние нагрузочные резисторы микроконтроллера (pull-up), так что никакие внешние резисторы для создания лог. 1 не нужны.

В следующем примере кода показано, как инициализируются порты GPIO. Эта часть программы должна быть выполнена 1 раз, когда включается питание AVR.

Инициализация

;
; Инициализация I/O портов для подключения клавиатуры
;
.DEF rmp = R16 ; определение регистра общего назначения
; определение портов
.EQU pKeyOut = PORTB  ; регистр для установки состояния выхода (Col3..Col1)
                      ; и настройки Pull-Up (Row4..Row1)
.EQU pKeyInp = PINB   ; регистр для чтения входов (Row4..Row1)
.EQU pKeyDdr = DDRB   ; регистр, задающий направление работы порта
; Код инициализации
InitKey:
   ldi rmp,0b01110000 ; подготовка данных для регистра DDRB, столбцы
                      ; настраиваются как выходы, строки как входы
   out pKeyDdr,rmp    ; запись в регистр направления
   ldi rmp,0b00001111 ; разрешить резисторы Pull-Up на строках
   out pKeyOut,rmp    ; вывод информации в регистр PORTB

Определение, нажата ли кнопка

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

;
; Проверка, нажата или нет любая из кнопок
;
AnyKey:
   ldi rmp,0b00001111 ; PB4..PB6(Col3..Col1)=0, pull-Up резисторы подключены 
                      ; на входы (PB0..PB3, Row4..Row1).
   out pKeyOut,rmp    ; установка регистра PORTB
   in rmp,pKeyInp     ; чтение состояния строк (Row4..Row1).
   ori rmp,0b11110000 ; установить все неиспользуемые биты в лог. 1
   cpi rmp,0b11111111 ; все биты в лог. 1?
   breq NoKey         ; да, не была нажата ни одна из кнопок

Прим. переводчика: можно также настроить пробуждение микроконтроллера по изменению уровня на любом из входов Row4..Row1. Для этого нужно установить все три столбца Col3..Col1 в состояние лог. 0 и отправить микроконтроллер в режим сна (Sleep, режим пониженного энергопотребления). После того, как пользователь нажмет любую кнопку, микроконтроллер проснется по появлению лог. 0 на входах строк Row4..Row1. В этом месте можно запустить сканирование Col3..Col1 и определить, какая именно кнопка нажата.

Идентификация нажатой кнопки

Теперь нужно прочитать клавиатуру, для этого на столбцы Col3..Col1 поочередно нужно выставить лог. 0. После того, как на одном из портов столбца PB4..PB6 (Col3..Col1) выставлен 0 и на остальных портах столбца лог. 1, проверяется состояние портов строк PB0..PB3 (Row4..Row1) на наличие 0. Регистр Z (регистровая пара R31:R30) указывает на таблицу (размещена в памяти программ FLASH), содержащую коды кнопок. После того, как код определения кнопки закончит работу, регистр Z будет указывать на код нажатой кнопки. С помощью инструкции LPM можно прочитать этот код и сохранить его в регистр R0.

;
; Идентификация нажатой кнопки
;
ReadKey:
   ldi ZH,HIGH(2*KeyTable) ; Z указывает на таблицу кодов кнопок
   ldi ZL,LOW(2*KeyTable)
   ; чтение столбца 1 (Col1)
   ldi rmp,0b00111111      ; PB6 = 0
   out pKeyOut,rmp
   in rmp,pKeyInp          ; чтение строк
   ori rmp,0b11110000      ; маскирование старших бит
   cpi rmp,0b11111111      ; нажата кнопка в этом столбце?
   brne KeyRowFound        ; найдена нажатая кнопка в этом столбце
   adiw ZL,4               ; в этом столбце не было нажатия, перемещение Z
                           ; на 4 кнопки дальше
   ; чтение столбца 2 (Col2)
   ldi rmp,0b01011111      ; PB5 = 0
   out pKeyOut,rmp
   in rmp,pKeyInp          ; снова чтение строк
   ori rmp,0b11110000      ; снова маскирование старших бит
   cpi rmp,0b11111111      ; нажата кнопка в этом столбце?
   brne KeyRowFound        ; найдена нажатая кнопка в этом столбце
   adiw ZL,4               ; в этом столбце не было нажатия, перемещение Z
                           ; на 4 кнопки дальше
   ; чтение столбца 3 (Col3)
   ldi rmp,0b01101111      ; PB4 = 0
   out pKeyOut,rmp
   in rmp,pKeyInp          ; последнее чтение строк
   ori rmp,0b11110000      ; снова маскирование старших бит
   cpi rmp,0b11111111      ; нажата кнопка в этом столбце?
   breq NoKey ; неожиданно обнаружилось, что не нажата кнопка ни в одном
              ; из столбцов
KeyRowFound:               ; найден столбец, где нажата кнопка, теперь
                           ; надо узнать, в какой строке
   lsr rmp                 ; сдвиг лог. 0 влево, бит 0 при этом вдвигается
                           ; в признак переноса
   brcc KeyFound           ; выдвинулся лог. 0, это значит что клавиша найдена
   adiw ZL,1               ; перейти к следующей кнопке в этом столбце
   rjmp KeyRowFound        ; повторить сдвиг
KeyFound:                  ; найдена нажатая кнопка
   lpm                     ; прочитать из таблицы код кнопки в регистр R0
   rjmp KeyProc            ; продолжить обработку кнопок
NoKey:
   rjmp NoKeyPressed       ; не была найдена нажатая кнопка
;
; Таблица кодов кнопок для преобразования
;
KeyTable:
.DB 0x0A,0x07,0x04,0x01 ; первый столбец, кнопки *, 7, 4 и 1
.DB 0x00,0x08,0x05,0x02 ; второй столбец, кнопки 0, 8, 5 и 2
.DB 0x0B,0x09,0x06,0x03 ; третий столбец, кнопки #, 9, 6 и 3

Как избавиться от дребезга контактов (дебоунсинг)

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

Недостатки, что еще можно улучшить

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

Внутренние нагрузочные резисторы микроконтроллера AVR (pull-up) имеют номинал около 50 кОм. Длинные провода или работа в условиях сильных помех могут привести к неустойчивому опросу клавиатуры. Чтобы снизить чувствительность к шумам и помехам, добавьте на сигналы строк Row4..Row1 внешние нагрузочные резисторы подходящего номинала (1..10 кОм).

Еще один недостаток схемы в том, что для работы клавиатуры требуется 7 портов GPIO микроконтроллера. Модификация с использованием цифроаналогового преобразователя (ЦАП, Analog-Digital Convertor, ADC) и цепочки резисторов более экономична и позволяет задействовать меньше ножек микроконтроллера.

[3. Подключение к ADC с использованием резисторной матрицы]

Большинство микроконтроллеров AVR серий Tiny и Mega в настоящее время имеют в своем составе ЦАП (ADC). Без дополнительной внешней аппаратуры ADC может измерить аналоговое напряжение с точностью 10 бит. Если хотите сохранить ножки GPIO и применить для чтения клавиатуры ADC, то Вы как-то должны заставить клавиатуру генерировать разное напряжение в зависимости от того, какая кнопка нажата. Это задача для резисторной матрицы.

Резисторная матрица

На рисунке показано, как устроена резисторная матрица. Строки матрицы подключены к земле (- источника питания), и между столбцами подключены соединенные в стек резисторы. Строки подключены к также к стеку резисторов, но подключенных к + питания (например, 5V). Вход АЦП (канал ADC0) заблокирован конденсатором 1 нФ (1000 пФ). Этот конденсатор и сопротивление резисторов матрицы образуют ФНЧ, который всегда необходим на входе ADC, так как ADC не может правильно оцифровать слишком высокие частоты, которые могут быть вызваны перепадами сигнала при нажатиях на кнопки.

Рассмотрим на примере, как работает чтение клавиатуры с использованием ADC. Если нажата кнопка «5», то активизируется следующий делитель напряжения:

1 кОм + 820 Ом = 1.82 кОм нижнее плечо (подключенное к земле),
3,3 кОм + 680 Ом + 180 Ом = 4.16 кОм верхнее плечо (подключенное к +5V).

При рабочем напряжении 5V на выходе делителя получится напряжение:
(5 * 1.82) / (1.82 + 4.16) = 1.522V

Это напряжение попадет на вход АЦП. Если принять допуск на номинал резисторов 5%, то результирующее напряжение будет между 1.468 и 1.627 вольтами. 10-битный АЦП преобразует это напряжение (если у ADC опорное напряжение то же самое, что и у матрицы, 5V) в значение между 300 и 333. Если мы проигнорируем два младших бита результата (например, разделим результат ADC на 4 сдвигом или если применить левое выравнивание результата, когда ADC это позволяет), это даст 8-битное значение в диапазоне 74..78. Любое нажатие кнопки на клавиатуре даст соответствующий диапазон значений на выходе ADC, который по таблице можно преобразовать в код нажатой кнопки.

Напряжения и распознавание кнопок

Комбинации резисторов в делителе матрицы дадут напряжения, которые собраны в следующую таблицу. Приведены диапазоны напряжений, приведенные к 8-битному результату преобразования ADC. 8-битный результат можно более оптимально использовать для детектирования различий между кнопками.

Кнопка Напряжение, V 8-битный результат ADC Детектирование
(из значения ADC)
min nom max min nom max
1 0.225 0.248 0.272 11 13 14 7
2 0.396 0.434 0.474 20 22 25 18
3 0.588 0.641 0.698 29 33 36 28
4 0.930 0.969 1.048 47 49 54 42
5 1.438 1.522 1.627 74 78 84 64
6 1.959 2.020 2.139 99 103 110 91
7 2.563 2.688 2.809 130 137 144 121
8 3.285 3.396 3.500 167 173 180 156
9 3.740 3.832 3.917 190 195 201 185
* 4.170 4.237 4.298 212 216 221 207
0 4.507 4.550 4.588 229 232 235 225
# 4.671 4.700 4.726 238 240 242 237

Как можно видеть из таблицы, нет перекрытия уровней напряжения при детектировании нажатий различных кнопок, при учете допуска на номинал резисторов 5%. Если захотите поэкспериментировать с другими комбинациями резисторов, то можете скачать лист в формате Excel и Open Office [4], который может производить нужные вычисления для составления таблицы преобразования.

Советы по использованию аппаратного АЦП микроконтроллеров AVR

Чипы ATtiny часто позволяют использовать для опорного напряжения АЦП только внутреннее опорное напряжение или напряжение источника питания. Для опроса клавиатуры в качестве опорного напряжения для АЦП подходит только вариант с напряжением источника питания в качестве опорного. Эта опция должна быть настроена при инициализации ADC, как только программа стартует (после сброса или включения питания).

Многие чипы ATmega могут брать опорное напряжение для ADC с внешнего вывода, AREF. Этот вывод может работать либо как вход, либо как выход. Он будет выходом, если в качестве опорного для ADC выбрано внутренне опорное напряжение, или напряжение источника питания. В этом случае к ножке AREF и к земле должен быть подключен конденсатор, чтобы уменьшить шумы и помехи на опорном напряжении. AREF работает как вход, если настроен выбор внешнего источника опорного напряжения. В этом случае опорное напряжение для ADC поступает от внешнего источника. Если опорное напряжение предоставляет внешний источник, то и матрица резисторов клавиатуры также должна быть запитана от этого же источника. Имейте в виду, что приведенная схема матрицы может потреблять ток до 10 мА, это сделано для уменьшения чувствительности к помехам.

Чипы ATmega позволяют питать ADC от внешнего отдельного источника питания через дополнительный вывод корпуса (AVCC), чтобы дополнительно снизить шумы. Если ADC используется только для клавиатуры, то из-за низкой используемой точности преобразования (8 бит) нет необходимости применять отдельный источник питания для вывода AVCC, и этот вывод может быть напрямую подключен к обычному напряжению питания. Если все же другие каналы ADC используются для других точных измерений, то рекомендуется подключить вывод AVCC к напряжению питания через дроссель номиналом около 22 мкГн, и между выводом AVCC и землей должен быть подключен блокирующий конденсатор 100 нФ (0.1 мкФ).

Инициализация и чтение результата преобразования ADC

Для чтения напряжений, которые генерирует матрица клавиатуры, нужен только один канал ADC. ADC инициализируется один раз, когда запускается программа микроконтроллера (включение питания или сброс). Два примера кода показывают: в одном примере последовательность запуска одиночного преобразования, где используется ATmega8, и в другом примере управляемый прерываниями запуск ADC, этот пример для ATtiny13.

ATmega8: ручной запуск преобразования ADC

Первый пример показывает подпрограмму для ATmega8, без прерываний, с ручным запуском и остановом ADC. Сигнал от клавиатурной матрицы приходит на канал ADC0.

.DEF rKey =R15   ; Регистр для хранения результата ADC
.DEF rmp = R16    ; Регистр общего назначения
   ; установка канала 0 мультиплексора, с левым выравниванием 
   ; результата, AREF берется от AVCC
   ldi rmp,(1<<REFS0)|(1<<ADLAR) ; ADMUX канал 0, AREF от AVCC
   out ADMUX,rmp
   ; включение ADC, запуск конверсии, делитель скорости = 128
   ldi rmp,(1<<ADEN)|(1<<ADSC)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
   out ADCSRA,rmp
   ; ожидание, пока не завершится преобразование
WaitAdc1:
   ; проверка бита ADSC, конверсия завершена если этот бит == 0
   sbic ADCSRA,ADSC  ; конверсия завершена?
   rjmp WaitAdc1     ; еще нет
   ; чтение старшего байта (MSB) результата преобразования ADC
   in rKey,ADCH
   ; выключение ADC
   ldi rmp,(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
   out ADCSRA,rmp

Пожалуйста имейте в виду, что это одиночное преобразование требует примерно 25 * 128 тактовых циклов, на тактовой частоте 1 МГц время преобразования составит 3.2 миллисекунды. Это довольно значительная трата времени, поэтому такой способ получения результата ADC допустим только в том случае, если Вам ничего не надо делать во время задержки на ожидание преобразования (за исключением того, что будет происходить в обработчиках прерываний).

ATtiny13: автозапуск конверсии ADC, с использованием прерываний

Даже ATtiny13 с её 8 ножками может прочитать матрицу клавиатуры, если задействовать ADC (мы не сможем традиционным образом подключить матрицу клавиатуры 3×4, потому что у ATtiny13 не хватит ножек I/O).

В этом примере выбран следующий способ преобразования: напряжение постоянно считывается с канала ADC3 (вывод 2 ATtiny13), и после того как преобразование завершится, следующее преобразование запустится автоматически.

;
; Настройка ADC, первый запуск конверсии
;
   ; PB3=ADC3, этот вход используется для чтения клавиатуры
   ldi rmp,0b00001000   ; отключение цифрового драйвера PB3,
                        ; это экономит потребляемую энергию
   out DIDR0,rmp
   ; Опорное напряжение = напряжению питания, левое выравнивание
   ; результата преобразования, ADMUX переключен на ADC3
   ldi rmp,0b00100011   ; опорное напряжение = напряжению питания,
                        ; выбор ADC3
   out ADMUX,rmp
   ; выбрана опция автостарта преобразования
   ldi rmp,0b00000000   ; постоянно запускающиеся сами по себе
                        ; преобразования (free-running, автостарт).
   out ADCSRB,rmp
   ; запуск ADC, разрешение прерывания, выбор делителя тактовой частоты
   ldi rmp,0b11101111 ; старт ADC, автостарт
   out ADCSRA,rmp       ; прерывание разрешено, делитель тактов 128
; инициализация завершена

Использование прерывания по завершению преобразования требует определения таблицы прерываний, где будет вектор соответствующего обработчика прерывания (rjmp intadc).

;
; Векторы сброса и прерываний ATtiny13
;
.CSEG          ; ассемблирование в сегмент кода,
.ORG $0000     ; с самого его начала
   rjmp main   ; Вектор сброса (Reset)
   reti ; Int0, вектор внешнего прерывания
   reti ; PCINT0, вектор прерывания по изменению уровня
   reti ; TC0, вектор прерывания по переполнению таймера 0
   reti ; вектор прерывания готовности EEPROM
   reti ; вектор прерывания аналогового компаратора
   reti ; вектор прерывания TC0 CompA (событие сравнения)
   reti ; вектор прерывания TC0 CompB (событие сравнения)
   reti ; вектор прерывания WDT
   rjmp intadc ; вектор прерывания по завершению конверсии ADC
;

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

Обработчик прерывания ADC читает результат преобразования. Поскольку выбрано левое выравнивание результата, то достаточно прочитать только старший (MSB) байт результата:

;
; Обработчик прерывания (ISR) для завершения конверсии ADC
;
.DEF rKey = R15   ; Регистр для хранения результата ADC
intadc:
   in rKey,ADCH   ; чтение старшего (MSB) байта преобразования
   reti           ; возврат из прерывания
;

Регистр rKey постоянно дает текущее состояние резисторной матрицы клавиатуры.

Декодирование результата ARC, получения кода нажатой кнопки

Результат преобразования ADC сам по себе не очень-то полезен. Напряжения и соответствующие им результаты преобразования ADC не укладываются в простые математические правила (должно быть, номиналы резисторов 4.7 — 5.6 — 6.8 — 8.2 придумал пьяный профессор математики, и формула V = R1 / (R1 + R2) не очень проста для обработки). Поэтому лучше всего использовать табличный метод для получения кодов кнопок. Таблица не получится примитивной, потому что у нас есть 256 возможных различных результатов преобразования ADC, и нам хотелось бы получить таблицу поменьше.

Как обезьяна, мы будем карабкаться по дереву матрицы шаг за шагом через следующую таблицу:

KeyTable:
.DB 7, 255, 18, 1, 28, 2, 42, 3, 64, 4, 91, 5
.DB 121, 6, 156, 7, 185, 8, 207, 9, 225, 10, 237, 0, 255, 11

Здесь первый байт это значение сравнения для результата преобразования, а второй байт это код кнопки, если значение сравнения больше, чем наш результат. Если результат находится в диапазоне между 0 и < 7, то это означает, что не была нажата ни одна из кнопок (код клавиши 255), если между 7 и < 18, то код кнопки 1, и так далее.

Или можно использовать сразу ASCII-коды для кнопок:

KeyTable:
.DB 7, 0 , 18, '1', 28, '2', 42, '3', 64, '4', 91, '5'
.DB 121, '6', 156, '7', 185, '8', 207, '9', 225, '*', 237, '0', 255, '#'

Код декодирования будет наподобие такого:

;
; Конвертация результата преобразования ADC в код кнопки
;
GetKeyCode:
   ; сначала нужно сделать копию результата, потому что результат
   ; может измениться во время проверки
   mov R1,rKey ; копирование результата ADC в регистр R1
   ldi ZH,HIGH(2*KeyTable)    ; Z указывает на таблицу преобразования
   ldi ZL,LOW(2*KeyTable)
GetKeyCode1:
   lpm               ; чтение значения сравнения из таблицы
   cp R1,R0          ; сравнение значения из таблицы и результата ADC
   brcs GetKeyCode2  ; результат меньше, чем табличное значение,
                     ; кнопка идентифицирована
   inc R0            ; проверка, достигнут ли конец таблицы
   breq GetKeyCode2  ; таблица закончилась
   adiw ZL,2         ; переход к следующей записи в таблице
   rjmp GetKeyCode1  ; переход к сравнению следующей записи
GetKeyCode2:
   adiw ZL,1         ; Z указывает на код кнопки
   lpm               ; чтение кода кнопки в регистр R0
;

Здесь конечно же есть проверка, что не одна их кнопок не нажата (в этом случае R0 = 0xFF, а если используется кодировка кнопок ASCII, то R0 = 0) и мы можем устранить ложные срабатывания (если проверить, что один и тот же код клавиши прочитан 20 или большее количество раз).
Experiences

Первые эксперименты показали, что слишком большие значения резисторов (сначала резисторы были в 10 раз больше по номиналу) делают чтение клавиатуры с использованием АЦП слишком чувствительной к высокочастотным помехам. К примеру, клавиатура отказывалась нормально работать, когда рядом находился передатчик VHF (УКВ) с мощностью около 2 Вт.

[Ссылки]

1. Connecting a keypad to an AVR site:avr-asm-tutorial.net.
2. AVR245: кодовый замок с клавиатурой 4×4 и I2C LCD.
3. Доступ к портам I/O AVR на языке C (GCC, WinAVR).
4. 131121-adc-keyboard.zip.
5. AVR240: клавиатура 4×4, пробуждение AVR от нажатия.

alexxlab

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *