Совместное использование ассемблера и Си для AVR

AVR

При программировании вещей, критичных к быстродействию и размеру кода хорошо использовать ассемблер. При этом обычно не обязательно писать на нем весь код, достаточно реализовать наиболее “чувствительные” подпрограммы. Компилятор GCC и среда Atmel Studio позволяют использовать в проекте ассемблер и С одновременно. При этом возникает вопрос организации взаимодействия между подпрограммами на разных языках: вызова методов с передачей им параметров и доступа к переменным.

Ассемблерные файлы сохраняются с расширением .S. Один из файлов проекта должен содержать функцию main(), которая будет являться точкой входа. Обычно, она объявляется в c-файлах, но ничего не запрещает объявить main() в ассемблерном файле (используя директиву .global).

При совмещении языков Си и ассемблера в одном проекте могут возникнуть следующие вопросы:

  1. Как вызывать ассемблерные прдпрограммы из Си-кода?
  2. Как вызывать Си-подпрограммы из ассемблерного кода?
  3. Как передавать аргументы функция в ассемблерном коде?
  4. Как передавать аргументы функция в Си-коде?
  5. Как обеспечить доступ к глобальным переменным из Си и ассемблера?

Видимость функций

Чтобы Си-функции были видны из ассемблерного кода нужно использовать директиву .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 (регистровая пара Z) могут свободно использоваться в ассемблерном коде. Компилятор Си не беспокоится об их сохранении. Поэтому, если ассемблерный код использует эти регистры и вызывает Си-функции, то он должен сам заботится о сохранении этих регистров перед вызовом и восстановлении после вызова.

Регистры r2-r17, а также r28-r29 (регистровая пара Y) не изменяются в Си-функциях. Если ассемблерная функция, вызываемая из Си-кода использует регистры из этой группы, то она сама должна заботится о сохранении и восстановлении их содержимого. Регистровая пара Y используется для сохранения начального значения указателя стека SP (см. далее).

Регистр r1 всегда содержит ноль. Если ассемблерный метод модифицирует этот регистр, то он должен всегда занулять его перед возвращением в Си-код (например, командой clr r1). Во встроенном ассемблере вы можете использовать __zero_reg__ для этого регистра.

Регистр r0 используется компилятором в качестве временного хранилища. Во встроенном ассемблере GCC вы можете использовать алиас __tmp_reg__ для доступа к нему. Ассемблерный код может не заботится о сохранении этого регистра (если только это не код обработчика прерываний!).

Аналогично временному регистру r0 компилятор может использовать и бит T статусного регистра SREG. Заботится о сохранении этого флага в "обычном" ассемблерном коде так же не надо.

Передача параметров

Если число параметров у метода фиксировано, то для передачи аргументов используются регистры 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, в зависимости от его длины следующим образом:

  • 8-битные - в r24
  • 16-битные - в r25:r24
  • 32-битные - в r25:r24:r23:r22
  • 64-битные - в r25:r24:r23:r22:r21:r20:r19:r18

Структура генерируемого AVRGCC файла прошивки

Создаваемый компилятором файл прошивки имеет следующую структуру:

  • таблица векторов прерываний
  • блок константных данных
  • процедура __ctors_end
  • процедура __do_clear_bss
  • вектор прерывания __bad_interrupt
  • код пользовательских подпрограмм
  • код библиотечных подпрограмм

Первым блоком следуте таблица векторов прерываний. Если в коде определены какие-то векторы, то тут будут короткие переходы на них. Для остальных же векторов в таблице будут переходы к подпрограмме __bad_interrupt, представляющую собой команду jmp 0.

После таблицы прерываний следует блок константных данных, представляющий собой строки и массивы, размещенными в области кода (PRG).

Далее следует процедура __ctors_end:


__ctors_end:
    clr	  r1
    out	  SREG, r1
    ldi	  r28, 0x5F
    ldi	  r29, 0x04
    out	  SPH, r29
    out	  SPL, r28

Тут мы видим зануление регистра r1 и SREG и инициализацию 16-битного указателя фрейма, которым служит регистровая пара Y (r29:r28). Указатель фрейма используется для получения местоположения стека.

Блок __do_clear_bss инициализирует секцию статических данных bss (block starting symbol) и очищает память

Среди библиотечных функций есть функции сохранения пролога и загрузки эпилога. Как было сказано выше, Си-функции не меняют регистры r2-r17, r28-r29. При этом, если функции недостаточно остальных регистров для размещения своих данных (локальных переменных), то GCC использует подпрограммы __prologue_saves__ и __epilogue_restores__.

Первая сохраняет нужные регистры в стеке и настраивает указатель стека SP и указатель фрейма Y следующим образом:

Y = SP - X
SP = Y

После чего продолжает выполнение функции переходя к адресу, переданному в Z.

Вторая, наоборот, восстанавливает сохраненные регистры и значения SP и Y к исходным:

SP = Y + ZL
Y += X

__prologue_saves__:
  push  r2
  push  r3
  push  r4
  push  r5
  push  r6
  push  r7
  push  r8
  push  r9
  push  r10
  push  r11
  push  r12
  push  r13
  push  r14
  push  r15
  push  r16
  push  r17
  push  r28
  push  r29
  in    r28, SPH
  in    r29, SPL
  sub   r28, r26
  sbc   r29, r27
  in    r0, SREG
  cli
  out   SPH, r29
  out   SREG, r0
  out   SPL, r28
  ijmp  

__epilogue_restores__:
  ldd	 r2, Y+18
  ldd	 r3, Y+17
  ldd	 r4, Y+16
  ldd	 r5, Y+15
  ldd	 r6, Y+14
  ldd	 r7, Y+13
  ldd	 r8, Y+12
  ldd	 r9, Y+11
  ldd	 r10, Y+10
  ldd	 r11, Y+9
  ldd	 r12, Y+8
  ldd	 r13, Y+7
  ldd	 r14, Y+6
  ldd	 r15, Y+5
  ldd	 r16, Y+4
  ldd	 r17, Y+3
  ldd	 r26, Y+2
  ldd	 r27, Y+1
  add	 r28, r30
  adc	 r29, r1
  in   r0, SREG
  cli
  out  SPH, r29
  out  SREG, r0
  out  SPL, r28
  movw r28, r26
  ret

Стек растет сверху вниз (меньшие адреса находятся внизу). После пролога функции, указатель фрейма будет указывать на один байт ниже фрейма стека, т.е. Y+1 будет указывать на низ стека фрейма. Как показано в таблице:

Аргументы функции (могут отсутствовать) верх стека
Адрес возврата из функции (2 или 3 байта)
Сохраненные регистры (могут отсутствовать)
Слоты стека (могут отсутствовать) низ стека, Y+1 указывает вниз этого блока
Рейтинг: 
0
Голосов еще нет