Исполнение ассемблерного кода
Иногда в программах, разрабатываемых на языке C, требуется напрямую использовать команды ассемблера
с целью оптимизации кода или просто из-за невозможности выполнить те или иные
операции средствами C.
Компилятор WinAVR
поддерживает
вставку в исходный текст программы фрагментов на ассемблере.
Для выполнения ассемблерного кода доступен специальный оператор
asm. Его синтаксис:
asm(код: список_выходных_операндов, список_входных_операндов)
Поясним использование оператора asm на простом примере чтения значения из порта D:
asm("in
%0, %1" :
"=r" (value) :
"I" (_SFR_IO_ADDR(PORTD)) );
Смысл каждой части оператора asm, отделенной с помощью двоеточия:
1. Ассемблерная
команда, определенная как строковая константа: "in %0,
%1".
2. Список выходных
операндов, разделенных запятыми. В нашем примере есть только один операнд: "=r" (value).
3. Список входных
операндов, разделенных запятыми. В нашем примере есть только один операнд:
"I" (_sfr_io_addr (PORTD)).
Вторая и третья части оператора asm предназначены для определения связи между
регистрами микроконтроллера и операндами C. В самих ассемблерных инструкциях
ссылки на операнды создаются с помощью символа "%" и порядкового номера операнда (начиная с нуля). Так,
в рассмотренном выше примере ссылке %0
соответствует входной операнд "=r" (value), а ссылке
%1 — выходной операнд
"I" (_SFR_IO_ADDR(PORTD)).
Синтаксис операндов мы рассмотрим чуть позже, а пока исследуем
часть ассемблерного листинга, который мог быть получен в результате компиляции
представленного выше оператора asm:
in r24, 12
В данном случае для хранения значения, считанного из порта D
компилятор выбрал регистр г24, хотя
это мог бы быть и любой другой регистр. Компилятор мог бы даже выполнить
неявную загрузку или сохранение значения или решить вообще не включать
пользовательский ассемблерный код.
Все эти решения — часть оптимизационной стратегии компилятора.
Например, если бы значение переменной ни разу не использовалось в оставшейся
части программы, то представленный выше код, скорее всего, был бы исключен. Во
избежание подобного, к оператору asm следует
добавить атрибут volatile:
asm volatile("in %0,
%1" : "=r" (value) : "I" (_SFR_IO_ADDR(PORTD))) ;
Если ассемблерные инструкции не используют операндов, то
соответствующие части оператора asm могут быть опущены. Например, в
случае общего запрета прерываний это будет выглядеть следующим образом:
asm
volatile("cli"::);
Ассемблерный код.
В первой части оператора asm можно использовать любые
команды AVR-ассемблера. При этом
для улучшения удобочитаемости каждую инструкцию можно помещать в отдельную
строку с помощью символьных литералов перевода строки:
asm
volatile("nop\n"
"nop\n"
"nop\n"
"nop\n"
: :);
Кроме того, можно использовать некоторые специальные символы,
соответствующие тем или иным регистрам микроконтроллера:
_SREG__—
регистр состояния;
__SP_H__— старший байт указателя стека;
__SP_L__— младший байт указателя стека;
__tmp_reg__— регистр r0, используемый для промежуточного хранения;
__zero_reg__— регистр rl, всегда нулевой.
Входные и выходные операнды.
Каждый входной и выходной операнд описывается строкой уточнений,
после которой следует выражение языка С в круглых скобках. Компилятор WinAVR поддерживает уточнения, перечисленные в табл.
3.4.
таблица 3.4. Уточнения в определении входных
операндов оператора asm
Уточнение |
|
Использование |
Диапазон значений |
a |
|
Обычные старшие регистры |
r16..r23 |
b |
|
Регистры двойной длины для указателя базы |
y. z |
d |
|
Старшие регистры |
r16..r31 |
е |
|
Регистры двойной длины — указатели |
х, у, z |
G |
|
Вещественная константа |
0,0 |
I |
|
Шестиразрядная положительная целая константа |
0..63 |
J |
|
Шестиразрядная отрицательная целая константа |
-63..0 |
К |
|
Целая константа |
2 |
L |
|
Целая константа |
0 |
1 |
|
Младшие регистры |
r0...r15 |
М |
|
Восьмиразрядная целая константа |
0..255 |
N |
|
Целая константа |
-1 |
0 |
|
Целая константа |
8, 16,24 |
Р |
|
Целая константа |
1 |
q |
|
Указатель стека |
SPH:SPL |
r |
|
Любой регистр |
r0..r31 |
t |
|
Временный регистр |
r0 |
w |
|
Специальные старшие регистровые пары |
r24, r26,
r28, r30 |
x |
|
Регистр-указатель двойной длины X |
х (r27:r26) |
y |
|
Регистр-указатель двойной длины Y |
у (r29:r28) |
z |
|
Регистр-указатель двойной длины Z |
z (r31
:r30) |
Символам уточнений могут предшествовать модификаторы (если модифика ftp не
указан, операнд считается "только для чтения"):
= — операнд
"только для записи" (для всех выходных операндов);
& — регистр
должен использоваться только для вывода.
Входные операнды — только для чтения. Но что делать, если
необходимо чтобы один и тот же операнд был и входным, и выходным одновременно?
Для этого во входном операнде можно использовать в качестве уточнения цифру,
соответствующую порядковому номеру выходного операнда. Например:
asm volatile("swap
%0" : "=r" (value) :
"0" (value));
Этот оператор поменяет местами полубайты восьмиразрядной
переменной value. Ограничитель "0"
указывает компилятору использовать тот же входной регистр, что и первый
операнд.
В тех случаях, когда код реализует различные регистры,
используемые для входных и выходных операндов, к выходному операнду следует добавить
моди фикатор &.
Например:
asm volatile("in
%0,%l" "\n\t" "out
%1, %2" "\n\t" :
"=&r" (input) : "I" (_SFR_IO_ADDR(port)), ) ;
В этом примере входное значение считывается из порта, а затем выходное
в тот же самый порт записывается значение. Если бы компилятор выбрал для ввода
и вывода один и тот же регистр, то после выполнения первой ассемблерной команды
выходное значение было бы потеряно. Благодаря использованию модификатора
&, компилятор распознал, что для выходного значения следует использовать
любой регистр, не занятый под входные операнды. Возвращаясь к примеру
перестановки, код перестановки старшего и младшего байта некоторого
16-тираз-рядного значения будет выглядеть следующим образом:
asm
volatile("mov __tmp_reg__, %A0" "\n\t"
"mov
%A0, %B0" "\n\t"
"mov
%B0, __tmp_reg__" "\n\t"
: "=r" (value)
: "0" (value) ) ;
Пример
перестановки байтов 32-хбитного значения:
asm
volatile("mov __tmp_reg__, %A0"
"\n\t"
"mov
%A0, %D0" "\n\t"
"mov
%D0, __tmp_reg__" "\n\t"
"mov
__tmp_reg__, %B0" "\n\t"
"mov
%B0, %C0" "\n\t"
"mov
%C0, __tmp_reg__" "\n\t"
: "=r" (value)
: "0" (value) );
Если операнды не помещаются в один регистр, компилятор
автоматически назначит дополнительные регистры, общий размер которых будет
достаточным для хранения всего операнда. В рассмотренных примерах спецификации
%А0 соответствует младший байт первого операнда, а спецификации %А1 — младший
байт второго операнда. Следующему байту первого операнда соответствует %В0, сле-.дующему — %со и т.д.
Резервирование регистров.
В том случае, если в операторе asm должны быть указаны регистры, которые не передаются в качестве операндов,
об этом следует каким-то образом уведомить компилятор. Для этой цели служит еще
одна, четвертая часть оператора asm, которая
в общем случае является необязательной, — часть резервированных регистров.
Рассмотрим пример реализации автоинкремента
восьмиразрядного значения, на которое указывает переменная-указатель, без
прерывания какой-либо подпрограммой обслуживания прерывания или параллельным
процессом (мы должны использовать указатель, поскольку инкрементированное
значение должно быть сохранено до разрешения прерываний).
asm
volatile( )
"cli" "\n\t"
"id
r24, %a0" "\n\t"
"inc
r24" "\n\t"
"st
%a0, r24" "\n\t"
"sei" "\n\t"
:
"e" (ptr) : "r24"
В результате компилятор сгенерирует следующий ассемблерный код: '
cli
Id
r24, Z inc r24 st Z, r24
sei
Для того чтобы избежать резервирования регистра г24, можно
воспользоваться специальным буферным регистром__tmp_reg__:
asm
volatile(
"cli"
"\n\t"
"Id
__tmp_reg__, %aO" "\n\t"
"inc
__tmp_reg__" "\n\t"
"st
%a0, __tmp_reg__" "\n\t"
"sei" "\n\t"
: "e"
(ptr) );
Еще одна проблема заключается в том, что рассматриваемый код не
может быть использован в тех программных секциях, где прерывания запрещены и не
должны активизироваться, поскольку в конце используется команда общего разрешения
прерываний sei. Конечно, можно сохранять текущее
состояние микроконтроллера, однако в таком случае потребуется еще один
регистр. Опять таки, это можно реализовать без резервирования фиксированного
регистра, а с помощью локальной переменной языка С:
{
unsigned
char s;
asm
volatile(
"in
%0, __SREG__" "\n\t"
"cli"
"\n\t"
"ld
__tmp_reg__, %al"
"\n\t"
"inc
__tmp_reg__" "\n\t"
"st
%al, __tmp_reg__" "\n\t"
"out
__SREG__, %0" "\n\t"
:
"=&r" (s) : "e" (ptr)
);
}
Теперь ассемблерный код модифицирует переменную, на которую
указывает ptr, однако компилятор может этого не
распознать и сохранить значение в каком-либо другом регистре. Кроме того,
значение переменной может модифицировать сама программа на С, а компилятор не
обновит ячейку памяти по причинам оптимизации. Во избежание подобных проблем
можно воспользоваться специальным резервирующим определением "memory":
unsigned
char s; asm volatile(
"in
%0, _SREG__" "\n\t"
"cli"
"\n\t"
"ld
__tmp_reg__, %al"
"\n\t"
"inc
__tmp_reg__" "\n\t"
"st
%al, __tmp_reg__"
"\n\t"
"out
__SREG__, %0" "\n\t"
:
"=&r" (s)
: "e" (ptr)
: "memory"
Определение "memory"
сообщает компилятору о том, что ассемблерный код может модифицировать любую
ячейку памяти. В результате компилятор перед выполнением ассемблерного кода
будет обновлять все переменные, содержимое которых уже содержится в регистрах.
И, конечно же, после выполнения этого кода все регистры будут восстановлены в
исходное состояние.