Библиотека генератора ассемблерного кода для микроконтроллеров AVR. Часть 3

?v=1

← Часть 2. Начало работы


Библиотека генератора ассемблерного кода для микроконтроллеров AVR


Часть 3. Косвенная адресация и управление потоком исполнения

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

К сожалению, разрядности ранее рассмотренных регистровых переменных явно не достаточно для того, чтобы их можно было использовать в качестве указателей памяти. Поэтому перед тем, как перейти непосредственно к разговору об указателях, рассмотрим еще один класс описания данных. Большинство команд в архитектуре AVR Mega рассчитано на работу только с регистровыми операндами, т е. и операнды и результат имеют размерность 8 бит. Однако существует и ряд операций, где два последовательно расположенных регистра РОН рассматриваются, как единый 16-разрядный регистр. Таких операций немного и они в основном и ориентированы на работу с указателями.

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

 var m = new Mega328();
 var dr1 = m.DREG();
 var dr2 = m.DREG();
 dr1.Load(0xAA55);
dr2.Load(0x55AA);
dr1++;
dr1--;
dr1 += 0x100;
dr1 += dr2;
dr2 *= dr1;
dr2 /= dr1;
var t = AVRASM.Text(m);

В этом примере мы командой DREG () объявили две 2-х байтных переменных, размещенных в регистровых парах. Следующими командами мы присвоили им начальное значение и выполнили ряд арифметических операций. Из примера видно, что синтаксис работы с регистровой парой во многом совпадает с работой с обычным регистром. Регистровую пару можно также рассматривать как переменную, состоящую из двух независимых регистров. Обращение к регистру, как к набору из двух 8-битных регистров, производится через свойство High для обращения к старшим 8 битам как к 8-битному регистру, и свойство Low для обращения к младшим 8 битам. Код при этом будет выглядеть следующим образом

var m = new Mega328();
var dr1 = m.DREG();
dr1.Load(0xAA55);
dr1.Low--;
dr1.High += dr1.Low;
var t = AVRASM.Text(m);

Как видно из примера, мы можем работать с High и Low как с независимыми регистровыми переменными, в том числе и выполнять между ними различные арифметические и логические операции.

Теперь, когда мы разобрались с переменными двойной длины, можно приступить к описанию работы с переменными в памяти. Библиотека позволяет работать с 8, 16-битными переменными и массивами байтов произвольной длины. Рассмотрим пример выделения пространства для переменных в оперативной памяти.

var m = new Mega328();
var bt = m.BYTE();  //8-битная переменная в памяти
var wd = m.WORD();  //16-битная переменная в памяти
var arr = m.ARRAY(16); //массив из 16 байт
var t = AVRASM.Text(m);

Посмотрим, что получилось.

RESET: ldi r16, high(RAMEND)
    out SPH,r16
    ldi r16, low(RAMEND)
    out SPL,r16
.DSEG
L0002:  .BYTE   16
L0001:  .BYTE   2
L0000:  .BYTE   1

В секции определения данных у нас появилось выделение памяти. Обратите внимание на то, что порядок выделения памяти отличается от порядка объявления переменных. Это не случайно. Выделение памяти под переменные происходит после сортировки в убывающним порядке по следующим признакам (в порядке убывания значимости) Максимальный делитель, кратный степени 2→ Размер выделяемой памяти. Это означает, что если мы захотим выделить 4 массива размером 64, 48,40 и 16 байт, то порядок выделения вне зависимости от порядка объявления будет выглядеть следующим образом:

Длина 64 — Максимальный делитель, кратный степени 2 = 64
Длина 48 — Максимальный делитель, кратный степени 2 = 16
Длина 16 — Максимальный делитель, кратный степени 2 = 16
Длина 40 — Максимальный делитель, кратный степени 2 = 8
Это сделано для того, чтобы упростить контроль границ массива
и уменьшить размер кода при операциях с указателями. Никаких операций с переменными в памяти мы непосредственно выполнить не можем, поэтому все, что нам доступно — чтение/запись в регистровые переменные. Простейшим способом работы с переменными в памяти является прямая адресация

var m = new Mega328();
var bt = m.BYTE(); //определили 8-битную переменную в памяти
var rr = m.REG(); // определили регистровую переменную
rr.Load(0xAA);  //в регистре rr установили значение 0xAA
rr.Mstore(bt);  //сохранили значение регистра в память
rr.Clear(); //очистили регистр
rr.Mload(bt);   //восстановили значение из памяти
var t = AVRASM.Text(m);

В этом примере мы объявили переменную в памяти и регистровую переменную. После этого мы присвоили переменной значение 0×55 и записали ее в переменную в памяти. Затем стерли и восстановили обратно.

Для работы с элементами массива мы используем следующий синтаксис

var rr = m.REG();
var arr = m.ARRAY(10);
rr.MLoad(arr[5]);

Нумерация элементов в массиве начинается с 0. Таким образом в приведенном примере в ячейку rr запишется значение 6 элемента массива.

Теперь можно перейти к косвенной адресации. Для указателя на пространство памяти ОЗУ в библиотеке предусмотрен свой тип данных — MEMPtr. Посмотрим, как мы можем его использовать. Изменим наш предыдущий пример так, чтобы работа с переменной в памяти велась через указатель.

var m = new Mega328();
var bt1 = m.BYTE();
var bt2  = m.BYTE();
var rr = m.REG();
var ptr = m.MEMPTR(); //создали указатель ptr
ptr.Load(bt1);      //ptr указывает на bt1
rr.Load(0xAA);      //регистр rr - 0xAA
ptr.MStore(rr);     //записали в bt1 0xAA
rr.Load(0x55);      //регистр rr - 0x55
ptr.Load(bt2);      //ptr указывает на bt2    
ptr.MStore(rr);     //записали в bt2 0x55
ptr.Load(bt1);      //ptr указывает на bt1
ptr.MLoad(rr);      //в регистр rr загрузили 0xAA
var t = AVRASM.Text(m);

Из текста видно, что мы сначала объявили указатель ptr, а затем выполнили с его помощью операции записи и чтения. Кроме возможности в процессе исполнения менять адрес чтения/записи в команде, использование указателя упрощает работу с массивами, совмещая операцию чтения/записи с инкрементом/декрементом указателя. Посмотрим на программу, которая умеет заполнять массив определенным значением.

var m = new Mega328();
var bt1 = m.ARRAY(4);  //объявили массив размером 4 байта 
var rr = m.REG();
var ptr = m.MEMPTR();
ptr.Load(bt1.Label);   //ptr указывает на bt1
rr.Load(0xAA);         //регистр rr - 0xAA
ptr.MStoreInc(rr);     //записали в bt1 0xAA
ptr.MStoreInc(rr);     //записали в bt1+1 0xAA
ptr.MStoreInc(rr);     //записали в bt1+2 0xAA
ptr.MStoreInc(rr);     //записали в bt1+3 0xAA
rr.Clear();
rr.MLoad(bt1[2]);      //загрузили в rr 3-ий элемент массива
var t = AVRASM.Text(m);

В этом примере мы воспользовались возможностью инкремента указателя при записи в память.
Далее мы переходим к возможностям библиотеки по управлению потоком команд. Если проще — к тому, как при помощи библиотеки запрограммировать условные и безусловные переходы и циклы. Наиболее простым способом управления является использование команд переходов к меткам. Метки в программе объявляются двумя различными способами. Первый заключается в том, что командой AVRASM.Label мы создаем метку для дальнейшего использования, но не вставляем ее в код программы. Этот способ используется для создания переходов вперед, т. е. в тех случаях, когда команда перехода должна предшествовать метке. Для установки метки в требуемое место ассемблерного кода, необходимо выполнить команду AVRASM.newLabel ([переменная ранее созданной метки]). Для перехода назад можно использовать более простой синтаксис, устанавливая метку и присваивая ее значение переменной одной командой AVRASM.newLabel () без параметров.

Самым простым видом перехода является безусловный переход. Для его вызова мы используем команду GO ([метка_перехода]). Посмотрим, как это выглядит на примере.

var m = new Mega328();
var r = m.REG();
//переход вперед
var lbl1 = AVRASM.Label;//объявляем переменную метки для использования в команде перехода
m.GO(lbl1);
r++;    //для наглядности добавляем команды
r++;
AVRASM.NewLabel(lbl1);//устанавливаем метку
//переход назад
var lbl2 = AVRASM.NewLabel();//объявляем и устанавливаем метку
r--;    //для наглядности добавляем команды
r--;
m.GO(lbl2);
var t = AVRASM.Text(m);

Более широкими возможностями по управлению потоком исполнения обладают условные переходы. Их поведение зависит от состояния флагов операций и это дает возможность управлять потоком операций в зависимости от результата их выполнения. В библиотеке для описания блока команд, которые должны выполняться только при определенных условиях, используется функция IF. Посмотрим это на примере.

var m = new Mega328();
var rr1 = m.REG();
var rr2 = m.REG();
rr1.Load(0x22);
rr2.Load(0x33);
m.IF(rr1 == rr2, () =>
             {
                 AVRASM.Comment("Здесь что-то происходит, если равно");
             });
var t = AVRASM.Text(m);

Так как синтаксис команды IF не совсем привычен, рассмотрим его подробнее. Первым аргументом здесь выступает условие перехода. Далее следует метод, в котором размещается блок кода, который следует исполнить, если условие выполняется. Вариантом функции является возможность описания альтернативной ветви, т. е. блока кода, который должен выполняться, если условие не выполнено. Дополнительно можно обратить внимание на функцию AVRASM.Comment (), при помощи которой мы можем добавлять комментарии в выходной ассемблер.

var m = new Mega328();
var rr1 = m.REG();
var rr2 = m.REG();
rr1.Load(0x22);
rr2.Load(0x33);
m.IF(rr1 == rr2, () =>
             {
                 AVRASM.Comment("Здесь что-то происходит, если равно");
             },()=>
             {
                 AVRASM.Comment("Здесь что-то происходит, если не равно");
             });
AVRASM.Comment("Прочий код");
var t = AVRASM.Text(m);

Результат в этом случае будет выглядеть следующим образом

RESET: ldi r16, high(RAMEND)
    out SPH,r16
    ldi r16, low(RAMEND)
    out SPL,r16
.DEF R0000 = r20
.DEF R0001 = r21
    ldi R0000,34
    ldi R0001,51
    cp  R0000,R0001
    brne    L0002
;--- Здесь что-то происходит, если равно ---
    xjmp    L0004
L0002:
;--- Здесь что-то происходит, если не равно ---
L0004:
;--- Прочий код ---
.DSEG

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

var m = new Mega328();
var rr1 = m.REG();
rr1.Load(0x22);
rr1--;
m.IFEMPTY(() =>AVRASM.Comment("Выполняем, если результат вычитания 0"));
var t = AVRASM.Text(m);

В этом примере функция IFEMPTY проверяет состояние флага Z после инкремента и выполняет код условного блока при достижении 0.
Самой гибкой в плане использования можно считать функцию LOOP. Она предназначена для удобного описания программных циклов. Рассмотрим ее сигнатуру

LOOP(Register iter, Action Condition, Action body)

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

m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { });

Результат компиляции приведен ниже

L0002:  xjmp    L0002

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

var m = new Mega328();
var rr1 = m.REG();
var rr2 = m.REG();
var arr = m.ARRAY(16);
var ptr = m.MEMPTR();
ptr.Load(arr[0]);   //установили указатель на начало массива
rr2.Load(16);       //число циклов для записи
rr1.Load(0xAA);     //значение для заполнения
m.LOOP(rr2, (r, l) => //rr2 используется как счетчик в цикле.
            {
                r--;            // декремент счетчика
                m.IFNOTEMPTY(l); // выходим, если закончили
            }, (r,l) => ptr.MStoreInc(rr1)); //заполняем массив значениями
var t = AVRASM.Text(m);

Выходной код у нас в этом случае будет выглядеть следующим образом

RESET: ldi r16, high(RAMEND)
    out SPH,r16
    ldi r16, low(RAMEND)
    out SPL,r16
.DEF R0000 = r20
.DEF R0001 = r21
    ldi YL, LOW(L0002+0)
    ldi YH, HIGH(L0002+0)
    ldi R0001,16
    ldi R0000,170
L0003:
    st  Y+,R0000
    dec R0001
    brne    L0003
L0004:
.DSEG
L0002:  .BYTE   16

Еще одним способом организации переходов, являются косвенно адресуемые переходы. Ближайший аналог в языках высокого уровня для них это указатель на функцию. Указатель в данном случае будет указывать не на пространство ОЗУ, а на программный код. Так как AVR имеет Гарвардскую архитектуру и для обращения к программной памяти используется свой специфический набор команд, в качестве указателя используется не описанный выше MEMPtr, а ROMPtr. Вариант использования косвенно адресуемых переходов можно проиллюстрировать следующим примером

var m = new Mega328();
var block1 = AVRASM.Label;
var block2 = AVRASM.Label;
var block3 = AVRASM.Label;
var ptr = m.ROMPTR();
ptr.Load(block1);
// Здесь начинается основной цикл
var loop = AVRASM.NewLabel();
AVRASM.Comment("Команды общего цикла приложения");  
m.GOIndirect(ptr);
// Здесь начинаются блоки, на которые передается управление
AVRASM.NewLabel(block1);
AVRASM.Comment("Команды блока 1");
ptr.Load(block2);
m.GO(loop);
AVRASM.NewLabel(block2);
AVRASM.Comment("Команды блока 2");
ptr.Load(block3);
m.GO(loop);
AVRASM.NewLabel(block3);
AVRASM.Comment("Команды блока 3");
ptr.Load(block1);
m.GO(loop);
var t = AVRASM.Text(m);

В этом примере мы имеем 3 блока команд. По завершению каждого блока управление передается назад в команду косвенно адресуемого перехода. Так как в конце блока команд мы каждый раз устанавливаем вектор перехода на новый блок, выполнение будет выглядеть следующим образом Блок1 → Блок2 → Блок3 → Блок1… и так далее по кругу. Данная команда, совместно с командами условных переходов, позволяет просто и удобно средствами языка описывать такие достаточно сложные алгоритмы, как машина состояний.

Более сложным вариантом косвенно адресуемого перехода является команда SWITCH. В ней для перехода используется не указатель на метку перехода, а указатель на переменную в памяти, в которой хранится адрес метки перехода.

var m = new Mega328();
var block1 = AVRASM.Label;
var block2 = AVRASM.Label;
var block3 = AVRASM.Label;
var arr = m.ARRAY(6);
var ptr = m.MEMPTR();
// заполняем таблицу переходов
m.Temp.Load(block1);
m.Temp.Store(arr[0]);
m.Temp.Load(block2);
m.Temp.Store(arr[2]);
m.Temp.Load(block3);
m.Temp.Store(arr[4]);
ptr.Load(arr[0]);
// Здесь начинается основной цикл
var loop = AVRASM.NewLabel();
m.SWITCH(ptr);
// Здесь начинаются блоки, на которые передается управление
AVRASM.NewLabel(block1);
AVRASM.Comment("Команды блока 1");
ptr.Load(arr[2]);  // изменили указатель к ячейке таблицы переходов
m.GO(loop);
AVRASM.NewLabel(block2);
AVRASM.Comment("Команды блока 2");
m.Temp.Load(block3);
ptr.MStore(m.Temp);  //заменили значение во второй ячейке таблицы переходов
m.GO(loop);
AVRASM.NewLabel(block3);
AVRASM.Comment("Команды блока 3");
ptr.Load(arr[0]); // изменили указатель к ячейке таблицы переходов
m.GO(loop);

В этом примере последовательность переходов будет следующая: Блок1 → Блок2 → Блок3 → Блок1 → Блок3 → Блок1 → Блок3 → Блок1… Мы смогли реализовать алгоритм, при котором команды Блок2 выполняются только в первом цикле.

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

© Habrahabr.ru