- PVSM.RU - https://www.pvsm.ru -
Про макросы в ассемблере написано много. И в документации, и в различных статьях. Но в большинстве случаев все сводится либо к простому перечислению директив с кратким описанием их функций, либо к набору разрозненных примеров готовых макросов.
Цель этой статьи — описать определенный подход к программированию на ассемблере для формирования максимально простого и читабельного кода с использованием макросов. В статье не будет описания синтаксиса отдельных команд и директив. Подробное описание уже дано производителем [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<<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 и существенно раздувает код самого макроса при реализации сложной логики.
Автор: idv2013
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/assembler/328417
Ссылки в тексте:
[1] производителем: https://www.microchip.com/webdoc/avrassembler/index.html
[2] Источник: https://habr.com/ru/post/465261/?utm_campaign=465261&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.