Подключение клавиатуры к МК по трем проводам на сдвиговых регистрах. Часть 2. Буквенный ввод как на телефоне

Данная статья является продолжением предыдущей о подключении клавиатуры к МК с помощью трех сигнальных проводов. В этой часте я расскажу вам о том, как увеличить число кнопок на клавиатуре до 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 байта, со сдвиговых регистров.

Общий план действий

  1. Устанавливаем бит T в регистре SREG. (Это пользовательский бит, который можно использовать для любых нужд. В нашем случае установленный бит будет означать, что мы считываем первый байт с нашей клавиатуры, если при проверке этот бит будет сброшен, то будем считать, что действие происходит со считыванием второго байта).
  2. В цикле считываем 8 бит из сдвигового регистра.
  3. Проверяем бит 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
У применения как программного, так и аппаратного антидребезга есть еще одна важная сторона — это испытание системы на помехоустойчивость. Ведь, если не дребезг, то помеху можно принять за “сработку” в момент сканирования. Для любительской практики это может и не важно, а для тех, кто профессионально занят в разработках — напротив. Подтянутая линия входа от кнопки потенциальный источник сбоя, нежели выход логики.

Действительно, случайная помеха может вызвать ложное срабатывание. Способ борьбы с этим очевиден: производить несколько сканирований клавиатуры, и при положительном результате каждого сканирования, считать что было нажатие. Это, естественно, увеличивает сложность кода и процессорное время, затрачиваемое на обработку нажатий. Но, как правило, время сканирования много меньше времени програмного антидребезга, поэтому дополнительная обработка не влечет за собой больших трудностей.

Так же существует варианты аппаратной защиты от дребезга с помощью конденсаторов, триггеров и др. Но это уже совсем другая статья.
Всем спасибо за внимание, вопросы и пожелания оставляйте в комментариях.

Файлы:

28 thoughts on “Подключение клавиатуры к МК по трем проводам на сдвиговых регистрах. Часть 2. Буквенный ввод как на телефоне”

  1. 1. Что-то, уж, редкая микросхемка 74198. Лучше заменить ее на более распространенную 74AC299 (74HC299).
    2. Бросились в глаза болтающиеся входы микросхем 74198. Но, если это по той же причине, что и отсутствие обвязки МК, то понятно почему.

    1. Можно еще использовать 74HC165. В проекте MIDIBox Клоссе их использует. Принцип — тот же. А то на фотках 155ир13 — они же сожрут больше, чем контроллер с индикатором вместе взятые. А в сериях 555, 1533 этой микросхемы, по моему, нет. Да и габариты…

  2. Еще с первой части у меня вопрос. Как производится защита от того, что во время сдвига байтов при чтении из регистров, пользователь нажмет на кнопку. Тогда мы запишем «0» в передаваемое число, при этом этот «0» никак не будет соответствовать нажатой кнопке. И программа ошибочно подумает, что пользователь нажал на кнопку…

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

  3. А нафига регистры такие большие (в DIP-24) и малораспространенные?
    Все равно ведь половина ног не используется. Есть же более удобные 164(DIP-14), 595 (DIP-16). Везде их полно, стоят копейки…

    1. Извиняюсь, надо было написать не 164 и 595, а 165 (с параллельными входами).
      Плохо, что нет редактирования комментов.

  4. Сложновато, вообще-то. Те же 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. для такого гламура надобно брать газоразрядные декадные индикаторы с драйверами на ИД1. гляньте в гугле поиск картинок по запросу tube clock — это просто прекрасно!

      1. Есть для гламура и ИД1 и пачка ИН-12х. С высоковольтным питанием небольшая загвоздка, руки никак не доходят довести до ума. Я чего-то думал, что там ток небольшой совсем, а оказалось по несколько мА на лампу. В сумме это несколько ватт, и оказалось, что у меня дросселя с достаточным током насыщения нет в наличии. Надо заказывать, так что пока на этом все и загнулось.

    2. [quote]Однако, можно исхитриться, и в качестве выходов использовать 4 пина, которые уже и так идут на LCD модуль. Остается 4 входа, то есть всего на один пин больше, чем в приведенном решении.[/quote]
      Ну, хитрость тоже потянет за собой компоненты… Хорошо, если кнопки в виде матрицы. А если разбросаны по плате?

      Неоспоримый плюс показанного решения — экономия линий. Другой вопрос, где этот плюс насущен.
      1. Пульт дистанционного управления, когда нет возможности применить ИК и эфир (недавно столкнулся с этим и дабы не плодить жилы в кабеле, применил этот способ совместно с программным антидребезгом).
      2. Ограничение в количестве свободных выводов МК (при условии, что скорость чтения регистров клавиатуры не критична). Места, кстати, много не потребуется, если применить корпуса с шагом 0,65 (0,635).

      1. Для хитрости, из дополнительных компонентов надо только 4 диода, чтобы выходы не коротить если юзер нажмет несколько кнопок одновременно. А матрицу — ее разводить не намного сложнее, чем «паука» от тех регистров.

  5. «Ну в прошлом посте Medik писал, что что было под рукой то и поставил»
    Удачно под руку экзотика попалась.

    Чаще под рукой шифты работающие на выход.
    Можно же и их использовать.
    сдвигать постоянно один бит. как только он пролезет через нажатую кнопку, запустит прерывание, которое посмотрит на счётчик его шагов, вот и номер нажатой кнопки!

    Шифты придётся использовать с открытым коллектором (а вдруг кто две нажмёт). 595 и 164 отпадают, да и их можно использовать в связке с диодами.
    MBI5026 и ей подобные позволят опрос аш на 25мгц. Канечно такое не нужно.
    использование сдвиговых позволит ваще по двум проводам управлять — через RC цепочку (была тут такая статья).

        1. Ты меня не понял к сожалению. И так уж по детски объясняю…

          Мы используем параллельные выходы для определения нажатия, а не инфу с последовательного выхода.

  6. Интерфейс можно упростить до 2 проводов, если использовать какой нибудь счетчик — дешифратор типа К561ИЕ8 и подобные, надо только позаботится об развязке чтобы не было замыкания выходов.

  7. Здравствуйте. Не могли бы вскинуть сюда еще протеусовский файл. Собрал вроде схему там, привязал к написанному вами авр, но экран просто горит и ничего более.
    Буду вам благодарен)

    1. У меня кстати в протеусе тоже не работает. На экране ничего не отображается. Скорее всего глюк протеуса. А причину не удалось выяснить

  8. Сделал я подобное дело на 74HC165 — парал.вход-послед.выход. Однако что-то не пойму — такое впечатление, что бит Q7 он выставляет ПЕРЕД тактированием. То есть, если я начинаю гнать «клок», и читать биты после переднего фронта — то Q7 у меня пропадает, а все остальное оказывается сдвинутым на один разряд. Если же сначала читаю, а «синхру» потом — то все получается правильно. В даташите в таблице истинности так, по-моему и прописано, а вот на временных диаграммах — как «по классике».

Добавить комментарий для BigLeha Отменить ответ

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

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