А у нас в проектах GAS. А у вас?

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 – именно ему принадлежит мысль использовать битовые отметки сохраненных регистров.

Комментарии   

#1 mag58 11.03.2013 18:38
Спавибо,очень интересно,а где ещё более подробно на русском можно об этом почитать?
#2 ARV 11.03.2013 23:05
а что именно интересует? я разбирался по обрывкам документации из WinAVR и с помощью ReAl-а.
#3 mag58 12.03.2013 15:45
А интересует именно отличие,и чем он лучше,
взять например,какой нить свой старый проект,написанн ый на ассемблере и написать то же самое но уже на GAS,правда теперь используя макросредства этого ассемблера и остальные приемущества,
"поиграться" самому ,посмотреть,и потом уже своё мнение сложить,удобнее мне,лучше или нет
ну например:
такое ли количество команд?
макросы имеют расширенные возможности,как я понял, ну вот в чём отличие
хотелось бы полный список этих отличий
и возможностей,
но как я понял,кроме Вас и ReAl
никто эту тему глубоко и не копал,
аидимо считая,ято если ты знаешь Си, то ассемблер это уже пройденный этап,и большего ожидать от него уже нечего,
но объективную оценку, так это или нет,полагаю ,может давать тот,кто перепробовал много чего САМ (MASM или TASM ),как Вы справедливо заметили ,и имеет возможность сравнивать.
#4 ARV 16.03.2013 15:23
в применении к AVR основное достоинство GAS заключается в элементарной интеграции ассемблерных модулей с Сишными исходниками. что касается команд и т.п., то тут все ассемблеры одинаковы, т.к. ассемблерная команда есть эквивалент машинного кода, поэтому одна и та же программа в итоге будет скомпилирована в одни и те же машинные команды. а отличия от изместного мне avrassemler-а я уже изложил в статье: совершенно иная система макросов с повышенной гибкостью. остальное уже менее существенно.
если есть еще вопросы, лучше обсуждать их на форуме, там это более удобно
#5 mag58 16.03.2013 18:34
Конечно есть интерес,я уже хочу пробовать :-) ,тем более вот эта статья чем привлекательна и полезна, она позволяет плавно перейти от практики программировани я на языке ассемблера к языку СИ и наоборот.
есть люди которые начали программировани е,освоив сразу СИ,и по этой причине не любят ассемблер,равно ,как есть те кто начинал с ассемблера и хотят научиться на СИ,а уж тут то,оперировать врезками на ассемблере,это вообще здорово,обладая навыками и в том и другом,языках,и применяя эти сочетания в зависимости от целессообразнос ти при написании программы. Не так много я видел статей на подобные темы, тем более по WINAWR у вас тут на ресурсе выложено прекрасное,на мой взгляд,пособие.

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

Обсудить на форуме (0 комментариев).

Copyright 2011 © simple-defices.ru.
При использовании материалов ссылка на www.simple-devices.ru обязательна.