Волшебная сила макросов, или как облегчить жизнь ассемблерного программиста AVR
Про макросы в ассемблере написано много. И в документации, и в различных статьях. Но в большинстве случаев все сводится либо к простому перечислению директив с кратким описанием их функций, либо к набору разрозненных примеров готовых макросов.
Цель этой статьи — описать определенный подход к программированию на ассемблере для формирования максимально простого и читабельного кода с использованием макросов. В статье не будет описания синтаксиса отдельных команд и директив. Подробное описание уже дано производителем. Мы же сосредоточимся на том, как можно использовать эти возможности для решения конкретных задач.
Компания ATMEL в свое время постаралась и разработала линейку восьмиразрядных микроконтроллеров с очень качественной архитектурой и простой, но при этом очень мощной системой команд. Но, как известно, нет предела совершенству, и некоторых часто употребляемых инструкций не хватает. К счастью макроассемблер, любезно и абсолютно бесплатно предоставленный производителем, позволяет существенно упростить код за счет использования директив. Перед тем, как перейти непосредственно к макросам, выполним некоторые предварительные действия
Определение констант
.EQU FOSC = 16000000
.EQU CLK8 = 0
Эти два определения позволяют избавиться от «магических чисел» в макросах, где значения регистров рассчитываются, исходя из частоты процессора и состояния фьюза делителя периферии. Превое определение — частота кристалла процессора в герцах, второе — состояние делителя частоты периферии.
Именование регистров
.DEF TempL = r16
.DEF TempH = r17
.DEF TempQL = r18
.DEF TempQH = r19
.DEF AL = r0
.DEF AH = r1
.DEF AQL = r2
.DEF AQH = r3
Несколько избыточное на первый взгляд именование регистров, которые могут быть использованы в макросах. Сразу четыре регистра для Temp нужны, если мы будем иметь дело с 32-х разрядными значениями (например, в операциях перемножения двух 16-и разрядных чисел). Если мы уверены, что двух регистров временного хранения для использования в макросах нам достаточно, то TempQL и TempQH можно не определять. Определения для A нужны для макросов, использующих операции умножения. Необходимость в AQ отпадает, если с наших макросах мы не используем с 32-х разрядную арифметику.
Макросы для реализации простых команд
Теперь, когда мы разобрались с именованием регистров, приступим к реализации команд, которых не хватает и начнем с того, что попытаемся упростить существующие. Ассемблер AVR имеет одну неудобную особенность. Для ввода и вывода первые 64 порта используются команды in/out, а для остальных lds/sts. Для того, чтобы каждый раз не смотреть в документацию в поисках нужной команды для конкретного порта, создадим набор универсальных команд, которые самостоятельно будет подставлять нужные значения.
.MACRO XOUT
.IF @0<64
out @0,@1
.ELSE
sts @0,@1
.ENDIF
.ENDMACRO
.MACRO XIN
.IF @1<64
in @0,@1
.ELSE
lds @0,@1
.ENDIF
.ENDMACRO
Для того, чтобы подстановка работала правильно, в макросе используется условная компиляция. В случае, когда адрес порта менее 64, выполняется первая условная секция, в противном случае — вторая. Наши макросы полностью повторяют функционал стандартных команд работы с портами ввода-вывода, поэтому для обозначения того, что наша команда обладает расширенными возможностями добавим стандартному именованию префикс X.
Одной из самых распространенных команд, которые отсутствуют в ассемблере, но постоянно требуются, является команда записи констант в регистры ввода вывода. Реализация макроса для этой команды будет выглядеть следующим образом
.MACRO OUTI
ldi TempL,@1
.IF @0<64
out @0, TempL
.ELSE
sts @0, TempL
.ENDIF
.ENDMACRO
В данном случае название в макроса, чтобы не нарушать логику именования команд, добавим к стандартному наименованию постфикс I, используемый разработчиком для обозначения команд работы с константами. В этом макросе мы используем для работы ранее определенный регистр временного хранения TempL.
В ряде случаев требуется запись не одного регистра, а целой пары, хранящей 16-битное значение. Создадим по новый макрос для записи 16-битного значения в пару регистров ввода-вывода
.MACRO OUTIW
ldi TempL,HIGH(@1)
.IF @0<64
out @0H, TempL
.ELSE
sts @0H, TempL
.ENDIF
ldi TempL,LOW(@1)
.IF @0<64
out @0L, TempL
.ELSE
sts @0L, TempL
.ENDIF
.ENDMACRO
В этом макросе мы используем встроенные функции LOW и HIGH для выделения младшего и старшего байта из 16-и битного значения. В название макроса к команде добавим постфиксы I и W для обозначения того, что в данном случае команда работает с 16 битным значением (словом).
Не менее часто в программах встречается загрузка регистровых пар, например для установки указателей на память. Создадим и такой макрос
.MACRO ldiw
ldi @0L, LOW(@1)
ldi @0H, HIGH(@1)
.ENDMACRO
В этом макросе мы используем тот факт, что стандартное именование регистров и портов у производителя подразумевает постфикс L для младшей и постфикс H для старшей части двухбайтного значения. Если при именовании собственных переменных придерживаться этого правила, то макрос будет правильно работать в том числе и с ними. Прелесть макросов еще и в том, что они обеспечивают простую подстановку, поэтому и в случае, когда второй операнд число, и в случае, когда это название метки, макрос отработает правильно.
Макросы для реализации сложных команд.
Когда речь идет о более сложных операциях, макросы, как правило, не используют, предпочитая подпрограммы. Однако и в этих случаях макросы могут облегчить жизнь и сделать код более читабельным. На помощь нам и в этом случае приходит условная компиляция. Подход к программированию может выглядеть следующим образом:
Все наши подпрограммы мы размещаем в отдельном файле, который назовем, например, Library.inc. Каждая подпрограмма в этом файле будет иметь следующий вид
_sub0:
.IFDEF __sub0
; ----- код нашей подпрограммы -----
ret
.ENDIF
В данном случае наличие определения __sub0 означает, что подпрограмма должна быть включена в результирующий код. В противном случае она игнорируется.
Далее в отдельном файле Macro.inc определяем макросы вида
.MACRO SUB0
.IFNDEF __sub0
.DEF __sub0
.ENDIF
; --- здесь мы располагаем команды инициализации регистров перед вызовом подпрограммы
call _sub0
.ENDMACRO
При использовании данного макроса мы проверяем наличие определения __sub0 и, в случае его отсутствия, выполняем определение. В результате, использование макроса разблокирует включение кода подпрограммы в выходной файл. В случае использования подпрограмм в макросах код основной программы приобретет следующий вид
.INCLUDE "Macro.inc”
;---- код основной программы ----
.INCLUDE "Library.inc”
В качестве примера, приведем реализацию макроса для деления 8-и разрядных целых беззнаковых чисел. Сохраняем логику производителя и результат размещаем в AL (r0), а остаток от деления в AH (r1). Подпрограмма будет выглядеть следующим образом
_div8u:
.IFDEF __ div8u
;AH - остаток
;AL результат
;TempL - делимое
;TempH - делитель
;TempQL -счетчик цикла
clr AL;
clr AH;
ldi TempQL,9
d8u_1: rol TempL
dec TempQL
brne d8u_2
ret
d8u_2: rol A
sub AH, TempH
brcc d8u_3
add AH,TempH
clc
rjmp d8u_1
d8u_3: sec
rjmp d8u_1
.ENDIF
Макроопределение для использования этой подпрограммы будет следующим
.MACRO DIV8U
.IFNDEF __div8u
.DEF __div8u
.ENDIF
mov TempL, @0
mov TempH, @1
call _div8u
.ENDMACRO
При желании можно добавить и версию для работы с константой
.MACRO DIV8UI
.IFNDEF __div8u
.DEF __div8u
.ENDIF
mov TempL, @0
ldi TempH, @1
call _div8u
.ENDMACRO
В результате использование в тексте программы операции деления получается тривиальным
DIV8U r10, r11 ; r0 = r10/r11 r1 = r10 % r11
DIV8UI r10, 35 ; r0 = r10/35 r1 = r10 % 35
Используя условную компиляцию мы можем разместить в Library.inc все подпрограммы, которые могли бы нам пригодиться. При этом в выходном коде окажутся только те из них, которые хотя бы раз вызывались. Обратите внимание на позицию метки входа. Вывод метки за границы условия обусловлен особенностями компилятора. Если разместить метку в тело условного блока, то компилятор может выдать ошибку. Наличие в коде неиспользуемых меток не страшно, так как наличие любого количества меток никак не влияет на результат.
Макросы для работы с периферией
Одной из операций, где трудно обойтись без использования документации производителя, является инициализация периферийных устройств. Даже с использованием мнемонических обозначений регистров и разрядов из кода бывает трудно понять, в каком режиме настроено то или иное устройство, тем более, что иногда режим настраивается комбинацией значений бит разных регистров. Посмотрим как можно использовать макросы на примере USART.
Начнем с макроса инициализации асинхронного режима.
.MACRO USART_INIT ; speed, bytes, parity, stop-bits
.IF CLK8 == 0
.SET DIVIDER = FOSC/16/@0-1
.ELSE
.SET DIVIDER = FOSC/128/@0-1
.ENDIF
; Set baud rate to UBRR0
outi UBRR0H, HIGH(DIVIDER)
outi UBRR0L, LOW(DIVIDER)
; Enable receiver and transmitter
.SET UCSR0B_ = (1<
Использование макроса нам позволило заменить инициализацию регистров настройки USART непонятными без чтения документации значениями на строчку, с который способен справится даже тот, кто впервые столкнулся с этим контроллером. В этом макросе так же наконец стало понятно для чего мы определяли константы частоты и делителя. Ну и следует отметить, что несмотря на внушительный код самого макроса, результирующий будет иметь тот же вид, как если бы мы писали инициализацию обычным образом.
Чтобы закончить с USART приведем еще несколько небольших макросов
.MACRO USART_SEND_ASYNC
outi UDR0, @0
.ENDMACRO
Здесь всего одна строчка, но использование этого макроса позволит лучше видеть, где в программе идет вывод данных в USART. Если мы предполагаем работу в синхронном режиме без использования прерываний, то вместо USART_SEND_ASYNC лучше использовать макрос, приведенный ниже
.MACRO USART_SEND
USART_Transmit:
xin TempL, UCSR0A
sbrs TempL, UDRE0
rjmp USART_Transmit
outi UDR0, @0
.ENDMACRO
В данном случае мы включаем проверку занятости порта и выводим данные только тогда, когда порт свободен. Очевидно, что данный подход к работе с периферийными устройствами будет работать для любых устройств, а не только для USART.
Сравнение программ без и с использованием макросов.
Посмотрим на небольшой пример и сравним код, написанный без использования макросов с кодом, где они используются. Для примера возьмем программу выводящую классический «Hello world!» в терминал через аппаратный UART.
RESET: ldi r16, high(RAMEND)
out SPH,r16
ldi r16, low(RAMEND)
out SPL,r16
USART_Init:
out UBRR0H, r17
out UBRR0L, r16
ldi r16, (1<
А вот как выглядит та же программа, но написанная с использованием макросов
.INCLUDE "macro.inc”
.EQU FOSC = 16000000
.EQU CLK8 = 0
RESET: ldiw SP, RAMEND;
USART_INIT 19200, 8, "N", 1
ldiw Z, STR<<1
LOOP: lpm TempL, Z+
test TempL
breq END
USART_SEND TempL
rjmp LOOP
END: rjmp END
STR: .DB "Hello world!”,0
В этом примере мы использовали описанные выше макросы, что позволило существенно упростить код программы и сделать его более понимаемым. Бинарный код в обоих программах при этом будет абсолютно идентичным.
Вывод
Использование макросов позволяет значительно сократить ассемблерный код программы, сделать его более понятным и читабельным. Условная компиляция позволяет создавать универсальные команды и библиотеки процедур без создания избыточного выходного кода. В качестве недостатка можно указать весьма скромный по меркам высокоуровневых языков набор допустимых операций и ограничения при объявлении данных «вперед». Это ограничение не позволяет, к примеру, написать средствами макросов полноценную универсальную команду для переходов jmp/rjmp и существенно раздувает код самого макроса при реализации сложной логики.