Реализация меню на таблицах. Теория и практический пример

Теория

   Память программ микроконтроллеров AVR, помимо своего прямого предназначения может быть использована для хранения константных данных. Для этого в AVR ассемблере есть специальные директивы .db и .dw. Первая определяет константный байт или несколько байтов, вторая константное слово или несколько слов. (слово - это 2 байта). Для того чтобы указать, что заданные константы расположены во flash памяти микроконтроллера,  используется директива .cseg  

Пример:

.cseg              //эта директива определяет начало сегмента памяти программ
                     // данные следующие после  нее будут размещены во flash

   
//массив байтов
key1:    .db 12,66,7,19,26

//строка
text:    .db “hi gringo”

//массив шестнадцатиразрядных слов
key2:    .dw 0xff12, 0x0134, 0x3056, 0x01ff

//массив указателей на подпрограммы
func:    .dw Led1On, Led1Off, Led2On, Led2Off
 
  С помощью этих директив можно организовывать во flash памяти массивы данных, массивы указателей на подпрограммы, многомерные массивы, а также структуры и массивы структур. Массив - это набор данных одного типа последовательно размещенных в памяти, тогда как структура – это набор данных разного типа. Конечно, тип данных, массив, структура - термины языков высокого уровня, в частности Си. В ассемблере никаких типов данных нет. Просто эту терминологию удобно использовать, чтобы как-то разграничить обычные данные и указатели на подпрограммы.
   Память программ микроконтроллеров AVR имеет 16-разрядную организацию.  Доступ к данным в памяти программ осуществляется с помощью команды lpm и индексного регистра Z. При этом старшие 15 разрядов содержимого регистра определяют адрес 16-разрядного слова, а младший разряд определяет, какой из байтов слова будет прочитан: 0 – младший байт, 1 – старший байт.    

Пример использования инструкции lpm:


.cseg
key1:    .db 12,66,7,19,26


ldi ZH, High(key1<<1)    //инициализируем индексный регистр
ldi ZL, Low(key1<<1)     //адресом метки key1
lpm r16, Z+                   //считываем в r16 первый байт массива – 12
                                    //значение Z увеличивается на единицу   
lpm r16, Z                     //считывем в r16 второй байт массива - 66


  Итак, мы можем хранить во flash памяти данные и имеем к ним доступ. Важно четко понимать, как это происходит.

Практика

   Я долго думал как изложить этот материал. Привести сначала абстрактный пример, а потом рабочий, как в статье про switch или сразу разбирать что-то конкретное. В конце концов остановился на втором варианте. Сам по себе знаю как трудно пробираться через дебри чужого ассемблерного кода, но что поделать. Напрягитесь и не будете разочарованы.
   Вообщем рассматривать меню на таблицах будем, используя предыдущий проект. Структура меню, функции кнопок и вся аппаратная часть осталась без изменений. В самом проекте изменились только 2 файла:

    Menu1.asm – главный файл проекта
    Menu.asm – файл содержащий обработчик кнопок и таблицы.

Новый проект можно сказать здесь.

   Программа, реализующая меню на switch`е, состояла из двух отдельных частей – диспетчера и четырех обработчиков. Диспетчер был построен с помощью инструкции косвенного перехода ijmp. А основу обработчиков составляли ветви switch`а построенные на макросе Case. Ему “передавались” три параметра – текущее состояние(где находимся сейчас), следующее состояние(куда перейти потом), имя подпрограммы. Ветви switch`а были зеркальным отражением таблицы, которую я приводил в предыдущей статье.
    В новом проекте диспетчер и обработчик нажатия кнопки совмещены в одной подпрограмме под названием HandlerMenu, а логика переходов по меню расписана в таблицах во флэш памяти микроконтроллера.

Таблицы

   У нас есть четыре кнопки – Up, Down, Enter, Cancel. Для каждой кнопки во flash памяти микроконтроллера существует своя таблица. Строки таблиц определены с помощью директивы .db и имеют следующую структуру.

1 байт
 2 байт  3 байт  4 байт

 Текущее состояние

(где находимся сейчас)

 Следующее состояние

(куда перейти потом)

 Младший байт

адреса подпрограммы

  Старший байт

адреса подпрограммы


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

.equ End = 255

HUpButton:
.db 1,1, Low(Empty), High(Empty)
.db End

где HUpButton – метка, название таблицы,
Low(Empty) – младший байт указателя на подпрограмму Empty,
High(Empty) – старший байт указателя на подпрограмму Empty,
End – маркер конца таблицы.

   Запись вида Low(Empty), High(Empty) довольно  неудобна, поэтому я изменил ее с помощью директивы препроцессора define. Благо AVR ассемблер поддерживает такие директивы

#define func(x)    Low(x),High(x)

Теперь таблицу для кнопки Up можно записать в таком виде

#define func(x)    Low(x),High(x)
.equ End = 255

HUpButton:
.db 1,1, func(Empty)
.db 2,1, func(lcdSelectLed1)
.db 3,3, func(Empty)
.db 4,3, func(lcdSelectLed1)
.db 5,5, func(Empty)
.db 6,5, func(lcdSelectLed1)
.db End

   Довольно наглядная запись. (Кстати, это чистой воды массив Си структур).
  Еще во flash памяти микроконтроллера есть массив, в котором содержатся указатели на таблицы кнопок. Он определен с помощью директивы .dw

Handlers:
.dw HUpButton, HDownButton, HEnterButton, HCancelButton

Обработчик

Обработчик имеет следующий вид.

HandlerMenu:
        //вычисляем указатель на таблицу
        dec r16
        clr zl
        clr zh
        mov zl,R16
        lsl zl
        subi zl, LOW(-(Handlers<<1))
        sbci zh, HIGH(-(Handlers<<1))
        lpm R16, Z+                           
        lpm R17, Z
        movw ZH:ZL, R17:R16
        lsl zl
        rol zh
       
        //ищем нужную строку таблицы
        lds r19, pCurrentState
    CheckState:       
        lpm r16, z
        cp r16, r19
        breq ChangeState
        cpi r16, End
        breq ExitHM
        adiw Z, 4
        rjmp CheckState   

        //если нашли – то меняем текущее состояние
        //и вызываем подпрограмму  
     
    ChangeState:
        adiw Z, 1
        lpm r16, Z+
        sts pCurrentState, r16
        lpm R16, Z+                           
        lpm R17, Z
        movw ZH:ZL, R17:R16
        icall   
    ExitHM:
        ret   


   Разберем алгоритм его работы

   Регистр r16 содержит считанный из ОЗУ номер кнопки. Он помещается туда перед вызовом обработчика HandlerMenu. Номер кнопки используется как смещение для доступа к соответствующему элементу массива Handlers. Кнопки имеют номера от 1 до 4. А смещение имеет диапазон от 0 до 3. (потому что элементы массива нумеруются начиная с нулевого) Итак, мы декрементируем значение r16, очищаем регистровую пару ZH:ZL,  загружаем в нее значение r16 и сдвигаем ZL.

        dec r16
        clr zl
        clr zh
        mov zl,R16
        lsl zl

   Теперь нам нужно сложить адрес метки Handlers и смещение. В системе команд микроконтроллера AVR нет команды сложения регистра с константой. Инструкцию adiw не в счет, потому что она не может прибавить к регистру больше 63. Выход из положения следующий. Вычитаем отрицательное значение адреса метки из регистров ZH:ZL. Минус на минус дает плюс и в итоге получается, что мы на самом деле складываем адрес метки со смещением.

        subi zl, LOW(-(Handlers<<1))
        sbci zh, HIGH(-(Handlers<<1))

Считываем указатель на нужную таблицу в r16 и r17, копируем в регистровую пару ZH:ZL и сдвигаем влево.

        lpm R16, Z+                           
        lpm R17, Z
        movw ZH:ZL, R17:R16
        lsl zl
        rol zh

   Понятно? Мы взяли адрес адрес метки Handlers, добавили смещение и считали из памяти программ адрес метки таблицы.
    Ок. Теперь переходим к поиску нужной строки таблицы.
    Считываем из ОЗУ текущее состояние, а затем в цикле сравниваем его с первым байтом строки таблицы. Если они не равны – проверяем, не дошли ли мы до маркера конца таблицы. Если дошли - выходим из обработчика, если не дошли – прибавляем к регистровой паре ZH:ZL смещение. Здесь то нам и пригодилась команда adiw. Каждая строка таблицы состоит из четырех байтов, и вся таблица расположена во flash памяти непрерывно. Собственно, поэтому мы и имеем доступ к ее строкам, зная только начальный адрес и значение смещения. 

        lds r19, pCurrentState
    CheckState:       
        lpm r16, z
        cp r16, r19
        breq ChangeState
        cpi r16, End
        breq ExitHM
        adiw Z, 4
        rjmp CheckState

   На метку ChangeState микроконтроллер попадает, если нашел нужную строчку в таблице. Здесь мы считываем второй байт строки, который определяет следующее состояние, и записываем его в ОЗУ по адресу pCurrentState. Далее считываем указатель на подпрограмму (это 3 и 4 байты) и вызываем ее.

    ChangeState:
        adiw Z, 1
        lpm r16, Z+
        sts pCurrentState, r16
        lpm R16, Z+                           
        lpm R17, Z
        movw ZH:ZL, R17:R16
        icall   

Вот и все, обработчик закончил свое дело.
 

 

У вас недостаточно прав для комментирования.