Embedded HandbookApps
All sections › V. Debugging and quality
STM32F103, STM32G474 · level intermediate · 21 min read
Open in the interactive handbook

36б. Run-time self-test: core, RAM, flash, clock

Sections 36 and 36а are about how a person hunts for a fault: with a debugger, a multimeter and an oscilloscope. In the field there is no person nearby. A device that controls a heater or a drive must notice by itself that a RAM cell has gone bad, a bit in flash memory has flipped or the crystal has drifted — and go to a safe state before the error reaches an output. For household appliances such checks are required by the IEC 60730 standard for class B software. The text of the standard is not on the disk, so there are no clause numbers and no tolerances from it here — only the checks themselves, each one calculated or run locally.

ST's self-test library (X-CUBE-CLASSB) is not on this machine either — neither in the STM32Cube archives nor in the Keil packs. The code in this section is written from scratch: a March C- test over RAM, a CRC of the flash image and a comparison of the HSE clock with LSE or LSI. What is already covered in other sections is not repeated here: the watchdog — 12.1–12.4, the CRC block and its variant — 12.5 and 43.1, the image CRC in a bootloader — 48, the clock security system (CSS) and HSI trimming against LSE — 6.8 and 6.9, timer capture — 13.6, the safe state — 129.7, the watchdog in a product — 204.6.

References: reference manuals RM0008 (F1) and RM0440 (G4), datasheets STM32F103x8/B and STM32G474 (DS12288), the Cortex-M3 core guide (DUI0552A) — PDFs from the Keil packs. The RAM fault model is run on a PC; the code is built with armclang for F103 and G474 and run in QEMU on a Cortex-M3.

Terms used in the documentation

Term Meaning
self-test, start-up / run-time test self-check at start-up / during operation
March test, March C- a marching RAM test
stuck-at fault (SAF) a bit stuck at 0 or 1
transition fault (TF) a bit that does not change in one direction
address decoder fault (AF) an error of the address decoder
coupling fault (CF) cells coupled: a write to one corrupts another
transparent test a test after which the contents are unchanged
reference signature the expected signature (here — the image CRC)
clock security system (CSS) the hardware monitor of the HSE clock
safe state the state in which the device can do no harm

36б.1 What to check and when

The checks fall into two groups. At start-up everything is checked in full: nothing is running yet, and the RAM contents may be erased. During operation — in pieces, one step per pass of the main loop, so that the application tasks do not stall.

Self-test step by step: at start-up — start the watchdog, core registers, all RAM with a March C- test, CRC of the whole image, HSE clock against LSE or LSI; during operation, in every pass of the main loop — a RAM chunk of 16 words with interrupts disabled, a CRC portion of 256 words, a clock check by TIM5 capture, and feeding the watchdog only if everything matched; any failure leads to the safe state and a watchdog reset

The order at start-up is:

  1. The watchdog first. If the check itself hangs, the device will restart anyway. A full RAM march on the F103 takes at least 1.9 ms (36б.3), and the watchdog timeout can be set up to 26 seconds (section 12).
  2. Core registers. Alternating patterns are written to every register R0–R12, the stack pointers and the link register and read back; the condition flags are checked. Such a test is written in assembler: the compiler decides which registers to use, and "all registers" cannot be checked from C. It is not built here — a description without numbers.
  3. All of RAM with a March C- test (36б.2). The march erases the contents, so it runs in the start-up code before .data and .bss are filled.
  4. CRC of the whole image against the reference written after the build (36б.5).
  5. Clock: HSE against an independent oscillator (36б.6).

On the G474 part of the RAM checks itself: according to the datasheet the first 32 KB of SRAM and all of CCM SRAM (32 KB) have hardware parity — one bit per byte, and on an error a non-maskable interrupt NMI. RM0440 explicitly explains it by "class B" requirements. It is enabled with the option bit SRAM_PE. Parity catches a flipped bit on read, but not an address error: if the address hit the wrong cell, that cell holds data with its own correct parity. Therefore the march is needed even where parity exists.


36б.2 RAM: which faults the march catches

A march is several passes over all words in a row, up (⇑) or down (⇓) the addresses; in each word a pass performs the same reads and writes. March C-:

⇕(w0); ⇑(r0,w1); ⇑(r1,w0); ⇓(r0,w1); ⇓(r1,w0); ⇕(r0)

w0 — write zeros, r1 — read and expect ones, ⇕ — either order. Per word this gives 1 + 2 + 2 + 2 + 2 + 1 = 10 accesses, in total 10N for N words. The lighter March X — ⇕(w0); ⇑(r0,w1); ⇓(r1,w0); ⇕(r0), 6N. The "simple" test written most often — write 0x55555555 to all words, read them, then the same with 0xAAAAAAAA — is 4N.

Which faults each test catches is shown by the model march36c.py: a memory of 32 words of 32 bits, exactly one fault is injected into it, the initial contents are random, the tests are word-oriented — like the C code. The fault classes are the generally accepted memory models: stuck-at, transition, address decoder fault and three kinds of coupling between cells.

Fault Instances March C- March X 0x55/0xAA
bit stuck at 0 or 1 256 100 % 100 % 100 %
bit does not go 0→1 or 1→0 256 100 % 100 % 76 %
two addresses select one cell 992 100 % 100 % 0 %
a transition in one word flips a bit in another 400 100 % 100 % 37 %
a transition in one word sets a bit in another to 0 or 1 800 100 % 57 % 19 %
while a bit in one word is in state s, a bit in another is held 800 100 % 80 % 68 %
coupling of two bits of the same word 2000 60 % 60 % 49 %

On good memory all three tests stay silent. The "simple" test does not see an address decoder fault at all: it writes the same value to all words, and it does not matter which cell the write landed in — the same value is read back. It makes transitions once each and only in the direction set by the initial contents. March X misses 43 % of the "transition sets a bit" couplings: it has only one transition per traversal direction — up 0→1, down 1→0 — and some combinations of "which transition, on which side of the victim, to which value" it never tries. March C- has both transitions in both directions.

Coupling of two bits of the same word cannot be seen with a solid background (all zeros, all ones): the bits of a word are always written the same way. A model with six backgrounds — 00000000, 55555555, 33333333, 0F0F0F0F, 00FF00FF, 0000FFFF — catches it in 100 % of cases, but costs six times more. Here a solid background is chosen to save time; if coupling inside a word matters, the march is repeated with backgrounds.


36б.3 March C- in C

uint32_t march_c_minus(volatile uint32_t *p, uint32_t n)
{
    uint32_t i, err = 0u;

    for (i = 0u; i < n; i++) { p[i] = 0u; }                       /* ⇕(w0)    */
    for (i = 0u; i < n; i++) { err |= p[i];  p[i] = ~0u; }        /* ⇑(r0,w1) */
    for (i = 0u; i < n; i++) { err |= ~p[i]; p[i] = 0u; }         /* ⇑(r1,w0) */
    for (i = n; i-- > 0u;)   { err |= p[i];  p[i] = ~0u; }        /* ⇓(r0,w1) */
    for (i = n; i-- > 0u;)   { err |= ~p[i]; p[i] = 0u; }         /* ⇓(r1,w0) */
    for (i = 0u; i < n; i++) { err |= p[i]; }                     /* ⇕(r0)    */
    return err;
}

volatile forces the compiler to really read and write every word: without it the compiler may drop a write that it immediately reads back. Errors are not checked with a branch on every word, but accumulated in err with OR: the loop is shorter, and the result shows which bits failed. Zero means the memory is good.

The file selftest36c.c is built with armclang (-Os -Wall -Wextra) for F103 and for G474 without warnings; _Static_assert checks the offsets of the CRC and timer registers against RM0008 and RM0440. The ⇑(r0,w1) loop in the F103 listing:

LDR      r4,[r2,#0]
SUBS     r3,#1
ORR      r0,r0,r4
STR      lr,[r2],#4
BNE      {pc}-0xc ; 0x28

Five instructions per word: read, counter, error accumulation, write with pointer advance, branch. Over the six march loops this gives 3 + 5 + 5 + 5 + 5 + 4 = 27 instructions per word; the G474 listing is the same. Each instruction takes at least one clock cycle, so the full march at start-up:

Chip RAM Words Instructions At least
F103, 72 MHz 20 KB 5120 138240 1.9 ms
G474, 170 MHz 128 KB 32768 884736 5.2 ms

This is a lower bound: a branch costs more than one cycle, and flash wait states add more (73.2). The exact time comes from the cycle counter on the board (73.1) — not measured here. In QEMU March C- over 256 words of good RAM returned zero.


36б.4 During operation: a transparent test in chunks

During operation RAM is in use and must not be erased. A transparent test takes a chunk, saves it, runs the march over it and puts the contents back:

uint32_t ram_step(volatile uint32_t *base, uint32_t words)
{
    uint32_t n = CHUNK_WORDS, i, err, primask;
    volatile uint32_t *p = base + ram_pos;

    if (ram_pos + n > words) { n = words - ram_pos; }
    primask = __get_PRIMASK();
    __disable_irq();                          /* никто не должен увидеть кусок, пока в нём нули и единицы */
    for (i = 0u; i < n; i++) { saved[i] = p[i]; }
    err = march_c_minus(p, n);
    for (i = 0u; i < n; i++) { p[i] = saved[i]; }
    __set_PRIMASK(primask);
    ram_pos = (ram_pos + n >= words) ? 0u : ram_pos + n - 1u;
    return err;
}

Three rules. Interrupts are disabled for the whole chunk: a handler that reads a variable from the chunk in the middle of the march will get zero or 0xFFFFFFFF. PRIMASK is restored, not enabled: if the function was called with interrupts already disabled, they stay disabled — in QEMU, after a call with interrupts disabled, PRIMASK stayed equal to 1. The saved[] buffer and the stack are not part of the tested area — the march would erase them together with the return address.

A chunk is CHUNK_WORDS = 16 words. According to the listing, each word of the chunk takes 27 march instructions and 10 for saving and restoring: 592 instructions with interrupts disabled, at least 8.2 µs at 72 MHz. For all that time the response to an interrupt may be delayed — that is the price of the transparent test, and the chunk size is chosen by the permissible latency.

Chunks overlap by one word. Coupling between neighbouring words at the junction of two chunks is not visible to either of them: the aggressor is in one, the victim in the other. The model runs the same order as ram_step() over an area of 64 words:

Partitioning Chunk starts Neighbouring-word coupling (756) Any-two-words coupling (300)
no overlap 0 16 32 48 95 % 24 %
1-word overlap 0 15 30 45 60 100 % 27 %
the whole march at once — — 100 %

The contents after a full round in the model are unchanged. The same chunk order — 0, 15, 30, 45, 60 — was produced by the ram_step() code in QEMU, and all 64 words after the round matched the copy. The last column is the limit of a transparent test: coupling between distant words rarely falls into one chunk. Only the full march at start-up catches it, so it is not replaced by a run-time check.

A full round over the 20 KB of the F103 is 342 calls of ram_step().


36б.5 Flash: image CRC with the reference at the end

RM0008 describes the purpose of the CRC block exactly like this: a program signature calculated at run time is compared with a reference calculated at build time and stored at a known location. The location is right after the image. Its address is given by the linker script:

  __image_end = LOADADDR(.data) + SIZEOF(.data);   /* здесь будет слово CRC */

After the build the script crc_image36c.py reads the .bin, calculates CRC-32/MPEG-2 the same way as the CRC block (words least significant byte first, each word fed into the calculation most significant bit first, initial value 0xFFFFFFFF, no reflection; 12.5, 43.1), and appends the result as a word at the end. It also checks the file length against __image_end: if it does not match, the wrong file or the wrong boundaries were built.

At start-up flash_check_all() runs the whole image through the block and compares the result with image[words]. During operation — 256 words per call:

int flash_step(const uint32_t *image, uint32_t words)
{
    static uint32_t pos;
    uint32_t i = pos, end = pos + FLASH_STEP_WORDS;

    if (i == 0u) { crc_reset(); }
    if (end > words) { end = words; }
    for (; i < end; i++) { crc_feed(image[i]); }
    if (i < words) { pos = i; return 0; }
    pos = 0u;
    return (crc_read() == image[words]) ? 1 : -1;
}

The feed loop is 4 instructions per word; the block itself spends 4 AHB bus cycles per word (RM0008, RM0440). The 64 KB flash of the F103 is 64 steps, the 512 KB of the G474 is 512 steps.

QEMU has no CRC block, so the run substitutes a software calculation of the same variant. An image of 379 words passed the check at start-up and step by step (two steps). The CRC over the image together with the reference word gave 0x00000000 — a property of CRC without final inversion: the remainder of "data plus their own CRC" is zero. Then one bit in the image constants was flipped — and both the start-up check and the step-by-step check reported a failure.

The G474 flash has ECC: according to RM0440 it detects and corrects a single error in a double word by itself, and detects two. This does not make the image CRC unnecessary: a partially written or foreign image is written with correct ECC codes and does not look like an error to ECC.


36б.6 Clock and the reaction to a failure

CSS catches only a stopped HSE: according to RM0440, on HSE failure the system switches to HSI16, the PLL fed from HSE is turned off, and an NMI arrives. A crystal of the wrong frequency or an error in the PLL setup is a healthy clock for CSS. Therefore the frequency is compared with an independent oscillator: a timer counts bus clock cycles (from HSE through the PLL) between edges of LSE or LSI.

On the G474 the 32-bit TIM5 is convenient for this (datasheet): according to table 270 of RM0440 its TI1 input is connected inside the chip to LSI (TISEL = 1) and to LSE (TISEL = 2), no wire is needed. Capture happens on every 8th edge:

uint32_t ticks_per_8(uint32_t tisel)
{
    uint32_t c1 = 0u, cap = 0u, k, t;

    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM5EN;
    TIM5->TISEL = tisel;
    TIM5->PSC = 0u;
    TIM5->ARR = 0xFFFFFFFFu;                  /* счётчик 32-битный: разность не переполнится */
    TIM5->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_IC1PSC;        /* вход TI1, захват на каждом 8-м фронте */
    TIM5->CCER = TIM_CCER_CC1E;
    TIM5->CR1 = TIM_CR1_CEN;
    for (k = 0u; k < 3u; k++) {               /* первый захват отбрасываем, берём разность двух следующих */
        for (t = 0u; (TIM5->SR & TIM_SR_CC1IF) == 0u; t++) {
            if (t > 2000000u) { return 0u; }  /* опора молчит: это тоже отказ */
        }
        cap = TIM5->CCR1;                     /* чтение CCR1 снимает CC1IF */
        if (k == 1u) { c1 = cap; }
    }
    return cap - c1;
}

The count over 8 periods = 8 × f timer / f reference. The window is all the counts a healthy HSE gives at any permissible reference value, plus one for quantization. LSI on the G474 is 29.5…34 kHz over the whole temperature range according to the datasheet, on the F103 30…60 kHz. LSE is 32768 Hz; RM0440 gives the accuracy of a watch crystal as "a few tens of ppm", and the ±1 % window around nominal is this section's choice, with a large margin. Run of calc36c.py:

Failure G474, LSE ±1 % G474, LSI F103, LSI
healthy: count window 41091…41925 39999…46103 9599…19201
HSE lost, running from HSI 3906 — caught 3765…4339 — caught 1067…2133 — caught
frequency ×0.5 caught caught may be missed
frequency ×0.9 caught may be missed may be missed
frequency ×0.995 not caught may be missed may be missed
frequency ×1.02 caught may be missed may be missed
frequency ×1.5 caught caught may be missed
frequency ×4 caught caught may be missed

"May be missed" means that for some permissible LSI the count of the faulty clock falls inside the window. The conclusion is simple: LSI catches only gross failures. The LSI window on the G474 is ±7.1 %, on the F103 ±33.3 %: for the F103 that is almost only the loss of HSE, which CSS sees anyway. An accurate frequency check uses LSE.

On the F103 there is no internal path from LSI to a timer in medium density (F103C8, F103CB): according to RM0008 the TIM5 CH4 input is connected to LSI by the TIM5CH4_IREMAP bit only in high-density, XL and connectivity line chips. On the F103C8 the reference is brought in by wire — the LSE clock divided by 64, through the TAMPER pin (6.8). The F1 timers are 16-bit (13.1), and the "×4" row is not a typo: see 36б.7.

What to do on a failure. Any check that did not match leads to one place: switch off the power outputs, record the reason where it survives a reset (a backup register, a non-initialized RAM area), and stop feeding the watchdog (129.7). A watchdog clocked from LSI (12.2) restarts the chip even if HSE itself is broken. After the reset the device reads the reset cause flags (12.4) and its own record: if the failure repeats, it stays in the safe state instead of restarting in a loop. The watchdog is fed only at the end of a main-loop pass in which all self-test steps returned "healthy".


36б.7 Pitfalls

The march erased the stack it was running on. The full March C- at start-up runs before .data and .bss are filled and does not touch its own stack; during operation the stack and the saved[] buffer are excluded from the tested area. Otherwise the march erases the return address — and you get a HardFault instead of a report.

A chunk was tested with interrupts enabled. A handler read a counter in the middle of the march and saw 0xFFFFFFFF. Interrupts are disabled for the whole chunk, from saving to restoring.

NMI arrives even with interrupts disabled. PRIMASK blocks only exceptions with configurable priority (DUI0552A); NMI and HardFault get through. The NMI handler — and that is where CSS and the G474 parity error arrive — must not touch the tested area.

Someone else uses the CRC block. Between flash_step() steps the block holds an intermediate result. If a communication protocol calculates its own checksum on it (and on the G474 also reconfigures the polynomial, 43.1), the pass ends with a false failure. The block is given to the self-test entirely, and the protocol gets a software CRC.

The reference was calculated over the wrong boundaries. The script got a .hex instead of a .bin, or the file was taken from a different build. That is why crc_image36c.py checks the length against __image_end and refuses if the length is not a multiple of four.

The clock is "OK" by LSI, but the crystal is wrong. The LSI window on the F103 is ±33 %: it lets through both ×0.5 and ×1.5. LSI is used only to catch loss of HSE; accuracy is checked against LSE.

The 16-bit counter wrapped around. On the F103 all timers are 16-bit: for a healthy clock that is enough (no more than 19201), but at a frequency above ×3.41 the difference is computed modulo 65536. At ×4 the count 38400…76800 becomes 38400…65535 and 0…11264 — and the lower piece falls into the window. The G474 has a 32-bit TIM5.

Parity was enabled, but RAM was not cleared. RM0440 advises writing all of RAM at the start of the program: reading a cell that has not been written yet may cause a parity error and an NMI.


36б.8 Summary

Full sources

selftest36c.c — run-time self-test: RAM with a March C- test, flash memory CRC, clock.

/* Раздел 36б: самопроверка во время работы — ОЗУ маршем March C-, флеш — CRC образа, частота — захватом таймера.
 * Собирается armclang для STM32F103 (-DSTM32F10X_MD, заголовок stm32f10x.h) и STM32G474 (-DSTM32G474xx, stm32g4xx.h)
 * без предупреждений; для прогона в QEMU (-DSELFTEST_SOFT_CRC) блок CRC заменяется программным расчётом того же варианта. */
#include <stdint.h>
#include <stddef.h>

/* ── ОЗУ ─────────────────────────────────────────────────────────────── */
#define CHUNK_WORDS 16u                       /* кусок прозрачного теста: слов за один вызов */
_Static_assert(CHUNK_WORDS >= 2u, "перекрытие в одно слово требует куска хотя бы из двух слов");

/* March C-: {⇕(w0); ⇑(r0,w1); ⇑(r1,w0); ⇓(r0,w1); ⇓(r1,w0); ⇕(r0)} — 10 обращений на слово.
 * Возвращает 0, если память исправна; иначе — биты, которые прочитались не так. */
uint32_t march_c_minus(volatile uint32_t *p, uint32_t n)
{
    uint32_t i, err = 0u;

    for (i = 0u; i < n; i++) { p[i] = 0u; }                       /* ⇕(w0)    */
    for (i = 0u; i < n; i++) { err |= p[i];  p[i] = ~0u; }        /* ⇑(r0,w1) */
    for (i = 0u; i < n; i++) { err |= ~p[i]; p[i] = 0u; }         /* ⇑(r1,w0) */
    for (i = n; i-- > 0u;)   { err |= p[i];  p[i] = ~0u; }        /* ⇓(r0,w1) */
    for (i = n; i-- > 0u;)   { err |= ~p[i]; p[i] = 0u; }         /* ⇓(r1,w0) */
    for (i = 0u; i < n; i++) { err |= p[i]; }                     /* ⇕(r0)    */
    return err;
}

static uint32_t saved[CHUNK_WORDS];           /* копия куска; сам буфер в проверяемую область не входит */
static uint32_t ram_pos;                      /* начало следующего куска */

/* Один шаг прозрачного теста: сохранить кусок, прогнать March C-, вернуть содержимое.
 * Соседние куски перекрываются на одно слово — так ловится связь соседних слов на стыке. */
uint32_t ram_step(volatile uint32_t *base, uint32_t words)
{
    uint32_t n = CHUNK_WORDS, i, err, primask;
    volatile uint32_t *p = base + ram_pos;

    if (ram_pos + n > words) { n = words - ram_pos; }
    primask = __get_PRIMASK();
    __disable_irq();                          /* никто не должен увидеть кусок, пока в нём нули и единицы */
    for (i = 0u; i < n; i++) { saved[i] = p[i]; }
    err = march_c_minus(p, n);
    for (i = 0u; i < n; i++) { p[i] = saved[i]; }
    __set_PRIMASK(primask);
    ram_pos = (ram_pos + n >= words) ? 0u : ram_pos + n - 1u;
    return err;
}

uint32_t ram_next_chunk(void) { return ram_pos; }

/* ── Флеш: CRC-32/MPEG-2 (полином 0x04C11DB7, начало 0xFFFFFFFF, без отражений) ── */
#ifdef SELFTEST_SOFT_CRC
void     crc_reset(void);                     /* прогон в QEMU: блока CRC там нет, расчёт программный */
void     crc_feed(uint32_t w);
uint32_t crc_read(void);
#else
_Static_assert(offsetof(CRC_TypeDef, DR) == 0x00u && offsetof(CRC_TypeDef, CR) == 0x08u,
               "CRC_DR и CRC_CR по RM0008 и RM0440 — смещения 0x00 и 0x08");
static void crc_reset(void)
{
#if defined(STM32G474xx)
    RCC->AHB1ENR |= RCC_AHB1ENR_CRCEN;
#else
    RCC->AHBENR |= RCC_AHBENR_CRCEN;
#endif
    CRC->CR = CRC_CR_RESET;                   /* к 0xFFFFFFFF; у G4 после сброса тот же вариант, что у F1 */
}
static void     crc_feed(uint32_t w)  { CRC->DR = w; }
static uint32_t crc_read(void)        { return CRC->DR; }
#endif

#define FLASH_STEP_WORDS 256u                 /* слов CRC за один шаг в работе */

/* При старте: CRC всего образа words слов сравнивается с эталоном, лежащим сразу за образом. */
int flash_check_all(const uint32_t *image, uint32_t words)
{
    uint32_t i;

    crc_reset();
    for (i = 0u; i < words; i++) { crc_feed(image[i]); }
    return crc_read() == image[words];
}

/* В работе — по FLASH_STEP_WORDS слов за вызов. 0 — идёт, 1 — проход сошёлся, −1 — отказ.
 * Блок CRC между шагами никто другой трогать не должен: его состояние — наш промежуточный итог. */
int flash_step(const uint32_t *image, uint32_t words)
{
    static uint32_t pos;
    uint32_t i = pos, end = pos + FLASH_STEP_WORDS;

    if (i == 0u) { crc_reset(); }
    if (end > words) { end = words; }
    for (; i < end; i++) { crc_feed(image[i]); }
    if (i < words) { pos = i; return 0; }
    pos = 0u;
    return (crc_read() == image[words]) ? 1 : -1;
}

/* ── Частота (G474): 32-битный TIM5 считает такты шины между фронтами LSE или LSI ── */
#if defined(STM32G474xx)
_Static_assert(offsetof(TIM_TypeDef, TISEL) == 0x5Cu, "TIMx_TISEL по RM0440 — смещение 0x5C");
#define TISEL_LSI 1u                          /* таблица 270 RM0440: у TIM5 tim_ti1_in1 — LSI */
#define TISEL_LSE 2u                          /* tim_ti1_in2 — LSE */

/* Сколько тактов таймера уложилось в 8 периодов опорной частоты; 0 — опора не пришла. */
uint32_t ticks_per_8(uint32_t tisel)
{
    uint32_t c1 = 0u, cap = 0u, k, t;

    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM5EN;
    TIM5->TISEL = tisel;
    TIM5->PSC = 0u;
    TIM5->ARR = 0xFFFFFFFFu;                  /* счётчик 32-битный: разность не переполнится */
    TIM5->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_IC1PSC;        /* вход TI1, захват на каждом 8-м фронте */
    TIM5->CCER = TIM_CCER_CC1E;
    TIM5->CR1 = TIM_CR1_CEN;
    for (k = 0u; k < 3u; k++) {               /* первый захват отбрасываем, берём разность двух следующих */
        for (t = 0u; (TIM5->SR & TIM_SR_CC1IF) == 0u; t++) {
            if (t > 2000000u) { return 0u; }  /* опора молчит: это тоже отказ */
        }
        cap = TIM5->CCR1;                     /* чтение CCR1 снимает CC1IF */
        if (k == 1u) { c1 = cap; }
    }
    return cap - c1;
}

int clock_ok(uint32_t ticks, uint32_t lo, uint32_t hi) { return ticks >= lo && ticks <= hi; }
#endif

Next: section 36в "Noise-robust firmware: traps in free Flash, empty vectors, program flow monitoring" — traps in free flash memory, a common vector handler and monitoring of the main loop flow.

Open in the interactive handbook