Embedded HandbookПриложения
Все разделы › V. Отладка и качество
STM32F103, STM32F407 · уровень продвинутый · 39 мин чтения
Открыть в интерактивном справочнике

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.

Схема помехоустойчивой прошивки: во флеш-памяти F103C8 таблица векторов на 76 мест без нулей, код и константы, свободная область 62 048 байт залита байтом 0xDE, то есть командой udf #222; ложное прерывание приводит в Unused_Handler, который берёт номер вектора из IPSR, прыжок в свободную область — в HardFault_Handler, который узнаёт по адресу команды полуслово 0xDEDE; главный цикл проходит проверочные точки CP_IN, CP_CALC, CP_OUT, CP_DIAG, и сторож кормится только при сигнатуре FLOW_END, иначе, как и из обоих обработчиков, — безопасное состояние: выходы в ноль, запись о сбое в .noinit, сторож не кормится и перезапускает камень

Все ловушки ведут в одно место — 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 Итоги

Полные исходники

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: вход канала, развёртка, память и режимы сбора» — с чего начинается работа с осциллографом: каждая ручка входа, развёртки и сбора по порядку.

Открыть в интерактивном справочнике