Embedded HandbookПриложения
Все разделы › II. Ядро и системные механизмы
STM32F407, STM32F405 · уровень продвинутый · 22 мин чтения
Открыть в интерактивном справочнике

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
сообщение об ошибке — нет

Все три числа прочитаны со сдвигом на слово: младшие и старшие половины перепутались. Ни отказа, ни предупреждения нет.

Где va_arg берёт 64-битные числа при стеке, кратном 8 и кратном 4

То же случится с 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 —

Число прерываний разное, потому что циклы разной длины идут разное время.

Цикл инкремента: прерывание между чтением и записью теряет прибавку, после LDREX — заставляет повторить

Потери легко посчитать. Цикл компилятора — пять команд: 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 прерываний, ровно треть.

Что выбрать:

Почему опыт шёл с -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 Итоги

Полные исходники

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)».

Открыть в интерактивном справочнике