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

36б. Самопроверка во время работы: ядро, ОЗУ, флеш, частота

Разделы 36 и 36а — о том, как неисправность ищет человек: с отладчиком, мультиметром и осциллографом. В поле человека рядом нет. Прибор, который управляет нагревателем или приводом, должен сам заметить, что у него испортилась ячейка ОЗУ, бит во флеш-памяти или ушёл кварц, — и перейти в безопасное состояние раньше, чем ошибка дойдёт до выхода. Для бытовой техники такие проверки требует стандарт IEC 60730 для программ класса B. Текста стандарта на диске нет, поэтому здесь нет ни номеров его пунктов, ни допусков — только сами проверки, и каждая посчитана или прогнана у себя.

Библиотеки самопроверки ST (X-CUBE-CLASSB) на этой машине нет — ни в архивах STM32Cube, ни в паках Keil. Код раздела написан заново: марш March C- по ОЗУ, CRC образа флеш-памяти и сравнение частоты HSE с LSE или LSI. Что уже разобрано в других разделах, здесь не повторяется: сторож — 12.1–12.4, блок CRC и его вариант — 12.5 и 43.1, CRC образа в загрузчике — 48, система защиты тактирования (CSS) и подстройка HSI по LSE — 6.8 и 6.9, захват таймера — 13.6, безопасное состояние — 129.7, сторож в изделии — 204.6.

Опоры: справочные руководства RM0008 (F1) и RM0440 (G4), даташиты STM32F103x8/B и STM32G474 (DS12288), руководство по ядру Cortex-M3 (DUI0552A) — PDF из паков Keil. Модель неисправностей ОЗУ — прогон на ПК; код собран armclang для F103 и G474 и прогнан в QEMU на Cortex-M3.

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

Оригинал Перевод
self-test, start-up / run-time test самопроверка при старте / во время работы
March test, March C- маршевый тест ОЗУ
stuck-at fault (SAF) залипание бита в 0 или 1
transition fault (TF) переходная неисправность: бит не меняется в одну сторону
address decoder fault (AF) ошибка дешифратора адреса
coupling fault (CF) связь ячеек: запись в одну портит другую
transparent test прозрачный тест: содержимое после него прежнее
reference signature эталонная сигнатура (здесь — CRC образа)
clock security system (CSS) система защиты тактирования
safe state безопасное состояние

36б.1 Что проверять и когда

Проверок две группы. При старте проверяют всё целиком: ничего ещё не работает, и содержимое ОЗУ можно стереть. В работе — по кускам, по одному шагу за проход главного цикла, чтобы прикладные задачи не стояли.

Самопроверка по шагам: при старте — запуск сторожа, регистры ядра, всё ОЗУ маршем March C-, CRC всего образа, частота HSE против LSE или LSI; в работе в каждом проходе главного цикла — кусок ОЗУ в 16 слов при запрещённых прерываниях, порция CRC в 256 слов, проверка частоты захватом TIM5 и кормление сторожа, только если всё сошлось; любой отказ ведёт в безопасное состояние и сброс сторожем

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

  1. Сторож — первым. Если проверка сама зависнет, прибор всё равно перезапустится. Полный марш ОЗУ на F103 — не меньше 1,9 мс (36б.3), а таймаут сторожа ставится до 26 секунд (раздел 12).
  2. Регистры ядра. В каждый регистр R0–R12, указатели стека и регистр связи пишут чередующиеся шаблоны и читают обратно, проверяют флаги условий. Такой тест пишут на ассемблере: компилятор сам решает, какие регистры занять, и проверить из C «все регистры» нельзя. Здесь он не собран — описание без чисел.
  3. ОЗУ целиком маршем March C- (36б.2). Марш стирает содержимое, поэтому он идёт в стартовом коде до заполнения .data и .bss.
  4. CRC всего образа против эталона, записанного после сборки (36б.5).
  5. Частота: HSE против независимого генератора (36б.6).

У G474 часть ОЗУ проверяет сама себя: по даташиту первые 32 КБ SRAM и вся CCM SRAM (32 КБ) имеют аппаратную чётность — бит на байт, при ошибке — немаскируемое прерывание NMI. RM0440 прямо объясняет её требованиями «класса B». Включают её опционным битом SRAM_PE. Чётность ловит перевёрнутый бит при чтении, но не ошибку адреса: если адрес попал не в ту ячейку, там лежат данные с их собственной правильной чётностью. Поэтому марш нужен и там, где чётность есть.


36б.2 ОЗУ: какие неисправности ловит марш

Марш — это несколько проходов по всем словам подряд, вверх (⇑) или вниз (⇓) по адресам; в каждом слове проход делает одни и те же чтения и записи. March C-:

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

w0 — записать нули, r1 — прочитать и ждать единицы, ⇕ — порядок любой. На слово выходит 1 + 2 + 2 + 2 + 2 + 1 = 10 обращений, всего 10N на N слов. Облегчённый March X — ⇕(w0); ⇑(r0,w1); ⇓(r1,w0); ⇕(r0), 6N. «Простой» тест, который пишут чаще всего, — записать 0x55555555 во все слова, прочитать, затем то же с 0xAAAAAAAA, — 4N.

Какие неисправности что ловит, показывает модель march36c.py: память 32 слова по 32 бита, в неё вносится ровно одна неисправность, начальное содержимое случайное, тесты пословные — как код на C. Классы неисправностей — общепринятые модели памяти: залипание, переходная, ошибка дешифратора адреса и три вида связи между ячейками.

Неисправность Экземпляров March C- March X 0x55/0xAA
бит залип в 0 или 1 256 100 % 100 % 100 %
бит не переходит 0→1 или 1→0 256 100 % 100 % 76 %
два адреса выбирают одну ячейку 992 100 % 100 % 0 %
переход в одном слове переворачивает бит в другом 400 100 % 100 % 37 %
переход в одном слове ставит бит в другом в 0 или 1 800 100 % 57 % 19 %
пока бит в одном слове в состоянии s, бит в другом держится 800 100 % 80 % 68 %
связь двух битов одного слова 2000 60 % 60 % 49 %

На исправной памяти все три теста молчат. «Простой» тест не видит ошибку дешифратора вовсе: он пишет одно и то же значение во все слова, и неважно, в какую ячейку попала запись, — прочитается то же. Переходы он делает по разу и только в ту сторону, куда их задало начальное содержимое. March X пропускает 43 % связей «переход ставит бит»: у него на каждое направление обхода только один переход — вверх 0→1, вниз 1→0, — и часть сочетаний «какой переход, по какую сторону от жертвы, в какое значение» он не пробует. У March C- оба перехода есть в обе стороны.

Связь двух битов одного слова сплошной фон (все нули, все единицы) разглядеть не может: биты слова всегда пишутся одинаково. Модель с шестью фонами — 00000000, 55555555, 33333333, 0F0F0F0F, 00FF00FF, 0000FFFF — ловит её в 100 % случаев, но стоит в шесть раз дороже. Здесь выбран сплошной фон ради времени; если связь внутри слова важна, марш повторяют с фонами.


36б.3 March C- на C

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

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

volatile заставляет компилятор действительно читать и писать каждое слово: без него он вправе выкинуть запись, которую тут же перечитывает. Ошибки не проверяются ветвлением на каждом слове, а копятся в err по «или»: цикл короче, а в результате видно, какие биты отказали. Ноль — память исправна.

Файл selftest36c.c собран armclang (-Os -Wall -Wextra) для F103 и для G474 без предупреждений; _Static_assert сверяет смещения регистров CRC и таймера с RM0008 и RM0440. Цикл ⇑(r0,w1) в листинге F103:

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

Пять команд на слово: чтение, счётчик, накопление ошибки, запись с продвижением указателя, переход. По шести циклам марша выходит 3 + 5 + 5 + 5 + 5 + 4 = 27 команд на слово, у G474 листинг тот же. Каждая команда — хотя бы один такт, поэтому полный марш при старте:

Камень ОЗУ Слов Команд Не меньше
F103, 72 МГц 20 КБ 5120 138 240 1,9 мс
G474, 170 МГц 128 КБ 32 768 884 736 5,2 мс

Это нижняя граница: переход стоит больше такта, а такты ожидания флеш-памяти добавят ещё (73.2). Точное время даёт счётчик тактов на плате (73.1) — здесь не замерено. В QEMU March C- по 256 словам исправного ОЗУ вернул ноль.


36б.4 В работе: прозрачный тест по кускам

В работе ОЗУ занято, и стирать его нельзя. Прозрачный тест берёт кусок, сохраняет его, гоняет по нему марш и возвращает содержимое на место:

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

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

Три правила. Прерывания запрещены на всё время куска: обработчик, который прочитает переменную из куска посреди марша, получит ноль или 0xFFFFFFFF. PRIMASK восстанавливается, а не включается: если вызвали с уже запрещёнными прерываниями, они такими и останутся — в QEMU после вызова с запретом PRIMASK остался равен 1. Буфер saved[] и стек в проверяемую область не входят — их марш сотрёт вместе с адресом возврата.

Кусок — CHUNK_WORDS = 16 слов. По листингу на слово куска приходится 27 команд марша и 10 — на сохранение и возврат: 592 команды с запрещёнными прерываниями, не меньше 8,2 мкс на 72 МГц. На всё это время может опоздать ответ на прерывание — это цена прозрачного теста, и кусок выбирают по допустимой задержке.

Куски перекрываются на одно слово. Связь соседних слов на стыке двух кусков не видна ни одному из них: агрессор в одном, жертва в другом. Модель гоняет тот же порядок, что и ram_step(), по области в 64 слова:

Разбиение Начала кусков Связь соседних слов (756) Связь любых двух слов (300)
без перекрытия 0 16 32 48 95 % 24 %
перекрытие 1 слово 0 15 30 45 60 100 % 27 %
весь марш целиком — — 100 %

Содержимое после полного круга в модели прежнее. Тот же порядок кусков — 0, 15, 30, 45, 60 — дал код ram_step() в QEMU, и все 64 слова после круга совпали с копией. Последний столбец — предел прозрачного теста: связь между далёкими словами попадает в один кусок редко. Её ловит только полный марш при старте, поэтому его не заменяют проверкой в работе.

Полный круг по 20 КБ F103 — 342 вызова ram_step().


36б.5 Флеш: CRC образа с эталоном в конце

RM0008 так и описывает назначение блока CRC: сигнатура программы, посчитанная во время работы, сравнивается с эталоном, посчитанным при сборке и записанным в известное место. Место — сразу за образом. Его адрес даёт скрипт компоновки:

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

После сборки скрипт crc_image36c.py читает .bin, считает CRC-32/MPEG-2 так же, как блок CRC (слова младшим байтом вперёд, слово в расчёт — старшим битом вперёд, начало 0xFFFFFFFF, без отражений; 12.5, 43.1), и дописывает результат словом в конец. Заодно он сверяет длину файла с __image_end: если не совпала, собран не тот файл или не те границы.

При старте flash_check_all() прогоняет весь образ через блок и сравнивает с image[words]. В работе — по 256 слов за вызов:

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

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

Цикл подачи — 4 команды на слово; сам блок тратит на слово 4 такта шины AHB (RM0008, RM0440). Флеш-память F103 на 64 КБ — 64 шага, G474 на 512 КБ — 512 шагов.

В QEMU блока CRC нет, и прогон подставляет программный расчёт того же варианта. Образ из 379 слов прошёл проверку при старте и по шагам (два шага). CRC по образу вместе с эталонным словом дала 0x00000000 — свойство CRC без итоговой инверсии: остаток от «данные плюс их собственная CRC» равен нулю. Затем в константах образа перевернули один бит — и проверка при старте, и проверка по шагам ответили отказом.

У G474 флеш-память с ECC: по RM0440 одну ошибку в двойном слове она находит и исправляет сама, две — находит. CRC образа это не отменяет: недописанный или чужой образ записан с правильными кодами ECC, и ошибкой для неё не выглядит.


36б.6 Частота и реакция на отказ

CSS ловит только остановку HSE: по RM0440 при отказе HSE система уходит на HSI16, PLL от HSE выключается, приходит NMI. Кварц не той частоты или ошибка в настройке PLL для CSS — исправный такт. Поэтому частоту сравнивают с независимым генератором: таймер считает такты шины (от HSE через PLL) между фронтами LSE или LSI.

У G474 для этого удобен 32-битный TIM5 (даташит): по таблице 270 RM0440 его вход TI1 внутри камня подключается к LSI (TISEL = 1) и к LSE (TISEL = 2), провод не нужен. Захват — на каждом 8-м фронте:

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

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

Счёт за 8 периодов = 8 × f таймера / f опоры. Окно — все счёты, которые даёт исправный HSE при любом допустимом значении опоры, плюс единица на квантование. LSI у G474 по даташиту — 29,5…34 кГц во всём диапазоне температур, у F103 — 30…60 кГц. LSE — 32 768 Гц; RM0440 называет точность часового кварца «несколько десятков ppm», и окно ±1 % вокруг номинала — выбор этого раздела, с большим запасом. Прогон calc36c.py:

Отказ G474, LSE ±1 % G474, LSI F103, LSI
исправно: окно счёта 41091…41925 39999…46103 9599…19201
HSE пропал, работа от HSI 3906 — ловит 3765…4339 — ловит 1067…2133 — ловит
частота ×0,5 ловит ловит может пропустить
частота ×0,9 ловит может пропустить может пропустить
частота ×0,995 не ловит может пропустить может пропустить
частота ×1,02 ловит может пропустить может пропустить
частота ×1,5 ловит ловит может пропустить
частота ×4 ловит ловит может пропустить

«Может пропустить» — значит, при каком-то допустимом LSI счёт неисправного такта попадает в окно. Вывод простой: LSI ловит только грубые отказы. Окно по LSI у G474 — ±7,1 %, у F103 — ±33,3 %: для F103 это почти только пропажа HSE, которую и так видит CSS. Точная проверка частоты — по LSE.

У F103 внутреннего пути от LSI к таймеру нет на средней плотности (F103C8, F103CB): по RM0008 вход TIM5 CH4 подключается к LSI битом TIM5CH4_IREMAP только у камней высокой плотности, XL и connectivity line. На F103C8 опору заводят проводом — такт LSE, делённый на 64, через вывод TAMPER (6.8). Таймеры у F1 16-битные (13.1), и строка «×4» — не опечатка: об этом в 36б.7.

Что делать при отказе. Любая проверка, которая не сошлась, ведёт в одно место: выключить силовые выходы, записать причину туда, где она переживёт сброс (резервный регистр, неинициализируемая область ОЗУ), и перестать кормить сторож (129.7). Сторож от LSI (12.2) перезапустит камень, даже если сломался сам HSE. После сброса прибор читает флаги причины (12.4) и свою запись: если отказ повторяется, он остаётся в безопасном состоянии, а не перезапускается по кругу. Кормят сторож только в конце прохода главного цикла, где все шаги самопроверки вернули «исправно».


36б.7 Грабли

Марш стёр стек, на котором работает. Полный March C- при старте идёт до заполнения .data и .bss и не касается своего стека; в работе стек и буфер saved[] исключают из проверяемой области. Иначе марш сотрёт адрес возврата — и HardFault вместо отчёта.

Кусок проверили с открытыми прерываниями. Обработчик прочитал счётчик посреди марша и увидел 0xFFFFFFFF. Запрет прерываний — на весь кусок, от сохранения до возврата.

NMI приходит и при запрещённых прерываниях. PRIMASK блокирует только исключения с настраиваемым приоритетом (DUI0552A), NMI и HardFault проходят. Обработчик NMI — а туда приходят CSS и ошибка чётности G474 — не должен трогать проверяемую область.

Блоком CRC пользуется кто-то ещё. Между шагами flash_step() в блоке лежит промежуточный итог. Если протокол связи посчитает на нём свою сумму (а у G474 ещё и перенастроит полином, 43.1), проход кончится ложным отказом. Блок отдают самопроверке целиком, а протоколу — программный CRC.

Эталон посчитан не по тем границам. Скрипт получил .hex вместо .bin, или файл взяли от другой сборки. Поэтому crc_image36c.py сверяет длину с __image_end и отказывается, если длина не кратна четырём.

Частота «в норме» по LSI, а кварц не тот. Окно по LSI у F103 — ±33 %: оно пропустит и ×0,5, и ×1,5. По LSI ловят только пропажу HSE, точность проверяют по LSE.

16-битный счётчик завернулся. У F103 все таймеры 16-битные: исправному хватает (не больше 19201), но при частоте выше ×3,41 разность считается по модулю 65536. При ×4 счёт 38400…76800 превращается в 38400…65535 и 0…11264 — и нижний кусок попадает в окно. У G474 — 32-битный TIM5.

Чётность включили, а ОЗУ не обнулили. RM0440 советует записать всё ОЗУ в начале программы: чтение ячейки, куда ещё не писали, может дать ошибку чётности и NMI.


36б.8 Итоги

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

selftest36c.c — самопроверка во время работы: ОЗУ маршем March C-, CRC флеш-памяти, частота.

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

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

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

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

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

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

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

uint32_t ram_next_chunk(void) { return ram_pos; }

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

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

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

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

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

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

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

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

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

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

Дальше: раздел 36в «Помехоустойчивая прошивка: ловушки в свободной Flash, пустые векторы, контроль хода программы» — ловушки в свободной флеш-памяти, общий обработчик векторов и контроль хода главного цикла.

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