Embedded HandbookApps
All sections › II. Core and system mechanisms
STM32F103, STM32F407 · level intermediate · 44 min read
Open in the interactive handbook

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:

F103 memory map and the checks: in RAM from bottom to top the application data .data and .bss, which during operation the transparent march checks in blocks of 16 words, the .st_ram section with the buffer and position of the self-test, which the march over the buffer checks once per round, a 1 KB stack painted with a pattern and with a canary at the bottom, and the .noinit fault record, which the startup march does not touch; in Flash memory the vector table, code and constants, the initial values of .data and the last word .crc_ref with the CRC reference written in after the build

The order at startup:

  1. The watchdog first (section 12): if the check itself hangs, the chip still restarts.
  2. All RAM — in Reset_Handler, before any C code (12б.2).
  3. Core registers and flags (12б.6).
  4. The CRC block, then the CRC of the whole image against the reference (12б.4).
  5. The clock — HSE against LSE (12б.5).
  6. 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:


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

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.

Open in the interactive handbook