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 после компоновки:
Порядок при старте:
- Сторож — первым (раздел 12): если зависнет сама проверка, камень всё равно перезапустится.
- ОЗУ целиком — в
Reset_Handler, раньше любого кода на C (12б.2). - Регистры ядра и флаги (12б.6).
- Блок CRC, затем CRC всего образа против эталона (12б.4).
- Такт — HSE против LSE (12б.5).
- Заливка стека шаблоном и канарейка (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 слов:
- каждый из 4096 байт по очереди испорчен случайным значением — поймано 4096 из 4096;
- два случайных байта сразу, 20 000 раз — поймано 20 000;
- CRC по образу вместе с его эталоном —
0x00000000: так ведёт себя CRC без итоговой инверсии; - круг — 4 шага. Порча в уже пройденной части посреди круга: этот круг ответил 1 (сошлось), следующий — −1. Задержка обнаружения — до двух кругов;
- испорченная позиция
fpos_inv— ответ −2.
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 Итоги
- При старте: сторож, марш всего ОЗУ в
Reset_Handlerна ассемблере (10 обращений и 31 команда на слово; 20 КБ F103 — не меньше 2,2 мс, 128 КБ F407 — 6,0 мс), регистры ядра, блок CRC, CRC образа, такт. - В работе: прозрачный March C− на своих данных блоками по 16 слов с перекрытием в слово — 715 команд на блок, не меньше 9,9 мкс при запрещённых прерываниях на F103; круг 20 КБ — 342 прохода.
- На ПК марш поймал все залипания, переходы, ошибки адреса и связи слов; блоки ловят связь соседних слов целиком, далёких — около четверти, поэтому полный марш при старте обязателен.
- CRC образа: эталон — последнее слово,
patch12b.pyвписывает его в ELF; сам блок проверяется ответом0xDF8A8A2B; все одиночные и двойные битые байты пойманы; шаг 256 слов — не меньше 17,8 мкс; задержка — до двух кругов. - Такт F407: TIM5 CH4 от LSE через
TI4_RMP, окно ±0,5 % ловит уход на 1 %; окно по LSI (17…47 кГц) не гарантирует даже ×0,5 и ×2. - Регистры: шахматные шаблоны в R0–R12 и LR, PSP, флаги — 114 команд; залипший бит R5 пойман. Стек: канарейка — 27 команд за проход.
- Отказ: выходы выключить, записать в
.noinitсо словом проверки, сторожа не кормить; сторожа кормят только после прохода, где всё сошлось.
Полные исходники
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 «Таймеры» — самый большой блок периферии: счёт, ШИМ, захват сигнала, энкодеры и синхронизация нескольких таймеров.