
При программировании вещей, критичных к быстродействию и размеру кода хорошо использовать ассемблер. При этом обычно не обязательно писать на нем весь код, достаточно реализовать наиболее “чувствительные” подпрограммы. Компилятор GCC и среда Atmel Studio позволяют использовать в проекте ассемблер и С одновременно. При этом возникает вопрос организации взаимодействия между подпрограммами на разных языках: вызова методов с передачей им параметров и доступа к переменным.
Ассемблерные файлы сохраняются с расширением .S. Один из файлов проекта должен содержать функцию main(), которая будет являться точкой входа. Обычно, она объявляется в c-файлах, но ничего не запрещает объявить main() в ассемблерном файле (используя директиву .global).
При совмещении языков Си и ассемблера в одном проекте могут возникнуть следующие вопросы:
Чтобы Си-функции были видны из ассемблерного кода нужно использовать директиву .extern
.extern my_C_function
Для того, чтобы видеть ассемблерные функции из Си, служит директива .global:
.global my_assembly_function
При этом, в Си-коде надо объявить эту ассемблерную функцию с ключевым словом extern:
extern uint8_t my_assembly_function (uint8_t, unsigned int);
Для того, чтобы использовать общие глобальные переменные коде, их следует объявить в Си коде:
unsigned char my_value;
В ассемблерном коде доступ к переменным организуется аналогично доступу к функциям, директивой .extern:
.extern my_value
Написание ассемблерного кода для совместного использования с Си-кодом требует знаний о том, каким образом компилятор Си использует регистры.
Регистры r18-r27, r30, r31 могут свободно использоваться в коде. Компилятор Си не беспокоится о их сохранении. Поэтому, если ассемблерный код использует эти регистры и вызывает Си-функции, то он должен сам заботится о сохранении этих регистров перед вызовом и восстановлении после вызова.
Регистры r2-r17, r28, r29 не изменяются из Си-функций. Если ассемблерная функция, вызываемая из Си-кода использует регистры из этой группы, то она сама должна заботится о сохранении и восстановлении их содержимого. Регистры r29:r28 образуют регистровую пару Y, которая часто может не использоваться в Си-коде (т.е., если не используется C++). Точнее, они используются в процедуре инициализации __ctors_end для настройки указателя стека SP.
Регистр r1 всегда содержит ноль. Если ассемблерный метод модифицирует этот регистр, то он должен всегда занулять его перед возвращением в Си-код (например, командой ”clr r1”).
Регистр r0 используется компилятором в качестве временного хранилища. Если ассемблерный код использует этот регистр, и вызывает Си-методы, то он обязан сохранить и восстановить этот регистр, посколько тот может быть использован компилятором
Если число параметров у метода фиксировано, то для передачи аргументов используются регистры r25-r8. Аргументы передаются слева-направо, каждый использует четное количество регистров, т.е., для передачи аргумента типа char/uint8_t будет зарезервировано два регистра (это сделано для того, чтобы компилятор мог использовать команду movw). Если регистров не хватит, часть аргументов будет переданы через стек. Последнее не лучшим образом сказывается на производительности, поэтому передачи больших объемов данных в функцию следует избегать.
Если параметр имеет переменное число аргументов, то они передаются через стек, справа-налево. Однобайтовые аргументы так же передаются как двубайтовые (при этом старший байт для них зануляется командой eor r25, r25).
Лучше всего понять, как передаются параметры и результат для функций можно рассмотрев примеры генерируемого компилятором кода. Например, для функции
uint8_t max_8(uint8_t a1, uint8_t a2, uint8_t a3, uint8_t a4)
Параметры a1, a2, a3, a4 будет передаваться в регистрах r24, r22, r20 и r18 соответственно.
Для функции
uint16_t max_16(uint16_t a1, uint16_t a2, uint16_t a3, uint16_t a4)
Параметры a1, a2, a3, a4 будет передаваться в регистрах r25:r24, r23:r22, r21:r20 и r19:r18 соответственно. Причем, r25, r23, r21, r19 - старшие байты, а r24, r22, r20, r18 - младшие.
Для функции
uint32_t max_32(uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4);
Параметры передадуться через регистры r25:r24:r23:r22, r21:r20:r19:r18, r17:r16:r15:r14, r13:r12:r11:r10 соответственно.
Значение, возвращаемое функцией передается через регистры r25 - r18, в зависимости от его длины следующим образом: