ARMv7-M Hardfaults, SVCALL, and Debuggers

Cortex-M3 and Cortex-M4

The most popular CPUs in current MCU designs are the Cortex-M3 (ARMv7-M) and the Cortex-M4 (ARMv7E-M). Handling of these two architectures is almost identical in NuttX (unless hardware floating point is enabled).

SVCALL

NuttX uses the SVCALL software interrupt in order to perform certain steps in the context switching for the Cortex-M3 and Cortext-M4. This sequence of logic appears in several places:

  • Create a short critical section by disabling exceptions,
  • Perform some set-up,
  • Initiate the software exception / SVCALL, and
  • When the software exception processing returns, re-enable exceptions.

NOTE: There is a technical difference between interrupts and exceptions. In this Wiki page, the term exception will be used; It is probably the more accurate since interrupts are really exceptions that result from device interrupt lines. Further, when we refer to exceptions in this Wiki page, we are referring specifically to ARMv7-M configurable exceptions.

Disabling Interrupts via the PRIMASK register

The ARMv7-M architecture supports a register called the PRIMASK register. The PRIMASK register contains a single valid bit. If that bit is set to one, then exceptions are disabled; if that bit is zero, exceptions are enabled. More correctly, when this bit is set to one, it prevents the activation of all exceptions with configurable priority.

The original NuttX implementation used this PRIMASK register to enable and disable exceptions. Things now get interesting in the sequence of logic listed above because the PRIMASK bit also disables the SVCALL exception! So, instead of taking the SVCALL exception vector, the Cortex-M3/4 generates a hardfault exception (see ARM.com's discussion of Activation Levels. These hardfaults are not really a problem; the design of the NuttX hardfault handler expects these exceptions and does the right thing. However, the occurrence of hardfaults may come as a surprise to many people – and especially to some debuggers.

Hardfaults and Debuggers

These hardfaults only become a technical issue when dealing with a debugger. What does the debugger do when the hardfault occurs?

We need to back off and think about some system philosophy here. The deep philosophical question here is: Who is in charge of system integrity? The debugger or the RTOS? If you are running a primitive NoOS program (like the famous blinky test program), then you are running a barebones system and you need all of the help you can get. So having the debugger make decisions about what is the proper behavior of the blinky program and what is not is a good thing for you.

But if you are using an advanced RTOS, then the RTOS will want to take responsibility of the health of your system and now there is the possibility of inconsistencies between the decisions that the RTOS makes and the decisions that your debugger makes – this hardfault handling is a perfect example of this. In NuttX, the hardfaults are controlled by the RTOS, but some debuggers will break when the hardfault occurs and make debugging impossible.

Some people might take issue with this; breaking on hardfault can make debugging easier, since the break happens in the throwing context and you have a hope of obtaining a backtrace and debugging the throwing side callstack. However, I would suggest that putting a break point on up_assert() would accomplish the same thing without being so intrusive.

How did the debugger know that the hardfault occurred? It knew because of settings in the “ARM's Debug Exception and Monitor Control Register” or DEMCR. The settings the DEMCR register will allow the debugger to get break exceptions when a hardfault occrs.

So one workaround is to just reconfigure the DEMCR register so that break exceptions are no longer generated when hardfaults occur. Here is an example of such logic for the LPC43xx MCU. Decoupling the hardfault from the break exception in this way does not work with all debuggers, however. Presumably because some debuggers re-enable break exceptions on hardfaults.

Disabling Interrupts via the BASEPRI register

The ARMv7-M architecture supports another way to disable exceptions using a register called the BASEPRI register. ARMv7-M exceptions are prioritized; Each exception can be assigned an 8-bit priority. If the BASEPRI register is set to a non-zero priority value, then it will filter exceptions in this sense: Exceptions with priority lower than or equal to the BASEPRI register will be disabled; Exceptions with priority higher than the BASEPRI register will still be enabled; Normally this interrupt prioritization is used to support nested interrupt handling, but it can also be used for disabling of all exceptions if configured properly.

NOTE: In the ARMv7-M, higher values correspond to lower priority. This can be really confusing!

NuttX supports a configuration option called CONFIG_ARMV7M_USEBASEPRI. If this option is selected, then the exception prioritization and control logic will be configured to use the BASEPRI register instead of the PRIMASK register to disable exceptions. This configuration includes the following changes in the behavior:

  • Normal interrupts and exceptions are restricted to the range { lowest priority … (highest priority - 1) }
  • The priority of the SVCALL exception is set to highest priority
  • When exceptions are enabled, the BASEPRI register is set to zero, enabling exceptions of all priorities.
  • When exceptions are disabled, the BASEPRI register is set to (highest priority - 1), disabling all exceptions except for the SVCALL exception.

In this way, the SVCALL exception remains enabled when exceptions are disabled and no hardfault occurs.

NOTE: The above is inaccurate on several counts. It was simplified to make the discussion sane. Not only do higher values correspond to lower priorities, but the increment between consecutive 8-bit priority values is probably not one. The supported maximum and minimum priority values (as well as the step in each priority value) may be different for each MCU. To handle this, these values are exported in NuttX for each ARMv7-M architecture by header files at nuttx/arch/arm/include/<chip>/chip.h.