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

12б. Самопроверка прошивки: ОЗУ, Flash, такты и ядро во время работы

Сторож из раздела 12 ловит только зависшую программу. Программа, которая крутится, но работает с испорченной ячейкой ОЗУ, битым байтом во флеш-памяти, кварцем не той частоты или залипшим битом регистра, кормит сторожа исправно — и исправно выдаёт неверный результат. Самопроверка — часть прошивки, которая ищет такие отказы сама: при старте целиком, в работе по кусочку. Для программ класса B в бытовой автоматике такие проверки обязательны; здесь нет пунктов требований — только сами проверки, и каждая посчитана или прогнана.

Раздел 36б уже сравнил марши ОЗУ по моделям неисправностей, разобрал прозрачный тест по кускам и проверку частоты на G474. Здесь — то, что там осталось за кадром, на F103 и F407: марш при старте на ассемблере прямо в Reset_Handler, прозрачный марш на собственных данных без отдельного возврата содержимого, тест регистров ядра — собран и прогнан с внесённым отказом, — канарейка стека, эталон CRC, вписанный в сам ELF, проверка самого блока CRC, опора внутри F407 для таймера и бюджет времени по командам, сосчитанным в QEMU.

Опоры: справочные руководства RM0008 (F1: глава 4 «CRC», AFIO_MAPR, FLASH_ACR) и RM0090 (F4: глава 4 «CRC», 6.2.7 — CSS, 6.2.11 — измерение тактов через TIM5 и TIM11, 18.4.20 — TIM5_OR), даташиты DS8626 (F407) и DocID13587 (F103x8/B), заголовки stm32f10x.h и stm32f407xx.h, разделы 12 (сторож, блок CRC), 7а (кадр исключения), 35в (область .noinit), 203.5 (прошивка против помех) и 204.6 (журнал отказов). Код собран arm-none-eabi-gcc 12.2 для F103 и F407 и прогнан в QEMU; логика марша, CRC и окна частоты — на ПК против модели неисправной памяти.

Как это называется в документации

Оригинал Перевод
data background фон данных: чем заполнена память во время марша
fault injection внесение отказа: нарочно испортить, чтобы проверка сработала
checkerboard pattern шахматный шаблон: 0xAAAAAAAA и 0x55555555 через регистр
stack painting заливка стека шаблоном, чтобы потом найти, докуда он дошёл
modified immediate constant константа, которую Thumb-2 кладёт прямо в команду
timer input remap (TI4_RMP) переназначение входа таймера на внутренний сигнал
redundant variable (value and inverse) переменная дважды: значение и его инверсия
time budget per pass бюджет времени на проход главного цикла

12б.1 Что ловит самопроверка и в каком порядке

Отказ Как проявляется Чем ловится Когда
бит ячейки ОЗУ залип, две ячейки связаны переменная меняется сама или не меняется марш March C− при старте всё ОЗУ, в работе блоками
байт флеш-памяти испорчен не та константа или команда CRC-32 образа против эталона при старте весь образ, в работе по 256 слов
кварц не той частоты, ошибка в PLL таймеры, UART и АЦП врут, программа крутится счёт TIM5 за 8 периодов LSE при старте и раз в несколько проходов
бит регистра ядра или флага неверные вычисления шахматные шаблоны на ассемблере при старте и раз за круг ОЗУ
стек перерос свою область портятся данные под стеком канарейка на дне стека в каждом проходе
испорчено состояние самой проверки проверка «ходит» не туда позиция хранится дважды в каждом шаге

Каждой проверке нужно место в памяти, куда она сама не залезет. Карта памяти F103 после компоновки:

Карта памяти F103 и проверки: в ОЗУ снизу вверх данные приложения .data и .bss, которые в работе проверяет прозрачный марш блоками по 16 слов, секция .st_ram с буфером и позицией самопроверки, которую раз за круг проверяет марш по буферу, стек в 1 КБ с заливкой шаблоном и канарейкой на дне, и запись об отказе .noinit, которую марш при старте не трогает; во флеш-памяти таблица векторов, код и константы, начальные значения .data и последнее слово .crc_ref с эталоном CRC, вписанным после сборки

Порядок при старте:

  1. Сторож — первым (раздел 12): если зависнет сама проверка, камень всё равно перезапустится.
  2. ОЗУ целиком — в Reset_Handler, раньше любого кода на C (12б.2).
  3. Регистры ядра и флаги (12б.6).
  4. Блок CRC, затем CRC всего образа против эталона (12б.4).
  5. Такт — HSE против LSE (12б.5).
  6. Заливка стека шаблоном и канарейка (12б.6) — в стартовом коде, до main.

В работе каждый проход главного цикла делает маленький шаг каждой проверки, и сторожа кормят, только если все шаги сошлись (12б.6).


12б.2 ОЗУ: марш при старте и прозрачный марш в работе

При старте ОЗУ ещё ничего не хранит, и марш вправе его стереть. Но писать его на C нельзя: функция на C кладёт на стек адрес возврата и регистры, а марш сотрёт их вместе с остальным ОЗУ. Поэтому марш — на ассемблере, только в регистрах r0–r3 и r12, которые по соглашению о вызовах можно портить без сохранения. Стек ему не нужен вовсе:

march_startup:
    mov     r12, #0                 @ накопитель ошибок
    movs    r3, #0
    mov     r2, r0
1:  str     r3, [r2], #4            @ ⇕(w0)
    cmp     r2, r1
    blo     1b
    mov     r2, r0
2:  ldr     r3, [r2]                @ ⇑(r0, w1)
    orr     r12, r12, r3
    mvn     r3, #0
    str     r3, [r2], #4
    cmp     r2, r1
    blo     2b
    mov     r2, r0
3:  ldr     r3, [r2]                @ ⇑(r1, w0)
    orn     r12, r12, r3
    movs    r3, #0
    str     r3, [r2], #4
    cmp     r2, r1
    blo     3b
    mov     r2, r1
4:  ldr     r3, [r2, #-4]!          @ ⇓(r0, w1)
    orr     r12, r12, r3
    mvn     r3, #0
    str     r3, [r2]
    cmp     r2, r0
    bhi     4b
    mov     r2, r1
5:  ldr     r3, [r2, #-4]!          @ ⇓(r1, w0)
    orn     r12, r12, r3
    movs    r3, #0
    str     r3, [r2]
    cmp     r2, r0
    bhi     5b
    mov     r2, r0
6:  ldr     r3, [r2], #4            @ ⇕(r0)
    orr     r12, r12, r3
    cmp     r2, r1
    blo     6b
    mov     r0, r12
    bx      lr
    .size   march_startup, . - march_startup

На слово — 10 обращений к памяти, как и положено March C−: 1 + 2 + 2 + 2 + 2 + 1. Команд — 3 + 6 + 6 + 6 + 6 + 4 = 31 на слово. Вызывают его из Reset_Handler — первым же делом после сброса:

Reset_Handler:
    ldr     r2, =0xE000E010         @ SysTick
    ldr     r3, =0x00FFFFFF
    str     r3, [r2, #4]            @ RVR
    str     r3, [r2, #8]            @ CVR: любая запись обнуляет
    movs    r3, #5
    str     r3, [r2]                @ CSR: такт ядра, счёт
    ldr     r0, =_sram
    ldr     r1, =_estack            @ до .noinit: запись об отказе переживает сброс
    bl      march_startup
    cbnz    r0, boot_fail
    ldr     r2, =0xE000E018
    ldr     r0, [r2]                @ остаток SysTick — в c_start
    bl      c_start
boot_fail:
    b       boot_fail               @ ОЗУ неисправно: C не запускать, выходы в покое, ждать сторожа
    .size   Reset_Handler, . - Reset_Handler

Верхняя граница — _estack, то есть начало области .noinit (35в): запись об отказе переживает не только сброс, но и марш. SysTick здесь только для опыта: в QEMU с -icount каждая команда стоит одинаковое виртуальное время, и счётчик даёт число выполненных команд. Прогон: на F1 марш по 2032 словам занял 63 007 команд, на F4 по 32 752 словам (128 КБ без 64 байт .noinit) — 1 015 328, по 31,0 на слово. Отсюда нижние границы — каждая команда хотя бы такт, а флеш-память F103 на 72 МГц ещё и с двумя тактами ожидания (RM0008):

Камень ОЗУ Слов Команд Не меньше
F103, 72 МГц 20 КБ 5120 158 720 2,2 мс
F407, 168 МГц 128 КБ (112 + 16) 32 768 1 015 808 6,0 мс
F407, 168 МГц CCM, 64 КБ 16 384 507 904 3,0 мс

Память CCM у F407 лежит по другому адресу (0x10000000, даташит DS8626), и для неё нужен второй вызов march_startup со своими границами.

Битой памяти в QEMU нет, но есть регистр, у которого часть битов всегда читается нулём: у VTOR биты 6:0 — нули. Марш по этому одному «слову» вернул 0x0000007F: семь залипших в нуль битов найдены.

В работе ОЗУ занято. Прозрачный марш не стирает содержимое, а ходит по самим данным: вместо записи нулей — копия слова a в буфер, вместо «записать единицы» — записать ~a, вместо «прочитать нули» — сравнить с a:

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

    for (i = 0u; i < n; i++) { s[i] = ST_RD(&p[i]); }                              /* копия: a     */
    for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i];  ST_WR(&p[i], ~s[i]); } /* ⇑(ra, w~a)   */
    for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); }  /* ⇑(r~a, wa)   */
    for (i = n; i-- > 0u;)   { err |= ST_RD(&p[i]) ^ s[i];  ST_WR(&p[i], ~s[i]); } /* ⇓(ra, w~a)   */
    for (i = n; i-- > 0u;)   { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); }  /* ⇓(r~a, wa)   */
    for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i]; }                       /* ⇕(ra)        */
    return err;
}

У раздела 36б другой прозрачный тест: сохранить кусок, прогнать по нему обычный марш по нулям и единицам и вернуть сохранённое. Здесь возвращать нечего: последняя запись в каждое слово — его же a, содержимое на месте само. К проверяемому слову — те же 10 обращений (1 + 8 + 1), к буферу — ещё 6: одна запись и пять чтений. По дизассемблеру F103 циклы занимают 4 + 8 + 7 + 9 + 7 + 6 = 41 команду на слово.

Марш по всему ОЗУ сразу занял бы процессор на миллисекунды, поэтому он идёт блоками по 16 слов, один блок за проход:

int ram_step(volatile uint32_t *base, uint32_t words, uint32_t *wrapped)
{
    uint32_t n = RAM_BLOCK, err, m, p = pos;

    if ((p ^ pos_inv) != 0xFFFFFFFFu || p >= words)
        return ST_STATE;                          /* испорчено само состояние проверки */
    if (p + n > words)
        n = words - p;
    m = irq_save();                               /* посреди марша блок не должен видеть никто */
    err = march_own(base + p, save, n);
    irq_restore(m);
    *wrapped = (p + n >= words);
    p = *wrapped ? 0u : p + n - 1u;               /* блоки перекрываются на одно слово */
    pos = p;
    pos_inv = ~p;
    ram_bits = err;
    return err ? ST_RAM : ST_OK;
}

Три правила. Прерывания запрещены на блок: обработчик, прочитавший переменную посреди марша, увидит ~a; PRIMASK восстанавливается, а не включается (36б.4). Блоки перекрываются на одно слово — иначе связь двух соседних слов на стыке не видна ни одному блоку. Позиция хранится дважды, значением и инверсией: если помеха испортит pos, проверка не уйдёт молча в чужую память — ram_step вернёт ST_STATE. Сама pos, буфер save[] и итог ram_bits лежат в своей секции .st_ram вне проверяемой области — это видно на рисунке. Буфер между шагами ничего не хранит, поэтому раз за круг его проверяет обычный марш march_startup.

Замер в QEMU: блок из 16 слов — 715 команд на F1 и 713 на F4 вместе с вызовом, марш по буферу — 518 команд. Блок идёт при запрещённых прерываниях, и ответ на прерывание может опоздать на это время: на F103 при 72 МГц — не меньше 9,9 мкс. Область опыта — 1025 слов (.bss с прикладным массивом): полный круг — 69 шагов, контрольная сумма области после круга та же.

Для 20 КБ F103 круг — 5120 слов: 1 + 15 × 342 ≥ 5120, то есть 342 шага, 244 530 команд, не меньше 3,4 мс процессорного времени на круг. Это время размазано по 342 проходам главного цикла.


12б.3 ОЗУ на ПК: внесённые отказы

Прошивочный код марша на ПК собран без изменений: обращения к ОЗУ в нём идут через ST_RD и ST_WR, которые на камне — обычные чтение и запись, а на ПК — модель неисправной памяти из 64 слов со случайным содержимым. В модель вносится ровно одна неисправность, затем гоняются три теста: прозрачный марш на своих данных целиком, тот же марш блоками — полный круг ram_step, как в прошивке, — и марш при старте (перенос на C ассемблерного march_startup с тем же порядком обращений).

Неисправность Случаев Марш на своих данных целиком Блоками по 16, круг Марш при старте
бит залип в 0 или 1 4096 100 % 100 % 100 %
бит не переходит 0→1 или 1→0 4096 100 % 100 % 100 %
два адреса выбирают одну ячейку 4032 100 % 24 % 100 %
переход в слове переворачивает бит в далёком слове 2000 100 % 24 % 100 %
переход в слове ставит бит далёкого слова в 0 или 1 2000 100 % 23 % 100 %
пока бит в состоянии s, бит далёкого слова держится 2000 100 % 64 % 100 %
то же, переворот, слова соседние 2000 100 % 100 % 100 %
то же, установка, слова соседние 2000 100 % 100 % 100 %
то же, удержание, слова соседние 2000 100 % 100 % 100 %
переход переворачивает бит того же слова 2000 100 % 100 % 100 %
переход ставит бит того же слова в 0 или 1 2000 50 % 50 % 50 %

На исправной памяти все три теста молчат, а содержимое после прозрачного марша и после круга блоков то же, что было. Блоки начинаются со слов 0, 15, 30, 45, 60 — перекрытие на слово видно по шагу 15.

Что видно из таблицы. Залипание и переходы ловят все три теста в каждом случае. Связь соседних слов блоки ловят целиком — ради неё перекрытие. Связь далёких слов и ошибку дешифратора адреса блоки видят, только если оба слова попали в один блок: около четверти случаев для переворота, установки и ошибки адреса, 64 % для удержания. Поэтому марш при старте прогоняют всегда: только он видит всё ОЗУ разом. Связь двух битов одного слова, когда переход ставит соседний бит в 0 или 1, в этой модели ловится примерно в половине случаев всеми тремя — фон собственных данных тут не помог; переворот бита того же слова виден всегда. Для связи внутри слова марш повторяют с другими фонами (36б.2).

Порча самого состояния тоже поймана: бит pos_inv перевернули — ram_step вернул 8 (ST_STATE).


12б.4 Флеш-память: эталон в ELF и проверка самого блока

RM0008 и RM0090 так и описывают назначение блока CRC: сигнатура программы, посчитанная в работе, сравнивается с эталоном, посчитанным при сборке. Эталон должен лежать в известном месте. Здесь его место — последнее слово образа, и оставляет его скрипт компоновки:

  .crc_ref :                             /* эталон: сразу за образом, пока — 0xFFFFFFFF */
  {
    __crc_ref = .;
    LONG(0xFFFFFFFF)
  } > FLASH

Пока там 0xFFFFFFFF. Вторая проверка компоновщика не даёт этому слову отойти от конца образа:

  ASSERT(__crc_ref == LOADADDR(.data) + SIZEOF(.data), "CRC reference must follow the image")

После сборки скрипт patch12b.py считает CRC-32/MPEG-2 так же, как блок CRC у F1 и F4 (слова младшим байтом вперёд, начало 0xFFFFFFFF, без отражений; 12.5), и вписывает её в сам ELF:

elf = sys.argv[1]
nm = subprocess.run(["arm-none-eabi-nm", elf], capture_output=True, text=True, check=True).stdout
ref_addr = int([l.split()[0] for l in nm.splitlines() if l.endswith(" __crc_ref")][0], 16)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf + ".raw"], check=True)
raw = open(elf + ".raw", "rb").read()
n = ref_addr - ORIGIN
if len(raw) != n + 4 or n % 4 or raw[n:] != b"\xff\xff\xff\xff":
    sys.exit("образ не тот: длина %d, __crc_ref на смещении %d" % (len(raw), n))
crc = crc_mpeg2(raw[:n])
open(elf + ".ref", "wb").write(struct.pack("<I", crc))
subprocess.run(["arm-none-eabi-objcopy", "--update-section", ".crc_ref=" + elf + ".ref", elf], check=True)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf[:-4] + ".bin"], check=True)
out = open(elf[:-4] + ".bin", "rb").read()
assert out[:n] == raw[:n] and struct.unpack("<I", out[n:])[0] == crc
print("crc_ref 0x%08X at 0x%08X, image %d bytes = %d words" % (crc, ref_addr, n, n // 4))

Почему в ELF, а не дописать слово в конец .bin (так сделано в 36б.5): отладчик и среда разработки прошивают ELF. Эталон, живущий только в .bin, на плату, прошитую из среды, не попадёт, и проверка при старте будет отказывать каждый раз. --update-section меняет содержимое секции на месте: адреса и остальные байты те же, скрипт это сверяет.

Прогон F1 в QEMU: образ 857 слов (3428 байт), эталон 0xD01F545B по адресу 0x08000D64; при старте CRC сошлась. Затем в скопированном ELF перевернули один бит в таблице констант app_table — CRC при старте вышла 0xE41DDB8D, и прошивка ушла в безопасное состояние с кодом 3.

Сам блок CRC тоже проверяют. Если его такт не включился или блок неисправен, CRC_DR вернёт одно и то же, и проверка сравнит мусор с мусором. Поэтому перед образом — известный ответ:

int crc_unit_ok(void)                      /* сам блок: сброс и одно известное слово */
{
    RCC_CRCEN_REG |= RCC_CRCEN;
    CRC_CR = CRC_CR_RESET;
    if (CRC_DR != 0xFFFFFFFFu)
        return 0;
    CRC_DR = 0x12345678u;
    return CRC_DR == 0xDF8A8A2Bu;
}

Ответ 0xDF8A8A2B на слово 0x12345678 проверка раздела пересчитала сама. В QEMU блока CRC нет — crc_unit_ok вернул 0, как на неисправном камне. Поэтому сборка для QEMU идёт с ключом SOFT_CRC — тот же вариант CRC программно; в сборке для изделия нулевой ответ ведёт в безопасное состояние.

В работе — по 256 слов за проход:

int flash_step(const uint32_t *img, uint32_t words, uint32_t ref)  /* в работе: по FLASH_STEP слов */
{
    uint32_t i = fpos, end;

    if ((i ^ fpos_inv) != 0xFFFFFFFFu || i >= words)
        return -2;                         /* испорчено состояние проверки */
    if (i == 0u)
        CRC_RESET();
    end = (words - i > FLASH_STEP) ? i + FLASH_STEP : words;
    for (; i < end; i++)
        CRC_FEED(img[i]);
    if (i < words) {
        fpos = i;
        fpos_inv = ~i;
        return 0;                          /* круг не кончился */
    }
    fpos = 0u;
    fpos_inv = ~0u;
    return (CRC_READ() == ref) ? 1 : -1;   /* круг кончился: сошлось или нет */
}

Цикл подачи — 5 команд на слово (дизассемблер F103 и F407), а сам блок по RM тратит на слово 4 такта шины AHB и задерживает следующую запись до конца расчёта. Нижняя граница шага — 256 × 5 = 1280 тактов: 17,8 мкс на 72 МГц. Вся флеш-память F103 на 64 КБ — 64 шага, не меньше 1,14 мс; F407 на 1 МБ — 1024 шага, не меньше 7,8 мс на 168 МГц. Проверять нужно только образ до __crc_ref, а не всю флеш-память: в опыте это 857 слов.

На ПК — образ из 1024 слов:


12б.5 Такт: HSE против LSE внутри F407

CSS (раздел 6) ловит только остановку HSE: по RM0090 система переходит на HSI, PLL от HSE выключается, приходит NMI. Кварц не той частоты, ошибка в делителях PLL или частота, уехавшая на проценты, для CSS — исправный такт. Такую частоту видно только сравнением с независимым генератором.

У F407 для этого есть путь внутри камня: вход канала 4 таймера TIM5 переключается битами TI4_RMP регистра TIM5_OR (смещение 0x50): 01 — LSI, 10 — LSE, 11 — прерывание пробуждения RTC (RM0090, 18.4.20). TIM5 32-битный, разность захватов не завернётся:

uint32_t clk_measure_f407(uint32_t ti4_rmp)   /* 1 — LSI, 2 — LSE; вывод не нужен */
{
    uint32_t c1 = 0u, cap = 0u, k, t;

    RCC_APB1ENR |= RCC_TIM5EN;
    TIM5_OR = ti4_rmp << TIM5_OR_TI4_RMP_Pos;  /* TI4_RMP: опора на вход канала 4 внутри камня */
    TIM5_PSC = 0u;
    TIM5_ARR = 0xFFFFFFFFu;                    /* TIM5 32-битный: разность не завернётся */
    TIM5_CCMR2 = TIM_CCMR2_CC4S_TI4 | TIM_CCMR2_IC4PSC_8;
    TIM5_CCER = TIM_CCER_CC4E;
    TIM5_SR = 0u;
    TIM5_CR1 = TIM_CR1_CEN;
    for (k = 0u; k < 3u; k++) {                /* первый захват — не с начала периода, его отбросить */
        for (t = 0u; (TIM5_SR & TIM_SR_CC4IF) == 0u; t++) {
            if (t > CLK_WAIT) {
                TIM5_CR1 = 0u;
                return 0u;
            }
        }
        cap = TIM5_CCR4;                       /* чтение CCR4 снимает CC4IF */
        if (k == 1u)
            c1 = cap;
    }
    TIM5_CR1 = 0u;
    return cap - c1;                           /* тактов TIM5 за 8 периодов опоры */
}

У F103 такого пути нет: бит TIM5CH4_IREMAP в AFIO_MAPR (LSI на вход TIM5) по RM0008 есть лишь у части семейства, а у F103C8 нет самого TIM5 — опору заводят проводом (36б.6).

Окно. TIM5 сидит на APB1. При HSE 8 МГц и 168 МГц на ядре APB1 — 42 МГц, а таймеры на нём при делителе APB больше 1 тактуются вдвое чаще (RM0090): 84 МГц. За 8 периодов LSE это 20 507,81 такта; один такт квантования — 48,8 ppm. Окно считает при сборке макрос:

#define CLK_LO(FT, F_MAX, N, TOL_PPM) \
    ((uint32_t)((unsigned long long)(FT) * (1000000ull - (TOL_PPM)) * (N) / ((F_MAX) * 1000000ull)) - 1u)
#define CLK_HI(FT, F_MIN, N, TOL_PPM) \
    ((uint32_t)(((unsigned long long)(FT) * (1000000ull + (TOL_PPM)) * (N) + (F_MIN) * 1000000ull - 1u) \
                / ((F_MIN) * 1000000ull)) + 1u)

Допуск 0,5 % — выбор этого раздела: он вмещает уход кварца и опоры с большим запасом. RM0090 о точности часового кварца говорит только «обычно несколько десятков ppm», и модель берёт LSE в ±50 ppm. LSI у F407 по даташиту — 17…47 кГц. Окна: по LSE — 20 404…20 612, по LSI — 14 225…39 729. Прогон на ПК — счёт при каждом значении опоры в её разбросе, с квантованием ±1:

Такт TIM5 Окно по LSE Окно по LSI
исправно, номинал молчит молчит
исправно, −0,4 % молчит молчит
исправно, +0,4 % молчит молчит
HSE пропал, CSS перевёл на HSI: 8 МГц ловит ловит
×0,5 ловит может пропустить
×0,98 ловит может пропустить
×0,99 ловит может пропустить
×0,995 не ловит не ловит
×1,01 ловит может пропустить
×1,02 ловит может пропустить
×1,5 ловит может пропустить
×2 ловит может пропустить

«Может пропустить» — при каком-то допустимом LSI счёт неисправного такта попадает в окно. Разброс LSI — почти втрое (47 / 17), поэтому окно по LSI не гарантирует даже грубые ×0,5 и ×2. RM0090 для грубой проверки (−50/+100 %) предлагает обратный путь — TIM11 считает HSE, делённый для RTC, при такте от HSI; точную проверку даёт только LSE.

В QEMU захват не моделируется: clk_measure_f407 не дождался ни одного фронта и вернул 0, прошивка ушла в безопасное состояние с кодом 5. Так и должно быть: молчащая опора — тоже отказ.


12б.6 Регистры ядра, стек, расписание и отказ

Регистры ядра. Из C проверить «все регистры» нельзя: компилятор сам решает, какие занять. Тест — на ассемблере: чётные регистры получают 0xAAAAAAAA, нечётные — 0x55555555, потом наоборот. Так каждый бит каждого регистра побывает и нулём, и единицей, а соседние регистры держат противоположные значения. Обе константы — «модифицированные» константы Thumb-2, они помещаются прямо в команды mov и cmp:

    .macro  FILL a, b
    mov     r0, #\a
    mov     r1, #\b
    mov     r2, #\a
    mov     r3, #\b
    mov     r4, #\a
    mov     r5, #\b
    mov     r6, #\a
    mov     r7, #\b
    mov     r8, #\a
    mov     r9, #\b
    mov     r10, #\a
    mov     r11, #\b
    mov     r12, #\a
    mov     lr, #\a
    .endm

CHECK — те же 14 регистров через cmp и bne 9f. Сама функция:

cpu_reg_test:
    push    {r4-r11, lr}
    FILL    0xAAAAAAAA, 0x55555555
    CHECK   0xAAAAAAAA, 0x55555555
    FILL    0x55555555, 0xAAAAAAAA
#ifdef INJECT_REG
    eor     r5, r5, #0x10           @ опыт: бит 4 регистра R5 «залип»
#endif
    CHECK   0x55555555, 0xAAAAAAAA
    mov     r0, #0xAAAAAAAA         @ PSP: биты 1:0 всегда нули
    bic     r0, r0, #3
    msr     psp, r0
    mrs     r1, psp
    cmp     r1, r0
    bne     9f
    mvn     r0, r0
    bic     r0, r0, #3
    msr     psp, r0
    mrs     r1, psp
    cmp     r1, r0
    bne     9f
    mov     r0, #0xA8000000         @ APSR: N, C, Q — затем Z, V
    msr     APSR_nzcvq, r0
    mrs     r1, APSR
    and     r1, r1, #0xF8000000
    cmp     r1, r0
    bne     9f
    mov     r0, #0x50000000
    msr     APSR_nzcvq, r0
    mrs     r1, APSR
    and     r1, r1, #0xF8000000
    cmp     r1, r0
    bne     9f
    movs    r0, #0
    msr     psp, r0
    pop     {r4-r11, pc}
9:  movs    r0, #1
    pop     {r4-r11, pc}
    .size   cpu_reg_test, . - cpu_reg_test

У PSP биты 1:0 всегда нули, поэтому шаблоны для него — 0xAAAAAAA8 и 0x55555554. Флаги APSR проверяются двумя наборами: N, C, Q, затем Z, V. Функция — 398 байт кода, в QEMU — 114 команд, на F103 при 72 МГц не меньше 1,6 мкс. Опыт с внесённым отказом: с ключом INJECT_REG после второго заполнения в r5 переворачивается бит 4 — тест вернул 1, и прошивка ушла в безопасное состояние с кодом 6 и на F1, и на F4. Не проверяются MSP (он в работе) и регистры FPU у F407 — S0–S31 проверяют так же, командами vmov; здесь это не собрано.

Стек. Стартовый код заливает стек шаблоном и кладёт на дно канарейку:

void stack_paint(void)                     /* один раз из стартового кода, до main */
{
    uint32_t *p = _sstack, *sp;

    __asm volatile("mov %0, sp" : "=r"(sp));
    for (uint32_t i = 0u; i < CANARY_WORDS; i++)
        *p++ = STACK_CANARY;
    while (p < sp - 16)                    /* 64 байта под своим кадром не трогать */
        *p++ = STACK_FILL;
}
int stack_canary_ok(void)                  /* в каждом проходе: четыре слова */
{
    for (uint32_t i = 0u; i < CANARY_WORDS; i++)
        if (_sstack[i] != STACK_CANARY)
            return 0;
    return 1;
}

Канарейку проверяют в каждом проходе — 27 команд. Глубину по заливке (stack_free_bytes) — изредка, для журнала. В опыте после всех проверок осталось нетронутыми 880 байт стека из 1008. С ключом INJECT_STACK функция с большим массивом дошла до дна стека — первый же проход вернул ST_STACK, код 7. Канарейка ловит переполнение после факта; первую же запись за границу ловит MPU (11а). Запас оставляют на кадр исключения: 32–36 байт у Cortex-M3 и 104–108 байт с FPU у F407 (7а) — заливка покажет прерывание, только если оно пришло в самый глубокий момент.

Расписание. Один проход главного цикла:

static int st_pass(void)                       /* один проход главного цикла */
{
    uint32_t wrapped = 0u;
    int r;

    if (!stack_canary_ok())
        return ST_STACK;
    r = ram_step(RAM_BASE, RAM_WORDS, &wrapped);
    if (r != ST_OK)
        return r;
    if (wrapped) {                             /* раз за круг ОЗУ: буфер и регистры ядра */
        r = ram_buf_test();
        if (r != ST_OK)
            return r;
        if (cpu_reg_test() != 0u)
            return ST_CPU;
    }
    r = flash_step(IMG, IMG_WORDS, *__crc_ref);
    if (r < 0)
        return (r == -1) ? ST_FLASH : ST_STATE;
    return ST_OK;                              /* только здесь кормят сторож */
}

Бюджет прохода — нижние границы по командам, сосчитанным в QEMU, и по циклу CRC:

Шаг Команд F103, 72 МГц F407, 168 МГц
канарейка 27 0,4 мкс 0,2 мкс
блок ОЗУ, 16 слов 715 / 713 9,9 мкс 4,2 мкс
шаг CRC, 256 слов 1280 тактов 17,8 мкс 7,6 мкс
раз за круг: буфер и регистры 518 + 114 8,8 мкс 3,8 мкс

Обычный проход F103 — не меньше 28 мкс: при проходе раз в миллисекунду это от 2,8 % процессора. Круг ОЗУ на 20 КБ — 342 прохода, 0,34 с; круг CRC по 64 КБ — 64 прохода. У F407 круг по 128 КБ ОЗУ — 2185 проходов, по 1 МБ флеш-памяти — 1024. В QEMU 2000 проходов подряд прошли без отказа.

Отказ. Любая проверка ведёт в одно место:

void st_fail(uint32_t code, uint32_t detail)   /* единственный выход при любом отказе */
{
    OUT_ODR = 0u;                              /* силовые выходы — выключить первым делом */
    if (st_rec.check != ~(st_rec.code ^ st_rec.detail ^ st_rec.count))
        st_rec.count = 0u;                     /* записи нет или она битая: начать счёт заново */
    st_rec.code = code;
    st_rec.detail = detail;
    st_rec.count++;
    st_rec.check = ~(code ^ detail ^ st_rec.count);
    puts1("SAFE code ");
    putu(code);
    puts1(" detail ");
    puthex(detail);
    puts1(" count ");
    putu(st_rec.count);
    puts1("\n");
    for (;;) {                                 /* сторож не кормится: сброс через его таймаут */
    }
}

Выходы выключаются первым делом. Запись об отказе — в .noinit со словом проверки: марш при старте её не трогает, и после сброса прошивка видит, что было и сколько раз. Повторный отказ — не перезапускаться по кругу, а остаться в безопасном состоянии (36б.6). Запись же уходит в журнал прибора (204.6): при возврате она вместе со строкой журнала выпуска (201.7) превращает «не работает» в диагноз.


12б.7 Грабли

Марш при старте стёр запись об отказе. Если марш идёт по всему ОЗУ, .noinit после сброса — нули, и прибор каждый раз «видит» первый отказ. Граница марша — _estack, начало .noinit.

Марш при старте на C. Функция на C сохраняет на стеке адрес возврата и регистры, марш их стирает — вместо отчёта HardFault. Только ассемблер и только регистры, которые можно портить.

Эталон только в .bin. Прошили ELF из среды разработки — в слове эталона 0xFFFFFFFF, проверка отказывает при каждом старте. Эталон вписывают в ELF, а .bin делают уже из него.

Сборка для QEMU ушла в изделие. Ключ SOFT_CRC отключает отказ по неответившему блоку CRC. В изделии без блока проверка сравнит мусор с мусором — crc_unit_ok должен вести в безопасное состояние.

Порчу флеш-памяти увидели через круг. Байт испортился в уже пройденной части — текущий круг сошёлся. Задержка обнаружения — до двух кругов CRC, её и закладывают во время реакции.

Состояние самопроверки в проверяемой области. Если pos лежит в .bss, марш гоняет по ней нули и единицы вперемешку с её же использованием. Своё хозяйство — в отдельной секции вне марша в работе, а позиция — значением и инверсией.

Окно по LSI «в норме», а кварц не тот. У F407 окно по LSI пропускает и ×0,5, и ×2 — по LSI видна только пропажа HSE, которую и так ловит CSS.

Буфер марша на стеке. Локальный массив вместо save[] в .st_ram съедает стек и уходит из проверяемого буфера в непроверяемый стек.


12б.8 Итоги

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

st12b.h — общий заголовок самопроверки.

/* Раздел 12б: самопроверка прошивки — ОЗУ, флеш-память, такт, ядро и стек. */
#ifndef ST12B_H
#define ST12B_H
#include <stdint.h>

#ifndef ST_RD                            /* на ПК обращения к ОЗУ подменяет модель неисправной памяти */
#define ST_RD(p)      (*(p))
#define ST_WR(p, v)   (*(p) = (v))
#endif

#define RAM_BLOCK     16u                /* слов в блоке прозрачного марша */
#define FLASH_STEP    256u               /* слов CRC за шаг */

enum { ST_OK = 0, ST_RAM = 1, ST_BUF = 2, ST_FLASH = 3, ST_CRCUNIT = 4,
       ST_CLOCK = 5, ST_CPU = 6, ST_STACK = 7, ST_STATE = 8 };

/* ОЗУ */
uint32_t march_own(volatile uint32_t *p, uint32_t *s, uint32_t n);
void     ram_reset(void);
int      ram_step(volatile uint32_t *base, uint32_t words, uint32_t *wrapped);
int      ram_buf_test(void);
extern uint32_t ram_bits;

/* флеш-память */
uint32_t crc_soft(uint32_t crc, uint32_t word);
int      crc_unit_ok(void);
uint32_t flash_crc(const uint32_t *img, uint32_t words);
void     flash_reset(void);
int      flash_step(const uint32_t *img, uint32_t words, uint32_t ref);

/* такт */
int      clk_in_window(uint32_t count, uint32_t lo, uint32_t hi);
uint32_t clk_measure_f407(uint32_t ti4_rmp);

/* ядро и стек (st12b_asm.S, st12b_stack.c) */
uint32_t cpu_reg_test(void);
uint32_t march_startup(uint32_t *start, uint32_t *end);
void     stack_paint(void);
int      stack_canary_ok(void);
uint32_t stack_free_bytes(void);

/* окно счёта таймера за n периодов опоры (F_MIN…F_MAX Гц), такт таймера FT Гц
 * с допуском TOL_PPM; ±1 — на квантование захвата. Считается при сборке. */
#define CLK_LO(FT, F_MAX, N, TOL_PPM) \
    ((uint32_t)((unsigned long long)(FT) * (1000000ull - (TOL_PPM)) * (N) / ((F_MAX) * 1000000ull)) - 1u)
#define CLK_HI(FT, F_MIN, N, TOL_PPM) \
    ((uint32_t)(((unsigned long long)(FT) * (1000000ull + (TOL_PPM)) * (N) + (F_MIN) * 1000000ull - 1u) \
                / ((F_MIN) * 1000000ull)) + 1u)

#endif

st12b_asm.S — марш ОЗУ без стека и тест регистров ядра на ассемблере.

/* Раздел 12б: то, что нельзя написать на C, — марш ОЗУ без стека и тест регистров ядра. */
    .syntax unified
    .thumb
    .text

/* uint32_t march_startup(uint32_t *start, uint32_t *end)
 * March C− по [start, end) словами; область не пустая. Стек не нужен: только r0–r3 и r12,
 * поэтому годится и до инициализации ОЗУ, и из C. Возврат: 0 — исправно, иначе — биты ошибок. */
    .global march_startup
    .type   march_startup, %function
    .thumb_func
march_startup:
    mov     r12, #0                 @ накопитель ошибок
    movs    r3, #0
    mov     r2, r0
1:  str     r3, [r2], #4            @ ⇕(w0)
    cmp     r2, r1
    blo     1b
    mov     r2, r0
2:  ldr     r3, [r2]                @ ⇑(r0, w1)
    orr     r12, r12, r3
    mvn     r3, #0
    str     r3, [r2], #4
    cmp     r2, r1
    blo     2b
    mov     r2, r0
3:  ldr     r3, [r2]                @ ⇑(r1, w0)
    orn     r12, r12, r3
    movs    r3, #0
    str     r3, [r2], #4
    cmp     r2, r1
    blo     3b
    mov     r2, r1
4:  ldr     r3, [r2, #-4]!          @ ⇓(r0, w1)
    orr     r12, r12, r3
    mvn     r3, #0
    str     r3, [r2]
    cmp     r2, r0
    bhi     4b
    mov     r2, r1
5:  ldr     r3, [r2, #-4]!          @ ⇓(r1, w0)
    orn     r12, r12, r3
    movs    r3, #0
    str     r3, [r2]
    cmp     r2, r0
    bhi     5b
    mov     r2, r0
6:  ldr     r3, [r2], #4            @ ⇕(r0)
    orr     r12, r12, r3
    cmp     r2, r1
    blo     6b
    mov     r0, r12
    bx      lr
    .size   march_startup, . - march_startup

/* uint32_t cpu_reg_test(void)
 * Шахматные шаблоны в R0–R12 и LR (чётные и нечётные — противоположные), затем наоборот;
 * PSP и флаги APSR. Возврат: 0 — исправно, 1 — отказ. Регистры R4–R11 сохраняются. */
    .macro  FILL a, b
    mov     r0, #\a
    mov     r1, #\b
    mov     r2, #\a
    mov     r3, #\b
    mov     r4, #\a
    mov     r5, #\b
    mov     r6, #\a
    mov     r7, #\b
    mov     r8, #\a
    mov     r9, #\b
    mov     r10, #\a
    mov     r11, #\b
    mov     r12, #\a
    mov     lr, #\a
    .endm

    .macro  CHECK a, b
    cmp     r0, #\a
    bne     9f
    cmp     r1, #\b
    bne     9f
    cmp     r2, #\a
    bne     9f
    cmp     r3, #\b
    bne     9f
    cmp     r4, #\a
    bne     9f
    cmp     r5, #\b
    bne     9f
    cmp     r6, #\a
    bne     9f
    cmp     r7, #\b
    bne     9f
    cmp     r8, #\a
    bne     9f
    cmp     r9, #\b
    bne     9f
    cmp     r10, #\a
    bne     9f
    cmp     r11, #\b
    bne     9f
    cmp     r12, #\a
    bne     9f
    cmp     lr, #\a
    bne     9f
    .endm

    .global cpu_reg_test
    .type   cpu_reg_test, %function
    .thumb_func
cpu_reg_test:
    push    {r4-r11, lr}
    FILL    0xAAAAAAAA, 0x55555555
    CHECK   0xAAAAAAAA, 0x55555555
    FILL    0x55555555, 0xAAAAAAAA
#ifdef INJECT_REG
    eor     r5, r5, #0x10           @ опыт: бит 4 регистра R5 «залип»
#endif
    CHECK   0x55555555, 0xAAAAAAAA
    mov     r0, #0xAAAAAAAA         @ PSP: биты 1:0 всегда нули
    bic     r0, r0, #3
    msr     psp, r0
    mrs     r1, psp
    cmp     r1, r0
    bne     9f
    mvn     r0, r0
    bic     r0, r0, #3
    msr     psp, r0
    mrs     r1, psp
    cmp     r1, r0
    bne     9f
    mov     r0, #0xA8000000         @ APSR: N, C, Q — затем Z, V
    msr     APSR_nzcvq, r0
    mrs     r1, APSR
    and     r1, r1, #0xF8000000
    cmp     r1, r0
    bne     9f
    mov     r0, #0x50000000
    msr     APSR_nzcvq, r0
    mrs     r1, APSR
    and     r1, r1, #0xF8000000
    cmp     r1, r0
    bne     9f
    movs    r0, #0
    msr     psp, r0
    pop     {r4-r11, pc}
9:  movs    r0, #1
    pop     {r4-r11, pc}
    .size   cpu_reg_test, . - cpu_reg_test

/* Сброс: марш всего ОЗУ до .noinit раньше любого кода на C. SysTick считает его
 * такты — только для опыта в QEMU. */
    .global Reset_Handler
    .type   Reset_Handler, %function
    .thumb_func
Reset_Handler:
    ldr     r2, =0xE000E010         @ SysTick
    ldr     r3, =0x00FFFFFF
    str     r3, [r2, #4]            @ RVR
    str     r3, [r2, #8]            @ CVR: любая запись обнуляет
    movs    r3, #5
    str     r3, [r2]                @ CSR: такт ядра, счёт
    ldr     r0, =_sram
    ldr     r1, =_estack            @ до .noinit: запись об отказе переживает сброс
    bl      march_startup
    cbnz    r0, boot_fail
    ldr     r2, =0xE000E018
    ldr     r0, [r2]                @ остаток SysTick — в c_start
    bl      c_start
boot_fail:
    b       boot_fail               @ ОЗУ неисправно: C не запускать, выходы в покое, ждать сторожа
    .size   Reset_Handler, . - Reset_Handler
    .ltorg

st12b_ram.c — прозрачный March C− в работе.

/* Раздел 12б: ОЗУ в работе — прозрачный March C− на своих данных, по блокам. */
#include "st12b.h"

#ifdef HOST
#define irq_save()     0u
#define irq_restore(m) ((void)(m))
#else
static inline uint32_t irq_save(void)
{
    uint32_t m;
    __asm volatile("mrs %0, primask\n\tcpsid i" : "=r"(m) : : "memory");
    return m;
}
static inline void irq_restore(uint32_t m)
{
    __asm volatile("msr primask, %0" : : "r"(m) : "memory");
}
#endif

/* Своё хозяйство самопроверки — в отдельной секции, вне проверяемой области. */
#define ST_SECT __attribute__((section(".st_ram")))
static uint32_t save[RAM_BLOCK] ST_SECT;   /* копия блока: ожидаемые значения */
static uint32_t pos ST_SECT;               /* начало следующего блока, в словах */
static uint32_t pos_inv ST_SECT;           /* то же, инверсией: порча видна */
uint32_t ram_bits ST_SECT;                 /* биты последней ошибки — для журнала */

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

    for (i = 0u; i < n; i++) { s[i] = ST_RD(&p[i]); }                              /* копия: a     */
    for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i];  ST_WR(&p[i], ~s[i]); } /* ⇑(ra, w~a)   */
    for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); }  /* ⇑(r~a, wa)   */
    for (i = n; i-- > 0u;)   { err |= ST_RD(&p[i]) ^ s[i];  ST_WR(&p[i], ~s[i]); } /* ⇓(ra, w~a)   */
    for (i = n; i-- > 0u;)   { err |= ST_RD(&p[i]) ^ ~s[i]; ST_WR(&p[i], s[i]); }  /* ⇓(r~a, wa)   */
    for (i = 0u; i < n; i++) { err |= ST_RD(&p[i]) ^ s[i]; }                       /* ⇕(ra)        */
    return err;
}

void ram_reset(void)
{
    pos = 0u;
    pos_inv = ~0u;
}

int ram_step(volatile uint32_t *base, uint32_t words, uint32_t *wrapped)
{
    uint32_t n = RAM_BLOCK, err, m, p = pos;

    if ((p ^ pos_inv) != 0xFFFFFFFFu || p >= words)
        return ST_STATE;                          /* испорчено само состояние проверки */
    if (p + n > words)
        n = words - p;
    m = irq_save();                               /* посреди марша блок не должен видеть никто */
    err = march_own(base + p, save, n);
    irq_restore(m);
    *wrapped = (p + n >= words);
    p = *wrapped ? 0u : p + n - 1u;               /* блоки перекрываются на одно слово */
    pos = p;
    pos_inv = ~p;
    ram_bits = err;
    return err ? ST_RAM : ST_OK;
}

int ram_buf_test(void)                            /* буфер между шагами ничего не хранит */
{
    return march_startup(save, save + RAM_BLOCK) ? ST_BUF : ST_OK;
}

st12b_flash.c — CRC-32 образа против эталона.

/* Раздел 12б: флеш-память — CRC-32 всего образа против эталона, записанного при сборке. */
#include "st12b.h"
#ifndef HOST
#include "regs12b.h"
#endif

#define ST_SECT __attribute__((section(".st_ram")))
static uint32_t fpos ST_SECT;              /* сколько слов уже подано в этом круге */
static uint32_t fpos_inv ST_SECT;          /* то же инверсией; начальные значения — flash_reset() */

uint32_t crc_soft(uint32_t crc, uint32_t word)   /* CRC-32/MPEG-2 словом: как блок CRC у F1 и F4 */
{
    crc ^= word;
    for (int b = 0; b < 32; b++)
        crc = (crc & 0x80000000u) ? (crc << 1) ^ 0x04C11DB7u : crc << 1;
    return crc;
}

#ifdef SOFT_CRC                            /* QEMU блок CRC не моделирует; на ПК — тоже программно */
static uint32_t acc ST_SECT;
#define CRC_RESET()  (acc = 0xFFFFFFFFu)
#define CRC_FEED(w)  (acc = crc_soft(acc, (w)))
#define CRC_READ()   (acc)
#else
#define CRC_RESET()  (CRC_CR = CRC_CR_RESET)
#define CRC_FEED(w)  (CRC_DR = (w))
#define CRC_READ()   (CRC_DR)
#endif

#ifndef HOST
int crc_unit_ok(void)                      /* сам блок: сброс и одно известное слово */
{
    RCC_CRCEN_REG |= RCC_CRCEN;
    CRC_CR = CRC_CR_RESET;
    if (CRC_DR != 0xFFFFFFFFu)
        return 0;
    CRC_DR = 0x12345678u;
    return CRC_DR == 0xDF8A8A2Bu;
}
#endif

uint32_t flash_crc(const uint32_t *img, uint32_t words)   /* при старте: весь образ сразу */
{
    CRC_RESET();
    for (uint32_t i = 0u; i < words; i++)
        CRC_FEED(img[i]);
    return CRC_READ();
}

void flash_reset(void)
{
    fpos = 0u;
    fpos_inv = ~0u;
}

int flash_step(const uint32_t *img, uint32_t words, uint32_t ref)  /* в работе: по FLASH_STEP слов */
{
    uint32_t i = fpos, end;

    if ((i ^ fpos_inv) != 0xFFFFFFFFu || i >= words)
        return -2;                         /* испорчено состояние проверки */
    if (i == 0u)
        CRC_RESET();
    end = (words - i > FLASH_STEP) ? i + FLASH_STEP : words;
    for (; i < end; i++)
        CRC_FEED(img[i]);
    if (i < words) {
        fpos = i;
        fpos_inv = ~i;
        return 0;                          /* круг не кончился */
    }
    fpos = 0u;
    fpos_inv = ~0u;
    return (CRC_READ() == ref) ? 1 : -1;   /* круг кончился: сошлось или нет */
}

st12b_clock.c — проверка такта по независимой опоре.

/* Раздел 12б: такт — счёт таймера за 8 периодов независимой опоры против окна. */
#include "st12b.h"
#ifndef HOST
#include "regs12b.h"
#endif

int clk_in_window(uint32_t count, uint32_t lo, uint32_t hi)
{
    return count >= lo && count <= hi;
}

#if !defined(HOST) && !defined(F1)
#define CLK_WAIT 2000000u                  /* ~50 мс при 168 МГц: опора молчит — это отказ */

uint32_t clk_measure_f407(uint32_t ti4_rmp)   /* 1 — LSI, 2 — LSE; вывод не нужен */
{
    uint32_t c1 = 0u, cap = 0u, k, t;

    RCC_APB1ENR |= RCC_TIM5EN;
    TIM5_OR = ti4_rmp << TIM5_OR_TI4_RMP_Pos;  /* TI4_RMP: опора на вход канала 4 внутри камня */
    TIM5_PSC = 0u;
    TIM5_ARR = 0xFFFFFFFFu;                    /* TIM5 32-битный: разность не завернётся */
    TIM5_CCMR2 = TIM_CCMR2_CC4S_TI4 | TIM_CCMR2_IC4PSC_8;
    TIM5_CCER = TIM_CCER_CC4E;
    TIM5_SR = 0u;
    TIM5_CR1 = TIM_CR1_CEN;
    for (k = 0u; k < 3u; k++) {                /* первый захват — не с начала периода, его отбросить */
        for (t = 0u; (TIM5_SR & TIM_SR_CC4IF) == 0u; t++) {
            if (t > CLK_WAIT) {
                TIM5_CR1 = 0u;
                return 0u;
            }
        }
        cap = TIM5_CCR4;                       /* чтение CCR4 снимает CC4IF */
        if (k == 1u)
            c1 = cap;
    }
    TIM5_CR1 = 0u;
    return cap - c1;                           /* тактов TIM5 за 8 периодов опоры */
}
#endif

st12b_stack.c — канарейка и заливка стека.

/* Раздел 12б: стек — канарейка на дне и заливка шаблоном для учёта глубины. */
#include "st12b.h"

extern uint32_t _sstack[], _estack[];      /* дно и вершина стека — из скрипта компоновки */
#define STACK_CANARY  0x5AFEC0DEu
#define STACK_FILL    0xA5A5A5A5u
#define CANARY_WORDS  4u

void stack_paint(void)                     /* один раз из стартового кода, до main */
{
    uint32_t *p = _sstack, *sp;

    __asm volatile("mov %0, sp" : "=r"(sp));
    for (uint32_t i = 0u; i < CANARY_WORDS; i++)
        *p++ = STACK_CANARY;
    while (p < sp - 16)                    /* 64 байта под своим кадром не трогать */
        *p++ = STACK_FILL;
}

int stack_canary_ok(void)                  /* в каждом проходе: четыре слова */
{
    for (uint32_t i = 0u; i < CANARY_WORDS; i++)
        if (_sstack[i] != STACK_CANARY)
            return 0;
    return 1;
}

uint32_t stack_free_bytes(void)            /* изредка: сколько стека ни разу не тронуто */
{
    uint32_t *p = _sstack + CANARY_WORDS;

    while (p < _estack && *p == STACK_FILL)
        p++;
    return (uint32_t)(p - (_sstack + CANARY_WORDS)) * 4u;
}

app12b.c — прошивка-опыт: самопроверка при старте, проходы в работе и внесённые отказы.

/* Раздел 12б: прошивка-опыт. Самопроверка при старте, затем проходы «в работе» с замером
 * SysTick (в QEMU с -icount он считает выполненные команды). Опыты с отказами — ключами
 * сборки INJECT_REG, INJECT_STACK и порчей байта образа после patch12b.py. */
#include "st12b.h"
#include "regs12b.h"

extern uint32_t _sram[], _estack[], _sdata[], _ebss[], _sstack[], __crc_ref[];
extern uint32_t boot_syst;

#define IMG        ((const uint32_t *)0x08000000u)
#define IMG_WORDS  ((uint32_t)(__crc_ref - (uint32_t *)0x08000000u))
#define RAM_BASE   ((volatile uint32_t *)_sdata)
#define RAM_WORDS  ((uint32_t)(_ebss - _sdata))

uint32_t app_buf[1024];                        /* прикладные данные: их марш и проверяет в работе */
const uint32_t app_table[8] = { 0x13572468u, 0x2468ACE0u, 0x0F1E2D3Cu, 0x4B5A6978u,
                                0x8796A5B4u, 0xC3D2E1F0u, 0x01234567u, 0x89ABCDEFu };

typedef struct { uint32_t code, detail, count, check; } st_rec_t;
st_rec_t st_rec __attribute__((section(".noinit")));  /* переживает сброс и марш при старте */

static void putc1(char c)
{
    while (!(USART1_SR & (1u << 7))) {
    }
    USART1_DR = (uint32_t)c;
}
static void puts1(const char *s)
{
    while (*s)
        putc1(*s++);
}
static void putu(uint32_t v)
{
    char b[11];
    int i = 10;
    b[i] = 0;
    do {
        b[--i] = (char)('0' + v % 10u);
        v /= 10u;
    } while (v);
    puts1(&b[i]);
}
static void puthex(uint32_t v)
{
    puts1("0x");
    for (int i = 28; i >= 0; i -= 4)
        putc1("0123456789ABCDEF"[(v >> i) & 0xFu]);
}
static void kv(const char *k, uint32_t v)
{
    puts1(k);
    putu(v);
    puts1("\n");
}

static uint32_t tick(void)
{
    return SYST_CVR;
}
static uint32_t since(uint32_t t0)
{
    return (t0 - SYST_CVR) & 0x00FFFFFFu;
}
__attribute__((noinline)) static void cal_loop(uint32_t n)   /* ровно 2 команды на круг */
{
    __asm volatile("1: subs %0, #1\n\tbne 1b" : "+r"(n));
}

void st_fail(uint32_t code, uint32_t detail)   /* единственный выход при любом отказе */
{
    OUT_ODR = 0u;                              /* силовые выходы — выключить первым делом */
    if (st_rec.check != ~(st_rec.code ^ st_rec.detail ^ st_rec.count))
        st_rec.count = 0u;                     /* записи нет или она битая: начать счёт заново */
    st_rec.code = code;
    st_rec.detail = detail;
    st_rec.count++;
    st_rec.check = ~(code ^ detail ^ st_rec.count);
    puts1("SAFE code ");
    putu(code);
    puts1(" detail ");
    puthex(detail);
    puts1(" count ");
    putu(st_rec.count);
    puts1("\n");
    for (;;) {                                 /* сторож не кормится: сброс через его таймаут */
    }
}

static int st_pass(void)                       /* один проход главного цикла */
{
    uint32_t wrapped = 0u;
    int r;

    if (!stack_canary_ok())
        return ST_STACK;
    r = ram_step(RAM_BASE, RAM_WORDS, &wrapped);
    if (r != ST_OK)
        return r;
    if (wrapped) {                             /* раз за круг ОЗУ: буфер и регистры ядра */
        r = ram_buf_test();
        if (r != ST_OK)
            return r;
        if (cpu_reg_test() != 0u)
            return ST_CPU;
    }
    r = flash_step(IMG, IMG_WORDS, *__crc_ref);
    if (r < 0)
        return (r == -1) ? ST_FLASH : ST_STATE;
    return ST_OK;                              /* только здесь кормят сторож */
}

#ifdef INJECT_STACK
extern uint32_t _sstack[];
__attribute__((noinline)) static void stack_hog(void)   /* опыт: стек дошёл до канарейки */
{
    uint32_t sp;
    __asm volatile("mov %0, sp" : "=r"(sp));
    uint32_t n = (sp - (uint32_t)_sstack) / 4u - 2u;          /* массив ляжет до дна, в канарейку */
    volatile uint32_t big[n];
    for (uint32_t i = 0u; i < n; i++)
        big[i] = i;
    (void)big[0];
}
#endif

static uint32_t ram_sum(void)
{
    uint32_t s = 0u;
    for (uint32_t i = 0u; i < RAM_WORDS; i++)
        s = (s << 5 | s >> 27) ^ RAM_BASE[i];
    return s;
}

int main(void)
{
    uint32_t t, r, c, i, lo = 0xFFFFFFFFu, hi = 0u, steps;

    RCC_APB2ENR |= USART1_EN;
    USART1_BRR = BRR_115200;
    USART1_CR1 = (1u << 13) | (1u << 3);
#if defined(F1)
    puts1("selftest12b F1\n");
#else
    puts1("selftest12b F4\n");
#endif
    puts1("boot march words ");
    putu((uint32_t)(_estack - _sram));
    kv(" syst ", 0x00FFFFFFu - boot_syst);

    /* ── при старте: всё целиком ── */
    r = cpu_reg_test();
    kv("cpu ", r);
    if (r != 0u)
        st_fail(ST_CPU, r);
    r = (uint32_t)crc_unit_ok();
    kv("crc unit ", r);
#ifndef SOFT_CRC
    if (r == 0u)
        st_fail(ST_CRCUNIT, 0u);
#endif
    c = flash_crc(IMG, IMG_WORDS);
    puts1("flash words ");
    putu(IMG_WORDS);
    puts1(" crc ");
    puthex(c);
    puts1(" ref ");
    puthex(*__crc_ref);
    puts1("\n");
    if (c != *__crc_ref)
        st_fail(ST_FLASH, c);
    kv("table ", app_table[3] >> 24);
    for (i = 0u; i < 1024u; i++)
        app_buf[i] = i * 2654435761u;
    ram_reset();
    flash_reset();

    /* ── замеры: такты SysTick на каждый шаг ── */
    t = tick(); cal_loop(1000u); kv("t cal2000 ", since(t));
    t = tick(); cpu_reg_test(); kv("t cpu ", since(t));
    t = tick(); stack_canary_ok(); kv("t canary ", since(t));
    t = tick(); march_startup((uint32_t *)app_buf, (uint32_t *)app_buf + 256); kv("t march256 ", since(t));
    for (i = 0u; i < 1024u; i++)
        app_buf[i] = i * 2654435761u;
    r = ram_sum();
    t = tick(); ram_step(RAM_BASE, RAM_WORDS, &c); kv("t ram_step ", since(t));
    t = tick(); ram_buf_test(); kv("t buf ", since(t));
    t = tick(); flash_step(IMG, IMG_WORDS, *__crc_ref); kv("t flash_step ", since(t));
    ram_reset();
    flash_reset();

    /* круг ОЗУ: содержимое после него прежнее */
    steps = 0u;
    do {
        if (ram_step(RAM_BASE, RAM_WORDS, &c) != ST_OK)
            st_fail(ST_RAM, ram_bits);
        steps++;
    } while (!c);
    puts1("ram words ");
    putu(RAM_WORDS);
    puts1(" steps ");
    putu(steps);
    kv(" same ", ram_sum() == r);

    /* регистр с залипшими битами вместо ОЗУ: VTOR, биты 6:0 всегда нули */
    r = march_startup((uint32_t *)0xE000ED08u, (uint32_t *)0xE000ED08u + 1);
    REG(0xE000ED08u) = 0u;
    puts1("vtor march err ");
    puthex(r);
    puts1("\n");

#ifdef INJECT_STACK
    stack_hog();
#endif
    /* ── в работе: проходы, пока не сойдутся оба круга ── */
    ram_reset();
    flash_reset();
    for (i = 0u; i < 2000u; i++) {
        t = tick();
        r = (uint32_t)st_pass();
        c = since(t);
        if (r != ST_OK)
            st_fail(r, ram_bits);
        lo = c < lo ? c : lo;
        hi = c > hi ? c : hi;
    }
    puts1("passes 2000 ok\n");
    kv("t pass min ", lo);
    kv("t pass max ", hi);
    kv("stack free ", stack_free_bytes());
    kv("stack canary ", (uint32_t)stack_canary_ok());

#if !defined(F1)
    /* такт: TIM5 CH4 от LSE, 8 периодов; окно ±0,5 % при 84 МГц */
    c = clk_measure_f407(2u);
    puts1("clock lse count ");
    putu(c);
    puts1(" window ");
    putu(CLK_LO(84000000u, 32768u, 8u, 5000u));
    puts1("..");
    putu(CLK_HI(84000000u, 32768u, 8u, 5000u));
    puts1("\n");
    if (!clk_in_window(c, CLK_LO(84000000u, 32768u, 8u, 5000u), CLK_HI(84000000u, 32768u, 8u, 5000u)))
        st_fail(ST_CLOCK, c);
#endif
    puts1("done\n");
    for (;;) {
    }
}

linker12b.ld — скрипт компоновки: порядок ОЗУ и место эталона CRC.

/* Раздел 12б: скрипт компоновки. Порядок ОЗУ: .data и .bss (их проверяет марш в работе),
 * .st_ram (хозяйство самопроверки), стек с канарейкой на дне, .noinit (запись об отказе).
 * Во флеш-памяти последнее слово образа — эталон CRC; его вписывает patch12b.py. */
ENTRY(Reset_Handler)
INCLUDE mem.ld

_stack_size = 0x400;
_sram = ORIGIN(RAM);
_estack = ORIGIN(NOINIT);                /* вершина стека — под записью об отказе */
_sstack = _estack - _stack_size;

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))
  } > FLASH

  .text :
  {
    *(.text .text.*)
    *(.rodata .rodata.*)
    . = ALIGN(4);
  } > FLASH

  .data :
  {
    _sdata = .;
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH
  _sidata = LOADADDR(.data);

  .crc_ref :                             /* эталон: сразу за образом, пока — 0xFFFFFFFF */
  {
    __crc_ref = .;
    LONG(0xFFFFFFFF)
  } > FLASH

  .bss (NOLOAD) :
  {
    _sbss = .;
    *(.bss .bss.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM

  .st_ram (NOLOAD) :
  {
    _sst = .;
    *(.st_ram)
    . = ALIGN(4);
    _est = .;
  } > RAM

  .noinit (NOLOAD) :
  {
    *(.noinit .noinit.*)
  } > NOINIT

  ASSERT(_est <= _sstack, "RAM too small for data + selftest + stack")
  ASSERT(__crc_ref == LOADADDR(.data) + SIZEOF(.data), "CRC reference must follow the image")
}

patch12b.py — вписывает эталон CRC в ELF после сборки.

# Раздел 12б: эталон CRC образа — после сборки, в сам ELF.
# python3 patch12b.py fw.elf  →  считает CRC-32/MPEG-2 (как блок CRC у F1 и F4) по образу от 0x08000000
# до __crc_ref и вписывает слово в секцию .crc_ref ELF-файла; .bin делается уже из исправленного ELF.
import struct, subprocess, sys

ORIGIN = 0x08000000


def crc_mpeg2(data):
    crc = 0xFFFFFFFF
    for (w,) in struct.iter_unpack("<I", data):       # слово — младшим байтом вперёд, как читает ядро
        crc ^= w
        for _ in range(32):
            crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if crc & 0x80000000 else (crc << 1) & 0xFFFFFFFF
    return crc


elf = sys.argv[1]
nm = subprocess.run(["arm-none-eabi-nm", elf], capture_output=True, text=True, check=True).stdout
ref_addr = int([l.split()[0] for l in nm.splitlines() if l.endswith(" __crc_ref")][0], 16)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf + ".raw"], check=True)
raw = open(elf + ".raw", "rb").read()
n = ref_addr - ORIGIN
if len(raw) != n + 4 or n % 4 or raw[n:] != b"\xff\xff\xff\xff":
    sys.exit("образ не тот: длина %d, __crc_ref на смещении %d" % (len(raw), n))
crc = crc_mpeg2(raw[:n])
open(elf + ".ref", "wb").write(struct.pack("<I", crc))
subprocess.run(["arm-none-eabi-objcopy", "--update-section", ".crc_ref=" + elf + ".ref", elf], check=True)
subprocess.run(["arm-none-eabi-objcopy", "-O", "binary", elf, elf[:-4] + ".bin"], check=True)
out = open(elf[:-4] + ".bin", "rb").read()
assert out[:n] == raw[:n] and struct.unpack("<I", out[n:])[0] == crc
print("crc_ref 0x%08X at 0x%08X, image %d bytes = %d words" % (crc, ref_addr, n, n // 4))

Дальше: часть III, раздел 13 «Таймеры» — самый большой блок периферии: счёт, ШИМ, захват сигнала, энкодеры и синхронизация нескольких таймеров.

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