В даний час для програмування навіть досить простих мікроконтролерів використовуються мови високого рівня, як правило, є підмножина мови С або С++.
Однак при вивченні архітектури процесорів та її особливостей доцільно використовувати мови Ассемблера, оскільки тільки такий підхід може забезпечити виявлення особливостей архітектури, що вивчається. Тому подальший виклад ведеться з використанням мови Асемблера.
Перш ніж розпочати розгляд команд ARM7, необхідно відзначити наступні її особливості:
Підтримка двох наборів команд: ARM із 32-бітовими командами та THUMB із 16-бітними командами. Далі розглядається 32-бітовий набір команд, слово ARM означатиме команди, що належать до цього формату, а слово ARM7 - ЦПУ.
Підтримка двох форматів 32-розрядної адреси: зі зворотним порядком біт (big-endian processor і з прямим порядком біт (little-endian processor)). У першому випадку старший біт (Most Significant Bit - MSB) розташовується в молодшому биті слова, а в другому - у старшому. Це забезпечує сумісність з іншими сімействами 32-розрядних процесорів при використанні мов високого рівня. Однак у ряді сімейств процесорів з ядром ARM використовується тільки прямий порядок байтів (тобто MSB є найстаршим бітом адреси), що значно полегшує роботу з процесором. Оскільки компілятор, що використовується для ARМ7, працює з кодом в обох форматах, необхідно переконатися, що формат слів заданий правильно, в іншому випадку отриманий код буде «вивернуто навиворіт».
Можливість виконання різних типів зсуву одного з операндів "на проході" перед використанням в АЛУ
Підтримка умовного виконання будь-якої команди
Можливість заборони зміни прапорів результатів виконання операції.
Умовне виконання команд
Однією з важливих особливостей набору команд ARM є те, що підтримується умовне виконання будь-якої команди. У традиційних микроконтроллерах єдиними умовними командами є команди умовних переходів, і, можливо, ряд інших, як-от команди перевірки чи зміни стану окремих бітів. У наборі команд ARM старші 4 біти коду команди завжди порівнюються з прапорами умов у регістрі CPSR. Якщо значення не збігаються, команда на стадії дешифрації замінюється команда NOP (немає операції).
Це значно скорочує час виконання ділянок програми з «короткими» переходами. Так, наприклад, при вирішенні квадратних рівнянь з речовими коефіцієнтами і довільним корінням при негативному дискримінанті необхідно перед обчисленням квадратного кореня змінити знак дискримінанта, а результат привласнити уявної частини відповіді.
При традиційному вирішенні цього завдання необхідно запровадити команду умовного переходу. Виконання цієї команди займає щонайменше 2 такти – дешифрація та завантаження нового значення адреси в лічильник команд та додаткове число тактів на завантаження конвеєра команд. При використанні умовного виконання команди за позитивного дискримінанта команда зміни знака замінюється порожньою операцією. При цьому не відбувається очищення конвеєра команд і втрати становлять не більше одного такту. Поріг, у якому заміна умовних команд командою NOP виявляється ефективніше виконання традиційних команд умовного переходу пов'язаного з цим повторним заповненням конвеєра дорівнює його глибині, тобто. трьом.
Для реалізації цієї можливості до базових мнемонічним позначенням команд асемблера (і те саме), потрібно додати будь-який з шістнадцяти префіксів, визначальних тестовані стану прапорів умов. Ці префікси наведені у табл. 3. Відповідно, існує 16 варіантів кожної команди. Наприклад, наступна команда:
MOVEQ R1, #0x008
означає, що завантаження числа 0x00800000 в регістр R1 буде зроблено тільки в тому випадку, якщо результат виконання останньої команди обробки даних був «рівно» або отримано результат 0 і відповідно встановлений прапор (Z) регістра CPSR.
Таблиця 3
Префікси команд
Значення |
||
Z встановлено | ||
Z скинутий | ||
З встановлений |
Вище чи одно (беззнакове) |
|
C скинутий |
Нижче (беззнакове) |
|
N встановлено |
Негативний результат |
|
N скинутий |
Позитивний результат або 0 |
|
V встановлено |
Переповнення |
|
V скинутий |
Немає переповнення |
|
З встановлено, Z скинутий |
Вище (беззнакове) |
|
З скинуто, Z встановлено |
Нижче чи одно (беззнакове) |
|
Більше чи одно (знакове) |
||
N не дорівнює V |
Менше (знакове) |
|
Z скинутий І (N дорівнює V) |
Більше (знакове) |
|
Z встановлено АБО (N не дорівнює V) |
Менше чи одно (знакове) |
|
(ігноруються) |
Безумовне виконання |
1. Лічильник годинника реального часу повинен бути включений (1); біт вибору джерела тактування скинутий (2), якщо тактування здійснюється від основного тактового генератора.
2. Один або обидва біти вибору поривної події (3) повинні бути встановлені. І вибрано, які саме події викликатимуть запит переривання (5).
3. Повинні бути задані маски подій, що переривають (4, 7).
2.5 Про програмування ARM7 на асемблері
Система команд 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 до програми не підключається.
2.5.1 Основні правила запису програм на асемблері
Текст програми на асемблері прийнято оформляти у чотири колонки. Можна сказати, що кожен рядок складається з чотирьох полів, а саме поля міток, операцій, операндів, коментарів. Поля відокремлюються один від одного символом табуляції або пробілами.
Основними є поля операцій та операндів. Допустимі операції та їх синтаксис наведені в таблиці (1.4.2)
Мітка – це символьне позначення адреси команди. Скрізь замість мітки виконуватиметься підстановка адреси команди, якій передує мітка. Найчастіше мітки використовуються у командах передачі управління. Кожна мітка має бути унікальною і при цьому є необов'язковою. На відміну від багатьох інших версій, в асемблері RealView мітки не закінчуються двокрапкою («:»).
Коментарі за бажанням розміщуються в кінці рядка і відокремлюються крапкою з комою («; »).
Наведемо найпростіший приклад.
2.5.2 Псевдокоманди
Асемблер RealView підтримує так звані псевдокоманди. Псевдокоманда - це мнемонічне позначення, яке насправді не відповідає системі команд процесора, а замінюється однією або (рідше) декількома командами. Псевдокоманди є свого роду макросами і служать спрощення синтаксису. Перелік підтримуваних псевдокоманд наведено у таблиці (2.5.1).
2.5.3 Директиви асемблера
На відміну від команд директиви не створюють код, що завантажується в пам'ять мікроконтролера. Директиви являють собою лише розпорядження асемблеру, керують формуванням здійснюваного коду.
Розглянемо директиви асемблера RealView 4, які часто використовуються.
Ім'я EQU Константа
Призначає Константі символьне позначення Ім'я , яке стає синонімом константи. Основне призначення - запровадження імен керуючих регістрів,
AREA Ім'я, Параметри
Визначає область пам'яті із заданим Ім'ям. За допомогою параметрів вказується призначення області пам'яті, наприклад DATA (дані) або CODE (код). Від обраного призначення залежать адреси визначеної області. Область CODE розміщується, починаючи з адреси 0x00000000, область DATA – з адреси 0x40000000. У програмі обов'язково має існувати область CODE з ім'ям 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-розрядне слово (відповідно).
Служить ознакою кінця файлу. Весь текст після цієї директиви ігнорується асемблером.
2.5.4 Макроси
Макрос являє собою зумовлений фрагмент програми, що виконує будь-яку поширену операцію. На відміну від підпрограм, що викликаються за допомогою команд передачі управління, використання макросів не знижує швидкодії, але не знижує витрати пам'яті програм. Тому що при кожному виклик макросу асемблер впроваджує в програму повністю його текст.
Для оголошення макросу служить така конструкція
$ Параметр1, $ Параметр2, ... |
|
Параметри дають змогу модифікувати текст макросу при кожному зверненні до нього. Усередині (у тілі) макросу параметри використовуються також із попереднім знаком «$». Замість параметрів у тілі макросу підставляються параметри, вказані під час виклику.
Виклик макросу здійснюється так:
Ім'я Параметр1, Параметр2, ...
Є можливість організувати перевірку умови та розгалуження.
IF "$ Параметр" == " Значення"
Звертаємо увагу, така конструкція не призводить до програмної перевірки умови мікроконтролером. Перевірку умови здійснює асемблер у ході формування коду, що здійснюється.
Спочатку 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 до потрібних командам).