![]() |
Данная статья является продолжением предыдущей о подключении клавиатуры к МК с помощью трех сигнальных проводов. В этой часте я расскажу вам о том, как увеличить число кнопок на клавиатуре до 16, опишу алгоритм обработки нажатий этих кнопок и то, как ассоциировать с каждой кнопкой свою функцию. Итак, приступим.
Аппаратная часть
Как вы уже догадались, чтобы подключить дополнительные кнопки к блоку клавиатуры, нужно добавить дополнительный сдвиговый регистр, который будет захватывать нажатия других восьми кнопок. Ниже приведена блок схема этой конструкции:
Рассмотрим режим работы, когда при каждом клике ногой CLK происходит сдвиг битов влево по направлению к старшему (S0 поднята, S1 опущена). Взглянем на сдвиговый регистр U1. При каждом дрыге ногой CLK, бит, который находится на выводе Qn, перемещается на вывод Qn+1, тоесть сдвигается в сторону старшего бита (влево). Но так как биту, который находится на ноге Q7 уже некуда сдвигаться, то он по идее должен бы был пропасть. Чтобы этого не произошло, мы посылаем его на следующий сдвиговй регистр U2, подключив Q7 регистра U1 к ноге SR SER регистра U2. Объясню, что же это за нога. В рассматриваемом нами режиме работы сдвигового регистра (S0 поднята, S1 опущена) биты смещаются в cторону старшего, а на место младшего становится бит, который в данный момент находится на ноге SR SER. Так как два наших сдвиговых регистра тактируются от одного источка (микроконтроллера), то бит на ноге Q7 сдвигового регистра U1, при сдвиге не теряется, а перескакивает на сдвиговый регистр U2, где продолжает свой путь в микроконтроллер.
Помимо SR SER, существует нога SL SER. Она обладает практически идентичными свойствами, за исключением того, что она используется при сдвигании регистров вправо, а не влево (режим, который мы не используем, S0 опущена, S1 поднята. В данном режиме биты будут двигаться по направлению к младшему байту, т.е вправо).
![]() |
Таким образом, соеденив два сдвиговых регистра, мы по сути получаем один 16-ти битный, поведение которого абсолютно идентично 8-ми битному, за исключением того, что каждый раз нам необходимо считывать не 8, а 16 бит. Как я уже говорил в первой статье, время на сканирования такой клавиатуры возрастает примерно в 2 раза. Безболезненно к данной схеме можно добавлять все новые и новые сдвиговые регистры, подключая Q7 пин предыдущего к SR SER последующего. В данной статье мы ограничимся лишь двумя.
Ниже представлена схема данного устройства
![]() |
Схема упрощенная, показано только подключение клавиатуры и LCD. Питание и прочая обвязка контроллера как обычно.
Повторюсь, немаловажная деталь — подтягивающие резисторы R1 — R16. Если вы не знаете их назначения, прочитайте еще раз пункт «Описание кнопок» в первой части.
Далее переходим к написанию кода, который будет сканировать все 16 кнопок и что-то делать.
Програмная часть
За основу программы я возьму микроядро (простая операционная система), описанное на этом сайте. Подробности его установки и работы с ним вы можете прочитать в соответствующих статьях AVR курса, тут же я не буду подробно останавливаться на тонкостях его работы.
Начинаю программы я всегда с пустого шаблона, в котором данная ОС уже готова к работе, а нам лишь нужно добавить задачи и инициализировать их запуск. Так же у меня есть привычка разбивать программу на отдельные файлы, в каждом из которых находятся разные части нашей программы. Например инициализация, макросы, векторы прерываний, дополнительные функции и т.п. По ходу статьи я буду пояснять отдельные куски кода, показывать что откуда вышло, и для чего мы что-то делаем. Проект вцелом вы можете скачать по ссылке в конце статьи, и по нему отслеживать описание всех функций. В проекте весь код достаточно подробно прокоментирован.
Итак, у нас есть заготовка (ее вы тоже можете скачать вконце статьи), помимо нее мы будем использовать готовые библиотеки, работу которых я пояснять не буду, просто расскажу что они делают. Исходники вы можете посмотреть сами. Переносим все необходимые нам файлы в папку с проектом и инклудим их в соответствующих местах кода.
Файлы с макросами инклудятся в самом начале проекта, а файлы с кодом, я обычно инклудю вконце (см. проект)
- библиотека для работы с LCD (lcd4,asm, lcd4_macro.inc, от DI HALT’a)
- Функция для прекодирования кирилических символов в ANSI кодировке, в символы, которые будут понятны дисплею. (ansi2lcd.asm) Данное решение встретил на форуме. Переписал его с Си на Асм и пользуюсь).
- Функция для сканирования нашей клавиатуры. Собственно ее я опишу в этой статье. Я ее вынес отдельным файлом, для удобства ее последующего использования (keyb_scan_init.asm, keyb_scan.asm)
Сканирование клавиатуры
Принцип сканирования клавиатуры я описал в предыдущей статье. В данном случае у нас будет небольшое отличие, т.к. нам нужно считать не 8, а 16 бит, т.е. 2 байта, со сдвиговых регистров.
Общий план действий
- Устанавливаем бит T в регистре SREG. (Это пользовательский бит, который можно использовать для любых нужд. В нашем случае установленный бит будет означать, что мы считываем первый байт с нашей клавиатуры, если при проверке этот бит будет сброшен, то будем считать, что действие происходит со считыванием второго байта).
- В цикле считываем 8 бит из сдвигового регистра.
- Проверяем бит T:
- Если он установлен, то мы только что считали первый байт, прячем его в закрома, сбрасываем бит T и возвращаемся на пункт 2.
- Если он сброшен, то мы только что считали второй байт. Задача выполнена, выходим.
Код
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | .equ BTN_PORT = PORTA .equ BTN_DDR = DDRA .equ BTN_PIN = PINA .equ BTN_DATA_IN = 0 .equ BTN_HOLD = 1 .equ BTN_CLK = 2 btn_start: SBI BTN_PORT,BTN_HOLD ; поднимаем S1 SBI BTN_PORT,BTN_CLK ; Кликаем CBI BTN_PORT,BTN_CLK CBI BTN_PORT,BTN_HOLD ; опускаем S1 SET ; устанавливаем бит T в регистре SREG. ; данный бит мы устанавливаем как флаг того, что мы считываем первый байт. btn_again: LDI R17,0 ; в этом регистре будет накапливаться результат. обнуляем его LDI R18,8 ; счетчик. цикл будем проделывать 8 раз btn_loop: LSL R17 ; если мы проходим тут, первый раз, то данная команда с нулем ничего не ; сделает, если же нет, то двигаем все биты влево SBIC BTN_PIN,BTN_DATA_IN ; если к нам на вход пришла 1, INC R17 ; записываем 1 в самый младший разряд регистра R17 SBI BTN_PORT,BTN_CLK ; кликаем CBI BTN_PORT,BTN_CLK DEC R18 ; уменьшаем счетчик BREQ btn_loop_end ; если счетчик досчитал до нуля, то переходим в btn_loop_end Rjmp btn_loop ; иначе повторяем цикл, где первой же командой сдвигаем все биты влево. ;Таким образом старые старшие байты постепенно сдвигаются на свое место. btn_loop_end: BRTC btn_exit ; если бит T сброшен (а это значит, что мы уже приняли второй байт), то выходим из функции CLT ; иначе сбрасываем бит T (это значит что мы закончили прием первого байта, и будем ; принимать второй MOV R16,R17 ; сохраняем первый принятый байт в регистре R16 RJMP btn_again ; и возвращаемся к считыванию байта btn_exit: RET |
Сохраняем данную функция в файл keyb_scan.asm и кидаем в папку с проектом.
Далее нам необходимо проинициализировать ноги контроллера. Это дело лучше автоматизировать, чтоб потом не заудмываться и не писать руками то, что можно не писать. Создадим файл keyb_scan_init.asm и напишем в нем следующее:
1 2 3 | SBI BTN_DDR,BTN_HOLD ;выход HOLD SBI BTN_DDR,BTN_CLK ;Выход CLK SBI BTN_PORT,BTN_DATA_IN ;вход DATA_IN |
Этот файл просто подключаем к проекту в разделе инициализации, ничего в нем не меняя.
1 2 | .include "init.asm" ; в данном файле хранится общая инициализация .include "keyb_scan_init.asm" ; инициализация ног для сканирования клавиатуры |
Итак, функция сканирования клавиатуры у нас готова, далее нужно обрабатывать приходящие данные. Но прежде я добавлю две задачи, которые будут служить индикацией работы нашей ОС. Ибо в процессе отладки кода, контроллер может уходить в ребут, и дабы это сразу замечать и не тратить время на локализацию проблемы, я делаю моргалку диодом. Диод моргает — ОС работает.
Создаем две задачи. Одна зажигает диод, другая гасит. Причем обе вызывают друг друга с задержкой 500 мс.
1 2 3 4 5 6 7 | SysLedOn: SetTimerTask TS_SysLedOff,500 SBI PORTD,5 RET ;----------------------------------------------------------------------------- SysLedOff: SetTimerTask TS_SysLedOn,500 CBI PORTD,5 RET |
И запустим их во время старта в области Background
1 | RCALL SysLedOn |
Я не буду полностью описывать как добавить задачу в микроядро. Это достаточно подробно описано в соответствующих статьях, ссылки на которые я дал выше.
Далее перейдем в сканированию клавиатуры. Создадим задачу KeyScan и запустим ее в области Background
1 2 | Background: RCALL SysLedOn RCALL KeyScan |
Функция KeyScan будет обрабатывать два байта последовательно, пришедших с клавиатуры, используя аналогичный метод с битом T. Так же для перехода по необходимым нам функнциям в зависимости от нажатой кнопки, мы будем использовать таблицу с адресами переходов.
1 2 | Code_Table: .dw Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8 Code_Table2: .dw Key9, Key10, Key11, Key12, Key13, Key14, Key15, Key16 |
Это две таблицы, одна для клавиш с 1 по 8, другая — с 9 по 16. В ней последовательно расположены адреса на функции, которые мы будем выполнять в зависимости от того, какая кнопка нажата.
Для этого мы заранее загрузим адрес начала таблицы в регистровую пару Z, и затем, вычислив, какая же по счету кнопка была нажата, прибавим это смещение к адресу начала таблицы. Получим адрес с ячейкой, в которой содержится адрес функции, которую нужно выполнить. Звучит немного сложно, но на самом деле, все достаточно просто и понятно. Главно вчитаться в предыдущее предложение.
Обратите внимание, что в таблице адреса занимают по 2 байта, а смещение у нас будет увеличиваться на один, поэтому нам необходимо будет умножить смещение на два. Вы увидете это в коде. Знайте, что это из за того, что адрес занимает два байта, и нам нужно перепрыгнуть через оба.
Другая особенность:
При отсутствии нажатий с клавиатуры приходит 0b11111111. Тоесть ненажатая кнопка — высокий уровень. К примеру нажмем кнопку 3, и к нам придет число 0b11110111 (соответствующий бит сброшен). Поэтому выведем алгоритм: пришедший байт мы сначала будем сравнивать с маской 0b11111110, потом с 0b11111101, затем 0b11111011 и т.д. Мы просто будем в цикле сдвигать биты в маске влево, каждый раз сверяя ее с пришедшим байтом и увеличивая счетчик. В тот момент, когда будет совпадение — в счетчике будет номер нажатой кнопки. Что нам собственно и требуется.
В функции будет использоваться один байт из оперативной памяти.
1 2 3 | ; RAM =========================================== .DSEG KeyFlag: .byte 1 |
В нем будет храниться последнее зарегестрированное нажатие. После записи туда номера кнопки, мы будем ставить функцию очистки этого байта через 200 мс. Это сделано для того, чтобы повторная обработка нажатия этой кнопки не производилась ранее этого времени, т.е. только до того, как KeyFlag будет сброшен. Это делается для защиты от случайных двойных нажатий.
Итак, код сканирования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | KeyScan: SetTimerTask TS_KeyScan,50 RCALL btn_start ; сканируем клавиатуру. результат приходит в регистрах R16 и R17 SET ; ставим флаг T в регистре SREG. Он будет означать, что мы обрабатываем первый принятный с клавиатуры байт LDI ZL,low(Code_Table*2) ; берем адрес первой таблицы с переходами ; (для кнопок 1-8) LDI ZH,high(Code_Table*2) KS_loop: CPI R16,0xFF ; если байт равен 0xFF, то нажатия не было, BREQ KS_loop_exit ; переходим на обработку следующего байта с клавиатуры LDS R18,KeyFlag ; берем последнее зарегестрированное нажатие CP R16, R18 ; сравниваем с текущим BREQ KS_loop_exit ; если одинаковы, переходим на обработку следующего ; байта с клавиатуры STS KeyFlag,R16 ; иначе сохраняем в RAM текущее нажатие ; как последнее зарегестрированное PUSH R16 PUSH R17 SetTimerTask TS_ClearFlag, 200 ; ставим на запуск через 200 мс функцию очистки ; последнего зарегестрированного нажатия POP R17 ; данная функция использует R16 и POP R16 ; R17, поэтому сохраняем их в стеке RJMP KS_got_smth ; если мы дошли до этого места, то у нас ; есть нажатие, которое нужно обработать. идем на обработку KS_loop_exit: BRTC KS_exit ; проверяем флаг T в регистре SREG. Если он ; не сброшен, а значит мы считали только ; один байт с клавиатуры, то идем ; дальше, иначе выходим CLT ; сбрасываем флаг T.Это означает что мы считали ; первый байт с клавиатуры, и готовы ; ко второму. LDI ZL,low(Code_Table2*2) ; берем адрес второй таблицы с LDI ZH,high(Code_Table2*2) ; переходами (для кнопок 9-16) MOV R16,R17 ; второй принятый байт перекидываем в R16 RJMP KS_loop ; и возвращаемся в цикл ; тут мы оказываемся, когда нам нужно обработать нажатие. ; KS_got_smth: CLR R18 ; R18 будет счетчиком. Нужно сравнить 8 возможных состояний пришедшего байта, ; поэтому будем считать до 8 LDI R19,0b11111110 ; первоначальная маска для сравнения ее с пришедшим битом, и дальнейшего сдвигания влево KS_loop2: CP R16,R19 ; сравниваем маску с пришедшим байтом BREQ KS_equal ; если равны, то переходим на действие INC R18 ; иначе увеличиваем счетчик CPI R18,8 ; сравниваем его с восьмеркой BREQ KS_exit ; если досчитали до 8, то выходим SEC ; тут двигаем нашу маску влево. так как младшие байты нам нужно заполнять ; единицами, а функция ROL устанавливаем эту единицу только при наличии флага C, ;то устанавливаем этот флаг ROL R19 ; двигаем биты в маске RJMP KS_loop2 ; и переходим опять на цикл KS_equal: LSL R18 ; R18 хранится число, до которого мы успели досчитать, ; пока ждали совпадения байта ; с клавиатуры с маской.В нем по сути находится номер нажатой кнопки. ; умножаем его на 2, так как в талице переходов адреса ; хранятся по 2 байта ADD ZL,R18 ; складываем смещение с заранее сохраненным адресом таблицы переходов ADC ZH,R0 ; в R0 я всегда храню ноль LPM R16,Z+ ;загружаю необходимый адрес из таблицы LPM R17,Z MOV ZL,R16 ; перекидываем его в адресный регистр Z MOV ZH,R17 ICALL ; и вызываем функцию по этому адресу KS_exit: RET |
Вместо ICALL можно в данном случае применить IJMP будет примерно тот же эффект, но выход из KeyScan будет через RET в вызваной функции. Не так очевидно, зато сэкономим два байта стека :) Формально это можно представить как то, что наша функция KeyScan это этакий многозадый кащей. Вошли в одну голову, а вывались через одну из задниц определенных нажатием клавиши.
Наверняка вы заметили следующуюю строчку:
1 | SetTimerTask TS_ClearFlag,200 |
Данный макрос устанавливает на выполнение функцию ClearFlag через 200 мс. Данная функция должна удалить из ячейки KeyFlag в оперативной памяти информацию о прошлом нажатии. Так как при отсутствии нажатий с клавиатуры приходит байт 0b11111111, то в функции ClearFlag и будем записывать в ячейку KeyFlag это число:
1 2 3 | ClearFlag: SER R16 ; R16 = 0xFF STS KeyFlag,R16 ; сохраняем это в RAM RET |
Теперь рассмотрим таблицу с адресами переходов повнимательнее.
1 | Code_Table: .dw Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8 |
Code_Table — адрес начала таблицы. Прибавляя к этому адресу необходимое нам смещение, мы будем получать адрес ячейки, в которой хранится адрес перехода (Key1, Key2, Key3 и т.д) на нужную нам функцию. Директива .dw означает что для каждого элемента, описанного далее в строке выделяется по 2 байта. Выделяем столько, ибо адреса у нас двухбайтовые.
Итак, переходы на нужные нам функции при нажатии клавишь у нас есть. Теперь, чтоб выполнить какой-либо код, при нажатии на кнопку 1, нам нужно в любом месте программы добавить следующую функцию:
1 2 3 4 5 | Key1: LDI R16,0x02 ; просто какой-то случайный код. не несет в себе смысла. ; Тут вы подставите то, что нужно будет выполнить вам при нажатии на кнопку 1 LDI R17,0x03 SUB R16,R17 RET ; обязательно выход из этой функции по RET, иначе будет переполнение стека |
Тут собственно можно было бы и остановиться. Я рассказал принцип действия данной клавиатуры, рассказал о функциях сканирования и перехода по заданным адресам в зависимости от нажатой клавиши, но все же я расскажу вам, как данную клавиатуру можно применить. Сделаем ввод текста на дисплей. Так как кнопок у нас немного, то полноразмерную QWERTY клаву сделать не получится, поэтому обойдемся тем что есть. Будем делать ввод текста как на телефоне. Т9 я реализовывать не буду, ибо это достаточно трудоемко в качестве примера. Поэтому на каждую кнопку прикрутим по 4 символа, которые будут поочередно выводиться на дисплей при каждом нажатии. При задержке нажатия на определенное время (например 1 секунда) происходит сдвиг курсора. Так же реализуем команды пробел, стереть символ, очистить дисплей, и перемещение курсора влево, вправо, вверх, вниз.
Начнем с букв и символов. Как я уже говорил, к каждой кнопке мы прикрутим по 4 символа. Для этого создадим таблицы этих символов, по которым мы будем их перебирать:
1 2 3 4 5 6 7 8 9 | Letter_K_Table1: .db 0x2E,0x2C,0x3F,0x21,0, 0 ;""., ",", "?", "!" Letter_K_Table2: .db 0xE0,0xE1,0xE2,0xE3,0, 0 ;а, б, в, г Letter_K_Table3: .db 0xE4,0xE5,0xE6,0xE7,0, 0 ;д, е, ж, з Letter_K_Table4: .db 0xE8,0xE9,0xEA,0xEB,0, 0 ;и, й, к, л Letter_K_Table5: .db 0xEC,0xED,0xEE,0xEF,0, 0 ;м, н, о, п Letter_K_Table6: .db 0xf0,0xf1,0xf2,0xf3,0, 0 ;р, с, т, у Letter_K_Table7: .db 0xf4,0xf5,0xf6,0xf7,0, 0 ;ф, х, ц, ч Letter_K_Table8: .db 0xf8,0xf9,0xfa,0xfb,0, 0 ;ш, щ, ъ, ы Letter_K_Table9: .db 0xfc,0xfd,0xfe,0xff,0, 0 ;ь, э, ю, я |
Тут последовательно забиты строчные буквы кирилицы в ANSI кодировке. каждая таблица заканчивается нулем. так как в таблице должно быть четное количество байт, то я добавил еще по нулю вконце. Это немного расточительно с точки зрения использования памяти, но увы и ах — адресация у нас тут словами.
Очевидно предположить, что при нажатии любой из девяти кнопок будет выполняться один и тот же код, но будут использоваться разные данные. А это значит, что мы создадим функцию, в которую будут передаваться номер нажатой кнопки и адрес начала нашей таблицы с символами для этой кнопки. Таблицы были только что описаны выше.
В обработчике нажатия первой кнопки Key1 запишем следующий код:
1 2 3 4 5 6 | ; символы "." "," "?" "!" key1: LDI ZL,low(Letter_K_Table1*2) ; загружаем в Z адрес начала таблицы LDI ZH,high(Letter_K_Table1*2) ; с символами, принадлежащей первой кнопке LDI R16,1 ; загружаем в R16 номер нажатой кнопки RCALL lcd_write_l ; вызов функции вывода символа в видеопамять RET |
Аналогичные действия проделываем с восемью другими функциями. Код приводить не буду, если нужно — все есть в проекте.
Как вы уже догалались, все действие будет происходить в функции lcd_write_l. Она будет сверять пришедшее нажатие с предыдущим, и в зависимости от результата брать следующий символ из таблицы символов и помещать его на место последней буквы (если кнопка нажата повторно), либо записывать первый символ из таблицы в новую ячейку видеопамяти (если нажата новая кнопка). Также будет использоваться макрос установки задачи по отчистке последнего нажатия кнопки с отсрочкой на определенное время.
Принцип действия практически аналогичный тому, который использовался для защиты от случайных повторов, при сканировании клавиатуры, только задержка по времени больше.
Символы в LCD мы будем записывать не напрямую, а через промежуточную видеопамять, которая будет находиться в оперативной памяти (Хехехе дается мне на это повлиял алгоритм демопроги, что шел в документации к Pinboard прим. DI HALT ;) ) . Это сделано для удобства последующего наращивания функционала программы. Набранный текст будет проще сохранять, обрабатывать, посылать на ПК и т.д. Позднее мы создадим задачу обновления дисплея, которая, периодически запускаясь, будет записывать символы из видеопамяти в дисплей. Получается такого рода отвязка основной логики программы от железа.
При необходимости, с легкостью можно будет применить любой другой дисплей, переписал лишь только функцию его обновления. Данную абстракцию логики программы от железа я произвожу в учебных целях. Пусть даже данное решение излишне для нашего задания, но правильно написанная программа впоследствии ползволяет сэкономить кучу времени себе и другим программистам. Поэтому лучше сразу привыкать писать правильно. (Как писать правильно, а как нет, это лишь мое сугубо личное мнение. У кого-то оно может отличаться. Я не навязываю свою точку зрения, я рассказываю то, что знаю сам).
Создаем ячейки для видеопамяти в RAM и кое-какие переменные:
1 2 3 4 5 6 | .equ LCD_MEM_WIDTH = 32 ; размер памяти LCD. у меня дисплей 2 строки по 16 символов. LCDMemory: .byte LCD_MEM_WIDTH PushCount: .byte 1 ; счетчик нажатий на кнопку KeyFlagLong: .byte 1 ; тут хранится номер последней нажатой кнопки CurrentPos: .byte 1 ; текущее положение курсора |
Далее, привожу код всей функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | .equ delay = 1000 ; задержка перед сдвигом курсора lcd_write_l: LDS R17,KeyFlagLong ; загружаем номер последней нажатой кнопки CP R16,R17 ; сравниваем его с текущей нажатой кнопкой BREQ lwl_match ;---нажата новая кнопка--- lwl_not_match: STS KeyFlagLong,R16 ; была нажата другая кнопка. сохраняем ее номер в RAM CLR R17 STS PushCount,R17 ; и обнуляем счетчик нажатий кнопки, ибо эту кнопку мы нажали первый раз RJMP lwl_action ;---повторно нажата кнопка--- lwl_match: LDS R17,PushCount ; если же была нажата кнопка повторно INC R17 ; увеличиваем счетчик нажатий LDS R18,CurrentPos DEC R18 ; сдвигаем текущее положение курсора ; влево. так как нам необходимо будет ; заново переписать букву на прежнем месте STS CurrentPos,R18 PUSH R17 ; макрос SetTimerTask использует регистр R17, поэтому заранее сохраняем его в стеке SetTimerTask TS_Reset_KeyFlagLong,delay ; ставим задачу отчистки номера ; о текущей кнопки.по истечению ; этого времени мы сможет повторно ; одной кнопкой вывести вторую букву POP R17 lwl_action: ADD ZL,R17 ;прибавляем смещение к адресу таблицы с ANSI ; символами, принадлежащими данной кнопке ADC ZH,R0 LPM R16,Z ; загружаем нужный нам символ из таблицы CPI R16,0 ; проверка на ноль. Если ноль - то конец таблицы BRNE lwl_next_act ; если не конец таблицы, то продолжаем действие переходом на next_act SUB ZL,R17 ; иначе нам нужно вернуться на начало таблицы, SBCI ZH,0 ; поэтому обратно вычитаем смещение из адреса нашей таблицы CLR R17 ; в R17 у нас лежит счетчик нажатий. Обнуляем его. RJMP lwl_action ; и повторяем все действие заново. но как будто это наше первое нажатие ; на данную кнопку lwl_next_act: STS PushCount,R17 ; прямчем в RAM счетчик нажатий RCALL ansi2lcd ; преобразование ANSI в кодировку, пригодную для LCD. ; Вход и выход - R16. изменяет регистр R17 lwl_wr_mem: LDS R17,CurrentPos ; загуржаем текущее положение курсора LDI ZL,low(LCDMemory*2) ; загружаем адрес таблицы видеопамяти LDI ZH,high(LCDMemory*2) ADD ZL,R17 ; складываем смещение (положение курсора) с началом таблицы ADC ZH,R0 ; R0 я держу всегда нулем ST Z,R16 ; сохраняем символ в видеопамяти INC R17 ; увеличиваем на 1 текущее положение CPI R17,LCD_MEM_WIDTH ; сравниваем, достигло ли текущее положение конца памяти LCD BRNE lwl_not_end CLR R17 ; если да, обнуляем текущее положение lwl_not_end: STS CurrentPos,R17 ; и сохраняем текущее положение в RAM RET |
RCALL ansi2lcd — данная строчка вызывает функцию преобразования ANSI символа в кодировку, понятную LCD на базе HD44780. Так как по умолчанию эти дисплеи плохо дружат с кирилицей, приходится немного извращаться, чтоб корректно выводить кирилические символы. Принцип действия данной функции я описывать не буду, можете самостоятельно подсмотреть код в файле ansi2lcd.asm. Скажу лишь, что символ посылаем в регистре R16, и получаем оттуда же. Данная функция также изменяет регистр R17, будьте аккуратны, не оставляйте в нем ничего нужного.
Вообщем, запись необходимого символа в видеопамять у нас реализована. Перейдем к функции отрисовки дисплея из видеопамяти. Она будет в цикле поочередно брать символы из видеопамяти и посылать их в LCD. По сути ничего сложного. Единственно надо будет отследить, когда курсор достигнет конца первой строки, затем перевести его на вторую. Иначе символы запишутся не в видимую часть дисплея. Подробнее об видимых и невидимых областях памяти дисплея можно прочитать в это статье http://easyelectronics.ru/avr-uchebnyj-kurs-podklyuchenie-k-avr-lcd-displeya-hd44780.html
Создадим новую задачу ОС и назовем ее LCD_Reflesh. Поставим ее на первоначальный запуск в области Background
1 2 3 | Background: RCALL SysLedOn RCALL KeyScan RCALL LCD_Reflesh |
и напишем саму функцию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | LCD_Reflesh: SetTimerTask TS_LCD_Reflesh,100 ; запускаем обновление дисплея каждый 100 мс LDI ZL,low(LCDMemory*2) ; грузим в Z адрес видеопамяти LDI ZH,high(LCDMemory*2) LCD_COORD 0,0 ; устанавливаем текуюю координату курсора в LCD в самое начало LDI R18,LCD_MEM_WIDTH ; грузим в R18 длину видеопамяти. это будет нас счетчик lcd_loop: LD R17,Z+ ; цикл. тут мы берем из видеопамяти один символ в регистр R17 RCALL DATA_WR ; и записываем его в LCD. DEC R18 ; уменьшаем счетчик BREQ lcd_exit ; если достигли конца видеопамяти - выходим CPI R18,LCD_MEM_WIDTH/2 ; достигли ли конца первой строки? brne lcd_next LCD_COORD 0,1 ; если да, устанавливаем текущую координату ; курсора в LCD на вторую строчку lcd_next: RJMP lcd_loop ; и продолжаем цикл lcd_exit: RET |
Можно считать что минимальный рабочий код написал. Компилируем и прошиваем. Работать будут кнопки с буквами и символами. Подключаем к микроконтроллеру наш блок клавиатуры и LCD дисплей.
Клавиатура:
- 1 пин блока кнопок — PORTA.0
- 2 пин блока кнопок — PORTA.1
- 3 пин блока кнопок — PORTA.2
- 4 пин блока кнопок — +5 V
- 5 пин блока кнопок — Общий провод (Ground)
LCD дисплей:
- Пин E — PORTB.0
- Пин RW — PORTB.1
- Пин RS — PORTB.2
- Пин Data.4 — PORTB.4
- Пин Data.5 — PORTB.5
- Пин Data.6 — PORTB.6
- Пин Data.7 — PORTB.7
О распиновке LCD дисплея очень много информации в интернете. Гуглите «подключение WH1602». Подробности о том, как подключить LCD дисплей к плате PinBoard — смотрите в инструкции к ней.
Запускаем демоплату и наблюдаем следующую картину.
Сначала удивляемся. С первого взгляда ничего не работает и на экране каракули. Но, понажимав на кнопки, можно убедиться, что символы последовательно выводятся на экран, заменяя собой эти самые каракули. Значит проблема в начальной инициализации дисплея, а точнее в видео памяти. С самого начала видеопамять заполнена нулями (Мы же в нее при инициализации ничего не записываем).
И далее эти нули посылаются в дисплей, где они превращаются в крякозябру. А нам нужна пустота, символ пробела (не знаю как его еще назвать=) ).
Немного погуглив, находим таблицу ANSI символов, где беглым взгялдом находим нужный нам символ — пустоту. Его код — 0x20. Отлично, при инициализации контроллера заполним ячейки видеопамяти этими числами. Создаем функцию очистки видеопамяти. Выглядит она следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 | LCD_Clear: LDI ZL,low(LCDMemory*2) ; загружаем в Z адрес таблицы видеопамяти LDI ZH,high(LCDMemory*2) LDI R16,0x20 ; 0x20 - код пустого символа. ; при отчистке заполняем им всю видеопамять LDI R17,LCD_MEM_WIDTH ; в счетчик кладем длину видеопамяти LCl_loop: ST Z+,R16 ; записываем 0x20 в текущую ячейку памяти DEC R17 ; уменьшаем счетчик BRNE LCl_loop ; если он еще не достиг нуля, повторяем цикл STS CurrentPos,R0 ; текущую позиция курсора устанавливаем в ноль RET |
Далее переходим в область инициализации (init.asm) нашего проекта и добавляем
1 | RCALL LCD_Clear ; отчищаем память дисплея |
Компилируем, прошиваем и убедждаемся что теперь все работает корректно.
Далее по заданию нам нужно реализовать кнопки: стереть текущий символ, отчистить дисплей, навигация вверх, вниз, влево, вправо, пробел.
Стереть
Данное действие прикручиваем к кнопке 4. А действие тут простое: уменьшаем текущее положение курсора на 1 (ибо стираем предыдущий символ), прибавляем текущее положение к адресу начала таблицы видео памяти (получаем адрес ячейки с текущим символом), и записываем туда число 0x20 (символ пустоты, пробела). Нам нужно будет произвести одну проверку: находясь в самой первой ячейке, мы не можем использовать данную функцию по понятным причинам.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ;стереть key4: LDS R17,CurrentPos ; загружаем текущее положение курсора TST R17 ; проверяем его BREQ key4_exit ; если оно ноль, то выходим DEC R17 ; уменьшаем текущее положение, т.е. ; переходим на символ, который нужно стереть STS CurrentPos,R17 ; сохраняем данное значение в RAM LDI ZL,low(LCDMemory*2) ; грузим адрес таблицы видеопамяти LDI ZH,high(LCDMemory*2) ADD ZL,R17 ; складываем смещение (положение курсора) с началом таблицы ADC ZH,R0 ; R0 я держу всегда нулем LDI R16,0x20 ; загружаем в R16 число 0x20. ; В LCD оно означает пустое место, пробел. ST Z,R16 ; сохраняем символ в видеопамять key4_exit: RET |
Отчистка экрана
Проще некуда. Вызываем функцию LCD_Clear
1 2 3 | ; Отчистка экрана key8: RCALL LCD_Clear RET |
Навигация
Все махинации сводятся к изменению текущего положения, но при наличии проверок: находясь на первой строке, мы не можем перейти вверх, находясь на нижней — вниз.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | ; курсор вверх key12: LDS R16,CurrentPos ; загружаем текущее положение курсора CPI R16,LCD_MEM_WIDTH/2+1 ; проверяем, на какой строке он находится BRMI key12_exit ; если на первой, то выходим SUBI R16,LCD_MEM_WIDTH/2 ; иначе переходим на строку выше STS CurrentPos,R16 ; сохраняем текущее положение в RAM STS KeyFlagLong,R0 ; обнуляем информацию о последней нажатой кнопки STS PushCount,R0 ; и счетчик нажатий key12_exit: RET ; курсор вниз key16: LDS R16,CurrentPos ; загружаем текущее положение курсора CPI R16,LCD_MEM_WIDTH/2 ; проверяем, на какой строке он находится BRPL key16_exit ; если на второй, то выходим LDI R17,LCD_MEM_WIDTH/2 ; иначе переходим на строку ниже ADD R16,R17 STS CurrentPos,R16 ; сохраняем текущее положение в RAM STS KeyFlagLong,R0 ; обнуляем информацию о последней нажатой кнопки STS PushCount,R0 ; и счетчик нажатий key16_exit: RET ; курсор влево key13: LDS R16,CurrentPos ; загружаем текущее положение курсора DEC R16 ; уменьшаем его на единицу STS CurrentPos,R16 ; и сохраняем где был STS KeyFlagLong,R0 ; обнуляем информацию о последней нажатой кнопки STS PushCount,R0 ; и счетчик нажатий key13_exit: RET ; курсор вправо key15: LDS R16,CurrentPos ; загружаем текущее положение курсора INC R16 ; увеличиваем его на единицу STS CurrentPos,R16 ; и сохраняем где был STS KeyFlagLong,R0 ; обнуляем информацию о последней нажатой кнопки STS PushCount,R0 ; и счетчик нажатий key15_exit: RET |
Ну и самое главное — клавиша пробела:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ;пробел key14: LDS R17,CurrentPos ; загружаем текущее положение курсора INC R17 ; увеличиваем его на единицу STS CurrentPos,R17 ; и сохраняем где был LDI ZL,low(LCDMemory*2) ; грузим адрес таблицы видеопамяти LDI ZH,high(LCDMemory*2) ADD ZL,R17 ; складываем смещение (положение курсора) с началом таблицы ADC ZH,R0 ; R0 я держу всегда нулем LDI R16,0x20 ; загружаем в R16 число. В LCD оно означает пустое место, пробел. ST Z,R16 ; сохраняем символ в видеопамяти STS KeyFlagLong,R0 ; обнуляем информацию о последней нажатой кнопки STS PushCount,R0 ; и счетчик нажатий RET |
Снова компилируем, прошиваем программу в контроллер и запускаем. Убеждаемся что все кнопки работают и выполняют свою функцию.
Про антидребезг и защиту от помех
В коментариях к прошлой статье поднялся вопрос об устранении дребезга и защиты такой клавиатуры от помех. Расскажу о том как он реализован тут, и о различных аппаратных и програмных способах его реализаци. Благодаря применению микроядра (операционная система), можно легко организовать програмный антидребезг, путем запуска сканирования клавиатуры через определенные промежутки времени, большие чем длительность дребезга. Таким образом, при улавливании нажатия, мы уходим на его обработку и возвращаемся к сканированию клавиатуры спустя какое-то время, когда дребезг от данного нажатия уже кончился. Таким образом отсутствует возможность из-за дребезга посчитать одно нажатие дважды. Но тут вылазиет другая проблема: защита от помех.
DVF января 4, 2011 at 5:51
У применения как программного, так и аппаратного антидребезга есть еще одна важная сторона — это испытание системы на помехоустойчивость. Ведь, если не дребезг, то помеху можно принять за “сработку” в момент сканирования. Для любительской практики это может и не важно, а для тех, кто профессионально занят в разработках — напротив. Подтянутая линия входа от кнопки потенциальный источник сбоя, нежели выход логики.
Действительно, случайная помеха может вызвать ложное срабатывание. Способ борьбы с этим очевиден: производить несколько сканирований клавиатуры, и при положительном результате каждого сканирования, считать что было нажатие. Это, естественно, увеличивает сложность кода и процессорное время, затрачиваемое на обработку нажатий. Но, как правило, время сканирования много меньше времени програмного антидребезга, поэтому дополнительная обработка не влечет за собой больших трудностей.
Так же существует варианты аппаратной защиты от дребезга с помощью конденсаторов, триггеров и др. Но это уже совсем другая статья.
Всем спасибо за внимание, вопросы и пожелания оставляйте в комментариях.
Файлы:
Не хватает включения курсора и вывода символа над ним.
1. Что-то, уж, редкая микросхемка 74198. Лучше заменить ее на более распространенную 74AC299 (74HC299).
2. Бросились в глаза болтающиеся входы микросхем 74198. Но, если это по той же причине, что и отсутствие обвязки МК, то понятно почему.
Можно еще использовать 74HC165. В проекте MIDIBox Клоссе их использует. Принцип — тот же. А то на фотках 155ир13 — они же сожрут больше, чем контроллер с индикатором вместе взятые. А в сериях 555, 1533 этой микросхемы, по моему, нет. Да и габариты…
Да, да.
Еще с первой части у меня вопрос. Как производится защита от того, что во время сдвига байтов при чтении из регистров, пользователь нажмет на кнопку. Тогда мы запишем «0» в передаваемое число, при этом этот «0» никак не будет соответствовать нажатой кнопке. И программа ошибочно подумает, что пользователь нажал на кнопку…
А ничего не будет. Данные отдельно загружаются, отдельно сдвигаются. В тот момент, когда происходит чтение, нажатя ни на что не влияют.
А нафига регистры такие большие (в DIP-24) и малораспространенные?
Все равно ведь половина ног не используется. Есть же более удобные 164(DIP-14), 595 (DIP-16). Везде их полно, стоят копейки…
Извиняюсь, надо было написать не 164 и 595, а 165 (с параллельными входами).
Плохо, что нет редактирования комментов.
Ну в прошлом посте Medik писал, что что было под рукой то и поставил.
Извиняюсь за оффтоп. Пару дней назад я посылал тебе письмо про схему на базе CPLD. Ты его получил?
Сложновато, вообще-то. Те же 16 кнопок можно было бы опрашивать как матрицу 4*4. В теории, для этого надо 8 пинов — 4 входа, и 4 выхода. Однако, можно исхитриться, и в качестве выходов использовать 4 пина, которые уже и так идут на LCD модуль. Остается 4 входа, то есть всего на один пин больше, чем в приведенном решении. Но зато экономим массу места на 2 крупных корпусах. А 155 серия еще и электричества потребляет прилично. По 5-10 мА на корпус.
Эх, 155 серия. РК-86. Во времена были.
Я когда в Киеве был последний раз, несколько горстей этого 155/555/1533-го добра с собой сюда за бугор взял. В макетку повтыкал, часы поднял на ИЕ5 и ИЕ6-х. Кварц на 10МГц до секунды честно делил ИЕ5-ми, никаких МК. :) Правда, все с индикацией в бинарном виде, на голых светодиодах. Дешифратора в 7-сегментов нет, надо наверное на ATtiny24 спрограмить, но только дешифратор :)
для такого гламура надобно брать газоразрядные декадные индикаторы с драйверами на ИД1. гляньте в гугле поиск картинок по запросу tube clock — это просто прекрасно!
Есть для гламура и ИД1 и пачка ИН-12х. С высоковольтным питанием небольшая загвоздка, руки никак не доходят довести до ума. Я чего-то думал, что там ток небольшой совсем, а оказалось по несколько мА на лампу. В сумме это несколько ватт, и оказалось, что у меня дросселя с достаточным током насыщения нет в наличии. Надо заказывать, так что пока на этом все и загнулось.
[quote]Однако, можно исхитриться, и в качестве выходов использовать 4 пина, которые уже и так идут на LCD модуль. Остается 4 входа, то есть всего на один пин больше, чем в приведенном решении.[/quote]
Ну, хитрость тоже потянет за собой компоненты… Хорошо, если кнопки в виде матрицы. А если разбросаны по плате?
Неоспоримый плюс показанного решения — экономия линий. Другой вопрос, где этот плюс насущен.
1. Пульт дистанционного управления, когда нет возможности применить ИК и эфир (недавно столкнулся с этим и дабы не плодить жилы в кабеле, применил этот способ совместно с программным антидребезгом).
2. Ограничение в количестве свободных выводов МК (при условии, что скорость чтения регистров клавиатуры не критична). Места, кстати, много не потребуется, если применить корпуса с шагом 0,65 (0,635).
Для хитрости, из дополнительных компонентов надо только 4 диода, чтобы выходы не коротить если юзер нажмет несколько кнопок одновременно. А матрицу — ее разводить не намного сложнее, чем «паука» от тех регистров.
«Ну в прошлом посте Medik писал, что что было под рукой то и поставил»
Удачно под руку экзотика попалась.
Чаще под рукой шифты работающие на выход.
Можно же и их использовать.
сдвигать постоянно один бит. как только он пролезет через нажатую кнопку, запустит прерывание, которое посмотрит на счётчик его шагов, вот и номер нажатой кнопки!
Шифты придётся использовать с открытым коллектором (а вдруг кто две нажмёт). 595 и 164 отпадают, да и их можно использовать в связке с диодами.
MBI5026 и ей подобные позволят опрос аш на 25мгц. Канечно такое не нужно.
использование сдвиговых позволит ваще по двум проводам управлять — через RC цепочку (была тут такая статья).
Просто не кашерно MBI5026 в такое ввязывать :)
Да и не прокатит — нужен тот драйвер, у которого есть диагностика ошибок MBI5027, MBI5029б MBI5030 и MBI5039.
Ты меня не понял к сожалению. И так уж по детски объясняю…
Мы используем параллельные выходы для определения нажатия, а не инфу с последовательного выхода.
Про кошерность спорить не буду, но скажу что при её стоимости получается 2 рубля за канал. Незнаю скока 595 стоит.
Интерфейс можно упростить до 2 проводов, если использовать какой нибудь счетчик — дешифратор типа К561ИЕ8 и подобные, надо только позаботится об развязке чтобы не было замыкания выходов.
Ди, скажи, сколько лет тебе понадобилось, чтоб стать таким профи в программировании МК?
Здравствуйте. Не могли бы вскинуть сюда еще протеусовский файл. Собрал вроде схему там, привязал к написанному вами авр, но экран просто горит и ничего более.
Буду вам благодарен)
У меня кстати в протеусе тоже не работает. На экране ничего не отображается. Скорее всего глюк протеуса. А причину не удалось выяснить
эх, жаль. если б заработала , было бы шикарно)
Так ни у кого в Протеусе не заработал?) А то у меня тоже косяки(
библиотека для работы с ЛСД в протеусе на пашет( из за этого косяк
Сделал я подобное дело на 74HC165 — парал.вход-послед.выход. Однако что-то не пойму — такое впечатление, что бит Q7 он выставляет ПЕРЕД тактированием. То есть, если я начинаю гнать «клок», и читать биты после переднего фронта — то Q7 у меня пропадает, а все остальное оказывается сдвинутым на один разряд. Если же сначала читаю, а «синхру» потом — то все получается правильно. В даташите в таблице истинности так, по-моему и прописано, а вот на временных диаграммах — как «по классике».