Библиотека генератора ассемблерного кода для микроконтроллеров AVR
Часть 2. Начало работы
Как и планировалось, в этой части рассмотрим более подробно особенности программирования с использованием библиотеки NanoRTOS. Те, кто начал чтение с этого поста, могут ознакомиться с общим описанием и возможностями библиотеки в предыдущей статье. В силу ограниченности планируемого объема публикации предполагается, что уважаемый читатель хотя бы в минимальном объеме знаком с программированием на C#, а так же имеет представление об архитектуре и программировании на языке ассемблер для контроллеров AVR серии Mega.
Изучение любой технологии лучше всего совмещать с выполнением примеров, поэтому предлагаю загрузить с https://drive.google.com/open?id=1FfBBpxlJkWC027ikYpn6NXbOGp7DS-5B саму библиотеку и попробовать ее подключить к проекту консольного приложения. В случае успеха, можно смело двигаться дальше. В качестве оболочки для выполнения примеров можно использовать любое приложение C# или проект UnitTest. Мне лично больше нравится последнее, так как позволяет хранить в одном месте несколько разных примеров и выполнять их по мере необходимости. В любом случае для экономии места в листинги будет включаться только сам текст примера.
Работа с библиотекой всегда должна начинаться с объявления типа микроконтроллера. Так как параметры и набор периферийных устройств зависит от типа контроллера, выбор конкретного контроллера влияет на формирование ассемблерного кода. Строка объявления контроллера для которого пишется программа выглядит следующим образом
var m = new Mega328();
Далее могут следовать дополнительные настройки микроконтроллера, такие, как параметры тактирования или назначение системных функций для выводов. Например, разрешение использовать аппаратный сброс исключает использование вывода в качестве порта. Все параметры контроллера имеют значения по умолчанию, и в примерах мы будем их опускать, кроме тех случаев, когда это важно, но в реальных проектах я советую их всегда устанавливать. Например, настройка тактирования может выглядеть следующим образом
m.FCLK = 16000000;
m.CKDIV8 = false;
Эта настройка означает, что микроконтроллер тактируется кварцевым резонатором или внешним источником с частотой 16MHz, а предделитель частоты для периферийных устройств выключен.
За вывод результатов работы отвечает функция Text статического класса AVRASM. Эта фунция всегда будет вызываться в конце кода для вывода результата в виде ассемблера. Назначенный ранее экземпляр класса контроллера, функция получает в виде параметра. Таким образом простейший каркас программы для работы с библиотекой приобретает следующий вид
var m = new Mega328(); // Объявляем экземпляр класса микроконтроллера
// Здесь мы размещаем настройки микроконтроллера
// Здесь мы размещаем тело программы генерации кода для микроконтроллера
var t = AVRASM.Text(m); //присваиваем результат строковой переменной t
Если мы попробуем запустить программу, она должна успешно выполниться, но при этом не создаст никакого кода. При всей бесполезности результата, это тем не менее дает основание сделать вывод, что библиотека не генерит никакого оберточного кода.
Пустую программу мы уже научились создавать. Теперь попробуем создать в ней какой-нибудь код. Начнем с самого примитивного. Посмотрим как можно решить задачу инкремента восьмибитной переменной, расположенной в произвольной ячейке РОН. С точки зрения ассемблера это команда inc [register] . Вставим в тело программы нашей заготовки следующие строки
var r = m.REG();
r++;
Назначение команд вполне очевидно. Первая команда связывает переменную r с одним из регистров процессора. Вторая команда говорит о необходимости инкремента этой переменной. После выполнения получаем первый результат выполнения кода.
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF R0000 = r20
inc R0000
.DSEG
Взглянем поподробнее что в результате получилось. Первые четыре команды — инициализация указателя стека. Далее — определение имени переменной. Ну и наконец наш inc, ради которого все и затевалось. Ничего лишнего, за исключением инициализации стека не появилось. Вопрос, который может возникнуть, глядя в этот код — что за R0000? У нас же переменная названа r? В C# программе программист может вполне осознано и легально использовать одни и те же имена, пользуясь областями видимости. С точки зрения ассемблера — все метки и определения должны быть уникальными. Чтобы не заставлять программиста следить за уникальностью имен, по умолчанию имена генерятся системой. Однако существует ситуация, когда в целях отладки все-таки хочется перенести в выходной код осознанное имя из программы, для того, чтобы его можно было легко найти. Не страшно. Заменим m.REG() на m.REG(”r”) и еще раз запустим код. В результате увидим следующее
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF r = r20
inc r
.DSEG
Итак, с именованием регистров разобрались. Теперь разберемся, почему вдруг регистры начали назначаться с 20, а не с 0? Для того, чтобы ответить на этот вопрос вспомним, что начиная с 16, у регистров появляется замечательная возможность инициализировать их константами. А так, как эта возможность весьма востребована, мы и начинаем раздачу с верхней половины, чтобы увеличить возможности оптимизации. Тогда все равно не понятно — почему с20 а не с 16? Причина в том, что трансляция ряда команд в ассемблерный код невозможна без использования ячеек временного хранения. Вот для этих целей мы и выделили 4 ячейки с 16 по 19. Это не означает, что они стали абсолютно недоступными для программиста. Просто доступ к ним организован немного иначе, чтобы программист отдавал себе отчет о возможных ограничениях их использования и действовал осознанно. Уберем из кода определение регистра r и заменим следующую за ним строку на
m.TempL++;
Посмотрим на результат
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
inc TempL
.DSEG
Здесь, видимо, следует отметить, что выходной ассемблер требует для правильной интерпретации подключения файла с определениями и макросами Common.inc из пакета разработки. В нем собственно и прописаны все необходимые макросы и определения, в том числе и соответствия имен для ячеек временного хранения. А именно TempL = r16, TempH =r17, TempQL=r18,TempQH=r19. В данном случае мы не задействовали ни одной команды, которые бы использовали для работы ячейки временного хранения, поэтому наше решение использовать в операции TempL вполне допустимо. А как следует поступить, если мы абсолютно уверенны в том, что никаких присвоений констант нашей переменной не светит и мы не хотим на нее тратить драгоценные ячейки верхней половины? Вернем наше определение в исходный код, изменив его на var r = m.REGL("r"); и оценим результат труда
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF r = r4
inc r
.DSEG
Цель достигнута. Нам удалось объяснить библиотеке где по нашему мнению должна размещаться переменная. Поехали дальше. Посмотрим, что произойдет, если объявить сразу несколько переменных. Скопируем наше определение и действия еще пару раз. Для разнообразия одну новую переменную обнулим, а значение другой уменьшим на 1. В результате должно получится нечто подобное.
var m = new Mega328();
var r = m.REGL("r");
r++;
var rr = m.REGL("rr");
rr--;
var rrr = m.REGL("rrr");
rrr.Clear();
var t = AVRASM.Text(m);
Посмотрим результат.
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF r = r4
inc r
.DEF rr = r5
dec rr
.DEF rrr = r6
clr rrr
.DSEG
Замечательно. То, что и просили. А теперь посмотрим, как нам освободить регистр для других целей, если он нам больше не нужен. Здесь, к сожалению, пока все руками. Правила C# о границах видимости и автоматическом освобождении переменных при выходе за границу для режима генерации кода пока не работают. Посмотрим как все-таки можно освободить ячейку при необходимости. Добавим в нашу программу всего одну строку и посмотрим на результат.
var m = new Mega328();
var r = m.REGL("r");
r++;
var rr = m.REGL("rr");
rr--;
r.Dispose();
var rrr = m.REGL("rrr");
rrr.Clear();
var t = AVRASM.Text(m);
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF r = r4
inc r
.DEF rr = r5
dec rr
.UNDEF r
.DEF rrr = r4
clr rrr
.DSEG
Нетрудно заметить, что освобожденный нами четвертый регистр стал вновь доступен для использования. С учетом того, что каждое новое объявление переменной захватывает регистр, можно сделать вывод, что при составлении программы нужно вовремя освобождать регистры, если не хочется столкнуться с ситуацией, когда их станет не хватать.
Разбирая примеры, мы попутно уже продемонстрировали, как можно выполнять на регистрах одноадресные операции. Теперь посмотрим как у нас обстоят дела с многоадресными. Архитектура процессора позволяет выполнять максимум двухадресные команды(для особо въедливых — за исключением двух команд, для которых результат размещается в фиксированных регистрах). Это следует понимать таким образом, что первый операнд в операции после ее выполнения будет содержать результат. Для этого вида операций предусмотрен специальный синтаксис [register1][operation]=[register2]. Посмотрим как это выглядит на практике. Попробуем объявить и сложить две регистровых переменных.
var m = new Mega328();
var op1 = m.REG();
var op2 = m.REG();
op1 += op2;
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
add R0000,R0001
.DSEG
Получили то, что и ожидали. Можно самостоятельно поэкспериментировать с операциями -, &, | и убедиться, что результат не хуже.
Всего вышеперечисленного пока явно не достаточно, чтобы написать даже простейшую программу. Дело в том, что мы пока не касались инициализации самих регистров. Архитектура микроконтроллера позволяет инициализировать регистры константой, значением другого регистра, значением ячейки памяти ОЗУ по определенному адресу, значением ячейки памяти ОЗУ по указателю, размещенному в специальной паре регистров, значением ячейки ввода-вывода по определенному адресу, а так же значением ячейки памяти программы по указателю, размещенному в специальной паре регистров. С косвенной адресацией будем разбираться позже, а пока рассмотрим более простые случаи. Напишем и выполним следующую тестовую программу.
var m = new Mega328();
var op1 = m.REG();
var op2 = m.REG();
op1.Load(0x10);
op2.Load('s');
op1.Load(op2);
var t = AVRASM.Text(m);
Здесь мы объявили и проинициализировали числом и символом две переменных, а затем значение переменной op2 скопировали в ячейку op1. Очевидно, что число должно укладываться в диапазон 0-255, чтобы не возникло ошибки. Результатом будет
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF R0000 = r20
.DEF R0001 = r21
ldi R0000,16
ldi R0001,'s'
mov R0000,R0001
.DSEG
Из примера видно, что для всех перечисленных операций используется одна функция, а библиотека сама формирует правильный набор ассемблерных команд. Как уже неоднократно упоминалось, прямая загрузка данных в регистр командой ldi доступна только для старшей половины регистров. Усложним нашей библиотеке задачу, изменив программу так, чтобы она выделила регистры для переменных из младшей половины.
var m = new Mega328();
var op1 = m.REGL();
var op2 = m.REGL();
op1.Load(0x10);
op2.Load('s');
op1.Load(op2);
var t = AVRASM.Text(m);
Получим
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF R0000 = r4
.DEF R0001 = r5
ldi TempL,16
mov R0000,TempL
ldi TempL,'s'
mov R0001,TempL
mov R0000,R0001
.DSEG
Библиотека справилась и с этой задачей, затратив при этом минимально возможное количество команд. А мы заодно увидели, для чего нам понадобилось выделять регистры временного хранения. Ну и наконец посмотрим как реализована математика для работы с константами. Мы знаем о существовании ассемблерной команды subi для вычитания констатнты из регистра, а теперь попробуем ее описать в терминах библиотеки.
var m = new Mega328();
var op1 = m.REG();
op1.Load(0x10);
op1 -= 10;
var t = AVRASM.Text(m);
Результатом будет
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF R0000 = r20
ldi R0000,16
subi R0000,0x0A
.DSEG
Получилось. А как поведет себя библиотека, если нет ассемблерной команды, которая бы умела выполнять нужную операцию? Например, если мы захотим не отнять, а прибавить константу. Попробуем и посмотрим
var m = new Mega328();
var op1 = m.REG();
op1.Load(0x10);
op1 += 10;
var t = AVRASM.Text(m);
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF R0000 = r20
ldi R0000,16
subi R0000,0xF6
.DSEG
Библиотека выкрутилась вычитанием отрицательного значения. Посмотрим, как обстоят дела со сдвигом. Сдвинем значение регистра на 5 вправо.
var m = new Mega328();
var op1 = m.REG();
op1.Load(0x10);
op1 >>= 5;
var t = AVRASM.Text(m);
Результатом будет
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
.DEF R0000 = r20
ldi R0000,16
swap R0000
andi R0000,15
lsr R0000
.DSEG
Здесь не все очевидно, зато выполняется на 2 команды быстрее, чем если бы использовалось лобовое решение из пяти команд сдвига.
Итак, в этой статье мы рассмотрели применение библиотеки для работы с регистровой арифметикой. В следующей статье мы продолжим описание работы библиотеки работой с указателями и рассмотрим методы управления потоком исполнения команд (циклы, переходы и т.п.)
Комментарии (5)
Shtucer
06.08.2019 21:20Библиотека выкрутилась вычитанием отрицательного значения. Посмотрим, как обстоят дела со сдвигом.
Хочется чего-нибудь поделить. Да, я понимаю, что если хочется чего-нибудь поделить на аврке, то прежде нужно крепко подумать, и, скорее всего, перехотеть. Но иногда просто хочется, прям сил нет. Или умножить чего-нибудь, например, на 257.
Alexeyslav
07.08.2019 09:46А что тут удивительного? Библиотеки для умножения и деления уже написаны, их надо включить в состав будущей программы и операция деления/умножения сведётся к вызову соответствующей функции с предварительным присвоением регистрам нужных переменных и сохранением результата. Возможно, в некоторых случаях, использовать сокращённые формы умножения/деления на степени двойки но это не принципиально.
Shtucer
07.08.2019 10:01Так это-то и интересно: все вышеприведённые примеры вручную, по памяти, можно применять. Да что там, с ними и gcc справится без вмешательства со стороны.
Alexeyslav
07.08.2019 10:29Это как раз ничем не отличается от примеров. Просто вызов сторонней процедуры.
Что должно быть гораздо интересней — это обработка прерываний, ветвление и циклы.
u_235
Вы предлагаете скачать исполняемый модуль неизвестного происхождения и использовать его при запуске приложения? Я то думал, будут исходники.