Иногда полезно некоторые куски кода писать на ассемблере, и при этом весь основной код должен быть написан на C. К примеру, некоторая часть программы очень критична ко времени выполнения, и нужно четко контролировать её выполнение. Компилятор C не может учесть все нюансы программы и архитектуры микроконтроллера, и не в состоянии сгенерировать максимально оптимальный код, поэтому приходится писать такой код вручную на ассемблере. Очень часто на ASM пишут обработчики прерывания для ускорения их выполнения. Другие же части программы, которые имеют сложный алгоритм, и скорость их выполнения некритична, рационально писать на C.
Общие причины, по которым приходится иногда писать части кода на ассемблере:
- Мало свободного места для переменных в RAM или для кода FLASH. - Приложения, очень критичные ко времени выполнения. - Специальное программирование, которое не может быть реализовано на C (что бывает очень редко).
Есть два метода встроить код ассемблера в программу на C. Один метод - прямое встраивание операторов ассемблера в код C. Метод называется in-line ASM, описание этого метода приведено в документации на используемый компилятор (для каждого компилятора это может осуществляться по-разному). Как использовать inline ассемблер для компилятора GCC - см. в статье [4]. Как использовать inline ассемблер для компилятора IAR - см. в статье [5]. Второй метод - написать код ассемблера в отдельном файле и скомпилировать его отдельно в объектный код, а затем присоединить (link) объектный код к основной программе на этапе линковки. Второй метод и будет рассматриваться в этой статье.
Хороший пример использования ассемблера в виде отдельного модуля - код библиотеки V-USB и примеры firmware USB-устройств на микроконтроллерах AVR, которые используют эту библиотеку [2]. Код ассемблера здесь сосредоточен в модуле usbdrv\usbdrvasm.asm -> usbdrvasm.S. Он необходим для очень быстрой обработки прерывания INT0, используемого для отслеживания событий перепада сигнала шины данных USB (обычно это сигнал D+). Удивительно, что микроконтроллер AVR успевает обработать перепады сигнала с частотой шины USB и декодировать их на физическом уровне - и все это благодаря кодированию на языке ассемблера!
Оба метода вставки кода ассемблера - in-line и в виде отдельного ASM модуля - требуют тщательного изучения документации на компилятор. Во-первых, нужно знать синтаксис in-line ассемблера, а во-вторых, нужно знать, какие регистры и каким образом использует компилятор C для временных переменных, для передачи переменных в функции и возврата значений из функций (см. далее раздел "Какие регистры использует компилятор AVR GCC?"). Это нужно для того, чтобы программа на ассемблере правильно восстанавливала значение используемых регистров, и могла корректно принимать и возвращать значения данных. Упростить написание модуля на ассемблере может написание болванки на C и компиляция её в ассемблер (см. раздел "Как скомпилировать модуль C в модуль на ASM?").
[Как скомпилировать модуль C в модуль на ASM?]
Хороший метод написания шаблона для модуля ASM - сделать модуль на C и скомпилировать его в код ассемблера. Все компиляторы умеют это делать, в том числе и компилятор AVR GCC. После того, как будет получена болванка модуля ASM, в ней уже будут корректно передаваться нужные параметры и возвращаться значения через нужные регистры. Нужно будет только вручную поправить код, добившись требуемой оптимизации.
Для компиляции в ассемблер используется опция -S (avr-gcc ... -S). За подробностями обратитесь к документации на компилятор avr-gcc [3]. Для вызова подсказки по командной строке выполните avr-gcc --help. Пример:
avr-gcc -S -mmcu=at90usb1287 -I. -Os -Wall -DF_CPU=16000000UL timerint.c -o timerint.S
[Какие регистры использует компилятор AVR GCC?]
Data types. Типы данных: char занимает 8 бит (1 байт), int 16 бит (2 байта), long 32 бита (4 байта), long long 64 бита (8 байт), float и double 32 бита (4 байта, поддерживается только формат с плавающей точкой, floating point format), указатели 16 бит (указатели функций адресуют не байты, а слова, т. е. реальный адрес нужно умножать на 2. Это для того, чтобы 16-битное число могло позволить обращаться к любому месту в 128K памяти программ). Имеется опция -mint8 (см. Options for the C compiler avr-gcc) чтобы сделать int размерностью 8 бит, однако это не поддерживается avr-libc и нарушает стандарты C (int должен быть как минимум 16-битным). Опция -mint8 может быть удалена в последующих версиях avr-gcc.
Call-used registers (r18-r27, r30-r31). Регистры, используемые при вызовах функций. Могут быть заняты компилятором gcc для локальных данных (переменных). Вы свободно можете их использовать в подпрограммах на ассемблере, без необходимости сохранения и восстановления (не нужно их сохранять в стек командой push и извлекать из стека командой pop). Если же из кода ASM вызываются подпрограммы на C, то эти регистры могут портиться произвольным образом, так что вызывающий код ассемблера должен отвечать за сохранение и восстановление данных в этих регистрах.
Call-saved registers (r2-r17, r28-r29). Регистры, сохраняемые при вызовах функций. Могут быть выделены gcc для локальных данных (переменных). Вызовы подпрограмм C оставляют эти регистры неизменными. Подпрограммы на языке ассемблера также их должны сохранять при входе и восстанавливать при выходе (обычно с помощью стека операциями push и pop). Если эти регистры изменяются в коде, то r29:r28 (Y pointer) используется при необходимости как указатель на фрейм (frame pointer, указывает на локальные данные в стеке). Требования системы вызова для сохранения/восстановления содержимого этих регистров также относится и к ситуациям, когда компилятор использует эти регистры для передачи аргументов в функцию.
Fixed registers (r0, r1), фиксированные регистры. Никогда не выделяются gcc для локальных данных, но часто используются для определенных целей:
r0 - временный регистр, может использоваться любым кодом на C (за исключением обработчиков прерываний, которые сохраняют значение этого регистра). Обычно используется для запоминания чего-нибудь в пределах кусков кода ассемблера.
r1 - подразумевается, что всегда 0 в любом коде C. Может использоваться для запоминания чего-нибудь в пределах кусков кода ассемблера, однако после использования должен очищаться (clr r1). Это относится к любому использованию инструкций [f]mul[s[u]], которые возвращают результат в паре регистров r1:r0. Обработчики прерываний сохраняют и очищают r1 на входе, и восстанавливают r1 на выходе (в том случае, если r1 не ноль).
Function call conventions, соглашения о вызовах функций. Аргументы выделяются слева направо, в регистрах от r25 до r8. Все аргументы выравниваются для начала с четно нумерованных регистров (нечетные по размеру аргументы, включая char, имеют один свободный регистр перед собой). Это позволяет эффективнее использовать инструкцию movw на расширенном ядре. Если переменных слишком много, то те что не влезли в регистры, передаются через стек.
Возвращаемые значения: 8 бит в r24 (не в r25!), 16 бит в r25:r24, до 32 бит в r22-r25, до 64 бит в r18-r25. 8-битные возвращаемые значения расширяются до нуля/знака к 16 битам вызванной функцией (unsigned char более эффективен, чем signed char - просто clr r25). Аргументы функций с переменным количеством аргументов (например printf и т. п.) все передаются через стек, и тип char расширяется до int.
Внимание: такого выравнивания не было до 2000-07-01, включая старые патчи для gcc-2.95.2. Проверьте Ваши старые подпрограммы на ассемблере, и сделайте соответствующие исправления.
[Ссылки]
1. AVR Studio: как написать обработчик прерывания. 2. AVR-USB-MEGA16: примеры применения, проекты с открытым исходным кодом и принципиальной схемой. 3. avr-gcc: опции компилятора C для микроконтроллеров AVR. 4. AVR-GCC: руководство по встраиванию кода на ассемблере. 5. IAR Embedded Workbench IDE, использование ассемблера в C-проекте. |