- Родительская категория: Статьи
- Категория: Программирование
- Автор: ARV
- Просмотров: 15571
- Печать
А у нас в проектах GAS. А у вас?
Лучше 3 дня потерять, зато потом за пол-часа долететь!
Из мультфильма «Крылья, ноги, хвост...»
GAS – это не полезное ископаемое, а GNU ASSEMBLER, что означает свободно распространяемый ассемблер. Ну а проекты наши, как обычно, это проекты для микроконтроллеров AVR, поэтому речь пойдет о версии GAS для AVR, то есть AVR-AS.
AVR-AS входит в комплект WinAVR, именно он в конечном итоге транслирует результат работы компилятора Си в объектные файлы, хотя для конечного пользователя этот процесс и остается незаметным в большинстве случаев. Если в проекте имеются ассемблерные модули – это так же работа для AVR-AS, однако, никто и ничто не препятствует использовать его для целиком ассемблерных проектов.
Любители писать программы для AVR на ассемблере вынуждены работать по сути с безальтернативным ассемблером, поставляемым фирмой Atmel вместе с IDE AVR Studio. Сама студия – весьма удобный продукт, а вот ассемблер... А вот ассемблер подкачал. Даже его «продвинутая» версия под номером 2 имеет весьма убогие возможности, что не делает жизнь программиста проще. Использование же AVR-AS для разработки программ позволит во многих случаях сделать труд программиста более комфортным.
{ads2}Ассемблер – это низкоуровневый язык, по существу предоставляющий всего лишь некое удобное средство записи машинных кодов. Такое мнение бытует у многих программистов, но оно верно лишь отчасти. Хороший ассемблер может иметь в своем арсенале средства, при помощи которых процесс разработки ассемблерных программ приближается к работе программиста Си. Например, таким ассемблером можно считать MASM или TASM для 80x86 процессоров – они позволяют использовать при программировании даже объектно-ориентированные подходы! Но для AVR все это по большей части лишнее, но все-таки немного комфорта не помешает.
А основной источник комфорта для ассемблерного программиста – это макросы. Те, кто привык к макросам из avrassembler от Atmel, скажут: «Подумаешь, макрос! Это всего лишь автозамена нескольких команд одной!». Увы, в том ассемблере это действительно так, но в рассматриваемом AVR-AS макрос – это очень мощное средство! Подчас настолько мощное, что программисту без опыта работы с ним все его «навороты» кажутся совершенно бесполезными, так как совершенно непонятны. Макроязык в AVR-AS – это уже почти язык высокого уровня, только используется для составления инструкций не микроконтроллеру, а компилятору. То есть эти макросы по сути позволяют создавать программу по ходу ее компиляции!
Рассмотрим простой пример: допустим, нам требуется иметь массив структур, состоящих из трех байтов, причем он должен быть проинициализирован определенными повторяющимися значениями. На Си это будет выглядеть примерно так:
{code}unsigned char array[][] = {
{1,2,3}, {1,2,3}, {1,2,3}
};{/code}
Способ записи самый обычный, но только представьте себе, как бы программист поступил, если бы ему потребовалось таким образом проинициализировать массив из 200 элементов?! Даже для Си с его препроцессором описать такой массив в программе очень непросто, и чаще всего сводится к рутинному ручному вводу кучи одинаковых значений. Одно радует – редакторы позволяют сделать это довольно просто, но вид программы от этого лучше не становится.
А теперь посмотрите, как описание такого массива из 200 (!!!) повторяющихся структур будет выглядеть в программе для AVR-AS:
{code}array:
.rept 200
.byte 1,2,3
.endr{/code}
Как видите, это даже проще, чем на Си. А все благодаря макро-оператору (или псевдооператору) rept, который заставляет компилятор повторить свое содержимое указанное число раз – в нашем случае 200. В итоге «в руки» компилятору попадет такой исходник:
{code}array:
.byte 1,2,3
.byte 1,2,3
... и так еще 198 раз{/code}
Получается, компилятор сам построил программу за программиста, руководствуясь заданием – разве это не напоминает работу с высокоуровневыми языками программирования?!
Псевдооператор rept – очень прост, но почему-то его нет и не было в Atmel-овском ассемблере... Однако, возможности AVR-AS не сводятся только к этому! В нем имеется большое количество возможностей и псевдооператоров, при помощи которых можно строить собственные чрезвычайно гибкие макросы.
Кратко рассмотрим некоторые из этих возможностей.
Макросы в привычном смысле
Обычный макрос определяется ключевым словом .macro и завершается ключевым словом .endm – тут все так же, как и в avrassembler. А вот теперь разница:
- макрос может иметь именованные параметры, перечисляемые после имени макроса, причем внутри макроса с этими параметрами можно работать как с символами или из значением;
- для параметров макроса можно задать некоторые атрибуты, например, указать, что какой-то параметр обязательно должен присутствовать, а какой-то или какие-то являются необязательными (аналог многоточия в параметрах сишной функции);
- работу макроса можно завершить досрочно при помощи псевдооператора .exitm (аналог break в Си).
Например:
{ads1}
{code}.macro pushwc data, reg
// макрос занесения в стек константного значения с использованием вспомогательного регистра
ldi reg, lo8(\data)
push reg
ldi reg, hi8(\data)
push reg
.endm{/code}
Этот макрос называется pushwc и имеет 2 параметра: data и reg, причем первый параметр – это число, а второй – вспомогательный регистр. Макрос загружает число data в стек, начиная с младшего байта. Обратите внимание на то, что перед указанием имени параметра ставится косая черта – это признак того, что берется ЗНАЧЕНИЕ параметра. Если черту убрать, то будет использован СИМВОЛ параметра.
Вот макрос с параметрами, имеющими значение по умолчанию:
{code}.macro pushwc data, reg=r18
.endm{/code}
Для упрощения содержимое макроса не показано. В этом макросе мы задаем наличие двух параметров, причем второй имеет значение по умолчанию, равное r18. Теперь мы можем использовать в программе макрос так:
{code}pushwc 1234,{/code}
и тогда параметр reg будет использовать значение r18, но можем и указать иное значение второго параметра:
{code}pushwc 1234, r20{/code}
Можно потребовать, чтобы часть параметров были обязательными, а часть – по усмотрению программиста:
{code}.macro pushwc data:req, reg{/code}
Такое описание указывает, что параметр data всегда должен быть задан, в то время как о параметре reg ничего не говорится, т.е. он может и отсутствовать.
Для макросов с произвольным числом параметров нужно использовать такую форму:
{code}.macro pushwc arg:vararg{/code}
В данном случае атрибут vararg для параметра arg обозначает, что этот параметр содержит в себе все параметры до конца строки. Разумеется, этот параметр может быть либо единственным, либо последним среди прочих:
{code}.macro pushwc n, reg, arg:vararg // правильно
.macro pushwc n, arg:vararg, reg // не правильно{/code}
О том, как использовать произвольное число параметров макроса, будет рассказано позже, после рассмотрения итераторов.
Итераторы
Итератор – это псевдооператор ассемблера, позволяющий выполнить некую последовательнсоть действий определенное число раз. Итераторы могут быть простыми или итераторами с перебором параметров. Завершается итератор ключевым словом .endr, а начинается с ключевого слова, определяющего его тип.
Простой итератор .rept
Записиывается так:
{code}.rept N
// повторяемые действия
.endr{/code}
Означает, что повторяемые действия надо просто повторить N раз. В качестве этих действий могут быть любые операторы или псевдооператоры ассемблера, а так же макросы. Пример использования этого итератора мы уже рассмотрели ранее.
Итератор с перебором параметров .irp
Записывается так:
{code}.irp var, <список аргументов>
// повторяемые действия
.endr{/code}
Этот макрос берет первый аргумент из списка и присваивает его значение параметру var, выполняет повторяемые действия, затем берет следующий аргумент из списка и так далее, пока список не кончится. То есть итератор перебирает все переданные ему аргументы, позволяя внутри повторяемых действий обработать каждый по-своему. Вот простой пример:
{code}.irp reg, r0,r2,r4,r6
push reg
.endr{/code}
После исполнения макроса получится следующее:
{code}push r0
push r2
push r4
push r6{/code}
То есть итератор поочередно использует перечисленные регистры для сохранения их в стеке. Так как нет ограничений на количество параметров в списке, напрашивается идея использования этого итератора в комплексе с макросом с переменным числом аргументов:
{code}.macro pushnow arg:vararg
.irp reg, \arg
push reg
.endr
.endm{/code}
В программе мы просто обратимся к макросу pushnow с перечислением сохраняемых регистров, и они окажутся сохраненными в стеке.
Итератор с перебором символов .irpc
Этот итератор очень похож на предыдущий, с той лишь разницей, что перебор ведется не по списку параметров, а по символам строки, переданной в качестве параметра:
{code}.irps c, 0123
push r\c
.endr{/code}
В результате работы этого макроса получатся следующие команды:
{code}push r0
push r1
push r2
push r3{/code}
Кроме итераторов и макросов в AVR-AS имеется большой набор операторов для различных функций.
Оператор присваивания .set
Записывается так:
{code}.set <символ>,<значение>{/code}
Назначает символу новое значение, например:
{code}.set tmp, r0 // делает tmp синонимом r0.{/code}
В качестве значения можно использовать выражения с использованием математических и логических операторов, записываемых в стиле Си, причем имеется возможность использовать предыдущее значение символа (учтите, что символ – это не переменная, под него не выделяется память или регистр процессора, это информационная единица, существующая только для компилятора):
{code}.set var, 0 // присваивает var нулевое значение
.set var, 12 // присваивает var значение 12
.set var, var + 3 // присваивает var значение 15{/code}
{ads1}
Самое главное: при математических вычислениях AVR-AS использует для чисел разрядность хост-платформы, то есть если в Си для AVR при вычислениях по умолчанию используется int (16 бит со знаком), то в AVR-AS для Windows будут использоваться числа 32 бита со знаком.
Оператор проверки условия .if
Этот оператор имеет классическую форму записи в стиле Си или несколько альтернативных вариантов. Классическая запись:
{code}.if <выражение>
// действия, если выражение НЕ РАВНО НУЛЮ
.endif{/code}
или
{code}.if <выражение>
// действия, если выражение НЕ РАВНО НУЛЮ
.else
// действия, если выражение РАВНО НУЛЮ
.endif{/code}
Выражение здесь понимается, как в Си: оно может состоять из математических и/или логических опреторов, причем результат вычисляется по правилам Си и ненулевое значение считается ИСТИНОЙ, а нулевое – ЛОЖЬЮ.
Альтернативные варианты служат для упрощения записи некоторых характерных проверок.
{code}.ifeq <выражение> // если выражение эквивалентно нулю
.ifne <выражение> // если выражение не эквивалентно нулю
.ifdef <символ> // если символ определен
.ifndef <символ> // если символ не определен
.ifb <аргумент> // если аргумент ПУСТ
.ifnb <аргумент> // если аргумент НЕ ПУСТ
.ifc <строка1>, <строка2> // если строки 1 и 2 совпадают посимвольно (с учетом регистра).{/code}
Строки могут быть в кавычках или без них, но следует помнить, что для строк без кавычек символы-разделители будут обозначать конец строки, поэтому если строка содержит пробел или запятую, она должна заключаться в кавычки.
Так же имеются вариант для условий больше нуля, меньше или равно и т.д. – ввиду простой возможности их заменить классической формой оператора, они не рассматриваюстя.
Рассмотренных возможностей уже достаточно для составления весьма гибких макросов.
Пример, который, безусловно, существенно облегчит жизнь программисту.
В обработчике прерываний необходимо сохранить контекст программы. Это означает, что при входе в обработчик надо сохранить состояние регистра SREG, и так же всех регистров, используемых в самом обработчике, а перед выходом из него – восстановить их значения. Обычно для этих целей используется стек. Главная «грабля» в этом случае заключается в том, что порядок сохранения регистров в стеке должен быть обратным по отношению к порядку извлечения регистров из стека, иначе ничего не будет работать. Если в обработчике используется 1-2 регистра – ошибиться с порядком их восстановления сложно, но если их требуется больше – проблема уже становится заметной. Да и вручную писать многочисленные push-pop не очень-то приятно.
Поставим задачу: сделать пару взаимодополняющих макросов ENTER и LEAVE, первый из которых будет сохранять в стеке любые указанные регистры, а второй – восстанавливать их. Кроме того, эти макросы должны без лишних телодвижений сохранять SREG. Названия макросов взяты по аналогии с командами от 80x86-процессоров, выполняющих почти то же самое. Для макроса ENTER должно использоваться переменное число параметров, а для LEAVE параметры вообще не должны использоваться – он сам должен уметь извлекать из стека нужные регистры в нужном порядке.
Тогда обработчик прерывания будет оформляться примерно так:
{code}TIMER0_OVF_vect:
ENTER r2, r5, r16, ZL, ZH
// решаем свои задачи с использованием перечисленных регистров
LEAVE
reti{/code}
Согласитесь, элегантно? Ну, тогда займемся составлением макросов.
Прежде всего, разберемся с тем, как вообще эту задачу мы решали бы на абстрактном языке высокого уровня. Вырисовывается примерно такой алгоритм для ENTER: сначала сохраним в стеке SREG, а затем будем перебирать все параметры макроса, каким-то образом помечая найденные регистры, и сохранять их в стеке. Для LEAVE напрашивается такой алгоритм: в качестве исходных данных используем «отметки», сделанные в ENTER, перебираем эти отметки и для каждого отмеченного регистра делаем восстановление из стека (перебор ведем в обратном порядке по отношению к ENTER), а затем восстанавливаем SREG.
Запишем скелет алгоритмов:
{code}.set selector, 0 // в этой переменной будем отмечать нужные регистры
.macro ENTER arg:vararg // макрос имеет переменное число параметров
push r0 // всегда сохраняем r0
in r0, _SFR_IO_ADDR(SREG) // извлекаем значение SREG
push r0 // сохраняем его в стеке
// тут надо перебрать аргументы и отметить их битами в selector
// затем надо перебрать биты selector от МЛАДШЕГО к старшему и
// сохранить соответствующие регистры в стеке
.endm
.macro LEAVE
// тут надо перебрать биты selector от СТАРШЕГО к младшему и
// извлечь соответствующие регистры из стека
pop r0 // извлечем значение SREG
out _SFR_IO_ADDR(SREG), r0 // восстановим SREG
pop r0 // восстановим r0
.endm{/code}
Как видите, наиболее сложные места я пока описал в виде комментариев. Очевидно, что косвенным эффектом нашего алгоритма является неизбежное сохранение r0, поэтому при использовании макроса ENTER можно не беспокоиться о нем, т.е. r0 будет сохранен даже при использовании макроса без параметров.
У AVR 32 рабочих регистра, что просто требует от нас отмечать сохраненный в стеке регистр битом с соответствующим номером. Отметка бита будет делаться традиционно, как мы привыкли в Си – при помощи логического ИЛИ:
{code}.set selector, selector | (1<< reg) // reg - это номер регистра{/code}
Ну а проверка, соответственно, при помощи логического И:
{code}.ifne selector & (1<<reg){/code}
Остается лишь разобраться с тем, как перебирать параметры и биты. Тут мы приходим к необходимости организации циклов. Простой итератор нам не подходит, т.к. заранее число параметров нам не известно, а итератор с перебором работает и без этого знания – но нам-то нужно знать НОМЕР регистра! Причем в аргументах регистры мы можем передать в любом порядке, поэтому придется определять номер очередного регистра так же перебором среди всех имеющихся.
Пусть очередной параметр у нас хранится в переменной reg. Как же определить НОМЕР соответствующего регистра? Только путем перебора среди всех доступных регистров:
{code}.set i, 1
.irp p, r1, r2, r3 ... r30, r31 // надо перечислить все регистры по порядку!!!
.if p == \reg // если регистр совпадает с аргументом макроса
.set selector, selector | (1 << i) // сделаем отметку в нужном бите
.endif
.set i, i+1 // и продолжим счет
.endr{/code}
Вышеописанный макрос установит бит в selector, соответствующий параметру reg. Теперь все это можно добавить в скелет макроса ENTER:
{code}.set selector, 0 // в этой переменной будем отмечать нужные регистры
.macro ENTER arg:vararg // макрос имеет переменное число параметров
push r0 // всегда сохраняем r0
in r0, _SFR_IO_ADDR(SREG) // извлекаем значение SREG
push r0 // сохраняем его в стеке
// тут надо перебрать аргументы и отметить их битами в selector
.set i, 1
.irp p, r1, r2, r3 ... r30, r31 // надо перечислить все регистры по порядку!!!
.if p == \reg // если регистр совпадает с аргументом макроса
.set selector, selector | (1 << i) // сделаем отметку в нужном бите
.endif
.set i, i+1 // и продолжим счет
.endr
// затем надо перебрать биты selector от МЛАДШЕГО к старшему и
// сохранить соответствующие регистры в стеке
.endm{/code}
Остается выполнить заключительную часть макроса – снова перебрать, но уже биты selector, и сохранить в стеке регистры, для которых бит установлен. Это сделать совсем просто:
{code}.set i, 1
.rept 31
.ifne selector & (1<< i)
push i
.endif
.set i, i+1
.endr{/code}
Итак, макрос ENTER готов! Вот как он выглядит:
{code}.set selector, 0 // в этой переменной будем отмечать нужные регистры
.macro ENTER arg:vararg // макрос имеет переменное число параметров
push r0 // всегда сохраняем r0
in r0, _SFR_IO_ADDR(SREG) // извлекаем значение SREG
push r0 // сохраняем его в стеке
// тут надо перебрать аргументы и отметить их битами в selector
.set i, 1
.irp p, r1, r2, r3 ... r30, r31 // надо перечислить все регистры по порядку!!!
.if p == \reg // если регистр совпадает с аргументом макроса
.set selector, selector | (1 << i) // сделаем отметку в нужном бите
.endif
.set i, i+1 // и продолжим счет
.endr
// затем надо перебрать биты selector от МЛАДШЕГО к старшему и сохранить нужные регистры
.set i, 1
.rept 31
.ifne selector & (1<< i)
push i
.endif
.set i, i+1
.endr
.endm{/code}
После сделанного макрос LEAVE делается ну совсем элементарно:
{code}.macro LEAVE
.set i, 31
.rept 31
.ifne (1<< i)
// если бит в маске установлен - регистр извлекается из стека
pop i
.endif
// перебор битов ведется в обратном порядке!!!
.set i, i - 1
.endr
// всегда восстанавливаем SREG и R0
pop r0
out _SFR_IO_ADDR(SREG), r0
pop r0
.endm{/code}
Эти макросы можно сохранить в файле, который затем подключать к любому своему проекту директивой #include, и писать обработчики прерываний станет просто удовольствием! Ведь часто бывает так, что в процессе работы надо еще один регистр задействовать или наоборот, удается обойтись без сохраненного в стеке, и приходится править push-pop-ы. А теперь достаточно только подкорректировать обращение к макросу ENTER – и все, можно не беспокоиться о прочем!
{ads1}
Подтверждается известный из мультика принцип: «лучше три дня потерять, зато потом за пол-часа долететь!». Согласитесь, что с avrassembler-ом лететь не получилось бы вообще.
P.S. Данный материал был бы не возможен без помощи, оказанной человеком, широко известным в узких кругах под ником ReAl – именно ему принадлежит мысль использовать битовые отметки сохраненных регистров.