all
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

Term Meaning
IRQ Interrupt ReQuest — external hardware interrupt
ISR Interrupt Service Routine — the handler function
Priority Lower number = higher urgency (0 is highest)
Preemption Higher-priority interrupt interrupts a running ISR
Tail-chaining Back-to-back ISRs without full state restore/save
Vector table Array 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

Register Address Description
NVIC_EN0 0xE000E100 Enable interrupts 0–31
NVIC_EN1 0xE000E104 Enable interrupts 32–63
NVIC_EN2 0xE000E108 Enable interrupts 64–95
NVIC_EN3 0xE000E10C Enable interrupts 96–127
NVIC_EN4 0xE000E110 Enable interrupts 128–138
NVIC_DIS0 0xE000E180 Disable interrupts 0–31
NVIC_PRI0 0xE000E400 Priority for IRQs 0–3
NVIC_PRI1 0xE000E404 Priority for IRQs 4–7
One register per 4 IRQs
SCB AIRCR 0xE000ED0C Priority grouping
SYSPRI1 0xE000ED18 Priority for MemManage, BusFault, UsageFault
SYSPRI2 0xE000ED1C Priority for SVCall
SYSPRI3 0xE000ED20 Priority 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 Point Details
Interrupt sources 138 on TM4C123 (240 Cortex-M4 max)
Priority levels 8 (0 = highest, 7 = lowest)
Priority bits [7:5] of priority byte
Enable register NVIC_EN0–EN4 (one bit per IRQ)
Global enable IntMasterEnable() / IntMasterDisable()
ISR naming Must match startup_ccs.c vector table name
Clear order Clear peripheral flag FIRST, then return from ISR
Tail-chaining Automatic hardware optimisation for back-to-back ISRs

Next Chapter

Memory Protection Unit (MPU)

Share: