7в. Cortex-M assembly in practice: start-up code, atomics, DSP and task switching
Section 7б taught you to read what the compiler has built. Here we write it ourselves — where
there is no way around assembly or where it gives a noticeable gain: start-up code,
atomic operations, DSP instructions, task switching. Each of these places
has a rule that the compiler follows by itself and a human easily breaks:
the Thumb bit in an address, stack alignment, saving r4–r11. Here each
rule is broken on purpose, so that you can see what exactly breaks.
The test bench is _verify/asm7v: start-up code and handlers in assembly (start7v.S),
functions for the experiments (asm7v.S), a C program that calls them (run7v.c).
Everything was built with arm-none-eabi-gcc for Cortex-M4 and run in QEMU (machine
netduinoplus2) with the switches -icount and -singlestep: time is counted by the number
of instructions, and an interrupt can come between any two of them, as on silicon.
The rules were checked against the Cortex-M4 core manual from the pack. The check is
_verify/asm7v_check.py.
Terms used in the documentation
Term Meaning assembler directive an instruction to the assembler, not a command to the processor Thumb state, T bit the Thumb state, the T bit in EPSR INVSTATE the "invalid state" fault: a branch without the Thumb bit stack alignment (8-byte) aligning the stack to 8 bytes veneer, long branch stub a linker insert for a long branch exclusive access, LDREX/STREX a read that sets a tag, and a write that succeeds only if the tag was not cleared SIMD, dual 16-bit two 16-bit operations in one 32-bit instruction saturation the result stops at the limit instead of overflowing context switch, PendSV switching tasks, the pendable system call
7в.1 The .S file: directives and the Thumb bit
An assembly file is named with a capital .S: it goes through the preprocessor,
and #include, #define, #ifdef work. A file with a lower-case .s goes
to the assembler as is. The skeleton of one function:
.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
| Directive | What it does | If forgotten |
|---|---|---|
.syntax unified |
one instruction syntax for ARM and Thumb, as in the manual | the old syntax: adds r0, r0, #1 — "not supported in Thumb16 mode", ldrlo — "Thumb does not support conditional execution" |
.thumb |
what follows are Thumb instructions | the assembler expects ARM instructions: "attempt to use an ARM instruction on a Thumb-only processor" on every line |
.section …, "ax" |
where to put the code: a — occupies memory, x — executable |
everything in one .text, the linker's garbage collection is powerless |
.global |
the name is visible to the linker | "undefined reference" when called from C |
.type …, %function |
the label is a Thumb function | an address without bit 0 — see below |
.size |
the size of the function | the debugger and nm -S do not know the boundaries |
1: … 1b / 1f |
a local label: the nearest 1 backward or forward |
— |
ldr r0, =_sdata |
load a 32-bit constant: the assembler itself puts it in the pool | — |
The Thumb bit. Cortex-M has only Thumb instructions, and the sign of this is a one
in bit 0 of every address branched to by BX, BLX, POP {pc} and
an exception vector. The assembler sets this bit only on labels marked as
a function. The experiment — two identical functions, one with .type forgotten:
good_func (.type present) |
bad_func (.type forgotten) |
|
|---|---|---|
| symbol in the object file | FUNC, value 1 |
NOTYPE, value 0 |
| the address taken in C | 0x080000D7 — odd | 0x080000DC — even |
| a direct call from C | bl — works |
the same bl — works |
| a call through a pointer | returned 42 | UsageFault, CFSR = 0x00020000 (INVSTATE), PC in the frame 0x080000DC |
A direct call forgives the mistake: bl does not change the processor state, and
the linker places it as usual. Everything that takes an address breaks:
a function pointer, a callback table, the vector table. That is why
the error shows up not where it was made but when the function is first called
through a pointer. In the bench's vector table .word Reset_Handler gave
0x08000041, .word PendSV_Handler — 0x08000095: bit 0 is set because
both labels are declared as functions.
A limitation of the model. If the even address is also not a multiple of 4 (bad_func2,
0x080001BA), QEMU 7.2 reports not INVSTATE but CFSR = 0x01000000
(UNALIGNED). According to the manual, a branch with the T bit cleared is a state fault;
on silicon expect INVSTATE for any even address.
7в.2 Start-up code in assembly
Before main someone has to lay out the memory: copy the initial values of
.data from Flash memory to RAM and zero .bss. In section 4в this is done in C;
in assembly it is seventeen instructions:
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
The code is 42 bytes, plus five addresses in the pool (20 bytes). The boundaries _sdata,
_edata, _sidata, _sbss, _ebss are set by the linker script (section 4в);
the assembler takes them as ordinary names.
Checking with a warm restart. In QEMU RAM is zero at start-up anyway, and
zeroing .bss would prove nothing. So on the first start the program
corrupts the variables and itself jumps to the reset vector — as after a watchdog
reset, without switching the power off:
| Variable | Section | First start | Written before the jump | After the restart |
|---|---|---|---|---|
init_word = 0x12345678 |
.data |
0x12345678 | 0xDEADBEEF | 0x12345678 |
zero_word |
.bss |
0 | 0xCAFEF00D | 0 |
boot_no |
.noinit |
1 | — | 2 |
.data and .bss are restored. The boot_no counter lies in the .noinit section,
which the start-up code does not touch, and it survived the restart. After power-up
there is garbage there, so a check word is kept next to it: in the experiment this is
boot_magic, and if it is not equal to 0xB007B007, the counter is zeroed and the word
is written. In the linker script the section is described separately from .bss:
.noinit (NOLOAD) : { *(.noinit) } > RAM. This is how the cause of the last
reset and a crash counter are kept.
7в.3 Calling C from assembly: the stack is a multiple of 8
The calling convention requires that on entry to any function sp is a multiple
of 8. The compiler counts on this: in a function with a variable number of
arguments it rounds the address of the next 64-bit number up to a multiple of
8 (add r3, r3, #7 and bic r3, r3, #7 in vsum64). And it rounds the real
address in memory, not the offset.
An assembly function that pushed an odd number of words onto the stack before bl
breaks this silently. The experiment: the C function vsum64(3, 0x1_00000002, 0x3_00000004,
0x5_00000006) is called through two assembly wrappers: one pushes
two words onto the stack, the other three.
| Stack a multiple of 8 | Stack a multiple of 4 | |
|---|---|---|
sp on entry to the called function |
0x2001FFC8 | 0x2001FFC4 |
| the first number | 0x1_00000002 — correct | 0x2_00000005 — half of it from garbage in r1 |
| the sum of three | 0x9_0000000C — correct | 0xC_00000009 |
| error message | — | none |
All three numbers were read shifted by a word: the low and high halves got mixed up. There is neither a fault nor a warning.
The same will happen with printf("%lld") and printf("%f") (a double is also
8 bytes), but not in an interrupt: on exception entry the processor aligns
the stack itself (the STKALIGN bit in SCB->CCR) — except on F1, where the bit is off after reset
and you enable it yourself (section 7а.2). The rule for your own code: push
an even number of registers — push {r4, lr}, not push {lr}.
7в.4 A long call: the linker veneer
BL reaches ±16 MB. From Flash memory (0x08000000) to RAM (0x20000000) it is
384 MB, and a function that was moved to RAM (for example, to erase
Flash memory without stalling) cannot be called with a direct bl.
The linker does this itself. The function ram_add lies in the .ramfunc section
inside .data, it is copied by the start-up code together with the variables, and at the place
of the call a veneer appeared:
__ram_add_veneer:
ldr.w pc, [pc] @ загрузить адрес прямо в pc
.word 0x20000001 @ ram_add в ОЗУ, бит Thumb на месте
Eight bytes and one extra read on every call. ram_add(40, 2) returned 42.
If veneers are undesirable (a hot loop, strict cycle counting), the function
is declared with __attribute__((long_call)) or built with -mlong-calls: then
the compiler itself puts the address into a register and branches through it — in the experiment
ldr r3, [pc, #4] and bx r3.
7в.5 LDREX/STREX: an increment that is not lost
counter++ is three instructions: read, add, write. If an interrupt
between the read and the write also increments counter, its increment is lost:
the main code writes its own value, computed from the old one.
The experiment: main increments the counter 20 000 times, the SysTick interrupt every
97 cycles also adds one. Losses = 20 000 + number of interrupts − the result.
| Method | Interrupts | Lost | STREX retries |
|---|---|---|---|
counter++ (ldr, add, str) |
177 | 107 | — |
LDREX/STREX |
213 | 0 | 71 |
cpsid i … cpsie i around ++ |
248 | 0 | — |
The number of interrupts differs because loops of different length run for different times.
The losses are easy to calculate. The compiler's loop is five instructions: ldr, subs, add,
str, bne. An interrupt that comes after ldr, subs or add takes away
the increment: three places out of five. 107 out of 177 is 60 %.
The loop with exclusive access:
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
According to the manual, the core clears the exclusive access tag on CLREX,
on any STREX and on any exception. An interrupt between ldrex and
strex — two places out of the six instructions of the loop — makes strex return 1, and
the loop reads again: 71 retries for 213 interrupts, exactly a third.
What to choose:
LDREX/STREXdo not touch interrupts at all: an urgent interrupt does not wait a single cycle. Suitable for counters, flags, queue pointers. Cortex-M0 does not have these instructions.cpsid iis simpler and works everywhere, but delays all interrupts for the duration of the masking. For two or three instructions this is unnoticeable, for a long section it is not.- In C the same is provided by
atomic_fetch_add(section 124) and the CMSIS functions__LDREXWand__STREXW.
Why the experiment ran with -singlestep. Without this switch QEMU checks for interrupts
only between blocks of instructions, and the whole ldr–str loop passes without
interrupts: there are no losses, and the race is not visible. On silicon an interrupt comes
between any instructions.
7в.6 DSP instructions: two operations for one
Cortex-M4 has instructions that treat a 32-bit register as a pair of
16-bit numbers. The dot product of int16_t samples is the basis of filters
and correlation:
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 samples | Loop instructions per sample | Loop instructions in total | Result |
|---|---|---|---|
C, -O2 |
5 (ldrh, ldrh, cmp, smlabb, bne) |
320 | −247 190 400 |
smlad |
1.5 (six instructions per four samples) | 96 | −247 190 400 |
The compiler used smlabb — multiply-accumulate of one pair — but did not produce the paired
smlad: for that it needs to know that the arrays are aligned and the length is a
multiple of 4. In C the same instructions are provided by the CMSIS functions __SMLAD, __QADD16, __SSAT.
Saturation. Two pairs of 16-bit numbers: (30 000 + 10 000) and (−30 000 + −10 000):
| Instruction | Upper half | Lower half | Register |
|---|---|---|---|
sadd16 — ordinary addition |
−25 536 | 25 536 | 0x9C4063C0 |
qadd16 — with saturation |
32 767 | −32 768 | 0x7FFF8000 |
Ordinary addition overflows and changes sign — in audio this is a loud click.
With saturation the signal stops at the limit — you hear only a clipped peak.
According to the manual, the 16- and 8-bit QADD/QSUB do not set the Q flag: you cannot
tell from the flag whether saturation occurred.
7в.7 Task switching with your own hands
This is what assembly in an operating system kernel is needed for: saving the registers of one task and fetching the registers of another can be done only with instructions that C does not have. Section 23 shows how this is done in FreeRTOS; here is the minimum, two tasks:
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 pends PendSV every 20 001 cycles. Both tasks are the same
assembly loop: it puts its own tag into r4–r11, spins 3000 iterations and
checks that the tag is intact. Then the same experiment with PendSV without stmdb and ldmia:
| PendSV | Code | Rounds A / B | Corrupted registers A / B |
|---|---|---|---|
saves r4–r11 |
38 bytes | 3955 / 3935 | 0 / 0 |
| does not save | 30 bytes | 3955 / 3936 | 1600 / 1592 |
Over 400 switches each task returned about 200 times, and each time
found its neighbour's tag in r4–r11: 200 × 8 = 1600. The round count, which
the tasks keep in r0–r3, is the same in both experiments: these registers, like
r12, lr, pc and xPSR, are saved by the processor on exception entry.
How to start the first task. The frame of the second task is assembled by hand as
if it had already been interrupted: eight words for what the processor will pop (in r0 —
the argument, in pc — the loop address, in xPSR — a one in the T bit, 0x01000000),
and below them eight words for r4–r11. The first task is started differently:
msr psp with its stack, CONTROL = 2 (thread mode runs on PSP), isb and
a branch into the loop. Without the T bit in xPSR the return from PendSV into the new task ends
in an INVSTATE fault — the same Thumb bit rule as in 7в.1.
PendSV — at the lowest priority. NVIC_SetPriority(PendSV_IRQn, 0xFF):
the function shifts the number into the significant bits, and the register ends up holding 0xF0. Then
the switch happens only when all other handlers have finished, and
r4–r11 hold the task's registers, not those of an interrupted handler. In the bench
the condition is met by itself: there are no other interrupts, all exceptions have
priority 0, and PendSV waits for SysTick to end. A separate experiment
(_verify/fixw2a3/pend7v) adds an interrupt with priority 0x80
that puts a tag into r4–r11 and runs longer than the tick period. PendSV
with priority 0 preempts its handler: all 99 of its runs found
foreign values in r4–r11 — 792 registers, and as many were corrupted in each
task. With 0xF0 — not a single one.
A real kernel adds to this masking through BASEPRI while choosing
the task (section 28) and, if the tasks compute on the FPU, the registers s16–s31
(section 7а).
7в.8 Pitfalls
A function from .S crashes when called through a pointer, but works directly.
.type name, %function is forgotten: the address is even, CFSR = 0x00020000 (INVSTATE).
printf("%lld") or "%f" prints garbage after a call from assembly.
The stack is not a multiple of 8: an odd number of words was pushed before bl. There is no error —
only wrong numbers.
A variable without an initial value is not zero. The start-up code does not
zero .bss. In the emulator RAM is zero at start-up, and the error is not visible; on
the board after a reset there are leftovers of earlier work.
A counter sometimes falls short. ++ on a variable shared with an interrupt is
three instructions. Use LDREX/STREX, atomic_fetch_add or a short cpsid i.
The race does not reproduce in the emulator. QEMU without -singlestep checks for
interrupts between blocks of instructions.
Tasks "see" each other's variables. PendSV does not save r4–r11, and
the compiler keeps local variables in them.
Registers get corrupted in interrupt handlers. PendSV has a higher priority than other interrupts and preempts them: it is given the lowest priority.
A task crashes with INVSTATE on the first switch to it. In the frame assembled
by hand xPSR = 0 instead of 0x01000000.
ldmia from an unaligned array is a UsageFault. For smlad through ldm
an int16_t array must be aligned to 4 (__attribute__((aligned(4)))).
A call to a function in RAM is suddenly slower. Every call goes through a linker veneer — an extra read from Flash memory.
7в.9 Summary
- A
.Sfile goes through the preprocessor; every function has.global,.type %function,.sizeand its own section. Without.typethe address is even, and a call through a pointer gives INVSTATE, although a directblworks. - The start-up code is seventeen instructions, 42 bytes: copying
.data, zeroing.bss,bl main. A warm restart checks this;.noinitsurvives a reset. - Before calling C the stack is a multiple of 8. Otherwise
va_argwith 64-bit numbers silently reads shifted by a word. BLreaches ±16 MB; to RAM the linker inserts the veneerldr.w pc, [pc].counter++against an interrupt lost 107 increments out of 177;LDREX/STREXandcpsid— not one. An exception clears the tag, andSTREXis retried.smladcomputes two pairs per instruction: 96 loop instructions instead of 320 on 64 samples.qadd16saturates but does not set theQflag.- PendSV must save
r4–r11: without two instructions, 400 switches corrupted 1600 registers.
Full sources
start7v.S — the start-up code entirely in assembly: the vector table, Reset_Handler, entry into the fault handler and PendSV switching two tasks.
/* Раздел 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 — assembly functions for the section's experiments: .type, calling C with the stack, LDREX/STREX, SMLAD and saturating additions, the task loop.
/* Раздел 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
Next: section 8 "Input/output ports (GPIO)".