Logo

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

Обсудить эту статью на форуме (0 ответов).
Template Design © Joomla Templates | GavickPro. All rights reserved.