Подключение клавиатуры к AVR |
Добавил(а) microsin | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
На этой страничке рассказывается, как подключать 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, то сканирование прекращается, и код нажатой клавиши определяется по таблице.
Чтобы читать такую клавиатуру с использованием цифровой логики, то нужно иметь как минимум следующее: • генератор, регистр сдвига и декодер запуска/останова для генерации сигналов столбцов, Лучше всего с такой задачей справится микроконтроллер, например 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 кОм нижнее плечо (подключенное к земле), При рабочем напряжении 5V на выходе делителя получится напряжение: Это напряжение попадет на вход АЦП. Если принять допуск на номинал резисторов 5%, то результирующее напряжение будет между 1.468 и 1.627 вольтами. 10-битный АЦП преобразует это напряжение (если у ADC опорное напряжение то же самое, что и у матрицы, 5V) в значение между 300 и 333. Если мы проигнорируем два младших бита результата (например, разделим результат ADC на 4 сдвигом или если применить левое выравнивание результата, когда ADC это позволяет), это даст 8-битное значение в диапазоне 74..78. Любое нажатие кнопки на клавиатуре даст соответствующий диапазон значений на выходе ADC, который по таблице можно преобразовать в код нажатой кнопки. Напряжения и распознавание кнопок Комбинации резисторов в делителе матрицы дадут напряжения, которые собраны в следующую таблицу. Приведены диапазоны напряжений, приведенные к 8-битному результату преобразования ADC. 8-битный результат можно более оптимально использовать для детектирования различий между кнопками.
Как можно видеть из таблицы, нет перекрытия уровней напряжения при детектировании нажатий различных кнопок, при учете допуска на номинал резисторов 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 (мы не сможем традиционным образом подключить матрицу клавиатуры 3x4, потому что у 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 или большее количество раз). Первые эксперименты показали, что слишком большие значения резисторов (сначала резисторы были в 10 раз больше по номиналу) делают чтение клавиатуры с использованием АЦП слишком чувствительной к высокочастотным помехам. К примеру, клавиатура отказывалась нормально работать, когда рядом находился передатчик VHF (УКВ) с мощностью около 2 Вт. [Ссылки] 1. Connecting a keypad to an AVR site:avr-asm-tutorial.net. |