AVRASM: «CeleronKeyInputLib» — Библиотека процедур для интеллектуальной обработки событий от Кнопок и Энкодеров (авторская методика и реализация)

Простая кнопкаМикроконтроллерное устройство может работать исключительно в автономном режиме: получать сигналы с датчиков, и выдавать управляющие импульсы, иногда оно ещё взаимодействует с ЭВМ или другими микроконтроллерами… Но большинству микроконтроллерных устройств требуется поддерживать интерфейс с пользователем-человеком: для вывода используются светодиоды или дисплеи, а для ввода — традиционные Кнопки и Энкодеры, редко используются и другие экзотические устройства ввода
В данной работе будут рассматриваться только традиционные инструменты ввода: «цифровые Кнопки / Клавиатуры» и «инкрементальные Энкодеры», поскольку именно они используются почти всегда.

Содержание:

Назначение

Данная реализация «Библиотеки процедур для интеллектуальной обработки ВВОДА» написана на языке ассемблера, для компилятора AVRASM. Соответственно, она предназначена для разработки программных прошивок (firmware) на языке ассемблер, для микроконтроллеров Atmel AVR (8-bit).

Особенности

Среди прочего, библиотека реализует следующие функции:

1) Математическое подавление ошибок, от наведенных электрических помех и дребезга контактов, для всех «физических каналов» подключённых Кнопок и Энкодеров, с заданной и регулируемой величиной помехоустойчивости. (используется оригинальная авторская методика «интегрирующей защёлки»: на двунаправленном инкрементальном счётчике)

2) Сложные поведения реальных кнопок — сводятся к стандартизованным кодам в «Статусных Регистрах» Кнопок, на основании которых можно легко реализовывать очень сложную и гибкую логику поведения устройства:

  • распознаются разные жесты с кнопками: серии нажатий и удержаний кнопок;
  • различаются варианты кнопочных аккордов: одновременное нажатие/удерживание нескольких кнопок;
  • кнопки различается по времени удержания: обычное, «короткое» или «длинное» удержание. А для продвинутых случаев, для наиболее функциональных кнопок, также допустимо различать ситуации: «сколько КОНКРЕТНО удерживается кнопка, в пределах 0..16сек?» и программировать различные реакции…
  • и что особенно полезно для низкоуровневого ассемблерного кода: дана чёткая и прозрачная методика написания прикладного кода обработчиков «реакции на события» (предлагаются макросы для тестирования и обработки статус-кодов кнопок). Также, имеется простая возможность назначать одинаковые обработчики различным альтернативным кнопочным жестам.
  • Простой и понятный механизм «сброса статусных кодов» для уже обработанных кнопок — исключает «побочные эффекты»: типа «залипания» или «ошибочные повторные нажатия» кнопок… (защита от дурака: при правильно организованном коде обработчиков, пользователь может жать на всё подряд — приложение отреагирует чётко и правильно)

3) Для распознавания и обработки кодовой последовательности, поступающей от инкрементального Энкодера по двум физическим каналам — предлагаются три разные реализации кода, обладающие разными функциональными возможностями и подходящие для разных ситуаций (лёгкий и простой код, для хорошего и быстрого железа; или усложнённый код, с коррекцией ошибок, для медленного или сбойного железа).
Разработчик выбирает вариант кода обработки энкодера — только директивами условной компиляции. Вся активность энкодера обрабатывается библиотекой автоматически и сводит все особенности к простому «Счётчику тиков», который затем используется в прикладном коде обработки событий. (Формат счётчика особой специальной структуры не содержит — это просто знаковое целое число: Signed Int = [-128..127]).

4) КОД библиотеки УНИВЕРСАЛЕН и не требует (даже не рекомендует) каких-либо вмешательств и переделок под ваше конкретное устройство!
Единственная процедура, код которой требуется адаптировать к вашей конкретной физической схеме — это KEY_SCAN_INPUT (через неё осуществляется связь с физическими каналами ввода).
Также, под вашу конкретную физическую схему, требуется адаптировать блок определения данных в DSEG (см. в файле ): определения блоков регистров Интегратора, статусных регистров Кнопок и Энкодеров, и некоторые константы.

5) Данная библиотека испытана в реальном физическом устройстве — и зарекомендовала себя как «реально работоспособная», гибкая и эффективная! Пример использования данной библиотеки, реализация клиентского прикладного кода, приведен в файлах: и .

Код и Зависимости

Код библиотеки процедур для интеллектуальной обработки ВВОДА «celeronkeyinputlib.inc» опубликован на GitHub (это веб-сервис для хостинга IT-проектов и их совместной разработки), на условиях лицензии MIT (разрешительной opensource, т.е. практически без ограничений к использованию). Ответвляйтесь!

Используется, и требует подключения, нестандартная внешняя Библиотека базовых Макроопределений «macrobaselib.inc» — расширяющая стандартный набор ассемблерных инструкций микроконтроллеров Atmel AVR (8-bit AVR Instruction Set), и рекомендующая парадигму программирования: с хранением «модели прикладных данных» в ОЗУ и использованием нескольких «временных регистров»…

Примечание: GitHub был выбран для распространения кода — как наиболее прогрессивный, удобный и функциональный метод взаимодействия opensource-разработчиков. Развивайте и дополняйте библиотеку — затем, сможете легко контрибутить…

Ликбез для неискушённых пользователей: те кто не используют системы управления версиями, могут просто скачать архив с кодом: нажав на кнопку «Download ZIP» на странице репозитория GitHub, по ссылкам выше.


Как это работает?

1. Физический канал

Предположим, к вашему микроконтроллеру подключена ЛЮБАЯ кнопка. Это может быть одна или много кнопок. Схемотехнически, Кнопка может быть организована по разному:

Простая кнопка ….. Группа кнопок на Сдвиговом регистре ….. Клавиатура на резистивных делителях

Однако, какой бы ни была схема подключения и мультиплексирования кнопок, в результате сканирования физического канала Кнопки — получаем цифровой код её состояния (принятая в библиотеке кодировка: «0»-кнопка отпущена или «1»-нажата).
Замечу, что если сканируется клавиатура на резистивных делителях, то после преобразования через АЦП и «табличку соответствия напряжений кодам кнопок» — также получаем цифровые коды «нажатых» и «отпущенных» кнопок…

1.1. Код сканирования «физических каналов» программист пишет сам

Замечание! Замечу: код сканирования «физических каналов» находится в процедуре KEY_SCAN_INPUT библиотеки. Эта процедура обеспечивает взаимодействие микроконтроллера с внешней аппаратной периферией ввода. При адаптации библиотеки к своему «железу», программист должен переписать код этой процедуры, с нуля! Таким образом, достигается универсальность и гибкость применения данной библиотеки, адаптивность к любым схемотехническим решениям подключений кнопок и энкодеров, без ограничений! Причём, это единственная процедура, в библиотеке, требующая вмешательства в код. Остальной код математического конвейера — универсален и неизменен, он подвязывается и запускается из этой процедуры KEY_SCAN_INPUT…

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

; Инициализация:
LDI	IntegratorAddressLow,	Low(DInputIntegrator)	; начало блока регистров Интегратора,
LDI	IntegratorAddressHigh,	High(DInputIntegrator)	; где регистр IntegratorAddress = X(R27:R26)
LDI	StatusAddressLow,	Low(DButtonStatus)	; начало блока регистров Кнопок,
LDI	StatusAddressHigh,	High(DButtonStatus)	; где регистр StatusAddress = Y(R29:R28)
LDI	IntegratorLatchDepth,	5			; "глубина защёлки" интегратора

; Сканирование аппаратной периферии:
IN	temp,	PINB
BST	temp,	PinSense				; Прочитать текущее состояние "физического канала" -> в T
; Или, для входных каналов читаемых в инверсной кодировке, следует также добавить безусловную инверсию, чтобы привести сигнал к стандартной кодировке ("0"-отпущена, "1"-нажата):
IN	temp1,	PINB
LDI	temp2,	1<<PinSense
EOR	temp1,	temp2
BST	temp1,	PinSense

; Обработка:
RCALL	KEY_PROCESS_INTEGRATOR				; Проинтегрировать сигнал
RCALL	KEY_UPDATE_BUTTON_STATUS			; Фиксировать статус Кнопки


; Далее, как вариант, можно отработать ещё несколько кнопок:
LD	temp,	X+					; адрес в регистре X++
LD	temp,	Y+					; адрес в регистре Y++
STOREB	PINB,	PinSense_xxx				; Прочитать текущее состояние "физического канала" -> в T
RCALL	KEY_PROCESS_INTEGRATOR				; Проинтегрировать сигнал
RCALL	KEY_UPDATE_BUTTON_STATUS			; Фиксировать статус Кнопки
и т.д.

При обработке Энкодеров — отличия небольшие: нужно помнить, что канала два; и вызывается другой метод Обработки…

; Инициализация:
LDI	IntegratorAddressLow,	Low(DInputIntegrator)	; начало блока регистров Интегратора,
LDI	IntegratorAddressHigh,	High(DInputIntegrator)	; где регистр IntegratorAddress = X(R27:R26)
LDI	StatusAddressLow,	Low(DEncoderStatus)	; начало блока регистров Энкодеров,
LDI	StatusAddressHigh,	High(DEncoderStatus)	; где регистр StatusAddress = Y(R29:R28)
LDI	IntegratorLatchDepth,	2			; "глубина защёлки" интегратора

; Сканирование аппаратной периферии:
STOREB	PINB,	PinSense1				; Прочитать текущее состояние "физического канала" №1 -> в T
RCALL	KEY_PROCESS_INTEGRATOR				; Проинтегрировать сигнал
LD	temp,	X+					; адрес в регистре X++
STOREB	PINB,	PinSense2				; Прочитать текущее состояние "физического канала" №2 -> в T
RCALL	KEY_PROCESS_INTEGRATOR				; Проинтегрировать сигнал
LD	temp,	-X					; адрес в регистре X-- (коррекция: шаг назад)

; Обработка:
RCALL	KEY_UPDATE_ENCODER_STATUS			; Фиксировать статус и счётчик Энкодера


; Далее, как вариант, можно отработать ещё несколько энкодеров:
LD	temp,	X+					; адрес в регистре X++ (здесь: только один шаг вперёд)
LD	temp,	Y+					; адрес в регистре Y++ (здесь: только один шаг вперёд)
STOREB	PINB,	PinSense_xxx1				; Прочитать текущее состояние "физического канала" №1 -> в T
RCALL	KEY_PROCESS_INTEGRATOR				; Проинтегрировать сигнал
LD	temp,	X+					; адрес в регистре X++
STOREB	PINB,	PinSense_xxx2				; Прочитать текущее состояние "физического канала" №2 -> в T
RCALL	KEY_PROCESS_INTEGRATOR				; Проинтегрировать сигнал
LD	temp,	-X					; адрес в регистре X-- (коррекция: шаг назад)
RCALL	KEY_UPDATE_ENCODER_STATUS			; Фиксировать статус и счётчик Энкодера
и т.д.

2. Интегратор канала

Интеграция «сырого» статуса входного канала (процедура математического подавления звона и помех KEY_PROCESS_INTEGRATOR) вызывается после опроса физической кнопки (любой реализации) и получения цифрового кода её текущего статуса: «0» или «1».

В процедуру KEY_PROCESS_INTEGRATOR передаются следующие параметры:

  • Через status bit «T» передаётся «сырой» цифровой код текущего состояния кнопки = «0» или «1».
  • Регистровым параметром «IntegratorAddress» передаётся адрес «регистра интегратора» в памяти (ОЗУ).
  • Регистровым параметром «IntegratorLatchDepth» специфицируется «глубина защёлки» для текущего канала. (Примечание: наличие этого параметра позволяет дифференцированно применять разную «глубину защёлки» для разных «физических каналов». Предполагается, что сигналы от тактовых кнопок и энкодера, например, требуют разных «глубин защёлки» — ввиду их различной динамичности и надёжности…)

2.1. Кодировка входного сигнала

Кодировка входного сигнала:

  • По принятому СТАНДАРТУ, статус входного «физического канала» приходит в следующей кодировке: «0»=(кнопка отпущена) или «1»=(кнопка нажата).
    Примечание: И только так! И не перенастраивается!
  • Рекомендация: А если ваша схемотехническая реализация кнопок возвращает другой, инверсный код? Тогда просто добавьте, в код процедуры KEY_SCAN_INPUT, безусловную математическую XOR-инверсию, после чтения кода из Порта.

Замечание! Вопрос: А зачем так жёстко фиксировать СТАНДАРТ кодировки сигнала? Ведь достаточно изменить только одну инструкцию BRTS/BRTC, чтобы кодировка входного сигнала стала инверсной? Можно было бы настраивать директивой условной компиляции…
Ответ: Для минимизации путаницы — код конвейера обработки кнопок не модифицируется, логика неизменна! Также, допускается, что разные группы Кнопок и Энкодеров, могут возвращать свои статусы в разных кодировках, вперемешку… Гораздо проще, понятнее и универсальнее: добавить безусловные XOR-инверсии к некоторым конкретным «физическим каналам»… Простота -> Ясность -> Выгода!

2.2. Формат «Регистра Интегратора»

(Справка: Расположены в секции DInputIntegrator в файле )

76543210
Snnnnnnn

В целом, регистр хранит одно число signed short int = [-128, 0, +127].
Результат интеграции — отражается в Sign bit (он же Negative Flag «N»), означает: «0»=(кнопка отпущена) или «1»=(кнопка нажата)…

Семантически — это инкрементальный двунаправленный счётчик:

  • Если физический канал (кнопка) возвращает статус «1» (нажата) — то на этой итерации производится декрементация (-1) регистра счётчика.
  • Если физический канал (кнопка) возвращает статус «0» (отпущена) — то на этой итерации производится инкрементация (+1) регистра счётчика.
  • Максимальное, по модулю, значение в регистре счётчика определяется константой «глубины защёлки» (LatchDepth) = [-LatchDepth, 0..+(LatchDepth-1)].
    Причём замечу: здесь, значение =0 включается в статус «кнопка отпущена»!
  • Начальное состояние = 0b00000000. Что означает: примерно среднее неопределённое положение защёлки, но кнопка всё же на стороне «отпущена» (что соответствует обычной ситуации: когда кнопки, конструктивно, «нормально разомкнутые»).

Схема работы Интегратора

2.3. Настройка интегратора: выбор «глубины защёлки»

«Глубина Защёлки» интегратора, по своему действию, аналогична «постоянной времени» помехоподавляющей RC-цепи…

Допустимые значения для регистрового параметра IntegratorLatchDepth = [1..128].

Выбор оптимального значения «глубины защёлки» зависит от разных факторов:

  1. Следует учитывать, по какому событию запускается процедура сканирования «физических каналов» KEY_SCAN_INPUT: по таймеру, периодически или по «Pin change Interrupt (INT0)»?
    • Если по таймеру, периодически — тогда нужно брать IntegratorLatchDepth > больше количества «замеров», за период самой длинной помехи (это требуется замерять экспериментально).
    • Если по изменению уровня на порте ввода-вывода (INT0), то выбор проще: на «тактовую частоту» микроконтроллера можно не смотреть, а выбрать IntegratorLatchDepth=[2..3].
  2. «Глубину защёлки» следует устанавливать и в расчёте на будущий износ кнопок: для плохого железа и лучшей помехозащищённости — требуется бОльшая разрешительная «глубина». Лучше взять значение IntegratorLatchDepth чуть больше, чем отрабатывается на лабораторном стенде (на порядок больше, для худших случаев).
  3. Но следует помнить, что большАя «глубина защёлки» — сильно замедляет реакцию микроконтроллера на события: кнопки становятся значительно «инертнее», а кратковременные нажатия могут быть и вовсе пропущены, что сильно ухудшает эргономику! Также, на медленных микроконтроллерах или при редкой частоте сканирования «физических каналов» — «глубину защёлки» следует брать поменьше.
  4. При опросе контактных групп Энкодера — «глубина защёлки» интегратора, обычно, задаётся на порядок меньше, чем для кнопок! Поскольку контакты Энкодера более стабильны и помехоустойчивы, а кроме того, Энкодер переключается ГОРАЗДО чаще, чем тактовые Кнопки (требуется выше быстродействие).
  5. Замечу: при IntegratorLatchDepth=1 — интегратор, фактически, отключён! При этом, код Интегратора будет отрабатываться, как часть конвейера, и потреблять ресурсы АЛУ, но исправлять ошибки не будет.
  6. Напомню: максимальное значение IntegratorLatchDepth=128. Однако, если микроконтроллер работает слишком быстро, и процедура KEY_SCAN_INPUT запускается слишком часто — то вам, возможно, не хватит этого максимума, для демпфирования долгих помех. Тогда, очевидно, запуск процедуры KEY_SCAN_INPUT следует запланировать пореже…

3. Работа с Кнопками

Всё что происходит с кнопкой — отражается в её «статусном регистре». Этот регистр имеет сложную структуру и семантическое наполнение, кодирует множество ситуаций…

Статус-код кнопки формируют две процедуры: KEY_UPDATE_BUTTON_STATUS (определяет нажатие-отжатие кнопки) и KEY_ENHANCE_TIME_FOR_ALL_BUTTONS (наращивает счётчики «времени удержания» кнопок).

Прикладной код обрабатывает события, опираясь, исключительно, на значения в «статусных регистрах» кнопок. Условие для запуска обработчика может приниматься, анализируя статусы сразу нескольких Кнопок — так появляется возможность различать «аккорды» и кнопки-модификаторы. А наличие в статусном регистре кодов, различающих события: «нажатие», «отжатие», «время удержания», отдельно для каждой кнопки — позволяет довольно гибко и функционально комбинировать начальные условия для запуска обработчика события!

После обработки событий, связанных с кнопкой, прикладной код сбрасывает «регистр статуса», соответствующий кнопке. Имеется также глобальная процедура KEY_RESET_STATUS_FOR_ALL_BUTTONS, которая сбрасывает статусы ВСЕМ кнопкам и энкодерам в системе — вызывается прикладным кодом асинхронно, и используется для подавления «ошибочных реакций» из-за того, что «пользователи давят на всё подряд»…

3.1. Формат «регистра Статуса» Кнопки

(Справка: Расположены в секции DButtonStatus в файле )

76543210
HPTnnnnn

Описание битов «регистра статуса» кнопки:

.equ	BUTTON_HOLDING_TIME	= 0	; Пять битов   DButtonStatus[4:0] = счётчик количества полусекунд, в течение которых Кнопка удержива(лась/ется) "нажатой".	(фиксирует время до 16сек!)
.equ	BUTTON_STATUS_CODE	= 5	; В трёх битах DButtonStatus[7:5] = кодируется итоговый "статус-код кнопки" (см. ниже макроопределения констант).
.equ	BUTTON_HOLDEN_LONG	= 5	; Флаг "времени удержания" кнопки: 0-короткое или 1-длинное.
.equ	BUTTON_IS_PRESSED	= 6	; Флаг "зафиксировано полноценное нажатие кнопки": "0" - кнопка не нажималась, "1" - было нажатие.
.equ	BUTTON_IS_HOLDDOWN	= 7	; Флаг "кнопка удерживается в нажатом состоянии": "0" - сейчас кнопка "отпущена", "1" - сейчас кнопка "нажата и удерживается".

Три бита DButtonStatus[7:5], кодирующие итоговый «статус-код кнопки», могут принимать следующие значения:

.equ	BSC_NotPressed	= 0b000	; "не нажата"		(исходное положение для всех кнопок - бывает только после "сброса")
.equ	BSC_ShortHold	= 0b100	; "короткое удержание"	(кнопка нажата, и всё ещё удерживается, пока "короткое" время)
.equ	BSC_LongHold	= 0b101	; "длинное удержание"	(кнопка нажата, и всё ещё удерживается, уже "длительное" время)
.equ	BSC_ShortPress	= 0b010	; "короткое нажатие"	(кнопка была нажата, и затем отпущена, а время её удержания было "незначительным")
.equ	BSC_LongPress	= 0b011	; "длинное нажатие"	(кнопка была нажата, и затем отпущена, а время её удержания было "длительным")

Диаграмма переключения Статус-кодов Кнопки

Специальные значения:

  • «регистр статуса» = 0b00000000, означает начальное состояние: кнопка «не нажата и не нажималась», исходное положение для всех кнопок — бывает только после «сброса в ноль» или при включении микроконтроллера.
    (Здесь, статус-код кнопки = «не нажата»; счётчик времени предыдущего нажатия = обнулён.)
  • «регистр статуса» = 0b11111111, означает служебное исключительное состояние «ОТЛОЖЕННЫЙ СБРОС»: «фиксация, до ожидания следующего отпускания» кнопки
    (Здесь, якобы, кнопка «удерживается и отпущена одновременно» — это запрещённый «статус-код кнопки», что запрещает прикладному коду реакцию на эту кнопку.)
    (Замечу: При следующем отпускании физической кнопки, статус-регистр будет АВТОМАТИЧЕСКИ «сброшен в ноль» — в исходное положение! А если кнопка уже отпущена — то «сброс в ноль» будет осуществлён сразу же.)

Процедура KEY_UPDATE_BUTTON_STATUS — фиксирует статус кнопки в «статусном регистре», в зависимости от динамически изменяющегося [проинтегрированного] состояния «физического канала» (определяет нажатия-отжатия). В неё передаются следующие параметры:

  • Регистровым параметром «IntegratorAddress» передаётся адрес «регистра интегратора» (в памяти ОЗУ), который кодирует текущее «физическое» состояние кнопки.
  • Регистровым параметром «StatusAddress» передаётся адрес «статусного регистра кнопки» (в памяти ОЗУ), который следует обновить.

3.2. «Время удержания» Кнопки

Процедура KEY_ENHANCE_TIME_FOR_ALL_BUTTONS наращивает таймеры для удерживаемых кнопок (должна запускаться по событию таймера, точно раз в 0.5 сек).
Для Энкодеров не используется — предназначена только для обработки Кнопок! С физическими кнопками также не работает, а только перебирает/обрабатывает их Статусные регистры!

Алгоритм её работы таков:

  • Для кнопок удерживаемых в нажатом состоянии, наращивает «счётчик времени удержания» (если он ещё меньше своего максимума < 0b11111).
  • А также, устанавливает флаг «времени удержания» кнопки BUTTON_HOLDEN_LONG в единицу, для «длительно удерживаемых» кнопок. Таким образом, эта процедура модифицирует «статус-код» кнопки: BSC_ShortHold -> BSC_LongHold.

Процедура KEY_ENHANCE_TIME_FOR_ALL_BUTTONS не имеет регистровых параметров, но использует один константный параметр (захардкоден в прошивке), для настройки времени «длительно удерживаемых» кнопок:

.equ	CShortButtonTouchDuration = 3	;

3.3. Обработка статус-кодов Кнопок («жесты» и «аккорды»)

Советы по порядку тестирования Кнопок:

Замечание! Совет: В большинстве случаев, обработчик события привязывается к значению BUTTON_STATUS_CODE в «статусном регистре» кнопки — это самый простой для кодирования и функциональный метод. Полагаю, для описания большинства распространённых «жестов» с кнопками — хватит 4 ситуации…
Для тестирования состояния кнопок по паттерну BUTTON_STATUS_CODE, рекомендуется использовать библиотечные макросы: IF_BUTTON_HAVE_STATUS, AND_BUTTON_HAVE_STATUS, OR_BUTTON_HAVE_STATUS (последние два используются в комбинации, для тестирования «кнопочных аккордов»). Пример, см. ниже…

Замечание! Совет: кроме тестирования чистого «статус-кода кнопки», можно также смотреть на «счётчик времени удержания кнопки» — и различать также ситуации «очень длительных» удерживаний кнопок! Но замечу, что эти ситуации уже не кодируются в «статус-коде кнопки», т.к. довольно редки. Если требуется данный функционал — его следует самостоятельно реализовать в прикладном коде, на основании арифметического сравнения с абсолютным значением счётчика BUTTON_HOLDING_TIME (инструкцией CPI). Это самый функциональный способ! Пример, см. ниже…

Замечание! Совет: чтобы разнообразить варианты ввода, прикладной код также может воспринимать «кнопочные аккорды» — тестировать группы кнопок на наличие одновременных нажатий/удержаний. Однако, делать это нужно в определённом порядке приоритетов:

  1. Сначала, проверить были ли нажатия отдельных кнопок, при удержании других «кнопок-модификаторов» (подобно: «Shift/Ctrl/Alt + X») ?
    Подсказка: У основной кнопки, которая «нажималась» — статус BUTTON_STATUS_CODE==»ShortPress/LongPress»; при этом, у модификаторов, при «удержании в нажатом состоянии» — установлен бит BUTTON_IS_HOLDDOWN==1…
  2. Затем, проверить простые одиночные нажатия отдельных кнопок: у которых статус==»ShortPress/LongPress»?
  3. Затем, проверить группы кнопок на одновременное удержание: у которых статус==»LongHold»? Причём, таким образом, можно проверить, последовательно, несколько групп: от более общих множеств, к более частным — с меньшим числом входящих кнопок…
  4. И наконец, проверить отдельные кнопки на длительное удержание: у которых статус==»LongHold»?

Замечание! Совет: во избежание ложных повторных срабатываний событий, после обработки статуса кнопки прикладным кодом и произведения ответной реакции, «СБРОСьте» статусный регистр — это простой способ сообщить остальному прикладному коду, что данное событие уже обработано и не требует дальнейшего участия…

3.4. Сброс «регистра статуса» Кнопки (завершение обработки события)

Сделал дело — прибери за собой!

После отработки события — обработчик сбрасывает статусные регистры вовлечённых кнопок: обычно для всех кнопок, составляющих «аккорд». Сброс статусного регистра — фактически, обнуляет историю произведённого с кнопкой действия ДО обработки события. Теперь, кнопки готовы к новым отдельным действиям с ними.

Хотя, в некоторых случаях, статусы отдельным кнопкам полезно оставить, не сбрасывать — например, для «удерживаемых» кнопок-модификаторов.
(Примечание: такая кнопка продолжит считаться «нажатой», и будет участвовать в составе дальнейших нажимаемых и обрабатываемых «аккордов», без необходимости «фиксации отдельного нажатия», т.е. без необходимости отпустить и нажать её заново.)

В то же время, нужно помнить, что если прикладной код вовремя не обнулит «статусный регистр» отдельной кнопки — то она продолжит считаться «нажатой» (что особенно опасно) или «удерживаемой», что в будущем, может вызвать «побочную реакцию»!
Обычно, интерфейс устройства состоит из разных подсистем, и не во всех режимах используются [и обрабатываются] все кнопки интерфейса. Если, в некотором режиме интерфейса, часть кнопок не задействована — то они не опрашиваются, не срабатывают, их статус не обнуляется, а запоминается и хранится (такая кнопка становится «миной отложенного действия»). Следует также учитывать «защиту от дурака»: пользователь будет нажимать на всё подряд!
Поэтому, при реализации прикладной логики вашего Устройства: когда переключаете интерфейс в другую Подсистему — следует безусловно обнулять все статусные регистры, вызовом процедуры KEY_RESET_STATUS_FOR_ALL_BUTTONS (это «разминирует все заложенные ловушки»).

Замечание! Это важный концептуальный момент: Обработчики событий программируются так, чтобы срабатывать по наступлению некоторого события в срезе времени. Обработчики не контролируют никаких состояний и данных ДО и ПОСЛЕ события — следовательно, их код проще и прозрачнее (аспектно-ориентированный). Но поэтому, так важно разделять события, во времени, на атомарные реакции… После отработки события, обработчик просто сбрасывает статусные регистры вовлечённых кнопок — таким образом, «подсистеме обработки ввода» указывается, что событие было обработано и данная кнопка готова к принятию нового, следующего ввода.

Есть два вида сброса:

  1. «Сброс статус-регистра В НУЛИ» (мгновенный) Такие кнопки не требуют явного отпускания кнопки, перед тем как они смогут повторно зафиксировать событие «нажатие». Здесь, возможен побочный эффект: если сбросить статус кнопки «в ноль» до того как физическая кнопка будет реально отпущена (т.е. пока она «удерживается»), то уже на следующей итерации, по состоянию «физического канала» = «1»(нажата) — будет опять зафиксирована новая ситуация «нажатие»…
    Этот способ не рекомендуется использовать НЕПОСРЕДСТВЕННО. Вместо этого: Для тактовых кнопок рассчитанных на одиночные «законченные нажатия (BSC_*Press)» — всегда используйте «сброс В ЕДИНИЦЫ» (отложенный, автоматический). А если требуются серийные реакции на событие «удерживание кнопки (BSC_*Hold)» — просто не сбрасывайте статус-регистр, пока кнопка удерживается.
  2. «Сброс статус-регистра В ЕДИНИЦЫ» (отложенный) Такие кнопки ждут «отдельных нажатий» (реальная кнопка должна быть отпущена и нажата заново) — это исключает «побочные эффекты», типа «залипания» или «ошибочные повторные нажатия» кнопок…

Для понимания ситуации, как это работает, рассмотрим несколько сценариев:

  1. После обработки статус-кодов BSC_*Press (замечу, что физическая кнопка уже отпущена), статус-регистр можно «сбросить в ноль (CLR)» -> так сразу установится исходное положение для всех кнопок «не нажата».
  2. После обработки статус-кодов BSC_*Hold (замечу, что физическая кнопка ещё удерживается), статус-регистр требуется «сбросить в единицы (SER)» -> так в регистре установится ошибочный статус-код, который не будет спутан ни с одной из разрешённых комбинаций, что запретит прикладному коду реагировать на эту кнопку, в дальнейшем. (Это служебное, исключительное состояние статус-регистра: «фиксация, до ожидания следующего отпускания» кнопки — ОТЛОЖЕННЫЙ СБРОС.)
  3. В то же время, как только физическая кнопка будет отпущена, сразу же, статус-регистр АВТОМАТИЧЕСКИ будет «сброшен в ноль», в исходное положение. (Это исключительная реакция конвейера обработки кнопок: лишнее нажатие на кнопку не будет зафиксировано!)

Рекомендуется: ВО ВСЕХ СЛУЧАЯХ, после обработки любых статус-кодов, СБРАСЫВАТЬ статус-регистры отработанных кнопок «В ЕДИНИЦЫ»! Так установится статус «ОТЛОЖЕННЫЙ СБРОС», который, даже если кнопка уже отжата, то при следующем цикле конвейера — будет автоматически «сброшен в ноль». Это безопасный стиль программирования!
Замечу: При таком стиле программирования — кнопка работает «с триггером-защёлкой состояния». Вариант поведения «с триггером»: заставляет пользователя отпускать кнопку каждый раз, перед её следующим нажатием — что обычно полезно, ибо предотвращает серии ошибочных повторных срабатываний кнопки.

Замечание! Хотя, в некоторых случаях, можно и отходить от этого правила: кроме кнопок-модификаторов, упомянутых выше… Ещё бывает полезно использовать кнопки «без триггера-защёлки состояния» — для умышленной организации серийных реакций на нажатую и удерживаемую кнопку.
Например, рецепт: как двумя кнопками «больше/меньше» организовать эргономичный интерфейс подстройки параметра: «пользователь нажал кнопку и удерживает — после секундной паузы, значение начинает автоматически набегать +1, +1,… а через некоторое время, начинает набегать быстрее +5, +5,…» Пример, см. ниже…

4. Работа с Энкодерами

В отличие от кнопок, Энкодер — гораздо проще в управлении! У него всего одна степень свободы: он может вращаться «по часовой» или «против часовой» стрелки. Скорость и динамика вращения не контролируются. Значение имеют лишь: количество сосчитанных тиков и направление вращения, которые отражаются числом в «регистре счётчика» Энкодера.

От энкодера, по двум «физическим каналам», поступает «2-битный рефлексивный код Грэя». Обычно, канал «B» сдвинут на 90°, относительно канала «A», при повороте по часовой стрелке:
2-битный рефлексивный код Грэя

4.1. Выбор алгоритма обработки Энкодера

Вся магия обработки энкодера реализована в процедуре KEY_UPDATE_ENCODER_STATUS — она Фиксирует статус Энкодера (устанавливает значения в «статус-регистре» и «счётчике тиков» Энкодера), в зависимости от динамически изменяющихся [проинтегрированных] состояний его двух управляющих «физических каналов»: AC и BC.

В процедуру KEY_UPDATE_ENCODER_STATUS передаются следующие параметры:

  • Регистровым параметром «IntegratorAddress» передаётся адрес ПЕРВОГО «регистра интегратора» (из пары последовательно расположенных регистров в памяти ОЗУ, которые кодируют текущее «физическое» состояние Энкодера).
    (Примечание: в целях оптимизации кода, при исполнении процедуры, указатель «IntegratorAddress» передвигается на (+1) байт вперёд. И после исполнения процедуры — указывает на следующую ячейку памяти.)
  • Регистровым параметром «StatusAddress» передаётся адрес «статусного регистра энкодера» (в памяти ОЗУ). При этом, предполагается, что следом за ним расположен регистр «счётчика тиков» этого Энкодера. Их оба — следует обновить…
    (Примечание: в целях оптимизации кода, при исполнении процедуры, указатель «StatusAddress» передвигается на (+1) байт вперёд. И после исполнения процедуры — указывает на следующую ячейку памяти.)

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

;.EQU	ENCODER_USE_SIMPLIFIED_CODE = 1		; "Упрощённый код"
;.EQU	ENCODER_USE_ACADEMIC_CODE = 2		; "Академический код"
.EQU	ENCODER_USE_PARANOID_CODE = 3		; "Параноидальный код"

Выберите один из трёх вариантов, раскомментировав определение соответствующего символа в файле :

  1. .EQU ENCODER_USE_SIMPLIFIED_CODE = 1 «Упрощённый код» (простой алгоритм, маленький и быстрый код! не защищён от ошибочных входных сигналов; предделитель фиксирован = 4 раза)
    Рекомендуется использовать на стабильном железе: быстром Микроконтроллере и при низком уровне помех по входным каналам. Примечание: в этом варианте, код «предварительного делителя счётчика» не используется — но делитель есть и фиксирован: показания «счётчика тиков» делятся в 4 раза.
  2. .EQU ENCODER_USE_ACADEMIC_CODE = 2 «Академический код» (самый компактный код и поражает воображение своим изяществом! но не защищён от ошибочных входных сигналов)
    Преимущества: он проще/меньше/быстрее параноидального кода, и с ним работает полноценный «предварительный делитель счётчика»! Но обычно, не рекомендуется его использовать: т.к. он является «серединкой на половинку» среди других вариантов, и особых преимуществ не даёт…
    Замечу что, на плохом железе: «Академический код» глючит столь же интенсивно, как и «Упрощённый код», но они по разному ощущаются — выбери себе глюки по вкусу! ;)
  3. .EQU ENCODER_USE_PARANOID_CODE = 3 «Параноидальный код» (тупой алгоритм и избыточный код, перебирающий все комбинации входных сигналов; защищён от «ОДНОКРАТНЫХ ошибок» по входу!)
    Предполагается, будет математически сглаживать и преодолевать последствия некоторых ошибок опроса физических каналов энкодера (при пропуске одного такта — запрещённая комбинация по входу, будет проигнорирована). Используйте этот вариант на плохом железе: при медленном микроконтроллере, или когда опрос энкодера происходит с ошибками.

Во-вторых, выберите коэффициент «предварительного делителя счётчика тиков» энкодера:

.EQU	ENCODER_USE_PRECOUNTER = 4
  • Здесь, допустимые значения предделителя = 2,3,4 переключения / на один тик счётчика. Рекомендуется = 4.
  • Если символ закомментирован — то предделитель отключён (т.е. установлен = 1 переключение/тик счётчика)!
  • Примечания: Предделитель не используется в режиме ENCODER_USE_SIMPLIFIED_CODE (там коэффициент деления фиксирован и, вследствие особенностей алгоритма, установлен = 4 раза).

4.2. Формат «регистра Статуса» Энкодера

(Справка: Расположены в секции DEncoderStatus в файле , первый из пары)

Данные, обслуживающие энкодер, хранятся в паре байтовых регистров, последовательно расположенных в памяти, в блоке DEncoderStatus:

DEncoder0Status:	.byte	1	; пара регистров: регистр "Статуса"		(Примечание: статус-регистр энкодера, напрямую, никогда не используется прикладным кодом.)
DEncoder0Counter:	.byte	1	; и сразу следом: регистр "Счётчик тиков"	(Формат счётчика: особой специальной структуры не содержит. Это просто знаковое целое число: Signed Int = [-128..127])

Замечание! Замечу, однако, что прикладной код никогда не использует «статусный регистр энкодера» напрямую! Он, вспомогательный, нужен лишь для вычисления направления вращения энкодера, например, используя булеву функцию: F2(x1,x2,y1,y2) = (НЕ(x1) * y2) + (x1 * НЕ(y2)). Вращения энкодера, если таковые были произведены, и их направление — инкрементируются к регистру «двунаправленного аддитивного счётчика тиков» энкодера. (Всё это происходит в служебных процедурах «конвейера обработки кнопок», в частности, в коде процедуры KEY_UPDATE_ENCODER_STATUS.)

 7   6   5   4  |   3   2   1   0
 F2  .  y2  x2  | PreCntr  y1  x1

8-битный регистр разделён на две «тетрады»:

  • В первой тетраде DEncoderStatus[3:0] = закодировано предыдущее состояние энкодера.
  • Во второй тетраде DEncoderStatus[7:4] = закодировано последнее состояние энкодера.
  • Биты DEncoderStatus[2:3] содержат «предварительный делитель счётчика» = 1..4, через который «по 4 переключения входных сигналов на один тик» преобразуются к фактическому числу тиков, накапливаемому в регистре «счётчика тиков» энкодера.
  • (бит номер 6 — не используется, и равен 0)

Описание битов «регистра статуса» энкодера:

.equ	ENCODER_STATUS_X1	= 0	; Предыдущее        состояние энкодера: значение входного канала AC(X1)
.equ	ENCODER_STATUS_Y1	= 1	; Предыдущее        состояние энкодера: значение входного канала BC(Y1)
.equ	ENCODER_STATUS_PRECOUNTER = 2	; Следующие два бита - это предварительный счётчик =1..4, через который "по 4 переключения входных сигналов на один тик" преобразуются к фактическому числу тиков, накапливаемому в регистре "счётчика тиков" энкодера...
.equ	ENCODER_STATUS_X2	= 4	; Последнее/текущее состояние энкодера: значение входного канала AC(X2)
.equ	ENCODER_STATUS_Y2	= 5	; Последнее/текущее состояние энкодера: значение входного канала BC(Y2)
.equ	ENCODER_STATUS_F2	= 7	; Последнее/текущее состояние энкодера: вычисленное направление вращения (F2) - это самая нужная, итоговая статусная информация: в какую сторону осуществлён последний тик поворота?

Кодировка значения функции вращения:

  • F = 0 C.W., вращение по часовой («правый винт») -> (+1) к счётчику
  • F = 1 C.C.W., вращение против часовой («левый винт») -> (-1) к счётчику

Причём, кодировка входных каналов X и Y (=0 или 1?), здесь, в принципе, не важна — используется лишь порядок их переключения…

Предупреждение: Начальное состояние статус-регистра = 0b00000000, не является «разрешённой комбинацией» входных сигналов, но алгоритм не сломает. Ну, в худшем случае: сразу после RESET, в «регистр счётчика» энкодера может попасть +1/-1… (не важно!)

4.3. Формат «регистра Счётчика» Энкодера

(Справка: Расположены в секции DEncoderStatus в файле , второй из пары)

76543210
Snnnnnnn

Регистр «счётчика тиков» особой специальной структуры не содержит — это просто знаковое целое число: Signed short int = [-128..127].
Начальное состояние = 0b00000000.

«Счётчик тиков» является конечным в конвейере обработки энкодера, и его использует прикладной код для реакции на жесты с энкодером:

  • если DEncoderCounter == 0, то вращений [с момента последней обработки] не было, ничего делать не нужно.
  • если DEncoderCounter > 0, то зафиксированы вращения, причём в сумме, больше провернули по часовой стрелке: положительное число [1..127] в регистре равно числу тиков [с момента последней обработки].
  • если DEncoderCounter < 0, то зафиксированы вращения, причём в сумме, больше провернули против часовой стрелке: отрицательное число [-128..-1] в регистре равно числу тиков [с момента последней обработки].

Причём, между обработками событий энкодера прикладным кодом (которые запускаются гораздо реже, чем опрос энкодера) пользователь может колбасить ручку энкодера произвольно в разные стороны, а итоговый результат будет правильно отражён в «Счётчике тиков»!

Запускайте обработчик событий энкодера чаще, чем переполняется его регистр «счётчика тиков» (чтобы значение в нём не превышало границы [-128..127]).

В обработчике событий — просто приплюсуйте результат из «Счётчика тиков» к вашей управляемой переменной величине…

И наконец, после обработки энкодера прикладным кодом и произведения ответной реакции, «СБРОСьте» регистр «Счётчика тиков» в ноль (но «статусный регистр» энкодера не трогайте!) — это простой способ сообщить остальному прикладному коду, что данное событие уже обработано и не требует дальнейшего участия…


Порядок внедрения библиотеки

Скачайте дистрибутив библиотеки «celeronkeyinputlib.inc».
Скачайте, по зависимостям, дистрибутив библиотеки «macrobaselib.inc».

Из них возьмите:

  1. Файл поместите в папку с вашим проектом.
    (Здесь расположена процедура KEY_SCAN_INPUT, код которой вам следует переписать под свою схему).
  2. Из файла возьмите только секцию , с определением регистров данных, констант и символов — интегрируйте в «секцию данных» вашей программы.
    (Эти определения вам следует переделать под свою схему.)
    Примечание: Здесь, я могу лишь рекомендовать, но не обязываю: хранить определения данных в отдельном исходном файле «data.inc»…
  3. Файл внешней используемой библиотеки поместите в папку с вашим проектом, как есть.
    (Примечание: Использование этой библиотеки рекомендует/накладывает на вас определённый стиль кодирования.)

Подключите приведенные библиотеки, добавив такие строки в основной ASM-файл (см. пример):

; В самом начале программы:
.include "macrobaselib.inc"		; Библиотека базовых Макроопределений.

; В сегмент кода (CSEG):
.include "celeronkeyinputlib.inc"	; Библиотека процедур для интеллектуальной обработки ВВОДА: сканирование Кнопок и Энкодеров.

Сконфигурируйте один из аппаратных таймеров вашего микроконтроллера, или обеспечьте иной механизм, для периодического вызова обработчика KEY_ENHANCE_TIME_FOR_ALL_BUTTONS (точно, каждые 0.5 сек):

; Прерывание: отсчёт полусекунд
TIMER0_OVERFLOW_HANDLER:
		; Сохранить в Стеке регистры, которые используются в данном обработчике:
		PUSHF		; сохраняет часто используемые регистры: SREG и TEMP (TEMP1)
		PUSH	temp2	; регистр используется в INVB и др.
		PUSH	temp3	;       регистр используется в KEY_ENHANCE_TIME_FOR_ALL_BUTTONS
		PUSH	R28	; (YL)	регистр используется в KEY_ENHANCE_TIME_FOR_ALL_BUTTONS
		PUSH	R29	; (YH)	регистр используется в KEY_ENHANCE_TIME_FOR_ALL_BUTTONS

		; Головная процедура конвейера обработки кнопок:	Наращивает таймеры для удерживаемых кнопок (запускать каждые полсекунды)
		RCALL	KEY_ENHANCE_TIME_FOR_ALL_BUTTONS

		; Выход из обработчика
		POP	R29
		POP	R28
		POP	temp3
		POP	temp2
		POPF
		RETI

Допилите библиотеку под вашу схему:
Приведенные здесь процедуры являются универсальными, их код модифицировать [обычно] не требуется! Единственная процедура, в этой библиотеке, код которой требуется адаптировать к вашей конкретной физической схеме — это KEY_SCAN_INPUT (через неё осуществляется связь с физическими каналами ввода).

Также, под вашу конкретную физическую схему, требуется адаптировать блок определения данных в из файла (определения блоков регистров Интегратора, статусных регистров Кнопок и Энкодеров, и некоторые константы):

  • Блоки регистров в DSEG следует располагать последовательно и начинать с характерных «системных меток»: DInputIntegrator, DEncoderStatus, DButtonStatus.
    (Примечание: в блоках важен лишь порядок и количество! названия самих прикладных регистров — произвольные, придумываете сами, как удобно использовать в прикладном коде.)
  • Затем, если требуется, подстройте «режимные константы»: CButtonLatchDepth, CEncoderLatchDepth, CShortButtonTouchDuration.
  • Наконец, в зависимости от реализации кода KEY_SCAN_INPUT, здесь может понадобится ввести дополнительные данные и определения…
    (Например, если вы используете клавиатуру на сдвиговых регистрах, как в стандартном примере, то следует сконфигурировать ещё и количества входных физических каналов/кнопок/энкодеров: CEncoderInputChannelCount, CButtonInputChannelCount, CSkipInputChannelCount…)

Спроектируйте и закодируйте процедуры с обработчиками событий: код прикладных реакций на нажатия кнопок, энкодеры, обслуживание интерфейса… Предположим, для примера, что все обработчики расположены в процедуре SWITCH_MODES, или вызываются из неё (удобно иметь одну общую точку входа в обработчики событий).

В прошивке требуется разместить код, запускающий сканирование Кнопок и переключение Режимов — он должен запускаться часто! Например:

; (Суперцикл: сканирование Кнопок и переключение Режимов)
MAIN:
		; Сканирование Кнопок
		OUTI	DKeyScanCnt,	20	; DKeyScanCnt = количество циклов "сканирования кнопок".	(а затем будет один цикл "реакции на события")
	LoopKeyScan__MAIN:
		RCALL	KEY_SCAN_INPUT		; Головная процедура конвейера обработки кнопок: Сканирует физические кнопки и устанавливает их статус-регистры (запускать часто)
		DEC8M	DKeyScanCnt
		BRNE	LoopKeyScan__MAIN
		
		; Переключение Режимов ("реакция на события")
		RCALL	SWITCH_MODES		; Важно: DKeyScanCnt, выше, следует подбирать так, чтобы "SWITCH_MODES" выполнялся ~10-20раз/сек. Тогда будет эргономичная "инерционность" реакции кнопок...
		
		RJMP	MAIN						
; (обработка событий завершена)

Методика проектирования прикладного кода (обработчики событий)

Кнопки в интерфейсе Устройства

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

Схема переключения режимов интерфейса («машина состояний» конечного автомата) — удобнее всего описывается UML диаграммой:
Схема переключения режимов Устройства
(Примечание для любопытствующих: я использовал UML-редактор «Altova MissionKit UModel» — самый безглючный, функциональный и эргономичный, среди всех что я пробовал. Правда, требует лечение…)

Для описания реального сложного устройства — обычно, требуется множество подобных диаграмм (для всех подсистем, и их детализаций). Выше представлена только одна «Диаграмма состояний в нотации UML». И мы рассмотрим только один аспект из этой диаграммы, обведенный красным овалом: часть подсистемы «переключение Функций».

Для тех, кто не совсем понял нарисованное, поясню задачу:

  • На передней панели Устройства есть три физические кнопки: «DButtonRTCStatus», «DButtonTimer1Status», «DButtonTimer2Status» (я перечислил названия соответствующих им «статусных регистров» — это, фактически, внутренние имена кнопок, для программы)
  • Я хочу запрограммировать реакции на следующие «жесты» с этими кнопками: короткое законченное нажатие «BSC_ShortPress» (клик), и длительное удержание «BSC_LongHold» (дольше 1.5 сек). В первом случае, событие сработает как только пользователь отпустит кнопку, после короткого удержания. Во втором случае, событие сработает ещё до отпускания кнопки, если пользователь будет удерживать её дольше 1.5 сек.
    Замечу: Событие длинного нажатия на кнопку «BSC_LongPress» (нажать + долго подержать + отпустить), в данном контексте, бессмысленно — не сработает никогда, потому что раньше отработает «BSC_LongHold». Такие вещи нужно учитывать, при планировании…
  • Как видим из диаграммы, эти кнопки имеют «глобальную область действия»: отрабатывают из любого режима. Каждая кнопка жёстко связана с конкретной «функцией», в которую следует переключить интерфейс, при её нажатии. Поэтому прикладной код будет довольно простым: условие срабатывания «обработчика события» не содержит проверки текущего режима, а содержит только проверки статус-кодов кнопок.

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

		;** Подсистема: "переключение Функций"
	SwitchFunc_RTC_Alarm__SWITCH_MODES:
		IF_BUTTON_HAVE_STATUS	DButtonRTCStatus,	BSC_ShortPress
		OR_BUTTON_HAVE_STATUS	DButtonRTCStatus,	BSC_LongHold
		BRTC	SwitchFunc_Timer1__SWITCH_MODES				; если кнопки не были нажаты...
		OUTI	DButtonRTCStatus,	0b11111111			; После обработки состояния кнопки - сделать "ОТЛОЖЕННЫЙ СБРОС" её статусного регистра.		(Примечание: это другой вариант поведения, "с триггером защёлкой-состояния": заставлять пользователя отпускать кнопку, перед следующим нажатием - что обычно полезно, ибо предотвращает серии ошибочных повторных срабатываний кнопки...)
		; <здесь располагается прикладной код обработки События>
		RJMP	EventButtonHavePressed__SWITCH_MODES


	SwitchFunc_Timer1__SWITCH_MODES:
		IF_BUTTON_HAVE_STATUS	DButtonTimer1Status,	BSC_ShortPress
		OR_BUTTON_HAVE_STATUS	DButtonTimer1Status,	BSC_LongHold
		BRTC	SwitchFunc_Timer2__SWITCH_MODES				; если кнопки не были нажаты...
		OUTI	DButtonTimer1Status,	0b11111111			; После обработки состояния кнопки - сделать "ОТЛОЖЕННЫЙ СБРОС" её статусного регистра.
		; <здесь располагается прикладной код обработки События>
		RJMP	EventButtonHavePressed__SWITCH_MODES


	SwitchFunc_Timer2__SWITCH_MODES:
		IF_BUTTON_HAVE_STATUS	DButtonTimer2Status,	BSC_ShortPress		
		OR_BUTTON_HAVE_STATUS	DButtonTimer2Status,	BSC_LongHold		
		BRTC	SwitchFunc_End__SWITCH_MODES				; если кнопки не были нажаты...
		OUTI	DButtonTimer2Status,	0b11111111			; После обработки состояния кнопки - сделать "ОТЛОЖЕННЫЙ СБРОС" её статусного регистра.
		; <здесь располагается прикладной код обработки События>
		RJMP	EventButtonHavePressed__SWITCH_MODES

	SwitchFunc_End__SWITCH_MODES:

Здесь, три обработчика событий, каждый из которых подвязан к условиям проверки состояния одной из трёх кнопок. Для тестирования статус-кодов кнопок по паттерну BUTTON_STATUS_CODE, используются вспомогательные библиотечные макросы IF_BUTTON_HAVE_STATUS, AND_BUTTON_HAVE_STATUS, OR_BUTTON_HAVE_STATUS (последние два используются в комбинации, для тестирования «кнопочных аккордов»):

; Установить status bit "T" = в значение булевого выражения: (Если статус-регистр @0 хранит статус-код == @1 ?)
; Пример вызова:  IF_BUTTON_HAVE_STATUS  DButtonStatus, BSC_ShortPress

; Установить status bit "T" = в значение булевого выражения: T && (Если статус-регистр @0 хранит статус-код == @1 ?)
; Пример вызова:  AND_BUTTON_HAVE_STATUS  DButtonStatus, BSC_ShortPress

; Установить status bit "T" = в значение булевого выражения: T || (Если статус-регистр @0 хранит статус-код == @1 ?)
; Пример вызова:  OR_BUTTON_HAVE_STATUS  DButtonStatus, BSC_ShortPress

; Памятка: эти макросы, при выполнении, портят содержимое временных регистров TEMP1 и TEMP2.

Если обработчик события сработал, то рекомендуется сразу же сбросить статусные регистры для вовлечённых кнопок (их может быть несколько):

OUTI	DButtonRTCStatus,	0b11111111			; После обработки состояния кнопки - сделать "ОТЛОЖЕННЫЙ СБРОС" её статусного регистра.

Что означает «переключение режима» интерфейса? И как это реализовывается в прикладном коде обработчика события?
Для разработки систем логического управления на базе конечных автоматов существуют: большая и сложная «Switch-технология», парадигма «Автоматного программирования», и другие страшные слова… Но пока, об этом, не стоит особо задумываться.
Проще всего, и обычно так делают, «машину состояний» конечного автомата можно реализовать с помощью «Флагового автомата», например так:

; Проверить в каком режиме находится интерфейс и выполнить соответствующую ветку кода:
STOREB	DMain_Mode,	MODE_SETTINGS		; Флаг "находимся в режиме настройки" -> T
BRTC	NormalMode__SWITCH_MODES
; Режим настройки активен! <выполнить соответствующий код...>
RJMP	Exit__SWITCH_MODES
NormalMode__SWITCH_MODES:
; Режим настройки выключён! <выполнить соответствующий код...>
RJMP	Exit__SWITCH_MODES


; Переключить режим интерфейса: 
SETB	DMain_Mode,	MODE_SETTINGS		; Прикладная реакция: войти в "режим настройки".
; или:
CLRB	DMain_Mode,	MODE_SETTINGS		; Прикладная реакция: выйти из "режима настройки".

Принципы проектирования:

  • Обработчик события — атомарен, по своему действию.
  • Начальные условия для запуска обработчика события определяются в срезе времени.
  • Обработчики событий следует объединять в группы по подсистемам. А обработку Подсистем располагать в коде, в порядке приоритетов: сперва обслуживать главные подсистемы и глобальные реакции; затем — код ветвится, обслуживая «случаи» вложенных подсистем…
  • «Состояние» конечного автомата — можно считать отдельной Подсистемой…

Ввод через «Хитрую кнопку»

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

Замечание! Например, рецепт: как двумя кнопками «больше/меньше» организовать эргономичный интерфейс подстройки параметра: «пользователь нажал кнопку и удерживает — после секундной паузы, значение начинает автоматически набегать +1, +1,… а через некоторое время, начинает набегать быстрее +5, +5,…»

		; Обработка МОДИФИКАЦИИ ЧИСЛОВОГО ЗНАЧЕНИЯ в памяти "Интеллектуальной Кнопкой"

		; (однократное нажатие)
		IF_BUTTON_HAVE_STATUS	DButtonStartStatus,	BSC_ShortPress
		BRTC	Button2__SWITCH_MODE_SETTINGS				; если кнопки не были нажаты...
		OUTI	DButtonStartStatus,	0b11111111			; После обработки состояния кнопки - сделать "ОТЛОЖЕННЫЙ СБРОС" её статусного регистра.
		LDI	ExtendDataByValue,	1
		RJMP	ModifyData__SWITCH_MODE_SETTINGS

		; (инерционный ввод, при удержании)
	Button2__SWITCH_MODE_SETTINGS:
		IF_BUTTON_HAVE_STATUS	DButtonStartStatus,	BSC_LongHold
		BRTC	NotButton__SWITCH_MODE_SETTINGS				; если кнопки не были нажаты...
		;OUTI	DButtonStartStatus,	0b11111111			; Внимание: в этом случае, статус кнопки НЕ сбрасываем - пусть продолжает считаться "нажатой" и набегает "таймер удержания"...
		LDI	ExtendDataByValue,	1
		; (хочу различать также ситуации "очень длительных" удерживаний кнопки)
		LDS	temp1,	DButtonStartStatus
		ANDI	temp1,	0b11111<<BUTTON_HOLDING_TIME			; выделить "счётчик времени удержания кнопки"
		CPI	temp1,	8						; при удержании кнопки свыше >=4сек, показания набегают в 2 раза быстрее.
		BRLO	SlowSpeedYet__SWITCH_MODE_SETTINGS
		LSL	ExtendDataByValue
		CPI	temp1,	16						; при удержании кнопки свыше >=8сек, показания набегают ещё в 2 раза быстрее.
		BRLO	SlowSpeedYet__SWITCH_MODE_SETTINGS
		LSL	ExtendDataByValue
	SlowSpeedYet__SWITCH_MODE_SETTINGS:
		RJMP	ModifyData__SWITCH_MODE_SETTINGS
	NotButton__SWITCH_MODE_SETTINGS:
		RJMP	Exit__SWITCH_MODES

	ModifyData__SWITCH_MODE_SETTINGS:
		; <здесь располагается прикладной код модификации "ячейки памяти" += значение из регистра ExtendDataByValue >

Особенности:

  • Здесь, обработчики реагируют на два жеста («BSC_ShortPress» и «BSC_LongHold») одной кнопки («DButtonStartStatus»).
  • Заметьте, что здесь, «регистр статуса» кнопки сбрасывается только по событию «отдельное нажатие (BSC_ShortPress)»!
  • Но по событию «удерживание кнопки (BSC_LongHold)» — осуществляется только прикладная реакция, а статус кнопки НЕ сбрасываем — пусть продолжает считаться «нажатой» и набегает «таймер удержания»…

Интерфейс — одна кнопка

Извращения, типа, «интерфейс — одна кнопка», актуальные для «миниатюрных устройств, обладающих только одной внешней кнопкой» — можно реализовать на конечных флаговых автоматах, гроздями атомарных обработчиков.
Примечание: к данной библиотеке эти трюки уже не относятся (и плохо поддерживаются) — этот рецепт, больше, для лулзов.

Ввод через Энкодер

В отличие от Кнопок, Энкодер — гораздо проще в управлении! У него всего одна степень свободы: он может вращаться «по часовой» или «против часовой» стрелки. Скорость и динамика вращения не контролируются. Значение имеют лишь: количество сосчитанных тиков и направление вращения, которые отражаются числом в «регистре счётчика» Энкодера.

«Счётчик тиков» (DEncoder0Counter) является конечным в конвейере обработки энкодера, и его использует прикладной код для реакции на жесты с энкодером:

		; Обработка МОДИФИКАЦИИ ЧИСЛОВОГО ЗНАЧЕНИЯ в памяти Энкодером
		LDS	ExtendDataByValue,	DEncoder0Counter
		TST	ExtendDataByValue
		BREQ	NotEncoder__SWITCH_MODE_SETTINGS
		OUTI	DEncoder0Counter,	0				; замечу: после прибавления к "ячейке памяти" этой аддитивной добавки, регистр "счётчика тиков" энкодера обнуляется.
		RJMP	ModifyData__SWITCH_MODE_SETTINGS
	NotEncoder__SWITCH_MODE_SETTINGS:
		RJMP	Exit__SWITCH_MODES

	ModifyData__SWITCH_MODE_SETTINGS:
		; <здесь располагается прикладной код модификации "ячейки памяти" += значение из регистра ExtendDataByValue >

Сперва, следует проверить текущее значение в «счётчике тиков» (TST): если значение равно = 0, значит пользователь не крутил ручку энкодера. Это критерий «наличия/отсутствия событий от энкодера» — важный аспект для логики прикладного кода! Если же в регистре = ненулевое значение, значит события от энкодера поступали — следует их обработать

В прикладном коде обработчика событияпросто приплюсуйте [или отминусуйте] значение из «cчётчика тиков» к вашей управляемой переменной величине.
Замечу, что значение в «счётчике тиков» — величина знаковая! Поэтому, в зависимости от знака, будет автоматически учтено направление вращения ручки энкодера: «по часовой» или «против часовой»…

И наконец, после обработки события энкодера, «СБРОСьте» регистр «Счётчика тиков» в ноль (но, при этом, «статусный регистр» энкодера не трогайте!)…

Комментарии закрыты.