Chapter 11 of 20

Interrupt Controller (NVIC) — Tiva C

Eslam El Hefny Apr 11, 2025 7 min read
55% done

Interrupt Controller (NVIC) — Tiva C

Overview

The Nested Vector Interrupt Controller (NVIC) is the Cortex-M4’s built-in interrupt management unit. It handles up to 240 external interrupt sources with up to 256 configurable priority levels (the TM4C123 implements 8 levels using 3 bits). The NVIC supports tail-chaining and late-arrival optimisations that dramatically reduce interrupt-to-ISR latency.


Beginner Level — What & Why

What is an Interrupt?

An interrupt is a signal that immediately pauses the CPU’s current work, saves its state, runs a special function (the ISR — Interrupt Service Routine), and then resumes. It is how hardware events (button press, timer expiry, data received) are handled efficiently without constantly checking (“polling”) each peripheral.

Real-World Analogy

Imagine you are writing an essay (main loop). The phone rings — that is an interrupt. You bookmark your place (save CPU state), answer the phone (run ISR), hang up, and resume writing where you left off (restore CPU state). The NVIC is the phone system that decides which call to take first when multiple phones ring at once.

What Problem Does It Solve?

  • Eliminates busy-wait polling loops that waste CPU cycles
  • Enables the CPU to sleep between events, saving power
  • Allows multiple hardware events to be handled in priority order

Key Terms

TermMeaning
IRQInterrupt ReQuest — external hardware interrupt
ISRInterrupt Service Routine — the handler function
PriorityLower number = higher urgency (0 is highest)
PreemptionHigher-priority interrupt interrupts a running ISR
Tail-chainingBack-to-back ISRs without full state restore/save
Vector tableArray of ISR addresses in Flash at 0x00000000

Intermediate Level — How It Works

NVIC on TM4C123

  • 138 interrupt sources implemented (of 240 maximum)
  • 8 priority levels: 0 (highest) to 7 (lowest)
  • Priority stored in bits [7:5] of each priority byte; bits [4:0] are ignored
  • Effective priority register value: priority 0 = 0x00, priority 7 = 0xE0

Key NVIC Registers

RegisterAddressDescription
NVIC_EN00xE000E100Enable interrupts 0–31
NVIC_EN10xE000E104Enable interrupts 32–63
NVIC_EN20xE000E108Enable interrupts 64–95
NVIC_EN30xE000E10CEnable interrupts 96–127
NVIC_EN40xE000E110Enable interrupts 128–138
NVIC_DIS00xE000E180Disable interrupts 0–31
NVIC_PRI00xE000E400Priority for IRQs 0–3
NVIC_PRI10xE000E404Priority for IRQs 4–7
One register per 4 IRQs
SCB AIRCR0xE000ED0CPriority grouping
SYSPRI10xE000ED18Priority for MemManage, BusFault, UsageFault
SYSPRI20xE000ED1CPriority for SVCall
SYSPRI30xE000ED20Priority for SysTick, PendSV

TivaWare DriverLib API

#include "driverlib/interrupt.h"

/* Enable global (master) interrupt — sets PRIMASK = 0 */
IntMasterEnable();

/* Disable global interrupt — sets PRIMASK = 1 */
IntMasterDisable();

/* Enable specific IRQ in NVIC */
IntEnable(INT_TIMER0A);   // Timer0A
IntEnable(INT_GPIOF);     // GPIO Port F
IntEnable(INT_UART0);     // UART0

/* Disable specific IRQ */
IntDisable(INT_TIMER0A);

/* Set priority (0 = highest, 7 = lowest on TM4C123) */
IntPrioritySet(INT_TIMER0A, 0);  // highest priority
IntPrioritySet(INT_UART0,   2);  // medium priority
IntPrioritySet(INT_GPIOF,   5);  // lower priority

/* Read current priority */
uint32_t prio = IntPriorityGet(INT_TIMER0A);

/* Configure priority grouping (preemption bits vs sub-priority bits) */
IntPriorityGroupingSet(4);  // 3 preemption bits, 0 sub-priority bits

ISR Declaration

In CCS, startup_ccs.c contains a vector table with weak ISR aliases. To override them, declare a function with the exact name:

/* Declared as weak in startup_ccs.c — override with your own */
void TIMER0A_Handler(void);    // Timer 0 subtimer A
void UART0_Handler(void);      // UART 0
void GPIOF_Handler(void);      // GPIO Port F
void SysTick_Handler(void);    // SysTick
void HardFault_Handler(void);  // Hard Fault

There is no __attribute__((interrupt)) needed on Cortex-M4 — standard C functions work because the hardware automatically saves/restores the required registers.


Advanced Level — Deep Dive

Bare-Metal NVIC Enable

#include "inc/hw_types.h"
#include "inc/hw_nvic.h"
#include "inc/hw_ints.h"

/* Enable Timer0A interrupt (IRQ number 19)
 * Register NVIC_EN0 controls IRQs 0–31
 * Bit = IRQ - 0 = 19 */
HWREG(NVIC_EN0) = (1U << (INT_TIMER0A - 16));
/* Note: Cortex-M4 IRQ0 = vector 16, so IRQ_num = INT_value - 16 */

/* Disable Timer0A interrupt */
HWREG(NVIC_DIS0) = (1U << (INT_TIMER0A - 16));

/* Set Timer0A priority to level 3 (0xE0 >> 5 = 7, 0x60 >> 5 = 3)
 * Priority register = 0x60 stored in bits [7:5] of byte */
uint32_t ui32Reg = HWREG(NVIC_PRI4);  // IRQ 16-19 → PRI4
ui32Reg &= ~(0xE0 << 24);             // clear bits [31:29] for IRQ19
ui32Reg |=  (0x60 << 24);             // set priority 3 (0x60 = 96)
HWREG(NVIC_PRI4) = ui32Reg;

Priority Grouping

The SCB AIRCR register’s PRIGROUP field splits the 3-bit priority into preemption priority and sub-priority:

/* PRIGROUP = 7 (all 3 bits = preemption priority, 0 sub-priority bits)
 * This means all 8 levels are preemption levels — most common setting */
HWREG(NVIC_APINT) = NVIC_APINT_VECTKEY | NVIC_APINT_PRIGROUP_7_1;

TivaWare IntPriorityGroupingSet(4) is equivalent (4 = 4 preemption bits, but TM4C123 only has 3).

Pending and Triggering Interrupts in Software

/* Software-trigger an interrupt (for testing) */
HWREG(NVIC_SW_TRIG) = INT_TIMER0A - 16;

/* Set interrupt pending flag manually */
HWREG(NVIC_PEND0) = (1U << (INT_TIMER0A - 16));

/* Clear pending flag */
HWREG(NVIC_UNPEND0) = (1U << (INT_TIMER0A - 16));

Critical Section Pattern

/* Enter critical section — disable all interrupts */
uint32_t ui32PriMask = IntMasterDisable();

/* Protected code — cannot be interrupted */
g_ui32SharedVar++;

/* Exit critical section — restore previous interrupt state */
if (!ui32PriMask)
    IntMasterEnable();

Tail-Chaining

When two interrupts are pending and the first ISR finishes, the Cortex-M4 does not perform a full state restore + save sequence. It directly fetches the second ISR vector — saving 6–12 cycles. This is handled entirely in hardware.

Gotchas

  • Priority 0 means highest — it is opposite to what many beginners expect.
  • Peripheral interrupt must ALSO be cleared in the ISR (e.g., TimerIntClear) — the NVIC only clears its own pending bit when the ISR returns, but the peripheral will re-assert its interrupt line if its flag is not cleared.
  • IntEnable alone does not make the ISR fire — the peripheral’s own interrupt enable must also be set (e.g., TimerIntEnable, UARTIntEnable).
  • SysTick and PendSV priorities are in SYSPRI3 — use HWREG(NVIC_SYS_PRI3), not IntPrioritySet.
  • Forgetting IntMasterEnable() is the most common bug for beginners — the NVIC may be correctly configured but global interrupts are still disabled.

Step-by-Step Example

/*
 * nvic_priority_demo.c
 * Two timers at different priorities; Timer1A (high) preempts Timer0A (low)
 * Board  : TM4C123GXL EK LaunchPad
 * SDK    : TivaWare_C_Series-2.2.x
 */

#include <stdint.h>
#include <stdbool.h>
#include "inc/hw_memmap.h"
#include "driverlib/sysctl.h"
#include "driverlib/gpio.h"
#include "driverlib/timer.h"
#include "driverlib/interrupt.h"

volatile uint32_t g_ui32T0Count = 0;
volatile uint32_t g_ui32T1Count = 0;

/* Timer0A ISR — low priority (handles ~1 Hz blink) */
void TIMER0A_Handler(void)
{
    TimerIntClear(TIMER0_BASE, TIMER_TIMA_TIMEOUT);
    g_ui32T0Count++;
    /* Simulate slow ISR work (can be preempted by Timer1A) */
    SysCtlDelay(SysCtlClockGet() / 600);  // ~5 ms inside ISR
    GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1,
                 GPIOPinRead(GPIO_PORTF_BASE, GPIO_PIN_1) ^ GPIO_PIN_1);
}

/* Timer1A ISR — high priority (handles ~10 Hz) */
void TIMER1A_Handler(void)
{
    TimerIntClear(TIMER1_BASE, TIMER_TIMA_TIMEOUT);
    g_ui32T1Count++;
    /* Fast ISR — just toggle blue LED */
    GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_2,
                 GPIOPinRead(GPIO_PORTF_BASE, GPIO_PIN_2) ^ GPIO_PIN_2);
}

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);

    /* Timer0 — 1 Hz, LOW priority (5) */
    SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER0);
    while (!SysCtlPeripheralReady(SYSCTL_PERIPH_TIMER0));
    TimerConfigure(TIMER0_BASE, TIMER_CFG_PERIODIC);
    TimerLoadSet(TIMER0_BASE, TIMER_A, 80000000 - 1);   // 1 Hz
    TimerIntEnable(TIMER0_BASE, TIMER_TIMA_TIMEOUT);
    IntPrioritySet(INT_TIMER0A, 0xA0);  // priority 5 (0xA0 >> 5 = 5)
    IntEnable(INT_TIMER0A);

    /* Timer1 — 10 Hz, HIGH priority (1) */
    SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER1);
    while (!SysCtlPeripheralReady(SYSCTL_PERIPH_TIMER1));
    TimerConfigure(TIMER1_BASE, TIMER_CFG_PERIODIC);
    TimerLoadSet(TIMER1_BASE, TIMER_A, 8000000 - 1);    // 10 Hz
    TimerIntEnable(TIMER1_BASE, TIMER_TIMA_TIMEOUT);
    IntPrioritySet(INT_TIMER1A, 0x20);  // priority 1 (0x20 >> 5 = 1)
    IntEnable(INT_TIMER1A);

    /* Enable global interrupts */
    IntMasterEnable();

    /* Start both timers */
    TimerEnable(TIMER0_BASE, TIMER_A);
    TimerEnable(TIMER1_BASE, TIMER_A);

    while (1)
    {
        /* Idle — all work done in ISRs */
    }
}

Summary

Key PointDetails
Interrupt sources138 on TM4C123 (240 Cortex-M4 max)
Priority levels8 (0 = highest, 7 = lowest)
Priority bits[7:5] of priority byte
Enable registerNVIC_EN0–EN4 (one bit per IRQ)
Global enableIntMasterEnable() / IntMasterDisable()
ISR namingMust match startup_ccs.c vector table name
Clear orderClear peripheral flag FIRST, then return from ISR
Tail-chainingAutomatic hardware optimisation for back-to-back ISRs

Next Chapter

Memory Protection Unit (MPU)

Share: