Print this page

Реализация меню на switch конструкции.

26/09/2009 - 21:00

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

   Вариант реализации меню, который я здесь предлагаю, основан на 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:
 
    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 конструкции. Не спрашивайте, почему я излагал материал именно так – я и сам не знаю. Просто в таком порядке мысли выливались на бумагу.
Теперь можно собрать все вышеизложенное воедино в виде блок схемы.
 
блок-схема обработчика нажатий кнопок