Arduino. Ассемблер AVR8
Иногда бывает так, что только ассемблер способен помочь создать эффективный код для скетча Arduino. И тогда на помощь приходит встроенный (inline) ассемблер AVR-GCC.
Arduino. Ассемблер AVR8
Пожалуй, простейший пример -- мы хотим, чтобы микроконтроллер подождал четыре такта в промежутке между изменением состояния вывода. С помощью встроенных функций delay() или delayMicroseconds() мы этого сделать не можем, для этого нам требуется использовать команду ассемблера NOP:
PORTD |= _BV(1);
__asm__ __volatile__(
"nop\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t"
);
PORTD &= ~_BV(1);
или, что несколько читабельнее:
PORTD |= _BV(1);
__asm__ __volatile__("nop");
__asm__ __volatile__("nop");
__asm__ __volatile__("nop");
__asm__ __volatile__("nop");
PORTD &= ~_BV(1);
Оформив таким образом все команды ассемблера AVR8 как набор inline-функций и макросов, я получил в результате заголовочный файл с говорящим названием avrasm.h (скачать). Файл этот следует поместить в каталог Libraries/avrasm программного обеспечения Arduino. С его использованием вышеприведённый код будет выглядеть следующим образом:
#include <avrasm.h>
PORTD |= _BV(1);
__NOP;
__NOP;
__NOP;
__NOP;
PORTD &= ~_BV(1);
Надо заметить, что в самом по себе программировании Arduino на ассемблере нет ничего сложного, но есть нюансы, которые неизбежно приходится учитывать. Например, результат команды умножения MUL помещается в пару регистров R0:R1, которая компилятором языка программирования C используется следующим образом: регистр R0 используется как временный (temporary) регистр, а регистр R1 как нулевой (zero) регистр (теоретически его значение всегда должно быть равно нулю).
Поэтому на ассемблере удобнее всего писать совсем небольшие программы. Например, когда я делал 8-битный синтезатор, изучая взаимодействие нескольких микроконтроллеров, то один из микроконтроллеров у меня выполнял функцию UART-приёмника MIDI с буфером FIFO. Соответственно лучше всего и быстрее всего он работал с микропрограммой, целиком написанной на ассемблере (на C работал значительно медленнее, и даже иногда подвисал).
С другой стороны программы на ассемблере намного сложнее отлаживать, поскольку код программы имеет свойство довольно быстро разрастаться, и найти в программе ошибку типа опечатки - задача крайне утомительная и трудоёмкая.
Регистры
Микропроцессор AVR8 является RISC-процессором, то есть процессором с уменьшенным набором команд (в сравнении с CISC-процессорами), но зато эти команды выполняются за 1-2 машинных такта, плюс он имеет 32 8-битных регистра, мнемонически обозначаемых R0..R31.
Регистры R0..R15 являются младшими регистрами (не все команды принимают их в качестве операндов), регистры R16..R31 являются старшими регистрами. Говоря проще, если требуется 8-битная переменная, то под неё можно отвести один из младших регистров, но если требуется 16-битная регистровая пара, то под переменную следует отвести один из старших регистров.
Регистровые пары R26:R27, R28:R29 и R30:R31 также дополнительно обозначаются мнемонически как X, Y и Z. Ими можно пользоваться для адресации данных при операциях ввода-вывода (чтения-записи оперативной памяти ОЗУ).
Кроме регистров общего назначения, есть несколько специальных регистров:
__SREG__ -- регистр состояния (status register), адрес 0x3F (0x5F)
__SP_H__ -- старший байт указателя вершины стека, адрес 0x3E (0x5E)
__SP_L__ -- младший байт указателя вершины стека, адрес 0x3D (0x5D)
__tmp_reg__ -- регистр R0, временный регистр
__zero_reg__ -- регистр R1, нулевой регистр
Использование регистров компилятором avr-gcc:
r0 -- временный регистр, используемый кодом, генерируемым компилятором. Если Ваш ассемблерный код использует регистр r0 и вызывает функцию C, то Вам следует сохранить и восстановить значение этого регистра.
r1 -- нуль-регистр, подразумевается что его всегда равно нулю. Ассемблерный код, который использует регистр r1, должен обнулить регистр r1 перед возвратом в C-код или при вызове функций C.
r2..r17, r28, r29 -- сохраняемые регистры. Если Ваш ассемблерный код использует эти регистры, то их значение следует сохранить и затем восстановить. При этом, если вызывается функция C, то её код также сохранит и восстановит эти регистры.
r18..r27, r30, r31 -- несохраняемые регистры. Их можно свободно использовать, не сохраняя и не восстанавливая их значения. Однако если из ассемблерного кода вызывается функция C, то их значения следует сохранить, поскольку в коде функции они так же могут использоваться без сохранения и восстановления.
Переменные C и вызов функций ассемблера и C
Чтобы переменная или функция, объявленная в коде C, была доступна в ассемблерном коде, её следует объявить как внешнюю переменную:
.extern my_C_variable
.extern my_C_function
И наоборот, чтобы ассемблерная была доступна из C-кода, её следует в ассемблерном коде объявить как глобальную:
.global my_asm_function
а в коде C -- как внешнюю:
extern unsigned char my_asm_function(unsigned char, unsigned int);
Для передачи фиксированного списка аргументов функции используются регистровые пары с r24..r25 по r8..r7, аргументы перечисляются слева направо. Таким образом аргументы типа char (uint8_t) фактически передаются как short (uint16_t).
При передаче аргументов переменного списка, аргументы помещаются на стек в порядке справа налево. Для типа char также резервируются два байта.
Возвращаемые значения передаются передаются в регистровых парах r25..r18, в зависимости от размера типа возвращаемого значения. Байтовый порядок таков:
Регистры ввода-вывода (GPIO)
Нижнюю часть адресов адресного пространства ОЗУ занимают регистры ввода-вывода (GPIO - General Purpose Input/Output). Первые 64 регистра ввода-вывода с адресами 0..0x3F доступны для чтения-записи с помощью команд IN и OUT, для доступа к остальным используются команды чтения-записи данных в ОЗУ.
Младшие 32 адреса ОЗУ занимают регистры R0..R31 и соответственно при использовании команд IN и OUT адрес например регистра SREG будет 0x3F, а при использовании команд LD и ST адрес регистра SREG будет 0x5F (0x3F+0x20).
В микроконтроллерах с объёмом Flash-памяти или памяти ОЗУ, большими 64 KБайт, используются регистры конкатенации:
RAMPX, RAMPY, RAMPZ -- позволяют в конкатенации с регистрами X, Y и Z неявно адресовать больше 64 КБайт адресного пространства данных и программ
RAMPD -- позволяет в конкатенации с регистром Z неявно адресовать больше 64 КБайт адресного пространства данных
EIND -- позволяет в конкатенации с регистром Z производить неявный переход либо вызов функций в адресном пространстве программ больше 64 КБайт
Регистр состояния (SREG)
Биты регистра состояния имеют специальное значение, показывающее результат выполнения предыдущей команды, разрешены ли прерывания, а также пользовательский бит общего назначения T, чтение и запись которого можно производить командами BLD и BST. Биты регистра состояния устанавливаются и сбрасываются командами SE* и CL*.
С - флаг переноса
Z - флаг нуля
N - флаг отрицательного результата
V - флаг переполнения в дополнении до двойки
S - N xor V, флаг знака
H - флаг полу-переноса
T - бит общего назначения
I - флаг глобального разрешения прерываний
Как посмотреть листинг скомпилированного кода
Возникает резонный вопрос, как увидеть листинг скомпилированного машинного кода. В Linux для этого нужно скомпилировать скетч (например Blink), затем в каталоге /tmp найти каталог /tmp/buildXXXXXXXXXXXXXXXXXXX.tmp, где XXXXXXXXXXXXXXXXXXX -- 19-значное число, и в этом каталоге найти файл Blink.cpp.elf:
find /tmp | grep Blink.cpp.elf
Это скомпилированный исполняемый файл формата ELF, в котором раздел .text содержит машинный код скетча, дизассемблировать который можно командой:
avr-objdump -d Blink.cpp.elf
Чтобы код удобнее было просматривать, можно воспользоваться командой less:
avr-objdump -d Blink.cpp.elf | less
либо вывести листинг в файл:
avr-objdump -d Blink.cpp.elf > Blink.cpp.lss
Пример использования AVRASM.H
Приведу пример использования файла avrasm.h.
#include <avrasm.h>
char var1; // Переменная в ОЗУ
void setup()
{
register uint8_t rd, rdx, rr; // 8-битные переменные
register uint16_t rdl asm("Z"), rdm; // 16-битные переменные
rdl = 0;
rdm = 0xFFFF;
rd = 0;
rdx = 0;
rr = 1;
// Метка в коде C (в ассемблерном коде не доступна)
L0:
__ADC(rd, rr);
__ADIW(rdl, rr);
__INC(rd);
__COM(rd);
__DEC(rd);
__TST(rd);
__CLR(rd);
__SER(rd);
__MUL(rd, rd);
__FMUL(rdx, rdx);
// Ассемблерная метка в коде C
__LABEL(L1);
__RJMP(L1);
__CPSE(rd, rdx);
__MOVW(rdl, rdm);
__LDD(rd, rdl, 0x3F);
__STM(rdl, rd);
__STS(var1, rd);
__LDP(rdx, rdl);
__STP(rdl, rd);
__STD(rdl, 0x3F, rdx);
goto L0;
}
Приведённый пример у меня компилируется в следующий код:
00000090 <setup>:
90: cf 93 push r28
92: df 93 push r29
94: 31 e0 ldi r19, 0x01 ; 1
96: 20 e0 ldi r18, 0x00 ; 0
98: 8f ef ldi r24, 0xFF ; 255
9a: 9f ef ldi r25, 0xFF ; 255
9c: e0 e0 ldi r30, 0x00 ; 0
9e: f0 e0 ldi r31, 0x00 ; 0
a0: 43 1f adc r20, r19
a2: 11 96 adiw r26, 0x01 ; 1
a4: 43 95 inc r20
a6: 40 95 com r20
a8: 4a 95 dec r20
aa: 44 23 and r20, r20
ac: 44 27 eor r20, r20
ae: 4f ef ldi r20, 0xFF ; 255
b0: 42 9f mul r20, r18
b2: 4a 03 fmul r20, r18
000000b4 <L1>:
b4: ff cf rjmp .-2 ; 0xb4 <L1>
b6: 22 13 cpse r18, r18
b8: dc 01 movw r26, r24
ba: 47 ad ldd r20, Z+63 ; 0x3f
bc: 2e 93 st -X, r18
be: 20 93 00 01 sts 0x0100, r18
c2: 41 91 ld r20, Z+
c4: 2d 93 st X+, r18
c6: 2f af std Y+63, r18 ; 0x3f
c8: eb cf rjmp .-42 ; 0xa0 <setup+0x10>
Автор: Андрей Шаройко <vanyamboe@gmail.com>