Keil C51. Ассемблерные вставки и функции с аргументами
Александр Бельченко
5 ноября 2004
Если перефразировать высказывание Эрика Реймонда о языках Си и Питон, то я бы сказал, что при программировании встраиваемых систем я использую язык Си когда могу, и ассемблер когда должен.
Компилятор Keil C51 позволяет создавать небольшие ассемблерные вставки в ваших си-функциях. Но во многом, в таком контексте их использование очень ограничено. Хотя, если попытаться, любую идею можно довести до абсолютного абсурда. В таком случае она уже перестает казаться абсурдом и превращается в изящное решение. Наиболее часто ассемблерные вставки приходится использовать при написании низкоуровневых драйверов для взаимодействия с некоторой периферией, тогда, когда язык Си не в состоянии обеспечить требуемого уровня гибкости и оптимальности.
Я хочу показать способ, при помощи которого можно полностью писать тело си-функций на асемблере. Этот способ требует определенной внимательности от программиста. Сразу оговорюсь, что в таких случаях правильным решением должно бы стать написание полностью ассемблерного модуля. Но, как вы помните из советского фильма «Айболит-66» Нормальные герои всегда идут в обход!
Итак, начнем. О том, как компилировать модули, содержащие ассемблерные вставки, я упоминал в FAQ по Keil C51, см. пункт «Как в исходном тексте на Си сделать ассемблерные вставки?».
Проблема первая: локальные переменные
В простейшем случае ваша функция не содержит аргументов. И ее прототип записывается примерно так:
void myfunc(void);
Тело функции частично или полностью может состоять из асмеблерной вставки. Неудобства начинаются, когда вам необходимо из асм-кода получить доступ к локальным переменным этой же функци. Задача, сразу скажу, нетривиальная. Для ее разрешения напишите сначала функцию полностью на си и скомпилируйте свой исходник в ассемблерный src-файл. В нем вы найдете ассемблерный эквивалент своей функции и объявления локальных переменных в виде отдельной секции данных. Далее скопируйте ассемблерный фрагмент и оформите все тело функции ввиде асм-вставки. Попутно исправьте все неоптимальные с вашей точки зрения места в коде и можно считать, что вы достигли своей цели.
Наибольшим недостатком функций с локальными переменными является то, что при глобальной перестройке кода и убавлении/добавлении переменных эти операции нужно проводить вручную или полуавтоматически вышеописанным способом.
Проблема вторая: аргументы функции
Если в функцию передаются аргументы, возникает еще одна необычная проблемка. В случае когда все тело функции представляет собой сплошную асм-вставку, то компилятор, во-первых, будет ругаться на то, что аргументы не используются внутри функции, а во-вторых, выделит локальные переменные для их хранения. Хотя, если вы читали главу 6 руководства по Си-компилятору, то знаете, что в большинстве случаев аргументы передаются через регистры. Так, на стр. 164 документа c51.pdf в параграфе «Parameter Passing in Registers» показано в таблице, как передаются параметры в функции с использованием регистров:
Arg Number char, 1-byte ptr int, 2-byte ptr long, float generic ptr
1 R7 R6 & R7 R4—R7 R1—R3
(MSB in R6, (Mem type in R3,
LSB in R7) MSB in R2,
LSB in R1)
2 R5 R4 & R5 R4—R7 R1—R3
(MSB in R4, (Mem type in R3,
LSB in R5) MSB in R2,
LSB in R1)
3 R3 R2 & R3 R1—R3
(MSB in R2, (Mem type in R3,
LSB in R3) MSB in R2,
LSB in R1)
Из этой таблицы следует, что в регистрах может быть передано до 3х аргументов типа char/int, только один аргумент типа long/float и только один аргумент типа обобщенного указателя (generic pointer).
Если аргументов будет больше, или аргумент long будет третьим в списке, то все остальные аргументы, неумещающиеся в регистрах будут передаваться в фиксированных ячейках памяти и работать с ними придется как с локальными переменными функции (см. проблему первую). С теми же аргументами, которые передаются в регистрах, работать легче, если мы сами не портим эти регистры.
Например, передается два аргумента. 1й типа unsigned long представляет собой адрес какой-нибудь большой внешней памяти. 2й типа unsigned char данные для записи в эту внешнюю память. В таком варианте прототип функции может быть записан так:
void myfunc(unsigned long addr, unsigned char byte);
Однако, в таком виде для переменной data (arg number = 2) нет места в регистрах и она будет помещена в локальные переменные. Если же объявить функцию как:
void myfunc(unsigned int addr_low, unsigned int addr_high, unsigned char byte);
то все поместится в регистрах.
Если основная задача функции не математические расчеты, а только пересылка данных в/из устройства, т.е. собственно реализация функций драйвера, то расположение аргументов в регистрах более чем желательно.
Для того, чтобы компилятор Си не пытался скопировать переданные аргументы в локальные переменные функции, необходимо объявить функцию как функцию, которая не имеет аргументов. Звучит парадоксально, но в этом и заключается наш хак.
Здесь есть две тонкости. Первая: как правильно записать функцию в си-модуле. Вторая: как записать прототип функции в заголовочном файле, который будет включен в другие си-модули.
Имя функции должно начинаться с подчеркивания
В той же главе 6 документа c51.pdf на стр. 162 рассказывается, как формируются имена секций, а также как различаются имена функций в зависимости от способа передачи аргуметов.
Так, имя функции (что практически тоже самое, что и асемблерная метка, указывающая на точку входа в функцию), которая не получает аргументов, совпадает с сишным именем функции, а символическое имя приводится к заглавному регистру.
C-код Асм-код
void func(void) FUNC:
Функция, аргументы в которую передаются через регистры (большинство типичных функций) в асм-представлении получает символ подчеркивания перед именем:
C-код Асм-код
void func(char b) _FUNC:
В реентерантные функции (допускающие рекурсивный вызов) аргументы передаются через специально организованный реентерантный стек. В асм-представлении перед именем функции добавляется два символа _?
C-код Асм-код
void func(char b) _?FUNC:
Таким образом, для того чтобы соблюсти внутренний стандарт именования компилятора Keil C51, мы должны записывать функцию в си-модуле без явных аргументов, но с подчеркиванием перед именем. Так для вышерасcматриваемого примера:
Чистый Си:
void myfunc(unsigned int addr_low, unsigned int addr_high, unsigned char byte)
{
...
}
Хак для сокрытия наличия аргументов:
//void myfunc(unsigned int addr_low, unsigned int addr_high, unsigned char byte)
void _myfunc (void)
{
#pragma ASM
...
#pragma ENDASM
}
Как видно, я всегда оставляю в комментариях указание того, чем эта функция является на самом деле. Именно эта строка должна использовать в качестве прототипа функции, объявляемой в заголовочном файле.
Доступ к переданным аргументам осуществляется через регистры. Так, переменная addr_low располагается в регистрах R6/R7, переменная addr_high располагается в регистрах R4/R5, а переменная byte в регистре R3. Такое расположение очень удобно для использования в асм-коде.
Прототип в заголовочном файле
В качестве прототипа рассматриваемой функции должно быть указано настоящее полное имя си-функции без подчеркивания, и со всеми аргументами функции. Но! Заголовочный файл с этим определением не может быть включен в тот си-модуль, в котором находится эта странная реализация функции. Иначе компилятор выдаст ошибку: две функции с одинаковыми именами, но с разным количеством аргументов.
Запись прототипа функции в заголовочном файле:
void myfunc(unsigned int addr_low, unsigned int addr_high, unsigned char byte);
Проблема третья: возвращаемое значение
Эта проблема подобна проблеме второй. Выражается она в том, что если функция объявлена, как возвращающая некоторое значение, то компилятор будет выдавать предупреждение, если тело функции целиком состоит из ассемблерной вставки. Предупреждение это будет гласить о том, что ваша функция ничего не возвращает, хотя должна бы.
Это обходится тем же методом-хаком, что и проблема 2. Функция записывается, как возвращающая void (т.е. ничего):
// unsigned char myfunc (void)
void myfunc (void)
{
#pragma ASM
...
; в конце перед RET положим в регистр R7 возвращаемое значение
MOV R7, A
#pragma ENDASM
}
А о возвращаемом значении должен позаботиться сам программист. Как показано в параграфе «Function Return Values» практически все типы данных возвращаются через регистры.
Аналогично проблеме второй, в заголовочном файле прототип функции указывается в своем настоящем си-виде.
Для чего эти хаки
Описанные выше хаки позволяют в особо извращенной форме писать короткие функции на чистом ассемблере, при этом не написав ни одного полноценного асм-модуля, и в тоже время без проблем вызывать эти функции из других си-модулей.
Описанный хак удобно применять при написании небольших низкоуровневых драйверов, там где критично быстродействие и размер кода. А также когда необходим доступ к таким ассемблерным операциям, для которых нет прямых аналогов в Си. Например, циклический сдвиг байта совместно с флагом переноса (часто используется при программной реализации различных последовательных интерфейсов).