Волшебная сила макросов, или как облегчить жизнь ассемблерного программиста AVR

?v=1

Про макросы в ассемблере написано много. И в документации, и в различных статьях. Но в большинстве случаев все сводится либо к простому перечислению директив с кратким описанием их функций, либо к набору разрозненных примеров готовых макросов.
Цель этой статьи — описать определенный подход к программированию на ассемблере для формирования максимально простого и читабельного кода с использованием макросов. В статье не будет описания синтаксиса отдельных команд и директив. Подробное описание уже дано производителем. Мы же сосредоточимся на том, как можно использовать эти возможности для решения конкретных задач.

Компания 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 и существенно раздувает код самого макроса при реализации сложной логики.

© Habrahabr.ru