Волшебная сила макросов, или как облегчить жизнь ассемблерного программиста 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<<RXEN0)|(1<<TXEN0)    outi    UCSR0B, UCSR0B_    .SET UCSR0C_  = 0   .IF @2 == 'E'   .SET UCSR0C_ |= (1<<UPM01)   .ENDIF  .IF @2 == 'O'   .SET UCSR0C_ |= (1<<UPM00)   .ENDIF   .IF @3== 2   .SET UCSR0C_ |= (1<<USBS0)   .ENDIF   .IF @1== 6   .SET UCSR0C_ |= (1<<UCSZ00)   .ENDIF   .IF @1== 7   .SET UCSR0C_ |= (1<<UCSZ01)   .ENDIF   .IF @1== 8   .SET UCSR0C_ = UCSR0C_ |(1<<UCSZ01)|(1<<UCSZ00)   .ENDIF   .IF @1== 9   .SET UCSR0C_ |= (1<<UCSZ02)|(1<<UCSZ01)|(1<<UCSZ00)   .ENDIF   ; Set frame format   outi    UCSR0C,UCSR0C_  .ENDMACRO

Использование макроса нам позволило заменить инициализацию регистров настройки 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<<RXEN0)|(1<<TXEN0)  out UCSRnB,r16 ldi r16, (1<<USBS0)|(3<<UCSZ00)  out UCSR0C,r16  ldi ZL, LOW(STR<<1) ldi ZH, HIGH(STR<<1) LOOP: lpm   r16, Z+ or  r16,r16 breq    END USART_Transmit:  in r17, UCSR0A  sbrs r17, UDRE0  rjmp USART_Transmit  out UDR0,r16  rjmp    LOOP END: rjmp END STR: .DB  “Hello world!”,0

А вот как выглядит та же программа, но написанная с использованием макросов

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


ссылка на оригинал статьи https://habr.com/ru/post/465261/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *