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

Рейтинг пользователей: / 7
ХудшийЛучший 
Статьи - Программирование

gas

Лучше 3 дня потерять, зато потом за пол-часа долететь!
Из мультфильма «Крылья, ноги, хвост...»

GAS – это не полезное ископаемое, а GNU ASSEMBLER, что означает свободно распространяемый ассемблер. Ну а проекты наши, как обычно, это проекты для микроконтроллеров AVR, поэтому речь пойдет о версии GAS для AVR, то есть AVR-AS.

AVR-AS входит в комплект WinAVR, именно он в конечном итоге транслирует результат работы компилятора Си в объектные файлы, хотя для конечного пользователя этот процесс и остается незаметным в большинстве случаев. Если в проекте имеются ассемблерные модули – это так же работа для AVR-AS, однако, никто и ничто не препятствует использовать его для целиком ассемблерных проектов.

Любители писать программы для AVR на ассемблере вынуждены работать по сути с безальтернативным ассемблером, поставляемым фирмой Atmel вместе с IDE AVR Studio. Сама студия – весьма удобный продукт, а вот ассемблер... А вот ассемблер подкачал. Даже его «продвинутая» версия под номером 2 имеет весьма убогие возможности, что не делает жизнь программиста проще. Использование же AVR-AS для разработки программ позволит во многих случаях сделать труд программиста более комфортным.

Ассемблер – это низкоуровневый язык, по существу предоставляющий всего лишь некое удобное средство записи машинных кодов. Такое мнение бытует у многих программистов, но оно верно лишь отчасти. Хороший ассемблер может иметь в своем арсенале средства, при помощи которых процесс разработки ассемблерных программ приближается к работе программиста Си. Например, таким ассемблером можно считать MASM или TASM для 80x86 процессоров – они позволяют использовать при программировании даже объектно-ориентированные подходы! Но для AVR все это по большей части лишнее, но все-таки немного комфорта не помешает.

А основной источник комфорта для ассемблерного программиста – это макросы. Те, кто привык к макросам из avrassembler от Atmel, скажут: «Подумаешь, макрос! Это всего лишь автозамена нескольких команд одной!». Увы, в том ассемблере это действительно так, но в рассматриваемом AVR-AS макрос – это очень мощное средство! Подчас настолько мощное, что программисту без опыта работы с ним все его «навороты» кажутся совершенно бесполезными, так как совершенно непонятны. Макроязык в AVR-AS – это уже почти язык высокого уровня, только используется для составления инструкций не микроконтроллеру, а компилятору. То есть эти макросы по сути позволяют создавать программу по ходу ее компиляции!

Рассмотрим простой пример: допустим, нам требуется иметь массив структур, состоящих из трех байтов, причем он должен быть проинициализирован определенными повторяющимися значениями. На Си это будет выглядеть примерно так:

unsigned char array[][] = {
{1,2,3}, {1,2,3}, {1,2,3}
};

Способ записи самый обычный, но только представьте себе, как бы программист поступил, если бы ему потребовалось таким образом проинициализировать массив из 200 элементов?! Даже для Си с его препроцессором описать такой массив в программе очень непросто, и чаще всего сводится к рутинному ручному вводу кучи одинаковых значений. Одно радует – редакторы позволяют сделать это довольно просто, но вид программы от этого лучше не становится.

А теперь посмотрите, как описание такого массива из 200 (!!!) повторяющихся структур будет выглядеть в программе для AVR-AS:

array:
.rept	200
.byte	1,2,3
.endr

Как видите, это даже проще, чем на Си. А все благодаря макро-оператору (или псевдооператору) rept, который заставляет компилятор повторить свое содержимое указанное число раз – в нашем случае 200. В итоге «в руки» компилятору попадет такой исходник:

array:
.byte	1,2,3
.byte	1,2,3
... и так еще 198 раз

Получается, компилятор сам построил программу за программиста, руководствуясь заданием – разве это не напоминает работу с высокоуровневыми языками программирования?!

Псевдооператор rept – очень прост, но почему-то его нет и не было в Atmel-овском ассемблере... Однако, возможности AVR-AS не сводятся только к этому! В нем имеется большое количество возможностей и псевдооператоров, при помощи которых можно строить собственные чрезвычайно гибкие макросы.

Кратко рассмотрим некоторые из этих возможностей.

Макросы в привычном смысле

Обычный макрос определяется ключевым словом .macro и завершается ключевым словом .endm – тут все так же, как и в avrassembler. А вот теперь разница:

  • макрос может иметь именованные параметры, перечисляемые после имени макроса, причем внутри макроса с этими параметрами можно работать как с символами или из значением;
  • для параметров макроса можно задать некоторые атрибуты, например, указать, что какой-то параметр обязательно должен присутствовать, а какой-то или какие-то являются необязательными (аналог многоточия в параметрах сишной функции);
  • работу макроса можно завершить досрочно при помощи псевдооператора .exitm (аналог break в Си).

Например:

.macro pushwc	data, reg
// макрос занесения в стек константного значения с использованием вспомогательного регистра
ldi	reg, lo8(\data)
push	reg
ldi	reg, hi8(\data)
push	reg
.endm

Этот макрос называется pushwc и имеет 2 параметра: data и reg, причем первый параметр – это число, а второй – вспомогательный регистр. Макрос загружает число data в стек, начиная с младшего байта. Обратите внимание на то, что перед указанием имени параметра ставится косая черта – это признак того, что берется ЗНАЧЕНИЕ параметра. Если черту убрать, то будет использован СИМВОЛ параметра.

Вот макрос с параметрами, имеющими значение по умолчанию:

.macro pushwc	data, reg=r18
.endm

Для упрощения содержимое макроса не показано. В этом макросе мы задаем наличие двух параметров, причем второй имеет значение по умолчанию, равное r18. Теперь мы можем использовать в программе макрос так:

pushwc	1234,

и тогда параметр reg будет использовать значение r18, но можем и указать иное значение второго параметра:

pushwc	1234, r20

Можно потребовать, чтобы часть параметров были обязательными, а часть – по усмотрению программиста:

.macro pushwc	data:req, reg

Такое описание указывает, что параметр data всегда должен быть задан, в то время как о параметре reg ничего не говорится, т.е. он может и отсутствовать.

Для макросов с произвольным числом параметров нужно использовать такую форму:

.macro pushwc	arg:vararg

В данном случае атрибут vararg для параметра arg обозначает, что этот параметр содержит в себе все параметры до конца строки. Разумеется, этот параметр может быть либо единственным, либо последним среди прочих:

.macro pushwc	n, reg, arg:vararg // правильно
.macro pushwc	n, arg:vararg, reg // не правильно

О том, как использовать произвольное число параметров макроса, будет рассказано позже, после рассмотрения итераторов.

Итераторы

Итератор – это псевдооператор ассемблера, позволяющий выполнить некую последовательнсоть действий определенное число раз. Итераторы могут быть простыми или итераторами с перебором параметров. Завершается итератор ключевым словом .endr, а начинается с ключевого слова, определяющего его тип.

Простой итератор .rept

Записиывается так:

.rept	N
// повторяемые действия
.endr

Означает, что повторяемые действия надо просто повторить N раз. В качестве этих действий могут быть любые операторы или псевдооператоры ассемблера, а так же макросы. Пример использования этого итератора мы уже рассмотрели ранее.

Итератор с перебором параметров .irp

Записывается так:

.irp	var, <список аргументов>
// повторяемые действия
.endr

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

.irp	reg, r0,r2,r4,r6
push	reg
.endr

После исполнения макроса получится следующее:

push	r0
push	r2
push	r4
push	r6

То есть итератор поочередно использует перечисленные регистры для сохранения их в стеке. Так как нет ограничений на количество параметров в списке, напрашивается идея использования этого итератора в комплексе с макросом с переменным числом аргументов:

.macro		pushnow	arg:vararg
.irp	reg, \arg
push	reg
.endr
.endm

В программе мы просто обратимся к макросу pushnow с перечислением сохраняемых регистров, и они окажутся сохраненными в стеке.

Итератор с перебором символов .irpc

Этот итератор очень похож на предыдущий, с той лишь разницей, что перебор ведется не по списку параметров, а по символам строки, переданной в качестве параметра:

.irps	c, 0123
push	r\c
.endr

В результате работы этого макроса получатся следующие команды:

push	r0
push	r1
push	r2
push	r3

Кроме итераторов и макросов в AVR-AS имеется большой набор операторов для различных функций.

Оператор присваивания .set

Записывается так:

.set	<символ>,<значение>

Назначает символу новое значение, например:

.set	tmp, r0 // делает tmp синонимом r0.

В качестве значения можно использовать выражения с использованием математических и логических операторов, записываемых в стиле Си, причем имеется возможность использовать предыдущее значение символа (учтите, что символ – это не переменная, под него не выделяется память или регистр процессора, это информационная единица, существующая только для компилятора):

.set	var, 0	// присваивает var нулевое значение
.set	var, 12 // присваивает var значение 12
.set	var, var + 3 // присваивает var значение 15

Самое главное: при математических вычислениях AVR-AS использует для чисел разрядность хост-платформы, то есть если в Си для AVR при вычислениях по умолчанию используется int (16 бит со знаком), то в AVR-AS для Windows будут использоваться числа 32 бита со знаком.

Оператор проверки условия .if

Этот оператор имеет классическую форму записи в стиле Си или несколько альтернативных вариантов. Классическая запись:

.if <выражение>
// действия, если выражение НЕ РАВНО НУЛЮ
.endif

или

.if <выражение>
// действия, если выражение НЕ РАВНО НУЛЮ
.else
// действия, если выражение РАВНО НУЛЮ
.endif

Выражение здесь понимается, как в Си: оно может состоять из математических и/или логических опреторов, причем результат вычисляется по правилам Си и ненулевое значение считается ИСТИНОЙ, а нулевое – ЛОЖЬЮ.

Альтернативные варианты служат для упрощения записи некоторых характерных проверок.

.ifeq <выражение> // если выражение эквивалентно нулю
.ifne <выражение> // если выражение не эквивалентно нулю
.ifdef <символ> // если символ определен
.ifndef <символ> // если символ не определен
.ifb <аргумент> // если аргумент ПУСТ
.ifnb <аргумент> // если аргумент НЕ ПУСТ
.ifc <строка1>, <строка2> // если строки 1 и 2 совпадают посимвольно (с учетом регистра).
 

Строки могут быть в кавычках или без них, но следует помнить, что для строк без кавычек символы-разделители будут обозначать конец строки, поэтому если строка содержит пробел или запятую, она должна заключаться в кавычки.

Так же имеются вариант для условий больше нуля, меньше или равно и т.д. – ввиду простой возможности их заменить классической формой оператора, они не рассматриваюстя.

Рассмотренных возможностей уже достаточно для составления весьма гибких макросов.

Пример, который, безусловно, существенно облегчит жизнь программисту.

В обработчике прерываний необходимо сохранить контекст программы. Это означает, что при входе в обработчик надо сохранить состояние регистра SREG, и так же всех регистров, используемых в самом обработчике, а перед выходом из него – восстановить их значения. Обычно для этих целей используется стек. Главная «грабля» в этом случае заключается в том, что порядок сохранения регистров в стеке должен быть обратным по отношению к порядку извлечения регистров из стека, иначе ничего не будет работать. Если в обработчике используется 1-2 регистра – ошибиться с порядком их восстановления сложно, но если их требуется больше – проблема уже становится заметной. Да и вручную писать многочисленные push-pop не очень-то приятно.

Поставим задачу: сделать пару взаимодополняющих макросов ENTER и LEAVE, первый из которых будет сохранять в стеке любые указанные регистры, а второй – восстанавливать их. Кроме того, эти макросы должны без лишних телодвижений сохранять SREG. Названия макросов взяты по аналогии с командами от 80x86-процессоров, выполняющих почти то же самое. Для макроса ENTER должно использоваться переменное число параметров, а для LEAVE параметры вообще не должны использоваться – он сам должен уметь извлекать из стека нужные регистры в нужном порядке.

Тогда обработчик прерывания будет оформляться примерно так:

TIMER0_OVF_vect:
ENTER	r2, r5, r16, ZL, ZH
// решаем свои задачи с использованием перечисленных регистров
LEAVE
reti

Согласитесь, элегантно? Ну, тогда займемся составлением макросов.

Прежде всего, разберемся с тем, как вообще эту задачу мы решали бы на абстрактном языке высокого уровня. Вырисовывается примерно такой алгоритм для ENTER: сначала сохраним в стеке SREG, а затем будем перебирать все параметры макроса, каким-то образом помечая найденные регистры, и сохранять их в стеке. Для LEAVE напрашивается такой алгоритм: в качестве исходных данных используем «отметки», сделанные в ENTER, перебираем эти отметки и для каждого отмеченного регистра делаем восстановление из стека (перебор ведем в обратном порядке по отношению к ENTER), а затем восстанавливаем SREG.

Запишем скелет алгоритмов:

.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

Как видите, наиболее сложные места я пока описал в виде комментариев. Очевидно, что косвенным эффектом нашего алгоритма является неизбежное сохранение r0, поэтому при использовании макроса ENTER можно не беспокоиться о нем, т.е. r0 будет сохранен даже при использовании макроса без параметров.

У AVR 32 рабочих регистра, что просто требует от нас отмечать сохраненный в стеке регистр битом с соответствующим номером. Отметка бита будет делаться традиционно, как мы привыкли в Си – при помощи логического ИЛИ:

.set	selector, selector | (1<< reg) // reg - это номер регистра

Ну а проверка, соответственно, при помощи логического И:

.ifne	selector & (1<<reg)

Остается лишь разобраться с тем, как перебирать параметры и биты. Тут мы приходим к необходимости организации циклов. Простой итератор нам не подходит, т.к. заранее число параметров нам не известно, а итератор с перебором работает и без этого знания – но нам-то нужно знать НОМЕР регистра! Причем в аргументах регистры мы можем передать в любом порядке, поэтому придется определять номер очередного регистра так же перебором среди всех имеющихся.

Пусть очередной параметр у нас хранится в переменной reg. Как же определить НОМЕР соответствующего регистра? Только путем перебора среди всех доступных регистров:

.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, соответствующий параметру reg. Теперь все это можно добавить в скелет макроса ENTER:

.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

Остается выполнить заключительную часть макроса – снова перебрать, но уже биты selector, и сохранить в стеке регистры, для которых бит установлен. Это сделать совсем просто:

.set	i, 1
.rept	31
.ifne selector & (1<< i)
push	i
.endif
.set	i, i+1
.endr

Итак, макрос ENTER готов! Вот как он выглядит:

.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

После сделанного макрос LEAVE делается ну совсем элементарно:

.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

Эти макросы можно сохранить в файле, который затем подключать к любому своему проекту директивой #include, и писать обработчики прерываний станет просто удовольствием! Ведь часто бывает так, что в процессе работы надо еще один регистр задействовать или наоборот, удается обойтись без сохраненного в стеке, и приходится править push-pop-ы. А теперь достаточно только подкорректировать обращение к макросу ENTER – и все, можно не беспокоиться о прочем!

Подтверждается известный из мультика принцип: «лучше три дня потерять, зато потом за пол-часа долететь!». Согласитесь, что с avrassembler-ом лететь не получилось бы вообще.

P.S. Данный материал был бы не возможен без помощи, оказанной человеком, широко известным в узких кругах под ником ReAl – именно ему принадлежит мысль использовать битовые отметки сохраненных регистров.

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



Темы форума

Нет сообщений для показа

Комментарии
Статистика
Просмотров:
mod_vvisit_counterСегодня3522
mod_vvisit_counterВчера5377
mod_vvisit_counterНа этой неделе13960
mod_vvisit_counterНа прошлой неделе32218
mod_vvisit_counterВ этом месяце60415
mod_vvisit_counterЗа все время16331502

Ваш IP: 54.226.227.175
 , 
13 Дек. 2017