PSoC5LP RTOS
Lab 27 — Fault Handling (HardFault Diagnosis & Recovery)

FreeRTOS on PSoC 5LP · Lab Series Platform: CY8CKIT-059 (PSoC 5LP, Cortex-M3) · PSoC Creator 4.4 · FreeRTOS Kernel V11 Pattern: a fault traps into a handler that recovers the stacked frame, reads the fault-status registers, reports a post-mortem over UART, then decides on reset or graceful degradation Pairs with: Chapter 18 — Reliability & the Watchdog & Chapter 13 — System Architecture


1. Introduction & Objectives

Up to now, every safety net in the course has caught a known kind of mistake: configASSERT catches API misuse (Lab 23), the stack-overflow hook catches a task that outgrew its stack (Lab 13), the malloc-failed hook catches an exhausted heap (Lab 13). But none of them sees the classic C disaster — a wild pointer write, a call through a corrupted function pointer, a divide-by-zero, an unaligned access. On a Cortex-M3, those trap into a fault exception, and if you have not installed a handler, the board simply hangs in the default HardFault loop: dead, silent, no clue why.

This lab closes that last gap. The Cortex-M3 does most of the work for you: on a fault it automatically stacks a register snapshot (r0–r3, r12, LR, PC, xPSR) and vectors to your HardFault handler. Your handler recovers that frame — in particular the PC, the exact instruction that faulted — reads the fault-status registers (HFSR, CFSR, BFAR) to learn what and where, and prints a one-line post-mortem over UART. From "it hangs" you get to "fix the write on this source line." Then, because a fault handler is a branch point, not an end, you decide: log and reset (tying into the Lab 18 watchdog / reset-reason), or disable the broken subsystem and limp home.

After this lab you can:

  • Explain the Cortex-M3 fault family (HardFault, BusFault, UsageFault, MemManage) and how configurable faults escalate to HardFault.
  • Install a HardFault handler into the vector table that FreeRTOS_Init() built, with a small asm stub that picks MSP vs PSP.
  • Recover the stacked exception frame and read HFSR / CFSR / BFAR to diagnose a fault's cause and address.
  • Produce a post-mortem dump over UART that names the faulting PC.
  • Decide recovery: controlled reset (Lab 18) vs graceful degradation, and know why a silent return is never an option.
  • (Extension) Enable the configurable faults by name via SHCSR, and persist the dump across a reset.

2. Prerequisites

Hardware - CY8CKIT-059 PSoC 5LP Prototyping Kit - RobotSoC Kit: the UART-to-USB path from Lab 14 (the post-mortem comes out here); an LED to show "degraded but alive"

Software - PSoC Creator 4.4 with the FreeRTOS V11 BaseProject

Chapters (sync progress) - Required: Chapter 18 (Reliability & the Watchdog — reset and reset-reason) and Chapter 13 (System Architecture — where fault policy lives). - Related: Lab 18 (watchdog, CyResetStatus / reset reason), Lab 23 (configASSERT, run-time stats, the other hooks), Lab 13 (stack-overflow & malloc-failed hooks), Lab 12 (the vector table FreeRTOS_Init() builds, NVIC). - Cortex-M3 prerequisite: basic exception model (vector table, MSP/PSP, exception entry) — covered in the architecture chapter.

3. Reading Materials / References

  • Chapter 18 slides — Reliability & the Watchdog (reset sources, reset-reason latches).
  • ARMv7-M Architecture Reference Manual — exception entry & the stacked frame; the System Control Block fault registers: SHCSR, CFSR (= MMFSR | BFSR | UFSR), HFSR, BFAR, MMFAR.
  • PSoC 5LP: CyResetStatus / CY_RESET_WD, CySoftwareReset(), CyIntSetSysVector() (installing a system exception vector into the RAM vector table).
  • FreeRTOS: configASSERT, vApplicationStackOverflowHook, vApplicationMallocFailedHook (Lab 13 / Lab 23).

4. Background & Theory of Operation

The fault family and escalation. The Cortex-M3 has four fault exceptions. HardFault is always enabled and is the catch-all. The three configurable faults — UsageFault (divide-by-zero, undefined instruction, invalid state, optionally unaligned access), BusFault (a bad or aborted bus transfer, e.g. an access to an unmapped address), and MemManage (an MPU region violation — the PSoC 5LP has no MPU in this course, so this one does not arise) — are disabled at reset. While disabled, any such fault escalates into a HardFault, and HFSR.FORCED is set to say "I am standing in for a configurable fault." So out of the box, everything lands in your HardFault handler; enabling the configurable faults via SHCSR (the Exp03 extension) only changes which named vector you arrive at — the diagnosis is the same.

What the hardware gives you for free. On exception entry the CPU automatically pushes eight words onto the stack that was active when the fault hit: r0, r1, r2, r3, r12, LR, PC, xPSR (low address first). This is the register snapshot at the moment of the fault. The single most valuable word is the stacked PC — the address of the faulting instruction. Look that address up in the .lst/.map or the debugger's disassembly and you have the exact line of code to fix. The stacked LR is the faulting code's own return address (its caller), useful for context; note it is not the same as the EXC_RETURN value the CPU places in the live LR on entry.

Which stack? MSP vs PSP. Under FreeRTOS, tasks run on the Process Stack Pointer (PSP) and the kernel/ISRs run on the Main Stack Pointer (MSP). The frame is pushed onto whichever was active. On entry the CPU loads EXC_RETURN into LR; bit 2 tells you which stack to read: 0 → the frame is on MSP (the fault hit the kernel or an ISR), 1 → the frame is on PSP (the fault hit a task — the usual case). A tiny assembly stub is therefore the correct top-level handler: it tests LR bit 2, loads the right stack pointer into r0, and tail-calls a normal C function prvGetRegsFromStack(uint32_t *frame) that does the real work. (Writing the stub in asm avoids the C prologue disturbing the stack before you capture it.)

Reading the evidence — three registers, in order. 1. HFSR (HardFault Status). FORCED (bit 30) → a configurable fault escalated here, so go read CFSR. VECTTBL (bit 1) → a fault reading the vector table. 2. CFSR (Configurable Fault Status) — one 32-bit register packing UFSR | BFSR | MMFSR. Bits such as DIVBYZERO, UNALIGNED (usage), PRECISERR, IMPRECISERR, IBUSERR (bus) tell you the kind. BFARVALID says the address register is trustworthy. It is write-1-to-clear — clear it once you have read it so the next fault is unambiguous. 3. BFAR / MMFAR (Fault Address). The address that was accessed — valid only when BFARVALID / MMARVALID is set. An imprecise bus error (write buffer drained late) leaves no valid address; precise errors (a straight null-pointer dereference) do.

Together: HFSR says something escalated, CFSR says what kind, BFAR says at which address, and the stacked PC says from which instruction. Dump all of them over UART and the post-mortem writes itself.

The decision — a handler is a branch point. Once diagnosed, choose deliberately: - Log & reset. Write the dump somewhere that survives a reset (a reset-safe SRAM region, EEPROM, or just the UART log on the bench), then trigger a controlled reset — call CySoftwareReset(), or simply stop feeding the watchdog (Lab 18) and let it bite. On the next boot, CyResetStatus() & CY_RESET_WD tells you the last run died, and you can surface the saved dump. - Degrade gracefully. If the fault is isolable and the rest of the system state is sane, disable just the broken subsystem and keep the safe core running (limp-home). This is only justified when you can argue the remaining state is trustworthy — often it is not after memory corruption, so reset is the safer default. - Never silently return. Returning from a HardFault without fixing the cause just re-executes the faulting instruction and faults again — an infinite fault loop. A fault is not a warning; treat it as terminal to the current execution.

Where this sits in defence-in-depth. configASSERT + the two allocation hooks stop known misuse early, before corruption spreads. The fault handler is the last net — it catches the hardware traps the others never see, and converts a dead board into a readable diagnosis. A robust product has all four.

5. RTOS Task Architecture

WHEN IT REALLY BREAKS — CATCH THE FAULT, FIND WHERE, THEN DECIDE The fault path: a bad access traps into a handler that reads the evidence 1 · A task faults bad pointer, bad call, /0, unaligned *(int*)0 = 1; runs on PSP 2 · CPU auto-stacks hardware pushes the 8-word frame, then vectors to the handler r0-r3,r12,LR,PC,xPSR 3 · HardFault handler asm stub picks MSP/PSP from EXC_RETURN (LR), passes frame to C installed in the vector table 4 · Read the evidence → report PC (stacked) = the faulting instruction HFSR = forced? · CFSR = which sub-fault BFAR/MMFAR = the bad address dump all of it over UART for post-mortem 5 · Decide what to do next — a fault handler is not an end, it is a branch point Log & reset Save the dump to non-volatile, then controlled reset — soft reset or let the watchdog bite (Lab 18). reset reason survives → read it on boot Degrade gracefully Disable only the broken subsystem, keep the safe core running — limp home rather than go dark. only if the fault is isolable & state is sane Never: silently return Returning from a HardFault with the same bad state just re-faults — an endless loop. A fault is not a warning. diagnose first; a blind retry hides the bug Defence in depth — four nets, each catching a different class of failure configASSERT (Lab 23) API misuse & bad args — caught at the call, before corruption spreads Stack-overflow hook (Lab 13) a task ran past its stack — names the culprit task at the context switch Malloc-failed hook (Lab 13) heap exhausted — a create returned NULL; caught before a NULL deref faults Fault handler (THIS lab) the hardware trap — bad pointer, bus error, /0 that the three above never see The first three stop known misuse early; the fault handler is the last net — it turns a dead board into a readable post-mortem.

A task commits a bad access and faults; the CPU auto-stacks the 8-word frame and vectors to the HardFault handler, whose asm stub picks MSP/PSP from EXC_RETURN and hands the frame to a C routine. That routine reads the evidence — the stacked PC, plus HFSR/CFSR/BFAR — and dumps a post-mortem over UART. Then comes the decision: log & reset (via the Lab 18 watchdog / soft reset), or degrade gracefully. The lower band shows the four nets of defence in depth and which failure each one catches.

6. Anatomy of a Fault

The stacked exception frame — where the CPU left the evidence:

THE EXCEPTION STACK FRAME — WHERE THE CPU LEFT THE EVIDENCE Which stack holds the frame? On entry the CPU copies EXC_RETURN into LR. LR bit 2 = 0 → MSP (fault hit the kernel/an ISR); LR bit 2 = 1 → PSP (fault hit a task — the usual case under FreeRTOS). The handler tests that bit, then reads the frame from the right pointer. The 8 words the hardware pushes automatically (low address at top), before the handler runs: r0 SP+0x00 r1 SP+0x04 r2 SP+0x08 r3 SP+0x0C r12 SP+0x10 LR (r14) SP+0x14 PC (r15) SP+0x18 xPSR SP+0x1C PC = the instruction that faulted. Look it up in the .lst / .map or the debugger’s disassembly — that is the line of code to fix. Reading the frame in C uint32_t *f = psp_used ? psp : msp; // f[0]=r0 f[1]=r1 f[2]=r2 f[3]=r3 // f[4]=r12 f[5]=LR uint32_t pc = f[6]; // faulting PC uint32_t psr = f[7]; The stacked LR here is the task’s own return address — not EXC_RETURN (that is in the CPU LR). The CPU does the hard part for free: by the time your handler runs, the register snapshot at the fault is already sitting on the stack. Cortex-M3 (no FPU): exactly these 8 words. The frame is 8-byte aligned, so xPSR bit 9 may flag an added pad word — account for it if you unwind further.

The hardware pushes r0–r3, r12, LR, PC, xPSR onto the active stack; EXC_RETURN bit 2 says whether that is MSP or PSP. The stacked PC is the faulting instruction — the line to fix.

Decoding the fault — which register answers which question:

DECODING THE FAULT — WHICH REGISTER ANSWERS WHICH QUESTION The fault family — configurable faults escalate to HardFault unless you enable them UsageFault /0, undefined instr, bad state, unaligned BusFault bad address, aborted bus transfer MemManage MPU region violation (no MPU here → n/a) escalate HardFault always enabled. Catches the three if they are disabled (SHCSR) — HFSR.FORCED marks it. Inside the handler, read three registers in order — each narrows “what” and “where”: A · HFSR — is this forced? SCB->HFSR FORCED (bit 30): a configurable fault escalated to here → go read CFSR for the real cause. VECTTBL (bit 1): bad vector fetch. B · CFSR — which sub-fault? SCB->CFSR (UFSR|BFSR|MMFSR) DIVBYZERO, UNALIGNED … → usage PRECISERR, IMPRECISERR → bus BFARVALID → BFAR holds the addr one write-1-to-clear register, three bytes C · BFAR/MMFAR — where? SCB->BFAR SCB->MMFAR The faulting address — valid only when BFARVALID/MMARVALID is set (imprecise bus errors: not valid). pair with the stacked PC = who touched it Put it together → the post-mortem line FAULT: PC=0x00001A3C LR=0x00001A07 CFSR=0x00000400 (PRECISERR) BFAR=0x00000000 → null-pointer write at 0x1A3C dump the 8 stacked words + HFSR + CFSR + BFAR over UART; that one line turns “it hangs” into “fix the write on this source line”. HFSR says “something escalated”, CFSR says “what kind”, BFAR says “at which address”, PC says “from which instruction”. To see BusFault/UsageFault by name instead of a bare HardFault, set SCB->SHCSR enable bits at boot — otherwise they all arrive as forced HardFaults.

Configurable faults escalate to HardFault unless enabled. Inside the handler, read HFSR (forced?), then CFSR (which sub-fault?), then BFAR (which address?), and pair with the stacked PC.

The lesson both diagrams carry: the snapshot is already on the stack and in the SCB registers — a good handler just reads it out and prints it, turning a silent hang into an actionable bug report.

7. Schematic Circuit & Pin Configuration

  • UART TX → the post-mortem output (reuse the Lab 14 UART-to-USB path); open a terminal at the project baud.
  • LED → lit in the "degraded / limp-home" path so a bench observer sees the board survived in a reduced mode.
  • Assign the pins in <project>.cydwr → Pins.

8. PSoC Creator Configuration

  • A UART component for the dump (as in Lab 14); no new hardware is required for Exp01/Exp02.
  • The HardFault vector is part of the table FreeRTOS_Init() builds. Install your handler with CyIntSetSysVector(CY_INT_HARD_FAULT_IRQN, (cyisraddress)HardFault_Handler); at startup, after FreeRTOS_Init() (the template does this). (Exp03: enable the configurable faults by setting the USGFAULTENA / BUSFAULTENA bits in SCB->SHCSR, and install BusFault_Handler / UsageFault_Handler the same way.)*
  • FreeRTOSConfig.h (already set): configASSERT defined (Lab 23), configCHECK_FOR_STACK_OVERFLOW = 2, configUSE_MALLOC_FAILED_HOOK = 1 (Lab 13), heap_4, configUSE_TIME_SLICING = 1 (course default).

9. Provided Template

You are given Lab27_Template_main.c:

  • Given: all includes (note #include "FreeRTOS_PSoC.h"), a report() UART-print helper sketch, the asm HardFault_Handler stub that selects MSP/PSP and tail-calls prvGetRegsFromStack, a complete main() that installs the handler after FreeRTOS_Init() and creates a "culprit" task plus a steady heartbeat task, and the SCB register names via CMSIS (core_cm3.h through project.h).
  • You write: the body of prvGetRegsFromStack() (pull pc, lr, psr from the frame; read HFSR/CFSR/BFAR; print the post-mortem; then decide reset vs degrade). The Exp02 recovery policy and the Exp03 SHCSR-enable path are sketched in comments.
  • Do not change the asm stub or the boot sequence in main().
/* top-level handler: asm stub picks the stack, then calls C */
__attribute__((naked)) void HardFault_Handler(void)
{
    __asm volatile(
        " tst lr, #4        \n"   /* EXC_RETURN bit 2: 0=MSP, 1=PSP */
        " ite eq            \n"
        " mrseq r0, msp     \n"
        " mrsne r0, psp     \n"
        " b prvGetRegsFromStack \n"
    );
}

/* C side: frame[6] = faulting PC, frame[7] = xPSR */
void prvGetRegsFromStack(uint32_t *frame)
{
    uint32_t pc = frame[6];               /* the instruction that faulted */
    uint32_t cfsr = SCB->CFSR;            /* which sub-fault + BFARVALID  */
    uint32_t bfar = SCB->BFAR;            /* faulting address (if valid)  */
    report(pc, frame[5], cfsr, SCB->HFSR, bfar);   /* post-mortem over UART */
    /* decide: CySoftwareReset();  OR  disable subsystem + continue        */
    for (;;) { }                          /* never silently return */
}

10. Lab Experiments

Exp01 — Basic: catch and diagnose a HardFault

Install the handler. From the "culprit" task, trigger a precise fault on command (e.g. a null-pointer write *(volatile uint32_t*)0 = 1;). Confirm the heartbeat task was running right up to the fault, then the handler fires and prints PC, LR, CFSR, HFSR, BFAR. Look the PC up in the .lst/map and confirm it points at your bad line. Expected: a one-line post-mortem that names the faulting instruction and (for the null write) BFARVALID with BFAR = 0.

Exp02 — Advanced: decide recovery (reset vs degrade)

Replace the for(;;) halt with a policy. (a) Log the dump to a reset-safe location and call CySoftwareReset(); on reboot, read CyResetStatus() and print "recovered from fault, last PC = …". (b) Alternatively, for an isolable fault, disable only the culprit subsystem, light the "degraded" LED, and keep the heartbeat running. Argue in your report which policy is right for your fault and why. Expected: a board that either restarts cleanly and reports the prior crash, or stays alive in a reduced mode — never an infinite fault loop.

Exp03 — Challenge: name the fault, and provoke each kind

Enable the configurable faults (SCB->SHCSR |= USGFAULTENA | BUSFAULTENA) and install UsageFault_Handler / BusFault_Handler. Provoke, one at a time: a divide-by-zero (also set SCB->CCR |= DIV_0_TRP), an unaligned access (UNALIGN_TRP), and a bus error (read a truly unmapped address). Show each now arrives at its named handler with the matching CFSR bit, instead of a bare forced HardFault. Expected: a table mapping each provoked fault to its vector and its CFSR bit, with the address shown where BFARVALID.

11. Testing & Verification

#Pass condition
Exp01 A deliberate fault is caught; the dump names the faulting PC (verified against the map) and the correct CFSR bit.
Exp02 Recovery is deliberate: a controlled reset that reports the prior crash, or a justified graceful degradation — no fault loop.
Exp03 Configurable faults are enabled and each provoked fault reaches its named handler with the matching CFSR bit.

Submit: your prvGetRegsFromStack() body, a captured post-mortem line with the PC resolved to a source line, your Exp02 recovery policy with justification, and the Exp03 fault→vector→CFSR table.

12. Q&A & Troubleshooting

  • Board just hangs, handler never prints → your handler is not in the table; install it with CyIntSetSysVector(...) after FreeRTOS_Init(), which (re)builds the vector table.
  • Handler fires but PC/registers look like garbage → you read the wrong stack; the asm stub must pick MSP vs PSP from EXC_RETURN bit 2. Don't add a C prologue before capturing the frame (keep the stub naked).
  • BFAR shows a stale/wrong address → check BFARVALID first; imprecise bus errors leave no valid address. Also clear CFSR (write-1-to-clear) after reading so the next fault is clean.
  • Everything arrives as a forced HardFault → that's normal until you enable the configurable faults in SHCSR (Exp03); HFSR.FORCED confirms the escalation.
  • UART prints nothing in the handler → you're at an exception priority above your UART ISR, or using a blocking driver; use a polled/blocking-by-register UART write in the handler, never a *FromISR queue call.
  • Fault loops forever → you returned from the handler with the same bad state; diagnose and reset or halt, don't retry blindly.
  • Divide-by-zero doesn't fault → it's disabled by default; set SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk and enable UsageFault.

13. Analysis Questions & Lab Report

  1. Name the four Cortex-M3 fault exceptions. Which is always enabled, and what does HFSR.FORCED tell you about the other three?
  2. List the eight words the CPU auto-stacks on a fault. Which one is most valuable for debugging, and how do you turn it into a source line?
  3. How does the handler know whether the frame is on MSP or PSP, and why is the top-level handler written in assembly?
  4. Walk through HFSR → CFSR → BFAR: what question does each answer, and when is BFAR not to be trusted?
  5. Compare the four nets of defence in depth (configASSERT, stack-overflow hook, malloc-failed hook, fault handler): give one failure each one catches that the others do not.
  6. (Extension) Why is returning from a HardFault dangerous, and what are the two legitimate things a handler can do instead?

Your report should include your prvGetRegsFromStack(), a resolved post-mortem, your recovery policy with justification, the Exp03 fault table, and answers to the six questions.

© 2026 Air Supply Information Center (Air Supply BBS)