Interrupt Controller (NVIC) — Tiva C
- Eslam El Hefny
- Tutorials, Tiva c
- April 11, 2025
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— useHWREG(NVIC_SYS_PRI3), notIntPrioritySet. - 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 |