36в. Помехоустойчивая прошивка: ловушки в свободной Flash, пустые векторы, контроль хода программы
Раздел 36б искал неисправность, которая осталась: залипший бит ОЗУ, испорченный байт флеш-памяти, ушедший кварц. Помеха обычно ничего не ломает навсегда. Разряд или выброс по питанию переворачивает бит на одно мгновение — в счётчике команд, в регистре, в ячейке ОЗУ, в регистре периферии, — и программа идёт дальше уже не туда. Через миллисекунду следа нет, а реле уже включилось не вовремя.
Здесь — приёмы, которые не дают такому сбою уйти незамеченным: свободная флеш-память, залитая командой-ловушкой; таблица векторов без единого пустого места и общий обработчик, который пишет номер вектора; контроль хода программы — проверочные точки и сигнатура, без которой сторож не кормится; перезапись настроечных регистров из эталона; двойное хранение важных переменных; повторное чтение входов. Всё сходится в одну функцию безопасного состояния. Каждый приём проверен внесением отказа в QEMU.
Что уже разобрано, здесь не повторяется: самопроверка ОЗУ, флеш-памяти и частоты — 36б и 12б, сторож задач — 33а, запись о сбое и её разбор после перезагрузки — 35в, сверка конфигурации по контрольной сумме и защита входов и выходов в изделии — 203.5, безопасное состояние в коде на C — 129.7, кадр исключения — 7а, слабые псевдонимы обработчиков — 4в.4, причина сброса — 12.4.
Опоры: справочные руководства RM0008 (F1) и RM0090 (F4) — таблицы векторов,
IWDG, AFIO_MAPR; руководство по ядру Cortex-M3 (DUI0552A) — карта памяти,
регистр IPSR, флаги отказов, повышение отказа до HardFault; заголовки
stm32f10x.h и stm32f407xx.h из паков и CMSIS. Код собран
arm-none-eabi-gcc 12.2.1 для F103 и F407 и прогнан в QEMU 7.2 на машинах
stm32vldiscovery (Cortex-M3) и netduinoplus2 (Cortex-M4); модели — на ПК.
Как это называется в документации
Оригинал Перевод control flow monitoring контроль хода программы checkpoint проверочная точка: место, где программа отмечает, что прошла flow signature сигнатура хода: число, в которое точки подмешивают свои токены unused memory fill заливка свободной памяти командой-ловушкой permanently undefined instruction ( UDF)заведомо неопределённая команда spurious interrupt ложное прерывание: пришло, хотя его никто не ждал ISR_NUMBER(регистрIPSR)номер исключения, которое сейчас обслуживается register refresh перезапись настроечных регистров из эталона majority vote, 2 out of 3 голосование «два из трёх» write-only bits биты только для записи: прочитать записанное нельзя
36в.1 Что помеха делает с работающей программой
Сбой короткий, последствия длинные. Что может испортиться и что ловит последствия:
| Что испортилось | Что дальше | Что ловит | Где |
|---|---|---|---|
| счётчик команд: прыжок в свободную флеш-память | ядро выполняет то, что там лежит | заливка командой-ловушкой | 36в.2 |
| счётчик команд: прыжок в область периферии | отказ выборки команды | общий обработчик отказов | 36в.3 |
| счётчик команд: прыжок внутрь своего кода | шаги пропущены, повторены, переставлены | контроль хода | 36в.4 |
| регистр периферии | делитель, режим вывода, такт блока — не те | перезапись из эталона | 36в.5 |
| ячейка ОЗУ с важной переменной | уставка или режим другие | двойное хранение | 36в.5 |
| ложное прерывание | вызов вектора, которого никто не ждал | общий обработчик с номером | 36в.3 |
| вход в момент чтения | ложное срабатывание датчика | повторное чтение | 36в.6 |
Порча регистра ядра выглядит как любой из этих случаев: испорченный указатель — обращение не туда и отказ шины, испорченный адрес возврата — тот же прыжок счётчика команд, испорченное число — неверный шаг алгоритма. Отдельного приёма для неё нет; тест регистров ядра — 12б.6.
Все ловушки ведут в одно место — safe_state() (36в.6): выходы выключить,
записать, что случилось, перестать кормить сторож. Сторож перезапустит
камень, а запись переживёт сброс.
Как проверено. Прошивка-опыт идёт по опытам: номер опыта лежит
в .noinit, каждый сбой кончается безопасным состоянием и сбросом, после
сброса прошивка печатает запись о сбое и берёт следующий опыт. IWDG
QEMU не моделирует, поэтому в сборке для опыта safe_state() после
короткой паузы сам делает программный сброс через SYSRESETREQ — там, где
на плате сработал бы сторож. Двенадцать сбоев подряд — счётчик в записи
дошёл до 12, запись пережила все сбросы. Опыты одинаково прошли на F1
и на F4.
36в.2 Свободная флеш-память: ловушка вместо пустоты
Программа занимает малую часть флеш-памяти. В опыте образ F103C8 — 3488 байт из 65 536, F407 — 3576 байт из 1 МБ. Остальное — то, что ядро выполнит, если счётчик команд туда попадёт. Что именно, зависит от того, чем область заполнена:
| Чем залито | Как разбирает дизассемблер | Опыт в QEMU: прыжок в свободную область |
|---|---|---|
0xDE — заливка этого раздела |
udf #222 |
сразу отказ, в записи — адрес прыжка и признак ловушки |
0xFF — стёртая флеш-память |
0xffffffff — неопределённая команда |
сразу отказ UNDEFINSTR, адрес прыжка в записи есть |
0x00 |
movs r0, r0 |
64 086 команд movs подряд до конца флеш-памяти, отказ IBUSERR по адресу 0x08020000 |
Нули — худший случай: movs r0, r0 ничего не портит, и ядро скользит по
ним, пока не упрётся в следующий код — или в конец флеш-памяти, как
в опыте. Где начался прыжок, после этого не узнать: в записи только конец
пути. Нули появляются в двух местах. Компоновщик заполняет ими
промежутки выравнивания внутри секций, если ему не сказать иначе: в образе
опыта таких промежутков 6, всего 11 байт. И QEMU читает нули там,
куда образ не записан; на кристалле там 0xFF после стирания — или старая
программа, если загрузчик стёр только страницы под новую.
Стёртая память тоже ловит прыжок, но молча — это обычный HardFault, как
от любой ошибки. Заливка 0xDE даёт признак: полуслово 0xDEDE — команда
udf #222, «заведомо неопределённая», и обработчик отказа по адресу
команды видит, что ядро выполняло именно заливку. Байт один и тот же,
поэтому с какого полуслова ни начни, первая же команда — ловушка.
Заливку делает скрипт компоновки. Промежутки внутри секций кода —
=0xDEDEDEDE после имени области; всё от конца образа до конца
флеш-памяти — отдельная секция:
.trap_fill __image_end :
{
__trap_start = .;
LONG(0xDEDEDEDE) /* хотя бы одно слово данных: иначе секция пустая и в образ не попадёт */
. = __flash_end - __image_end; /* внутри секции «.» — смещение от её начала */
} > FLASH =0xDEDEDEDE
Слово LONG обязательно: секция из одних присваиваний «.» получает только
место, без данных, и в образ не попадает (36в.7). После сборки скрипт
image36v.py разбирает ELF и .bin побайтно:
| Сборка | Файл | Свободная область | Промежутки внутри секций |
|---|---|---|---|
| F103C8, заливка компоновщиком | .bin 65 536 байт |
62 048 байт, все 0xDE |
11 байт, все 0xDE |
F103C8 без заливки, objcopy --gap-fill 0xDE --pad-to |
.bin 65 536 байт |
62 048 байт, все 0xDE |
11 байт, все 0x00 |
F103C8 без заливки, просто objcopy -O binary |
.bin 3488 байт |
в файле нет | 11 байт, все 0x00 |
| F407, заливка компоновщиком | .bin 1 048 576 байт |
1 045 000 байт, все 0xDE |
11 байт, все 0xDE |
Второй путь — objcopy — заполняет только то, что лежит между секциями
и после образа. Промежутки внутри секций он не трогает: они уже в файле,
и в них нули. Третий путь — частый: в файле только сам образ, а что
окажется за ним во флеш-памяти, решает программатор. Поэтому заливку
делают в скрипте компоновки: она попадает в ELF, .hex и .bin одинаково.
В опыте прыжок на 256 байт за конец образа и прыжок в последнее полуслово
флеш-памяти (0x0801FFFE: у машины QEMU stm32vldiscovery флеш 128 КБ, у F103C8
конец — 0x0800FFFE) кончились одинаково: HardFault, CFSR =
0x00010000 (UNDEFINSTR), запись с видом «ловушка» и точным адресом.
36в.3 Векторы: ни одного пустого места
Таблица векторов F103 по RM0008 — 16 исключений ядра и прерывания с номерами до 59; у F407 по RM0090 — до 81. В таблице опыта 76 мест у F1 и 98 у F4, и в каждом — адрес обработчика:
__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, /* все прерывания: своих обработчиков в опыте нет */
};
Своих обработчиков у опыта нет, поэтому все прерывания — в один. Когда в программе появится обработчик USART, его место вписывают отдельной строкой, разбив диапазон. Слабые псевдонимы из 4в.4 дают то же самое другим путём; важно не это, а чтобы общий обработчик не молчал, как пустой цикл, а записывал, кто его позвал.
Номер берётся из регистра IPSR: по DUI0552A его поле ISR_NUMBER — номер
текущего исключения, 16 и дальше — прерывания. HardFault отдельно:
по адресу команды, на которой случился отказ, он узнаёт ловушку из 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] — адрес возврата в кадре исключения (7а.1); для неопределённой
команды это адрес её самой. Чтение *(uint16_t *)pc — только после
проверки, что адрес лежит во флеш-памяти и чётный: прочитать по
случайному адресу внутри HardFault — значит устроить второй отказ там,
где ловить его уже некому. MemManage, BusFault и UsageFault после сброса
выключены, и по DUI0552A отказ, обработчик которого не включён,
повышается до HardFault, — поэтому одного обработчика хватает на все.
Три опыта:
| Опыт | Запись после сброса | Что видно |
|---|---|---|
| IRQ 23 разрешено и поставлено в ожидание, обработчика нет | вид «вектор», номер 0x27 |
39 = 16 + 23: какое прерывание пришло |
| то же, но в копии таблицы на месте IRQ 23 — ноль | вид «отказ», PC 0x00000000, CFSR = 0x00020000 |
INVSTATE: переход без бита Thumb; какое прерывание — не узнать |
прыжок по адресу 0x40000000 (периферия) |
вид «отказ», PC 0x40000000, CFSR = 0x00000001 |
IACCVIOL: область периферии по карте памяти — без исполнения |
Ноль в таблице — не «ничего не делать». По DUI0552A младший бит каждого
вектора должен быть единицей, иначе ядро пытается перейти в состояние ARM,
которого у Cortex-M нет. Отказ всё равно случится, но в записи останется
только PC = 0. Общий обработчик называет номер — по нему видно, какой блок
поднял флаг без спроса: разрешённое не то прерывание, флаг, который
никто не сбрасывает, или бит в NVIC_ISER, перевёрнутый помехой.
36в.4 Контроль хода: проверочные точки и сигнатура
Прыжок внутрь собственного кода ловушка не видит: там настоящие команды. Видно другое — проход главного цикла пошёл не тем путём. Каждая проверочная точка подмешивает свой токен в сигнатуру — сдвиг по кругу на 7 и «исключающее или»:
#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 — сигнатура правильного пути, компилятор считает её сам:
0x79DCD248. Точка и проверка в конце прохода:
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;
}
Сигнатура хранится дважды — значение и инверсия (36в.5): помеха, попавшая в саму сигнатуру, видна на следующей же точке. Главный цикл:
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;
}
Сторож кормится в одном месте — после того как весь путь пройден по
порядку. Прыжок прямо на кормление накормит сторож один раз, но перескочит
и flow_end(), которая возвращает сигнатуру к FLOW_SEED. Следующий
проход начнётся с испорченного начала и не сойдётся — в опыте так и вышло.
Обработчики прерываний и задачи ОС в одну цепочку точек не встраивают:
у них свой ритм, их проверяют отметками и сроками — 33а.
Опыты в QEMU — каждый раз на месте кормления:
| Опыт | Сигнатура в записи | Сторож |
|---|---|---|
| 1000 правильных проходов | — | накормлен 1000 раз |
точка CP_CALC пропущена |
0x42C05ECB вместо 0x79DCD248 |
не накормлен |
CP_CALC выполнена дважды |
0xF79A93D5 |
не накормлен |
CP_CALC и CP_OUT переставлены |
0x607AB8C8 |
не накормлен |
бит 9 сигнатуры перевёрнут после CP_IN |
0x7689098A, инверсия 0x8976F475 — расходятся ровно в бите 9 |
отказ на следующей точке |
прыжок после CP_CALC прямо на wdg_feed(), затем правильный проход |
0xB87A5D14 |
накормлен один раз, на следующем проходе — отказ |
Почему именно сдвиг и «исключающее или», показывает перебор на ПК: все последовательности точек длиной от 0 до 8 из тех же четырёх токенов — 87 380 неправильных:
| Схема | Неправильных путей с той же отметкой |
|---|---|
| счётчик точек (дошло ли до четырёх) | 255 |
| сумма токенов | 23 — все перестановки |
сдвиг по кругу и «исключающее или» (FLOW_STEP) |
0 |
Счётчику всё равно, какие точки пройдены, — лишь бы их было четыре. Сумма не видит порядка. Сдвиг делает вклад каждой точки зависимым от того, что было до неё.
36в.5 Настройки из эталона и двойное хранение
Настроечные регистры. Помеха перещёлкнула бит делителя USART — программа цела, сторож сыт, а обмен идёт на другой скорости. В 203.5 эталон снимается после инициализации в ОЗУ и сверяется по контрольной сумме. Здесь эталон — таблица констант во флеш-памяти: её не портит та же помеха, что портит ОЗУ, а если её испортит, это поймает CRC образа (36б.5). В таблице — адрес, маска своих битов и их значение:
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; /* сколько регистров пришлось вернуть — в журнал */
}
Маска нужна, чтобы не трогать чужое: флаги, которые меняет само
железо, биты, которыми управляет другой модуль. Перезаписывается только
то, что разошлось, и число исправлений идёт в журнал: прибор, который
чинит себе делитель раз в час, стоит у сильной помехи. В опыте бит 8
делителя USART1_BRR перевернули (0x0145 вместо 0x0045), а VTOR
обнулили: один вызов вернул 2 регистра, следующий — 0.
В QEMU регистры GPIO и RCC не моделируются — запись в них теряется,
чтение даёт ноль, — поэтому в сборке для опыта таблица из трёх строк:
USART1_BRR, USART1_CR1, VTOR. Таблицы для F103 и F407 собраны, но
на плате не проверены.
Двойное хранение. Уставку, режим работы, номер шага автомата хранят
дважды: значение и его инверсию. Копии разнесены по ОЗУ — значения в .bss
в начале, инверсии в своей секции в конце, по ту сторону стека:
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;
}
Опыты:
| Что испорчено | Чтение |
|---|---|
| бит 4 значения | отказ, в записи — индекс 0 и разошедшиеся биты 0x00000010 |
| бит 4 и значения, и инверсии | прочитано 1218 вместо 1234 — не замечено |
| после этого бит 4 инверсии вернули | отказ, биты 0x00000010 |
Один и тот же бит в обеих копиях проверка не видит: значение и инверсия испорчены согласованно. Поэтому копии держат далеко друг от друга, а не в соседних словах одной структуры: одна помеха редко попадает в один бит двух далёких слов. Инверсия, а не простая копия, — против залипания: если линия данных или ячейка застряла в нуле, две одинаковые копии совпадут, а значение с инверсией — нет.
36в.6 Входы, безопасное состояние и цена
Повторное чтение входа. Помеха на проводе датчика короче периода опроса. Вход читают три раза с паузой и берут по каждому биту два из трёх:
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); /* по каждому биту — два из трёх */
}
Модель на ПК: вход — ноль, помеха — единица длиной L, пауза между чтениями G = 10; перебраны все положения помехи, задевающие окно чтений:
| Длина помехи L | Положений | Одно чтение: ложных | Два из трёх: ложных |
|---|---|---|---|
| 5 | 25 | 5 | 0 |
| 10 | 30 | 10 | 0 |
| 15 | 35 | 15 | 10 |
| 20 | 40 | 20 | 20 |
| 25 | 45 | 25 | 25 |
Помеху не длиннее паузы голосование гасит всегда. Помеху в две паузы и длиннее — не гасит никогда: она накрывает два чтения из трёх. Паузу подбирают по самой длинной помехе, которую нужно пережить, а дальше — фильтр по нескольким проходам главного цикла.
Безопасное состояние. Одна функция на все случаи:
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 (;;) { /* сторож не кормится: сброс через его таймаут */
}
}
Выходы — первым делом, до записи: записать можно и после, а реле
должно отпустить сразу. Запись о сбое со словом проверки лежит в .noinit
(35в.2) — стартовый код её не обнуляет. Без проверки нельзя: после
включения питания там случайные числа. Счёт сбоев ведётся в записи:
в опытах он дошёл до 12 за 12 сбросов. После сброса прошивка читает
запись и флаги причины сброса (12.4, 35в.4); повторяющийся сбой — повод
остаться в безопасном состоянии, а не перезапускаться по кругу (36б.6).
Цена. Размеры — из сборки для F103 (-Os), команды — счёт QEMU
с -icount на F4, за вычетом пустого вызова:
| Что | Байт кода | Команд за вызов |
|---|---|---|
HardFault_Handler и fault_c |
18 + 60 | — |
Unused_Handler |
18 | — |
safe_state и rec_valid |
80 + 56 | — |
flow_cp — одна точка |
40 | 10 |
flow_end — конец прохода |
60 | 10 |
mir_set / mir_get |
24 / 44 | 4 / 8 |
cfg_refresh, 3 регистра; таблица F103 на 5 строк |
44 + 60 | 48 |
in_read3 с двумя паузами in_gap по 20 кругов |
40 + 22 | 305, из них паузы 146 × 2 |
wdg_feed |
28 | — |
Код всех приёмов — 594 байта, ОЗУ — 24 байта записи, 8 байт сигнатуры с инверсией и по 8 байт на каждую пару значение — инверсия. Проход главного цикла опыта — 456 команд, из них 292 — две паузы между чтениями входа; сами проверки (четыре точки, конец прохода, перезапись трёх регистров, чтение копии) — 106 команд, не меньше 0,63 мкс на 168 МГц и 1,5 мкс на 72 МГц. Это нижние границы: команда — не меньше такта. Дороже всего заливка: флеш-память пишется целиком, 62 048 байт на F103C8 и 1 045 000 байт на F407 вместо образа в несколько килобайт.
36в.7 Грабли
Секция заливки оказалась пустой. Без слова LONG секция .trap_fill
получает место — 62 048 байт, — но не данные: objdump -h показывает
только ALLOC, без CONTENTS, и в образ она не попадает. Флеш-память за
образом остаётся какой была.
Внутри секции «.» — смещение, а не адрес. Строка . = __flash_end;
внутри секции дала адрес 0x10010DA0 и ошибку «not within region FLASH»:
компоновщик прибавил адрес конца флеш-памяти к началу секции. Нужна
разность: . = __flash_end - __image_end;.
--gap-fill залил не всё. objcopy заполняет промежутки между
секциями и хвост до --pad-to, но не промежутки выравнивания внутри секций:
11 байт в образе опыта остались нулями. Заливка — в скрипте компоновки.
Прошили .hex, а заливки нет. В .hex и в простом .bin только
образ; что за ним, зависит от того, какие страницы стёр программатор
или загрузчик. Заливка в ELF уходит во флеш-память вместе с образом.
Заливка заняла место загрузчика или эмуляции EEPROM. .trap_fill
тянется до конца флеш-памяти. Страницы, которые программа пишет сама,
выносят в отдельную область скрипта компоновки и в заливку не включают.
Бит только для записи в таблице эталона. У F103 поле SWJ_CFG
в AFIO_MAPR по RM0008 только для записи: прочитанное значение не
определено. cfg_refresh() будет «чинить» его в каждом проходе и
забьёт журнал. Такие поля в таблицу не кладут — их пишут один раз
при старте или пишут каждый раз без сравнения.
Отметку хода свели к счётчику. Счётчик точек пропускает 255 неправильных путей из 87 380, сумма токенов — 23. Нужна функция, в которой порядок важен.
Копии рядом. Значение и инверсия в соседних словах одной структуры портятся одной записью за границу массива. В опыте согласованная порча одного бита прошла незамеченной.
Пауза между чтениями короче помехи. Три чтения подряд без паузы попадают в одну помеху и дружно голосуют за неё: при L = 2G голосование ошиблось столько же раз, сколько одно чтение (20 из 40).
Пустой вектор вместо обработчика. Ноль в таблице даёт INVSTATE и
PC = 0: прибор перезапустится, но по записи не понять, какое прерывание
пришло. Общий обработчик называет номер.
36в.8 Итоги
- Помеха портит бит на мгновение; ловят не её, а последствия: прыжок счётчика команд, пропуск шага, чужое значение в регистре или ячейке.
- Свободная флеш-память — байтом
0xDE: полуслово0xDEDE=udf #222. 62 048 байт на F103C8 залиты целиком, разбор образа подтвердил каждый байт; прыжок в заливку — HardFault с признаком ловушки и точным адресом. По нулям ядро проскользнуло 64 086 команд, и адрес прыжка потерялся. - Таблица векторов — 76 мест у F1 и 98 у F4, ни одного нуля; общий
обработчик берёт номер из
IPSR: ложное IRQ 23 записано как 39. Ноль в таблице далINVSTATEбез номера. - Контроль хода: четыре точки, сигнатура сдвигом и «исключающим или»,
сторож кормится только при
FLOW_END. Пропуск, повтор, перестановка и порча самой сигнатуры пойманы; в переборе 87 380 неправильных путей совпадений с правильным — 0 (у счётчика — 255, у суммы — 23). - Настройки — из таблицы-эталона во флеш-памяти с масками: после порчи
делителя и
VTORвозвращены 2 регистра. - Двойное хранение: перевёрнутый бит виден при чтении; один и тот же бит в обеих копиях — нет, поэтому копии разнесены по ОЗУ.
- Вход — три чтения и голосование: помеха не длиннее паузы не проходит.
- Цена проверок — 106 команд на проход и 594 байта кода; дороже всего заливка — прошивается вся флеш-память.
Полные исходники
robust36v.h — общий заголовок.
/* Раздел 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 — обработчик сбоев, контроль хода, двойное хранение, безопасное состояние.
/* Раздел 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 — таблица векторов без пустых мест и стартовый код.
/* Раздел 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 — повторное чтение входа.
/* Раздел 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 — скрипт компоновки с заливкой свободной флеш-памяти ловушкой.
/* Раздел 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 — прошивка-опыт для 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 (;;) {
}
}
Дальше: раздел 36г «Осциллограф, часть 1: вход канала, развёртка, память и режимы сбора» — с чего начинается работа с осциллографом: каждая ручка входа, развёртки и сбора по порядку.