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» практически все типы данных возвращаются через регистры.

Аналогично проблеме второй, в заголовочном файле прототип функции указывается в своем настоящем си-виде.

Для чего эти хаки

Описанные выше хаки позволяют в особо извращенной форме писать короткие функции на чистом ассемблере, при этом не написав ни одного полноценного асм-модуля, и в тоже время без проблем вызывать эти функции из других си-модулей.

Описанный хак удобно применять при написании небольших низкоуровневых драйверов, там где критично быстродействие и размер кода. А также когда необходим доступ к таким ассемблерным операциям, для которых нет прямых аналогов в Си. Например, циклический сдвиг байта совместно с флагом переноса (часто используется при программной реализации различных последовательных интерфейсов).