36в. Noise-robust firmware: traps in free Flash, empty vectors, program flow monitoring
Section 36б looked for a fault that stays: a stuck RAM bit, a corrupted flash byte, a drifted crystal. Noise usually breaks nothing permanently. A discharge or a supply transient flips a bit for an instant — in the program counter, in a register, in a RAM cell, in a peripheral register — and the program carries on, but down the wrong path. A millisecond later there is no trace, and the relay has already switched at the wrong time.
This section covers techniques that keep such a glitch from going unnoticed: free flash memory filled with a trap instruction; a vector table without a single empty slot and a common handler that records the vector number; program flow monitoring — checkpoints and a signature without which the watchdog is not fed; rewriting configuration registers from a reference; double storage of important variables; repeated reading of inputs. Everything converges in one safe-state function. Each technique is verified by fault injection in QEMU.
What is already covered is not repeated here: self-test of RAM, flash memory and clock — 36б and 12б, the task watchdog — 33а, the fault record and its analysis after reboot — 35в, configuration verification by checksum and protection of inputs and outputs in a product — 203.5, the safe state in C code — 129.7, the exception frame — 7а, weak handler aliases — 4в.4, the reset cause — 12.4.
References: reference manuals RM0008 (F1) and RM0090 (F4) — vector tables,
IWDG, AFIO_MAPR; the Cortex-M3 core guide (DUI0552A) — memory map,
the IPSR register, fault flags, escalation of faults to HardFault; the headers
stm32f10x.h and stm32f407xx.h from the packs and CMSIS. The code is built with
arm-none-eabi-gcc 12.2.1 for F103 and F407 and run in QEMU 7.2 on the machines
stm32vldiscovery (Cortex-M3) and netduinoplus2 (Cortex-M4); models run on a PC.
Terms used in the documentation
Term Meaning control flow monitoring checking that the program takes the right path checkpoint a place where the program marks that it has passed flow signature a number into which checkpoints mix their tokens unused memory fill filling free memory with a trap instruction permanently undefined instruction ( UDF)an instruction guaranteed to be undefined spurious interrupt an interrupt that arrived although nobody expected it ISR_NUMBER(IPSRregister)the number of the exception currently being serviced register refresh rewriting configuration registers from a reference majority vote, 2 out of 3 voting "two out of three" write-only bits bits whose written value cannot be read back
36в.1 What noise does to a running program
The glitch is short, the consequences are long. What can be corrupted and what catches the consequences:
| What got corrupted | What happens next | What catches it | Where |
|---|---|---|---|
| program counter: jump into free flash memory | the core executes whatever lies there | fill with a trap instruction | 36в.2 |
| program counter: jump into the peripheral region | instruction fetch fault | common fault handler | 36в.3 |
| program counter: jump inside its own code | steps skipped, repeated, reordered | flow monitoring | 36в.4 |
| peripheral register | divider, pin mode, block clock — wrong | rewrite from a reference | 36в.5 |
| RAM cell with an important variable | setpoint or mode is different | double storage | 36в.5 |
| spurious interrupt | a call of a vector nobody expected | common handler with the number | 36в.3 |
| input at the moment of reading | false sensor trigger | repeated reading | 36в.6 |
Corruption of a core register looks like any of these cases: a corrupted pointer means an access to the wrong place and a bus fault, a corrupted return address means the same program counter jump, a corrupted number means a wrong algorithm step. There is no separate technique for it; the core register test is in 12б.6.
All traps lead to one place — safe_state() (36в.6): switch outputs off,
record what happened, stop feeding the watchdog. The watchdog restarts
the chip, and the record survives the reset.
How it was verified. The test firmware steps through experiments: the experiment number lives
in .noinit, every glitch ends in the safe state and a reset, after
the reset the firmware prints the fault record and takes the next experiment. QEMU does not
model the IWDG, so in the experiment build safe_state(), after
a short pause, performs a software reset itself via SYSRESETREQ — where
on a board the watchdog would fire. Twelve glitches in a row — the counter in the record
reached 12, and the record survived all the resets. The experiments passed the same way on F1
and on F4.
36в.2 Free flash memory: a trap instead of emptiness
A program occupies a small part of flash memory. In the experiment the F103C8 image is 3488 bytes out of 65536, the F407 image 3576 bytes out of 1 MB. The rest is what the core will execute if the program counter lands there. What exactly depends on what the area is filled with:
| Filled with | How the disassembler decodes it | QEMU experiment: jump into the free area |
|---|---|---|
0xDE — this section's fill |
udf #222 |
immediate fault, the record has the jump address and the trap flag |
0xFF — erased flash memory |
0xffffffff — an undefined instruction |
immediate UNDEFINSTR fault, the jump address is in the record |
0x00 |
movs r0, r0 |
64086 movs instructions in a row to the end of flash memory, an IBUSERR fault at address 0x08020000 |
Zeros are the worst case: movs r0, r0 corrupts nothing, and the core slides along
them until it runs into the next code — or into the end of flash memory, as
in the experiment. Where the jump started can no longer be found afterwards: the record has only the end of
the path. Zeros appear in two places. The linker fills
alignment gaps inside sections with them unless told otherwise: in the experiment
image there are 6 such gaps, 11 bytes in total. And QEMU reads zeros where
the image is not written; on silicon there is 0xFF after erase — or an old
program, if the bootloader erased only the pages for the new one.
Erased memory also catches the jump, but silently — it is an ordinary HardFault, as
from any error. The 0xDE fill gives a signature: the halfword 0xDEDE is the instruction
udf #222, "permanently undefined", and the fault handler, looking at the instruction
address, sees that the core was executing the fill. The byte is always the same,
so whichever halfword you start from, the very first instruction is a trap.
The fill is done by the linker script. Gaps inside code sections —
=0xDEDEDEDE after the region name; everything from the end of the image to the end
of flash memory is a separate section:
.trap_fill __image_end :
{
__trap_start = .;
LONG(0xDEDEDEDE) /* хотя бы одно слово данных: иначе секция пустая и в образ не попадёт */
. = __flash_end - __image_end; /* внутри секции «.» — смещение от её начала */
} > FLASH =0xDEDEDEDE
The LONG word is mandatory: a section made only of assignments to "." gets only
space, without data, and does not go into the image (36в.7). After the build the script
image36v.py parses the ELF and the .bin byte by byte:
| Build | File | Free area | Gaps inside sections |
|---|---|---|---|
| F103C8, fill by the linker | .bin 65536 bytes |
62048 bytes, all 0xDE |
11 bytes, all 0xDE |
F103C8 without fill, objcopy --gap-fill 0xDE --pad-to |
.bin 65536 bytes |
62048 bytes, all 0xDE |
11 bytes, all 0x00 |
F103C8 without fill, plain objcopy -O binary |
.bin 3488 bytes |
not in the file | 11 bytes, all 0x00 |
| F407, fill by the linker | .bin 1048576 bytes |
1045000 bytes, all 0xDE |
11 bytes, all 0xDE |
The second way — objcopy — fills only what lies between sections
and after the image. It does not touch gaps inside sections: they are already in the file,
and they contain zeros. The third way is common: the file contains only the image itself, and what
ends up after it in flash memory is decided by the programmer. That is why the fill is
done in the linker script: it gets into the ELF, the .hex and the .bin alike.
In the experiment a jump 256 bytes past the end of the image and a jump to the last halfword
of flash memory (0x0801FFFE: the QEMU stm32vldiscovery machine has 128 KB of flash, on the F103C8
the end is 0x0800FFFE) ended the same way: HardFault, CFSR =
0x00010000 (UNDEFINSTR), a record of type "trap" with the exact address.
36в.3 Vectors: not a single empty slot
The F103 vector table according to RM0008 is 16 core exceptions and interrupts with numbers up to 59; on the F407 according to RM0090 up to 81. The experiment table has 76 slots on F1 and 98 on F4, and each holds a handler address:
__attribute__((section(".isr_vector"), used))
const vec_t vectors[16 + NIRQ] = {
[0] = (vec_t)(uintptr_t)_estack,
[1] = Reset_Handler,
[2] = Unused_Handler, /* NMI */
[3] = HardFault_Handler, /* MemManage, BusFault, UsageFault выключены и приходят сюда */
[4 ... 15] = Unused_Handler, /* остальные исключения ядра и зарезервированные места */
[16 ... 15 + NIRQ] = Unused_Handler, /* все прерывания: своих обработчиков в опыте нет */
};
The experiment has no handlers of its own, so all interrupts go to one. When a USART handler appears in the program, its slot is written on a separate line, splitting the range. The weak aliases from 4в.4 achieve the same thing another way; what matters is not that, but that the common handler is not silent like an empty loop, but records who called it.
The number is taken from the IPSR register: according to DUI0552A its ISR_NUMBER field is the number
of the current exception, 16 and above are interrupts. HardFault is separate:
by the address of the instruction on which the fault occurred, it recognizes the trap from 36в.2:
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile("tst lr, #4\n\t" /* каким стеком пользовался прерванный код */
"ite eq\n\t"
"mrseq r0, msp\n\t"
"mrsne r0, psp\n\t"
"b fault_c");
}
void fault_c(const uint32_t *frame)
{
uint32_t pc = frame[6]; /* адрес команды, на которой случился отказ (7а) */
uint32_t kind = FAULT_HARD;
if (pc >= (uint32_t)__flash_start && pc < (uint32_t)__flash_end && (pc & 1u) == 0u
&& *(const volatile uint16_t *)pc == TRAP_HALF)
kind = FAULT_TRAP; /* ядро выполнило заливку свободной флеш-памяти */
safe_state(kind, pc, SCB_CFSR);
}
void Unused_Handler(void) /* все векторы, для которых нет своего обработчика */
{
uint32_t n;
__asm volatile("mrs %0, ipsr" : "=r"(n)); /* номер исключения, которое сюда привело */
safe_state(FAULT_VECTOR, n & 0x1FFu, 0u);
}
frame[6] is the return address in the exception frame (7а.1); for an undefined
instruction it is the address of that instruction. Reading *(uint16_t *)pc happens only after
checking that the address lies in flash memory and is even: reading from
a random address inside HardFault means causing a second fault where
nobody is left to catch it. MemManage, BusFault and UsageFault are disabled after reset,
and according to DUI0552A a fault whose handler is not enabled
is escalated to HardFault — so one handler is enough for all of them.
Three experiments:
| Experiment | Record after reset | What it shows |
|---|---|---|
| IRQ 23 enabled and set pending, no handler | type "vector", number 0x27 |
39 = 16 + 23: which interrupt arrived |
| the same, but with zero in the IRQ 23 slot of a table copy | type "fault", PC 0x00000000, CFSR = 0x00020000 |
INVSTATE: a branch without the Thumb bit; which interrupt — impossible to tell |
jump to address 0x40000000 (peripherals) |
type "fault", PC 0x40000000, CFSR = 0x00000001 |
IACCVIOL: the peripheral region in the memory map is execute-never |
A zero in the table is not "do nothing". According to DUI0552A the least significant bit of every
vector must be one, otherwise the core tries to switch to ARM state,
which Cortex-M does not have. A fault happens anyway, but the record keeps
only PC = 0. The common handler names the number — it shows which block
raised a flag uninvited: the wrong interrupt enabled, a flag that
nobody clears, or a bit in NVIC_ISER flipped by noise.
36в.4 Flow monitoring: checkpoints and a signature
A jump inside the program's own code is invisible to the trap: there are real instructions there. Something else is visible — the main loop pass took the wrong path. Each checkpoint mixes its token into the signature — a rotate by 7 and an "exclusive or":
#define FLOW_SEED 0x5EED1234u
#define FLOW_STEP(s, t) ((((s) << 7) | ((s) >> 25)) ^ (t)) /* сдвиг по кругу и «исключающее или» */
enum { CP_IN = 0x11A5u, CP_CALC = 0x22C3u, CP_OUT = 0x4496u, CP_DIAG = 0x8869u };
#define FLOW_END FLOW_STEP(FLOW_STEP(FLOW_STEP(FLOW_STEP(FLOW_SEED, CP_IN), CP_CALC), CP_OUT), CP_DIAG)
FLOW_END is the signature of the correct path; the compiler calculates it itself:
0x79DCD248. The checkpoint and the check at the end of a pass:
void flow_cp(uint32_t tok) /* проверочная точка: подмешать свой токен */
{
uint32_t s = flow_sig;
if (s != ~flow_inv) /* сама сигнатура испорчена */
safe_state(FAULT_FLOW, s, flow_inv);
s = FLOW_STEP(s, tok);
flow_sig = s;
flow_inv = ~s;
}
int flow_end(void) /* конец прохода: весь ли путь пройден и по порядку */
{
int ok = (flow_sig == FLOW_END) && (flow_inv == ~(uint32_t)FLOW_END);
flow_sig = FLOW_SEED;
flow_inv = ~FLOW_SEED;
return ok;
}
The signature is stored twice — value and inverse (36в.5): noise that hits the signature itself is visible at the very next checkpoint. The main loop:
uint32_t main_pass(void) /* один проход главного цикла */
{
uint32_t in, on, fixed, sig;
flow_cp(CP_IN);
in = in_read3();
flow_cp(CP_CALC);
on = ((in & IN_MASK) != 0u) && (mir_get(MIR_MODE) == MODE_RUN);
flow_cp(CP_OUT);
OUT_PUT(on);
flow_cp(CP_DIAG);
fixed = cfg_refresh(cfg_table, cfg_count);
sig = flow_sig;
if (!flow_end())
safe_state(FAULT_FLOW, sig, FLOW_END);
wdg_feed(); /* только если весь путь пройден по порядку */
return fixed;
}
The watchdog is fed in one place — after the whole path has been passed in
order. A jump straight to the feed will feed the watchdog once, but it also skips
flow_end(), which returns the signature to FLOW_SEED. The next
pass starts from a corrupted initial value and does not match — which is exactly what happened in the experiment.
Interrupt handlers and OS tasks are not built into the same checkpoint chain:
they have their own rhythm and are checked with marks and deadlines — 33а.
Experiments in QEMU — each time at the feed point:
| Experiment | Signature in the record | Watchdog |
|---|---|---|
| 1000 correct passes | — | fed 1000 times |
checkpoint CP_CALC skipped |
0x42C05ECB instead of 0x79DCD248 |
not fed |
CP_CALC executed twice |
0xF79A93D5 |
not fed |
CP_CALC and CP_OUT swapped |
0x607AB8C8 |
not fed |
bit 9 of the signature flipped after CP_IN |
0x7689098A, inverse 0x8976F475 — differ exactly in bit 9 |
failure at the next checkpoint |
jump after CP_CALC straight to wdg_feed(), then a correct pass |
0xB87A5D14 |
fed once, failure on the next pass |
Why exactly a rotate and an "exclusive or" is shown by exhaustive search on a PC: all checkpoint sequences of length 0 to 8 from the same four tokens — 87380 wrong ones:
| Scheme | Wrong paths with the same mark |
|---|---|
| checkpoint counter (did it reach four) | 255 |
| sum of tokens | 23 — all permutations |
rotate and "exclusive or" (FLOW_STEP) |
0 |
The counter does not care which checkpoints were passed, as long as there are four of them. The sum does not see order. The rotate makes the contribution of each checkpoint depend on what came before it.
36в.5 Settings from a reference and double storage
Configuration registers. Noise flipped a bit of the USART divider — the program is intact, the watchdog is fed, and the link runs at a different speed. In 203.5 the reference is captured into RAM after initialization and verified by a check sum. Here the reference is a table of constants in flash memory: it is not corrupted by the same noise that corrupts RAM, and if it does get corrupted, the image CRC catches it (36б.5). The table holds an address, a mask of the owned bits and their value:
typedef struct {
volatile uint32_t *reg; /* адрес регистра */
uint32_t mask; /* какие биты наши */
uint32_t val; /* их значение */
} cfg_t;
const cfg_t cfg_table[] = { /* F103: тактирование, вывод PC13, USART1 */
{ &RCC_APB2ENR, (1u << 14) | (1u << 4), (1u << 14) | (1u << 4) }, /* USART1EN, IOPCEN */
{ &GPIOC_CRH, 0x00F00000u, 0x00200000u }, /* PC13: выход 2 МГц, двухтактный */
{ &USART1_BRR, 0xFFFFu, BRR_115200 },
{ &USART1_CR1, CR1_UE | CR1_TE, CR1_UE | CR1_TE },
{ &SCB_VTOR, 0xFFFFFF80u, 0x08000000u },
};
uint32_t cfg_refresh(const cfg_t *t, uint32_t n)
{
uint32_t i, fixed = 0u;
for (i = 0u; i < n; i++) {
uint32_t now = *t[i].reg;
uint32_t want = (now & ~t[i].mask) | t[i].val; /* чужие биты не трогаем */
if (now != want) {
*t[i].reg = want;
fixed++;
}
}
return fixed; /* сколько регистров пришлось вернуть — в журнал */
}
The mask is needed so as not to touch other people's bits: flags that the
hardware changes itself, bits controlled by another module. Only what has
diverged is rewritten, and the number of corrections goes into the log: a device that
repairs its divider once an hour is standing next to strong noise. In the experiment bit 8
of the USART1_BRR divider was flipped (0x0145 instead of 0x0045), and VTOR
was zeroed: one call restored 2 registers, the next one 0.
QEMU does not model the GPIO and RCC registers — writes to them are lost,
reads return zero — so in the experiment build the table has three rows:
USART1_BRR, USART1_CR1, VTOR. The tables for F103 and F407 are built, but
not verified on a board.
Double storage. A setpoint, an operating mode, the step number of a state machine are stored
twice: the value and its inverse. The copies are spread across RAM — the values in .bss
at the beginning, the inverses in their own section at the end, on the other side of the stack:
volatile uint32_t mir_val[MIR_N]; /* прямые значения — в .bss */
volatile uint32_t mir_inv[MIR_N] __attribute__((section(".mirror"))); /* инверсии — по ту сторону стека */
void mir_set(uint32_t k, uint32_t x)
{
mir_val[k] = x;
mir_inv[k] = ~x;
}
uint32_t mir_get(uint32_t k)
{
uint32_t v = mir_val[k], i = mir_inv[k];
if (v != ~i)
safe_state(FAULT_MIRROR, k, v ^ ~i); /* в записи — какие биты разошлись */
return v;
}
Experiments:
| What was corrupted | Read |
|---|---|
| bit 4 of the value | failure, the record has index 0 and the differing bits 0x00000010 |
| bit 4 of both the value and the inverse | read 1218 instead of 1234 — not noticed |
| after that bit 4 of the inverse was restored | failure, bits 0x00000010 |
The check does not see the same bit in both copies: the value and the inverse are corrupted consistently. That is why the copies are kept far apart, not in neighbouring words of one structure: one noise event rarely hits the same bit of two distant words. An inverse rather than a plain copy guards against stuck bits: if a data line or a cell is stuck at zero, two identical copies will match, but a value and its inverse will not.
36в.6 Inputs, the safe state and the cost
Repeated reading of an input. Noise on a sensor wire is shorter than the polling period. The input is read three times with a pause, and each bit is taken by two out of three:
uint32_t in_read3(void)
{
uint32_t a, b, c;
a = IN_PORT;
in_gap();
b = IN_PORT;
in_gap();
c = IN_PORT;
return (a & b) | (a & c) | (b & c); /* по каждому биту — два из трёх */
}
Model on a PC: the input is zero, the noise is a one of length L, the pause between reads is G = 10; all noise positions that touch the read window are enumerated:
| Noise length L | Positions | Single read: false | Two out of three: false |
|---|---|---|---|
| 5 | 25 | 5 | 0 |
| 10 | 30 | 10 | 0 |
| 15 | 35 | 15 | 10 |
| 20 | 40 | 20 | 20 |
| 25 | 45 | 25 | 25 |
Noise no longer than the pause is always suppressed by voting. Noise of two pauses or longer is never suppressed: it covers two reads out of three. The pause is chosen by the longest noise that has to be survived, and beyond that a filter over several passes of the main loop is used.
The safe state. One function for all cases:
void safe_state(uint32_t kind, uint32_t a, uint32_t b)
{
__asm volatile("cpsid i" ::: "memory"); /* дальше ничто не должно вмешаться */
OUT_SAFE(); /* выходы — в безопасное состояние первым делом */
if (!rec_valid())
fault_rec.count = 0u; /* записи нет или она битая: счёт заново */
fault_rec.magic = REC_MAGIC;
fault_rec.kind = kind;
fault_rec.a = a;
fault_rec.b = b;
fault_rec.count++;
fault_rec.check = rec_sum(&fault_rec);
#ifdef QEMU_RUN
for (volatile uint32_t i = 0u; i < 1000u; i++) { /* опыт: сторожа в QEMU нет — сброс вместо него */
}
__asm volatile("dsb" ::: "memory");
SCB_AIRCR = AIRCR_RESET;
#endif
for (;;) { /* сторож не кормится: сброс через его таймаут */
}
}
Outputs come first, before the record: the record can be written later, but the relay
must release at once. The fault record with a check word lives in .noinit
(35в.2) — the start-up code does not clear it. The check word is essential: after
power-up there are random numbers there. The fault count is kept in the record:
in the experiments it reached 12 over 12 resets. After a reset the firmware reads
the record and the reset cause flags (12.4, 35в.4); a recurring fault is a reason to
stay in the safe state instead of restarting in a loop (36б.6).
The cost. Sizes are from the F103 build (-Os), instruction counts from QEMU
with -icount on F4, minus an empty call:
| What | Code bytes | Instructions per call |
|---|---|---|
HardFault_Handler and fault_c |
18 + 60 | — |
Unused_Handler |
18 | — |
safe_state and rec_valid |
80 + 56 | — |
flow_cp — one checkpoint |
40 | 10 |
flow_end — end of a pass |
60 | 10 |
mir_set / mir_get |
24 / 44 | 4 / 8 |
cfg_refresh, 3 registers; F103 table of 5 rows |
44 + 60 | 48 |
in_read3 with two in_gap pauses of 20 iterations |
40 + 22 | 305, of which pauses 146 × 2 |
wdg_feed |
28 | — |
The code of all techniques is 594 bytes; RAM is 24 bytes of record, 8 bytes of signature with its inverse and 8 bytes for each value–inverse pair. A pass of the experiment's main loop is 456 instructions, of which 292 are the two pauses between input reads; the checks themselves (four checkpoints, end of pass, rewriting three registers, reading a copy) are 106 instructions, at least 0.63 µs at 168 MHz and 1.5 µs at 72 MHz. These are lower bounds: an instruction takes at least one cycle. The fill is the most expensive: the whole flash memory is programmed, 62048 bytes on the F103C8 and 1045000 bytes on the F407 instead of an image of a few kilobytes.
36в.7 Pitfalls
The fill section turned out empty. Without the LONG word the .trap_fill section
gets space — 62048 bytes — but no data: objdump -h shows
only ALLOC, without CONTENTS, and it does not go into the image. The flash memory after
the image stays as it was.
Inside a section "." is an offset, not an address. The line . = __flash_end;
inside a section gave the address 0x10010DA0 and the error "not within region FLASH":
the linker added the end-of-flash address to the start of the section. You need
the difference: . = __flash_end - __image_end;.
--gap-fill did not fill everything. objcopy fills gaps between
sections and the tail up to --pad-to, but not alignment gaps inside sections:
11 bytes in the experiment image remained zeros. The fill belongs in the linker script.
A .hex was flashed, and there is no fill. A .hex and a plain .bin contain only
the image; what comes after it depends on which pages the programmer
or bootloader erased. The fill in the ELF goes into flash memory together with the image.
The fill took the space of the bootloader or of EEPROM emulation. .trap_fill
extends to the end of flash memory. Pages that the program writes itself
are moved into a separate region of the linker script and excluded from the fill.
A write-only bit in the reference table. On the F103 the SWJ_CFG field
in AFIO_MAPR is write-only according to RM0008: the value read back is
undefined. cfg_refresh() would "repair" it on every pass and
flood the log. Such fields are not put in the table — they are written once
at start-up or written every time without comparison.
The flow mark was reduced to a counter. A checkpoint counter misses 255 wrong paths out of 87380, a sum of tokens 23. You need a function in which order matters.
Copies next to each other. A value and its inverse in neighbouring words of one structure are corrupted by a single write past the end of an array. In the experiment a consistent corruption of one bit went unnoticed.
The pause between reads is shorter than the noise. Three reads in a row without a pause fall into the same noise and vote for it together: at L = 2G the vote was wrong as many times as a single read (20 of 40).
An empty vector instead of a handler. A zero in the table gives INVSTATE and
PC = 0: the device restarts, but from the record you cannot tell which interrupt
arrived. The common handler names the number.
36в.8 Summary
- Noise corrupts a bit for an instant; you catch not the noise but its consequences: a program counter jump, a skipped step, a wrong value in a register or a cell.
- Free flash memory — the byte
0xDE: the halfword0xDEDE=udf #222. 62048 bytes on the F103C8 are filled entirely, image analysis confirmed every byte; a jump into the fill is a HardFault with the trap flag and the exact address. Over zeros the core slid through 64086 instructions, and the jump address was lost. - Vector table — 76 slots on F1 and 98 on F4, not a single zero; the common
handler takes the number from
IPSR: a spurious IRQ 23 is recorded as 39. A zero in the table gaveINVSTATEwithout a number. - Flow monitoring: four checkpoints, a signature by rotate and "exclusive or",
the watchdog is fed only at
FLOW_END. Skip, repeat, reorder and corruption of the signature itself are caught; in the search over 87380 wrong paths matches with the correct one — 0 (counter — 255, sum — 23). - Settings — from a reference table in flash memory with masks: after corruption
of the divider and
VTOR, 2 registers were restored. - Double storage: a flipped bit is visible on read; the same bit in both copies is not, so the copies are spread across RAM.
- Input — three reads and a vote: noise no longer than the pause does not get through.
- The cost of the checks is 106 instructions per pass and 594 bytes of code; the most expensive is the fill — the whole flash memory is programmed.
Full sources
robust36v.h — common header.
/* Раздел 36в: помехоустойчивая прошивка — общий заголовок.
* Адреса регистров сверяет hdr36v.c с заголовками паков (stm32f10x.h, stm32f407xx.h);
* регистры ядра — с DUI0552A. F1 — STM32F103 (в QEMU stm32vldiscovery), иначе — STM32F407
* (в QEMU netduinoplus2, F405). Ключ QEMU_RUN — сборка для опыта: сторож заменён сбросом. */
#ifndef ROBUST36V_H
#define ROBUST36V_H
#include <stdint.h>
#ifndef REG
#define REG(a) (*(volatile uint32_t *)(a))
#endif
#if defined(F1)
#define NIRQ 60u /* F103: прерывания 0…59 (DMA2_Channel4_5 — 59) */
#define U1_BASE 0x40013800u
#define BRR_115200 0x0045u /* HSI 8 МГц / 115200 */
#define RCC_APB2ENR REG(0x40021018u)
#define GPIOA_IDR REG(0x40010808u)
#define GPIOC_CRH REG(0x40011004u)
#define GPIOC_BSRR REG(0x40011010u)
#define AFIO_MAPR REG(0x40010004u)
#define OUT_SAFE() (GPIOC_BSRR = 0xFFFFu << 16) /* все выходы порта C — в ноль */
#define OUT_PUT(on) (GPIOC_BSRR = (on) ? (1u << 13) : (1u << 29)) /* PC13 */
#else
#define NIRQ 82u /* F407: прерывания 0…81 (FPU — 81) */
#define U1_BASE 0x40011000u
#define BRR_115200 0x008Bu /* HSI 16 МГц / 115200 */
#define RCC_AHB1ENR REG(0x40023830u)
#define RCC_APB2ENR REG(0x40023844u)
#define GPIOA_IDR REG(0x40020010u)
#define GPIOD_MODER REG(0x40020C00u)
#define GPIOD_BSRR REG(0x40020C18u)
#define OUT_SAFE() (GPIOD_BSRR = 0xFFFFu << 16) /* все выходы порта D — в ноль */
#define OUT_PUT(on) (GPIOD_BSRR = (on) ? (1u << 12) : (1u << 28)) /* PD12 */
#endif
#ifndef IN_PORT
#define IN_PORT GPIOA_IDR
#endif
#define IN_MASK 0x0001u /* PA0 — датчик */
#define USART1_SR REG(U1_BASE + 0x00u)
#define USART1_DR REG(U1_BASE + 0x04u)
#define USART1_BRR REG(U1_BASE + 0x08u)
#define USART1_CR1 REG(U1_BASE + 0x0Cu)
#define CR1_UE (1u << 13)
#define CR1_TE (1u << 3)
#define IWDG_KR REG(0x40003000u)
#define SCB_ICSR REG(0xE000ED04u)
#define SCB_VTOR REG(0xE000ED08u)
#define SCB_AIRCR REG(0xE000ED0Cu)
#define SCB_SHCSR REG(0xE000ED24u)
#define SCB_CFSR REG(0xE000ED28u)
#define SCB_HFSR REG(0xE000ED2Cu)
#define NVIC_ISER0 REG(0xE000E100u)
#define NVIC_ISPR0 REG(0xE000E200u)
#define SYST_CSR REG(0xE000E010u)
#define SYST_RVR REG(0xE000E014u)
#define SYST_CVR REG(0xE000E018u)
#define AIRCR_RESET 0x05FA0004u /* VECTKEY + SYSRESETREQ */
/* ── ловушка в свободной флеш-памяти ── */
#define TRAP_BYTE 0xDEu /* полуслово 0xDEDE — команда udf #222 */
#define TRAP_HALF 0xDEDEu
/* ── виды сбоя в записи ── */
enum { FAULT_NONE = 0, FAULT_TRAP = 1, FAULT_HARD = 2, FAULT_VECTOR = 3, FAULT_FLOW = 4, FAULT_MIRROR = 5 };
typedef struct { /* запись о сбое: в .noinit, переживает сброс (35в) */
uint32_t magic, kind, a, b, count, check;
} fault_rec_t;
#define REC_MAGIC 0x52364256u
extern fault_rec_t fault_rec;
int rec_valid(void);
/* ── контроль хода ── */
#define FLOW_SEED 0x5EED1234u
#define FLOW_STEP(s, t) ((((s) << 7) | ((s) >> 25)) ^ (t)) /* сдвиг по кругу и «исключающее или» */
enum { CP_IN = 0x11A5u, CP_CALC = 0x22C3u, CP_OUT = 0x4496u, CP_DIAG = 0x8869u };
#define FLOW_END FLOW_STEP(FLOW_STEP(FLOW_STEP(FLOW_STEP(FLOW_SEED, CP_IN), CP_CALC), CP_OUT), CP_DIAG)
extern volatile uint32_t flow_sig, flow_inv;
void flow_cp(uint32_t tok);
int flow_end(void);
/* ── двойное хранение ── */
enum { MIR_SETPOINT, MIR_MODE, MIR_N };
#define MODE_RUN 0x0000A55Au
extern volatile uint32_t mir_val[MIR_N], mir_inv[MIR_N];
void mir_set(uint32_t k, uint32_t x);
uint32_t mir_get(uint32_t k);
/* ── перезапись настроек из эталона ── */
typedef struct {
volatile uint32_t *reg; /* адрес регистра */
uint32_t mask; /* какие биты наши */
uint32_t val; /* их значение */
} cfg_t;
uint32_t cfg_refresh(const cfg_t *t, uint32_t n);
extern const cfg_t cfg_table[];
extern const uint32_t cfg_count;
/* ── входы ── */
uint32_t in_read3(void);
void in_gap(void);
#ifndef IN_GAP_LOOPS
#define IN_GAP_LOOPS 20u /* пауза между чтениями: подобрать под длину помехи */
#endif
/* ── отказ ── */
void safe_state(uint32_t kind, uint32_t a, uint32_t b) __attribute__((noreturn));
void wdg_feed(void);
extern uint32_t wdg_feeds;
uint32_t main_pass(void);
/* вывод (app36v.c) */
void puts1(const char *s);
void puthex(uint32_t v);
void putu(uint32_t v);
#endif
robust36v.c — fault handler, flow monitoring, double storage, safe state.
/* Раздел 36в: помехоустойчивая прошивка — общий обработчик сбоев, ловушка свободной
* флеш-памяти, контроль хода, двойное хранение, перезапись настроек, безопасное состояние.
* Листинги раздела взяты из этого файла дословно (robust36v_check.py сверяет). */
#include "robust36v.h"
extern const uint16_t __flash_start[], __flash_end[]; /* границы флеш-памяти: из скрипта компоновки */
fault_rec_t fault_rec __attribute__((section(".noinit")));
volatile uint32_t flow_sig = FLOW_SEED, flow_inv = ~FLOW_SEED;
volatile uint32_t mir_val[MIR_N]; /* прямые значения — в .bss */
volatile uint32_t mir_inv[MIR_N] __attribute__((section(".mirror"))); /* инверсии — по ту сторону стека */
uint32_t wdg_feeds;
static uint32_t rec_sum(const fault_rec_t *r)
{
return ~(r->magic ^ r->kind ^ r->a ^ r->b ^ r->count);
}
int rec_valid(void)
{
return fault_rec.magic == REC_MAGIC && fault_rec.check == rec_sum(&fault_rec);
}
void safe_state(uint32_t kind, uint32_t a, uint32_t b)
{
__asm volatile("cpsid i" ::: "memory"); /* дальше ничто не должно вмешаться */
OUT_SAFE(); /* выходы — в безопасное состояние первым делом */
if (!rec_valid())
fault_rec.count = 0u; /* записи нет или она битая: счёт заново */
fault_rec.magic = REC_MAGIC;
fault_rec.kind = kind;
fault_rec.a = a;
fault_rec.b = b;
fault_rec.count++;
fault_rec.check = rec_sum(&fault_rec);
#ifdef QEMU_RUN
for (volatile uint32_t i = 0u; i < 1000u; i++) { /* опыт: сторожа в QEMU нет — сброс вместо него */
}
__asm volatile("dsb" ::: "memory");
SCB_AIRCR = AIRCR_RESET;
#endif
for (;;) { /* сторож не кормится: сброс через его таймаут */
}
}
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile("tst lr, #4\n\t" /* каким стеком пользовался прерванный код */
"ite eq\n\t"
"mrseq r0, msp\n\t"
"mrsne r0, psp\n\t"
"b fault_c");
}
void fault_c(const uint32_t *frame)
{
uint32_t pc = frame[6]; /* адрес команды, на которой случился отказ (7а) */
uint32_t kind = FAULT_HARD;
if (pc >= (uint32_t)__flash_start && pc < (uint32_t)__flash_end && (pc & 1u) == 0u
&& *(const volatile uint16_t *)pc == TRAP_HALF)
kind = FAULT_TRAP; /* ядро выполнило заливку свободной флеш-памяти */
safe_state(kind, pc, SCB_CFSR);
}
void Unused_Handler(void) /* все векторы, для которых нет своего обработчика */
{
uint32_t n;
__asm volatile("mrs %0, ipsr" : "=r"(n)); /* номер исключения, которое сюда привело */
safe_state(FAULT_VECTOR, n & 0x1FFu, 0u);
}
void flow_cp(uint32_t tok) /* проверочная точка: подмешать свой токен */
{
uint32_t s = flow_sig;
if (s != ~flow_inv) /* сама сигнатура испорчена */
safe_state(FAULT_FLOW, s, flow_inv);
s = FLOW_STEP(s, tok);
flow_sig = s;
flow_inv = ~s;
}
int flow_end(void) /* конец прохода: весь ли путь пройден и по порядку */
{
int ok = (flow_sig == FLOW_END) && (flow_inv == ~(uint32_t)FLOW_END);
flow_sig = FLOW_SEED;
flow_inv = ~FLOW_SEED;
return ok;
}
void mir_set(uint32_t k, uint32_t x)
{
mir_val[k] = x;
mir_inv[k] = ~x;
}
uint32_t mir_get(uint32_t k)
{
uint32_t v = mir_val[k], i = mir_inv[k];
if (v != ~i)
safe_state(FAULT_MIRROR, k, v ^ ~i); /* в записи — какие биты разошлись */
return v;
}
uint32_t cfg_refresh(const cfg_t *t, uint32_t n)
{
uint32_t i, fixed = 0u;
for (i = 0u; i < n; i++) {
uint32_t now = *t[i].reg;
uint32_t want = (now & ~t[i].mask) | t[i].val; /* чужие биты не трогаем */
if (now != want) {
*t[i].reg = want;
fixed++;
}
}
return fixed; /* сколько регистров пришлось вернуть — в журнал */
}
#if defined(QEMU_RUN)
const cfg_t cfg_table[] = { /* опыт: только то, что QEMU моделирует */
{ &USART1_BRR, 0xFFFFu, BRR_115200 },
{ &USART1_CR1, CR1_UE | CR1_TE, CR1_UE | CR1_TE },
{ &SCB_VTOR, 0xFFFFFF80u, 0x08000000u },
};
#elif defined(F1)
const cfg_t cfg_table[] = { /* F103: тактирование, вывод PC13, USART1 */
{ &RCC_APB2ENR, (1u << 14) | (1u << 4), (1u << 14) | (1u << 4) }, /* USART1EN, IOPCEN */
{ &GPIOC_CRH, 0x00F00000u, 0x00200000u }, /* PC13: выход 2 МГц, двухтактный */
{ &USART1_BRR, 0xFFFFu, BRR_115200 },
{ &USART1_CR1, CR1_UE | CR1_TE, CR1_UE | CR1_TE },
{ &SCB_VTOR, 0xFFFFFF80u, 0x08000000u },
};
#else
const cfg_t cfg_table[] = { /* F407: тактирование, вывод PD12, USART1 */
{ &RCC_AHB1ENR, 1u << 3, 1u << 3 }, /* GPIODEN */
{ &RCC_APB2ENR, 1u << 4, 1u << 4 }, /* USART1EN */
{ &GPIOD_MODER, 3u << 24, 1u << 24 }, /* PD12: выход */
{ &USART1_BRR, 0xFFFFu, BRR_115200 },
{ &USART1_CR1, CR1_UE | CR1_TE, CR1_UE | CR1_TE },
{ &SCB_VTOR, 0xFFFFFF80u, 0x08000000u },
};
#endif
const uint32_t cfg_count = sizeof cfg_table / sizeof cfg_table[0];
void wdg_feed(void)
{
IWDG_KR = 0xAAAAu; /* перезагрузить счётчик IWDG (12.2) */
wdg_feeds++;
}
void in_gap(void) /* пауза между чтениями входа */
{
for (volatile uint32_t i = 0u; i < IN_GAP_LOOPS; i++) {
}
}
uint32_t main_pass(void) /* один проход главного цикла */
{
uint32_t in, on, fixed, sig;
flow_cp(CP_IN);
in = in_read3();
flow_cp(CP_CALC);
on = ((in & IN_MASK) != 0u) && (mir_get(MIR_MODE) == MODE_RUN);
flow_cp(CP_OUT);
OUT_PUT(on);
flow_cp(CP_DIAG);
fixed = cfg_refresh(cfg_table, cfg_count);
sig = flow_sig;
if (!flow_end())
safe_state(FAULT_FLOW, sig, FLOW_END);
wdg_feed(); /* только если весь путь пройден по порядку */
return fixed;
}
start36v.c — a vector table without empty slots and the start-up code.
/* Раздел 36в: таблица векторов и стартовый код. Каждый вектор, для которого нет своего
* обработчика, — в Unused_Handler; ни одного нулевого места, включая зарезервированные. */
#include "robust36v.h"
extern uint32_t _estack[], _sidata[], _sdata[], _edata[], _sbss[], _ebss[];
void Reset_Handler(void);
void HardFault_Handler(void);
void Unused_Handler(void);
int main(void);
typedef void (*vec_t)(void);
__attribute__((section(".isr_vector"), used))
const vec_t vectors[16 + NIRQ] = {
[0] = (vec_t)(uintptr_t)_estack,
[1] = Reset_Handler,
[2] = Unused_Handler, /* NMI */
[3] = HardFault_Handler, /* MemManage, BusFault, UsageFault выключены и приходят сюда */
[4 ... 15] = Unused_Handler, /* остальные исключения ядра и зарезервированные места */
[16 ... 15 + NIRQ] = Unused_Handler, /* все прерывания: своих обработчиков в опыте нет */
};
void Reset_Handler(void)
{
uint32_t *s = _sidata, *d = _sdata;
while (d < _edata)
*d++ = *s++;
for (d = _sbss; d < _ebss; d++)
*d = 0u;
SCB_VTOR = 0x08000000u;
main();
for (;;) {
}
}
input36v.c — repeated input reading.
/* Раздел 36в: повторное чтение входа. На ПК (host36v.c) IN_PORT и in_gap() — модель
* сигнала с помехой; в прошивке — GPIOA_IDR и пауза из robust36v.c. */
#include "robust36v.h"
uint32_t in_read3(void)
{
uint32_t a, b, c;
a = IN_PORT;
in_gap();
b = IN_PORT;
in_gap();
c = IN_PORT;
return (a & b) | (a & c) | (b & c); /* по каждому биту — два из трёх */
}
link36v.ld — linker script that fills free flash memory with a trap.
/* Раздел 36в: скрипт компоновки с заливкой свободной флеш-памяти командой-ловушкой.
* Промежутки внутри секций кода и всё от конца образа до конца флеш-памяти — байт 0xDE:
* полуслово 0xDEDE — команда udf #222. Варианты для опыта (без заливки, 0xFF) делает run36v.sh. */
ENTRY(Reset_Handler)
INCLUDE mem.ld
_estack = ORIGIN(MIRROR); /* стек растёт вниз от инверсных копий */
__flash_start = ORIGIN(FLASH);
__flash_end = ORIGIN(FLASH) + LENGTH(FLASH);
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH =0xDEDEDEDE
.text :
{
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
} > FLASH =0xDEDEDEDE
.data :
{
_sdata = .;
*(.data .data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
_sidata = LOADADDR(.data);
__image_end = LOADADDR(.data) + SIZEOF(.data);
.trap_fill __image_end :
{
__trap_start = .;
LONG(0xDEDEDEDE) /* хотя бы одно слово данных: иначе секция пустая и в образ не попадёт */
. = __flash_end - __image_end; /* внутри секции «.» — смещение от её начала */
} > FLASH =0xDEDEDEDE
.bss (NOLOAD) :
{
_sbss = .;
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
.mirror (NOLOAD) :
{
*(.mirror)
} > MIRROR
.noinit (NOLOAD) :
{
*(.noinit .noinit.*)
} > NOINIT
}
app36v.c — test firmware for QEMU.
/* Раздел 36в: прошивка-опыт для QEMU. Опыты идут по одному на запуск: номер опыта лежит
* в .noinit, сбой кончается безопасным состоянием и программным сбросом (вместо сторожа),
* после сброса прошивка печатает запись о сбое и берёт следующий опыт.
* Замер: SysTick от такта ядра, QEMU с -icount — счёт идёт по выполненным командам;
* cal_loop даёт ровно 2 команды на круг и калибрует шкалу. */
#include "robust36v.h"
extern const uint16_t __trap_start[], __flash_end[];
extern const uint32_t vectors[];
#ifndef FIRST_EXP
#define FIRST_EXP 0u
#endif
#ifndef LAST_EXP
#define LAST_EXP 12u
#endif
uint32_t exp_no __attribute__((section(".noinit")));
uint32_t exp_chk __attribute__((section(".noinit")));
static uint32_t ram_vectors[128] __attribute__((aligned(512))); /* копия таблицы с «пустым» вектором */
static void putc1(char c)
{
while (!(USART1_SR & (1u << 7))) {
}
USART1_DR = (uint32_t)c;
}
void puts1(const char *s)
{
while (*s)
putc1(*s++);
}
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]);
}
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 void kx(const char *k, uint32_t v)
{
puts1(k);
puthex(v);
puts1("\n");
}
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));
}
__attribute__((noinline)) static void empty_call(void)
{
__asm volatile("" ::: "memory");
}
static void pend_irq(uint32_t irq)
{
NVIC_ISER0 = 1u << irq;
NVIC_ISPR0 = 1u << irq;
__asm volatile("dsb\n\tisb" ::: "memory");
}
static void flow_finish(void) /* конец прохода — как в main_pass() */
{
uint32_t sig = flow_sig;
if (!flow_end())
safe_state(FAULT_FLOW, sig, FLOW_END);
wdg_feed();
}
static void measure(void)
{
uint32_t t, x = 0u;
SYST_RVR = 0x00FFFFFFu;
SYST_CVR = 0u;
SYST_CSR = 5u; /* такт ядра, без прерывания */
t = SYST_CVR; cal_loop(1000u); kv("t cal2000 ", since(t));
t = SYST_CVR; empty_call(); kv("t empty ", since(t));
t = SYST_CVR; flow_cp(CP_IN); kv("t flow_cp ", since(t));
flow_end();
t = SYST_CVR; x = flow_end(); kv("t flow_end ", since(t));
t = SYST_CVR; mir_set(MIR_SETPOINT, 1234u); kv("t mir_set ", since(t));
t = SYST_CVR; x += mir_get(MIR_SETPOINT); kv("t mir_get ", since(t));
t = SYST_CVR; x += cfg_refresh(cfg_table, cfg_count); kv("t cfg_refresh ", since(t));
t = SYST_CVR; x += in_read3(); kv("t in_read3 ", since(t));
t = SYST_CVR; in_gap(); kv("t in_gap ", since(t));
t = SYST_CVR; x += main_pass(); kv("t main_pass ", since(t));
kv("cfg entries ", cfg_count);
kv("in gap loops ", IN_GAP_LOOPS);
kv("x ", x);
}
int main(void)
{
uint32_t e, i, fixed, v;
USART1_BRR = BRR_115200;
USART1_CR1 = CR1_UE | CR1_TE;
if (exp_chk == ~exp_no) {
e = exp_no + 1u;
if (rec_valid()) {
kv("rec kind ", fault_rec.kind);
kx("rec a ", fault_rec.a);
kx("rec b ", fault_rec.b);
kv("rec count ", fault_rec.count);
} else {
puts1("rec none\n");
}
} else {
e = FIRST_EXP; /* включение: записи ещё нет */
fault_rec.magic = 0u;
puts1("power on\n");
}
exp_no = e;
exp_chk = ~e;
kv("== exp ", e);
mir_set(MIR_SETPOINT, 1234u);
mir_set(MIR_MODE, MODE_RUN);
if (e > LAST_EXP)
e = 99u;
switch (e) {
case 0: /* исправная работа и замеры */
kx("trap start ", (uint32_t)__trap_start);
kx("flash end ", (uint32_t)__flash_end);
kx("free half ", __trap_start[64]);
kx("FLOW_END ", FLOW_END);
REG(0x40003004u) = 5u; /* IWDG_PR: моделирует ли QEMU сторож */
kx("iwdg pr ", REG(0x40003004u));
#if defined(F1)
GPIOC_CRH = 0x00200000u;
kx("gpio cfg ", GPIOC_CRH);
#else
GPIOD_MODER = 1u << 24;
kx("gpio cfg ", GPIOD_MODER);
#endif
RCC_APB2ENR = 1u << 4;
kx("rcc apb2enr ", RCC_APB2ENR);
for (i = 0u, fixed = 0u; i < 1000u; i++)
fixed += main_pass();
kv("passes 1000 feeds ", wdg_feeds);
kv("fixed ", fixed);
USART1_BRR = BRR_115200 ^ 0x0100u; /* помеха перещёлкнула бит делителя */
SCB_VTOR = 0u; /* и таблицу векторов */
kx("brr spoiled ", USART1_BRR);
fixed = cfg_refresh(cfg_table, cfg_count);
kv("refresh fixed ", fixed);
kx("brr now ", USART1_BRR);
kx("vtor now ", SCB_VTOR);
kv("refresh again ", cfg_refresh(cfg_table, cfg_count));
measure();
puts1("reset\n");
__asm volatile("dsb" ::: "memory");
SCB_AIRCR = AIRCR_RESET;
break;
case 1: /* PC — в свободную флеш-память */
v = (uint32_t)__trap_start + 0x100u;
kx("jump ", v);
((void (*)(void))(v | 1u))();
break;
case 2: /* PC — в последнее полуслово флеш-памяти */
v = (uint32_t)__flash_end - 2u;
kx("jump ", v);
((void (*)(void))(v | 1u))();
break;
case 3: /* ложное прерывание: обработчика нет */
puts1("pend irq 23\n");
pend_irq(23u);
break;
case 4: /* то же, но вектор в таблице — ноль */
for (i = 0u; i < 16u + NIRQ; i++)
ram_vectors[i] = vectors[i];
ram_vectors[16 + 23] = 0u;
SCB_VTOR = (uint32_t)ram_vectors;
puts1("pend irq 23, vector 0\n");
pend_irq(23u);
break;
case 5: /* проход пропустил проверочную точку */
flow_cp(CP_IN); flow_cp(CP_CALC); flow_cp(CP_OUT); flow_cp(CP_DIAG); flow_finish();
kv("good pass feeds ", wdg_feeds);
puts1("skip CALC\n");
flow_cp(CP_IN); flow_cp(CP_OUT); flow_cp(CP_DIAG); flow_finish();
kv("fed anyway ", wdg_feeds);
break;
case 6: /* шаг выполнен дважды */
puts1("repeat CALC\n");
flow_cp(CP_IN); flow_cp(CP_CALC); flow_cp(CP_CALC); flow_cp(CP_OUT); flow_cp(CP_DIAG); flow_finish();
kv("fed anyway ", wdg_feeds);
break;
case 7: /* шаги переставлены */
puts1("swap CALC OUT\n");
flow_cp(CP_IN); flow_cp(CP_OUT); flow_cp(CP_CALC); flow_cp(CP_DIAG); flow_finish();
kv("fed anyway ", wdg_feeds);
break;
case 8: /* помеха испортила саму сигнатуру */
puts1("flip bit 9 of flow_sig\n");
flow_cp(CP_IN);
flow_sig ^= 1u << 9;
flow_cp(CP_CALC); flow_cp(CP_OUT); flow_cp(CP_DIAG); flow_finish();
kv("fed anyway ", wdg_feeds);
break;
case 9: /* один бит в одной копии */
puts1("flip bit 4 of mir_val[SETPOINT]\n");
mir_val[MIR_SETPOINT] ^= 1u << 4;
kv("read ", mir_get(MIR_SETPOINT));
break;
case 10: /* тот же бит в обеих копиях */
puts1("flip bit 4 in both copies\n");
mir_val[MIR_SETPOINT] ^= 1u << 4;
mir_inv[MIR_SETPOINT] ^= 1u << 4;
kv("read ", mir_get(MIR_SETPOINT));
puts1("flip bit 4 of mir_inv only\n");
mir_inv[MIR_SETPOINT] ^= 1u << 4;
kv("read ", mir_get(MIR_SETPOINT));
break;
case 11: /* PC — в область периферии */
kx("jump ", 0x40000000u);
((void (*)(void))0x40000001u)();
break;
case 12: /* прыжок через проверку прямо на кормление */
puts1("jump over check to feed\n");
flow_cp(CP_IN); flow_cp(CP_CALC);
wdg_feed(); /* CP_OUT, CP_DIAG и flow_end() пропущены */
kv("fed by jump ", wdg_feeds);
flow_cp(CP_IN); flow_cp(CP_CALC); flow_cp(CP_OUT); flow_cp(CP_DIAG); flow_finish();
kv("fed anyway ", wdg_feeds);
break;
default:
puts1("== end36v\n");
exp_chk = 0u;
for (;;) {
}
}
puts1("not caught\n");
for (;;) {
}
}
Next: section 36г "Oscilloscope, part 1: channel input, time base, memory and acquisition modes" — where oscilloscope work starts: every control of the input, time base and acquisition in turn.