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

Итак, клавиатуру я сделал и написал процедуру сканирующую клавиатурную матрицу 4х4 кнопки. Пора бы рассказать как организовать опрос такой клавы. Напомню, что клава представляет из себя строки, висящие на портах и столбцы, которые сканируются другим портом. Код написан для контроллера ATMega8535, но благодаря тому, что все там указано в виде макросов его можно быстро портировать под любой другой контроллер класса Mega, а также под большую часть современных Tiny. Хотя в случае с Tiny может быть некоторый затык ввиду неполного набора команд у них. Придется чуток дорабатывать напильником.

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

Теперь коротко о файлах:
keyboard_define.inc — файл конфигурации клавиатуры.
В этом файле хранятся все макроопределения используемые клавиатурой. Здесь мы задаем какие ножки микроконтроллера к какой линии подключены. Одна тонкость — выводы на столбцы (сканирующий порт) должны быть последовательным набором линий одного порта. То есть, например, ножки 0,1,2,3 или 4,5,6,7, или 3,4,5,6. Неважно какого порта, главное чтобы последовательно.
С определением ножек, думаю проблем не возникнет, а вот по поводу параметра KEYMASK я хочу рассказать особо.
Это маска по которой будет выделяться сканируемый порт. В ней должны быть 6 единиц и один 0. Ноль выставляется в крайне правую позицию сканирующего порта.

Пример:
У меня сканирующий порт висит на битах 7,6,5,4 крайне правый бит сканирующего порта это бит 4, следовательно маска равна 0b11101111 — ноль стоит на 4й позиции. Если сканирующие линии будут висеть на ножках 5,4,3,2, то маска уже будет 0b11111011 — ноль на второй позиции. Зачем это все будет объяснено ниже.

Также есть маска активных линий сканирующего порта — SCANMSK. В ней единицы стоят только напротив линий столбцов. У меня столбцы заведены на старшую тетраду порта, поэтому сканирующая маска имеет вид 0b11110000.

В разделе инициализации нужно не забыть настроить ножки сканирующего порта на выход, а ноги считывающего на вход с подтяжкой. А потом вставить код обработчика клавиатуры куда-нибудь в виде обычной подпрограммы. Пользоваться просто — вызываем подпрограмму чтения с клавы, а когда возвращаемся у нас в регистре R16 находится скан код клавиши.

Вот так у меня выглядел тестовый код:

Main: SEI ; Разрешаем прерывания.

RCALL KeyScan ; Сканируем клавиатуру
CPI R16,0 ; Если вернулся 0 значит нажатия не было
BREQ Main ; В этом случае переход на начало
RCALL CodeGen ; Если вернулся скан код, то переводим его в
; ASCII код.

MOV R17,R16 ; Загружаем в приемный регистр LCD обработчика
RCALL DATA_WR ; Выводим на дисплей.

RJMP Main ; Зацикливаем все нафиг.

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

Теперь расскажу как работает процедура KeyScan

.def COUNT = R18
KeyScan: LDI COUNT,4 ; Сканим 4 колонки
LDI R16,KEYMASK ; Загружаем маску на скан 0 колонки.
Вначале мы подготавливаем сканирующую маску. Дело в том, что мы не можем вот так взять и гнать данные в порт. Ведь строки висят только на последних четырех битах, а на первых может быть что угодно, поэтому нам главное ни при каких условиях не изменить состояние битов младшей тетрады порта.
KeyLoop: IN R17,COL_PORT ; Берем из порта прежнее значение
ORI R17,SCANMSK ; Выставляем в 1 биты сканируемой части.

Вначале загружаем данные из регистра порта, чтобы иметь на руках первоначальную конфигурацию порта. Также нам нужно выставить все сканирующие биты порта в 1, это делается посредством операции ИЛИ по сканирующей маске. В той части где стояли единицы после операции ИЛИ по маске 11110000 (мое значение SCANMASK) все биты станут единицами, а где был ноль останутся без изменений.
AND R17,R16 ; Сбрасываем бит сканируемого столбца
OUT COL_PORT,R17 ; Выводим сформированный байт из порта.

Теперь мы на сформированный байт накладываем маску активного столбца. В ней вначале ноль на первой позиции, а все остальные единицы. В результате, другие значения порта не изменятся, а вот в первом столбце возникнет 0. Потом маска сдвинется, а вся операция повторится снова. В результате ноль будет уже в следующем столбце и так далее. Таким образом, мы организуем «бегающий» нолик в сканирующем порте, при неизменности других, посторонних, битов порта. А дальше сформированное число загружается в регистр порта и ножки принимают соответствующие уровни напряжений.
NOP ; Задержка на переключение ноги.
NOP
NOP
NOP

SBIS ROW0_PIN,ROW0 ; Проверяем на какой строке нажата
RJMP bt0

SBIS ROW1_PIN,ROW1
RJMP bt1

SBIS ROW2_PIN,ROW2
RJMP bt2

SBIS ROW3_PIN,ROW3
RJMP bt3


Серия NOP нужна для того, чтобы перед проверкой дать ножке время на то, чтобы занять нужный уровень. Дело в том, что реальная цепь имеет некоторое значение емкости и индуктивности, которое делает невозможным мгновенное изменение уровня, небольшая задержка все же есть. А на скорости в 8Мгц и выше процессор щелкает команды с такой скоростью, что напряжение на ноге еще не спало, а мы уже проверяем состояние вывода. Вот я и влепил несколько пустых операций. На 8Мгц все работает отлично. На большую частоту, наверное, надо будет поставить еще штук пять шесть NOP или влепить простенький цикл. Впрочем, тут надо поглядеть на то, что по байтам будет экономичней.
После циклов идет четыре проверки на строки. И переход на соответствующую обработку события.
ROL R16 ; Сдвигаем маску сканирования
DEC COUNT ; Уменьшаем счетчик столбцов
BRNE KeyLoop ; Если еще не все перебрали делаем еще одну итерацию

CLR R16 ; Если нажатий не было возвращаем 0
RET
.undef COUNT

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

bt0: ANDI R16,SCANMSK ; Формируем скан код
ORI R16,0x01 ; Возвращаем его в регистре 16
RET
А вот один из возможных концов при нажатии. Тут формируется скан код который вернется в регистре R16. Я решил не заморачиваться, а как всегда зажать десяток байт и сделать как можно быстрей и короче. Итак, что мы имеем по приходу в этот кусок кода. А имеем мы один из вариантов сканирующего порта (1110,1101,1011,0111), а также знаем номер строки по которой мы попали сюда. Конкретно в этот кусок можно попасть только из первой строки по команде RJMP bt0.
Так давай сделаем скан код из сканирующей комбинации и номера строки! Сказано — сделано! Сначала нам надо выделить из значения порта сканирующую комбинацию — она у нас хранится в регистре R16, поэтому выковыривать из порта ее нет нужды. Продавливаем операцией И значение R16 через SCANMASK и все что было под единичками прошло без изменений, а где были нули — занулилось. Опа, и у нас выведен сканирующий кусок — старший полубайт. Теперь вклеим туда номер строки — операцией ИЛИ. Раз, и получили конструкцию вида [скан][строка]
Вот ее и оставляем в регистре R16, а сами выходим прочь! Также и с остальными строками. Погляди в исходнике, я их не буду тут дублировать.

Декодирование скан кода.
Отлично, скан код есть, но что с ним делать? Его же никуда не приткнуть. Мы то знаем, что вот эта шняга вида 01110001 это код единички, а какой нибудь LCD экран или стандартная терминалка скорчит нам жуткую кракозябру и скажет, нам все что она думает о нашей системе обозначений — ей видите ли ASCII подавай. Ладно, будет ей ASCII.

Как быть? Прогнать всю конструкцию по CASE где на каждый скан код присвоить по ASCII коду меня давит жаба — это же сколько надо проверок сделать! Это же сколько байт уйдет на всю эту тряхомудию? А память у нас не резиновая, жалкие восемь килобайт, да по два байта на команду, это в лучшем случае. Я мог все это сделать прям в обработчике клавиатуры. НЕТ!!! В ТОПКУ!!! Мы пойдем своим путем.
Ок, а что у нас есть в запасе? Метод таблиц перехода не катит, по причине жуткой неупорядоченности скан кодов. Почесал я тыковку, пошарился по квартире… и тут меня осенило. Конечно же!!! Брутфорс!!!

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

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

  1. Здорово это все, только ничего не понял) млин надо учить асм, но с эти позже, вот у меня ничего не работает, может кто подскажет!
    Вот мой код:
    void decodecod (int code)
    {
    unsigned char symbol;
    switch (code)
    {
    case 11: symbol = ‘1’; break;
    case 21: symbol = ‘2’; break;
    case 31: symbol = ‘3’; break;
    case 12: symbol = ‘4’; break;
    case 22: symbol = ‘5’; break;
    case 32: symbol = ‘6’; break;
    case 13: symbol = ‘7’; break;
    case 23: symbol = ‘8’; break;
    case 33: symbol = ‘9’; break;
    case 14: symbol = ‘0’; break;
    case 24: symbol = ‘*’; break;
    case 34: symbol = ‘#’; break;
    default : break;
    }
    UDR = symbol;
    }

    /*Фунуция чиатающих портов*/
    void keyread()
    {
    int byte=0;
    delay_us(400);
    //Читаем линию [1]
    if (PIND.2 == 0)
    {
    byte = port + 1;
    port = 0;
    }

    //Читаем линию [2]
    if (PIND.3 == 0)
    {
    byte = port + 2;
    port = 0;
    }

    //Читаем линию [3]
    if (PIND.4 == 0)
    {
    byte = port + 3;
    port = 0;
    }

    //Читаем линию [4]
    if (PIND.5 == 0)
    {
    byte = port + 4;
    port = 0;
    }
    if (byte > 0)
    {
    decodecod(byte);
    }
    }

    /*Функция сканирующих портов*/
    void keyscan()
    {
    //Сканируем столбик [1]
    PIND.6=0;
    PINB.0=1;
    PINB.1=1;
    port = 30;
    keyread();

    //Сканируем столбик [2]
    PIND.6=1;
    PINB.0=0;
    PINB.1=1;
    port = 20;
    keyread();
    //Сканируем столбик [3]
    PIND.6=1;
    PINB.0=1;
    PINB.1=0;
    port = 10;
    keyread();
    }

    Да знаю, код детский, все можно было сделать по другому, но решил писать в лоб, так как времени мало. Вот почему не работают эти функции. Я нажимаю клавишу, а у меня черти что в UART сыпется, допустим, нажимаю на клавишу 1, а он мне выводит 21222, нажимаю клавишу 4 он 54555 и т.д. это если первый столбец а второй и третий так вообще отказываются работать либо суют в UART символ «я» чаще всего. Зато 0 работает всегда как надо))) Вот боюсь, не в схеме ли дело ((
    P.S. Di Halt собирал по схеме которую тебе прислал на e-mail.

    1. Прогу не читал, т.к. Си читаю плохо. Попробуй протрассировать программу в отладчике. Мне это помогает выловить 99% багов.

      Поставь задержку между сканированием столбцов.

  2. трассировал, там как бы все нормально, на всякий пожарный щаз плату переделываю, ещё мне посоветовали в функции keyscan(), поменять PIN на PORT, сказали якобы PORT изменяет лог, на ноге!?
    Задержку ставил таже картина.

  3. Есть небольшое сомнение.
    Предположим нажата клавиша и мы получили [скан][строка] — на выходе код соответствующего символа.
    Далее будет [скан][строка + 1]. Здесь ничего нет и в r16 ноль.
    Далее будет [скан][строка + 2]. Здесь ничего нет и в r16 ноль.
    Далее будет [скан][строка + 3]. Здесь ничего нет и в r16 ноль.
    Клавиша все еще нажата.
    Дальше опять будет [скан][строка] и в r16 — снова код символа.
    Это будет интерпретироваться «внешней» програмой как многократное нажатие клавиши.
    Или я не прав?

    1. Будет многократным нажатием. Такие вещи надо разруливать уже уровнем выше, на этапе проверки скан кода. И, если надо, игнорировать повторные нажатия.

      В любом случае, у нас же последовательное исполнение кода, с опросом, это не ПЛИС, так что тут нет такого понятия как длительное нажатие. Есть только частое многократное и другого не дано.

      Потому что скан кода FF быть не может. Это запрещенная комбинация. Ну и потому, что я так захотел. =)

  4. Насчет FF — вопрос снят, не доглядел в таблице. Виноват за глупый вопрос.
    А насчет многократного нажатия — честно говоря неудобно. Тяжело отличить снаружи было ли повторное нажатие или нет. Нужна еще информация наружу, а не только r16. Кроме того у Вас на LCD выходит — значит вместо одного символа за нажатие — их будет ооочень много.
    Хотя может я и придираюсь….. =)

    1. Вопрос удобности и неудобности тут не стоит, т.к. задачи могут быть разные. Да и по другому же все равно никак.

      А что мешает добавить обработку одиночного нажатия? Например если скан_код(t)=скан_код(t-1) то пропустить это нажатие. Вклинить это дело перед генерацией скан кода и нет проблем.

      Это же простейший пример, показывающий работу с клавиатурной матрицей. А вывод на ЛСД тут как индикация работы. Можно было и в уарт рыгать. Мне просто влом было провода тащить до компа.

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

    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
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    
    .def	temp	=r16	
    .org 0x0 rjmp main
    .org 0x100
     
    main:
    	ldi temp, 0b11111111
    	out ddrc, temp
     
    	ldi temp, 0x04
    	out SPH, temp
    	ldi temp, 0x5F
    	out SPl, temp
     
    rcall scan
    clt
    rjmp main
     
    ldi temp, 0xff
    out ddrc, temp
     
    .equ	ROW1	=3	;строки вводы
    .equ	ROW2	=2
    .equ	ROW3	=1
    .equ	ROW4	=0
    .equ	COL1	=7	;столбцы вывода
    .equ	COL2	=6
    .equ	COL3	=5	
    .equ	COL4	=4	
     
    .include "m16def.inc"
     
    .def	key	=r17	;к
     
    		ldi temp,0xF0		
    		out DDRA,temp		
    		ldi temp,0x0F		
    		out PORTA,temp		
     
    ;сканирование
    scan:
           	rcall   zad
     
     
    		ldi temp , 0xF0
    		out ddra, temp
    		ldi temp, 0x0F
    		out porta, temp
       nop
       nop
       nop
     
    		sbic PINA,ROW1		;
    		ldi key,           0b00001000		
    		sbic PINA,ROW2
    		ldi key,           0b00000100
    		sbic PINA,ROW3
    		ldi key,           0b00000010
    		sbic PINA,ROW4
    		ldi key,           0b00000001
     
     
    		ldi temp,0x0F		;
    		out DDRA,temp		;
    		ldi temp,0xF0		;
    		out PORTA,temp		;
     
    		rcall   zad
     
    		sbic PINA,COL1		
    		ldi temp,             0b00010000		
    		sbic PINA,COL2
    		ldi temp,             0b00100000
    		sbic PINA,COL3
    		ldi temp,             0b01000000
    		sbic PINA,COL4
    		ldi temp,             0b10000000
     
     
    		add key,temp		;
    		ldi temp,0xF0		;
    		out DDRA,temp		; 
    		ldi temp,0x0F		;
    		out PORTA,temp		;
     
    ;зажигание диодов
     
    out portc, key
    rcall zad
    	ret
     
    	zad:      
    		ldi  temp,$FF    ; задержка
    tt:     dec  temp
    		brne tt
            ldi  temp,$FF    ; 
    tt1:    dec  temp
    		brne tt1
    		ldi  temp,$FF    
    tt2:    dec  temp
    		brne tt2		
    		ret
    1. Всю программу целенаправленно не разбирал, так бегло глянул. Вот что бросилось в глаза:

      1) Не стоит в цикле майн каждый раз инициализировать стек. И каждый раз перенаправлять порты.

      2) Чет не догоняю как ты сделал сканирование? У тебя же только один вывод в порт вначале число 0F потом F0. Где бегающий нолик?

      3) А ты учел емкость и время на переключение? Между сменой активного столбца должно быть около 1мс, между сменой столбца и снятием значения с порта тоже бы выдержку в 1мс сделать. Можно,конечно, и меньше, но лишь после того как будет все проверено и алгоритм без ошибок окажется.

      1. Сканирование осуществлено следующим образом:
        -сначала сканируется строка и в зависимости от номера присеваиваться значение
        типа 0b00000100
        -потом столбец, значение 0b01000000
        -потом идет операция сложение двух регистров столбца и строки в результате имеем код клавиши вида 0b01000100

        Время не учел, сейчас попробую исправить.
        А про емкость не понял. Емкость чего ?

        П.С. Большое спасибо за ответ.

        1. Строки и столбцы нельзя считывать по отдельности. Результат будет непредсказуем. В матрице строки замыкаются на столбцы. ПОэтому мы на столбцах задаем уровень (обычно низкий), а на строках этот уровень считываем по считанной строке опеределяя что нажато.

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

          У тебя же идет считывание сначала со столбца, потом со строки. В результате — чушь. Т.к. уровень ничем не задан.

          2) Имелась ввиду емкость проводов, дорожек и самих выводов. Она небольшая, но есть. И на больших скоростях влияет очень сильно

          1. изменив значение PORTA на FF и добавив задержку в 1мс программа все таки заработала но глючит, строку определяет через раз, а столбец четко.

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

            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
            82
            83
            84
            85
            86
            87
            88
            89
            90
            91
            92
            93
            94
            
            .include "m16def.inc"
            .def temp = R16
            .def key = R17
             
            .equ  KEY_1 = 0b11111110;     
            .equ  KEY_2 = 0b11111100
            .equ  KEY_3 = 0b11111000
            .equ  KEY_4 = 0b00001111
            .equ  KEY_5 = 0b00011100
            .equ  KEY_6 = 0b00000111
            .equ  KEY_7 = 0b11100010
            .equ  KEY_8 = 0b00111111
            .equ  KEY_9 = 0b00001111
            .equ  KEY_0 = 0b00000000
            .equ  KEY_10 =0b11111111  ;*
            .equ  KEY_11 =0b11100111  ;#
             
            .org 0x0 rjmp main
            .org 0x100
            main:
             
            	 ldi temp, 0b01110000;
            	out DDRA, temp
            	ldi temp, 0b10001111
            	out PORTA, temp	
            	ldi temp, 0b11111111
            	out ddrc, temp
             
            	ldi temp, 0x04
            	out SPH, temp
            	ldi temp, 0x5F
            	out SPl, temp
             
            rcall scan
            clt
            rjmp main
             
            scan:
             
               ldi temp, 0b10011111        ;1столбец
               out porta, temp
               rcall zad 
               sbic pina,0
               ldi  key,                KEY_1
               sbic pina,1
               ldi  key,                KEY_4
               sbic pina,2
               ldi  key,                KEY_7
               sbic pina,3
               ldi  key,                KEY_10
             
            	rcall zad
             
             
               ldi temp,0b10101111    ;2столбец
               out porta, temp
             
               rcall zad
             
               sbic pina,0
               ldi  key,                 KEY_2
               sbic pina,1
               ldi  key,                 KEY_5
               sbic pina,2
               ldi  key,                 KEY_8
               sbic pina,3
               ldi  key,                 KEY_0     
             
            		rcall zad					
             
               ldi temp,0b11001111       ;3столбец
               out porta, temp
               	rcall zad
               sbic pina,0
               ldi  key,            KEY_3
               sbic pina,1
               ldi  key,            KEY_6
               sbic pina,2
               ldi  key,            KEY_9
               sbic pina,3
               ldi  key,            KEY_11  
             
            	rcall zad
             
            ;зажигание диодов
            out portc, key
             
            ret
             
            zad:      
            		ldi  temp,$FF    ; задержка 1 вариант
            tt:     dec  temp
            		brne tt
            		ret
            1. Не догоняю как оно вообще может работать. Покажи схему матрицы.

              У тебя одновременно по столбцу зажигаются два нуля. Соответственно нельзя определить на каком именно столбце идет нажатие.

              1. в первом случае стандартная матрица 4Х4, все ножки PORTA посажены на землю через резистор в 1КОм а между резистором и МК уже подключаться клавиатура.
                Во втором случаи матрица 3Х4 подключена практически также как в первом случаи за исключением резисторов на столбцах (на столбцах их нет)..

                А почему все таки если вынести инициализацию стека и портов из цикла маин все перестает работать?
                Если вынести инициализацию стека то вроде понятно, там возвращение из подпрограммы zad выполняться неправильно…
                а вот с портами вообще непонятно…

                1. Смотри что мне не нравится:
                  ldi temp, 0b1001 1111 ;1столбец
                  out porta, temp

                  ***

                  ldi temp,0b1010 1111 ;2столбец
                  out porta, temp

                  ***
                  ldi temp,0b1100 1111 ;3столбец
                  out porta, temp

                  Если учесть, что младшие биты это у тебя строки, то сканирование столбцов должно быть вида:
                  1110 1111 первый столбец
                  1101 1111 второй столбец
                  1011 1111 третий столбец
                  0111 1111 четвертый столбец

                  Почему у тебя там по два нуля?

                  1. 7 пин не задействован т.к. клавиатура 3х4. а сканирование идет не по 0 а по 1..
                    мне все таки непонятно, почему при инициализации портов вне цикла маин программа не работает. а в цикле маин все прекрасно работает
                    в чем может быть косяк?

                    1. include «m16def.inc»

                      .def temp = R16

                      .org 0x0 rjmp reset ; стартуем с этой метки

                      .macro outi ; макрос вывода числа в порт
                      ldi temp, @1
                      out @0, temp
                      .endm

                      .org 0x100

                      reset:
                      outi ddrc, 0b11111111

                      outi SPH, 0x04 ; почему не high(RAMEND) ?
                      out SPL, 0x5F ; а тут low(RAMEND)
                      loop: ; ну а тут тот самый цикл
                      rcall scan
                      clt
                      rjmp loop

  6. здравствуйте, давно не был)) прошу свежего стороннего взгляда на код, т.к. запутался, моск клинит уже сильно, может, и не там вообще смотрю… :/
    или я недопонял основные принципы сканирования, т.к. в аврстудии вместе с хапсимовской клавой происходит чушь: kbdval при ненажатой кнопке !=0. получается, что вывод в PORT не меняет состояние PIN при ноге, сконфигуренной как выход?

    кусок кода, где происходит непосредственно сканирование всей матрицы 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
    
    scan_loop:
    in   kbdval, portA
    mov  scanval, cval	;копирование счётчика в маску
    swap scanval		;сдвиг на тетраду влево (перестановка тетрад)
    ori  scanval, 0b00001111;маска для бегущего нуля
    ori  kbdval,  0b11110000;маска для значения строк
    and  kbdval,  scanval	;старое значение строк+новое бегущего нуля
    out  portA,   kbdval	;вывод в порт
    rcall delay				;задержка на переключение ножек
    nop
    nop
    nop
    nop
    in   kbdval, pinA		;формируем скан-код из кода строки
    ori  kbdval, 0b11110000	;сбрасываем биты колонок
    com  kbdval				;инвертируем, получаем !=0 при нажатой кнопке
    cpi  kbdval, 0x00		;сравниваем с нулём
    brne kboff				;если !=0 (нажатие), то выходим из цикла
     
    lsl  cval				;двигаем единицу в счётчике
    cpi  cval, 0b00010000	;сравниваем положение счётной единицы
    breq kboff				;если 5-й шаг, то выходим
    rjmp scan_loop			;иначе прыгаем в начало цикла
    kboff: ret
    1. косяк тут:

      1
      2
      3
      4
      5
      6
      
      ;начальные настройки
      ;настройка portA (клавиатура)
      ldi    R16, 0xF0 ;старшие 4 бита - выходы
      out    DDRA,R16
      com    R16       ;подтяжка на младшие 4 бита (инверсия регистра)
      out    PORTA,R16

      чтобы раз и навсегда: как правильно сконфигурить вход с подтяжкой, выход, куда писать и откуда читать?

      1. Эмм.. а в чем проблема то?

        Хочешь записать в порт — пишешь в PORT
        Хочешь считать читаешь из PIN
        DDR — вход/выход.

        КОнфиг с подтяжкой DDR=0, PORT=1
        Читаешь из PIN

          1. Ну а чо ты хотел от второй бета версии третьего альфа релиза кустарной поделки? Хапсим ваще не очень стабильная штука. Что либо сложное на нем довольно сложно отлаживать. ПРавь все в железе.

            1. так там же просто модуль клавы да пара светодиодных… светодиоды даже необязательны — вставать по брейкпоинтам. может, хоть Шпрот 7-й вканает?..
              не успеваю банально, опять дотянул… :/

              1. Прогони вручную в студии. Предварительно, когда ноги встанут на подтяг протыкай сам вручную PIN’ы на то как они должны встать при этом. Т.к. студия при внутреннем подтяге пины не трогает, это надо учитывать! По идее хапсим это должен делать сам, но бывает лажает. Либо ты что то не то сконфигурировал там.

                Шпрот 7.. хз, я его сто лет не юзал.

                1. Попробовал запустить в протеусе переделанный на мегу8 аппноут avr 240 со слип-модом — сразу начинает мигать светодиод, который должен был светиться после нажатия «0».

                  Может я чего не заметил — помогите разобраться.
                  Версии:

                  слипмод требует внешнего кварца или RC
                  слипмод не работает в протеусе.

                  ;**** A P P L I C A T I O N N O T E A V R 2 4 0 ************************
                  ;*
                  ;* Title: 4×4 keypad, wake-up on keypress
                  ;* Version: 1.2
                  ;* Last Updated: 2004.11.11
                  ;* Target: All AVR Devices
                  ;*
                  ;* Support E-mail: avr@atmel.com
                  ;*
                  ;* DESCRIPTION
                  ;* This Application note scans a 4 x 4 keypad and uses sleep mode
                  ;* causing the AVR to wake up on keypress. The design uses a minimum of
                  ;* external components. Included is a test program that wakes up the AVR
                  ;* and performs a scan when a key is pressed and flashes one of two LEDs
                  ;* the number of the key pressed. The external interrupt line is used for
                  ;* wake-up. The example runs on the AT90S1200 but can be any AVR with suitable
                  ;* changes in vectors, EEPROM and stack pointer. The timing assumes a 4 MHz clock.
                  ;* A look up table is used in EEPROM to enable the same structure to be used
                  ;* with more advanced programs e.g ASCII output to displays.
                  ;***************************************************************************

                  ;***** Register used by all programs
                  ;******Global variable used by all routines

                  .def temp =r16 ;general scratch space

                  ;Port B pins

                  .equ ROW1 =3 ;keypad input rows
                  .equ ROW2 =2
                  .equ ROW3 =1
                  .equ ROW4 =0
                  .equ COL1 =7 ;keypad output columns
                  .equ COL2 =6
                  .equ COL3 =5
                  .equ COL4 =4

                  ;Port D pins

                  .equ GREEN =0 ;green LED
                  .equ RED =1 ;red LED
                  .equ INTR =2 ;interrupt input

                  .include «M8def.inc»

                  ;***** Registers used by interrupt service routine

                  .def key =r17 ;key pointer for EEPROM
                  .def status =r21 ;preserve sreg here

                  ;***** Registers used by delay subroutine
                  ;***** as local variables

                  .def fine =r18 ;loop delay counters
                  .def medium =r19
                  .def coarse =r20

                  ;*****Look up table for key conversion******************************
                  .eseg ;EEPROM segment
                  .org 0

                  .db 1,2,3,15,4,5,6,14,7,8,9,13,10,0,11,12
                  ;****Source code***************************************************
                  .cseg ;CODE segment
                  .org 0
                  rjmp reset ;Reset handler
                  rjmp scan ;interrupt service routine
                  reti ;unused timer interrupt
                  reti ;unused analogue interrupt

                  ;*** Reset handler **************************************************
                  reset:

                  cli ;disable global interrupts
                  ldi r16, high(RAMEND)
                  out SPH, r16 ; Set Stack Pointer to top of RAM
                  ldi r16, low(RAMEND)
                  out SPL, r16
                  ldi temp,0xFB ;initialise port D as O/I
                  out DDRD,temp ;all OUT except PD2 ext.int.
                  ldi temp,0x30 ;turn on sleep mode and power
                  out MCUCR,temp ;down plus interrupt on low level.
                  ldi temp,0x40 ;enable external interrupts
                  out GIMSK,temp
                  sbi ACSR,ACD ;shut down comparator to save power
                  main:
                  ldi temp,0xF0 ;initialise port B as I/O
                  out DDRB,temp ; 4 OUT 4 IN
                  ldi temp,0x0F ;key columns all low and
                  out PORTB,temp ;active pull ups on rows enabled
                  ldi temp,0x07 ;enable pull up on PD2 and
                  out PORTD,temp ;turn off LEDs
                  sei ;enable global interrupts ready
                  sleep ;fall asleep
                  rcall flash ;flash LEDs for example usage
                  ldi temp,0x40
                  out GIMSK,temp ;enable external interrupt
                  rjmp main ;go back to sleep after keyscan

                  ;****Interrupt service routine***************************************
                  scan:
                  in status,SREG ;preserve status register
                  sbis PINB,ROW1 ;find row of keypress
                  ldi key,0 ;and set ROW pointer
                  sbis PINB,ROW2
                  ldi key,4
                  sbis PINB,ROW3
                  ldi key,8
                  sbis PINB,ROW4
                  ldi key,12
                  ldi temp,0x0F ;change port B I/O to
                  out DDRB,temp ;find column press
                  ldi temp,0xF0 ;enable pull ups and
                  out PORTB,temp ;write 0s to rows
                  rcall settle ;allow time for port to settle
                  sbis PINB,COL1 ;find column of keypress
                  ldi temp,0 ;and set COL pointer
                  sbis PINB,COL2
                  ldi temp,1
                  sbis PINB,COL3
                  ldi temp,2
                  sbis PINB,COL4
                  ldi temp,3
                  add key,temp ;merge ROW and COL for pointer
                  ldi temp,0xF0 ;reinitialise port B as I/O
                  out DDRB,temp ; 4 OUT 4 IN
                  ldi temp,0x0F ;key columns all low and
                  out PORTB,temp ;active pull ups on rows enabled
                  out SREG,status ;restore status register

                  ldi temp,0x00
                  out GIMSK,temp ;disable external interrupt
                  ;have to do this, because we’re
                  ;using a level-triggered interrupt

                  reti ;go back to main for example program

                  ;***Example test program to flash LEDs using key press data************

                  flash: out EEARL,key ;address EEPROM
                  sbi EECR,EERE ;strobe EEPROM
                  in temp,EEDR ;set number of flashes
                  tst temp ;is it zero?
                  breq zero ;do RED LED
                  green_flash:
                  cbi PORTD,GREEN ;flash green LED ‘temp’ times
                  rcall delay
                  sbi PORTD,GREEN
                  rcall delay
                  dec temp
                  brne green_flash
                  exit: ret
                  zero: ldi temp,10
                  flash_again: cbi PORTD,RED ;flash red LED ten times
                  rcall delay
                  sbi PORTD,RED
                  rcall delay
                  dec temp
                  brne flash_again
                  rjmp exit

                  ;****Time Delay Subroutine for LED flash*********************************
                  delay:
                  ldi coarse,8 ;triple nested FOR loop
                  cagain: ldi medium,255 ;giving about 1/2 second
                  magain: ldi fine,255 ;delay on 4 MHz clock
                  fagain: dec fine
                  brne fagain
                  dec medium
                  brne magain
                  dec coarse
                  brne cagain
                  ret

                  ;***Settling time delay for port to stabilise******************************
                  settle:
                  ldi temp,255
                  tagain: dec temp
                  brne tagain
                  ret

                  1. заменил команду слип на rjmp main — светодиод больше не светится — все ок — это слип не работает в протеусе.

                    Чем же можно потестить прошивку с учетом слип?

                    1. Гхм, а в железе собрать не айс? Или тебе надо замерить потребление? Думаю микроамперметр покаже что проц ушел в слип.

  7. ПРИВЕТ. ЧЕЛОВЕК В МИКРОКОНТРОЛЛЕРАХ Я НАЧИНАЮЩИЙ, ТАК ЧТО СТРОГО НЕ СУДИТЕ…А ВОПРОС ТАКОЙ:
    «Одна тонкость — выводы на столбцы (сканирующий порт) должны быть последовательным набором линий одного порта. То есть, например, ножки 0,1,2,3 или 4,5,6,7, или 3,4,5,6. Неважно какого порта, главное чтобы последовательно.».

    СО СТОЛБЦАМИ ЯСНО, А ВОТ СТРОКИ МОГУТ СИДЕТЬ НА PD3,PD6? (КЛАВА НА 6 КНОПОК, 2 СТРОКИ)КАМЕНЬ — МЕГА 16…СТОЛБЦЫ КАК И СКАЗАНО: РС2-РС4

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

    1. Видел подобную в телевизоре, только вместо стабилитронов были резисторные делители. Использовалось 2 провода и общий для шести кнопок. Так вот, очень не стабильная штука — много ложных срабатываний, достаточно сухим пальцем взяться за плату… Хозяева телека протёрли его влажной тряпкой, после чего начала сама добавляться громкость и переключаться каналы.

  9. Уважаемые знатоки! Подскажите в чем может быть затык…
    Матрица 4х2, в студии все отрабатывается правильно, запускаю в Протеусе — начинает тупить. То есть вроде бы все нормально срабатывает но жутко медленно начинает работать программа. За минуту (реального времени) протеус отсчитывает 0,01-0,018 секунды. В чем может быть проблема? Глюк самого протеуса, глюк модели, мои кривые ручки???
    Вот код:

    Skan_cod:

    ldi temp2,0b00001111 ; маска для клавиатуры
    in temp1,ButtonPin ; копируем состояние порта С
    and temp1,temp2 ; выделяем биты клавиатуры
    eor temp1,temp2 ; определяем старший полубайт
    mov button,temp1 ; сохраняем в регистр кнопок
    swap button ; меняем местами тетрады

    sbi ButtonPort,str1 ; переключаем скан-бит
    cbi ButtonPort,str2 ;

    nop ; ожидаем переключения пинов
    nop ;
    nop ;
    nop ;
    nop ;
    nop ;
    nop ;
    nop ;

    in temp1,ButtonPin ; копируем состояние порта С
    and temp1,temp2 ; выделяем биты клавиатуры
    eor temp1,temp2 ; определяем младший полубайт
    add button,temp1 ; сохраняем в регистр кнопок

    sbi ButtonPort,str2 ; переключаем скан-бит
    cbi ButtonPort,str1 ;

    cpi button,0 ; клавиша нажата?
    breq PC+3 ; не нажата -> перейти
    sbr flag_reg,$01 ; нажата -> установить признак наж. клавиши
    ret ; выйти

    cbr flag_reg,$01 ; не нажата -> сбросить признак наж. клавиши
    ret ; выйти

    Контроллер Мега8; сканирующие столбцы — PC4-PC5; сканируемые строки — PC0-PC3.

    Заранее ПАСИБА!!!

    1. вживую прогони и узнаешь. Мало ли чего протеус там себе тупит. Я им вообще не пользуюсь.

  10. Опять же… для клавиатур 4х4 есть вполне достойное недорогое аппаратное решение. Микруха 74С922. Она сама сканит клаву. Как замечает нажатую клавишу, опускает ногу «фас» и выдаёт 4-х битный код нажатой клавиши. Можно повесить ентот «фас» на прерывание по смене уравня (по-моему работает в лог «0», как того требует 8051-подобный комп, а не как AVR в «1»), а потом читать данные с шины. Более подробно в даташит.

  11. А как посчитать кол-во байт? Может у меня и короче сканирование клавы вышла. Уж проще и понятней, так точно.. ))) Твой вариант явно не для начинающих — много сложных слов..)))
    Кстати, не увидел я защиты от дребезга. Не досмотрел или нету ?

  12. Не заметил сразу — а зачем надо код кнопки отображать на ЖКИ ? Это текстовая клавиатура, что ли? Да нет же, вроде — 4х4 кнопки всего..

  13. Мне вот жутко интересно. А где в этом примере обрабатывается удержание, автоповтор, комбниции клавиш….

        1. Можно, но пока есть вещи поважней. Да и к клавиатуре это уже не особо относится. Такие вещи выносятся на уровень диспетчера. Т.к. потребуются флаги и таймеры.

          1. Как ты думаешь, нужно ли для кнопок делать битовое поле? То есть, если клавиатура 4х4, то получается 16 битов. Я поспорил. Считаю, что достаточно кода полученного с матрицы. 0b01110111 это верхняя левая кнопа. Если нажать две три кнопы, то все равно можно понять, какие кнопы нажаты. Например 0b00110011 это нажаты две верхние левые кнопы. Маски 0b01110111
            и 0b10111011. Соответственно верхняя самая левая и вехняя вторая слева кнопы.
            К чему весь этот спич. Я считаю не целесообразным делать отдавать 8 байтов, если клава будет 8х8. Соответственно раздуется код обработки 64 битов.

            1. Очень сильно зависит от реализации. И того чего ты хочешь достичь. Если надо отслеживать нажатие клавиши, длительное нажатие и тыды, то битовое поле возможно необходимо. Если же просто опрос, то дублировать клавиатуру в памяти смысла нет. Но опять же от реализации зависит. Если клава обрабатывается в прерывании, а задача клавиатуры по диспетчеру ядра, то битовое поле нужно, для записи скан кода.

  14. а как насчет такого варианта сканирования матрицы кнопок:
    1. на строках выставили 0. столбцы подтянули к питанию. прочитали столбцы.
    2. на столбцах выставили 0. строки подтянули к питанию. прочитали строки.

    получили скан код.

  15. Уважаемый DI HALT!!! Прежде всего хочу выразить благодарность за интереснейшую статью, в которой простым и понятным (даже для такого тугодума, как я) языком описана процедура сканирования клавиатуры, а также предоставлен код с дельными комментами. Но мне, как всё-таки в большей степени схемотехнику, для полноты картины хотелось бы взглянуть принципиальную электрическую схему сего устройства, если конечно это возможно.

  16. Заметил в программе одну деталь если выставить в настройка выводы на столбцы следующую последовательность
    .equ COL0_Port = PORTC
    .equ COL0_Pin = PINC
    .equ COL0_DDR = DDRC
    .equ COL0 = 0

    .equ COL1_Port = PORTC
    .equ COL1_Pin = PINC
    .equ COL1_DDR = DDRC
    .equ COL1 = 1

    .equ COL2_Port = PORTC
    .equ COL2_Pin = PINC
    .equ COL2_DDR = DDRC
    .equ COL2 = 2

    .equ COL3_Port = PORTC
    .equ COL3_Pin = PINC
    .equ COL3_DDR = DDRC
    .equ COL3 = 3

    .equ KEYMASK = 0b11111110
    .equ SCANMSK = 0b00001111

    то подпрограмме Keyscan
    при операции
    ROL R16
    произоидет следующее
    до операции R16=11111110
    а после 11111100
    что по сути будет ошибкой при наложении маски
    AND r17,r16
    Необходимо перед ROL R16
    ставить операцию SEC установка флага С
    И еще для удобства записи в настройках я предложил бы вместо
    .equ KEYMASK = 0b11111110
    .equ SCANMSK = 0b00001111
    такую запись
    .equ KEYMASK = 0xFF^1<<COL0
    .equ SCANMSK = 1<<COL3|1<<COL2|1<<COL1|1<<COL0

  17. И еще одна деталь в подпрограмме Codegen процедура
    CPI R16,0 ; Если ноль,
    BREQ CG_Exit ; то выходим
    Лишняя так как R16 по условию не может быть равен 0 так как до этого мы его проверяли на 0
    CPI R16,0
    BREQ Main

  18. В архиве лежат файлы, там в keyboard_define определения ROW_PIN0…3(и ROW_PORT) то на порт А, то на порт С — это просто строки по факту повешены на ноги и от С и от А вразнобой?

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

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha