Хабрахабр

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

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

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

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

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

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

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

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

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

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

Для указателя на пространство памяти ОЗУ в библиотеке предусмотрен свой тип данных — 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);

Если проще — к тому, как при помощи библиотеки запрограммировать условные и безусловные переходы и циклы. В этом примере мы воспользовались возможностью инкремента указателя при записи в память.
Далее мы переходим к возможностям библиотеки по управлению потоком команд. Метки в программе объявляются двумя различными способами. Наиболее простым способом управления является использование команд переходов к меткам. Label мы создаем метку для дальнейшего использования, но не вставляем ее в код программы. Первый заключается в том, что командой AVRASM. в тех случаях, когда команда перехода должна предшествовать метке. Этот способ используется для создания переходов вперед, т. е. Для перехода назад можно использовать более простой синтаксис, устанавливая метку и присваивая ее значение переменной одной командой 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, () => );
var t = AVRASM.Text(m);

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

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<Register, string> Condition, Action<Register, string> 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 выполняются только в первом цикле.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть