Ассемблерные вставки в С коде GCC для AVR и Arduino

Ассемблерные вставки в С коде GCC для AVR и Arduino

Компилятор GCC — встроенный Assembler

Примечание переводчика MBS Electronics: Поскольку стандартная оболочка Arduino сипользует для компиляции скетчей компилятор GCC, то материал данной статьи можно использовать и для работы с Arduino. Я перевел эту статью с английского. Ссылка на оригинал будет в конце текста. Это довольно старая статья, написанная на английском языке автором, проживающим в Германии. Но тем не менее этот тект может пролить свет на некоторые неочевидные моменты, связанные с данной темой.

Бесплатный компилятор GCC (GNU C) для процессоров Atmel AVR позволяет встраивать код на языке ассемблера в программы на C. Эту замечательную функцию можно использовать для ручной оптимизации критичных по времени частей программного обеспечения или для использования специальных инструкций процессора, которые недоступны в языке C.

Эта статья призвана прояснить некоторые моменты встраивания ассемблерных вставок, так как эта нема недостаточно освещена в сети.

Предполагается, что вы знакомы с написанием программ на ассемблере для AVR, так как данная статья не является учебником по программированию на ассемблере для AVR.

Эта статья написана для компилятора GCC версии 3.3. Возможно, есть какие-то моменты, которые не были до конца поняты самим автором, к тому же не все примерыдо сих пор были опробованы. Поскольку автор немец и не знаком с английским языком, в тексте определенно есть опечатки и синтаксические ошибки. Как программист автор знает, что неправильная документация иногда может быть хуже, чем ее отсутствие. В любом случае, автор решил поделиться своими небольшими знаниями с общественностью в надежде получить достаточно откликов, чтобы улучшить этот документ. Не стесняйтесь обращаться к автору по электронной почте.

Ассемблер для компилятора GCC

Начнем с простого примера чтения значения из порта D:

asm("in %0, %1" : "=r" (value) : "I" (_SFR_IO_ADDR(PORTD)) );

Каждый оператор asm разделяется на несколько секций с помошью символа двоеточия. Может быть до четырех таких секций:

  1. Инструкции ассемблера, определенные как одна строковая константа:
    «in %0, %1»
  2. Список выходных операндов, разделенных запятыми. В нашем примере используется только один:
    «=r» (value)
  3. Разделенный запятыми список входных операндов. Опять же, в нашем примере используется только один операнд:
    «I» (_SFR_IO_ADDR(PORTD))
  4. «Затертые» регистры (клобберы), в нашем примере оставленные пустыми.

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

В общем виде это выглядит так:

asm(code : output operand list : input operand list [: clobber list]);

В разделе кода операнды обозначаются знаком процента, за которым следует одна цифра. %0 относится к первому операнду, %1 ко второму операнду и так далее. Из приведенного выше примера:

asm("in %0, %1" : "=r" (value) : "I" (_SFR_IO_ADDR(PORTD)) );

<strong>%0</strong> относится к <strong>"=r"</strong> (value) 
и
%<strong>1</strong> относится к <strong>"I" (_SFR_IO_ADDR(PORTD))</strong>

Сейчас это все еще может выглядеть немного странно, но синтаксис списка операндов будет объяснен позже. Давайте сначала рассмотрим часть листинга компилятора, которая могла быть сгенерирована из нашего примера:

lds r24,value
/* #APP */
in r24, 12
/* #NOAPP */
sts value,r24

Комментарии были добавлены компилятором, чтобы сообщить ассемблеру, что включенный код был сгенерирован не компиляцией операторов C, а встроенными операторами ассемблера. Компилятор выбрал регистр r24 для хранения значения, прочитанного из PORTD. Однако компилятор мог выбрать любой другой регистр. Он может не загружать или сохранять значение явно и может даже решить вообще не включать ваш ассемблерный код. Все эти решения являются частью стратегии оптимизации компилятора. Например, если вы никогда не используете значение переменной в оставшейся части программы на C, компилятор, скорее всего, удалит ваш код, если вы не отключите оптимизацию. Чтобы избежать этого, вы можете добавить атрибут volatile в оператор asm:

asm volatile("in %0, %1" : "=r" (value) : "I" (_SFR_IO_ADDR(PORTD)));

Кроме того, операндам можно давать имена. Имя в квадратных скобках добавляется к ограничениям в списке операндов, а ссылки на именованный операнд используют имя в квадратных скобках вместо числа после знака %. Таким образом, приведенный выше пример также может быть записан как

asm("in %[retval], %[port]" :
[retval] "=r" (value) :
[port] "I" (_SFR_IO_ADDR(PORTD)) );

Последняя часть ассемблерной инструкции — список «затирания». В основном он используется, чтобы сообщить компилятору об изменениях, сделанных ассемблерным кодом. Эта часть может быть опущена, все остальные части обязательны, но могут быть оставлены пустыми.

Если ваша подпрограмма на ассемблере не будет использовать никаких входных или выходных операндов, два двоеточия все равно должны следовать за строкой кода на ассемблере. Хорошим примером является простой оператор для отключения прерываний:

asm volatile("cli"::);

Ассемблерный код

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

Чтобы сделать код (сгенерированный компилятором — прим. переводчика) более читабельным, вы должны поместить каждое утверждение в отдельную строку:

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

Символы перевода строки и табуляции (\n\t) сделают листинг на ассемблере, сгенерированный компилятором, более читабельным. В первый раз это может показаться немного странным, но именно так компилятор создает свой собственный ассемблерный код.

Вы также можете использовать некоторые специальные регистры:

СИМВОЛРЕГИСТР
__SREG__Регистр Статуса (address 0x3F)
__SP_H__Старший байт указателя стека (address 0x3E)
__SP_L__Младший байт указателя стека (address 0x3D)
__tmp_reg__регистр r0, используемый как временное хранилище
_zero_reg__Регистр r1, всегда содержит ноль

Вы можете свободно использовать регистр r0 в вашем коде на ассемблере и не заботиться о восстановлении его содержимого в конце вашего кода. Рекомендуется использовать tmp_reg и zero_reg вместо r0 или r1 на тот случай, если новая версия компилятора изменит определения использования регистров.

Входные и выходные операнды

Каждый входной и выходной операнд описывается строкой ограничения, за которой следует выражение C в скобках. AVR-GCC 3.3 поддерживает следующие символы ограничения:

Примечание:
Наиболее актуальную и подробную информацию об ограничениях для avr можно найти в руководстве по GCC.


регистры x, y и z представлябют собой специальные 16-разрядные адресные регистры, состоящие из пар стандартных регистров следующим обрахом:

Регистр x это регистровая пара r27:r26
регистр yr29:r28
регистр zr31:r30

таблица ограничителей:

ОграничительИспользуется дляДиапазон
aПростые верхние регистрыr16 … r23
bосновные регистровые пары — указателиy, z
dВерхние регистрыr16 … r31
eВсе регистровые пары — указателиx, y, z
qУказатель стекаSPH:SPL
rЛюбой регистрr0 … r31
tВременный регистрr0
wСпециальные пары верхних регистровr24, r26, r28, r30
xрегистровая пара Xx (r27:r26)
yрегистровая пара Yy (r29:r28)
zрегистровая пара Zz (r31:r30)
GКонстанта с плавающей точкой0.0
I6-битная положительная целая константа0 … 63
J6-битная отрицательная целая константа-63 … 0
KЦелая константа2
LЦелая константа0
lНижние регистрыr0 … r15
M8-битная целая константа0 … 255
NЦелая константа-1
OЦелая константа8, 16, 24
PЦелая константа1
QАдрес памяти на основе указателя Y или Z со смещением
(GCC >= 4.2.x)
RЦелая константа
(GCC >= 4.3.x)
-6 … 5

Выбор правильного ограничения зависит от диапазона констант или регистров, которые должны быть приемлемы для инструкции AVR, с которой они используются. Компилятор C не проверяет ни одну строку вашего ассемблерного кода. Но он может проверить ограничение на ваше выражение C. Однако, если вы укажете неправильные ограничения, компилятор может молча передать ассемблеру неправильный код. И, конечно же, ассемблер «выйдет из строя»сломается» с какими-то загадочнымм выходными данными или с внутренними ошибками. Например, если вы указываете ограничение «r» и используете этот регистр с инструкцией «ori» в своем ассемблерекоде на , тогда компилятор может выбрать любой регистр. Это не удастся, если компилятор выберет от r2 до r15. (Он никогда не выберет r0 или r1, потому что они используются для специальных целей.) Вот почему правильным ограничением в этом случае является «d». С другой стороны, если вы используете ограничение «M», компилятор позаботится о том, чтобы вы не передавали ничего, кроме 8-битного значения. Позже мы увидим, как передать результат многобайтового выражения в ассемблерный код.

В следующей таблице показаны все мнемоники ассемблера AVR, для которых требуются операнды, и соответствующие ограничения. Из-за неправильных определений ограничений в версии 3.3 они недостаточно строгие. Например, нет ограничения, ограничивающего целочисленные константы диапазоном от 0 до 7 для операций установки и очистки битов.

Мнемоника asmОграничительМнемоника asmОграничитель
adcr,raddr,r
adiww,Iandr,r
andid,Masrr
bclrIbldr,I
brbcI, labelbrbsI,label
bsetIbstr,I
cbiI, Icbrd,I
comrcpr,r
cpcr,rcpid,M
cpser,rdecr
elpmt,zeorr,r
inr,Iincr
ldr,elddr,b
ldid,Mldsr,label
lpmt,zlslr
lsrrmovr,r
movwr,rmulr,r
negrorr,r
orid,MoutI,r
poprpushr
rolrrorr
sbcr,rsbcid,M
sbiI,IsbicI,I
sbiww,Isbrd,M
sbrcr,Isbrsr,I
serdste,r
stdb,rstslabel,r
subr,rsubid,M
swapr

Перед символами ограничения может стоять один модификатор ограничения. Ограничения без модификатора определяют операнды только для чтения. Модификаторы:

= Операнд только для записи, обычно используемый для всех выходных операндов
+ Чтение-запись операнда
& Регистр следует использовать только для вывода

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

Входные операнды, как вы уже догадались, доступны только для чтения. Но что делать, если вам нужен один и тот же операнд для ввода и вывода? Как указано выше, операнды чтения-записи не поддерживаются во встроенном коде ассемблера. Но есть и другое решение. Для операторов ввода можно использовать одну цифру в строке ограничения. Использование цифры n указывает компилятору использовать тот же регистр, что и для n-го операнда, начиная с нуля. Вот пример:

asm volatile("swap %0" : "=r" (value) : "0" (value));

Этот оператор поменяет местами полубайты в 8-битной переменной с именем value. Ограничение «0» указывает компилятору использовать тот же входной регистр, что и для первого операнда. Однако обратите внимание, что это не означает автоматически обратного случая. Компилятор может выбрать одни и те же регистры для ввода и вывода, даже если ему не было сказано сделать это. В большинстве случаев это не проблема, но может быть фатальным, если оператор вывода модифицируется ассемблерным кодом до использования оператора ввода. В ситуации, когда ваш код зависит от разных регистров, используемых для входных и выходных операндов, вы должны добавить модификатор ограничения & к вашему выходному операнду. Следующий пример демонстрирует эту проблему:

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

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

asm volatile("mov __tmp_reg__, %A0" "\n\t"
"mov %A0, %B0" "\n\t"
"mov %B0, __tmp_reg__" "\n\t"
: "=r" (value)
: "0" (value)
);

Вы заметите, что мы использовали регистр tmp_reg, который мы перечислили среди других специальных регистров в разделе Ассемблерный код. Вы можете использовать этот регистр без сохранения его содержимого. Совершенно новыми являются буквы A и B в %A0 и %B0. На самом деле они ссылаются на два разных 8-битных регистра, каждый из которых содержит часть значения.

Другой пример замены байтов 32-битного значения:

asm volatile("mov __tmp_reg__, %A0" "\n\t"
"mov %A0, %D0" "\n\t"
"mov %D0, __tmp_reg__" "\n\t"
"mov __tmp_reg__, %B0" "\n\t"
"mov %B0, %C0" "\n\t"
"mov %C0, __tmp_reg__" "\n\t"
: "=r" (value)
: "0" (value)
);

Вместо того, чтобы указывать один и тот же операнд в качестве операнда ввода и вывода, его также можно объявить как операнд чтения-записи. Это должно быть применено к выходному операнду, и соответствующий список входных операндов остается пустым:

asm volatile("mov __tmp_reg__, %A0" "\n\t"
"mov %A0, %D0" "\n\t"
"mov %D0, __tmp_reg__" "\n\t"
"mov __tmp_reg__, %B0" "\n\t"
"mov %B0, %C0" "\n\t"
"mov %C0, __tmp_reg__" "\n\t"
: "+r" (value));

Если операнды не помещаются в один регистр, компилятор автоматически выделяет достаточное количество регистров для хранения всего операнда. В коде на ассемблере вы используете %A0 для ссылки на младший байт первого операнда, %A1 на младший байт второго операнда и так далее. Следующий байт первого операнда будет %B0, следующий байт %C0 и так далее.

Это также означает, что часто необходимо привести тип входного операнда к желаемому размеру.

Последняя проблема может возникнуть при использовании пар регистров указателей. Если вы определяете входной операнд «e» (ptr)

и компилятор выбирает регистр Z (r30:r31), затем
%A0 относится к r30 и
%B0 относится к r31

Но обе версии потерпят неудачу на этапе сборки компилятора, если вам явно нужен Z, как в ld r24,Z

Если вы напишите:
ld r24, %a0
со строчной буквой «a» после знака процента, то компилятор создаст правильную строку ассемблера.

Затирающие элементы — клобберы.

Как указывалось ранее, последняя часть оператора asm — это список клобберов. Эта часть может быть опущена, включая двоеточие-разделитель.

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

asm volatile("mov __tmp_reg__, %A0" "\n\t"
"mov %A0, %D0" "\n\t"
"mov %D0, __tmp_reg__" "\n\t"
"mov __tmp_reg__, %B0" "\n\t"
"mov %B0, %C0" "\n\t"
"mov %C0, __tmp_reg__" "\n\t"
: "+r" (value));

Компилятор может выдать следующий код:

cli
ld r24, Z
inc r24
st Z, r24
sei

Одним из простых способов избежать затирания регистра r24 является использование специального временного регистра tmp_reg, определенного компилятором.

asm volatile(
"cli" "\n\t"
"ld __tmp_reg__, %a0" "\n\t"
"inc __tmp_reg__" "\n\t"
"st %a0, __tmp_reg__" "\n\t"
"sei" "\n\t"
:
: "e" (ptr)
);

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

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

{
uint8_t s;
asm volatile(
"in %0, __SREG__" "\n\t"
"cli" "\n\t"
"ld __tmp_reg__, %a1" "\n\t"
"inc __tmp_reg__" "\n\t"
"st %a1, __tmp_reg__" "\n\t"
"out __SREG__, %0" "\n\t"
: "=&r" (s)
: "e" (ptr)
);
}

Теперь все кажется правильным, но на самом деле это не так.

Ассемблерный код модифицирует переменную, на которую указывает ptr. Компилятор не распознает это и может сохранить его значение в любом из других регистров. С неправильным значением работает не только компилятор, но и ассемблерный код. Программа на C также могла изменить это значение, но компилятор не обновлял расположение в памяти из соображений оптимизации. Худшее, что вы можете сделать в этом случае, это:

{
uint8_t s;
asm volatile(
"in %0, __SREG__" "\n\t"
"cli" "\n\t"
"ld __tmp_reg__, %a1" "\n\t"
"inc __tmp_reg__" "\n\t"
"st %a1, __tmp_reg__" "\n\t"
"out __SREG__, %0" "\n\t"
: "=&r" (s)
: "e" (ptr)
: "memory"
);
}

Специальный клоббер «memory» информирует компилятор о том, что ассемблерный код может модифицировать любое место в памяти. Это заставляет компилятор обновлять все переменные, содержимое которых в данный момент хранится в регистре, перед выполнением ассемблерного кода. И, конечно же, после этого кода все приходится перезагружать заново.

В большинстве ситуаций гораздо лучшим решением было бы объявить само место назначения указателя volatile:

«memory» информирует компилятор о том, что ассемблерный код может модифицировать любое место в памяти. Это заставляет компилятор обновлять все переменные, содержимое которых в данный момент хранится в регистре, перед выполнением ассемблерного кода. И, конечно же, после этого кода все приходится перезагружать заново.

В большинстве ситуаций гораздо лучшим решением было бы объявить само место назначения указателя volatile:

volatile uint8_t *ptr;

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

Ситуации, в которых вам нужны клобберы, очень редки. В большинстве случаев будут лучшие способы. Забитые регистры заставят компилятор сохранять их значения до и перезагружать их после кода ассемблера. Избегание clobbers дает компилятору больше свободы при оптимизации вашего кода.


Ассемблерные макросы

Чтобы повторно использовать части вашего кода ассемблера, полезно определить их как макросы и поместить во включаемые файлы.

AVR Libc поставляется с набором макросов, которые можно найти в каталоге avr/include. Использование таких включаемых файлов может вызвать предупреждения компилятора, если они используются в модулях, скомпилированных в строгом режиме ANSI. Чтобы избежать этого, вы можете написать asm вместо asm и volatile вместо volatile. Это эквивалентные псевдонимы.

Другая проблема с повторно используемыми макросами возникает, если вы используете метки. В таких случаях вы можете использовать специальный шаблон %=, который заменяется уникальным номером в каждом операторе asm. Следующий код был взят из avr/include/iomacros.h:

#define loop_until_bit_is_clear(port,bit)  \
        __asm__ __volatile__ (             \
        "L_%=: " "sbic %0, %1" "\n\t"      \
                 "rjmp L_%="               \
                 : /* no outputs */        
                 : "I" (_SFR_IO_ADDR(port)),  
                   "I" (bit)    
        )

При первом использовании L_%= может быть преобразовано в L_1404, при следующем использовании может быть создано L_1405 или что-то еще. В любом случае, метки тоже стали уникальными.

Другой вариант — использовать числовые метки в стиле ассемблера Unix. Тогда приведенный выше пример будет выглядеть так:

#define loop_until_bit_is_clear(port,bit)  
        __asm__ __volatile__ (             
        "1: " "sbic %0, %1" "\n\t"      
                 "rjmp 1b"               
                 : /* no outputs */        
                 : "I" (_SFR_IO_ADDR(port)),  
                   "I" (bit)    
        )

Функции-заглушки C

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

void delay(uint8_t ms)
{
uint16_t cnt;
asm volatile (
"\n"
"L_dl1%=:" "\n\t"
"mov %A0, %A2" "\n\t"
"mov %B0, %B2" "\n"
"L_dl2%=:" "\n\t"
"sbiw %A0, 1" "\n\t"
"brne L_dl2%=" "\n\t"
"dec %1" "\n\t"
"brne L_dl1%=" "\n\t"
: "=&w" (cnt)
: "r" (ms), "r" (delay_count)
);
}

Цель этой функции — задержать выполнение программы на заданное количество миллисекунд с помощью цикла подсчета. Глобальная 16-битная переменная delay_count должна содержать тактовую частоту ЦП в герцах, деленную на 4000, и должна быть установлена до первого вызова этой процедуры. Как описано в разделе о клобберах, подпрограмма использует локальную переменную для хранения временного значения.

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

uint16_t inw(uint8_t port)
{
uint16_t result;
asm volatile (
"in %A0,%1" "\n\t"
"in %B0,(%1) + 1"
: "=r" (result)
: "I" (_SFR_IO_ADDR(port))
);
return result;
}

Имена C, используемые в ассемблерном коде

По умолчанию AVR-GCC использует одни и те же символические имена функций или переменных в коде C и ассемблере. Вы можете указать другое имя для ассемблерного кода, используя специальную форму оператора asm:

unsigned long value asm("clock") = 3686400;

Этот оператор указывает компилятору использовать имя символа clock, а не значение. Это имеет смысл только для внешних или статических переменных, потому что локальные переменные не имеют символических имен в коде ассемблера. Однако локальные переменные могут храниться в регистрах.

С помощью AVR-GCC вы можете указать использование определенного регистра:

void Count(void)
{
register unsigned char counter asm("r3");
... some code...
asm volatile("clr r3");
... more code...
}

Ассемблерная инструкция «clr r3» очистит счетчик переменной. AVR-GCC не будет полностью резервировать указанный регистр. Если оптимизатор распознает, что на переменную больше не будет ссылок, регистр можно будет использовать повторно. Но компилятор не может проверить, конфликтует ли использование этого регистра с каким-либо предопределенным регистром. Если вы зарезервируете слишком много регистров таким образом, компилятор может даже исчерпать регистры во время генерации кода.

Чтобы изменить имя функции, вам нужно объявление прототипа, потому что компилятор не примет ключевое слово asm в определении функции:

extern long Calc(void) asm ("CALCULATE");

Читать также статью по встроенному ассемблеру Arduino…

оригинал статьи на английском…


Полезные ссылки:

Arduino:

//www.atmel.com/Images/doc8161.pdfages/doc8161.pdf
http://arduino.cc/en/uploads/Main/Arduino_Uno_Rev3-schematic.pdf [ARDUINO SCHEMATIC]

http://arduino.cc/hu/Hacking/PinMapping
http://arduino.cc/en/Reference/PortManipulation

Inline Assembler:

http://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-As [GCC EXTENDED-ASM]

http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

http://www.nongnu.org/avr-libc/user-manual/inline_asm.html

http://savannah.nongnu.org/download/avr-libc/avr-libc-user-manual-1.8.0.pdf.bz2

http://www.mikrocontroller.net/articles/AVR_Assembler_Makros [ASSEMBLER MACROS]

http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial

http://www.lowlevel.eu/wiki/Inline-Assembler_mit_GCC

http://www.rn-wissen.de/index.php/Inline-Assembler_in_avr-gcc

http://www.k2.t.u-tokyo.ac.jp/members/alvaro/courseMaterials/MicroProgramming

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *