all
Chapter 20 of 20

Error Handling — Tiva C

Eslam El Hefny Apr 20, 2025 9 min read
100% done

Error Handling — Tiva C

Overview

Robust embedded firmware does not simply work correctly under ideal conditions — it gracefully handles faults, unexpected inputs, and hardware errors. The Cortex-M4 provides a rich set of fault exceptions (HardFault, MemManage, BusFault, UsageFault) with detailed status registers. Combined with strategic assert macros, defensive programming patterns, and well-designed error LED indicators, firmware can detect, report, and recover from errors in the field.


Beginner Level — What & Why

What is a Fault?

A fault is an exception triggered by the CPU when it detects an illegal operation: accessing an invalid memory address, executing an undefined instruction, dividing by zero (with certain configurations), or violating MPU permissions. Rather than silently corrupting memory or hanging forever, the Cortex-M4 jumps to a fault handler function where you can diagnose and respond.

Real-World Analogy

A fault is like a circuit breaker in your home’s electrical panel. When something dangerous happens (overload, short circuit), the breaker trips and power is cut before further damage occurs. The breaker tells you which circuit had a problem so you can fix it. Fault handlers are your microcontroller’s circuit breakers.

What Problem Does It Solve?

  • Transforms silent corruption into detectable, diagnosable events
  • Enables graceful recovery or safe shutdown instead of undefined behaviour
  • Provides debug information (fault address, instruction that caused it) even without a debugger
  • Required by safety standards for embedded systems

Key Terms

Term Meaning
HardFault Catch-all fault; escalates from lower priority faults
MemManage MPU violation — access to forbidden region
BusFault Invalid memory access detected by bus
UsageFault Undefined instruction, divide-by-zero, unaligned access
CFSR Combined Fault Status Register (32-bit)
HFSR HardFault Status Register
MMFAR MemManage Fault Address Register
BFAR Bus Fault Address Register
assert Macro that checks a condition and halts/logs on failure

Intermediate Level — How It Works

Cortex-M4 Fault Hierarchy

UsageFault  (priority configurable, default disabled)
BusFault    (priority configurable, default disabled)
MemManage   (priority configurable, default disabled)
    ↓ (escalates if not enabled or not high enough priority)
HardFault   (always enabled, fixed highest priority among faults)
    ↓ (escalates if lockup conditions met)
Lockup      (CPU stops; external reset required)

Fault Status Registers

Register Address Description
HFSR 0xE000ED2C HardFault Status (FORCED, VECTTBL bits)
CFSR 0xE000ED28 Combined: UFSR [31:16] + BFSR [15:8] + MMFSR [7:0]
MMFAR 0xE000ED34 Faulting address (valid when CFSR.MMARVALID=1)
BFAR 0xE000ED38 Bus fault address (valid when CFSR.BFARVALID=1)
AFSR 0xE000ED3C Auxiliary Fault Status (implementation-specific)
SHCSR 0xE000ED24 System Handler Control (enable MemManage, BusFault, UsageFault)

CFSR sub-field breakdown:

MMFSR bits [7:0]:

  • Bit 0 (IACCVIOL): Instruction fetch from MPU-forbidden region
  • Bit 1 (DACCVIOL): Data access from MPU-forbidden region
  • Bit 7 (MMARVALID): MMFAR contains valid address

BFSR bits [15:8]:

  • Bit 8 (IBUSERR): Instruction prefetch bus error
  • Bit 9 (PRECISERR): Precise data bus error (BFAR valid)
  • Bit 10 (IMPRECISERR): Imprecise error (BFAR may not be valid)
  • Bit 15 (BFARVALID): BFAR contains valid address

UFSR bits [31:16]:

  • Bit 16 (UNDEFINSTR): Undefined instruction
  • Bit 17 (INVSTATE): Invalid state (e.g., Thumb bit not set)
  • Bit 18 (INVPC): Invalid EXC_RETURN value
  • Bit 19 (NOCP): No coprocessor (FPU access without enabling it)
  • Bit 24 (UNALIGNED): Unaligned memory access (when UNALIGN_TRP enabled)
  • Bit 25 (DIVBYZERO): Divide-by-zero (when DIV_0_TRP enabled)

Enabling Individual Fault Handlers

/* Enable MemManage, BusFault, UsageFault separately
 * (otherwise they escalate to HardFault) */
HWREG(0xE000ED24) |= (1 << 16) |  // MemManage enable
                     (1 << 17) |  // BusFault enable
                     (1 << 18);   // UsageFault enable

/* Enable divide-by-zero UsageFault */
HWREG(0xE000ED14) |= (1 << 4);   // DIV_0_TRP in CCR

/* Enable unaligned access fault */
HWREG(0xE000ED14) |= (1 << 3);   // UNALIGN_TRP in CCR

TivaWare Assert Macro

TivaWare uses its own assert mechanism in driverlib/debug.h:

/* From driverlib/debug.h */
#ifdef DEBUG
extern void __error__(char *pcFilename, uint32_t ui32Line);
#define ASSERT(expr)  { if (!(expr)) __error__(__FILE__, __LINE__); }
#else
#define ASSERT(expr)
#endif

/* You must provide the __error__ implementation in your project */
void __error__(char *pcFilename, uint32_t ui32Line)
{
    /* Log filename and line number via UART, then halt */
    UARTprintf("ASSERT FAILED: %s:%d\r\n", pcFilename, ui32Line);
    while (1);  // halt for debugging
}

Advanced Level — Deep Dive

HardFault Handler with Stack Frame Decode

When a fault occurs, the CPU automatically pushes 8 registers onto the stack (the “exception frame”). Reading these from the handler gives you the exact PC, LR, and xPSR at the time of fault:

/* Stack frame layout pushed by hardware on exception entry */
typedef struct
{
    uint32_t r0;
    uint32_t r1;
    uint32_t r2;
    uint32_t r3;
    uint32_t r12;
    uint32_t lr;   /* Link Register — caller's return address */
    uint32_t pc;   /* Program Counter — address of faulting instruction */
    uint32_t xpsr; /* Program Status Register */
} ExceptionFrame_t;

/* HardFault handler that extracts the stack frame */
__attribute__((naked)) void HardFault_Handler(void)
{
    /* Determine which stack was in use (MSP or PSP) and pass to C handler */
    __asm volatile (
        " tst   lr, #4           \n"  /* test bit 2 of LR (EXC_RETURN) */
        " ite   eq               \n"
        " mrseq r0, msp          \n"  /* EXC_RETURN bit 2 = 0 → used MSP */
        " mrsne r0, psp          \n"  /* EXC_RETURN bit 2 = 1 → used PSP */
        " b     HardFault_C      \n"  /* jump to C handler with frame in R0 */
    );
}

void HardFault_C(ExceptionFrame_t *frame)
{
    uint32_t hfsr  = HWREG(0xE000ED2C);
    uint32_t cfsr  = HWREG(0xE000ED28);
    uint32_t mmfar = HWREG(0xE000ED34);
    uint32_t bfar  = HWREG(0xE000ED38);

    UARTprintf("=== HARD FAULT ===\r\n");
    UARTprintf("PC     = 0x%08X\r\n", frame->pc);
    UARTprintf("LR     = 0x%08X\r\n", frame->lr);
    UARTprintf("xPSR   = 0x%08X\r\n", frame->xpsr);
    UARTprintf("HFSR   = 0x%08X\r\n", hfsr);
    UARTprintf("CFSR   = 0x%08X\r\n", cfsr);

    if (cfsr & (1 << 7))   /* MMARVALID */
        UARTprintf("MMFAR  = 0x%08X (MPU fault address)\r\n", mmfar);
    if (cfsr & (1 << 15))  /* BFARVALID */
        UARTprintf("BFAR   = 0x%08X (bus fault address)\r\n", bfar);

    /* Blink all LEDs rapidly as fault indicator */
    while (1)
    {
        GPIOPinWrite(GPIO_PORTF_BASE,
                     GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3,
                     GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3);
        SysCtlDelay(SysCtlClockGet() / 3 / 10);  // 100 ms
        GPIOPinWrite(GPIO_PORTF_BASE,
                     GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3, 0);
        SysCtlDelay(SysCtlClockGet() / 3 / 10);
    }
}

Custom DEBUG_ASSERT Macro

/* debug.h */
#ifndef NDEBUG
  #define DEBUG_ASSERT(condition, message)        \
    do {                                           \
      if (!(condition)) {                          \
        UARTprintf("ASSERT: %s\r\n  File: %s\r\n  Line: %d\r\n", \
                   message, __FILE__, __LINE__);   \
        /* Flash error LED pattern */              \
        Error_LED_Signal(3);                       \
        while (1);                                 \
      }                                            \
    } while (0)
#else
  #define DEBUG_ASSERT(condition, message)  /* nothing in release */
#endif

/* Usage */
DEBUG_ASSERT(ptr != NULL, "Null pointer dereference");
DEBUG_ASSERT(len <= MAX_BUF, "Buffer overflow");

A common technique to communicate error codes via LED when UART is unavailable:

/* Blink the red LED a specific number of times to indicate error code */
void Error_LED_Signal(uint8_t code)
{
    while (1)
    {
        uint8_t i;
        for (i = 0; i < code; i++)
        {
            GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, GPIO_PIN_1);
            SysCtlDelay(SysCtlClockGet() / 6);   // 500 ms ON
            GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, 0);
            SysCtlDelay(SysCtlClockGet() / 6);   // 500 ms OFF
        }
        SysCtlDelay(SysCtlClockGet());            // 3 sec pause between groups
    }
}

Semi-Hosting vs UART Debug Output

Method Pros Cons
UART printf Works standalone Requires UART + terminal
CCS semi-hosting Uses debugger console Halts if no debugger attached
SWV/ITM trace Non-intrusive, high speed Requires TPIU, CCS SWV viewer
LED blink codes No hardware needed Limited information

Defensive Programming Patterns

/* Pattern 1: Null pointer check */
void process_data(uint8_t *buf, uint32_t len)
{
    if (buf == NULL || len == 0) return;
    /* ... */
}

/* Pattern 2: Return code checking */
typedef enum { ERR_OK = 0, ERR_TIMEOUT, ERR_NACK, ERR_OVERFLOW } Status_t;

Status_t I2C_Write_Safe(uint8_t addr, uint8_t reg, uint8_t data)
{
    I2CMasterSlaveAddrSet(I2C0_BASE, addr, false);
    I2CMasterDataPut(I2C0_BASE, reg);
    I2CMasterControl(I2C0_BASE, I2C_MASTER_CMD_BURST_SEND_START);

    uint32_t timeout = 10000;
    while (I2CMasterBusy(I2C0_BASE) && timeout--);
    if (!timeout) return ERR_TIMEOUT;

    if (I2CMasterErr(I2C0_BASE) != I2C_MASTER_ERR_NONE) return ERR_NACK;

    /* ... */
    return ERR_OK;
}

/* Pattern 3: State machine validation */
typedef enum { STATE_IDLE, STATE_ACTIVE, STATE_ERROR } State_t;
static State_t g_eState = STATE_IDLE;

void StateMachine_Update(void)
{
    switch (g_eState)
    {
        case STATE_IDLE:   /* ... */ break;
        case STATE_ACTIVE: /* ... */ break;
        case STATE_ERROR:  /* ... */ break;
        default:
            DEBUG_ASSERT(0, "Invalid state machine state");
            g_eState = STATE_ERROR;
            break;
    }
}

/* Pattern 4: Range validation */
static inline uint32_t Clamp(uint32_t val, uint32_t min, uint32_t max)
{
    if (val < min) return min;
    if (val > max) return max;
    return val;
}

Startup Fault Handler Registration

In startup_ccs.c, the default handlers are weak aliases to Default_Handler. Override by defining a non-weak function with the same name:

/* These names must match exactly those in startup_ccs.c */
void HardFault_Handler(void);   /* override weak alias */
void MemManage_Handler(void);
void BusFault_Handler(void);
void UsageFault_Handler(void);

/* Default_Handler is caught if you miss one */
void Default_Handler(void)
{
    while (1);  /* unexpected interrupt */
}

Step-by-Step Example

/*
 * error_handling_demo.c
 * Demonstrates HardFault handler, assert macro, and error LED patterns
 * Board  : TM4C123GXL EK LaunchPad
 * SDK    : TivaWare_C_Series-2.2.x
 */

#include <stdint.h>
#include <stdbool.h>
#include "inc/hw_memmap.h"
#include "inc/hw_types.h"
#include "driverlib/sysctl.h"
#include "driverlib/gpio.h"
#include "driverlib/uart.h"
#include "driverlib/pin_map.h"
#include "utils/uartstdio.h"

/* TivaWare assert implementation */
void __error__(char *pcFilename, uint32_t ui32Line)
{
    UARTprintf("ASSERT FAIL: %s line %d\r\n", pcFilename, ui32Line);
    /* Blink red LED rapidly */
    while (1)
    {
        GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, GPIO_PIN_1);
        SysCtlDelay(SysCtlClockGet() / 3 / 20);  // 50 ms
        GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1, 0);
        SysCtlDelay(SysCtlClockGet() / 3 / 20);
    }
}

/* Naked HardFault wrapper — determines stack in use */
__attribute__((naked)) void HardFault_Handler(void)
{
    __asm volatile (
        " tst   lr, #4    \n"
        " ite   eq        \n"
        " mrseq r0, msp   \n"
        " mrsne r0, psp   \n"
        " b     HardFault_C \n"
    );
}

void HardFault_C(uint32_t *frame)
{
    UARTprintf("HARD FAULT! PC=0x%08X CFSR=0x%08X\r\n",
               frame[6],              /* PC in frame */
               HWREG(0xE000ED28));    /* CFSR */
    /* Error code 5 = HardFault */
    uint8_t i;
    while (1)
    {
        for (i = 0; i < 5; i++)
        {
            GPIOPinWrite(GPIO_PORTF_BASE,
                         GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3,
                         GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3);
            SysCtlDelay(SysCtlClockGet()/6);
            GPIOPinWrite(GPIO_PORTF_BASE,
                         GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3, 0);
            SysCtlDelay(SysCtlClockGet()/6);
        }
        SysCtlDelay(SysCtlClockGet());
    }
}

int main(void)
{
    SysCtlClockSet(SYSCTL_SYSDIV_2_5 | SYSCTL_USE_PLL |
                   SYSCTL_OSC_MAIN   | SYSCTL_XTAL_16MHZ);

    /* LEDs */
    SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);
    while (!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOF));
    GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE,
                          GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3);

    /* UART */
    SysCtlPeripheralEnable(SYSCTL_PERIPH_UART0);
    SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
    while (!SysCtlPeripheralReady(SYSCTL_PERIPH_UART0));
    while (!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOA));
    GPIOPinConfigure(GPIO_PA0_U0RX); GPIOPinConfigure(GPIO_PA1_U0TX);
    GPIOPinTypeUART(GPIO_PORTA_BASE, GPIO_PIN_0 | GPIO_PIN_1);
    UARTStdioConfig(0, 115200, SysCtlClockGet());

    UARTprintf("Error handling demo started.\r\n");

    /* Enable UsageFault for divide-by-zero detection */
    HWREG(0xE000ED24) |= (1 << 18);  // UsageFault enable
    HWREG(0xE000ED14) |= (1 << 4);   // DIV_0_TRP enable

    /* Normal operation: green LED heartbeat */
    uint32_t count = 0;
    while (1)
    {
        GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_3, GPIO_PIN_3);
        SysCtlDelay(SysCtlClockGet() / 6);
        GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_3, 0);
        SysCtlDelay(SysCtlClockGet() / 6);
        count++;
        UARTprintf("Loop count: %d\r\n", count);

        /* After 5 iterations, deliberately trigger a fault (NULL ptr write) */
        /* Comment this out for normal operation */
        if (count == 5)
        {
            UARTprintf("Triggering deliberate HardFault...\r\n");
            volatile uint32_t *p = (uint32_t *)0xDEADBEEF;
            *p = 0;  /* BusFault → escalates to HardFault */
        }
    }
}

Summary

Key Point Details
HardFault Catch-all fault; check HFSR for FORCED bit
CFSR Combined status: MMFSR + BFSR + UFSR
PC at fault Recovered from stack frame (frame[6])
MMFAR / BFAR Faulting address (check MMARVALID / BFARVALID first)
Enable faults SHCSR bits 16, 17, 18 for MemManage, BusFault, UsageFault
Div-by-zero Enable via CCR DIV_0_TRP bit
assert TivaWare: ASSERT macro + user error function
Error LED Blink count = error code for field debugging
Naked handler Recovers stack frame without compiler interference
Defensive code Null checks, return codes, state validation, range clamping

Series Complete

Congratulations on completing the Tiva C Series tutorial track. You have covered every major peripheral from GPIO to USB, along with system-level topics like the MPU, DMA, and robust error handling. The knowledge here forms a solid foundation for building professional embedded systems on the TM4C123GXL LaunchPad.

Return to the full series → Tiva C Tutorials

Share: