В моем понимании меню – это часть интерфейса осуществляющего взаимодействие между пользователем и устройством. В обязанности этого интерфейса входит обработка нажатий кнопок, вывод информации на дисплей и выполнение различных действий инициированных пользователем. Меню имеет графическое отображение, которое представляет из себя одноуровневую или многоуровневую иерархическую структуру и часть этой структуры обычно видна на дисплее.
Вариант реализации меню, который я здесь предлагаю, основан на switch конструкции. В языке Си switch – это один из операторов ветвления. Он позволяет осуществлять выбор между несколькими заданными альтернативами. Поскольку ноги Си растут из ассемблера, в ассемблере можно реализовать точно такой же оператор, ну а точнее конструкцию. Пример ассемблерного варианта switch приведен ниже.
Switch:
Case1:
cpi r16, 1
brne Case2
rcall func1
ret
Case2:
cpi r16, 2
brne Case3
rcall func2
ret
Case3:
cpi r16, 3
brne Default
rcall func3
ret
Default:
rcall func4
ret
Эта конструкция оформлена в виде подпрограммы названной Switch и работает следующим образом. Значение регистра r16 последовательно сравнивается с константами (1, 2, 3). Если значение r16 не равно константе - микроконтроллер переходит на следующую метку (Case2, Case3, Default), если равно – запускает соответствующую продпрограмму (func1, func2, func3), выполняет ее и выходит из подрограммы Switch. На метку Default микроконтроллер попадает, только когда сравнит значение r16 со всеми константами и не обнаружит ни одного совпадения.
Ветви подпрограммы Switch (куски от метки Case до команды ret) можно оформить в виде макроса и тогда наращивать switch конструкцию станет намного удобнее.
Например, так:
//@0 – константа с которой происходит сравнение,
//@1 – подпрограмма вызываемая в данной ветви
.MACRO Case
cpi r16, @0
brne ExitCase
rcall @1
ret
ExitCase:
.ENDMACRO
……
Switch:
Switch:
Case1:
cpi r16, 1
brne Case2
rcall func1
ret
Case2:
cpi r16, 2
brne Case3
rcall func2
ret
Case3:
cpi r16, 3
brne Default
rcall func3
ret
Default:
rcall func4
ret
Эта конструкция оформлена в виде подпрограммы названной Switch и работает следующим образом. Значение регистра r16 последовательно сравнивается с константами (1, 2, 3). Если значение r16 не равно константе - микроконтроллер переходит на следующую метку (Case2, Case3, Default), если равно – запускает соответствующую продпрограмму (func1, func2, func3), выполняет ее и выходит из подрограммы Switch. На метку Default микроконтроллер попадает, только когда сравнит значение r16 со всеми константами и не обнаружит ни одного совпадения.
Ветви подпрограммы Switch (куски от метки Case до команды ret) можно оформить в виде макроса и тогда наращивать switch конструкцию станет намного удобнее.
Например, так:
//@0 – константа с которой происходит сравнение,
//@1 – подпрограмма вызываемая в данной ветви
.MACRO Case
cpi r16, @0
brne ExitCase
rcall @1
ret
ExitCase:
.ENDMACRO
……
Switch:
Case 1, func1
Case 2, func2
Case 3, func3
Default:
rcall func4
ret
……
Навигация по меню и запуск каких-то действий в микроконтроллерном девайсе осуществляется с помощью кнопок. Поэтому программа микроконтроллера имеет подпрограммы обрабатывающие их нажатия (обработчики). Собственно на switch конструкции и построен обработчик нажатия одной кнопки.
Проще всего разобраться в его работе на примере. Допустим, у нас есть одноуровневое меню из трех пунктов, которые имеют нумерацию 1, 2, 3. Есть кнопка Up, нажатие которой вызывает перемещение вверх по пунктам меню. Вот как будет выглядеть обработчик этой кнопки.
.def CurrentState = r16
.equ pCurrentState = 0x60
HandlerUpButton:
lds CurrentState, pCurrentState
Case1:
cpi CurrentState, 1
brne Case2
rcall func1
ldi CurrentState, 1
rjmp ChangeState
Case2:
cpi CurrentState, 2
brne Case3
rcall func2
ldi CurrentState, 1
rjmp ChangeState
Case3:
cpi CurrentState, 3
brne ExitHandler
rcall func3
ldi CurrentState, 2
rjmp ChangeState
ExitHandler:
ret
ChangeState:
sts pCurrentState, CurrentState
ret
pCurrentState – это адрес в оперативной памяти микроконтроллера. По этому адресу хранится номер пункта меню, в котором мы находимся в данный момент. В начале обработчика микроконтроллер считывает это значение и записывает его в переменную CurrentState. Далее начинается последовательный обход switch конструкции, каждая ветвь которой выполняет обработку нажатия кнопки Up в конкретном пункте меню. Как только микроконтроллер находит нужную ветвь, он вызывает подпрограмму. Потом записывает в переменную CurrentState номер пункта меню, в который мы должны перейти после завершения работы обработчика, переходит на метку ChangeState и сохраняет значение CurrentState в ОЗУ. Если микроконтроллер не найдет нужную ветвь, то он просто выйдет из подпрограммы HandlerUpButton.
А вот какая логика работы будет у меню. Если мы находимся в первом пункте – нажатие кнопки Up вызывает выполнение подпрограммы func1, но не вызывает перехода. Если мы находимся во втором пункте меню – нажатие Up вызывает переход в первый пункт и вызывает выполнение подпрограммы func2. И наконец, если мы находимся в третьем пункте – нажатие Up вызывает переход во второй пункт меню и выполнение подпрограммы func3.
Вообще запись обработчика в таком виде довольно громоздка и не наглядна. Исправить положение можно с помощью макроса.
.def CurrentState = r16
.equ pCurrentState = 0x60
.MACRO Case
cpi CurrentState, @0
brne ExitCase
rcall @2
ldi CurrentState, @1
rjmp ChangeState
ExitCase:
.ENDMACRO
Case – макрос одной ветви конструкции switch, где @0 – номер пункта меню, @1 – константа задающая переход, @2 – подпрограмма
Вот как будет выглядеть обработчик кнопки Up с использованием макросов.
HandlerUpButton:
lds CurrentState, pCurrentState
Case 1, 1, func1
Case 2, 1, func2
Case 3, 2, func3
ret
ChangeState:
sts pCurrentState, CurrentState
ret
Обработчик теперь стал куда более наглядным. Видно, к какому пункту меню относится каждая ветвь, куда нужно перейти и какую подпрограмму вызвать. И плюс ко всему обработчик теперь легко модифицировать и наращивать.
Одних обработчиков кнопок недостаточно - нужна еще подпрограмма (диспетчер), которая будет их вызывать, в зависимости от того какая кнопка нажата. Эту подпрограмму можно тоже в принципе сделать на switch конструкции, но лучше на таблицах перехода. Это ускорит вызов обработчика. Вот как может выглядеть такая подпрограмма для четырех кнопок – Up, Down, Enter, Cancel
.def tmp = r16
.equ pButtonBuf= 0x61
Main:
//проверяем кнопочный буфер
lds tmp, pButtonBuf
cpi tmp, 0
breq Main
cpi tmp, 5
brsh ClearBufButtons
//вычисляем адрес
//и прыгаем на него
dec tmp
lsl tmp
ldi ZL, Low(CheckButtons)
ldi ZH, High (CheckButtons)
add ZL, tmp
ijmp
CheckButtons:
rcall HandlerUpButton
rjmp ClearBufButtons
rcall HandlerDownButton
rjmp ClearBufButtons
rcall HandlerEnterButton
rjmp ClearBufButtons
rcall HandlerCancelButton
//rjmp ClearBufButtons
//очищаем кнопочный буфер
//возвращаемся на Main
ClearBufButtons:
clr tmp
sts pButtonBuf, tmp
rjmp main
PButtonBuf - это адрес буфера в ОЗУ, в котором сохраняется номер нажатой кнопки. У нас 4 кнопки и каждая имеет свой номер: Up - 1, Down - 2, Enter - 3, Cancel – 4. В начале обработчика микроконтроллер считывает содержимое буфера и проверяет его на нулевое значение, если оно равно нулю – прыгает на метку Main, если нет – проверяет что содержащееся в буфере число не выходит за заданный диапазон. Это сделано для подстраховки. Затем в регистровую пару ZH:ZL загружается адрес метки CheckButtons и добавляется смещение. Смещение предварительно вычисляется так (номер кнопки – 1)*2. По команде ijmp микроконтроллер прыгает на нужный обработчик. А после выхода из него на метку ClearBufButtons, для того чтобы очистить кнопочный буфер.
Действую от обратного, мы разобрали идеологию реализации меню на switch конструкции. Не спрашивайте, почему я излагал материал именно так – я и сам не знаю. Просто в таком порядке мысли выливались на бумагу.
Теперь можно собрать все вышеизложенное воедино в виде блок схемы.
Case 2, func2
Case 3, func3
Default:
rcall func4
ret
……
Навигация по меню и запуск каких-то действий в микроконтроллерном девайсе осуществляется с помощью кнопок. Поэтому программа микроконтроллера имеет подпрограммы обрабатывающие их нажатия (обработчики). Собственно на switch конструкции и построен обработчик нажатия одной кнопки.
Проще всего разобраться в его работе на примере. Допустим, у нас есть одноуровневое меню из трех пунктов, которые имеют нумерацию 1, 2, 3. Есть кнопка Up, нажатие которой вызывает перемещение вверх по пунктам меню. Вот как будет выглядеть обработчик этой кнопки.
.def CurrentState = r16
.equ pCurrentState = 0x60
HandlerUpButton:
lds CurrentState, pCurrentState
Case1:
cpi CurrentState, 1
brne Case2
rcall func1
ldi CurrentState, 1
rjmp ChangeState
Case2:
cpi CurrentState, 2
brne Case3
rcall func2
ldi CurrentState, 1
rjmp ChangeState
Case3:
cpi CurrentState, 3
brne ExitHandler
rcall func3
ldi CurrentState, 2
rjmp ChangeState
ExitHandler:
ret
ChangeState:
sts pCurrentState, CurrentState
ret
pCurrentState – это адрес в оперативной памяти микроконтроллера. По этому адресу хранится номер пункта меню, в котором мы находимся в данный момент. В начале обработчика микроконтроллер считывает это значение и записывает его в переменную CurrentState. Далее начинается последовательный обход switch конструкции, каждая ветвь которой выполняет обработку нажатия кнопки Up в конкретном пункте меню. Как только микроконтроллер находит нужную ветвь, он вызывает подпрограмму. Потом записывает в переменную CurrentState номер пункта меню, в который мы должны перейти после завершения работы обработчика, переходит на метку ChangeState и сохраняет значение CurrentState в ОЗУ. Если микроконтроллер не найдет нужную ветвь, то он просто выйдет из подпрограммы HandlerUpButton.
А вот какая логика работы будет у меню. Если мы находимся в первом пункте – нажатие кнопки Up вызывает выполнение подпрограммы func1, но не вызывает перехода. Если мы находимся во втором пункте меню – нажатие Up вызывает переход в первый пункт и вызывает выполнение подпрограммы func2. И наконец, если мы находимся в третьем пункте – нажатие Up вызывает переход во второй пункт меню и выполнение подпрограммы func3.
Вообще запись обработчика в таком виде довольно громоздка и не наглядна. Исправить положение можно с помощью макроса.
.def CurrentState = r16
.equ pCurrentState = 0x60
.MACRO Case
cpi CurrentState, @0
brne ExitCase
rcall @2
ldi CurrentState, @1
rjmp ChangeState
ExitCase:
.ENDMACRO
Case – макрос одной ветви конструкции switch, где @0 – номер пункта меню, @1 – константа задающая переход, @2 – подпрограмма
Вот как будет выглядеть обработчик кнопки Up с использованием макросов.
HandlerUpButton:
lds CurrentState, pCurrentState
Case 1, 1, func1
Case 2, 1, func2
Case 3, 2, func3
ret
ChangeState:
sts pCurrentState, CurrentState
ret
Обработчик теперь стал куда более наглядным. Видно, к какому пункту меню относится каждая ветвь, куда нужно перейти и какую подпрограмму вызвать. И плюс ко всему обработчик теперь легко модифицировать и наращивать.
Одних обработчиков кнопок недостаточно - нужна еще подпрограмма (диспетчер), которая будет их вызывать, в зависимости от того какая кнопка нажата. Эту подпрограмму можно тоже в принципе сделать на switch конструкции, но лучше на таблицах перехода. Это ускорит вызов обработчика. Вот как может выглядеть такая подпрограмма для четырех кнопок – Up, Down, Enter, Cancel
.def tmp = r16
.equ pButtonBuf= 0x61
Main:
//проверяем кнопочный буфер
lds tmp, pButtonBuf
cpi tmp, 0
breq Main
cpi tmp, 5
brsh ClearBufButtons
//вычисляем адрес
//и прыгаем на него
dec tmp
lsl tmp
ldi ZL, Low(CheckButtons)
ldi ZH, High (CheckButtons)
add ZL, tmp
ijmp
CheckButtons:
rcall HandlerUpButton
rjmp ClearBufButtons
rcall HandlerDownButton
rjmp ClearBufButtons
rcall HandlerEnterButton
rjmp ClearBufButtons
rcall HandlerCancelButton
//rjmp ClearBufButtons
//очищаем кнопочный буфер
//возвращаемся на Main
ClearBufButtons:
clr tmp
sts pButtonBuf, tmp
rjmp main
PButtonBuf - это адрес буфера в ОЗУ, в котором сохраняется номер нажатой кнопки. У нас 4 кнопки и каждая имеет свой номер: Up - 1, Down - 2, Enter - 3, Cancel – 4. В начале обработчика микроконтроллер считывает содержимое буфера и проверяет его на нулевое значение, если оно равно нулю – прыгает на метку Main, если нет – проверяет что содержащееся в буфере число не выходит за заданный диапазон. Это сделано для подстраховки. Затем в регистровую пару ZH:ZL загружается адрес метки CheckButtons и добавляется смещение. Смещение предварительно вычисляется так (номер кнопки – 1)*2. По команде ijmp микроконтроллер прыгает на нужный обработчик. А после выхода из него на метку ClearBufButtons, для того чтобы очистить кнопочный буфер.
Действую от обратного, мы разобрали идеологию реализации меню на switch конструкции. Не спрашивайте, почему я излагал материал именно так – я и сам не знаю. Просто в таком порядке мысли выливались на бумагу.
Теперь можно собрать все вышеизложенное воедино в виде блок схемы.