Ассемблерные вставки в AVR-GCC

AVR
Практически всегда, когда в проекте задействованы АЦП, встаёт необходимость провести математическую обработку того, что там нацифровалось. Мат. обработка, в зависимости от задачи, может варьироваться от примитивного «сложить два измерения и поделить пополам (ака сдвинуть на разряд вправо)» до всяких там БПФ, цифровых фильтров и далее по списку. Если математика чуть сложнее, чем «найти максимум за период», а измерения непрерывные, то частенько встаёт вопрос в скорости обработки. Собственно говоря, это вообще-то отправная точка для выбора платформы, на которую будет опираться проект, тут надо здраво оценить потребности задачи и возможности различных платформ. Конечно, для ядрёной числодробилки лучше взять какой-нибудь DSP, а может даже и FPGA. А если наша числодробилка не особо ядрёная, зато требуется минимизировать энергопотребление этой фигни, да и конечная стоимость должна быть не как у самолёта? А ещё есть такие факторы, как опыт разработчика, доступность комплектухи и т.д. Короче если мы решили, что мозгами в нашем проекте должна работать старая добрая AVR, но мозга у неё не хватает, на то что бы осмыслить наш алгоритм, объяснённый ей на языке C, придётся объяснять на Assembler`е.
Итого, постановка задачи:
Дано
  1. микроконтроллер семейства AVR;
  2. основная часть управляющей программы пишется на C;
  3. используется компилятор AVR-GCC;
  4. есть фрагмент алгоритма, который выполняется достаточно часто и кушает большую часть времени работы контроллера.
Требуется минимизировать время выполнения ресусоёмких операций.

Традиционно существует два подхода к смешению кода на C и ASM: написание отдельно сишных исходников и отдельно ассемблерных с последующей совместной линковкой и написание ассемблерного кода прямо в сишных исходниках в виде вставок. Оба метода не переносимы и требуют знания соглашений конкретного компилятора, которым предполагается собирать проект. Первый способ немножко более универсальный — при портировании на другой компилятор нужно будет только поправить имена используемых регистров (где аргументы, куда результат и что свободно для промежуточной работы). Второй… эээ… ну в общем случае, возможно, немного нагляднее, хотя спорный вопрос. Но у нас-то случай не общий, у нас компилятор gcc (см. заголовок статьи). Ассемблерные вставки в GCC хоть и имеют свои недостатки (о них ниже), зато позволяют почти полностью забыть о вопросах планирования регистров — компилятор берёт это на себя.
Итак, что такое ассемблерная вставка? Попрошу внимательно прочитать следующее предложение и осмыслить его — это важно. Ассемблерная вставка — это фрагмент текста, который парсер языка C на прямую передаёт компилятору языка Assembler, вместе с ассемблерным текстом, сгенерированным в результате трансляции сишного исходника. А для упрощения осмысления я напомню, как работает компилятор, на примере GCC4. Ниже приведена диаграмма, упрощённо показывающая основные стадии сборки программы.
Струкура GCC

Думаю, пояснений здесь не требуется — нам сейчас не важно, что такое GENERIC, GIMPLE, RTL и прочие умные слова, важно только понимание на какой стадии что делается и о чём в это время знает компилятор (впрочем, если надо расписать подробнее — спрашивайте). Далее по тексту под термином «компилятор» я буду понимать всё, что происходит до ассемблирования.

Для объявления ассемблерных вставок в языке C используется встроенная функция asm() (ещё встречаются вариации на тему __asm__, но обычно ключевые слова и функции, начинающиеся с двух подчёркиваний подразумеваются для внутреннего использования). “Встроенная” — значит что для её использования не нужно подключать никакие библиотеки. Простейшая ассемблерная вставка может выглядеть так:

asm("cli");

Ну вот, в первом приближении как-то так. Всё вышесказанное было кросс-компиляторным, но для того, что бы написать что-то полезное этого явно не достаточно, так что поехали копать вглубь, в gcc-специфичные аспекты.
На самом деле asm() — это не совсем функция, потому что эта инструкция имеет не стандартный формат вызова. Во-первых, между словом asm и списком аргументов может находиться ключевое слово volatile, а во-вторых, в отличие от обычных функций, здесь аргументы разделяются не запятой, а двоеточием.
Ключевое слово volatile в большинстве C-компиляторов означает инструкцию бравому оптимизатору «ЭТО РУКАМИ НЕ ТРОЖЬ!» и обычно применяется для защиты глобальных переменных, изменяемых в прерываниях, или для работы с MMIO. Зачем оно здесь? Казалось бы, из приведённого выше определения следует, что оптимизатор не может запустить свои шаловливые ручки в наш ассемблерный код. Это так, однако, при определённых условиях, он может решить, что наш код ничего полезного для программы не делает и удалить его целиком. Какие это условия? Ну, в первую очередь, под угрозу попадают функции с возвращаемыми значениями, результаты выполнения которых далее в программе не используются. Так же могут быть убиты вставки вообще без входов и выходов (например просто вызывающие nop, wdr, cli/sei), впрочем, здесь точно не уверен. В любом случае, volatile после слова asm лучше указывать всегда — хуже не будет.

asm volatile("cli");

Теперь что касается аргументов. Число их переменное и может быть от одного до четырёх:

asm (code: output_operand_list: input_operand_list: clobber_list);

Как правило, используется первые три аргумента, четвёртый — это костыль, подставляемый для экранирования регистров и переменных.
  • code — собственно текст ассемблерной вставки;
  • output_operand_list — список выходных операндов;
  • input_operand_list — список входных операндов;
  • clobber_list — список эранируемых элементов.

Разберём по порядку.
1) Code — это строка, содержащая исходник подпрограммы на ассемблере. Строка — это, конечно, хорошо, но даже небольшой ассемблерный исходник, написанный в одну строку, является нечитабельной кашей. Спасает то, что в C есть, простите за тавтологию, многострочные строки — строки, между которыми нет ни одного непробельного символа, склеиваются в одну. Это улучшает читабельность в исходном сишном файле, но в сгенерённом асме по-прежнему будет каша. Для окончательного искоренения каши перевод строки в явном виде пишут в тексте вставки. В итоге получается что-то такое:

asm volatile(
	"nop\n\t"
	"nop\n\t"
	"nop\n\t"
	"nop\n\t"
);

Для написания кода используются стандартные мнемоники GCC AVR ассемблера (внимание! они немного отличаются от мнемоник Atmel AVR ассемблера), имена операндов, записываемые после знака %, и специальные регистры, описанные в таблице:

	Имя		Описание
	__SREG__	Регистр статуса (0x3F)
	__SP_H__	Старший байт указателя стека (0x3E)
	__SP_L__	Младший байт указателя стека (0x3D)
	_PC_		Счётчик инструкций
	__tmp_reg__	Временный регистр (r0)
	__zero_reg__	Нулевой регистр (r1)

Обратите внимание на две последние строки — это особые тараканы avr-gcc. Регистр r0 используется как временный регистр, куда можно положить что-то на передержку на пару тактов, не задумываясь о том, что там было что-то раньше и что мы может сломать что-то в будущем. Регистр r1 предполагается всегда равным нулю, используется когда нужен 0 в инструкциях, которые не принимают константу.
Лирическое отступление: в AVR команды аппаратного умножения 8*8=16 складывают результат в регистры r1:r0. Отсюда вывод — не забывайте обнулять r1 после умножения!
Кроме того, есть ещё хитрый синтаксис для меток. Дело в том, что если наша вставка будет инлайниться в бинарнике несколько раз, то получится, что программа содержит несколько одинаковых меток. Что бы избежать этого к имени метки добавляется спец. последовательность "%=", которая внутри каждого блока asm заменяется на число, уникальное для всей программы.

//бесконечный цикл
asm volatile (
	"Loop_%=: " "RJMP Loop_%="
);


2) Списки входных и выходных операндов.
Собственно, самая важная и интересная тема — как ассемблерная вставка общается с внешним миром. Списки операндов описывают, что нужно данному коду для работы.
Синтаксис у входного и выходного списков одинаковый — перечисление описаний операндов, разделённых запятой. Описание операнда в общем случае имеет следующий вид

[name] "type" (value)

где name — имя операнда (может быть опущено), type — тип операнда и value — значение операнда.
Самое интересное поле — это тип операнда — строка состоящая, из ограничителя и модификатора. Ограничитель описывается буквой и указывает компилятору каким условиям должен соответствовать данный операнд. Наиболее часто-используемые ограничители приведены в таблице (более подробный список см. в [1]):

Ограничитель	Описание						Допустимые значения
r		Любой регистр						r0 - r31
l		Нижний регистр						r0 - r15
d		Верхний регистр						r16 - r31
a		Простой верхний регистр					r16 - r23
e		Адресный регистр					x (r27:r26), y (r29:r28), z(r31:r30)
I		Положительная 6-ти битная целочисленная константа	0 - 63
J		Отрицательная 6-ти битная целочисленная константа	-63 - 0
M		8-ми битная целочисленная константа			0 - 255

Думаю, понятно, что ограничитель должен соответствовать требованиям к аргументам тех инструкций, которые используют данный операнд (не забываем, что в AVR далеко не все команды принимают любые регистры, например беззнаковое умножение принимает все регистры, знаковое — верхние, а знаковое на беззнаковое — только простые верхние).
Модификатор — это символ, который при необходимости добавляется перед ограничителем. Операнд без модификатора трактуется как «read-only», модификатор '=', делает операнд «write-only», а модификатор '&' говорит, что операнд используется только как выход. Эта информация нужна в первую очередь оптимизатору что бы знать как что используется и где что можно выбросить.
Кроме того, модификатор может содержать имя другого операнда. Это нужно, когда операнд является и входом и выходом.

asm volatile("swap %[VAL]" : [VAL]"=r" (value) : "[VAL]" (value));

Значение оператора (value) это то, до чего мы хотим дотянуться из ассемблерной вставки. В зависимости от ограничителя это может быть регистр, в котором хранится переменная, указатель на переменную в памяти или константа. Если требуется передать в код многобайтную переменную, то её имя точно так же указывается в качестве value, а в коде используется расширенная адресация — между знаком % и именем операнда добавляется буква, обозначающая номер байта в переменной — 'A' младший байт, 'B' второй байт и так далее, в зависимости от типа значения. Для указателей в инструкциях, работающих с адресными регистрами, между знаком % и именем операнда необходимо писать букву 'a' (именно прописную, не заглавную!).
Имя операнда указывает как мы будем обращаться к нему внутри ассемблерной вставки. Обязательной частью имени операнда являются квадратные скобки. Имя можно не указывать вообще, тогда обращаться к операндам можно по номеру:

asm volatile(
	"in %0,%1" "\n\t"
	"out %1, %2" "\n\t"
	: "=&r" (input)
	: "I" (port), "r" (output)
);

Номера присваиваются в порядке объявления операндов, если один из них ссылается на другой, то он пропускается. Правила расширенной адресации остаются те же.
3) Список экранирования содержит имена регистров, которые используются на запись в ассемблерной вставке, но не объявлены в списках операндов. Эти регистры, в случае необходимости, будут запиханы в стек перед входом во вставку и вынуты обратно после выхода. Кроме того, может быть указано специальное слово «memory», которое говорит, что код во вставке может модифицировать произвольную область памяти. В этом случае компилятор перед входом во вставку сохранит в память все переменные, которые в тот момент загружены в регистры, а по выходу вновь загрузит значения из памяти.
Следует избегать использования списка экранирования, так как это сильно стесняет свободу оптимизатора. Например временные переменные лучше объявлять вне ассемблерной вставки — это позволит компилятору самому выбирать регистры для их хранения.

С форматом команды asm разобрались, теперь пару слов об использовании.
Основной недостаток ассемблерных вставок вытекает из определения, на которое я просил обратить внимание в начале — в тексте вставки нельзя использовать макросы препроцессора, в том числе и имена портов, регистров, битов и так далее:

asm volatile("sbi PORTB, 0x07");

Для получения этих значений нужно использовать целочисленные операнды:

asm volatile("sbi %0, 0x07" :: "I" (_SFR_IO_ADDR(PORTB)));

Следует так же обратить внимание на конструкцию _SFR_IO_ADDR(PORTB): дело в том, что PORTB — это не просто адрес, соответствующий порту, а хитрый макрос, преобразующий адресные пространства, что необходимо для реализации Memory Mapped IO. Если попытаться передать значение PORTB напрямую во ставку компилятор ругнётся на ошибку в ограничителе:

main.c:14: warning: asm operand 0 probably doesn't match constraints
main.c:14: error: impossible constraint in 'asm'

А, может, и не ругнётся — зависит от того, какой контроллер мы используем. Нам же нужно передать во вставку значение изначального адреса. Макрос _SFR_IO_ADDR делает обратное преобразование, выдавая именно ту константу, которая нам требуется.

Едем дальше. Если вставка используется более одного раза, то её можно оформить либо как функцию, либо как макрос. С функцией всё просто — объявляем локальные переменные нужных типов и передаём их в asm. С макросами по-сложнее — здесь тоже можно объявить локальные переменные, но только при условии, что используется не стандарт «C ANSI», а что-то посвежее. Кстати, я рекомендую выставлять стандарт GNU99 (-std=gnu99) — много полезных фич добавляется. Ниже приведено несколько примеров использования — результаты моих колупаний (исходные алгоритмы не мои, а потыреные в инете, ссылки на источники прилагаются).

Внимание! Я не гарантирую, что функции работают правильно на всём диапазоне. Если решите использовать эту фигню в своих проектах, обязательно проведите самостоятельное тестирование. Кроме того, нужно внимательно относиться к типу аргументов и результатов (знаковый/беззнаковый)

У умных дядек обычно принято в таких случаях указывать размер кода в словах и длительность выполнения в циклах, но я к ним не отношусь, поэтому могу только указать данные по исходным алгоритмам и время выполнения кода по осциллографу (atmega644PA на 1MHz).

1) Умножение int32_t a_result = int16_t a_x * int16_t a_y
Исходный алгоритм
Cycles: 17 + ret
Words: 13 + ret
Register usage: 11 registers
В моём варианте время выполнения 30мкс (встроенное умножение 32*32=32 — 66мкс).

#define MULS16X16_32(a_x, a_y, a_result)			\
do {								\
	uint8_t tmp = 0;					\
	asm volatile (						\
	"clr	%[TMP]"			"\n\t" 			\
	"muls	%B[AX], %B[AY]"		"\n\t" 			\
	"movw	%C[RES], r0"		"\n\t" 			\
	"mul	%A[AX], %A[AY]"		"\n\t" 			\
	"movw	%A[RES], r0"		"\n\t" 			\
	"mulsu	%B[AX], %A[AY]"		"\n\t" 			\
	"sbc	%D[RES], %[TMP]"	"\n\t" 			\
	"add	%B[RES], r0"		"\n\t" 			\
	"adc	%C[RES], r1"		"\n\t" 			\
	"adc	%D[RES], %[TMP]"	"\n\t" 			\
	"mulsu	%B[AY], %A[AX]"		"\n\t" 			\
	"sbc	%D[RES], %[TMP]"	"\n\t" 			\
	"add	%B[RES], r0"		"\n\t" 			\
	"adc	%C[RES], r1"		"\n\t" 			\
	"adc	%D[RES], %[TMP]"	"\n\t" 			\
	"clr	r1"			"\n\t" 			\
		: [RES]"=&r" (a_result), [TMP]"=&r" (tmp)	\
		: [AX]"a" (a_x), [AY]"a" (a_y)			\
	);							\
} while(0)


2) Целочисленный квадратный корень uint16_t a_result = sqrt(uint32_t a_arg)
Исходный алгоритм
Cycles: 271 — 316
Words: 43
Register usage: 8 registers
В моём варианте время выполнения в среднем 280мкс (avr-libc-шный оптимизированный корень с плавающей точкой — 600-700мкс, из них примерно 500мкс, собственно, сам корень, а остальное — преобразование из целочисленных типов во float и обратно).
Внимание! Аргумент a_arg разрушается. Если он ещё нужен в дальнейшем — необходимо заводить временную переменную

#define SQRT32(a_arg, a_result)									\
do {												\
	uint16_t tmp;										\
	asm volatile (										\
		"ldi   %B[TMP],0xc0"		"\n\t"						\
		"clr   %A[TMP]"			"; rotation mask in [TMP]\n\t"			\
		"ldi   %B[RES],0x40"		"\n\t"						\
		"sub   %A[RES],%A[RES]"		"; developing sqrt in [RES], C=0\n\t"		\
"_sq32_1_%=:"	"brcs  _sq32_2_%="		"; C --> Bit is always 1\n\t"			\
		"cp    %C[ARG],%A[RES]"		"\n\t"						\
		"cpc   %D[ARG],%B[RES]"		"; Does test value fit?\n\t"			\
		"brcs  _sq32_3_%="		"; C --> nope, bit is 0\n\t"			\
"_sq32_2_%=:"	"sub   %C[ARG],%A[RES]"		"\n\t"						\
		"sbc   %D[ARG],%B[RES]"		"; Adjust argument for next bit\n\t"		\
		"or    %A[RES],%A[TMP]"		"\n\t"						\
		"or    %B[RES],%B[TMP]"		"; Set bit to 1\n\t"				\
"_sq32_3_%=:"	"lsr   %B[TMP]"			"\n\t"						\
		"ror   %A[TMP]"			"; Shift right mask, C --> end loop\n\t"	\
		"eor   %B[RES],%B[TMP]"		"\n\t"						\
		"eor   %A[RES],%A[TMP]"		"; Shift right only test bit in result\n\t"	\
		"rol   %A[ARG]"			"; Bit 0 only set if end of loop\n\t"		\
		"rol   %B[ARG]"			"\n\t"						\
		"rol   %C[ARG]"			"\n\t"						\
		"rol   %D[ARG]"			"; Shift left remaining argument (C used at _sq32_1)\n\t" \
		"sbrs  %A[ARG],0"		"; Skip if 15 bits developed\n\t"		\
		"rjmp  _sq32_1_%="		"; Develop 15 bits of the sqrt\n\t"		\
		"brcs  _sq32_4_%="		"; C--> Last bits always 1\n\t"			\
		"cp    %A[RES],%C[ARG]"		"\n\t"						\
		"cpc   %B[RES],%D[ARG]"		"; Test for last bit 1\n\t"			\
		"brcc  _sq32_5_%="		"; NC --> bit is 0\n\t"				\
"_sq32_4_%=:"	"sbc   %B[ARG],%B[TMP]"		"; Subtract C (any value from 1 to 0x7f will do)\n\t" \
		"sbc   %C[ARG],%A[RES]"		"\n\t"						\
		"sbc   %D[ARG],%B[RES]"		"; Update argument for test\n\t"		\
		"inc   %A[RES]"			"; Last bit is 1\n\t"				\
"_sq32_5_%=:"	"lsl   %B[ARG]"			"; Only bit 7 matters\n\t"			\
		"rol   %C[ARG]"			"\n\t"						\
		"rol   %D[ARG]"			"; Remainder * 2 + C\n\t"			\
		"brcs  _sq32_6_%="		"; C --> Always round up\n\t"			\
		"cp    %A[RES],%C[ARG]"		"\n\t"						\
		"cpc   %B[RES],%D[ARG]"		"; C decides rounding\n\t"			\
"_sq32_6_%=:"	"adc   %A[RES],%B[TMP]"		"\n\t"						\
		"adc   %B[RES],%B[TMP]"		"; Round up if C (B[TMP]=0)\n\t"		\
			: [ARG]"=r" (a_arg), [RES]"=&d" (a_result), [TMP]"=&d" (tmp)		\
			: "[ARG]" (a_arg)							\
	);											\
} while(0)


3) Корень из суммы квадратов uint16_t a_result = sqrt((int16_t a_x)^2 + (int16_t a_x)^2)
Исходный алгоритм (примерно, без учёта подсчёта корня)
Cycles: 49
Words: 40
Register usage: 14 registers (в моём варианте, в исходном — не считал)
В моём варианте время выполнения 320мкс.

#define SQRT_SQ_SUMM_16(a_x, a_y, a_result)							\
do {												\
	uint16_t tmp16;										\
	uint32_t tmp32;										\
	asm volatile (										\
		"clr	%A[TMP16]"		"\n\t"						\
		"sbrs	%B[AX], 7"		"\n\t"						\
		"rjmp	_ss16_1_%="		"\n\t"						\
		"com	%A[AX]"			"\n\t"						\
		"com	%B[AX]"			"\n\t"						\
		"adc	%A[AX], %A[TMP16]"	"\n\t"						\
		"adc	%B[AX], %A[TMP16]"	"\n\t"						\
"_ss16_1_%=:"	"sbrs	%B[AY], 7"		"\n\t"						\
		"rjmp	_ss16_2_%="		"\n\t"						\
		"com	%A[AY]"			"\n\t"						\
		"com	%B[AY]"			"\n\t"						\
		"adc	%A[AY], %A[TMP16]"	"\n\t"						\
		"adc	%B[AY], %A[TMP16]"	"\n\t"						\
"_ss16_2_%=:"	"mul	%A[AY], %A[AY]"		"\n\t"						\
		"movw	%A[TMP32], r0"		"\n\t"						\
		"mul	%B[AY], %B[AY]"		"\n\t"						\
		"movw	%C[TMP32], r0"		"\n\t"						\
		"mul	%A[AY], %B[AY]"		"\n\t"						\
		"add	%B[TMP32], r0"		"\n\t"						\
		"adc	%C[TMP32], r1"		"\n\t"						\
		"adc	%D[TMP32], %A[TMP16]"	"\n\t"						\
		"add	%B[TMP32], r0"		"\n\t"						\
		"adc	%C[TMP32], r1"		"\n\t"						\
		"adc	%D[TMP32], %A[TMP16]"	"\n\t"						\
		"mul	%A[AX], %A[AX]"		"\n\t"						\
		"movw	%A[RES], r0"		"\n\t"						\
		"mul	%B[AX], %B[AX]"		"\n\t"						\
		"add	%A[TMP32], %A[RES]"	"\n\t"						\
		"adc	%B[TMP32], %B[RES]"	"\n\t"						\
		"adc	%C[TMP32], r0"		"\n\t"						\
		"adc	%D[TMP32], r1"		"\n\t"						\
		"mul	%A[AX], %B[AX]"		"\n\t"						\
		"add	%B[TMP32], r0"		"\n\t"						\
		"adc	%C[TMP32], r1"		"\n\t"						\
		"adc	%D[TMP32], %A[TMP16]"	"\n\t"						\
		"add	%B[TMP32], r0"		"\n\t"						\
		"adc	%C[TMP32], r1"		"\n\t"						\
		"adc	%D[TMP32], %A[TMP16]"	"\n\t"						\
		"clr	r1"			"\n\t"						\
		"ldi   %B[TMP16],0xc0"		"\n\t"						\
		"clr   %A[TMP16]"		"; rotation mask in [TMP16]\n\t"		\
		"ldi   %B[RES],0x40"		"\n\t"						\
		"sub   %A[RES],%A[RES]"		"; developing sqrt in [RES], C=0\n\t"		\
"_sq32_1_%=:"	"brcs  _sq32_2_%="		"; C --> Bit is always 1\n\t"			\
		"cp    %C[TMP32],%A[RES]"	"\n\t"						\
		"cpc   %D[TMP32],%B[RES]"	"; Does test value fit?\n\t"			\
		"brcs  _sq32_3_%="		"; C --> nope, bit is 0\n\t"			\
"_sq32_2_%=:"	"sub   %C[TMP32],%A[RES]"	"\n\t"						\
		"sbc   %D[TMP32],%B[RES]"	"; Adjust argument for next bit\n\t"		\
		"or    %A[RES],%A[TMP16]"	"\n\t"						\
		"or    %B[RES],%B[TMP16]"	"; Set bit to 1\n\t"				\
"_sq32_3_%=:"	"lsr   %B[TMP16]"		"\n\t"						\
		"ror   %A[TMP16]"		"; Shift right mask, C --> end loop\n\t"	\
		"eor   %B[RES],%B[TMP16]"	"\n\t"						\
		"eor   %A[RES],%A[TMP16]"	"; Shift right only test bit in result\n\t"	\
		"rol   %A[TMP32]"		"; Bit 0 only set if end of loop\n\t"		\
		"rol   %B[TMP32]"		"\n\t"						\
		"rol   %C[TMP32]"		"\n\t"						\
		"rol   %D[TMP32]"		"; Shift left remaining argument (C used at _sq32_1)\n\t" \
		"sbrs  %A[TMP32],0"		"; Skip if 15 bits developed\n\t"		\
		"rjmp  _sq32_1_%="		"; Develop 15 bits of the sqrt\n\t"		\
		"brcs  _sq32_4_%="		"; C--> Last bits always 1\n\t"			\
		"cp    %A[RES],%C[TMP32]"	"\n\t"						\
		"cpc   %B[RES],%D[TMP32]"	"; Test for last bit 1\n\t"			\
		"brcc  _sq32_5_%="		"; NC --> bit is 0\n\t"				\
"_sq32_4_%=:"	"sbc   %B[TMP32],%B[TMP16]"	"; Subtract C (any value from 1 to 0x7f will do)\n\t" \
		"sbc   %C[TMP32],%A[RES]"	"\n\t"						\
		"sbc   %D[TMP32],%B[RES]"	"; Update argument for test\n\t"		\
		"inc   %A[RES]"			"; Last bit is 1\n\t"				\
"_sq32_5_%=:"	"lsl   %B[TMP32]"		"; Only bit 7 matters\n\t"			\
		"rol   %C[TMP32]"		"\n\t"						\
		"rol   %D[TMP32]"		"; Remainder * 2 + C\n\t"			\
		"brcs  _sq32_6_%="		"; C --> Always round up\n\t"			\
		"cp    %A[RES],%C[TMP32]"	"\n\t"						\
		"cpc   %B[RES],%D[TMP32]"	"; C decides rounding\n\t"			\
"_sq32_6_%=:"	"adc   %A[RES],%B[TMP16]"	"\n\t"						\
		"adc   %B[RES],%B[TMP16]"	"; Round up if C (B[TMP16]=0)\n\t"		\
			: [RES]"=&d" (a_result), [TMP32]"=&r" (tmp32), [TMP16]"=&d" (tmp16)	\
			: [AX]"r" (a_x), [AY]"r" (a_y)						\
	);											\
} while(0)


Ссылки:
1) GCC-AVR Inline Assembler Cookbook
2) AN201 (на сайте atmel`а его что-то нет, так что ссылка левая)
3) http://members.chello.nl/j.beentjes3/Ruud/sqrt32avr.htm
4) http://elm-chan.org/docs/avrlib/sqrt32.S
5) AVR Instruction Set

  • +12
  • 11 марта 2011, 07:47
  • Alatar

Комментарии (48)

RSS свернуть / развернуть
Полезная статья. Пока просмотрел бегло, домой приду прочитаю внимательней. Нужно бывает вставки делать…
+1
Хорошо бы макросы обернуть не просто в скобки, а в do/while:
#define MULS16X16_32(a_x, a_y, a_result)                        \\
do {                                                            \\
        ...                                                     \\
} while (0)
+1
  • avatar
  • John
  • 11 марта 2011, 14:19
Честно говоря, не вижу в этом никакого смысла. По крайней мере при работе с -std=gnu99 разницы между просто блоком и блоком с while(0) ИМХО нету.
0
Например, без do/while следующий код вызовет ошибку компиляции:
if (condition)
    MULS16X16_32(a_x, a_y, a_result);
else
    statement;
+1
Да, в этом плане разница есть, согласен. В if придётся точку с запятой перед else убирать, что не правильно. Я обычно просто выражения в фигурные скобки заключаю, так что про этот нюанс забыл. Действительно, пожалуй while добавлю, что б правильнее было.
Спасибо.
+1
Шикарно, шикарно! Пример почти что образцовой статьи.

1) Поднята очень редкая тема, раскрытие которой я не видел ни разу (честно говоря сам хорошо туплю на асмовых вставках в WinAVR, а Cookbook только моск сьел и ясности не добавил), а тут прям все так по полочкам да с примерами.
+1
1) Поднята очень редкая тема, раскрытие которой я не видел ни разу (честно говоря сам хорошо туплю на асмовых вставках в WinAVR, а Cookbook только моск сьел и ясности не добавил), а тут прям все так по полочкам да с примерами.
Чесно говоря, половина примеров, как впрочем, и половина текста, взято из того самого кукбука, но вообще да, мне он тоже с неделю мозг ел…
0
Там все по аглицки и непонятно :(
0
Елы-палы))
как раз писал статью на эту тему… ну ладно

Кстати, а кто-нибудь разобрался с использованием ассемблерных функций, скомпилированных до этого в объектные файлы? (функция пишется в отдельном файле с директивой
.global (имя)
, потом создается заголовочный файл с ее прототипом)

Я так и не разобрался, как использовать в такой функции параметры и возвращать значение
0
Ну тут-то, вроде, как раз всё просто (пока аргументов не очень много) — аргументы прибиты гвоздями к регистрам, возвращаемые значения забираются так же с жестко заданных мест. В доке на avr-libc всё прописано www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_reg_usage
Function call conventions:
Arguments — allocated left to right, r25 to r8. All arguments are aligned to start in even-numbered registers (odd-sized arguments, including char, have one free register above them). This allows making better use of the movw instruction on the enhanced core.

If too many, those that don't fit are passed on the stack.

Return values: 8-bit in r24 (not r25!), 16-bit in r25:r24, up to 32 bits in r22-r25, up to 64 bits in r18-r25. 8-bit return values are zero/sign-extended to 16 bits by the called function (unsigned char is more efficient than signed char — just clr r25). Arguments to functions with variable argument lists (printf etc.) are all passed on stack, and char is extended to int.

В чём вопросы то?
0
Ох… Огромный респект автору за статью, давно хотел узнать по этой теме побольше.

Для себя же окончательно понял, что мой метод — отдельные ассемблерные модули. Уж больно суровый синтаксис у инлайнового асма…
0
  • avatar
  • _YS_
  • 11 марта 2011, 23:52
А мне нравится — не надо трахать мозг с занятостью регистров — компилятор сам все продумает. Компилятор не спотыкается о внезапно не оптимизируемые куски и так далее.
0
Дело вкуса, да.
0
поддерживаю))) мне вообще тогда проще всё на си писать
0
Ну на С то, конечно, проще, но не всегда приемлемо получается. Можно, конечно, писать в стиле «высокоуровневый ассемблер» — это даёт определённый прирост скорости, но до асма всё равно в ряде случаев не дотягивает. Да и не факт, что это будет сильно читабельнее. =)
0
Вот потому, когда мне не хочется ломать голову над распределением регистров и меток, я пишу на С. А в тех местах, где нужна большая скорость/хитрая оптимизация, перехожу на честный ассемблер и пишу функцию в отдельном модуле.
0
Поглядите на мою релизацию 1-Wire. Там как раз на асме достаточно написано.
0
Вообще, я не работаю с AVR Studio 6. Мне она кажется слишком раздутой.

Какое именно сообщение об ошибке?

Еще тут можно почитать, это как раз про шестую.

Проект смотрел, явных косяков не вижу.
0
инсталятор 6.1 весит намного меньше чем 6 версия.

И вытягивает 100500 мегабайт из интернета?
0
Судя по тексту, не нравится не .global, а что-то до него. Например, в заголовочнике. Проверьте, есть ли в шестой студии соглашение о дефайне __ASSEMBLER__ (80% что-то с этим). Попробуйте скомпилировать, закомментировав содержимое asmroutine.h целиком. Попробуйте убрать в ассемблерном исходнике комментарии в сишном стиле.

Не, шестую студию я ставить, конечно, не буду.: ) Мне жалко места на диске, тем более что 4.18 меня полностью удовлетворяет.
0
Вы удивитесь, но в мануале.: ) В AVR Studio используется компилятор AVR GCC, точнее целиком AVR GCC Toolchain. В моей стаье есть ссылка на мануал, на сайте Atmel тоже 100% доки есть.

.global — директива линкеру, что это определение надо сделать видимым извне.

extern — директива линкеру, что символ надо искать где-то во внешних модулях.

Лучше почитайте про то, определена ли директива __ASSEMBLER__.
0
Точнее, __ASSEMBLER__ это не директива, а дефайн.
0
Это надо читать в документации.
0
Смысла вставлять его руками нет. Идея в том, что по этому дефайну компиляторы при парсинге узнают, что для кого, ибо при парсинге файла компилятором Си либо ассемблером он определяется автоматически в зависимости от того, какая тулза читает файл. Скорее всего, там есть какой-то аналогичный дефайн, поищите документацию.

Или просто сделайте два заголовочника, один чисто для асма, другой чисто для Си и продублируйте общие определения.
0
Сделать раздельные файлы пробовали? Это скорее всего решит проблему…
0
Раздельные заголовочники с определениями для асма и С. Сейчас определения с разным синтаксисои разделяются проверкой макроса __ASSEMBLER__. За счет этого ассемблер не видит кода на Си, и не ругается на него. А в шестой студии, видимо, что-то с этим сломано. Так что можно просто разделить файлы. В один сложить определения для асма и включить в .s, в другой — определения для C с extern, и включать по необходимости.

Я гляжу, Вы плохо представляете, как это все работает вместе. Почитайте про базовые принципы сборки программы GNU Toolchain'ом — что в какой последовательности компилируется и линкуется.
0
Не за что. Если чего, комментируйте в моем блоге в соотв. статье, а то тут уже слишком затянулась ветка. Там регистрации не надо.
0
Как вариант — поищи как объявить дефайн для ассемблера и объяви __ASSEMBLER__ вручную в настройках ассемблера в студии.
0
Отличная статья, сразу в закладки :)
0
Прекрасная статья. По ней я вкуривал в ассемблеровские вставки gcc х86 ;). Я вам рекомендую аналогичную статью для х86 написать, ей не будет цены. Что-то на просторах рунета сходу не нагуглилось.
0
Ну программирование под x86, а тем более на асме, мне сейчас как-то не по пути, так что не в ближайшее время точно. Может лучше Вы напишите, раз уже всё равно вкурили?
0
Пытаюсь разобраться с этими чудесами со вставками, но что-то пока глухо. Толи лижи не едут, ну дальше знаете…
компилятор на заявления типа

asm volatile(«sbi %0, 0x07»: «I» ((unsigned short)(PORTB)):);

ругаецо, говорит:

error: lvalue required in asm statement
error: output operand constraint lacks '='
error: output operand constraint lacks '='

затем покурив кукбук, увидел там и ссылаясь на
asm (code: output_operand_list: input_operand_list: clobber_list);

исправил на

asm volatile(«sbi %0, 0x07» :: «I» ((unsigned short)(PORTB)));

у нас же только инпут. на что всё равно компилятор говорит

warning: asm operand 0 probably doesn't match constraints
error: impossible constraint in 'asm'

подскажите, господа, где засада.
0
Мда, действительно косяк… Никогда не было необходимости передавать константы в ассемблерные вставки, так что не заметил тут фигню, тупо скопировал из мануала. С первой ошибкой всё ясно — простая очепятка, а во втором случае засада сложнее. Курение исходников показало, что ошибка вылазит из-за хитрого объявления портов: PORTB — это же не константа с адресом, а значение по адресу. Что бы получить именно значение адреса надо воспользоваться макросом _SFR_IO_ADDR:
asm volatile(«sbi %0, 0x07» :: «I» (_SFR_IO_ADDR(PORTB)));
PS: прощу прощения, что так долго — последнее время сильно занят, а ответ на донный вопрос требовал некоторое время на разбирательства. Чуть попозже обновлю статью.
0
Вот так заработало, спасибо за пинок в сторону _SFR_IO_ADDR.
uint8_t pin = 0x07;
uint8_t port = _SFR_IO_ADDR(PORTB);

asm volatile("sbi %0, %1" :: "I" (port), "I" (pin));
Задача в принципе такая: есть массив с адресами портов и пинами в них, за которые надо дёргать, и в цикле вся карусель шевелится. Пробовал по началу хранить указатели на порт, а потом выковыривать из массива указатель и дёргать пины, но всё это должно происходить в прерывании и когда я взглянул на асм код, что компилятор нагородил, решил почесать тыковку ещё немного.
Теперь есть решение, благодарю.
0
… прошло 20 мин…
Как дошло дело до массива, работать отказалось, видать ему нужны именно константы. Может есть ещё идеи?
0
Конечно, sbi то на вход константы требует =) Тебе следует покурить маны и статьи по программированию на асме и, в частности AVR Instruction Set. Боюсь, что одной командой тут не обойдёшься.
0
Да, я тоже боюсь что не обойдётся просто так, короче остаюсь на указателях пока. По крайней мере работает, потом долизывать буду.
0
Ну по сути тебе нужно посмотреть код, который генерит компилятор и написать тоже самое, но выкинув всё лишнее. =)
0
Да, я как раз подумал о том, чтобы украсть у компилятора идею :)
С асмом я не очень, как собака — всё понимаю, а сказать сложно…
0
А ещё такой вопрос, может немного не в тему, у меня прерывание по совпадению на Timer1, при старте, разрешаю прерывание по совпадению и sei, когда срабатывает прерывание, поднимается соответствующий флаг и убегаем на вектор, I сбрасывается, и при выходе из прерывания, обратно не возвращается, получается что прерывание вырубает флаг I в SREG. Это глюк или может чё ещё покрутить? Думал надо сохранять SREG и потом его восстанавливать, но I падает ещё до входа в прерывание, и при выходе не становится на место.
0
Курить тут. Это что касается асма, при писании на сях курить ман по avr-libc
0
А это ничего что переменные(регистры) в программах не описаны как и для чтения тоже, у меня вот такая вставка
asm volatile(«and %0, %1»:"=r"(buf2):«r»(buf4)); и такая asm volatile(«and %0, %1»:"=r"(buf2),"=r"(buf4):«0»(buf2),«1»(buf4)); собираются по разному, хотя в большом блоке как и у Вас, я тоже поставил их только "=r", и еще один момент, почему-то порты невозможно поставить куда-либо кроме последней секции где описываются входные переменные, но ведь они бывают не только на чтение, Вы не могли бы объяснит в чем тут дело, работать-то работает но непонятно)
0
  • avatar
  • basil
  • 18 августа 2011, 19:36
Порты можно читать и писать так:
asm("in %[retval], %[port]" :
    [retval] "=r" (value) :
    [port] "I" (_SFR_IO_ADDR(PORTD)) );

Это пример из avr-libc:
http://www.nongnu.org/avr-libc/user-manual/inline_asm.html
0
Спасибо это понятно, непонятно почему «I» (_SFR_IO_ADDR(PORTD)) нельзя запихать в середину, вернее мочь то можно но не соберется, непонятно чего ставить в поле ограничителя-модификатора чтобы порт был в поле для output.
0
Когда возникают ошибки в ассемблерных вставках, получается такая проблема: у тебя в сишном коде вроде всё правильно, а ассемблер ругается на то что он видит косяки. Но ты этих косяков не видишь, так как они во временном ассемблерном файле, и сразу удаляются. Чтобы они не удалялись, и их можно было посмотреть, есть опция компилятора -save-temps.
0
Хочу поделиться своими макросами на ассемблере (может кому и пригодятся). Я их оформил в отдельный хэдер и прикалываю к своим проектам.
//макрос установки разряда порта
//пример использования: sbi_(PORTC,2)
#define sbi_(_port, _bit) \
asm volatile ( \
«sbi %0, %1» \
: /* no outputs */ \
: «I» (_SFR_IO_ADDR(_port)), \
«I» (_bit) \
);

//макрос сброса разряда порта
//пример использования: cbi_(PORTC,2)
#define cbi_(_port, _bit) \
asm volatile ( \
«cbi %0, %1» \
: /* no outputs */ \
: «I» (_SFR_IO_ADDR(_port)), \
«I» (_bit) \
);
//макрос ожидания сброса бита РВВ
#define wait_clear_bit(_port, _bit) \
asm volatile ( \
«1:» «sbic %0, %1» "\n\t" \
«rjmp 1b» \
: /* no outputs */ \
: «I» (_SFR_IO_ADDR(_port)), \
«I» (_bit) \
);

//макрос ожидания установки бита РВВ
#define wait_set_bit(_port, _bit) \
asm volatile ( \
«1:» «sbis %0, %1» "\n\t" \
«rjmp 1b» \
: /* no outputs */ \
: «I» (_SFR_IO_ADDR(_port)), \
«I» (_bit) \
);
//макрос установки бита в РВВ
//пример использования: set_bit(TIMSK,TOIE2)
#define set_bit(_port, _bit) \
{ uint8_t tm; \
asm volatile ( \
«in %[a], %[b]» "\n\t" \
«ori %[a], (1<<%[c])» "\n\t" \
«out %[b], %[a]» "\n\t" \
: /* no outputs */ \
: [a] «a» (tm), \
[b] «I» (_SFR_IO_ADDR(_port)), \
[c] «I» (_bit)); \
}
//макрос сброса бита в РВВ
//пример использования: clr_bit(TIMSK,TOIE2)
#define clr_bit(_port, _bit) \
{ uint8_t tm; \
asm volatile ( \
«in %[a], %[b]» "\n\t" \
«andi %[a], (~(1<<%[c]))» "\n\t" \
«out %[b], %[a]» "\n\t" \
: /* no outputs */ \
: [a] «a» (tm), \
[b] «I» (_SFR_IO_ADDR(_port)), \
[c] «I» (_bit)); \
}
Данные макросы я использую по двум причинам:
1. Мне не нравится написание TIMSK &= ~(_BV(TOIE2)). По-моему clr_bit(TIMSK,TOIE2) выглядит нагляднее
2. Если не использовать оптимизацию, то после компиляции команд подобных TIMSK &= ~(_BV(TOIE2)) ассемблер весит ощутимо много, т.к. подобных команд в программе предостаточно.
0
Только не будем забывать о наличии deprecated.h, подключив который
#include <compat/deprecated.h>
можно получить несколько часто используемых команд в кратком виде. Так ли уж часто кто-то не использует оптимизацию по умолчанию?

Некоторые вещи могут неудобно выглядеть, зато быть переносимыми на другие компиляторы.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.