35в. Отказ в поле: слепок состояния, переживающий сброс, и разбор после перезагрузки
Раздел 35.2 научил читать HardFault, когда отладчик подключён: обработчик
кладёт PC, LR и CFSR в переменные и крутится в цикле, пока человек не
посмотрит. У прибора в поле отладчика нет. Там отказ выглядит так: прибор
перезагрузился, и всё. Сторож (раздел 12) вернёт его в работу, но причину
унесёт с собой — если её не сохранить туда, где она переживёт сброс.
Здесь — полный путь такого сохранения. Обработчик отказа пишет запись в
88 байт в область ОЗУ, которую стартовый код не трогает, и перезапускает
камень. Новая прошивка находит запись, проверяет её по CRC и выдаёт по UART.
На ПК скрипт переводит PC и LR из записи в имя функции и номер строки
через arm-none-eabi-addr2line. Всё показано прогоном в QEMU: два
намеренных отказа подряд, два перезапуска, разбор — и сверка строк с
исходником.
Опоры: справочные руководства RM0090 (F407) и RM0008 (F103) — виды сброса,
резервный домен, флеш-память; core_cm4.h из CMSIS — адреса регистров
отказа; SVD-файлы паков Keil — адреса RCC и USART1. Опыт — в образе
stm-arm:1: arm-none-eabi-gcc 12.2.1, QEMU 7.2.22 (машины netduinoplus2
для F4 и stm32vldiscovery для F1), addr2line 2.40, gdb-multiarch. Тот
же код собирается armclang для обоих камней. Кадр стека и EXC_RETURN
разобраны в 7а, биты CFSR и HFSR — в 35.2, скрипт компоновщика — в 4в,
флаги причины сброса — в 12.4; здесь они только используются.
Как это называется в документации
Оригинал Перевод crash record / crash dump запись о сбое / слепок состояния .noinit,NOLOADсекция без начальных значений, которую стартовый код не обнуляет system reset системный сброс: вывод NRST, сторожа, программный power reset (POR/PDR, BOR) сброс по питанию SYSRESETREQ(AIRCR)бит программного сброса в регистре ядра reset flags ( RCC_CSR)флаги причины сброса GNU build ID отпечаток сборки: хеш в заметке ELF backup SRAM / backup registers резервная SRAM (F407) / резервные регистры (F103) addr2lineперевод адреса в функцию и строку по отладочной информации
35в.1 Что сохранить: запись в 88 байт
Отказ случается один раз, и повторить его на столе часто не удаётся. Поэтому
запись должна сразу отвечать на три вопроса: где сломалось, почему и
какой прошивкой. Где — это кадр исключения: восемь слов, которые ядро
уложило в стек (7а.1), среди них PC отказавшей команды и LR — откуда её
позвали. Почему — регистры CFSR, HFSR, MMFAR, BFAR (35.2). Какой
прошивкой — номер версии и начало отпечатка сборки: без него адреса из записи
не с чем сверять.
Раскладка записи — обычная структура, и её размер закреплён проверкой компилятора:
typedef struct {
uint32_t magic; /* признак записи */
uint16_t format; /* CRASH_FORMAT */
uint16_t size; /* sizeof(crash_rec_t) */
uint32_t count; /* сбоев подряд с последнего чистого старта */
uint32_t fw_version; /* 0x00MMmmpp */
uint8_t build_id[8]; /* начало GNU build-id: какой ELF брать */
uint32_t reset_csr; /* RCC->CSR при следующем старте */
uint32_t exc_return; /* LR на входе в обработчик */
uint32_t sp; /* где лежал кадр */
uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr; /* кадр исключения */
uint32_t cfsr, hfsr, mmfar, bfar; /* почему */
uint32_t crc; /* CRC-32 всех байтов до этого поля */
} crash_rec_t;
_Static_assert(sizeof(crash_rec_t) == 88, "record layout changed: bump CRASH_FORMAT");
_Static_assert(offsetof(crash_rec_t, crc) == sizeof(crash_rec_t) - 4, "crc must be last");
Три поля служебные. magic — признак «здесь запись»: 0x48535243, в памяти
это байты C R S H. format и size — номер раскладки и её размер: новая
прошивка с другой структурой не примет старую запись за свою. crc — CRC-32
(тот же, что у zlib, полином 0xEDB88320) по первым 84 байтам. Признак
отличает запись от пустоты, CRC — от мусора и от записи, оборванной на
середине.
count считает отказы подряд: если при новом отказе старая запись цела,
счётчик растёт. Прибор, который падает каждые пять минут, и прибор, упавший
раз за год, дают разные записи. reset_csr заполняется не в обработчике, а
уже после перезапуска: флаги причины сброса появляются только тогда.
35в.2 Секция .noinit: своя область в конце ОЗУ
Стартовый код из 4в делает две вещи до main: копирует .data из флеш-памяти
и обнуляет .bss. Всё, что лежит вне этих двух секций, после сброса остаётся
как было. Раздел 7в показал это на счётчике загрузок; раздел 48 — на признаке
перепрошивки, и там же выяснилось главное условие: адрес не должен зависеть от
того, сколько переменных в программе. Поэтому запись получает свою область в
конце ОЗУ, а RAM укорачивается на тот же размер:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K - 128
NOINIT (rw) : ORIGIN = 0x2001FF80, LENGTH = 128
}
В скрипт компоновщика из 4в добавлены два правила: отпечаток сборки во
флеш-памяти с меткой __build_id и сама секция:
.noinit (NOLOAD) : /* стартовый код её не обнуляет */
{
*(.noinit .noinit.*)
} > NOINIT
Вершина стека считается от укороченной RAM — _estack = ORIGIN(RAM) +
LENGTH(RAM), — так что стек растёт вниз от начала области и запись не
затирает. Переменная объявляется с атрибутом:
crash_rec_t crash_rec __attribute__((section(".noinit")));.
Сборка подтверждает раскладку. arm-none-eabi-nm для F4 показывает
crash_rec и _estack по одному адресу 2001ff80, размер .noinit —
88 байт. Слово NOLOAD видно в заголовках программы: у этого сегмента
0 байт в файле и 0x00058 (88) в памяти. Значит, ни загрузчик, ни отладчик
при прошивке в эту область ничего не пишут. Для F1 в QEMU (8 КБ ОЗУ) область —
0x20001F80; у STM32F103C8 с 20 КБ ОЗУ это было бы 0x20004F80.
35в.3 Обработчик: кадр, регистры отказа, программный сброс
Первая половина обработчика — та же, что в 35.2: бит 2 EXC_RETURN выбирает
стек, где лежит кадр. Добавлено одно: LR на входе передаётся вторым
аргументом, чтобы попасть в запись.
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile(
"tst lr, #4 \n" /* бит 2 EXC_RETURN: какой стек */
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"mov r1, lr \n" /* EXC_RETURN — вторым аргументом */
"b crash_save \n");
}
Вторая половина вместо бесконечного цикла пишет запись и перезапускает камень:
__attribute__((used, noreturn)) void crash_save(const uint32_t *frame, uint32_t exc_return)
{
uint32_t n = crash_valid() ? crash_rec.count + 1u : 1u;
crash_rec.magic = CRASH_MAGIC;
crash_rec.format = CRASH_FORMAT;
crash_rec.size = sizeof(crash_rec_t);
crash_rec.count = n;
crash_rec.fw_version = FW_VERSION;
for (int i = 0; i < 8; i++)
crash_rec.build_id[i] = __build_id[16 + i]; /* 16 байт — заголовок заметки */
crash_rec.reset_csr = CSR_NOT_READ;
crash_rec.exc_return = exc_return;
crash_rec.sp = (uint32_t)frame;
crash_rec.r0 = frame[0];
crash_rec.r1 = frame[1];
crash_rec.r2 = frame[2];
crash_rec.r3 = frame[3];
crash_rec.r12 = frame[4];
crash_rec.lr = frame[5];
crash_rec.pc = frame[6];
crash_rec.xpsr = frame[7];
crash_rec.cfsr = SCB_CFSR;
crash_rec.hfsr = SCB_HFSR;
crash_rec.mmfar = SCB_MMFAR;
crash_rec.bfar = SCB_BFAR;
crash_reseal(); /* CRC — последним: оборванная запись не пройдёт */
__asm volatile("dsb" ::: "memory");
SCB_AIRCR = AIRCR_RESET; /* программный сброс: ОЗУ не трогается */
__asm volatile("dsb" ::: "memory");
for (;;) {
}
}
Порядок важен. CRC считается последним: если что-то прервёт запись на
середине, проверка её отбросит. dsb перед сбросом дожидается, пока все
записи в ОЗУ дойдут до памяти. AIRCR_RESET — ключ 0x5FA в битах 31:16 и
бит 2 SYSRESETREQ; позиции обоих — из core_cm4.h, так же сбрасывает
NVIC_SystemReset().
Отпечаток сборки — заметка ELF, которую компоновщик создаёт по ключу
-Wl,--build-id=sha1: 16 байт заголовка (длины, тип 3, строка GNU) и
20 байт хеша SHA-1. В прогоне заметка лежит по 0x08000040, её размер —
36 байт; запись берёт первые 8 байт хеша. Хеш считается по содержимому
сборки, поэтому прошивки F4 и F1 из одних исходников получили разные
отпечатки: 4aba8274… и 6d8076eb….
Обработчик ничего не выводит и никого не ждёт: в момент отказа неизвестно, цел ли стек, настроен ли UART и не он ли сломался. Вывод — забота следующего запуска.
35в.4 После перезапуска: проверка, причина сброса, выдача
В начале main прошивка проверяет запись — признак, формат, размер и CRC — и,
если она цела, дописывает причину сброса и выдаёт её:
puts1("boot\n");
if (crash_valid()) {
if (crash_rec.reset_csr == CSR_NOT_READ) {
crash_rec.reset_csr = RCC_CSR; /* причина сброса после сбоя */
crash_reseal();
}
crash_report();
} else {
puts1("no crash record\n");
}
RCC_CSR |= RCC_CSR_RMVF;
reset_csr пишется ровно один раз — при первом старте после отказа. Если
прибор потом перезагрузят кнопкой, запись сохранит ту причину, которая
относится к отказу. Флаги стираются битом RMVF, иначе они копятся (12.4).
Опытная прошивка устроена так, чтобы за один прогон пройти всё. Первый запуск
не находит записи и делит сумму отсчётов на их число, которое ещё ноль; бит
DIV_0_TRP в SCB->CCR включён, и деление становится отказом. Второй запуск
находит запись № 1, выдаёт её и читает «датчик» по указателю 0x60000000, где
в QEMU ничего нет. Третий запуск выдаёт запись № 2 и ждёт команд по UART:
d — выдать запись ещё раз, c — стереть. Вывод F4 целиком:
boot
no crash record
boot
CRASH #1 pc=0x080004de lr=0x080004fb cfsr=0x02000000 hfsr=0x40000000 bfar=0x00000000 csr=0x00000000
REC 435253480100580001000000020401004aba8274789c0f3200000000f9ffffff40ff0120000000000800002008000020e803000000000000fb040008de0400080000004100000002000000400000000000000000d2e8e30b
boot
CRASH #2 pc=0x08000510 lr=0x08000525 cfsr=0x00008200 hfsr=0x40000000 bfar=0x60000000 csr=0x00000000
REC 435253480100580002000000020401004aba8274789c0f3200000000f9ffffff40ff012000000060000000000800002000000020d4ff012025050008100500080000006100820000000000400000000000000060dc0be734
ready: d - dump, c - clear
record cleared
no crash record
Строка CRASH — для человека у терминала, строка REC — все 88 байт записи
шестнадцатерично, для скрипта. Три слова boot — три запуска: запись
пережила два программных сброса подряд, и счётчик дошёл до 2. После команды
c признак обнулён, и d честно отвечает no crash record. F1 в QEMU дал ту
же картину со своими адресами: pc=0x0800053e и pc=0x08000570.
Поле csr в прогоне — нули: QEMU флаги причины сброса не ведёт. На плате
после SYSRESETREQ здесь будут SFTRSTF и PINRSTF (таблица 12.4), после
сторожа — IWDGRSTF или WWDGRSTF.
В изделии запись выдают не только при старте. Типовая схема — команда d по
тому же каналу, что и настройки: сервисная программа забирает запись,
убеждается, что CRC сошёлся, и только потом посылает c. Пока запись не
забрали, она ждёт в ОЗУ.
35в.5 Разбор на ПК: addr2line и gdb по ELF
decode35d.py берёт строки REC из вывода UART и делает четыре шага:
проверяет CRC тем же алгоритмом (zlib.crc32), сверяет отпечаток записи с
отпечатком ELF (readelf -n), раскладывает CFSR и HFSR по битам и
переводит адреса в строки через arm-none-eabi-addr2line. Разбор записей
F4:
record #1: 88 bytes, crc ok, fw 1.4.2, build-id 4aba8274789c0f32 = ELF
CFSR 0x02000000: DIVBYZERO HFSR 0x40000000: FORCED
EXC_RETURN 0xfffffff9: frame on MSP, r0=0x00000000 r1=0x20000008
PC 0x080004de -> average at app35d.c:58
LR 0x080004fb -> call at 0x080004f8: scenario_div at app35d.c:70
record #2: 88 bytes, crc ok, fw 1.4.2, build-id 4aba8274789c0f32 = ELF
CFSR 0x00008200: PRECISERR BFARVALID HFSR 0x40000000: FORCED
BFAR 0x60000000: address of the bad access
EXC_RETURN 0xfffffff9: frame on MSP, r0=0x60000000 r1=0x00000000
PC 0x08000510 -> read_sensor at app35d.c:65
LR 0x08000525 -> call at 0x08000522: scenario_bus at app35d.c:76
Проверка сверяет найденные строки с исходником: строка 58 app35d.c — это
int32_t avg = sample_sum / count;, строка 65 — return *reg + 1u;, строки
70 и 76 — вызовы average и read_sensor. Дизассемблер подтверждает: по
0x080004de лежит sdiv r0, r3, r0, по 0x08000510 — ldr r0, [r0, #0].
Операнды деления видны прямо в кадре: r0 — делитель, ноль; r3 — делимое,
0x3e8, то есть 1000. У второй записи r0 — сам плохой указатель, и он же в
BFAR.
С LR есть тонкость. В кадре лежит адрес возврата — следующей команды
после вызова, с единицей в младшем бите (признак Thumb). Эта команда может
относиться уже к следующей строке. Поэтому скрипт снимает бит Thumb и
отступает ещё на 2 байта: 0x080004fb → 0x080004f8, середина команды
bl 80004d8 <average>, которая начинается по 0x080004f6. Так находится
именно строка вызова.
То же руками — одной командой, без скрипта:
average at /tmp/c35d/app35d.c:58
scenario_div at /tmp/c35d/app35d.c:70
Это вывод arm-none-eabi-addr2line -f -p -e fw35d_f4.elf 0x080004de 0x080004f8.
Отладчик по тому же ELF отвечает так же — gdb-multiarch -batch -ex "info line *0x080004de":
Line 58 of "app35d.c" starts at address 0x80004de <average+6> and ends at 0x80004e2 <average+10>.
Плата для этого не нужна: только ELF той самой сборки и 88 байт записи. ELF каждой выпущенной версии поэтому хранят рядом с двоичным образом — по отпечатку его потом и находят.
35в.6 Что переживает запись, а что нет
Системный сброс. RM0090 (7.1.1) и RM0008 делят сбросы на системный и по
питанию. Системный — низкий уровень на NRST, оба сторожа, программный сброс
через SYSRESETREQ, сброс при входе в Stop или Standby по байтам
конфигурации. Он возвращает к исходным значениям все регистры, кроме флагов
в RCC_CSR и резервного домена. ОЗУ в этом перечне нет: его содержимое
портит только стартовый код, а .noinit он обходит. Опыт показал это для
программного сброса; сторож приводит к сбросу того же вида.
Сторож без отказа. Если программа зависла, а не упала, обработчик отказа не
вызывается, и писать запись некому. Сторож перезапустит камень, и новая
прошивка увидит только IWDGRSTF. Чтобы узнать, где висели, запись пишут
заранее: из прерывания раннего предупреждения WWDG (12.3) — там есть кадр с
PC зависшего кода — или периодически кладут в ту же область «последнее, что
делала программа».
Снятие питания. При сбросе по питанию ОЗУ не сохраняется: RM0090 для Standby прямо пишет, что содержимое SRAM и регистров теряется, кроме резервного домена. После включения в области лежат случайные байты; признак и CRC-32 их отсеют — случайно сойтись CRC может с вероятностью 1 к 2³².
Как пережить питание. Два пути, оба с оговорками.
- Резервная SRAM у F407. 4 КБ в резервном домене по адресу
0x40024000; содержимое хранится и в Standby, и от батарейки на выводе VBAT, если включён резервный регулятор (RM0090, 5.1.2; раздел 10.5). Без батарейки она теряется вместе с питанием, как обычное ОЗУ. У F103 резервного ОЗУ нет, а резервных регистров у STM32F103C8 (средняя плотность) — 20 байт: запись в 88 байт туда не влезет. - Страница флеш-памяти. У F103C8 страница — 1 КБ, у F407 сектора — 16, 64 и 128 КБ (RM0008, RM0090). Писать туда прямо в обработчике — плохая мысль. RM0090 предупреждает: пока идёт запись или стирание, любое чтение флеш-памяти останавливает шину — код из неё не выполняется. И ещё: если сброс придёт во время записи или стирания, содержимое флеш-памяти не гарантируется. В обработчике отказа неизвестно, в каком состоянии флеш-контроллер, и сторож может сработать посреди стирания.
Рабочая схема — в два шага. Обработчик пишет запись в ОЗУ и сбрасывает камень, как в опыте. Уже здоровая прошивка после старта находит запись и копирует её во флеш-память журналом из 11.8 — в заранее стёртую область, в обычном порядке и с проверкой. Питание, снятое после этого, запись уже не сотрёт.
35в.7 Грабли
Запись после перезапуска пропала или стала мусором. Секция попала под
обнуление: в скрипте компоновщика нет правила для .noinit, она оказалась
внутри .bss, или стартовый код обнуляет всё ОЗУ целиком. Проверка — в
заголовках программы у сегмента записи должно быть 0 байт в файле и адрес
области NOINIT.
Запись испорчена, хотя всё собрано верно. Стек дорос до области: вершина
стека стоит выше записи. _estack считают от укороченной RAM, как в
35в.2, а не от конца настоящего ОЗУ. Загрузчик со своим скриптом (раздел 48)
должен знать про ту же область.
Мусор при включении принят за отказ. Проверка только по признаку ловит случайное совпадение одного слова; на ОЗУ после включения этого мало. Признак, формат, размер и CRC-32 вместе такую ошибку практически исключают.
Разбор по чужому ELF даёт правдоподобную чушь. Запись F4, разобранная по
ELF от F1, указала на putc1 at app35d.c:10 и putc1 at app35d.c:8 —
существующие функция и строки, только не те. Скрипт это поймал по отпечатку:
build-id 4aba8274789c0f32 != ELF 6d8076ebcf50f931. Без отпечатка ошибка
незаметна. То же бывает после перепрошивки отладчиком: он в .noinit не
пишет, и старая запись переживает смену прошивки.
addr2line и gdb называют разные строки для одного адреса. В функции
return a / b; при -O2 команда деления стоит по адресу 0, и в таблице строк
на этом адресе три записи: строки 6 ({) и 7 с пометкой «начало
оператора» и строка 8 (}) без неё. addr2line берёт последнюю и
отвечает quirk35d.c:8, gdb выбирает начало оператора и отвечает Line 7.
Если строка указывает на }, смотрите на строку выше.
Строка вызова по LR — не та. В LR — адрес возврата с битом Thumb.
Без поправки на -1 и ещё -2 байта строка может оказаться следующей
после вызова (35в.5).
Причина сброса всегда одна и та же. Флаги RCC_CSR копятся до RMVF
или сброса по питанию (12.4); запись берёт их один раз и стирает. В QEMU поле
будет нулём при любом сбросе: эмулятор их не ведёт.
Запись о сбое во флеш прямо из обработчика. Стирание сектора останавливает выполнение кода из флеш-памяти, а сброс посреди стирания оставляет её в неизвестном состоянии. Писать во флеш — после перезапуска, из здоровой прошивки.
35в.8 Итоги
- Запись о сбое — 88 байт: кадр исключения (8 слов),
CFSR,HFSR,MMFAR,BFAR,EXC_RETURN, счётчик, версия, 8 байт отпечатка сборки, причина сброса и CRC-32 последним полем; размер закреплён_Static_assert. - Область
NOINIT— 128 байт в конце ОЗУ (0x2001FF80у F407), секцияNOLOAD: 0 байт в образе, стартовый код её не обнуляет, стек ниже неё. - Обработчик пишет запись, CRC — последним, и сбрасывает камень через
SYSRESETREQ(ключ0x5FA); ничего не выводит. - В опыте запись пережила два программных сброса подряд: деление на ноль в
average(строка 58,DIVBYZERO) и чтение по0x60000000вread_sensor(строка 65,PRECISERR,BFAR=0x60000000) — на F4 и F1. - На ПК
addr2lineпо ELF переводитPCв строку отказа,LR − 1 − 2— в строку вызова; отпечаток сборки ловит чужой ELF. - Системный сброс (кнопка, сторожа, программный) ОЗУ не трогает, снятие питания — стирает. Пережить питание — копией во флеш из здоровой прошивки или резервной SRAM F407 (4 КБ, от VBAT).
Полные исходники
crash35d.h — запись о сбое.
/* Раздел 35в: запись о сбое, которая переживает сброс. */
#ifndef CRASH35D_H
#define CRASH35D_H
#include <stddef.h>
#include <stdint.h>
#define CRASH_MAGIC 0x48535243u /* байты C R S H в памяти */
#define CRASH_FORMAT 1u /* версия раскладки записи */
#define CSR_NOT_READ 0xFFFFFFFFu /* причина сброса ещё не прочитана */
typedef struct {
uint32_t magic; /* признак записи */
uint16_t format; /* CRASH_FORMAT */
uint16_t size; /* sizeof(crash_rec_t) */
uint32_t count; /* сбоев подряд с последнего чистого старта */
uint32_t fw_version; /* 0x00MMmmpp */
uint8_t build_id[8]; /* начало GNU build-id: какой ELF брать */
uint32_t reset_csr; /* RCC->CSR при следующем старте */
uint32_t exc_return; /* LR на входе в обработчик */
uint32_t sp; /* где лежал кадр */
uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr; /* кадр исключения */
uint32_t cfsr, hfsr, mmfar, bfar; /* почему */
uint32_t crc; /* CRC-32 всех байтов до этого поля */
} crash_rec_t;
_Static_assert(sizeof(crash_rec_t) == 88, "record layout changed: bump CRASH_FORMAT");
_Static_assert(offsetof(crash_rec_t, crc) == sizeof(crash_rec_t) - 4, "crc must be last");
extern crash_rec_t crash_rec;
uint32_t crc32(const void *p, size_t n);
int crash_valid(void);
void crash_reseal(void);
#endif
crash35d.c — обработчик отказа: запись в .noinit и перезапуск.
/* Раздел 35в: обработчик отказа пишет запись в .noinit и перезапускает камень. */
#include "crash35d.h"
#include "regs35d.h"
#define FW_VERSION 0x00010402u /* 1.4.2 */
crash_rec_t crash_rec __attribute__((section(".noinit")));
extern const uint8_t __build_id[]; /* из скрипта компоновщика */
uint32_t crc32(const void *p, size_t n) /* CRC-32 как у zlib: полином 0xEDB88320 */
{
const uint8_t *b = p;
uint32_t c = 0xFFFFFFFFu;
while (n--) {
c ^= *b++;
for (int k = 0; k < 8; k++)
c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u)));
}
return ~c;
}
int crash_valid(void)
{
return crash_rec.magic == CRASH_MAGIC && crash_rec.format == CRASH_FORMAT
&& crash_rec.size == sizeof(crash_rec_t)
&& crash_rec.crc == crc32(&crash_rec, offsetof(crash_rec_t, crc));
}
void crash_reseal(void) /* после правки поля — пересчитать CRC */
{
crash_rec.crc = crc32(&crash_rec, offsetof(crash_rec_t, crc));
}
__attribute__((used, noreturn)) void crash_save(const uint32_t *frame, uint32_t exc_return)
{
uint32_t n = crash_valid() ? crash_rec.count + 1u : 1u;
crash_rec.magic = CRASH_MAGIC;
crash_rec.format = CRASH_FORMAT;
crash_rec.size = sizeof(crash_rec_t);
crash_rec.count = n;
crash_rec.fw_version = FW_VERSION;
for (int i = 0; i < 8; i++)
crash_rec.build_id[i] = __build_id[16 + i]; /* 16 байт — заголовок заметки */
crash_rec.reset_csr = CSR_NOT_READ;
crash_rec.exc_return = exc_return;
crash_rec.sp = (uint32_t)frame;
crash_rec.r0 = frame[0];
crash_rec.r1 = frame[1];
crash_rec.r2 = frame[2];
crash_rec.r3 = frame[3];
crash_rec.r12 = frame[4];
crash_rec.lr = frame[5];
crash_rec.pc = frame[6];
crash_rec.xpsr = frame[7];
crash_rec.cfsr = SCB_CFSR;
crash_rec.hfsr = SCB_HFSR;
crash_rec.mmfar = SCB_MMFAR;
crash_rec.bfar = SCB_BFAR;
crash_reseal(); /* CRC — последним: оборванная запись не пройдёт */
__asm volatile("dsb" ::: "memory");
SCB_AIRCR = AIRCR_RESET; /* программный сброс: ОЗУ не трогается */
__asm volatile("dsb" ::: "memory");
for (;;) {
}
}
__attribute__((naked)) void HardFault_Handler(void)
{
__asm volatile(
"tst lr, #4 \n" /* бит 2 EXC_RETURN: какой стек */
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"mov r1, lr \n" /* EXC_RETURN — вторым аргументом */
"b crash_save \n");
}
app35d.c — прошивка-опыт: деление на ноль, неверный адрес, выдача записи.
/* Раздел 35в: прошивка-опыт. Первый запуск — деление на ноль, второй — чтение по
* неверному адресу, третий — ждёт команд: d — выдать запись, c — стереть. */
#include "crash35d.h"
#include "regs35d.h"
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 puthex(uint32_t v, int digits)
{
static const char hex[] = "0123456789abcdef";
for (int i = (digits - 1) * 4; i >= 0; i -= 4)
putc1(hex[(v >> i) & 0xFu]);
}
static void put32(const char *name, uint32_t v)
{
puts1(name);
puts1("=0x");
puthex(v, 8);
}
static void crash_report(void)
{
const uint8_t *b = (const uint8_t *)&crash_rec;
puts1("CRASH #");
puthex(crash_rec.count, 1);
put32(" pc", crash_rec.pc);
put32(" lr", crash_rec.lr);
put32(" cfsr", crash_rec.cfsr);
put32(" hfsr", crash_rec.hfsr);
put32(" bfar", crash_rec.bfar);
put32(" csr", crash_rec.reset_csr);
puts1("\nREC ");
for (unsigned i = 0; i < sizeof(crash_rec_t); i++)
puthex(b[i], 2);
puts1("\n");
}
volatile int32_t sample_sum = 1000; /* сумма отсчётов */
volatile int32_t sample_count = 0; /* ошибка: ещё ни одного отсчёта */
volatile uint32_t *volatile sensor_reg = (volatile uint32_t *)0x60000000u;
volatile int32_t last_average;
volatile uint32_t sink;
__attribute__((noinline)) int32_t average(int32_t count)
{
int32_t avg = sample_sum / count; /* СБОЙ-1 */
last_average = avg;
return avg;
}
__attribute__((noinline)) uint32_t read_sensor(volatile uint32_t *reg)
{
return *reg + 1u; /* СБОЙ-2 */
}
__attribute__((noinline)) static void scenario_div(void)
{
sink = (uint32_t)average(sample_count); /* ВЫЗОВ-1 */
puts1("never\n");
}
__attribute__((noinline)) static void scenario_bus(void)
{
sink = read_sensor(sensor_reg); /* ВЫЗОВ-2 */
puts1("never\n");
}
int main(void)
{
RCC_APB2ENR |= USART1_EN;
USART1_BRR = BRR_115200;
USART1_CR1 = (1u << 13) | (1u << 3) | (1u << 2); /* UE, TE, RE */
SCB_CCR |= CCR_DIV_0_TRP; /* деление на ноль — отказ, а не тихий ноль */
puts1("boot\n");
if (crash_valid()) {
if (crash_rec.reset_csr == CSR_NOT_READ) {
crash_rec.reset_csr = RCC_CSR; /* причина сброса после сбоя */
crash_reseal();
}
crash_report();
} else {
puts1("no crash record\n");
}
RCC_CSR |= RCC_CSR_RMVF;
uint32_t n = crash_valid() ? crash_rec.count : 0u;
if (n == 0u)
scenario_div();
if (n == 1u)
scenario_bus();
puts1("ready: d - dump, c - clear\n");
for (;;) {
while (!(USART1_SR & (1u << 5))) {
}
char c = (char)USART1_DR;
if (c == 'd') {
if (crash_valid())
crash_report();
else
puts1("no crash record\n");
} else if (c == 'c') {
crash_rec.magic = 0u;
puts1("record cleared\n");
}
}
}
mem_f4.ld — память F407 с областью под запись о сбое.
/* STM32F407: 1 МБ флеш-памяти, 128 КБ ОЗУ; последние 128 байт ОЗУ — под запись о сбое. */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K - 128
NOINIT (rw) : ORIGIN = 0x2001FF80, LENGTH = 128
}
Дальше: раздел 35г «Журнал на приборе: уровни, кольцевой буфер и двоичные записи вместо printf» — журнал, в который можно писать из прерывания: в прибор уходит номер сообщения, а строка формата остаётся на компьютере.