1. Случай 1
При использовании Keil для моделирования программы STM32 программа иногда убегает.Остановите программу моделирования и остановитесь в бесконечном цикле while (1) в функции HardFault_Handler. Это показывает, что STM32 имеет аппаратную ошибку.
1.1 Аппаратные ошибки STM32 могут иметь следующие причины:
- Операция с массивом вне границ;
- Переполнение памяти, доступ за пределы;
- Переполнение стека, сбой программы;
- Ошибка обработки прерывания;
- Переполнение памяти или доступ за пределы. Когда вам нужно написать свою собственную программу, вам нужно стандартизировать код.Если вы столкнетесь с ней, вам нужно будет медленно ее проверить.
- Переполнение стека. Увеличьте размер стопки.
1.2 Как устранить неполадки при возникновении проблем:
1. После возникновения исключения вы можете сначала проверить значение в регистре LR, чтобы определить, является ли текущий используемый стек MSP или PSP, затем найти указатель соответствующего стека и просмотреть содержимое соответствующего стека в памяти. Когда возникает исключение, ядро по очереди помещает в стек регистры R0
R3, R12, адрес возврата, PSR и LR, где адрес возврата — это адрес следующей инструкции, которая должна быть выполнена ПК до возникновения исключения, поэтому третья инструкция засчитывается в стеке обратно. Слово — это место ошибки.
2. Метод обработки HardFault_Handler по умолчанию — B. Измените его на форму прямого возврата BX LR. Затем нажмите точку останова в этом операторе, как только вы остановитесь на точке останова, это означает, что что-то пошло не так, а затем вернувшись, вы можете вернуться к следующему оператору, в котором произошла ошибка.
Иногда может потребоваться отладка в режиме дизассемблирования, поскольку HardFault_Handler может появиться после того, как программа на некоторое время завершит работу.
3. Измените функцию прерывания и распечатайте некоторую информацию при прерывании:
CPSR Текущий государственный регистр программы (Current Program State Register)
SPSR Имеется шесть регистров сохраненного состояния программы (Saved Program State Register), которые в основном используются при обработке исключений.
В каждом режиме процессора имеется специальный физический регистр в качестве регистра состояния программы резервного копирования SPSR. Когда возникает определенное исключение, этот физический регистр отвечает за сохранение содержимого регистра текущего состояния программы CPSR. Когда обработчик исключения возвращается, тогда Восстановите содержимое на устройстве текущего состояния программы и продолжите выполнение исходной программы.
PC Программный счетчик используется для подсчета, указывая место хранения инструкции в памяти, которая является адресной информацией.
1.3 Есть две основные причины ошибки HardFault_Handler в STM32:
Как устранить неполадки при возникновении проблем:
После возникновения исключения вы можете сначала проверить значение в регистре LR, чтобы определить, является ли текущий используемый стек MSP или PSP, затем найти указатель соответствующего стека и просмотреть содержимое соответствующего стека в памяти. Когда возникает исключение, ядро последовательно помещает регистры R0
R3, R12, LR, PC и XPRS в стек, где LR — адрес следующей инструкции, которую ПК выполнит до возникновения исключения.
Примечание: все регистры 32-битные, а STM32 находится в режиме прямого порядка байтов. (Ссылка на Cortex-M3)
Значение SP — 0x20008560, а значения в стеке — R0
R3, R12, LR, PC, XPRS, например R0 (10 27 00 00). Очевидно, что байты с 21-го по 24-й после стека являются LR. Адрес 0x08001FFD — это адрес следующей инструкции, которую ПК выполнит перед исключением (то есть RCC-> CR & = (uint32_t) 0xFFFBFFFF в инструкции, следующей за StackFlow ())
Значение SP равно 0x20001198, а значения в стеке — R0
R3, R12, LR, PC, XPRS, например R0 (10 27 00 00). Очевидно, что байты с 21-го по 24-й после стека являются LR. Адрес 0x08003B61 — это адрес следующей инструкции, которую ПК выполнит перед исключением (то есть RCC-> CR & = (uint32_t) 0xFFFBFFFF в инструкции, следующей за StackFlow ())



2. Случай 2
После возникновения исключения мы можем сначала проверить значение регистра LR, подтвердить, является ли текущий используемый стек MSP или PSP, затем найти соответствующий указатель стека и просмотреть содержимое соответствующего стека в памяти, ядро будет R0
R3, R12, LR , Регистры PC и XPRS по очереди помещаются в стек, где LR — адрес следующей инструкции, которую ПК выполнит до возникновения исключения.
Тогда методы отладки и позиционирования ошибок HardFault ядра Cortex-M3:
2.1 Метод 1 Как точно определить местоположение кода проблемы:
В качестве примера возьмем трансграничный доступ: (имитируйте EEPROM для внутренней флэш-памяти STM32F103C8T6)
#define STM32_FLASH_SIZE 64
#define STM32_FLASH_WREN 1
#define FLASH_SAVE_ADDR 0X08078000
#define FLASH_HIS_ADDR 0X08078002
FLASH_SAVE_ADDR — это базовый адрес для начала хранения. Размер внутренней флэш-памяти STM32F103C8T6 составляет 64 КБ. Адрес внутренней флэш-памяти (FLASH) STM32 начинается с 0x08000000. Как правило, программа начинает запись с этого адреса. Следовательно, конечный адрес STM32F103C8T6 должен быть 64 * 1024, преобразованный в шестнадцатеричный, а результат, полученный путем добавления базового адреса флэш-памяти микроконтроллера, равен 0x08010000, тогда указанная выше установка кода FLASH_SAVE_ADDR на 0X08078000 выходит за рамки микроконтроллера, поэтому, если вы используете это Когда микроконтроллер управляет флеш-памятью, произойдет ошибка, если адрес будет записан или прочитан. Теперь предположим, что вы работаете с этим адресом без вашего ведома, а затем вводите его, когда программа работает.
HardFault_Handler прерван. Затем, чтобы узнать, где находится код ошибки, вы можете использовать следующий метод (отладка программного обеспечения MDK):
1: войдите в интерфейс отладки и поместите точку останова в while (1) HardFault_Handler.
3: Подождите, пока код дойдет до этого, затем проверьте регистр LR
Если он работает нормально, отображаемый регистр похож на следующий рисунок:
После возникновения исключения вы можете сначала проверить значение в регистре LR, чтобы определить, является ли текущий используемый стек MSP или PSP, затем найти указатель соответствующего стека и просмотреть содержимое соответствующего стека в памяти. В авторитетном руководстве Cortex_M3 вы можете увидеть следующую картинку:

Из этого рисунка видно, что функция этого побитового ИЛИ состоит в том, чтобы установить младшие 4 бита регистра R14 в D. После возврата этого исключения он переходит в режим потока и использует стек потоков PSP, потому что режим потока должен использоваться при выполнении задачи. Только при возникновении прерывания или исключения позвольте системе войти в режим обработки и использовать MSP.
Причина, по которой в xPortPendSVHandler нет строки, заключается в том, что перед вводом исключения система запускает задачу, используя режим потока и PSP, после ввода исключения она становится режимом обработки и MSP, но автоматически вернется к этому, когда исключение вернется. Режим до возникновения исключения — это режим потока и PSP.
Перед вызовом функции vPortSVCHandler система находилась в режиме обработки и использовала MSP. (Поскольку после сброса он находится в режиме Handle, большинство проектов STM32, которые не используют систему, запускаются в самом высоком режиме Handle.)
Я вижу, что значение в регистре LR равно 0xFFFFFFFD, поэтому я должен посмотреть на адрес PSP, найти адрес адреса, а затем открыть память, как показано на рисунке ниже, ввести адрес регистра, указанный выше, щелкнуть правой кнопкой мыши и выбрать длинный, чтобы просмотреть адрес, как показано ниже:

Затем посмотрите на этот адрес и отсчитайте шесть длинных адресов. Почему существует шесть длинных адресов? Потому что, когда возникает исключение, ядро последовательно помещает регистры R0
R3, R12, Returnaddress, PSR и LR в стек, где адрес возврата — это вхождение Адрес следующей инструкции, которую ПК выполнит перед исключением;
Вероятно, 0x08xxxxxx — это расположение кода ошибки, который можно разобрать для просмотра, как показано на следующем рисунке:

Видно, что соответствующая программа на языке C имеет ошибку при чтении функции FLASH, поэтому ее можно расценить как проблему нарушения доступа.

2.2 Метод 2: самый простой и очевидный
В состоянии отладки после входа в точку останова HardFault в строке меню Peripherals> Core Peripherals> FaultReports открывается отчет об аномальном возникновении, чтобы просмотреть причину сбоя.

В приведенном выше отчете произошел сбой шины, и служба прерывания сбоя была переведена в режим жесткого сбоя.
Более важно определить местонахождение аномалии, чем обнаружить аномалию.
(1) Откройте окно стека вызовов (как показано на рисунке ниже, точка останова останавливается в служебной программе Hard Fault)



2.3 Метод 3: этот метод примерно такой же, как и метод 1
Далее описывается, как обнаружить отклонения в программе.

Затем в проекте keil_MDK скомпилируйте код, выполните отладку и затем запустите на полной скорости, вы можете увидеть, что программа входит в исключение HardFault, как показано на рисунке ниже.

Как показано ниже, мы находим регистр SP, 0x200045B8 — это адрес стека, а значения в стеке — R0
R3, R12, PC (адрес возврата), xPSR (CPSR или SPSR), LR. Как показано на рисунке, мы видим место, где проведена красная линия, обратите внимание на то, чтобы смотреть справа налево. Это 0x0800427D и 0x08004BFA.

Введите 0x08004BFA в коде показа по адресу и нажмите «Перейти к», чтобы найти программу, которая будет выполняться рядом с сегментом аномального кода

Мы используем тот же метод, чтобы ввести 0x0800427D в код показа по адресу и найти следующий сегмент кода

Можно обнаружить, что аномальный код находится в функции uart_send_noackdata.В этой функции мы определяем указатель и начинаем его использовать, не выделяя для него места. Из этого мы освоили первый метод поиска аномалий. Просто запишите содержимое байтов с 21-го по 24-й и с 25-го по 28-й в стек, чтобы легко найти код исключения. Ниже описывается использование файла .map для поиска аномалий. Файл .map автоматически создается в проекте keil при компиляции программы.
В файле .map мы искали 0x08004BFA и обнаружили, что 0x08004bd8 указывает на функцию uart_send_noackdata.Пока мы нашли местоположение аномального кода.

Из этого мы знаем, что нам нужно только найти адрес памяти в регистрах ПК (адрес возврата) и xPSR (CPSR или SPSR) в стеке, чтобы найти код исключения.

CPSR
Текущий государственный регистр программы (Current Program State Register)
SPSR
Имеется 6 регистров сохраненного состояния программы (Saved Program State Register), которые в основном используются при обработке исключений.
В каждом режиме процессора есть специальный физический регистр в качестве регистра состояния программы резервного копирования SPSR. Когда возникает конкретное исключение, этот физический регистр отвечает за сохранение содержимого регистра текущего состояния программы CPSR. Когда обработчик исключения возвращается, тогда Восстановите содержимое на устройстве текущего состояния программы и продолжите выполнение исходной программы.
PC
Программный счетчик используется для подсчета, указывая место хранения инструкции в памяти, которая является адресной информацией.
Interrupt
Faults happen on embedded devices all the time for a variety of reasons – ranging from something as simple as a NULL pointer dereference to something more unexpected like running a faulty code path only when in a zero-g environment on the Tower of Terror in Disneyland 1 . It’s important for any embedded engineer to understand how to debug and resolve this class of issue quickly.
In this article, we explain how to debug faults on ARM Cortex-M based devices. In the process, we learn about fault registers, how to automate fault analysis, and figure out ways to recover from some faults without rebooting the MCU. We include practical examples, with a step by step walk-through on how to investigate them.
If you’d rather listen to me present this information and see some demos in action, watch this webinar recording.
Like Interrupt? Subscribe to get our latest posts straight to your mailbox.
Table of Contents
Determining What Caused The Fault
All MCUs in the Cortex-M series have several different pieces of state which can be analyzed when a fault takes place to trace down what went wrong.
First we will explore the dedicated fault status registers that are present on all Cortex-M MCUs except the Cortex-M0.
If you are trying to debug a Cortex-M0, you can skip ahead to the next section where we discuss how to recover the core register state and instruction being executed at the time of the exception.
NOTE: If you already know the state to inspect when a fault occurs, you may want to skip ahead to the section about how to automate the analysis.
Relevant Status Registers
Configurable Fault Status Registers (CFSR) — 0xE000ED28
This 32 bit register contains a summary of the fault(s) which took place and resulted in the exception. The register is comprised of three different status registers – UsageFault, BusFault & MemManage Fault Status Registers:

The register can be accessed via a 32 bit read at 0xE000ED28 or each register can be read individually. For example, in GDB it would look something like this:
- Entire CFSR — print/x *(uint32_t *) 0xE000ED28
- UsageFault Status Register (UFSR) — print/x *(uint16_t *)0xE000ED2A
- BusFault Status Register (BFSR) — print/x *(uint8_t *)0xE000ED29
- MemManage Status Register (MMFSR) — print/x *(uint8_t *)0xE000ED28
NOTE: If multiple faults have occurred, bits related to several faults may be set. Fields are only cleared by a system reset or by writing a 1 to them.
UsageFault Status Register (UFSR) — 0xE000ED2A
This register is a 2 byte register which summarizes any faults that are not related to memory access failures, such as executing invalid instructions or trying to enter invalid states.

- DIVBYZERO — Indicates a divide instruction was executed where the denominator was zero. This fault is configurable.
- UNALIGNED — Indicates an unaligned access operation occurred. Unaligned multiple word accesses, such as accessing a uint64_t that is not 8-byte aligned, will always generate this fault. With the exception of Cortex-M0 MCUs, whether or not unaligned accesses below 4 bytes generate a fault is also configurable.
- NOCP — Indicates that a Cortex-M coprocessor instruction was issued but the coprocessor was disabled or not present. One common case where this fault happens is when code is compiled to use the Floating Point extension ( -mfloat-abi=hard -mfpu=fpv4-sp-d16 ) but the coprocessor was not enabled on boot.
- INVPC — Indicates an integrity check failure on EXC_RETURN . We’ll explore an example below. EXC_RETURN is the value branched to upon return from an exception. If this fault flag is set, it means a reserved EXC_RETURN value was used on exception exit.
- INVSTATE — Indicates the processor has tried to execute an instruction with an invalid Execution Program Status Register (EPSR) value. Among other things the ESPR tracks whether or not the processor is in thumb mode state. Instructions which use “interworking addresses” 2 ( bx & blx or ldr & ldm when loading a pc -relative value) must set bit[0] of the instruction to 1 as this is used to update ESPR.T . If this rule is violated, a INVSTATE exception will be generated. When writing C code, the compiler will take care of this automatically, but this is a common bug which can arise when hand-writing assembly.
- UNDEFINSTR — Indicates an undefined instruction was executed. This can happen on exception exit if the stack got corrupted. A compiler may emit undefined instructions as well for code paths that should be unreachable.
Configurable UsageFault
It is worth noting that some classes of UsageFaults are configurable via the Configuration and Control Register (CCR) located at address 0xE000ED14 .
- Bit 4 ( DIV_0_TRP ) — Controls whether or not divide by zeros will trigger a fault.
- Bit 3 ( UNALIGN_TRP ) — Controls whether or not unaligned accesses will always generate a fault.
NOTE: On reset both of these optional faults are disabled. It is generally a good idea to enable DIV_0_TRP to catch mathematical errors in your code.
BusFault Status Register (BFSR) — 0xE000ED29
This register is a 1 byte register which summarizes faults related to instruction prefetch or memory access failures.

- BFARVALID — Indicates that the Bus Fault Address Register (BFAR), a 32 bit register located at 0xE000ED38 , holds the address which triggered the fault. We’ll walk through an example using this info below.
- LSPERR & STKERR — Indicates that a fault occurred during lazy state preservation or during exception entry, respectively. Both are situations where the hardware is automatically saving state on the stack. One way this error may occur is if the stack in use overflows off the valid RAM address range while trying to service an exception. We’ll go over an example below.
- UNSTKERR — Indicates that a fault occurred trying to return from an exception. This typically arises if the stack was corrupted while the exception was running or the stack pointer was changed and its contents were not initialized correctly.
- IMPRECISERR — This flag is very important. It tells us whether or not the hardware was able to determine the exact location of the fault. We will explore some debug strategies when this flag is set in the next section and walk through a code exampe below.
- PRECISERR — Indicates that the instruction which was executing prior to exception entry triggered the fault.
Imprecise Bus Error Debug Tips
Imprecise errors are one of the hardest classes of faults to debug. They result asynchronously to instruction execution flow. This means the registers stacked on exception entry will not point to the code that caused the exception.
Instruction fetches and data loads should always generate synchronous faults for Cortex-M devices and be precise. Conversely, store operations can generate asynchronous faults. This is because writes will sometimes be buffered prior to being flushed to prevent pipeline stalls so the program counter will advance before the actual data store completes.
When debugging an imprecise error, you will want to inspect the code around the area reported by the exception for a store that looks suspicious. If the MCU has support for the ARM Embedded Trace Macrocell (ETM), the history of recently executed instructions can be viewed by some debuggers 3 .
Auxiliary Control Register (ACTLR) — 0xE000E008
This register allows for some hardware optimizations or features to be disabled typically at the cost of overall performance or interrupt latency. The exact configuration options available are specific to the Cortex-M implementation being used.
For the Cortex M3 & Cortex M4 only, there is a trick to make all IMPRECISE accesses PRECISE by disabling any write buffering. This can be done by setting bit 1 ( DISDEFWBUF ) of the register to 1.
For the Cortex M7, there is no way to force all stores to be synchronous / precise.
Auxiliary Bus Fault Status Register (ABFSR) — 0xE000EFA8
This register only exists for Cortex-M7 devices. When an IMPRECISE error occurs it will at least give us an indication of what memory bus the fault occurred on 4 :

A full discussion of memory interfaces is outside the scope of this article but more details can be found in the reference manual 4 .
MemManage Status Register (MMFSR) — 0xE000ED28
This register reports Memory Protection Unit faults.
Typically MPU faults will only trigger if the MPU has been configured and enabled by the firmware. However, there are a few memory access errors that will always result in a MemManage fault – such as trying to execute code from the system address range ( 0xExxx.xxxx ).
The layout of the register looks like this:

- MMARVALID — Indicates that the MemManage Fault Address Register (MMFAR), a 32 bit register located at 0xE000ED34 , holds the address which triggered the MemManage fault.
- MLSPERR & MSTKERR — Indicates that a MemManage fault occurred during lazy state preservation or exception entry, respectively. For example, this could happen if an MPU region is being used to detect stack overflows.
- MUNSTKERR — Indicates that a fault occurred while returning from an exception
- DACCVIOL — Indicates that a data access triggered the MemManage fault.
- IACCVIOL — Indicates that an attempt to execute an instruction triggered an MPU or Execute Never (XN) fault. We’ll explore an example below.
HardFault Status Register (HFSR) — 0xE000ED2C
This registers explains the reason a HardFault exception was triggered.

There’s not too much information in this register but we will go over the fields real quickly
- DEBUGEVT — Indicates that a debug event (i.e executing a breakpoint instruction) occurred while the debug subsystem was not enabled
- FORCED — This means a configurable fault (i.e. the fault types we discussed in previous sections) was escalated to a HardFault, either because the configurable fault handler was not enabled or a fault occurred within the handler.
- VECTTBL — Indicates a fault occurred because of an issue reading from an address in the vector table. This is pretty atypical but could happen if there is a bad address in the vector table and an unexpected interrupt fires.
Recovering the Call Stack
To fix a fault, we will want to determine what code was running when the fault occurred. To accomplish this, we need to recover the register state at the time of exception entry.
If the fault is readily reproducible and we have a debugger attached to the board, we can manually add a breakpoint for the function which handles the exception. In GDB this will look something like
Upon exception entry some registers will always be automatically saved on the stack. Depending on whether or not an FPU is in use, either a basic or extended stack frame will be pushed by hardware.
Regardless, the hardware will always push the same core set of registers to the very top of the stack which was active prior to entering the exception. ARM Cortex-M devices have two stack pointers, msp & psp . Upon exception entry, the active stack pointer is encoded in bit 2 of the EXC_RETURN value pushed to the link register. If the bit is set, the psp was active prior to exception entry, else the msp was active.
Let’s look at the state when we break in HardFault_Handler for a pathological example:
Offset 6 and 7 in the array dumped hold the LR ( illegal_instruction_execution ) & PC ( 0xe0000000 ) so we now can see exactly where the fault originated!
Faults from Faults!
The astute observer might wonder what happens when a new fault occurs in the code dealing with a fault. If you have enabled configurable fault handlers (i.e MemManage, BusFault, or UsageFault), a fault generated in these handlers will trigger a HardFault.
Once in the HardFault Handler, the ARM Core is operating at a non-configurable priority level, -1. At this level or above, a fault will put the processor in an unrecoverable state where a reset is expected. This state is known as Lockup.
Typically, the processor will automatically reset upon entering lockup but this is not a requirement per the specification. For example, you may have to enable a hardware watchdog for a reset to take place. It’s worth double checking the reference manual for the MCU being used for clarification.
When a debugger is attached, lockup often has a different behavior. For example, on the NRF52840, “Reset from CPU lockup is disabled if the device is in debug interface mode” 5 .
When a lockup happens, the processor will repeatedly fetch the same fixed instruction, 0xFFFFFFFE or the instruction which triggered the lockup, in a loop until a reset occurs.
Fun Fact: Whether or not some classes of MemManage or BusFaults trigger a fault from an exception is actually configurable via the MPU_CTRL.HFNMIENA & CCR.BFHFNMIGN register fields, respectively.
Automating the Analysis
At this point we have gone over all the pieces of information which can be manually examined to determine what caused a fault. While this might be fun the first couple times, it can become a tiresome and error prone process if you wind up doing it often. In the following sections we’ll explore how we can automate this analysis!
Halting & Determining Core Register State
What if we are trying to debug an issue that is not easy to reproduce? Even if we have a debugger attached, useful state may be overwritten before we have a chance to halt the debugger and take a look.
The first thing we can do is to programmatically trigger a breakpoint when the system faults:
Above, we discussed how to hand unroll the register state prior to the exception taking place. Let’s explore how we can instrument the code to make this a less painful process.
First, we can easily define a C struct to represent the register stacking:
We can determine the stack pointer that was active prior to the exception using a small assembly shim that applies the logic discussed above and passes the active stack pointer as an argument into my_fault_handler_c :
Finally, we can put together my_fault_handler_c that looks something like:
Now when a fault occurs and a debugger is attached, we will automatically hit a breakpoint and be able to look at the register state! Re-examining our illegal_instruction_execution example we have:
Furthermore, we now have a variable we can read stack info from and a C function we can easily extend for postportem analysis!
Fault Register Analyzers
Instrumenting the code
Many Real Time Operating Systems (RTOS) targetting Cortex-M devices will add options to dump verbose fault register information to the console upon crash. Some examples include Arm Mbed OS 6 and Zephyr 7 . For example, with Zephyr, the illegal_instruction_execution() crash looks like:
This approach has a couple notable limitations:
- It bloats the code & data size of the binary image and consequently often gets turned off.
- It can increase the stack size requirements for the fault handler (due to printf calls)
- It requires a firmware update to improve or fix issues with the analyzers
- It requires a console session be active to see what fault occurred. Furthermore, this can be flaky if the system is in a crashed state.
Debugger Plugins
Many embedded IDEs expose a system view that can be used to look at registers. The registers will often be decoded into human readable descriptions. These implementations typically leverage the CMSIS System View Description (SVD) format 8 , a standardized XML file format for describing the memory mapped registers in an ARM MCU. Most silicon vendors expose this information on their own website, ARM’s website 9 , or provide the files upon request.
You can even load these files in GDB using PyCortexMDebug 10 , a GDB python script .
To use the utility, all you need to do is update your .gdbinit to use PyPi packages from your environment (instructions here) and then run:
When you next start gdb, you can source the svd_gdb.py script and use it to start inspecting registers. Here’s some output for the svd plugin we will use in the examples below:
Postmortem Analysis
The previous two approaches are only helpful if we have a debug or physical connection to the device. Once the product has shipped and is out in the field these strategies will not help to triage what went wrong on devices.
One approach is to simply try and reproduce the issue on site. This is a guessing game (are you actually reproducing the same issue the customer hit?), can be a huge time sink and in some cases is not even particularly feasible 1 .
Another strategy is to log the fault register and stack values to persistent storage and periocially collect or push the error logs. On the server side, the register values can be decoded and addresses can be symbolicated to try to root cause the crash.
Alternatively, an end-to-end firmware error analysis system, such as Memfault, can be used to automatically collect, transport, deduplicate and surface the faults and crashes happening in the field. Here is some example output from Memfault for the bad memory read example we will walk through below:

Recovering From A Fault
DISCLAIMER: Typically when a fault occurs, the best thing to do is reset the MCU since it’s hard to be certain what parts of the MCU were corrupted as part of the fault (embedded MCUs don’t offer a MMU like you would find on a bigger processors).
Occasionally, you may want to recover the system from a fault without rebooting it. For example, maybe you have one RTOS task isolated by the MPU that just needs to be restarted.
Let’s quickly explore how we could implement a recovery mechanism that puts a RTOS task which experience a UsageFault into an idle loop and reboots the system otherwise.
We will use the Application Interrupt and Reset Control Register to reset the device if the fault is unrecoverable. We can easily extend my_fault_handler_c from above:
Now, the interesting part, how do we clean up our state and return to normal code from the HardFault handler?!
There’s a few things we will need to do:
- Clear any logged faults from the CFSR by writing 1 to each bit which is set.
- Change the function we return to so we idle the task. In the example case it’s recover_from_task_fault .
- Scribble a known pattern over the lr . The function we are returning to will need to take special action (i.e like deleting the task or entering a while(1) loop). It can’t just exit and branch to where we were before so we want to fault if this is attempted.
- Reset the xpsr . Among other things the xpsr tracks the state of previous comparison instructions which were run and whether or not we are in the middle of a “If-Then” instruction block. The only bit that needs to remain set is the “T” field (bit 24) indicating the processor is in thumb mode 11 .
This winds up looking like:
You may recall from the RTOS Context Switching post that fault handlers can work just like regular C functions so after these changes we will exit from my_fault_handler_c and start executing whatever is in recover_from_task_fault function. We will walk through an example of this below.
Examples
In the sections below we will walk through the analysis of a couple faults.
For this setup we will use:
- a nRF52840-DK 12 (ARM Cortex-M4F) as our development board
- SEGGER JLinkGDBServer 13 as our GDB Server.
- GCC 8.3.1 / GNU Arm Embedded Toolchain as our compiler 14
- GNU make as our build system
All the code can be found on the Interrupt Github page with more details in the README in the directory linked.
Setup
Start a GDB Server:
Follow the instructions above to setup support for reading SVD files from GDB, build, and flash the example app:
The app has eight different crashes you can configure by changing FAULT_EXAMPLE_CONFIG at compile time or by editing the value at runtime:
eXecute Never Fault
Analysis
We can check the CFSR to see if there is any information about the fault which occurred.
That’s interesting! We hit a Memory Management instruction access violation fault even though we haven’t enabled any MPU regions. From the CFSR, we know that the stacked frame is valid so we can take a look at that to see what it reveals:
We can clearly see that the executing instruction was 0xe0000000 and that the calling function was prvQueuePingTask .
From the ARMv7-M reference manual 15 we find:
The MPU is restricted in how it can change the default memory map attributes associated with System space, that is, for addresses 0xE0000000 and higher. System space is always marked as XN, Execute Never.
So the fault registers didn’t lie to us, and it does make sense that we hit a memory management fault!
Hardfault handler stm32 как бороться
Ядро ARM Cortex-M реализует набор исключений отказов (fault exceptions). Каждое исключение относится к определенному условию возникновения ошибки. Если ошибка произошла, то ядро ARM Cortex-M останавливает выполнение текущей инструкции и делает ветвление на функцию обработчика исключения (exception handler). Этот механизм очень похож на тот, который используется для прерываний, где ядро ARM Cortex-M делает ветвление на обработчик прерывания (interrupt handler, ISR), когда принимает прерывание.
CMSIS определяет следующие имена для обработчиков отказов (fault handlers):
UsageFault_Handler()
BusFault_Handler()
MemMang_Handler()
HardFault_Handler()
Перечисление всех причин и обстоятельств, при которых ядро ARM Cortex-M вызывает каждый из этих обработчиков, выходит за рамки этого документа (перевод статьи [1]). См. литературу по ARM Cortex-M для ARM и разные другие источники, если нужны подробности. Ошибки типа HardFault встречаются наиболее часто, поскольку другие типы отказов, не разрешенные по отдельности, пройдут эскалацию, чтобы превратиться в hard fault.
Несмотря на многочисленные запросы поддержки RTOS, когда люди жаловались, что при использовании ядра RTOS, их приложение падает в обработчик ошибки hard fault, причина аппаратного сбоя оказывалась вовсе не в ядре. Обычно это было одно из следующего:
• Неправильное понимание приоритетов прерываний ядра ARM Cortex-M (эту оплошность допустить весьма просто!), или неправильное понимание, как использовать модель вложенности прерываний FreeRTOS (см. [2]).
• Общая пользовательская ошибка RTOS. См. статью FAQ «My Application Does Not Run – What Could Be Wrong» [3], специально написанную для помощи в подобных случаях.
• Баг в коде приложения.
Отладка ошибки Hard Fault должна начаться с проверки, что программа приложения следует руководствам [2, 3]. Если после этого ошибка hard fault все еще не исправлена, то необходимо определить состояние системы (system state) в момент времени, когда произошел сбой. Отладчики не всегда упрощают эту задачу, поэтому остальная часть этой статьи описывает техники программирования, используемые для отладки.
[Какой Exception Handler выполнился?]
В таблице векторов прерываний обычно устанавливается один и тот же обработчик для каждого источника прерывания/исключения. Обработчики по умолчанию (default handlers) декларируются как weak-символы (код заглушки), чтобы разработчик приложения мог установить свой собственный обработчик простой реализацией функции с корректным именем. Если произошло прерывание, для которого разработчик приложения не предоставил свой отдельный обработчик, то будет выполнен обработчик по умолчанию (default handler).
[weak-функции на ассемблере]
Символ weak это по сути метка с указанием на код, который может быть при необходимости переопределен простым созданием функции с таким же именем. К примеру, weak-обработчики прерываний проекта IAR для STM32, сгенерированного с помощью STM32CubeMX, для микроконтроллера STM32F407 в коде запуска startup_stm32f407xx.s ;будут выглядеть примерно так (weak-обработчики отказов выделены жирным шрифтом):
В модуле кода (это тоже автоматически сгенерированный код) заглушки обработчиков отказов выглядят следующим образом:
[weak-функции на языке C]
Обработчики по умолчанию обычно реализуются как бесконечный цикл. Если приложение заканчивает работу на таком default handler, то сначала необходимо определить, где именно произошло прерывание выполнения кода.
Следующий кусок кода демонстрирует, как добавить несколько инструкций к бесконечному циклу обработчика по умолчанию, чтобы загрузить номер выполняющегося прерывания в регистр 2 (r2) перед входом в бесконечный цикл.
Номера прерываний здесь считываются из NVIC относительно начала таблицы векторов, в которой есть записи для системных исключений (таких как hard fault), они находятся выше записей прерываний периферийных устройств. Если в r2 находится значение 3, то обработано исключение hard fault. Если r2 содержит значение, равное или больше 16, то это обрабатывается прерывание периферии, и периферийное устройство, которое вызвало прерывание, можно определить вычитанием 16 из номера прерывания.
[Отладка ARM Cortex-M Hard Fault]
Окно стека (stack frame) обработчика fault handler содержит состояние регистров ARM Cortex-M в момент времени, когда произошла ошибка. Код ниже показывает, как прочитать значения регистров из стека в переменные C. Когда это сделано, значения этих переменных могут быть проинспектированы в отладчике точно так же, как и другие переменные.
Сначала определяется очень короткая функция на ассемблере, чтобы определить, какой стек использовался, когда произошла ошибка. Как только это выполнено код ассемблера fault handler передает указатель на стек в C-функцию с именем prvGetRegistersFromStack() .
Обработчик fault handler показан ниже в синтаксисе GCC. Обратите внимание, что функция была декларирована как naked, так что она не содержит никакого кода, генерированного компилятором (например, здесь нет кода пролога входа в функцию).
Реализация функции prvGetRegistersFromStack() показана ниже. Она копирует значения из стека в переменные C, после чего падает в цикл. Имена переменных выбраны, в соответствии с именами регистров, чтобы было проще проанализировать значения, считанные из соответствующих регистров. Другие регистры не будут изменяться с момента возникновения ошибки, и их можно просмотреть в окне отображения регистров CPU отладчика.
[Использование значений регистров]
Самый первый из интересующих регистров это программный счетчик. В показанном выше коде переменная pc как раз и содержит значение программного счетчика. Когда ошибка это точный отказ (precise fault), pc хранит адрес инструкции, которая была выполнена, когда произошла ошибка hard fault (или другой fault). Когда ошибка это неточный отказ (imprecise fault), то требуются дополнительные шаги, чтобы найти адрес инструкции, которая привела к ошибке.
Чтобы найти инструкцию по адресу которая хранится в переменной pc, сделайте одно из следующего.
1. Откройте окно кода ассемблера (точнее дизассемблированного кода) в отладчике, и вручную введите значение адреса из переменной pc, чтобы посмотреть инструкции по этому адресу.
2. Откройте окно точек останова (break point) в отладчике, и вручную определите точку останова (break point) на этом адресе (execution break) или точку останова по доступу (access break) к этому адресу. С установленной break point перезапустите приложение, чтобы увидеть строку кода, относящуюся к адресу инструкции, которая соответствует переменной pc.
Когда известна инструкция, которая была выполнена при возникновении fault, можно узнать интересующие значения в других регистрах. Например, если инструкция использовала значение R7 в качестве адреса, то нужно знать значение R7. В будущем, анализируя код ассемблера и код C, из которого был сгенерирован этот ассемблерный код, можно увидеть, что реально содержится в R7 (например, это может быть значение переменной).
[Как разобраться с неточным отказом]
Отказы (faults) платформы ARM Cortex-M могут быть точными (precise fault) или неточными (imprecise fault). Если установлен бит IMPRECISERR (бит 2) в регистре отказа шины (BusFault Status Register, или BFSR , который доступен как байт по адресу 0xE000ED29), то это сигнал неточного отказа (imprecise fault).
Обнаружить причину imprecise fault сложнее, потому что fault не обязательно будет происходить одновременно с выполнением инструкции, приведшей к отказу. Например, если идет кэшированная запись в память, то может быть задержка между инструкцией ассемблера, инициировавшей запись в память, и реальной операцией записи в память. Если такая отложенная запись в память недопустима (например, попытка записи в несуществующую физически память) то произойдет imprecise fault, и значение программного счетчика, полученного с помощью вышеуказанного примера кода, не будет соответствовать адресу инструкции, которая инициировала недопустимую операцию записи.
В примере, показанном выше, выключите буферизацию записи установкой бита DISDEFWBUF (бит 1) в регистре ACTLR (Auxiliary Control Register), что в результате превратит imprecise fault в precise fault, и это упростит отладку ценой замедления выполнения программы.
Hardfault handler stm32 как бороться
Ноя 10, 2014 |
1 comment
Hard Fault, MemManage Fault, Usage Fault, Bus Fault — Cortex-M3
Иногда при отлаживании программы можно увидеть как при зависании программы мы попадаем в HardFault_Handler. Давайте разберемся с причинами.
Если появляются ситуации, когда процессор не может правильно выполнить свою работу — то возникают отказы. Отказы можно наблюдать в обработчике прерывания отказов.
Процессор Cortex-M3 имеет следующие виды отказов:
- тяжелые отказы (Hard Fault)
- отказы системы управления памятью (MemManage Fault)
- отказы программы (Usage Fault)
- отказы шины (Bus Fault)
В startup файле от keil прерывания отказов:
Но если предварительно не разрешить прерывания для отказов, то при возникновении любого (Bus, Usage, MemManage, Hard Fault) отказа попадать будем в HardFaultHandler (по умолчанию HardFaultHandler всегда разрешен — запретить его можно в регистре FAULTMASK).
Для разрешения прерывания для других отказов:
Рассмотрим причины отказов.
Hard Fault
Исключение Hard Fault может быть вызвано отказом программы, отказом шины или отказом системы управления памятью в том случае, если выполнение обработчиков указанных системных исключений невозможно. Также тяжёлый отказ может быть вызван отказом шины во время выборки вектора (чтения значения из таблицы векторов). Регистр состояния тяжёлого отказа HFSR контроллера NVIC позволяет определить, был ли вызван данный отказ выборкой вектора. Ecли нет, то обработчик исключения Hard Faultдолжен будет проверить содержимое других регистров xFSR для определения причины возникновения отказа (отказ Bus, Usage, MemManage).

Bus Fault
Отказы шины возникают при получении сигнала ошибки во время обмена по шине АНВ. Обычно это происходит в следующих случаях:
- При попытке обращения по недопустимому адресу (например, в вашем микроконтроллере RAM начинается с 0x20000000 и размером0x4000, а вы пытаетесь прочитать/записать 0x20004010 — адрес не задействован ни одной периферией микроконтроллера).
- Если устройство не готово к обмену (например, при попытке обращения к динамическому ОЗУ до инициализации контроллера динамической памяти).
- При попытке обмена с разрядностью, не поддерживаемой конечным устройством.

Точный отказ — отказ , вызванный последней завершенной операцией(например, операция чтения вызовет точный отказ, поскольку команда чтения не может быть завершена до тех пор, пока не будут получены данные).
Неточный отказ — отказы, вызванный выполнением уже завершенной операции (запись с буферизацией), которая могла произойти несколько тактов назад.
В случае точного отказа шины адрес команды, вызвавшей ошибку, может быть определён из значения, сохранённого в стеке счётчика команд. Если при этом установлен бит BFARVALID регистра BFSR, то дополнительно можно будет определить адрес памяти, обращение к которому вызвало отказ шины. Значение данного адреса содержится в регистре адреса отказа шины BFAR (&(SCB->BFAR)) контроллера NVIC. ДЛЯ неточных отказов шины подобная информация недоступна, поскольку на момент получения сообщения об ошибке процессор мoг уже выполнить несколько других команд.
MemManage Fault
Отказы системы управления памятью мoгут быть вызваны обращением к памяти, конфликтующим с настройкам модуля MPU, или же некорректным обращением (например, при попытке выполнить код из области памяти, не имеющей такой возможности), которое может генерировать отказ даже в случае отсутствия модуля MPU. Наиболее частыми отказами, связанными с модулем MPU, являются:
- обращение к областям памяти, не заданным в настройках модуля MPU;
- запись в области памяти, предназначенные только для чтения;
- обращение на пользовательском уровне к области, допускающей обращение только на привилегированном уровне.

Если MMFSR регистр показывает, что отказ произошёл при нарушении прав доступа во время обращения к данным (бит DACCVIOL) или к команде (бит IACCVIOL), то адрес команды, вызвавшей ошибку, может быть определён из значения счётчика команд, сохранённого в стеке. Если при этом установлен бит MMARVALID регистра MMFSR, то из регистра адреса отказа системы управления памятью MMFAR контроллера NVIC можно будет прочитать адрес памяти, обращение к которому вызвало отказ.
Usage Fault
Наиболее распространенной причиной возникновения отказов программы является попытка переключения процессора в режим ARM. Это может произойти при загрузке в PC нового значения со сброшенным младшим битом. Как правило это происходит при попытке перехода по команде BX или BLX по адресу, содержащемуся в регистре без предварительной установки младшего бита значения.

Note: Более подробно об отказах можно прочитать в книге Ядро «Cortex-M3 компании ARM, полное руководство» стр. 154.