12б. Firmware self-test: RAM, Flash, clocks and the core during operation
The watchdog from section 12 catches only a hung program. A program that keeps running but works with a corrupted RAM cell, a bad byte in Flash memory, a crystal of the wrong frequency or a stuck register bit feeds the watchdog faithfully — and faithfully produces a wrong result. A self-test is the part of the firmware that looks for such failures itself: all at once at startup, during operation piece by piece. For class B software in household appliance controls such checks are mandatory; there are no clauses of requirements here — only the checks themselves, and each one was calculated or run.
Section 36б already compared RAM marches by fault model, went through
the piece-by-piece transparent test and the frequency check on the G474. Here is what was
left off-screen there, on the F103 and F407: a startup march in assembler right
in Reset_Handler, a transparent march on the data itself with no separate
restore of the contents, a core register test — built and run with an injected
fault — a stack canary, a CRC reference written into the ELF itself, a check of
the CRC block itself, an internal reference inside the F407 for the timer and a time budget based on
instructions counted in QEMU.
Prerequisites: reference manuals RM0008 (F1: chapter 4 "CRC", AFIO_MAPR,
FLASH_ACR) and RM0090 (F4: chapter 4 "CRC", 6.2.7 — CSS, 6.2.11 — clock
measurement with TIM5 and TIM11, 18.4.20 — TIM5_OR), datasheets DS8626 (F407)
and DocID13587 (F103x8/B), headers stm32f10x.h and stm32f407xx.h,
sections 12 (watchdog, CRC block), 7а (exception frame), 35в (the .noinit area),
203.5 (firmware against interference) and 204.6 (fault log). The code was built with
arm-none-eabi-gcc 12.2 for the F103 and F407 and run in QEMU; the logic of the march, CRC
and frequency window was run on a PC against a model of faulty memory.
Terms used in the documentation
Term Meaning data background what the memory is filled with during the march fault injection breaking something on purpose so the check fires checkerboard pattern 0xAAAAAAAA and 0x55555555 in alternate registers stack painting filling the stack with a pattern to find later how deep it went modified immediate constant a constant that Thumb-2 encodes directly in the instruction timer input remap (TI4_RMP) routing a timer input to an internal signal redundant variable (value and inverse) a variable kept twice: the value and its inverse time budget per pass the time allowed for one pass of the main loop
12б.1 What the self-test catches and in what order
| Failure | How it shows | What catches it | When |
|---|---|---|---|
| a RAM cell bit stuck, two cells coupled | a variable changes by itself or does not change | the March C− test | at startup all RAM, during operation in blocks |
| a Flash byte corrupted | the wrong constant or instruction | CRC-32 of the image against a reference | at startup the whole image, during operation 256 words at a time |
| crystal of the wrong frequency, PLL error | timers, UART and ADC are wrong, the program runs | TIM5 count over 8 LSE periods | at startup and once every few passes |
| a core register or flag bit | wrong calculations | checkerboard patterns in assembler | at startup and once per RAM round |
| the stack outgrew its area | data below the stack is corrupted | a canary at the bottom of the stack | in every pass |
| the state of the check itself is corrupted | the check "walks" to the wrong place | the position is stored twice | in every step |
Each check needs a place in memory that it will not trample itself. The F103 memory map after linking:
The order at startup:
- The watchdog first (section 12): if the check itself hangs, the chip still restarts.
- All RAM — in
Reset_Handler, before any C code (12б.2). - Core registers and flags (12б.6).
- The CRC block, then the CRC of the whole image against the reference (12б.4).
- The clock — HSE against LSE (12б.5).
- Stack painting and the canary (12б.6) — in the startup code, before
main.
During operation each pass of the main loop takes a small step of each check, and the watchdog is fed only if all the steps agreed (12б.6).
12б.2 RAM: the startup march and the transparent march during operation
At startup the RAM holds nothing yet, and the march may erase it. But
it cannot be written in C: a C function pushes the return address and
registers onto the stack, and the march would erase them along with the rest of the RAM. So the march is in
assembler, using only registers r0–r3 and r12, which under the calling
convention may be clobbered without saving. It does not need a stack at all:
march_startup:
mov r12, #0 @ накопитель ошибок
movs r3, #0
mov r2, r0
1: str r3, [r2], #4 @ ⇕(w0)
cmp r2, r1
blo 1b
mov r2, r0
2: ldr r3, [r2] @ ⇑(r0, w1)
orr r12, r12, r3
mvn r3, #0
str r3, [r2], #4
cmp r2, r1
blo 2b
mov r2, r0
3: ldr r3, [r2] @ ⇑(r1, w0)
orn r12, r12, r3
movs r3, #0
str r3, [r2], #4
cmp r2, r1
blo 3b
mov r2, r1
4: ldr r3, [r2, #-4]! @ ⇓(r0, w1)
orr r12, r12, r3
mvn r3, #0
str r3, [r2]
cmp r2, r0
bhi 4b
mov r2, r1
5: ldr r3, [r2, #-4]! @ ⇓(r1, w0)
orn r12, r12, r3
movs r3, #0
str r3, [r2]
cmp r2, r0
bhi 5b
mov r2, r0
6: ldr r3, [r2], #4 @ ⇕(r0)
orr r12, r12, r3
cmp r2, r1
blo 6b
mov r0, r12
bx lr
.size march_startup, . - march_startup
Per word — 10 memory accesses, as March C− requires:
1 + 2 + 2 + 2 + 2 + 1. Instructions — 3 + 6 + 6 + 6 + 6 + 4 = 31 per word.
It is called from Reset_Handler — the very first thing after reset:
Reset_Handler:
ldr r2, =0xE000E010 @ SysTick
ldr r3, =0x00FFFFFF
str r3, [r2, #4] @ RVR
str r3, [r2, #8] @ CVR: любая запись обнуляет
movs r3, #5
str r3, [r2] @ CSR: такт ядра, счёт
ldr r0, =_sram
ldr r1, =_estack @ до .noinit: запись об отказе переживает сброс
bl march_startup
cbnz r0, boot_fail
ldr r2, =0xE000E018
ldr r0, [r2] @ остаток SysTick — в c_start
bl c_start
boot_fail:
b boot_fail @ ОЗУ неисправно: C не запускать, выходы в покое, ждать сторожа
.size Reset_Handler, . - Reset_Handler
The upper bound is _estack, that is, the start of the .noinit area (35в): the fault
record survives not only a reset but the march too. SysTick is here only for
the experiment: in QEMU with -icount every instruction costs the same virtual time,
and the counter gives the number of instructions executed. The run: on the F1 the march over 2032
words took 63 007 instructions, on the F4 over 32 752 words (128 KB without
the 64 bytes of .noinit) — 1 015 328, 31.0 per word. Hence lower
bounds — each instruction takes at least one cycle, and F103 Flash at 72 MHz also
has two wait states (RM0008):
| Chip | RAM | Words | Instructions | At least |
|---|---|---|---|---|
| F103, 72 MHz | 20 KB | 5120 | 158 720 | 2.2 ms |
| F407, 168 MHz | 128 KB (112 + 16) | 32 768 | 1 015 808 | 6.0 ms |
| F407, 168 MHz | CCM, 64 KB | 16 384 | 507 904 | 3.0 ms |
The CCM memory of the F407 lies at a different address (0x10000000, datasheet DS8626),
and it needs a second call of march_startup with its own bounds.
There is no bad memory in QEMU, but there is a register some of whose bits always
read as zero: in VTOR bits 6:0 are zeros. The march over this one "word"
returned 0x0000007F: seven bits stuck at zero were found.
During operation the RAM is in use. A transparent march does not erase the contents but walks
over the data itself: instead of writing zeros — copy word a to a buffer, instead of
"write ones" — write ~a, instead of "read zeros" — compare
with a:
uint32_t march_own(volatile uint32_t *p, uint32_t *s, uint32_t n)
{
uint32_t i, err = 0u;
for (i = 0u; i < n; i++) { s[i] = ST_RD(&p[i]); } /* копия: a */
for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i]; ST_WR(&p[i], ~s[i]); } /* ⇑(ra, w~a) */
for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); } /* ⇑(r~a, wa) */
for (i = n; i-- > 0u;) { err |= ST_RD(&p[i]) ^ s[i]; ST_WR(&p[i], ~s[i]); } /* ⇓(ra, w~a) */
for (i = n; i-- > 0u;) { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); } /* ⇓(r~a, wa) */
for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i]; } /* ⇕(ra) */
return err;
}
Section 36б has a different transparent test: save a piece, run an ordinary
march of zeros and ones over it and restore what was saved. Here there is nothing
to restore: the last write to each word is its own a, the contents are in place
by themselves. The word under test gets the same 10 accesses (1 + 8 + 1), the buffer another
6: one write and five reads. According to the F103 disassembly the loops take
4 + 8 + 7 + 9 + 7 + 6 = 41 instructions per word.
A march over all RAM at once would occupy the processor for milliseconds, so it goes in blocks of 16 words, one block per pass:
int ram_step(volatile uint32_t *base, uint32_t words, uint32_t *wrapped)
{
uint32_t n = RAM_BLOCK, err, m, p = pos;
if ((p ^ pos_inv) != 0xFFFFFFFFu || p >= words)
return ST_STATE; /* испорчено само состояние проверки */
if (p + n > words)
n = words - p;
m = irq_save(); /* посреди марша блок не должен видеть никто */
err = march_own(base + p, save, n);
irq_restore(m);
*wrapped = (p + n >= words);
p = *wrapped ? 0u : p + n - 1u; /* блоки перекрываются на одно слово */
pos = p;
pos_inv = ~p;
ram_bits = err;
return err ? ST_RAM : ST_OK;
}
Three rules. Interrupts are disabled for the block: a handler that read
a variable in the middle of the march would see ~a; PRIMASK is restored, not
enabled (36б.4). Blocks overlap by one word — otherwise a coupling between
two neighbouring words at the joint is seen by no block. The position is stored
twice, as a value and its inverse: if noise corrupts pos, the check will not
silently wander into someone else's memory — ram_step returns ST_STATE. pos itself,
the save[] buffer and the result ram_bits live in their own .st_ram section outside
the area under test — this is visible in the figure. The buffer holds nothing
between steps, so once per round it is checked by the ordinary march_startup march.
Measurement in QEMU: a block of 16 words is 715 instructions on the F1 and 713 on the F4 including
the call, the march over the buffer is 518 instructions. The block runs with
interrupts disabled, and the response to an interrupt may be late by this time: on the F103
at 72 MHz at least 9.9 µs. The area in the experiment is 1025 words (.bss
with the application array): a full round is 69 steps, the checksum of
the area after the round is the same.
For the 20 KB of the F103 a round is 5120 words: 1 + 15 × 342 ≥ 5120, that is 342 steps, 244 530 instructions, at least 3.4 ms of processor time per round. This time is spread over 342 passes of the main loop.
12б.3 RAM on the PC: injected faults
The firmware march code was built on the PC without changes: RAM accesses in it
go through ST_RD and ST_WR, which on the chip are ordinary reads and writes,
and on the PC a model of faulty memory of 64 words with random contents.
Exactly one fault is injected into the model, then three tests are run:
the transparent march on its own data in full, the same march in blocks — a full round of
ram_step, as in the firmware — and the startup march (a C port of the
assembler march_startup with the same access order).
| Fault | Cases | Transparent march in full | Blocks of 16, a round | Startup march |
|---|---|---|---|---|
| bit stuck at 0 or 1 | 4096 | 100 % | 100 % | 100 % |
| bit does not go 0→1 or 1→0 | 4096 | 100 % | 100 % | 100 % |
| two addresses select one cell | 4032 | 100 % | 24 % | 100 % |
| a transition in a word flips a bit in a distant word | 2000 | 100 % | 24 % | 100 % |
| a transition in a word sets a bit of a distant word to 0 or 1 | 2000 | 100 % | 23 % | 100 % |
| while a bit is in state s, a bit of a distant word is held | 2000 | 100 % | 64 % | 100 % |
| the same, flip, neighbouring words | 2000 | 100 % | 100 % | 100 % |
| the same, set, neighbouring words | 2000 | 100 % | 100 % | 100 % |
| the same, hold, neighbouring words | 2000 | 100 % | 100 % | 100 % |
| a transition flips a bit of the same word | 2000 | 100 % | 100 % | 100 % |
| a transition sets a bit of the same word to 0 or 1 | 2000 | 50 % | 50 % | 50 % |
On good memory all three tests are silent, and the contents after the transparent march and after the round of blocks are the same as before. The blocks start at words 0, 15, 30, 45, 60 — the one-word overlap shows in the step of 15.
What the table shows. Stuck bits and transitions are caught by all three tests in every case. Coupling of neighbouring words is caught by the blocks completely — that is what the overlap is for. Coupling of distant words and an address decoder fault the blocks see only if both words fell into one block: about a quarter of cases for flip, set and address fault, 64 % for hold. So the startup march is always run: only it sees all the RAM at once. Coupling of two bits of one word, when a transition sets the neighbouring bit to 0 or 1, is caught in this model in about half the cases by all three — the background of the data itself did not help here; a flip of a bit of the same word is always seen. For coupling inside a word the march is repeated with other backgrounds (36б.2).
Corruption of the state itself was also caught: a bit of pos_inv was flipped —
ram_step returned 8 (ST_STATE).
12б.4 Flash memory: the reference in the ELF and a check of the block itself
RM0008 and RM0090 describe the purpose of the CRC block exactly so: a program signature computed at run time is compared with a reference computed at build time. The reference must be in a known place. Here its place is the last word of the image, and the linker script reserves it:
.crc_ref : /* эталон: сразу за образом, пока — 0xFFFFFFFF */
{
__crc_ref = .;
LONG(0xFFFFFFFF)
} > FLASH
For now it holds 0xFFFFFFFF. A second linker check prevents this word
from moving away from the end of the image:
ASSERT(__crc_ref == LOADADDR(.data) + SIZEOF(.data), "CRC reference must follow the image")
After the build the script patch12b.py computes CRC-32/MPEG-2 the same way as the CRC
block of the F1 and F4 (words least significant byte first, start 0xFFFFFFFF, no
reflections; 12.5), and writes it into the ELF itself:
elf = sys.argv[1]
nm = subprocess.run(["arm-none-eabi-nm", elf], capture_output=True, text=True, check=True).stdout
ref_addr = int([l.split()[0] for l in nm.splitlines() if l.endswith(" __crc_ref")][0], 16)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf + ".raw"], check=True)
raw = open(elf + ".raw", "rb").read()
n = ref_addr - ORIGIN
if len(raw) != n + 4 or n % 4 or raw[n:] != b"\xff\xff\xff\xff":
sys.exit("образ не тот: длина %d, __crc_ref на смещении %d" % (len(raw), n))
crc = crc_mpeg2(raw[:n])
open(elf + ".ref", "wb").write(struct.pack("<I", crc))
subprocess.run(["arm-none-eabi-objcopy", "--update-section", ".crc_ref=" + elf + ".ref", elf], check=True)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf[:-4] + ".bin"], check=True)
out = open(elf[:-4] + ".bin", "rb").read()
assert out[:n] == raw[:n] and struct.unpack("<I", out[n:])[0] == crc
print("crc_ref 0x%08X at 0x%08X, image %d bytes = %d words" % (crc, ref_addr, n, n // 4))
Why into the ELF rather than appending a word to the end of the .bin (as done in 36б.5):
the debugger and the IDE flash the ELF. A reference that lives only in the .bin
will not reach a board flashed from the IDE, and the startup check will
fail every time. --update-section changes the section contents in place:
addresses and other bytes stay the same, and the script checks this.
An F1 run in QEMU: an image of 857 words (3428 bytes), reference 0xD01F545B
at address 0x08000D64; at startup the CRC matched. Then in a copied ELF
one bit in the constant table app_table was flipped — the startup CRC
came out as 0xE41DDB8D, and the firmware went into the safe state with code 3.
The CRC block itself is also checked. If its clock did not come on or the block
is faulty, CRC_DR returns the same value every time, and the check compares garbage
with garbage. So before the image comes a known answer:
int crc_unit_ok(void) /* сам блок: сброс и одно известное слово */
{
RCC_CRCEN_REG |= RCC_CRCEN;
CRC_CR = CRC_CR_RESET;
if (CRC_DR != 0xFFFFFFFFu)
return 0;
CRC_DR = 0x12345678u;
return CRC_DR == 0xDF8A8A2Bu;
}
The answer 0xDF8A8A2B for the word 0x12345678 was recomputed by the section's check itself.
QEMU has no CRC block — crc_unit_ok returned 0, as on a faulty chip.
So the QEMU build uses the SOFT_CRC option — the same CRC variant
in software; in the product build a zero answer leads to the safe state.
During operation — 256 words per pass:
int flash_step(const uint32_t *img, uint32_t words, uint32_t ref) /* в работе: по FLASH_STEP слов */
{
uint32_t i = fpos, end;
if ((i ^ fpos_inv) != 0xFFFFFFFFu || i >= words)
return -2; /* испорчено состояние проверки */
if (i == 0u)
CRC_RESET();
end = (words - i > FLASH_STEP) ? i + FLASH_STEP : words;
for (; i < end; i++)
CRC_FEED(img[i]);
if (i < words) {
fpos = i;
fpos_inv = ~i;
return 0; /* круг не кончился */
}
fpos = 0u;
fpos_inv = ~0u;
return (CRC_READ() == ref) ? 1 : -1; /* круг кончился: сошлось или нет */
}
The feed loop is 5 instructions per word (F103 and F407 disassembly), and the block itself
according to the RM spends 4 AHB bus cycles per word and holds off the next write until
the calculation ends. The lower bound for a step is 256 × 5 = 1280 cycles: 17.8 µs at
72 MHz. The whole 64 KB Flash of the F103 is 64 steps, at least 1.14 ms;
the 1 MB of the F407 is 1024 steps, at least 7.8 ms at 168 MHz. Only the image
up to __crc_ref needs checking, not all Flash memory: in the experiment that is 857 words.
On the PC — an image of 1024 words:
- each of the 4096 bytes in turn corrupted with a random value — caught 4096 of 4096;
- two random bytes at once, 20 000 times — 20 000 caught;
- the CRC over the image together with its reference is
0x00000000: this is how a CRC without a final inversion behaves; - a round is 4 steps. Corruption in the already covered part in the middle of a round: this round answered 1 (matched), the next one −1. The detection delay is up to two rounds;
- a corrupted position
fpos_inv— answer −2.
12б.5 The clock: HSE against LSE inside the F407
CSS (section 6) catches only HSE stopping: according to RM0090 the system switches to HSI, the PLL fed from HSE is switched off, an NMI arrives. A crystal of the wrong frequency, an error in the PLL dividers or a frequency that has drifted by a few percent is, for CSS, a healthy clock. Such a frequency can only be seen by comparison with an independent oscillator.
The F407 has an internal path for this: the input of channel 4 of timer TIM5
is switched by the TI4_RMP bits of the TIM5_OR register (offset 0x50):
01 — LSI, 10 — LSE, 11 — the RTC wakeup interrupt (RM0090, 18.4.20).
TIM5 is 32-bit, so the difference between captures will not wrap:
uint32_t clk_measure_f407(uint32_t ti4_rmp) /* 1 — LSI, 2 — LSE; вывод не нужен */
{
uint32_t c1 = 0u, cap = 0u, k, t;
RCC_APB1ENR |= RCC_TIM5EN;
TIM5_OR = ti4_rmp << TIM5_OR_TI4_RMP_Pos; /* TI4_RMP: опора на вход канала 4 внутри камня */
TIM5_PSC = 0u;
TIM5_ARR = 0xFFFFFFFFu; /* TIM5 32-битный: разность не завернётся */
TIM5_CCMR2 = TIM_CCMR2_CC4S_TI4 | TIM_CCMR2_IC4PSC_8;
TIM5_CCER = TIM_CCER_CC4E;
TIM5_SR = 0u;
TIM5_CR1 = TIM_CR1_CEN;
for (k = 0u; k < 3u; k++) { /* первый захват — не с начала периода, его отбросить */
for (t = 0u; (TIM5_SR & TIM_SR_CC4IF) == 0u; t++) {
if (t > CLK_WAIT) {
TIM5_CR1 = 0u;
return 0u;
}
}
cap = TIM5_CCR4; /* чтение CCR4 снимает CC4IF */
if (k == 1u)
c1 = cap;
}
TIM5_CR1 = 0u;
return cap - c1; /* тактов TIM5 за 8 периодов опоры */
}
The F103 has no such path: the TIM5CH4_IREMAP bit in AFIO_MAPR (LSI to the TIM5
input) according to RM0008 exists only on part of the family, and the F103C8 has no TIM5 at all —
the reference is brought in by a wire (36б.6).
The window. TIM5 sits on APB1. With an 8 MHz HSE and 168 MHz on the core, APB1 is 42 MHz, and the timers on it, with an APB divider greater than 1, are clocked twice as fast (RM0090): 84 MHz. Over 8 LSE periods that is 20 507.81 cycles; one quantisation step is 48.8 ppm. The window is computed at build time by a macro:
#define CLK_LO(FT, F_MAX, N, TOL_PPM) \
((uint32_t)((unsigned long long)(FT) * (1000000ull - (TOL_PPM)) * (N) / ((F_MAX) * 1000000ull)) - 1u)
#define CLK_HI(FT, F_MIN, N, TOL_PPM) \
((uint32_t)(((unsigned long long)(FT) * (1000000ull + (TOL_PPM)) * (N) + (F_MIN) * 1000000ull - 1u) \
/ ((F_MIN) * 1000000ull)) + 1u)
A tolerance of 0.5 % is this section's choice: it accommodates the drift of the crystal and the reference with a large margin. RM0090 says about the accuracy of a watch crystal only "typically a few tens of ppm", and the model takes LSE as ±50 ppm. LSI on the F407 is 17…47 kHz according to the datasheet. Windows: for LSE 20 404…20 612, for LSI 14 225…39 729. A run on the PC — the count at every value of the reference within its spread, with ±1 quantisation:
| TIM5 clock | LSE window | LSI window |
|---|---|---|
| healthy, nominal | silent | silent |
| healthy, −0.4 % | silent | silent |
| healthy, +0.4 % | silent | silent |
| HSE lost, CSS switched to HSI: 8 MHz | catches | catches |
| ×0.5 | catches | may miss |
| ×0.98 | catches | may miss |
| ×0.99 | catches | may miss |
| ×0.995 | does not catch | does not catch |
| ×1.01 | catches | may miss |
| ×1.02 | catches | may miss |
| ×1.5 | catches | may miss |
| ×2 | catches | may miss |
"May miss" means that for some permissible LSI the count of the faulty clock falls into the window. The LSI spread is almost threefold (47 / 17), so the LSI window does not guarantee even the gross ×0.5 and ×2. For a coarse check (−50/+100 %) RM0090 proposes the reverse path — TIM11 counts HSE divided for the RTC while clocked from HSI; only LSE gives an accurate check.
In QEMU input capture is not modelled: clk_measure_f407 did not see a single
edge and returned 0, and the firmware went into the safe state with code 5. That is
as it should be: a silent reference is a failure too.
12б.6 Core registers, the stack, the schedule and the failure
Core registers. "All registers" cannot be checked from C: the compiler itself
decides which ones to use. The test is in assembler: even registers get
0xAAAAAAAA, odd ones 0x55555555, then the other way round. Thus every bit
of every register is both zero and one, and neighbouring registers hold
opposite values. Both constants are Thumb-2 "modified" constants,
they fit directly into the mov and cmp instructions:
.macro FILL a, b
mov r0, #\a
mov r1, #\b
mov r2, #\a
mov r3, #\b
mov r4, #\a
mov r5, #\b
mov r6, #\a
mov r7, #\b
mov r8, #\a
mov r9, #\b
mov r10, #\a
mov r11, #\b
mov r12, #\a
mov lr, #\a
.endm
CHECK — the same 14 registers through cmp and bne 9f. The function itself:
cpu_reg_test:
push {r4-r11, lr}
FILL 0xAAAAAAAA, 0x55555555
CHECK 0xAAAAAAAA, 0x55555555
FILL 0x55555555, 0xAAAAAAAA
#ifdef INJECT_REG
eor r5, r5, #0x10 @ опыт: бит 4 регистра R5 «залип»
#endif
CHECK 0x55555555, 0xAAAAAAAA
mov r0, #0xAAAAAAAA @ PSP: биты 1:0 всегда нули
bic r0, r0, #3
msr psp, r0
mrs r1, psp
cmp r1, r0
bne 9f
mvn r0, r0
bic r0, r0, #3
msr psp, r0
mrs r1, psp
cmp r1, r0
bne 9f
mov r0, #0xA8000000 @ APSR: N, C, Q — затем Z, V
msr APSR_nzcvq, r0
mrs r1, APSR
and r1, r1, #0xF8000000
cmp r1, r0
bne 9f
mov r0, #0x50000000
msr APSR_nzcvq, r0
mrs r1, APSR
and r1, r1, #0xF8000000
cmp r1, r0
bne 9f
movs r0, #0
msr psp, r0
pop {r4-r11, pc}
9: movs r0, #1
pop {r4-r11, pc}
.size cpu_reg_test, . - cpu_reg_test
In PSP bits 1:0 are always zero, so the patterns for it are 0xAAAAAAA8
and 0x55555554. The APSR flags are checked in two sets: N, C, Q, then
Z, V. The function is 398 bytes of code, in QEMU 114 instructions, on the F103
at 72 MHz at least 1.6 µs. The fault injection experiment: with the
INJECT_REG option bit 4 in r5 is flipped after the second fill —
the test returned 1, and the firmware went into the safe state with code 6 on both the F1
and the F4. Not checked are MSP (it is in use) and the FPU registers of the F407 —
S0–S31 are checked the same way, with vmov instructions; this is not built here.
The stack. The startup code paints the stack with a pattern and puts a canary at the bottom:
void stack_paint(void) /* один раз из стартового кода, до main */
{
uint32_t *p = _sstack, *sp;
__asm volatile("mov %0, sp" : "=r"(sp));
for (uint32_t i = 0u; i < CANARY_WORDS; i++)
*p++ = STACK_CANARY;
while (p < sp - 16) /* 64 байта под своим кадром не трогать */
*p++ = STACK_FILL;
}
int stack_canary_ok(void) /* в каждом проходе: четыре слова */
{
for (uint32_t i = 0u; i < CANARY_WORDS; i++)
if (_sstack[i] != STACK_CANARY)
return 0;
return 1;
}
The canary is checked in every pass — 27 instructions. The depth from the paint
(stack_free_bytes) is checked occasionally, for the log. In the experiment, after all the checks
880 bytes of the stack out of 1008 remained untouched. With the
INJECT_STACK option a function with a large array reached the bottom of the stack — the very first
pass returned ST_STACK, code 7. The canary catches an overflow after the fact;
the very first write past the boundary is caught by the MPU (11а). Leave headroom for an exception
frame: 32–36 bytes on Cortex-M3 and 104–108 bytes with the FPU on the F407 (7а) —
the paint shows an interrupt only if it came at the deepest moment.
The schedule. One pass of the main loop:
static int st_pass(void) /* один проход главного цикла */
{
uint32_t wrapped = 0u;
int r;
if (!stack_canary_ok())
return ST_STACK;
r = ram_step(RAM_BASE, RAM_WORDS, &wrapped);
if (r != ST_OK)
return r;
if (wrapped) { /* раз за круг ОЗУ: буфер и регистры ядра */
r = ram_buf_test();
if (r != ST_OK)
return r;
if (cpu_reg_test() != 0u)
return ST_CPU;
}
r = flash_step(IMG, IMG_WORDS, *__crc_ref);
if (r < 0)
return (r == -1) ? ST_FLASH : ST_STATE;
return ST_OK; /* только здесь кормят сторож */
}
The budget of a pass — lower bounds from the instructions counted in QEMU and from the CRC loop:
| Step | Instructions | F103, 72 MHz | F407, 168 MHz |
|---|---|---|---|
| canary | 27 | 0.4 µs | 0.2 µs |
| RAM block, 16 words | 715 / 713 | 9.9 µs | 4.2 µs |
| CRC step, 256 words | 1280 cycles | 17.8 µs | 7.6 µs |
| once per round: buffer and registers | 518 + 114 | 8.8 µs | 3.8 µs |
An ordinary F103 pass is at least 28 µs: with one pass per millisecond that is from 2.8 % of the processor. A RAM round over 20 KB is 342 passes, 0.34 s; a CRC round over 64 KB is 64 passes. On the F407 a round over 128 KB of RAM is 2185 passes, over 1 MB of Flash 1024. In QEMU 2000 passes in a row went through without a failure.
The failure. Any check leads to one place:
void st_fail(uint32_t code, uint32_t detail) /* единственный выход при любом отказе */
{
OUT_ODR = 0u; /* силовые выходы — выключить первым делом */
if (st_rec.check != ~(st_rec.code ^ st_rec.detail ^ st_rec.count))
st_rec.count = 0u; /* записи нет или она битая: начать счёт заново */
st_rec.code = code;
st_rec.detail = detail;
st_rec.count++;
st_rec.check = ~(code ^ detail ^ st_rec.count);
puts1("SAFE code ");
putu(code);
puts1(" detail ");
puthex(detail);
puts1(" count ");
putu(st_rec.count);
puts1("\n");
for (;;) { /* сторож не кормится: сброс через его таймаут */
}
}
The outputs are switched off first of all. The fault record goes to .noinit with a check
word: the startup march does not touch it, and after a reset the firmware sees
what happened and how many times. On a repeated failure — do not restart round and round,
but stay in the safe state (36б.6). The record also goes to the device log
(204.6): when a unit comes back, together with the production log line (201.7) it
turns "does not work" into a diagnosis.
12б.7 Pitfalls
The startup march erased the fault record. If the march goes over all RAM,
.noinit after a reset is zeros, and the device "sees" the first failure every time.
The march boundary is _estack, the start of .noinit.
The startup march in C. A C function saves the return address and registers on the stack, the march erases them — a HardFault instead of a report. Only assembler and only registers that may be clobbered.
The reference only in the .bin. The ELF was flashed from the IDE — the reference word
holds 0xFFFFFFFF, and the check fails at every start. The reference is
written into the ELF, and the .bin is made from that.
The QEMU build went into the product. The SOFT_CRC option disables the failure on
a CRC block that does not answer. In a product without the block the check compares garbage
with garbage — crc_unit_ok must lead to the safe state.
Flash corruption was seen a round later. A byte was corrupted in the already covered part — the current round matched. The detection delay is up to two CRC rounds, and that is what goes into the reaction time.
Self-test state inside the area under test. If pos lies
in .bss, the march drives zeros and ones through it while also
using it. The self-test's own data goes into a separate section outside the run-time march,
and the position is kept as a value and its inverse.
The LSI window is "normal", but the crystal is wrong. On the F407 the LSI window lets through both ×0.5 and ×2 — LSI only shows the loss of HSE, which CSS catches anyway.
The march buffer on the stack. A local array instead of save[] in .st_ram
eats the stack and moves from the checked buffer into the unchecked stack.
12б.8 Summary
- At startup: the watchdog, a march over all RAM in
Reset_Handlerin assembler (10 accesses and 31 instructions per word; 20 KB of the F103 at least 2.2 ms, 128 KB of the F407 6.0 ms), core registers, the CRC block, the image CRC, the clock. - During operation: a transparent March C− on the data itself in blocks of 16 words with a one-word overlap — 715 instructions per block, at least 9.9 µs with interrupts disabled on the F103; a 20 KB round is 342 passes.
- On the PC the march caught all stuck bits, transitions, address faults and word couplings; the blocks catch coupling of neighbouring words completely, of distant ones about a quarter, so the full march at startup is mandatory.
- Image CRC: the reference is the last word,
patch12b.pywrites it into the ELF; the block itself is checked with the answer0xDF8A8A2B; all single and double bad bytes were caught; a 256-word step is at least 17.8 µs; the delay is up to two rounds. - F407 clock: TIM5 CH4 from LSE through
TI4_RMP, a ±0.5 % window catches a 1 % drift; an LSI window (17…47 kHz) does not guarantee even ×0.5 and ×2. - Registers: checkerboard patterns in R0–R12 and LR, PSP, flags — 114 instructions; a stuck bit of R5 was caught. Stack: the canary is 27 instructions per pass.
- Failure: switch off the outputs, write to
.noinitwith a check word, do not feed the watchdog; the watchdog is fed only after a pass where everything agreed.
Full sources
st12b.h — the common self-test header.
/* Раздел 12б: самопроверка прошивки — ОЗУ, флеш-память, такт, ядро и стек. */
#ifndef ST12B_H
#define ST12B_H
#include <stdint.h>
#ifndef ST_RD /* на ПК обращения к ОЗУ подменяет модель неисправной памяти */
#define ST_RD(p) (*(p))
#define ST_WR(p, v) (*(p) = (v))
#endif
#define RAM_BLOCK 16u /* слов в блоке прозрачного марша */
#define FLASH_STEP 256u /* слов CRC за шаг */
enum { ST_OK = 0, ST_RAM = 1, ST_BUF = 2, ST_FLASH = 3, ST_CRCUNIT = 4,
ST_CLOCK = 5, ST_CPU = 6, ST_STACK = 7, ST_STATE = 8 };
/* ОЗУ */
uint32_t march_own(volatile uint32_t *p, uint32_t *s, uint32_t n);
void ram_reset(void);
int ram_step(volatile uint32_t *base, uint32_t words, uint32_t *wrapped);
int ram_buf_test(void);
extern uint32_t ram_bits;
/* флеш-память */
uint32_t crc_soft(uint32_t crc, uint32_t word);
int crc_unit_ok(void);
uint32_t flash_crc(const uint32_t *img, uint32_t words);
void flash_reset(void);
int flash_step(const uint32_t *img, uint32_t words, uint32_t ref);
/* такт */
int clk_in_window(uint32_t count, uint32_t lo, uint32_t hi);
uint32_t clk_measure_f407(uint32_t ti4_rmp);
/* ядро и стек (st12b_asm.S, st12b_stack.c) */
uint32_t cpu_reg_test(void);
uint32_t march_startup(uint32_t *start, uint32_t *end);
void stack_paint(void);
int stack_canary_ok(void);
uint32_t stack_free_bytes(void);
/* окно счёта таймера за n периодов опоры (F_MIN…F_MAX Гц), такт таймера FT Гц
* с допуском TOL_PPM; ±1 — на квантование захвата. Считается при сборке. */
#define CLK_LO(FT, F_MAX, N, TOL_PPM) \
((uint32_t)((unsigned long long)(FT) * (1000000ull - (TOL_PPM)) * (N) / ((F_MAX) * 1000000ull)) - 1u)
#define CLK_HI(FT, F_MIN, N, TOL_PPM) \
((uint32_t)(((unsigned long long)(FT) * (1000000ull + (TOL_PPM)) * (N) + (F_MIN) * 1000000ull - 1u) \
/ ((F_MIN) * 1000000ull)) + 1u)
#endif
st12b_asm.S — the RAM march without a stack and the core register test in assembler.
/* Раздел 12б: то, что нельзя написать на C, — марш ОЗУ без стека и тест регистров ядра. */
.syntax unified
.thumb
.text
/* uint32_t march_startup(uint32_t *start, uint32_t *end)
* March C− по [start, end) словами; область не пустая. Стек не нужен: только r0–r3 и r12,
* поэтому годится и до инициализации ОЗУ, и из C. Возврат: 0 — исправно, иначе — биты ошибок. */
.global march_startup
.type march_startup, %function
.thumb_func
march_startup:
mov r12, #0 @ накопитель ошибок
movs r3, #0
mov r2, r0
1: str r3, [r2], #4 @ ⇕(w0)
cmp r2, r1
blo 1b
mov r2, r0
2: ldr r3, [r2] @ ⇑(r0, w1)
orr r12, r12, r3
mvn r3, #0
str r3, [r2], #4
cmp r2, r1
blo 2b
mov r2, r0
3: ldr r3, [r2] @ ⇑(r1, w0)
orn r12, r12, r3
movs r3, #0
str r3, [r2], #4
cmp r2, r1
blo 3b
mov r2, r1
4: ldr r3, [r2, #-4]! @ ⇓(r0, w1)
orr r12, r12, r3
mvn r3, #0
str r3, [r2]
cmp r2, r0
bhi 4b
mov r2, r1
5: ldr r3, [r2, #-4]! @ ⇓(r1, w0)
orn r12, r12, r3
movs r3, #0
str r3, [r2]
cmp r2, r0
bhi 5b
mov r2, r0
6: ldr r3, [r2], #4 @ ⇕(r0)
orr r12, r12, r3
cmp r2, r1
blo 6b
mov r0, r12
bx lr
.size march_startup, . - march_startup
/* uint32_t cpu_reg_test(void)
* Шахматные шаблоны в R0–R12 и LR (чётные и нечётные — противоположные), затем наоборот;
* PSP и флаги APSR. Возврат: 0 — исправно, 1 — отказ. Регистры R4–R11 сохраняются. */
.macro FILL a, b
mov r0, #\a
mov r1, #\b
mov r2, #\a
mov r3, #\b
mov r4, #\a
mov r5, #\b
mov r6, #\a
mov r7, #\b
mov r8, #\a
mov r9, #\b
mov r10, #\a
mov r11, #\b
mov r12, #\a
mov lr, #\a
.endm
.macro CHECK a, b
cmp r0, #\a
bne 9f
cmp r1, #\b
bne 9f
cmp r2, #\a
bne 9f
cmp r3, #\b
bne 9f
cmp r4, #\a
bne 9f
cmp r5, #\b
bne 9f
cmp r6, #\a
bne 9f
cmp r7, #\b
bne 9f
cmp r8, #\a
bne 9f
cmp r9, #\b
bne 9f
cmp r10, #\a
bne 9f
cmp r11, #\b
bne 9f
cmp r12, #\a
bne 9f
cmp lr, #\a
bne 9f
.endm
.global cpu_reg_test
.type cpu_reg_test, %function
.thumb_func
cpu_reg_test:
push {r4-r11, lr}
FILL 0xAAAAAAAA, 0x55555555
CHECK 0xAAAAAAAA, 0x55555555
FILL 0x55555555, 0xAAAAAAAA
#ifdef INJECT_REG
eor r5, r5, #0x10 @ опыт: бит 4 регистра R5 «залип»
#endif
CHECK 0x55555555, 0xAAAAAAAA
mov r0, #0xAAAAAAAA @ PSP: биты 1:0 всегда нули
bic r0, r0, #3
msr psp, r0
mrs r1, psp
cmp r1, r0
bne 9f
mvn r0, r0
bic r0, r0, #3
msr psp, r0
mrs r1, psp
cmp r1, r0
bne 9f
mov r0, #0xA8000000 @ APSR: N, C, Q — затем Z, V
msr APSR_nzcvq, r0
mrs r1, APSR
and r1, r1, #0xF8000000
cmp r1, r0
bne 9f
mov r0, #0x50000000
msr APSR_nzcvq, r0
mrs r1, APSR
and r1, r1, #0xF8000000
cmp r1, r0
bne 9f
movs r0, #0
msr psp, r0
pop {r4-r11, pc}
9: movs r0, #1
pop {r4-r11, pc}
.size cpu_reg_test, . - cpu_reg_test
/* Сброс: марш всего ОЗУ до .noinit раньше любого кода на C. SysTick считает его
* такты — только для опыта в QEMU. */
.global Reset_Handler
.type Reset_Handler, %function
.thumb_func
Reset_Handler:
ldr r2, =0xE000E010 @ SysTick
ldr r3, =0x00FFFFFF
str r3, [r2, #4] @ RVR
str r3, [r2, #8] @ CVR: любая запись обнуляет
movs r3, #5
str r3, [r2] @ CSR: такт ядра, счёт
ldr r0, =_sram
ldr r1, =_estack @ до .noinit: запись об отказе переживает сброс
bl march_startup
cbnz r0, boot_fail
ldr r2, =0xE000E018
ldr r0, [r2] @ остаток SysTick — в c_start
bl c_start
boot_fail:
b boot_fail @ ОЗУ неисправно: C не запускать, выходы в покое, ждать сторожа
.size Reset_Handler, . - Reset_Handler
.ltorg
st12b_ram.c — transparent March C− during operation.
/* Раздел 12б: ОЗУ в работе — прозрачный March C− на своих данных, по блокам. */
#include "st12b.h"
#ifdef HOST
#define irq_save() 0u
#define irq_restore(m) ((void)(m))
#else
static inline uint32_t irq_save(void)
{
uint32_t m;
__asm volatile("mrs %0, primask\n\tcpsid i" : "=r"(m) : : "memory");
return m;
}
static inline void irq_restore(uint32_t m)
{
__asm volatile("msr primask, %0" : : "r"(m) : "memory");
}
#endif
/* Своё хозяйство самопроверки — в отдельной секции, вне проверяемой области. */
#define ST_SECT __attribute__((section(".st_ram")))
static uint32_t save[RAM_BLOCK] ST_SECT; /* копия блока: ожидаемые значения */
static uint32_t pos ST_SECT; /* начало следующего блока, в словах */
static uint32_t pos_inv ST_SECT; /* то же, инверсией: порча видна */
uint32_t ram_bits ST_SECT; /* биты последней ошибки — для журнала */
uint32_t march_own(volatile uint32_t *p, uint32_t *s, uint32_t n)
{
uint32_t i, err = 0u;
for (i = 0u; i < n; i++) { s[i] = ST_RD(&p[i]); } /* копия: a */
for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i]; ST_WR(&p[i], ~s[i]); } /* ⇑(ra, w~a) */
for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); } /* ⇑(r~a, wa) */
for (i = n; i-- > 0u;) { err |= ST_RD(&p[i]) ^ s[i]; ST_WR(&p[i], ~s[i]); } /* ⇓(ra, w~a) */
for (i = n; i-- > 0u;) { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); } /* ⇓(r~a, wa) */
for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i]; } /* ⇕(ra) */
return err;
}
void ram_reset(void)
{
pos = 0u;
pos_inv = ~0u;
}
int ram_step(volatile uint32_t *base, uint32_t words, uint32_t *wrapped)
{
uint32_t n = RAM_BLOCK, err, m, p = pos;
if ((p ^ pos_inv) != 0xFFFFFFFFu || p >= words)
return ST_STATE; /* испорчено само состояние проверки */
if (p + n > words)
n = words - p;
m = irq_save(); /* посреди марша блок не должен видеть никто */
err = march_own(base + p, save, n);
irq_restore(m);
*wrapped = (p + n >= words);
p = *wrapped ? 0u : p + n - 1u; /* блоки перекрываются на одно слово */
pos = p;
pos_inv = ~p;
ram_bits = err;
return err ? ST_RAM : ST_OK;
}
int ram_buf_test(void) /* буфер между шагами ничего не хранит */
{
return march_startup(save, save + RAM_BLOCK) ? ST_BUF : ST_OK;
}
st12b_flash.c — CRC-32 of the image against the reference.
/* Раздел 12б: флеш-память — CRC-32 всего образа против эталона, записанного при сборке. */
#include "st12b.h"
#ifndef HOST
#include "regs12b.h"
#endif
#define ST_SECT __attribute__((section(".st_ram")))
static uint32_t fpos ST_SECT; /* сколько слов уже подано в этом круге */
static uint32_t fpos_inv ST_SECT; /* то же инверсией; начальные значения — flash_reset() */
uint32_t crc_soft(uint32_t crc, uint32_t word) /* CRC-32/MPEG-2 словом: как блок CRC у F1 и F4 */
{
crc ^= word;
for (int b = 0; b < 32; b++)
crc = (crc & 0x80000000u) ? (crc << 1) ^ 0x04C11DB7u : crc << 1;
return crc;
}
#ifdef SOFT_CRC /* QEMU блок CRC не моделирует; на ПК — тоже программно */
static uint32_t acc ST_SECT;
#define CRC_RESET() (acc = 0xFFFFFFFFu)
#define CRC_FEED(w) (acc = crc_soft(acc, (w)))
#define CRC_READ() (acc)
#else
#define CRC_RESET() (CRC_CR = CRC_CR_RESET)
#define CRC_FEED(w) (CRC_DR = (w))
#define CRC_READ() (CRC_DR)
#endif
#ifndef HOST
int crc_unit_ok(void) /* сам блок: сброс и одно известное слово */
{
RCC_CRCEN_REG |= RCC_CRCEN;
CRC_CR = CRC_CR_RESET;
if (CRC_DR != 0xFFFFFFFFu)
return 0;
CRC_DR = 0x12345678u;
return CRC_DR == 0xDF8A8A2Bu;
}
#endif
uint32_t flash_crc(const uint32_t *img, uint32_t words) /* при старте: весь образ сразу */
{
CRC_RESET();
for (uint32_t i = 0u; i < words; i++)
CRC_FEED(img[i]);
return CRC_READ();
}
void flash_reset(void)
{
fpos = 0u;
fpos_inv = ~0u;
}
int flash_step(const uint32_t *img, uint32_t words, uint32_t ref) /* в работе: по FLASH_STEP слов */
{
uint32_t i = fpos, end;
if ((i ^ fpos_inv) != 0xFFFFFFFFu || i >= words)
return -2; /* испорчено состояние проверки */
if (i == 0u)
CRC_RESET();
end = (words - i > FLASH_STEP) ? i + FLASH_STEP : words;
for (; i < end; i++)
CRC_FEED(img[i]);
if (i < words) {
fpos = i;
fpos_inv = ~i;
return 0; /* круг не кончился */
}
fpos = 0u;
fpos_inv = ~0u;
return (CRC_READ() == ref) ? 1 : -1; /* круг кончился: сошлось или нет */
}
st12b_clock.c — checking the clock against an independent reference.
/* Раздел 12б: такт — счёт таймера за 8 периодов независимой опоры против окна. */
#include "st12b.h"
#ifndef HOST
#include "regs12b.h"
#endif
int clk_in_window(uint32_t count, uint32_t lo, uint32_t hi)
{
return count >= lo && count <= hi;
}
#if !defined(HOST) && !defined(F1)
#define CLK_WAIT 2000000u /* ~50 мс при 168 МГц: опора молчит — это отказ */
uint32_t clk_measure_f407(uint32_t ti4_rmp) /* 1 — LSI, 2 — LSE; вывод не нужен */
{
uint32_t c1 = 0u, cap = 0u, k, t;
RCC_APB1ENR |= RCC_TIM5EN;
TIM5_OR = ti4_rmp << TIM5_OR_TI4_RMP_Pos; /* TI4_RMP: опора на вход канала 4 внутри камня */
TIM5_PSC = 0u;
TIM5_ARR = 0xFFFFFFFFu; /* TIM5 32-битный: разность не завернётся */
TIM5_CCMR2 = TIM_CCMR2_CC4S_TI4 | TIM_CCMR2_IC4PSC_8;
TIM5_CCER = TIM_CCER_CC4E;
TIM5_SR = 0u;
TIM5_CR1 = TIM_CR1_CEN;
for (k = 0u; k < 3u; k++) { /* первый захват — не с начала периода, его отбросить */
for (t = 0u; (TIM5_SR & TIM_SR_CC4IF) == 0u; t++) {
if (t > CLK_WAIT) {
TIM5_CR1 = 0u;
return 0u;
}
}
cap = TIM5_CCR4; /* чтение CCR4 снимает CC4IF */
if (k == 1u)
c1 = cap;
}
TIM5_CR1 = 0u;
return cap - c1; /* тактов TIM5 за 8 периодов опоры */
}
#endif
st12b_stack.c — the stack canary and painting.
/* Раздел 12б: стек — канарейка на дне и заливка шаблоном для учёта глубины. */
#include "st12b.h"
extern uint32_t _sstack[], _estack[]; /* дно и вершина стека — из скрипта компоновки */
#define STACK_CANARY 0x5AFEC0DEu
#define STACK_FILL 0xA5A5A5A5u
#define CANARY_WORDS 4u
void stack_paint(void) /* один раз из стартового кода, до main */
{
uint32_t *p = _sstack, *sp;
__asm volatile("mov %0, sp" : "=r"(sp));
for (uint32_t i = 0u; i < CANARY_WORDS; i++)
*p++ = STACK_CANARY;
while (p < sp - 16) /* 64 байта под своим кадром не трогать */
*p++ = STACK_FILL;
}
int stack_canary_ok(void) /* в каждом проходе: четыре слова */
{
for (uint32_t i = 0u; i < CANARY_WORDS; i++)
if (_sstack[i] != STACK_CANARY)
return 0;
return 1;
}
uint32_t stack_free_bytes(void) /* изредка: сколько стека ни разу не тронуто */
{
uint32_t *p = _sstack + CANARY_WORDS;
while (p < _estack && *p == STACK_FILL)
p++;
return (uint32_t)(p - (_sstack + CANARY_WORDS)) * 4u;
}
app12b.c — the experiment firmware: self-test at startup, passes during operation and injected faults.
/* Раздел 12б: прошивка-опыт. Самопроверка при старте, затем проходы «в работе» с замером
* SysTick (в QEMU с -icount он считает выполненные команды). Опыты с отказами — ключами
* сборки INJECT_REG, INJECT_STACK и порчей байта образа после patch12b.py. */
#include "st12b.h"
#include "regs12b.h"
extern uint32_t _sram[], _estack[], _sdata[], _ebss[], _sstack[], __crc_ref[];
extern uint32_t boot_syst;
#define IMG ((const uint32_t *)0x08000000u)
#define IMG_WORDS ((uint32_t)(__crc_ref - (uint32_t *)0x08000000u))
#define RAM_BASE ((volatile uint32_t *)_sdata)
#define RAM_WORDS ((uint32_t)(_ebss - _sdata))
uint32_t app_buf[1024]; /* прикладные данные: их марш и проверяет в работе */
const uint32_t app_table[8] = { 0x13572468u, 0x2468ACE0u, 0x0F1E2D3Cu, 0x4B5A6978u,
0x8796A5B4u, 0xC3D2E1F0u, 0x01234567u, 0x89ABCDEFu };
typedef struct { uint32_t code, detail, count, check; } st_rec_t;
st_rec_t st_rec __attribute__((section(".noinit"))); /* переживает сброс и марш при старте */
static void putc1(char c)
{
while (!(USART1_SR & (1u << 7))) {
}
USART1_DR = (uint32_t)c;
}
static void puts1(const char *s)
{
while (*s)
putc1(*s++);
}
static void putu(uint32_t v)
{
char b[11];
int i = 10;
b[i] = 0;
do {
b[--i] = (char)('0' + v % 10u);
v /= 10u;
} while (v);
puts1(&b[i]);
}
static void puthex(uint32_t v)
{
puts1("0x");
for (int i = 28; i >= 0; i -= 4)
putc1("0123456789ABCDEF"[(v >> i) & 0xFu]);
}
static void kv(const char *k, uint32_t v)
{
puts1(k);
putu(v);
puts1("\n");
}
static uint32_t tick(void)
{
return SYST_CVR;
}
static uint32_t since(uint32_t t0)
{
return (t0 - SYST_CVR) & 0x00FFFFFFu;
}
__attribute__((noinline)) static void cal_loop(uint32_t n) /* ровно 2 команды на круг */
{
__asm volatile("1: subs %0, #1\n\tbne 1b" : "+r"(n));
}
void st_fail(uint32_t code, uint32_t detail) /* единственный выход при любом отказе */
{
OUT_ODR = 0u; /* силовые выходы — выключить первым делом */
if (st_rec.check != ~(st_rec.code ^ st_rec.detail ^ st_rec.count))
st_rec.count = 0u; /* записи нет или она битая: начать счёт заново */
st_rec.code = code;
st_rec.detail = detail;
st_rec.count++;
st_rec.check = ~(code ^ detail ^ st_rec.count);
puts1("SAFE code ");
putu(code);
puts1(" detail ");
puthex(detail);
puts1(" count ");
putu(st_rec.count);
puts1("\n");
for (;;) { /* сторож не кормится: сброс через его таймаут */
}
}
static int st_pass(void) /* один проход главного цикла */
{
uint32_t wrapped = 0u;
int r;
if (!stack_canary_ok())
return ST_STACK;
r = ram_step(RAM_BASE, RAM_WORDS, &wrapped);
if (r != ST_OK)
return r;
if (wrapped) { /* раз за круг ОЗУ: буфер и регистры ядра */
r = ram_buf_test();
if (r != ST_OK)
return r;
if (cpu_reg_test() != 0u)
return ST_CPU;
}
r = flash_step(IMG, IMG_WORDS, *__crc_ref);
if (r < 0)
return (r == -1) ? ST_FLASH : ST_STATE;
return ST_OK; /* только здесь кормят сторож */
}
#ifdef INJECT_STACK
extern uint32_t _sstack[];
__attribute__((noinline)) static void stack_hog(void) /* опыт: стек дошёл до канарейки */
{
uint32_t sp;
__asm volatile("mov %0, sp" : "=r"(sp));
uint32_t n = (sp - (uint32_t)_sstack) / 4u - 2u; /* массив ляжет до дна, в канарейку */
volatile uint32_t big[n];
for (uint32_t i = 0u; i < n; i++)
big[i] = i;
(void)big[0];
}
#endif
static uint32_t ram_sum(void)
{
uint32_t s = 0u;
for (uint32_t i = 0u; i < RAM_WORDS; i++)
s = (s << 5 | s >> 27) ^ RAM_BASE[i];
return s;
}
int main(void)
{
uint32_t t, r, c, i, lo = 0xFFFFFFFFu, hi = 0u, steps;
RCC_APB2ENR |= USART1_EN;
USART1_BRR = BRR_115200;
USART1_CR1 = (1u << 13) | (1u << 3);
#if defined(F1)
puts1("selftest12b F1\n");
#else
puts1("selftest12b F4\n");
#endif
puts1("boot march words ");
putu((uint32_t)(_estack - _sram));
kv(" syst ", 0x00FFFFFFu - boot_syst);
/* ── при старте: всё целиком ── */
r = cpu_reg_test();
kv("cpu ", r);
if (r != 0u)
st_fail(ST_CPU, r);
r = (uint32_t)crc_unit_ok();
kv("crc unit ", r);
#ifndef SOFT_CRC
if (r == 0u)
st_fail(ST_CRCUNIT, 0u);
#endif
c = flash_crc(IMG, IMG_WORDS);
puts1("flash words ");
putu(IMG_WORDS);
puts1(" crc ");
puthex(c);
puts1(" ref ");
puthex(*__crc_ref);
puts1("\n");
if (c != *__crc_ref)
st_fail(ST_FLASH, c);
kv("table ", app_table[3] >> 24);
for (i = 0u; i < 1024u; i++)
app_buf[i] = i * 2654435761u;
ram_reset();
flash_reset();
/* ── замеры: такты SysTick на каждый шаг ── */
t = tick(); cal_loop(1000u); kv("t cal2000 ", since(t));
t = tick(); cpu_reg_test(); kv("t cpu ", since(t));
t = tick(); stack_canary_ok(); kv("t canary ", since(t));
t = tick(); march_startup((uint32_t *)app_buf, (uint32_t *)app_buf + 256); kv("t march256 ", since(t));
for (i = 0u; i < 1024u; i++)
app_buf[i] = i * 2654435761u;
r = ram_sum();
t = tick(); ram_step(RAM_BASE, RAM_WORDS, &c); kv("t ram_step ", since(t));
t = tick(); ram_buf_test(); kv("t buf ", since(t));
t = tick(); flash_step(IMG, IMG_WORDS, *__crc_ref); kv("t flash_step ", since(t));
ram_reset();
flash_reset();
/* круг ОЗУ: содержимое после него прежнее */
steps = 0u;
do {
if (ram_step(RAM_BASE, RAM_WORDS, &c) != ST_OK)
st_fail(ST_RAM, ram_bits);
steps++;
} while (!c);
puts1("ram words ");
putu(RAM_WORDS);
puts1(" steps ");
putu(steps);
kv(" same ", ram_sum() == r);
/* регистр с залипшими битами вместо ОЗУ: VTOR, биты 6:0 всегда нули */
r = march_startup((uint32_t *)0xE000ED08u, (uint32_t *)0xE000ED08u + 1);
REG(0xE000ED08u) = 0u;
puts1("vtor march err ");
puthex(r);
puts1("\n");
#ifdef INJECT_STACK
stack_hog();
#endif
/* ── в работе: проходы, пока не сойдутся оба круга ── */
ram_reset();
flash_reset();
for (i = 0u; i < 2000u; i++) {
t = tick();
r = (uint32_t)st_pass();
c = since(t);
if (r != ST_OK)
st_fail(r, ram_bits);
lo = c < lo ? c : lo;
hi = c > hi ? c : hi;
}
puts1("passes 2000 ok\n");
kv("t pass min ", lo);
kv("t pass max ", hi);
kv("stack free ", stack_free_bytes());
kv("stack canary ", (uint32_t)stack_canary_ok());
#if !defined(F1)
/* такт: TIM5 CH4 от LSE, 8 периодов; окно ±0,5 % при 84 МГц */
c = clk_measure_f407(2u);
puts1("clock lse count ");
putu(c);
puts1(" window ");
putu(CLK_LO(84000000u, 32768u, 8u, 5000u));
puts1("..");
putu(CLK_HI(84000000u, 32768u, 8u, 5000u));
puts1("\n");
if (!clk_in_window(c, CLK_LO(84000000u, 32768u, 8u, 5000u), CLK_HI(84000000u, 32768u, 8u, 5000u)))
st_fail(ST_CLOCK, c);
#endif
puts1("done\n");
for (;;) {
}
}
linker12b.ld — the linker script: RAM order and the place of the CRC reference.
/* Раздел 12б: скрипт компоновки. Порядок ОЗУ: .data и .bss (их проверяет марш в работе),
* .st_ram (хозяйство самопроверки), стек с канарейкой на дне, .noinit (запись об отказе).
* Во флеш-памяти последнее слово образа — эталон CRC; его вписывает patch12b.py. */
ENTRY(Reset_Handler)
INCLUDE mem.ld
_stack_size = 0x400;
_sram = ORIGIN(RAM);
_estack = ORIGIN(NOINIT); /* вершина стека — под записью об отказе */
_sstack = _estack - _stack_size;
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
} > FLASH
.data :
{
_sdata = .;
*(.data .data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
_sidata = LOADADDR(.data);
.crc_ref : /* эталон: сразу за образом, пока — 0xFFFFFFFF */
{
__crc_ref = .;
LONG(0xFFFFFFFF)
} > FLASH
.bss (NOLOAD) :
{
_sbss = .;
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
.st_ram (NOLOAD) :
{
_sst = .;
*(.st_ram)
. = ALIGN(4);
_est = .;
} > RAM
.noinit (NOLOAD) :
{
*(.noinit .noinit.*)
} > NOINIT
ASSERT(_est <= _sstack, "RAM too small for data + selftest + stack")
ASSERT(__crc_ref == LOADADDR(.data) + SIZEOF(.data), "CRC reference must follow the image")
}
patch12b.py — writes the CRC reference into the ELF after the build.
# Раздел 12б: эталон CRC образа — после сборки, в сам ELF.
# python3 patch12b.py fw.elf → считает CRC-32/MPEG-2 (как блок CRC у F1 и F4) по образу от 0x08000000
# до __crc_ref и вписывает слово в секцию .crc_ref ELF-файла; .bin делается уже из исправленного ELF.
import struct, subprocess, sys
ORIGIN = 0x08000000
def crc_mpeg2(data):
crc = 0xFFFFFFFF
for (w,) in struct.iter_unpack("<I", data): # слово — младшим байтом вперёд, как читает ядро
crc ^= w
for _ in range(32):
crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if crc & 0x80000000 else (crc << 1) & 0xFFFFFFFF
return crc
elf = sys.argv[1]
nm = subprocess.run(["arm-none-eabi-nm", elf], capture_output=True, text=True, check=True).stdout
ref_addr = int([l.split()[0] for l in nm.splitlines() if l.endswith(" __crc_ref")][0], 16)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf + ".raw"], check=True)
raw = open(elf + ".raw", "rb").read()
n = ref_addr - ORIGIN
if len(raw) != n + 4 or n % 4 or raw[n:] != b"\xff\xff\xff\xff":
sys.exit("образ не тот: длина %d, __crc_ref на смещении %d" % (len(raw), n))
crc = crc_mpeg2(raw[:n])
open(elf + ".ref", "wb").write(struct.pack("<I", crc))
subprocess.run(["arm-none-eabi-objcopy", "--update-section", ".crc_ref=" + elf + ".ref", elf], check=True)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf[:-4] + ".bin"], check=True)
out = open(elf[:-4] + ".bin", "rb").read()
assert out[:n] == raw[:n] and struct.unpack("<I", out[n:])[0] == crc
print("crc_ref 0x%08X at 0x%08X, image %d bytes = %d words" % (crc, ref_addr, n, n // 4))
Next: part III, section 13 "Timers" — the largest peripheral block: counting, PWM, input capture, encoders and synchronising several timers.