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

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). Какой прошивкой — номер версии и начало отпечатка сборки: без него адреса из записи не с чем сверять.

Путь записи о сбое: отказ, обработчик HardFault пишет запись в область .noinit в конце ОЗУ, программный сброс SYSRESETREQ, стартовый код обнуляет только .bss, main проверяет признак и CRC и выдаёт запись по UART, на ПК addr2line переводит PC в функцию и строку; при снятии питания ОЗУ теряется и запись не проходит проверку

Раскладка записи — обычная структура, и её размер закреплён проверкой компилятора:

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³².

Как пережить питание. Два пути, оба с оговорками.

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

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

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

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