7в. Ассемблер Cortex-M на практике: стартовый код, атомики, DSP и переключение задач
Раздел 7б учил читать то, что собрал компилятор. Здесь пишем сами — там, где
без ассемблера не обойтись или где он заметно выигрывает: стартовый код,
атомарные операции, DSP-команды, переключение задач. У каждого такого места
есть правило, которое компилятор соблюдает сам, а человек легко нарушает:
бит Thumb в адресе, выравнивание стека, сохранение r4–r11. Здесь каждое
правило нарушено нарочно, чтобы было видно, что именно ломается.
Стенд — _verify/asm7v: стартовый код и обработчики на ассемблере (start7v.S),
функции для опытов (asm7v.S), программа на C, которая их вызывает (run7v.c).
Всё собрано arm-none-eabi-gcc для Cortex-M4 и прогнано в QEMU (машина
netduinoplus2) с ключами -icount и -singlestep: время считается по числу
команд, и прерывание может прийти между любыми двумя из них, как на кристалле.
Правила сверены с руководством по ядру Cortex-M4 из пакета. Сверяет
_verify/asm7v_check.py.
Как это называется в документации
Оригинал Перевод assembler directive директива ассемблера: указание ассемблеру, а не команда процессору Thumb state, T bit состояние Thumb, бит T в EPSR INVSTATE отказ «недопустимое состояние»: переход без бита Thumb stack alignment (8-byte) выравнивание стека на 8 байт veneer, long branch stub вставка компоновщика для дальнего перехода exclusive access, LDREX/STREX эксклюзивный доступ: чтение с меткой и запись, если метку не сняли SIMD, dual 16-bit две 16-битные операции в одной 32-битной команде saturation насыщение: результат упирается в предел вместо переполнения context switch, PendSV переключение задач, отложенный системный вызов
7в.1 Файл .S: директивы и бит Thumb
Ассемблерный файл называют с большой .S: его прогоняет препроцессор,
и работают #include, #define, #ifdef. Файл с маленькой .s идёт
в ассемблер как есть. Скелет одной функции:
.syntax unified @ единый синтаксис ARM/Thumb — как в документации
.cpu cortex-m4
.thumb
.section .text.good_func, "ax", %progbits @ своя секция: --gc-sections выбросит, если не нужна
.global good_func @ видна из других файлов
.type good_func, %function @ это функция Thumb: в её адресе бит 0 = 1
good_func:
movs r0, #42
bx lr
.size good_func, . - good_func @ размер — для отладчика и nm -S
| Директива | Что делает | Если забыть |
|---|---|---|
.syntax unified |
одна запись команд для ARM и Thumb, как в руководстве | старый синтаксис: adds r0, r0, #1 — «not supported in Thumb16 mode», ldrlo — «Thumb does not support conditional execution» |
.thumb |
дальше — команды Thumb | ассемблер ждёт команды ARM: «attempt to use an ARM instruction on a Thumb-only processor» на каждой строке |
.section …, "ax" |
куда положить код: a — занимает память, x — исполняемый |
всё в одной .text, сборщик мусора компоновщика бессилен |
.global |
имя видно компоновщику | «undefined reference» при вызове из C |
.type …, %function |
метка — функция Thumb | адрес без бита 0 — ниже |
.size |
размер функции | отладчик и nm -S не знают границ |
1: … 1b / 1f |
местная метка: ближайшая 1 назад или вперёд |
— |
ldr r0, =_sdata |
загрузить 32-битную константу: ассемблер сам положит её в пул | — |
Бит Thumb. У Cortex-M есть только команды Thumb, и признак этого — единица
в бите 0 каждого адреса, по которому переходят BX, BLX, POP {pc} и
вектор исключения. Ассемблер ставит этот бит только меткам, помеченным как
функция. Опыт — две одинаковые функции, у одной .type забыт:
good_func (.type есть) |
bad_func (.type забыт) |
|
|---|---|---|
| символ в объектном файле | FUNC, значение 1 |
NOTYPE, значение 0 |
| адрес, взятый в C | 0x080000D7 — нечётный | 0x080000DC — чётный |
| прямой вызов из C | bl — работает |
тот же bl — работает |
| вызов по указателю | вернула 42 | UsageFault, CFSR = 0x00020000 (INVSTATE), PC в кадре 0x080000DC |
Прямой вызов прощает ошибку: bl не меняет состояние процессора, и
компоновщик ставит его как обычно. Ломается всё, что берёт адрес:
указатель на функцию, таблица обратных вызовов, таблица векторов. Поэтому
ошибка проявляется не там, где сделана, а когда функцию впервые вызовут
по указателю. В таблице векторов стенда .word Reset_Handler дало
0x08000041, .word PendSV_Handler — 0x08000095: бит 0 проставлен, потому
что обе метки объявлены функциями.
Ограничение модели. Если чётный адрес ещё и не кратен 4 (bad_func2,
0x080001BA), QEMU 7.2 сообщает не INVSTATE, а CFSR = 0x01000000
(UNALIGNED). По руководству переход со сброшенным битом T — отказ состояния,
на кристалле ждите INVSTATE при любом чётном адресе.
7в.2 Стартовый код на ассемблере
До main кто-то должен разложить память: скопировать начальные значения
.data из флеш-памяти в ОЗУ и обнулить .bss. В разделе 4в это сделано на C;
на ассемблере — семнадцать команд:
Reset_Handler:
ldr r0, =_sdata @ куда: .data в ОЗУ
ldr r1, =_edata
ldr r2, =_sidata @ откуда: образ .data во флеш-памяти
1: cmp r0, r1
ittt lo @ три условные команды; переход — последней в блоке
ldrlo r3, [r2], #4
strlo r3, [r0], #4
blo 1b
ldr r0, =_sbss
ldr r1, =_ebss
movs r2, #0
2: cmp r0, r1
itt lo
strlo r2, [r0], #4
blo 2b
bl main
3: b 3b
Код — 42 байта, плюс пять адресов в пуле (20 байт). Границы _sdata,
_edata, _sidata, _sbss, _ebss задаёт скрипт компоновщика (раздел 4в);
ассемблер берёт их как обычные имена.
Проверка тёплым перезапуском. В QEMU ОЗУ при старте и так нулевое, и
обнуление .bss ничего бы не доказало. Поэтому программа при первом запуске
портит переменные и сама прыгает в вектор сброса — как после сторожевого
таймера, без выключения питания:
| Переменная | Секция | Первый запуск | Записано перед прыжком | После перезапуска |
|---|---|---|---|---|
init_word = 0x12345678 |
.data |
0x12345678 | 0xDEADBEEF | 0x12345678 |
zero_word |
.bss |
0 | 0xCAFEF00D | 0 |
boot_no |
.noinit |
1 | — | 2 |
.data и .bss восстановлены. Счётчик boot_no лежит в секции .noinit,
которую стартовый код не трогает, и пережил перезапуск. После включения
питания там мусор, поэтому рядом держат контрольное слово: в опыте это
boot_magic, и если он не равен 0xB007B007, счётчик обнуляют и слово
записывают. В скрипте компоновщика секция описана отдельно от .bss:
.noinit (NOLOAD) : { *(.noinit) } > RAM. Так хранят причину последнего
сброса и счётчик аварий.
7в.3 Вызов C из ассемблера: стек кратен 8
Соглашение о вызовах требует, чтобы на входе в любую функцию sp был кратен
8. Компилятор на это рассчитывает: в функции с переменным числом
аргументов он округляет адрес следующего 64-битного числа вверх до кратного
8 (add r3, r3, #7 и bic r3, r3, #7 в vsum64). Причём округляет настоящий
адрес в памяти, а не смещение.
Ассемблерная функция, которая перед bl положила в стек нечётное число слов,
ломает это молча. Опыт: C-функция vsum64(3, 0x1_00000002, 0x3_00000004,
0x5_00000006) вызвана через две ассемблерные обёртки: одна кладёт в стек
два слова, другая — три.
| Стек кратен 8 | Стек кратен 4 | |
|---|---|---|
sp на входе в вызванную функцию |
0x2001FFC8 | 0x2001FFC4 |
| первое число | 0x1_00000002 — верно | 0x2_00000005 — половина из мусора в r1 |
| сумма трёх | 0x9_0000000C — верно | 0xC_00000009 |
| сообщение об ошибке | — | нет |
Все три числа прочитаны со сдвигом на слово: младшие и старшие половины перепутались. Ни отказа, ни предупреждения нет.
То же случится с printf("%lld") и printf("%f") (у double тоже
8 байт), а в прерывании — нет: при входе в исключение процессор выравнивает
стек сам (бит STKALIGN в SCB->CCR) — кроме F1, где бит после сброса
выключен и его включают сами (раздел 7а.2). Правило для своего кода: класть в стек
чётное число регистров — push {r4, lr}, а не push {lr}.
7в.4 Дальний вызов: вставка компоновщика
BL достаёт на ±16 МБ. От флеш-памяти (0x08000000) до ОЗУ (0x20000000) —
384 МБ, и функцию, которую перенесли в ОЗУ (например, чтобы стирать
флеш-память, не останавливаясь), прямым bl не вызвать.
Компоновщик делает это сам. Функция ram_add лежит в секции .ramfunc
внутри .data, копируется стартовым кодом вместе с переменными, а на месте
вызова появилась вставка:
__ram_add_veneer:
ldr.w pc, [pc] @ загрузить адрес прямо в pc
.word 0x20000001 @ ram_add в ОЗУ, бит Thumb на месте
Восемь байт и одно лишнее чтение на каждый вызов. ram_add(40, 2) вернула 42.
Если вставки нежелательны (горячий цикл, строгий подсчёт тактов), функцию
объявляют с __attribute__((long_call)) или собирают с -mlong-calls: тогда
компилятор сам кладёт адрес в регистр и переходит по нему — в опыте
ldr r3, [pc, #4] и bx r3.
7в.5 LDREX/STREX: инкремент, который не теряется
counter++ — это три команды: прочитать, прибавить, записать. Если прерывание
между чтением и записью тоже увеличит counter, его прибавка пропадёт:
основной код запишет своё, посчитанное от старого значения.
Опыт: main увеличивает счётчик 20 000 раз, прерывание SysTick каждые
97 тактов — тоже на единицу. Потери = 20 000 + число прерываний − итог.
| Способ | Прерываний | Потеряно | Повторов STREX |
|---|---|---|---|
counter++ (ldr, add, str) |
177 | 107 | — |
LDREX/STREX |
213 | 0 | 71 |
cpsid i … cpsie i вокруг ++ |
248 | 0 | — |
Число прерываний разное, потому что циклы разной длины идут разное время.
Потери легко посчитать. Цикл компилятора — пять команд: ldr, subs, add,
str, bne. Прерывание, пришедшее после ldr, subs или add, отнимет
прибавку: три места из пяти. 107 из 177 — это 60 %.
Цикл с эксклюзивным доступом:
inc_ldrex: @ uint32_t inc_ldrex(volatile uint32_t *p, uint32_t n)
movs r12, #0 @ сколько раз STREX не прошёл
1: ldrex r2, [r0] @ прочитать и поставить метку
adds r2, r2, #1
strex r3, r2, [r0] @ записать, только если метка цела; r3 = 0 — записано
cbz r3, 2f
adds r12, r12, #1
b 1b @ метку сняли — прочитать заново
2: subs r1, r1, #1
bne 1b
mov r0, r12
bx lr
По руководству ядро снимает метку эксклюзивного доступа при CLREX,
при любом STREX и при любом исключении. Прерывание между ldrex и
strex — два места из шести команд цикла — заставляет strex вернуть 1, и
цикл читает заново: 71 повтор на 213 прерываний, ровно треть.
Что выбрать:
LDREX/STREXне трогают прерывания вовсе: срочное прерывание не ждёт ни такта. Годится для счётчиков, флагов, указателей очереди. У Cortex-M0 этих команд нет.cpsid iпроще и работает везде, но на время запрета задерживает все прерывания. Для двух-трёх команд это незаметно, для длинного участка — нет.- В C то же дают
atomic_fetch_add(раздел 124) и функции CMSIS__LDREXWи__STREXW.
Почему опыт шёл с -singlestep. Без этого ключа QEMU проверяет прерывания
только между блоками команд, и весь цикл ldr–str проходит без
прерываний: потерь нет, и гонка не видна. На кристалле прерывание приходит
между любыми командами.
7в.6 DSP-команды: две операции за одну
У Cortex-M4 есть команды, которые считают 32-битный регистр парой
16-битных чисел. Скалярное произведение отсчётов int16_t — основа фильтров
и корреляции:
dot_smlad: @ n кратно 4 и больше нуля, массивы выровнены на 4
push {r4-r7}
movs r3, #0
1: ldmia r0!, {r4, r5} @ четыре отсчёта a
ldmia r1!, {r6, r7} @ четыре отсчёта b
smlad r3, r4, r6, r3 @ r3 += a0*b0 + a1*b1
smlad r3, r5, r7, r3 @ r3 += a2*b2 + a3*b3
subs r2, r2, #4
bne 1b
mov r0, r3 @ результат
pop {r4-r7}
bx lr
| 64 отсчёта | Команд в цикле на отсчёт | Команд цикла всего | Результат |
|---|---|---|---|
C, -O2 |
5 (ldrh, ldrh, cmp, smlabb, bne) |
320 | −247 190 400 |
smlad |
1,5 (шесть команд на четыре отсчёта) | 96 | −247 190 400 |
Компилятор взял smlabb — умножение одной пары с накоплением — но парные
smlad не собрал: для этого нужно знать, что массивы выровнены и длина
кратна 4. В C те же команды дают функции CMSIS __SMLAD, __QADD16, __SSAT.
Насыщение. Две пары 16-битных чисел: (30 000 + 10 000) и (−30 000 + −10 000):
| Команда | Верхняя половина | Нижняя половина | Регистр |
|---|---|---|---|
sadd16 — обычное сложение |
−25 536 | 25 536 | 0x9C4063C0 |
qadd16 — с насыщением |
32 767 | −32 768 | 0x7FFF8000 |
Обычное сложение переполняется и меняет знак — в звуке это громкий щелчок.
С насыщением сигнал упирается в предел — слышно лишь срезанную вершину.
По руководству 16- и 8-битные QADD/QSUB флаг Q не ставят: узнать,
было ли насыщение, по флагу нельзя.
7в.7 Переключение задач своими руками
Ради этого ассемблер в ядре операционной системы и нужен: сохранить регистры одной задачи и достать регистры другой можно только командами, которых нет в C. Раздел 23 показывает, как это сделано во FreeRTOS; здесь — минимум, две задачи:
PendSV_Handler:
mrs r0, psp @ стек уходящей задачи: r0–r3, r12, lr, pc, xPSR уже в нём
stmdb r0!, {r4-r11} @ дописать то, что процессор не сохранил
ldr r1, =task_sp
ldr r2, =task_cur
ldr r3, [r2]
str r0, [r1, r3, lsl #2] @ task_sp[cur] = вершина её стека
eor r3, r3, #1 @ следующая из двух
str r3, [r2]
ldr r0, [r1, r3, lsl #2]
ldmia r0!, {r4-r11}
msr psp, r0
bx lr @ EXC_RETURN 0xFFFFFFFD: остальное процессор достанет сам
SysTick каждые 20 001 такт ставит PendSV в очередь. Обе задачи — один и тот же
цикл на ассемблере: кладёт в r4–r11 свою метку, крутится 3000 оборотов и
проверяет, что метка цела. Затем тот же опыт с PendSV без stmdb и ldmia:
| PendSV | Код | Кругов A / B | Испорчено регистров A / B |
|---|---|---|---|
сохраняет r4–r11 |
38 байт | 3955 / 3935 | 0 / 0 |
| не сохраняет | 30 байт | 3955 / 3936 | 1600 / 1592 |
За 400 переключений каждая задача возвращалась около 200 раз, и каждый раз
находила в r4–r11 метку соседки: 200 × 8 = 1600. Счёт кругов, который
задачи ведут через r0–r3, в обоих опытах одинаков: эти регистры, как и
r12, lr, pc и xPSR, сохраняет процессор при входе в исключение.
Как запустить первую задачу. Кадр второй задачи собран вручную как
будто её уже прервали: восемь слов под то, что снимет процессор (в r0 —
аргумент, в pc — адрес цикла, в xPSR — единица в бите T, 0x01000000),
и ниже восемь слов под r4–r11. Первая задача запускается иначе:
msr psp с её стеком, CONTROL = 2 (поток работает на PSP), isb и
переход в цикл. Без бита T в xPSR выход из PendSV в новую задачу кончится
отказом INVSTATE — то же правило бита Thumb, что в 7в.1.
PendSV — самым низким приоритетом. NVIC_SetPriority(PendSV_IRQn, 0xFF):
функция сдвинет число в значащие биты, и в регистре окажется 0xF0. Тогда
переключение идёт, только когда все прочие обработчики закончились, и в
r4–r11 лежат регистры задачи, а не прерванного обработчика. В стенде
условие выполнено само собой: других прерываний нет, у всех исключений
приоритет 0, и PendSV ждёт конца SysTick. Отдельный опыт
(_verify/fixw2a3/pend7v) добавляет прерывание с приоритетом 0x80,
которое кладёт метку в r4–r11 и работает дольше периода тика. PendSV
с приоритетом 0 вытесняет его обработчик: все 99 его запусков нашли
в r4–r11 чужие значения — 792 регистра, столько же испорчено у каждой
задачи. С 0xF0 — ни одного.
Настоящее ядро добавляет к этому маскирование через BASEPRI на время выбора
задачи (раздел 28) и, если задачи считают на FPU, регистры s16–s31
(раздел 7а).
7в.8 Грабли
Функция из .S падает при вызове по указателю, а напрямую работает.
Забыт .type имя, %function: адрес чётный, CFSR = 0x00020000 (INVSTATE).
printf("%lld") или "%f" печатает мусор после вызова из ассемблера.
Стек не кратен 8: перед bl положено нечётное число слов. Ошибки нет —
только неверные числа.
Переменная без начального значения не равна нулю. Стартовый код не
обнуляет .bss. В эмуляторе ОЗУ при старте нулевое, и ошибка не видна; на
плате после сброса там остатки прежней работы.
Счётчик иногда не досчитывает. ++ над общей с прерыванием переменной —
три команды. LDREX/STREX, atomic_fetch_add или короткий cpsid i.
Гонка не воспроизводится в эмуляторе. QEMU без -singlestep проверяет
прерывания между блоками команд.
Задачи «видят» переменные друг друга. PendSV не сохраняет r4–r11, а
компилятор держит в них локальные переменные.
Портятся регистры в обработчиках прерываний. PendSV приоритетнее других прерываний и вытесняет их: его ставят самым низким приоритетом.
Задача падает с INVSTATE при первом переключении на неё. В собранном
вручную кадре xPSR = 0 вместо 0x01000000.
ldmia с невыровненного массива — UsageFault. Для smlad через ldm
массив int16_t нужно выровнять на 4 (__attribute__((aligned(4)))).
Вызов функции в ОЗУ вдруг медленнее. Каждый вызов идёт через вставку компоновщика — лишнее чтение из флеш-памяти.
7в.9 Итоги
- Файл
.S— через препроцессор; у каждой функции.global,.type %function,.sizeи своя секция. Без.typeадрес чётный, и вызов по указателю даёт INVSTATE, хотя прямойblработает. - Стартовый код — семнадцать команд, 42 байта: копирование
.data, обнуление.bss,bl main. Тёплый перезапуск это проверяет;.noinitпереживает сброс. - Перед вызовом C стек кратен 8. Иначе
va_argс 64-битными числами молча читает со сдвигом на слово. BLдостаёт на ±16 МБ; до ОЗУ компоновщик ставит вставкуldr.w pc, [pc].counter++против прерывания потерял 107 прибавок из 177;LDREX/STREXиcpsid— ни одной. Исключение снимает метку, иSTREXповторяется.smladсчитает две пары за команду: 96 команд цикла вместо 320 на 64 отсчётах.qadd16насыщает, но флагQне ставит.- PendSV обязан сохранить
r4–r11: без двух команд за 400 переключений испорчено 1600 регистров.
Полные исходники
start7v.S — стартовый код целиком на ассемблере: таблица векторов, Reset_Handler, вход в обработчик отказа и PendSV с переключением двух задач.
/* Раздел 7в: стартовый код целиком на ассемблере — таблица векторов, Reset_Handler,
* вход в обработчик отказа и PendSV с переключением двух задач.
* С -DNO_R4R11 PendSV переключает только указатель стека, без r4–r11 (ошибка для опыта). */
.syntax unified
.cpu cortex-m4
.thumb
/* ── таблица векторов: адреса обработчиков с единицей в бите 0 ── */
.section .isr_vector, "a", %progbits
.global vectors
vectors:
.word _estack @ 0: начальный MSP
.word Reset_Handler @ 1
.word Default_Handler @ 2: NMI
.word HardFault_Handler @ 3
.word Default_Handler @ 4: MemManage
.word Default_Handler @ 5: BusFault
.word UsageFault_Handler @ 6
.word 0, 0, 0, 0 @ 7–10: зарезервировано
.word Default_Handler @ 11: SVCall
.word Default_Handler @ 12: DebugMon
.word 0 @ 13
.word PendSV_Handler @ 14
.word SysTick_Handler @ 15
/* ── сброс: скопировать .data, обнулить .bss, вызвать main ── */
.section .text.Reset_Handler, "ax", %progbits
.global Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr r0, =_sdata @ куда: .data в ОЗУ
ldr r1, =_edata
ldr r2, =_sidata @ откуда: образ .data во флеш-памяти
1: cmp r0, r1
ittt lo
ldrlo r3, [r2], #4
strlo r3, [r0], #4
blo 1b
ldr r0, =_sbss
ldr r1, =_ebss
movs r2, #0
2: cmp r0, r1
itt lo
strlo r2, [r0], #4
blo 2b
bl main
3: b 3b
.size Reset_Handler, . - Reset_Handler
.section .text.Default_Handler, "ax", %progbits
.global Default_Handler
.type Default_Handler, %function
Default_Handler:
b .
.size Default_Handler, . - Default_Handler
/* ── отказ: какой стек был у прерванного кода — по биту 2 EXC_RETURN в lr ── */
.section .text.fault_entry, "ax", %progbits
.global HardFault_Handler, UsageFault_Handler
.type HardFault_Handler, %function
.type UsageFault_Handler, %function
HardFault_Handler:
UsageFault_Handler:
tst lr, #4
ite eq
mrseq r0, msp
mrsne r0, psp
b fault_c @ void fault_c(uint32_t *frame); lr всё ещё EXC_RETURN
.size HardFault_Handler, . - HardFault_Handler
/* ── PendSV: сохранить уходящую задачу, достать следующую ── */
.section .text.PendSV_Handler, "ax", %progbits
.global PendSV_Handler
.type PendSV_Handler, %function
PendSV_Handler:
mrs r0, psp @ стек уходящей задачи: r0–r3, r12, lr, pc, xPSR уже в нём
#ifndef NO_R4R11
stmdb r0!, {r4-r11} @ дописать то, что процессор не сохранил
#endif
ldr r1, =task_sp
ldr r2, =task_cur
ldr r3, [r2]
str r0, [r1, r3, lsl #2] @ task_sp[cur] = вершина её стека
eor r3, r3, #1 @ следующая из двух
str r3, [r2]
ldr r0, [r1, r3, lsl #2]
#ifndef NO_R4R11
ldmia r0!, {r4-r11}
#endif
msr psp, r0
bx lr @ EXC_RETURN 0xFFFFFFFD: остальное процессор достанет сам
.size PendSV_Handler, . - PendSV_Handler
/* ── первая задача: перейти на PSP и прыгнуть в её цикл ── */
.section .text.start_first, "ax", %progbits
.global start_first
.type start_first, %function
start_first: @ void start_first(uint32_t seed, uint32_t *stat, uint32_t *sp)
msr psp, r2
movs r3, #2 @ CONTROL.SPSEL = 1: поток работает на PSP
msr control, r3
isb
b task_loop
.size start_first, . - start_first
asm7v.S — функции на ассемблере для опытов раздела: .type, вызов C со стеком, LDREX/STREX, SMLAD и сложения с насыщением, цикл задачи.
/* Раздел 7в: функции на ассемблере для опытов в QEMU. */
.syntax unified
.cpu cortex-m4
.thumb
/* ── 1. одна и та же функция с .type и без ── */
.section .text.good_func, "ax", %progbits
.global good_func
.type good_func, %function @ метка — функция Thumb: в её адресе бит 0 = 1
good_func:
movs r0, #42
bx lr
.size good_func, . - good_func
.section .text.bad_func, "ax", %progbits
.p2align 2
.global bad_func @ .type забыт: для компоновщика это просто адрес
bad_func:
movs r0, #42
bx lr
/* ── 3. вызов C-функции со стеком, выровненным на 8 и на 4 ── */
.section .text.call_aligned, "ax", %progbits
.global call_aligned, call_misaligned
.type call_aligned, %function
.type call_misaligned, %function
call_aligned: @ int64_t call_aligned(int64_t (*fn)(void))
push {r4, lr} @ два слова: кратность 8 сохранена
blx r0
pop {r4, pc}
.size call_aligned, . - call_aligned
call_misaligned: @ то же, но sp сдвинут на одно слово
push {r4, lr}
sub sp, sp, #4
blx r0
add sp, sp, #4
pop {r4, pc}
.size call_misaligned, . - call_misaligned
/* ── 5. атомарный инкремент: LDREX/STREX, возвращает число повторов ── */
.section .text.inc_ldrex, "ax", %progbits
.global inc_ldrex
.type inc_ldrex, %function
inc_ldrex: @ uint32_t inc_ldrex(volatile uint32_t *p, uint32_t n)
movs r12, #0 @ сколько раз STREX не прошёл
1: ldrex r2, [r0]
adds r2, r2, #1
strex r3, r2, [r0] @ r3 = 0 — записано, 1 — метка снята, повторить
cbz r3, 2f
adds r12, r12, #1
b 1b
2: subs r1, r1, #1
bne 1b
mov r0, r12
bx lr
.size inc_ldrex, . - inc_ldrex
/* ── 6. скалярное произведение int16: две пары за команду SMLAD ── */
.section .text.dot_smlad, "ax", %progbits
.global dot_smlad
.type dot_smlad, %function
dot_smlad: @ int32_t dot_smlad(const int16_t *a, const int16_t *b, uint32_t n), n кратно 4
push {r4-r7}
movs r3, #0
1: ldmia r0!, {r4, r5} @ четыре отсчёта a
ldmia r1!, {r6, r7} @ четыре отсчёта b
smlad r3, r4, r6, r3 @ r3 += a0*b0 + a1*b1
smlad r3, r5, r7, r3 @ r3 += a2*b2 + a3*b3
subs r2, r2, #4
bne 1b
mov r0, r3
pop {r4-r7}
bx lr
.size dot_smlad, . - dot_smlad
/* ── 6. два 16-битных сложения одной командой: с насыщением и без ── */
.section .text.lanes, "ax", %progbits
.global add16_wrap, add16_sat
.type add16_wrap, %function
.type add16_sat, %function
add16_wrap: @ uint32_t add16_wrap(uint32_t a, uint32_t b)
sadd16 r0, r0, r1
bx lr
.size add16_wrap, . - add16_wrap
add16_sat:
qadd16 r0, r0, r1
bx lr
.size add16_sat, . - add16_sat
/* ── 7. цикл задачи: кладёт метку в r4–r11, крутится, проверяет, что метка цела ── */
.section .text.task_loop, "ax", %progbits
.global task_loop
.type task_loop, %function
task_loop: @ void task_loop(uint32_t seed, uint32_t stat[2]) — не возвращается
mov r4, r0 @ пользуется только r0–r3, r12 (их сохраняет процессор)
add r5, r0, #1 @ и r4–r11 (их обязан сохранить PendSV)
add r6, r0, #2
add r7, r0, #3
add r8, r0, #4
add r9, r0, #5
add r10, r0, #6
add r11, r0, #7
movw r2, #3000
1: subs r2, r2, #1 @ пауза: здесь задачу и прервут
bne 1b
movs r3, #0 @ сколько регистров испорчено
cmp r4, r0
it ne
addne r3, r3, #1
add r2, r0, #1
cmp r5, r2
it ne
addne r3, r3, #1
add r2, r0, #2
cmp r6, r2
it ne
addne r3, r3, #1
add r2, r0, #3
cmp r7, r2
it ne
addne r3, r3, #1
add r2, r0, #4
cmp r8, r2
it ne
addne r3, r3, #1
add r2, r0, #5
cmp r9, r2
it ne
addne r3, r3, #1
add r2, r0, #6
cmp r10, r2
it ne
addne r3, r3, #1
add r2, r0, #7
cmp r11, r2
it ne
addne r3, r3, #1
ldr r2, [r1, #4] @ stat[1] += испорченные
add r2, r2, r3
str r2, [r1, #4]
ldr r2, [r1] @ stat[0] += 1 круг
adds r2, r2, #1
str r2, [r1]
b task_loop
.size task_loop, . - task_loop
/* ── 2. та же забытая .type, но по адресу, не кратному 4 (для сравнения поведения модели) ── */
.section .text.bad_func2, "ax", %progbits
.p2align 2
nop
.global bad_func2
bad_func2:
movs r0, #42
bx lr
Дальше: раздел 8 «Порты ввода-вывода (GPIO)».