Привет всем!
По роду деятельности я программист на Java. Последние месяцы работы заставили меня познакомиться с разработкой под Android NDK и соответственно написание нативных приложений на С. Тут я столкнулся с проблемой оптимизации Linux библиотек. Многие оказались абсолютно не оптимизированы под ARM и сильно нагружали процессор. Ранее я практически не программировал на ассемблере, поэтому сначала было сложно начать изучать этот язык, но все же я решил попробовать. Эта статья написана, так сказать, от новичка для новичков. Я постараюсь описать те основы, которые уже изучил, надеюсь кого-то это заинтересует. Кроме того, буду рад конструктивной критике со стороны профессионалов.
Архитектура ARM (Advanced RISC Machine, Acorn RISC Machine, усовершенствованная RISC-машина) - семейство лицензируемых 32-битных и 64-битных микропроцессорных ядер разработки компании ARM Limited. Компания занимается исключительно разработкой ядер и инструментов для них (компиляторы, средства отладки и т. п.), зарабатывая на лицензировании архитектуры сторонним производителям.
Если кто не знает, сейчас большая часть мобильных устройств, планшетов разработаны именно на этой архитектуре процессоров. Основным преимуществом данного семейства является низкое энергопотребление, благодаря чему он часто используется в различных встроенных системах. Архитектура развивалась с течением времени, и начиная с ARMv7 были определены 3 профиля: ‘A’(application) - приложения, ‘R’(real time) - в реальном времени,’M’(microcontroller) - микроконтроллер. Историю разработки этой технологии и другие интересный данные вы можете прочитать в Википедии или погуглив в интернете. ARM поддерживает разные режимы работы (Thumb и ARM, кроме того в последние время появился Thumb-2, являющийся смесью ARM и Thumb). В данной статье рассмотрим собственно режим ARM, в котором исполняется 32-битный набор команд.
Каждый ARM процессор создан из следующих блоков:
1. Этап выборки (F)
На этом этапе инструкции поступают из ОЗУ в конвейер процессора.
2. Этап декодирования (D)
Инструкции декодируются и распознаётся их тип.
3. Этап исполнения (E)
Данные поступают в ALU и исполняются и полученное значение записывается в заданный регистр.
Но при разработке надо учитывать, что, есть инструкции, которые используют несколько циклов исполнения, например, load(LDR) или store. В таком случае этап исполнения (E) разделяется на этапы (E1, E2, E3...).
3.v1-v8 (переменные регистры, с r4 по r11)
4.sb and SB (статический регистр, r9)
5.sl and SL (r10)
6.fp and FP (r11)
7.ip and IP (r12)
8.sp and SP (r13)
9.lr and LR (r14)
10.pc and PC (программный счетчик, r15).
Чтобы закрепить использование основных инструкций давайте напишем несколько простых примеров, но сначала нам понадобится arm toolchain. Я работаю в Linux поэтому выбрал: frank.harvard.edu/~coldwell/toolchain (arm-unknown-linux-gnu toolchain). Ставится он проще простого, как и любая другая программа на Linux. В моем случае (Russian Fedora) понадобилось только установить rpm пакеты с сайта.
Теперь пришло время написать простейший пример. Программа будет абсолютно бесполезной, но главное, что будет работать:) Вот код, который я вам предлагаю:
start: @ Необязательная строка, обозначающая начало программы
mov r0, #3 @ Грузим в регистр r0 значение 3
mov r1, #2 @ Делаем тоже самое с регистром r1, только теперь с значением 2
add r2, r1, r0 @ Складываем значения r0 и r1, ответ записываем в r2
mul r3, r1, r0 @ Умножаем значение регистра r1 на значение регистра r0, ответ записываем в r3
stop: b stop @ Строка завершения программы
Компилируем программу до получения.bin файла:
/usr/arm/bin/arm-unknown-linux-gnu-as -o arm.o arm.s
/usr/arm/bin/arm-unknown-linux-gnu-ld -Ttext=0x0 -o arm.elf arm.o
/usr/arm/bin/arm-unknown-linux-gnu-objcopy -O binary arm.elf arm.bin
(код в файле arm.s, а toolchain в моем случае лежит в директории /usr/arm/bin/)
Если все прошло успешно, у вас будет 3 файла: arm.s (собственно код), arm.o, arm.elf, arm.bin (собственно исполняемая программа). Для того, чтобы проверить работу программы не обязательно иметь собственное arm устройство. Достаточно установить QEMU. Для справки:
QEMU - свободная программа с открытым исходным кодом для эмуляции аппаратного обеспечения различных платформ.Включает в себя эмуляцию процессоров Intel x86 и устройств ввода-вывода. Может эмулировать 80386, 80486, Pentium, Pentium Pro, AMD64 и другие x86-совместимые процессоры; PowerPC, ARM, MIPS, SPARC, SPARC64, m68k - лишь частично.
Работает на Syllable, FreeBSD, FreeDOS, Linux, Windows 9x, Windows 2000, Mac OS X, QNX, Android и др.
Итак, для эмуляции arm понадобится qemu-system-arm. Этот пакет есть в yum, так что тем, у кого Fedora, можно не заморачиваться и просто выполнить комманду:
yum install qemu-system-arm
Далее надо запустить эмулятор ARM, так, чтобы он выполнил нашу программу arm.bin. Для этого создадим файл flash.bin, который будет флэш памятью для QEMU. Сделать это очень просто:
dd if=/dev/zero of=flash.bin bs=4096 count=4096
dd if=arm.bin of=flash.bin bs=4096 conv=notrunc
Теперь грузим QEMU с полученой flash памятью:
qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
На выходе вы получите что-то вроде этого:
$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
QEMU 0.15.1 monitor - type "help" for more information
(qemu)
Наша программа arm.bin должна была изменить значения четырех регистров, следовательно для проверки правильности работы давайте посмотрим на эти самые регистры. Делается это очень простой коммандой: info registers
На выходе вы увидите все 15 ARM регистров, при чем у четырех из них будут измененные значения. Проверьте:) Значения регистров совпадают с теми, которые можно ожидать после исполнения программы:
(qemu) info registers
R00=00000003 R01=00000002 R02=00000005 R03=00000006
R04=00000000 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=00000000 R14=00000000 R15=00000010
PSR=400001d3 -Z-- A svc32
P.S. В этой статье я постарался описать основы программирования на ARM ассемблер. Надеюсь вам понравилось! Этого хватит для того, чтобы далее углубляться в дебри этого языка и писать на нем программы. Если все получится, буду писать дальше о том, что узнаю сам. Если есть ошибки, прошу не пинать, так как я новичок в ассемблере.
Поначалу ARM довольно непривычный ассемблер (если переучиваться с x86 , MCS51 или AVR ). Но у него довольно простая и логичная организация, поэтому усваивается быстро.
Документации на русском языке по ассемблеру совсем мало. Могу посоветовать зайти на 2 ссылки (может, Вы найдете больше, и подскажете мне? Буду благодарен.):
Архитектура и система команд RISС-процессоров семейства ARM - http://www.gaw.ru/html.cgi/txt/doc/micros/arm/index.htm
Постигаем ассемблер ARM, из цикла статей «GBA ASM», автор Mike H, пер. Aquila - http://wasm.ru/series.php?sid=21 .
Последняя ссылка мне очень помогла, развеяла туман =). Второе, что хорошо может помочь - это, как ни странно, C-компилятор IAR Embedded Workbench for ARM (далее просто IAR EW ARM ). Дело в том, что он со стародавних времен умеет (как и все уважающие себя компиляторы, впрочем) компилировать C-код в код ассемблера, который, в свою очередь, так же легко компилируется ассемблером IAR в объектный код. Поэтому нет ничего лучше написать простейшую функцию на C, скомпилировать её в ассемблер, и сразу станет понятно, какая команда ассемблера что делает, как передаются аргументы и как возвращается результат. Убиваете сразу двух зайцев - обучаетесь ассемблеру и заодно получаете информацию, как интергрировать ассемблерный код в проект на C. Я тренировался на функции подсчета CRC16, и в результате получил её полноценную версию на ассемблере.
Вот исходная функция на C (u16 означает unsigned short, u32 - unsigned int, u8 - unsigned char):
// файл crc16.c
u16 CRC16 (void* databuf, u32 size)
{
u16 tmpWord, crc16, idx;
u8 bitCnt;
#define CRC_POLY 0x1021;
Crc16 = 0;
idx=0;
while (size!=0)
{
/* сложим по xor старший байт crc16 и входной байт */
tmpWord = (crc16>>8) ^ (*(((u8*)databuf)+idx));
/* результат запишем в старший байт crc16 */
tmpWord <<= 8;
crc16 = tmpWord + (0x00FF & crc16);
for (bitCnt=8;bitCnt!=0;bitCnt--)
{
/* проверим старший разряд аккумулятора CRC */
if (crc16 & 0x8000)
{
crc16 <<= 1;
crc16 ^= CRC_POLY;
}
else
crc16 <<= 1;
}
idx++;
size--;
}
return crc16;
}
Заставить генерировать код ассемблера IAR EW ARM очень легко. В опциях файла crc16.c (добавленного к проекту) поставил галку Override inherited settings , а потом на закладке List поставил 3 галки - Output assembler file , Include source и Include call frame information (хотя последнюю галку, наверное, можно не ставить - она генерит кучу ненужных CFI -директив). После компиляции получился файл папка_проекта \ ewp \ at91sam7x256_sram \ List \ crc16.s. Этот файл также легко можно добавить в проект, как и C-файл (он будет нормально компилироваться).
Конечно, когда я подсунул C-компилятору необрезанный вариант C-кода, то он мне выдал такой листинг ассемблера, что я ничего в нем не понял. Но когда выкинул из функции все C-операторы, кроме одного, стало понятнее. Потом шаг за шагом добавлял C-операторы, и вот в итоге что получилось:
; файл crc16.s
NAME crc16
PUBLIC CRC16
CRC_POLY EQU 0x1021
SECTION `.text`:CODE:NOROOT(2)
ARM
// u16 CRC16 (void* databuf, u32 size)
;R0 - результат возврата, CRC16
;R1 - параметр size
;R2 - параметр databuf (он был при входе в R0)
;R3, R12 - временные регистры
CRC16:
PUSH {R3,R12} ;методом тыка выяснил, что R3 и R13 сохранять
; необязательно. Но решил сохранить на всякий
; случай.
MOVS R2,R0 ;теперь R2==databuf
MOV R3,#+0
MOVS R0,R3 ;crc16 = 0
CRC16_LOOP:
CMP R1, #+0 ;все байты обработали (size==0)?
BEQ CRC16_RETURN ;если да, то выход
LSR R3, R0, #+8 ;R3 = crc16>>8
LDRB R12, ;R12 = *databuf
EOR R3, R3, R12 ;R3 = *databuf ^ HIGH (crc16)
LSL R3, R3, #+8 ;R3 <<= 8 (tmpWord <<= 8)
AND R0, R0, #+255 ;crc16 &= 0x00FF
ADD R0, R0, R3 ;crc16 = tmpWord + (0x00FF & crc16)
MOV R12, #+8 ;bitCnt = 8
CRC16_BIT_LOOP:
BEQ CRC16_NEXT_BYTE ;bitCnt == 0?
TST R0,#0x8000 ;Еще не все биты обработаны.
BEQ CRC16_BIT15ZERO ;Проверяем старший бит crc16.
LSL R0,R0,#+1 ;crc16 <<= 1
MOV R3, #+(LOW (CRC_POLY)) ;crc16 ^= CRC_POLY
ORR R3,R3,#+(HIGH(CRC_POLY) << 8) ;
EOR R0,R3,R0 ;
B CRC16_NEXT_BIT
CRC16_BIT15ZERO:
LSL R0,R0,#+1 ;crc16 <<= 1
CRC16_NEXT_BIT:
SUBS R12,R12,#+1 ;bitCnt--
B CRC16_BIT_LOOP ;
CRC16_NEXT_BYTE:
ADD R2,R2,#+1 ;databuf++
SUBS R1,R1,#+1 ;size--
B CRC16_LOOP ;цикл по всем байтам
CRC16_RETURN:
POP {R3,R12} ;восстанавливаем регистры
BX LR ;выход из подпрограммы, R0==crc16
Компилятор C от IAR делает на удивление хороший код. Мне совсем мало удалось его оптимизировать. Выкинул только лишний временный регистр, который хотел использовать компилятор (он почему-то взял в качестве лишнего временного регистра LR, хотя R3 и R12 было достаточно), а также убрал пару лишних команд, проверяющих счетчики и выставляющих флаги (просто добавив суффикс S к нужным командам).
В данном разделе приводится описание наборов инструкций процессора ARM7TDMI.
4.1 Краткое описание формата
В данном разделе представлено краткое описание наборов инструкций ARM и Thumb.
Ключ к таблицам наборов инструкций представлен в таблице 1.1.
Процессор ARM7TDMI выполнен на основе архитектуры ARMv4T. Более полное описание обоих наборов инструкций представлено в "ARM Architecture Reference Manual".
Таблица 1.1. Ключ к таблицам
Форматы набора инструкций ARM показаны на рисунке 1.5.
Более детальная информация относительно форматов набора инструкций ARM приведена в "ARM Architectural Reference Manual".
Рисунок 1.5. Форматы набора инструкций ARM
Некоторые коды инструкций не определены, но они не вызывают поиска неопределенных инструкций, например, инструкция умножения с битом 6 измененным к 1. Запрещается использовать такие инструкции, т.к. в будущем их действие может быть изменено. Результат выполнения данных кодов инструкций в составе процессора ARM7TDMI непредсказуем.
4.2 Краткое описание инструкций ARM
Набор инструкций ARM представлен в таблице 1.2.
Таблица 1.2. Краткое преставление инструкций ARM
Операции | Синтаксис Ассемблера | |
Пересылка | Пересылка | MOV {cond}{S} Rd, |
Пересылка NOT | MVN {cond}{S} Rd, |
|
Пересылка SPSR в регистр | MRS {cond} Rd, SPSR | |
Пересылка CPSR в регистр | MRS {cond} Rd, CPSR | |
Пересылка регистра SPSR | MSR {cond} SPSR{field}, Rm | |
Пересылка CPSR | MSR {cond} CPSR{field}, Rm | |
Пересылка константы во флаги SPSR | MSR {cond} SPSR_f, #32bit_Imm | |
Пересылка константы во флаги CPSR | MSR {cond} CPSR_f, #32bit_Imm | |
Арифметические | Сложение | ADD {cond}{S} Rd, Rn, |
Сложение с переносом | ADC {cond}{S} Rd, Rn, |
|
Вычитание | SUB {cond}{S} Rd, Rn, |
|
Вычитание с переносом | SBC {cond}{S} Rd, Rn, |
|
Вычитание обратного вычитания | RSB {cond}{S} Rd, Rn, |
|
Вычитание обратного вычитания с переносом | RSC {cond}{S} Rd, Rn, |
|
Умножение | MUL {cond}{S} Rd, Rm, Rs | |
Умножение-накопление | MLA {cond}{S} Rd, Rm, Rs, Rn | |
Умножение длинных беззнаковых чисел | UMULL | |
Умножение - беззнаковое накопление длинных значений | UMLAL {cond}{S} RdLo, RdHi, Rm, Rs | |
Умножение знаковых длинных | SMULL {cond}{S} RdLo, RdHi, Rm, Rs | |
Умножение - знаковое накопление длинных значений | SMLAL {cond}{S} RdLo, RdHi, Rm, Rs | |
Сравнение | CMP {cond} Rd, |
|
Сравнение отрицательное | CMN {cond} Rd, |
|
Логические | Проверка | TST {cond} Rn, |
Проверка на эквивалентность | TEQ {cond} Rn, |
|
Лог. И | AND {cond}{S} Rd, Rn, |
|
Искл. ИЛИ | EOR {cond}{S} Rd, Rn, |
|
ORR | ORR {cond}{S} Rd, Rn, |
|
Сброс бита | BIC {cond}{S} Rd, Rn, |
|
Переход | Переход | {cond} label |
Переход по ссылке | {cond} label | |
Переход и изменение набора инструкций | {cond} Rn | |
Чтение | слова | LDR {cond} Rd, |
LDR {cond}T Rd, |
||
байта | LDR {cond}B Rd, |
|
LDR {cond}BT Rd, |
||
байта со знаком | LDR {cond}SB Rd, |
|
полуслова | LDR {cond}H Rd, |
|
полуслова со знаком | LDR {cond}SH Rd, |
|
операции с несколькими блоками данных | - | |
LDM {cond}IB Rd{!}, |
||
LDM {cond}IA Rd{!}, |
||
LDM {cond}DB Rd{!}, |
||
LDM {cond}DA Rd{!}, |
||
LDM {cond} |
||
LDM {cond} |
||
операция над стеком с регистрами пользователя | LDM {cond} |
|
Запись | слова | STR {cond} Rd, |
слова с преимуществом режима пользователя | STR {cond}T Rd, |
|
байта | STR {cond}B Rd, |
|
байта с преимуществом режима пользователя | STR {cond}BT Rd, |
|
полуслова | STR {cond}H Rd, |
|
операции над несколькими блоками данных | - | |
STM {cond}IB Rd{!}, |
||
STM {cond}IA Rd{!}, |
||
STM {cond}DB Rd{!}, |
||
o с последующим декрементом | STM {cond}DA Rd{!}, |
|
STM {cond} |
||
STM {cond} |
||
Обмен | слов | SWP {cond} Rd, Rm, |
байт | SWP {cond}B Rd, Rm, | |
Сопроцессор | Операция над данными | CDP {cond} p |
Пересылка в ARM-регистр из сопроцессора | MRC {cond} p |
|
Пересылка в сопроцессор из ARM-регистра | MCR {cond} p |
|
Чтение | LDC {cond} p |
|
Запись | STC {cond} p |
|
Программное прерывание | SWI 24bit_Imm |
Подробно ознакомиться с системой команд в режиме ARM можно .
Режимы адресации
Режимы адресации - процедуры, которые используются различными инструкциями для генерации значений, используемых инструкциями. Процессор ARM7TDMI поддерживает 5 режимов адресации:
Режимы адресации с указанием их типов и мнемонических кодов представлены в таблице 1.3.
Таблица 1.3. Режимы адресации
Режим адресации | Тип или режим адресации | Мнемонический код или тип стека |
Режим 2 |
Константа смещения | |
Регистр смещения | ||
Масштабный регистр смещения | ||
Предварительное индексированное смещение | - | |
Константа | ! | |
Регистр | ! | |
Масштабный регистр | ! | |
! | ||
! | ||
! | ||
! | ||
- | ||
Константа | , #+/-12bit_Offset | |
Регистр | , +/-Rm | |
Масштабный регистр | ||
Режим 2, привилегированный |
Константа смещения | |
Регистр смещения | ||
Масштабный регистр смещения | ||
Смещение с последующим индексированием | - | |
Константа | , #+/-12bit_Offset | |
Регистр | , +/-Rm | |
Масштабный регистр | , +/-Rm, LSL #5bit_shift_imm | |
, +/-Rm, LSR #5bit_shift_imm | ||
, +/-Rm, ASR #5bit_shift_imm | ||
, +/-Rm, ROR #5bit_shift_imm | ||
Режим 3, |
Константа смещения | |
! | ||
Последующее индексирование | , #+/-8bit_Offset | |
Регистр | ||
Предварительное индексирование | ! | |
Последующее индексирование | , +/-Rm | |
Режим 4, чтение |
IA, последующий инкремент | FD, full descending |
ED, empty descending | ||
DA, последующий декремент | FA, full ascending | |
DB предварительный декремент | EA, empty ascending | |
Режим 4, запись |
IA, последующий инкремент | FD, full descending |
IB, предварительный инкремент | ED, empty descending | |
DA, последующий декремент | FA, full ascending | |
DB предварительный декремент | EA, empty ascending | |
Режим 5, передача данных сопроцессора |
Константа смещения | |
Предварительное индексирование | ! | |
Последующее индексирование | , #+/-(8bit_Offset*4) |
Операнд 2
Операнд является частью инструкции, которая ссылается на данные или периферийное устройство. Операнды 2 представлены в таблице 1.4.
Таблица 1.4. Операнд 2
Поля представлены в таблице 1.5.
Таблица 1.5. Поля
Поля условий
Поля условий представлены в таблице 1.6.
Таблица 1.6. Поля условий
Тип поля | Суффикс | Описание | Условие |
Условие {cond} | EQ | Равно | Z=1 |
NE | Неравно | Z=0 | |
CS | Беззнаковое больше или равно | C=1 | |
CC | Беззнаковое меньше | C=0 | |
MI | Отрицательное | N=1 | |
PL | Положительное или ноль | N=0 | |
VS | Переполнение | V=1 | |
VC | Нет переполнения | V=0 | |
HI | Беззнаковое больше | C=1, Z=0 | |
LS | Беззнаковое меньше или равно | C=0, Z=1 | |
GE | Больше или равно | N=V (N=V=1 или N=V=0) | |
LT | Меньше | NV (N=1 и V=0) или (N=0 и V=1) | |
GT | Больше | Z=0, N=V (N=V=1 или N=V=0) | |
LE | Меньше или равно | Z=0 или NV (N=1 и V=0) или (N=0 и V=1) | |
AL | Всегда истинный | флаги игнорируются |
4.3 Краткое описание набора инструкций Thumb
Форматы набора инструкций Thumb показаны на рисунке 1.6. Более подробная информация по форматам наборов инструкций ARM приведена "ARM Architectural Reference Manual".
Рисунок 1.6. Форматы набора инструкций Thumb
Набор инструкций Thumb представлен в таблице 1.7.
Таблица 1.7. Краткое описание набора инструкций Thumb
Операция | Синтаксис Ассемблера | |
Пересылка (копирование) | константы | MOV Rd, #8bit_Imm |
старшего в младший | MOV Rd, Hs | |
младшего в старший | MOV Hd, Rs | |
старшего в старший | MOV Hd, Hs | |
Арифметические | сложение | ADD Rd, Rs, #3bit_Imm |
прибавить младший к младшему | ADD Rd, Rs, Rn | |
прибавить старший к младшему | ADD Rd, Hs | |
прибавить младший к старшему | ADD Hd, Rs | |
прибавить старший к старшему | ADD Hd, Hs | |
сложение с константой | ADD Rd, #8bit_Imm | |
прибавить значение к SP | ADD SP, #7bit_Imm ADD SP, #-7bit_Imm | |
сложение с учетом переноса | ADC Rd, Rs | |
вычитание | SUB Rd, Rs, Rn SUB Rd, Rs, #3bit_Imm | |
вычитание константы | SUB Rd, #8bit_Imm | |
вычитание с переносом | SBC Rd, Rs | |
инверсия знака | NEG Rd, Rs | |
умножение | MUL Rd, Rs | |
сравнить младший с младшим | CMP Rd, Rs | |
сравнить младший и старший | CMP Rd, Hs | |
сравнить старший и младший | CMP Hd, Rs | |
сравнить старший и старший | CMP Hd, Hs | |
сравнить отрицательные | CMN Rd, Rs | |
сравнить с константой | CMP Rd, #8bit_Imm | |
Логические | И | AND Rd, Rs |
Искл. ИЛИ | EOR Rd, Rs | |
ИЛИ | ORR Rd, Rs | |
Сброс бита | BIC Rd, Rs | |
Пересылка NOT | MVN Rd, Rs | |
Тестирование бит | TST Rd, Rs | |
Сдвиг/вращение | Логический сдвиг влево | LSL Rd, Rs, #5bit_shift_imm LSL Rd, Rs |
Логический сдвиг вправо | LSR Rd, Rs, #5bit_shift_imm LSR Rd, Rs | |
Арифметический сдвиг вправо | ASR Rd, Rs, #5bit_shift_imm ASR Rd, Rs | |
Вращение вправо | ROR Rd, Rs | |
Переход | условные переходы | - |
BEQ label | ||
BNE label | ||
BCS label | ||
BCC label | ||
BMI label | ||
BPL label | ||
BVS label | ||
BVC label | ||
BHI label | ||
BLS label | ||
BGE label | ||
BLT label | ||
BGT label | ||
BLE label | ||
Безусловный переход | B label | |
Длинный переход по ссылке | BL label | |
Опциональное изменение состояния | - | |
BX Rs | ||
BX Hs | ||
Чтение | с константой смещения | - |
LDR Rd, | ||
LDRH Rd, | ||
LDRB Rd, | ||
с регистром смещения | - | |
LDR Rd, | ||
LDRH Rd, | ||
LDRSH Rd, | ||
LDRB Rd, | ||
LDRSB Rd, | ||
относительно счетчика программы PC | LDR Rd, | |
относительно указателя стека SP | LDR Rd, | |
Адрес | - | |
ADD Rd, PC, #10bit_Offset | ||
ADD Rd, SP, #10bit_Offset | ||
Множественное чтение | LDMIA Rb!, |
|
Запись | с константой смещения | - |
STR Rd, | ||
STRH Rd, | ||
STRB Rd, | ||
с регистром смещения | - | |
STR Rd, | ||
STRH Rd, | ||
STRB Rd, | ||
относительно SP | STR Rd, | |
Множественная запись | STMIA Rb!, |
|
Помещение/ извлечение из стека | Поместить регистры в стек | PUSH |
Поместить LR и регистры в стек | PUSH |
|
Извлечь регистры из стека | POP |
|
Извлечь регистры и PC из стека | POP |
|
Программное прерывание | - | SWI 8bit_Imm |
1. Счетчик часов реального времени должен быть включен (1); бит выбора источника тактирования сброшен (2), если тактирование не осуществляется от основного тактового генератора.
2. Один или оба бита выбора прерывающего события (3) должны быть установлены. И выбрано, какие именно события будут вызывать запрос прерывания (5).
3. Должны быть заданы маски прерывающих событий (4, 7).
Система команд ARM7 (раздел 1.4) включает всего 45 инструкций, которые довольно сложны из-за многообразия методов адресации, условных полей и модификаторов. Программа на ассемблере получается громоздкой и
с трудом читается. Поэтому ассемблер редко применяется в программировании для архитектуры ARM7.
Вместе с тем, язык высокого уровня Си скрывает от программиста многие особенности архитектуры. Программист практически не касается таких процедур, как выбор режима ядра, выделение памяти под стек и обработка прерываний. Для изучения этих процедур полезно составить хотя бы одну простую программу на ассемблере.
Кроме того, даже при использовании Си к языку ассемблера все же приходится прибегать.
1. Следует контролировать Си-компилятор, отслеживая, не исключил ли он в ходе оптимизации важные команды, посчитав их ненужными. Не генерирует ли компилятор исключительно неэффективный код для сравнительно простой операции, из-за недостаточной оптимизации. Чтобы убедиться, что компилятор действительно задействует те аппаратные ресурсы, которые, призваны повысить эффективность конкретного алгоритма.
2. В ходе поиска ошибок или причин возникновения исключительных ситуаций (раздел 2.4.1).
3. Для получения кода, абсолютно оптимального по быстродействию или расходу памяти (разделы 2.2.20, 3.1.5).
Рассмотрим основные приемы составления программы на ассемблере
с целью продемонстрировать весь код исполняемый микроконтроллером, как есть, и без посредничества Си-компилятора.
Порядок создания проекта на основе ассемблера почти тот же, что и для Си-программ (разделы 2.3.1–2.3.3). Исключения лишь два:
а) файлу исходного текста присваивается расширение *.S;
б) здесь предполагается, что файл STARTUP.S к программе не подключается.
Текст программы на ассемблере принято оформлять в четыре колонки. Можно сказать, что каждая строка состоит из четырех полей, а именно: поля меток, операций, операндов, комментариев. Поля отделяются друг от друга символом «табуляция» или пробелами.
Основными являются поля операций и операндов. Допустимые операции и их синтаксис приведены в таблице (1.4.2)
Метка - это символьное обозначение адреса команды. Везде вместо метки будет выполняться подстановка адреса команды, которой предшествует метка. Чаще всего метки используются в командах передачи управления. Каждая метка должна быть уникальной и при этом является не обязательной. В отличие от многих других версий, в ассемблере RealView метки не заканчиваются двоеточием («: »).
Комментарии по желанию помещаются в конце строки и отделяются точкой с запятой («; »).
Приведем простой пример.
Ассемблер RealView поддерживает так называемые псевдокоманды. Псевдокоманда - это мнемоническое обозначение, которое на самом деле не соответствует системе команд процессора, а заменяется одной или (реже) несколькими командами. Псевдокоманды являются своего рода макросами и служат для упрощения синтаксиса. Перечень поддерживаемых псевдокоманд приведен в таблице (2.5.1).
В отличие от команд директивы не создают исполнимого кода, загружаемого в память микроконтроллера. Директивы представляю собой лишь предписания ассемблеру, управляют формированием исполнимого кода.
Рассмотрим часто используемые директивы ассемблера RealView 4.
Имя EQU Константа
Назначает Константе символьное обозначение Имя , которое становится синонимом константы. Основное назначение - введение имен управляющих регистров,
AREA Имя, Параметры
Определяет область памяти с заданным Именем . С помощью параметров указывается назначение области памяти, например, DATA (данные) или CODE (код). От выбранного назначения зависят адреса определяемой области. Область CODE размещается, начиная с адреса 0x00000000, область DATA - с адреса 0x40000000. В программе обязательно должна существовать область CODE c именем RESET . Константы, размещаемые в памяти программ, следует объявлять в секции с парой параметров CODE, READONLY .
Обозначает точку входа в программу, показывает ее «начало». Одна такая директива всегда должна присутствовать в программе. Обычно помещается непосредственно после директивы AREA RESET, CODE .
Таблица 2.5.1 – Псевдокоманды, поддерживаемые ассемблером RealView 4
Мнемоническое обозначение |
Операция |
Фактическая реализация |
||||||
и синтаксис |
||||||||
ADR{Усл } |
в регистр |
Сложение или вычитание константы из PC ко- |
||||||
мандами ADD или SUB |
||||||||
ADRL{Усл } |
в регистр |
Дважды ADD или SUB с участием PC |
||||||
(расширенный диапазон адресов) |
||||||||
ASR{Усл }{S} |
Арифметический сдвиг вправо |
|||||||
ASR{Усл }{S} |
нием сдвигового операнда |
|||||||
LDR{Усл } |
в регистр |
|||||||
адресацией (PC + непосредственное смещение) |
||||||||
Размещение константы |
в памяти программ |
|||||||
LDR{с индексной адреса- |
||||||||
цией. Смещением служит PC. |
||||||||
LSL{Усл }{S} |
Логический сдвиг влево |
|||||||
LSL{Усл }{S} |
нием сдвигового операнда |
|||||||
LSR{Усл }{S} |
Логический сдвиг вправо |
|||||||
LSR{Усл }{S} |
нием сдвигового операнда |
|||||||
POP{Усл } |
Восстановить регистры из стека |
Восстановление |
регистров |
командой |
||||
LDMIA R13!,{...} |
||||||||
PUSH{Усл } |
Сохранение |
регистров |
командой |
|||||
STMDB R13!,{...} |
||||||||
ROR{Усл }{S} |
Циклический сдвиг вправо |
|||||||
ROR{Усл }{S} |
нием сдвигового операнда |
|||||||
RRX{Усл }{S} |
Циклический сдвиг вправо через |
|||||||
перенос на 1 разряд |
нием сдвигового операнда |
|||||||
Имя SPACE Размер
Резервирует память для хранения данных заданного Размера . Имя становится синонимом адреса зарезервированного пространства. Единство адресного пространства позволяет применять эту директиву, как для постоянной, так и для оперативной памяти. Основное назначение - создание глобальных переменных в оперативной памяти (в области DATA ).
Метка DCB/DCW/DCD Константа
«Прошивают» данные (числовые Константы ) в памяти программ. Метка становиться синонимом адреса, по которому будут записаны данные. Разные директивы (DCB , DCW и DCD ) служат для данных разного размера: байт, 16-разрядное слово, 32-разрядное слово (соответственно).
Служит признаком конца файла. Весь текст после этой директивы игнорируется ассемблером.
Макрос представляет собой предопределенный фрагмент программы, выполняющий какую-либо распространенную операцию. В отличие от подпрограмм, вызываемых с помощью команд передачи управления, использование макросов не снижает быстродействия, но не снижает и расход памяти программ. Потому что при каждом вызове макроса ассемблер внедряет в программу полностью его текст.
Для объявления макроса служит следующая конструкция
$ Параметр1, $ Параметр2, ... |
|
Параметры позволяют модифицировать текст макроса при каждом обращении к нему. Внутри (в теле) макроса параметры используются также с предшествующим знаком «$ ». Вместо параметров в теле макроса подставляются параметры, указанные при вызове.
Вызов макроса осуществляется так:
Имя Параметр1, Параметр2, ...
Имеется возможность организовать проверку условия и ветвление.
IF "$ Параметр" == " Значение"
Обращаем внимание на то, такая конструкция не приводит к программной проверке условия микроконтроллером. Проверку условия осуществляет ассемблер в ходе формирования исполнимого кода.
Итак, мы создали новый проект, выполнили основные настройки, создали и подключили к проекту файл, в котором хотим написать на ассемблере какую-нибудь простенькую программу.
Что дальше? Дальше, собственно говоря, можно писать программу, используя набор команд thumb-2, поддерживаемый ядром Cortex-M3. Список и описание поддерживаемых команд можно посмотреть в документе под названием Cortex-M3 Generic User Guide (глава The Cortex-M3 Instruction Set ), который можно найти на вкладке Books в менеджере проекта, в Keil uVision 5. Подробно о командах thumb-2 будет написано в одной из следующих частей этой статьи, а пока поговорим о программах для STM32 в общем.
Как и любая другая программа на ассемблере, программа для STM32 состоит из команд и псевдокоманд, которые будут транслированы непосредственно в машинные коды, а также из различных директив, которые в машинные коды не транслируются, а используются в служебных целях (разметка программы, присвоение константам символьных имён и т.д.)
Например, разбить программу на отдельные секции позволяет специальная директива — AREA . Она имеет следующий синтаксис: AREA Section_Name {,type} {, attr} … , где:
Директива EQU наверняка всем хорошо знакома, поскольку встречается в любом ассемблере и предназначена для присвоения символьных имён различным константам, ячейкам памяти и т.д. Она имеет следующий синтаксис: Name EQU number и сообщает компилятору, что все встречающиеся символьные обозначения Name нужно заменять на число number . Скажем, если в качестве number использовать адрес ячейки памяти, то в дальнейшем к этой ячейке можно будет обращаться не по адресу, а используя эквивалентное символьное обозначение (Name ).
Директива GET filename вставляет в программу текст из файла с именем filename . Это аналог директивы include в ассемблере для AVR. Её можно использовать, например, для того, чтобы вынести в отдельный файл директивы присвоения символьных имён различным регистрам. То есть мы выносим все присвоения имён в отдельный файл, а потом, чтобы в программе можно было пользоваться этими символьными именами, просто включаем этот файл в нашу программу директивой GET.
Разумеется, кроме перечисленных выше есть ещё куча всяких разных директив, полный список которых можно найти в главе Directives Reference документа Assembler User Guide , который можно найти в Keil uVision 5 по следующему пути: вкладка Books менеджера проектов -> Tools User’s Guide -> Complete User’s Guide Selection -> Assembler User Guide .
Большинство команд, псевдокоманд и директив в программе имеют следующий синтаксис:
{label} SYMBOL {expr} {,expr} {,expr} {; комментарий}
{label} — метка. Она нужна для того, чтобы можно было определить адрес следующей за этой меткой команды. Метка является необязательным элементом и используется только когда необходимо узнать адрес команды (например, чтобы выполнить переход на эту команду). Перед меткой не должно быть пробелов (то есть она должна начинаться с самой первой позиции строки), кроме того, имя метки может начинаться только с буквы.
SYMBOL — команда, псевдокоманда или директива. Команда, в отличии от метки, наоборот, должна иметь некоторый отступ от начала строки даже если перед ней нет метки.
{expr} {,expr} {,expr} — операнды (регистры, константы…)
; — разделитель. Весь текст в строке после этого разделителя воспринимается как комментарий.
Ну а теперь, как и обещал, простейшая программа:
AREA START , CODE , READONLY dcd 0x20000400 dcd Program_start ENTRY Program_start b Program_start END |
AREA START, CODE, READONLY dcd 0x20000400 dcd Program_start ENTRY Program_start b Program_start END
В этой программе у нас всего одна секция, которая называется START. Эта секция размещается во flash-памяти (поскольку для неё использован атрибут readonly).
Первые 4 байта этой секции содержат адрес вершины стека (в нашем случае 0x20000400), а вторые 4 байта — адрес точки входа (начало исполняемого кода). Далее следует сам код. В нашем простейшем примере исполняемый код состоит из одной единственной команды безусловного перехода на метку Program_start, то есть снова на выполнение этой же команды.
Поскольку секция во флеше всего одна, то в scatter-файле для нашей программы в качестве First_Section_Name нужно будет указать именно её имя (то есть START).
В данном случае у нас перемешаны данные и команды. Адрес вершины стека и адрес точки входа (данные) записаны с помощью директив dcd прямо в секции кода. Так писать конечно можно, но не очень красиво. Особенно, если мы будем описывать всю таблицу прерываний и исключений (которая получится достаточно длинной), а не только вектор сброса. Гораздо красивее не загромождать код лишними данными, а поместить таблицу векторов прерываний в отдельную секцию, а ещё лучше — в отдельный файл. Аналогично, в отдельной секции или даже файле можно разместить и инициализацию стека. Мы, для примера, разместим всё в отдельных секциях:
AREA STACK, NOINIT, READWRITE SPACE 0x400 ; пропускаем 400 байт Stack_top ; и ставим метку AREA RESET, DATA, READONLY dcd Stack_top ; адрес метки Stack_top dcd Program_start ; адрес метки Program_start AREA PROGRAM, CODE, READONLY ENTRY ; точка входа (начало исполняемого кода) Program_start ; метка начала программы b Program_start END
Ну вот, та же самая программа (которая по прежнему не делает нифига полезного), но теперь выглядит намного нагляднее. В scatter-файле для этой программы нужно указать в качестве First_Section_Name имя RESET, чтобы эта секция располагалась во flash-памяти первой.