CMSIS Explained: Core Headers, SystemInit and Portable Cortex-M Code
Before CMSIS, every vendor shipped its own header files with its own names for the same Cortex-M core registers. Code that enabled an interrupt on one chip had to be rewritten for another. ARM introduced the Cortex Microcontroller Software Interface Standard (CMSIS) to fix this. Today almost every Cortex-M device header, vendor library and RTOS is built on it, so understanding its files and functions lets you read any vendor's example code and write drivers that move between chips with little change.
This article explains the parts of CMSIS, what each file in a CMSIS-based project does, and shows a complete timer interrupt program for the NXP LPC1768 written using only CMSIS and the device header, without any vendor driver library.
The CMSIS Components
CMSIS is a collection of separate specifications. You will use CMSIS-Core in every project; the others are optional.
| Component | What it provides |
|---|---|
| CMSIS-Core (M) | Core register definitions, NVIC and SysTick functions, intrinsics, startup and system file templates |
| CMSIS-DSP | Optimised filters, FFT, matrix and statistics functions for Cortex-M |
| CMSIS-RTOS2 | A standard RTOS API, implemented by Keil RTX and by a FreeRTOS wrapper |
| CMSIS-Driver | Generic peripheral driver interfaces for UART, SPI, I2C, USB and Ethernet |
| CMSIS-SVD | XML description of every peripheral register, used by debuggers to show register views |
| CMSIS-DAP | Firmware and protocol for low-cost USB debug probes |
| CMSIS-Pack | The packaging format Keil and other tools use to install device support |
Files in a CMSIS-Core Project
| File (LPC1768 example) | Supplied by | Role |
|---|---|---|
| core_cm3.h | ARM | Core registers (NVIC, SCB, SysTick) and inline functions for the Cortex-M3 |
| cmsis_compiler.h, cmsis_gcc.h, cmsis_armclang.h | ARM | Compiler-specific intrinsics and attribute macros |
| LPC17xx.h | NXP | IRQ numbers, peripheral register structures and base addresses; includes core_cm3.h |
| system_LPC17xx.c | NXP | SystemInit() for clock and PLL setup, SystemCoreClock variable, SystemCoreClockUpdate() |
| startup_LPC17xx.s or .c | NXP / toolchain | Vector table, Reset_Handler, weak default handlers |
Your application includes only the device header, LPC17xx.h. Before including the core header it defines __NVIC_PRIO_BITS (5 for the LPC17xx) and the IRQ number enumeration, so CMSIS functions such as NVIC_SetPriority know how many priority bits the chip really implements.
The reset sequence is fixed by convention: the startup file's Reset_Handler calls SystemInit() to set up clocks, initialises the C runtime (copying .data and zeroing .bss), then calls main(). The global SystemCoreClock holds the CPU frequency in hertz. If your code changes the PLL later, call SystemCoreClockUpdate() so that the variable stays correct.
Most Useful CMSIS-Core Functions
| Function or intrinsic | Purpose |
|---|---|
| NVIC_EnableIRQ(IRQn), NVIC_DisableIRQ(IRQn) | Enable or disable one interrupt in the NVIC |
| NVIC_SetPriority(IRQn, prio) | Set priority; lower number means more urgent |
| NVIC_SetPendingIRQ, NVIC_ClearPendingIRQ | Trigger or cancel an interrupt from software |
| SysTick_Config(ticks) | Set up the SysTick timer to interrupt every ticks cycles |
| __enable_irq(), __disable_irq() | Clear or set PRIMASK |
| __get_PRIMASK(), __set_BASEPRI(x) | Read and write special registers |
| WFI(), WFE(), __NOP() | Wait for interrupt, wait for event, no-operation |
| DSB(), ISB(), __DMB() | Memory and instruction barriers |
| REV(x), CLZ(x) | Byte-reverse a word, count leading zeros |
Example: Timer0 Interrupt Blink on LPC1768
This program toggles LED1 of an mbed LPC1768 board (pin P1.18) every 250 ms from the Timer0 interrupt. Timer0's clock defaults to one quarter of the CPU clock on the LPC1768 (PCLKSEL0 reset value), and the code derives everything from SystemCoreClock, so it remains correct if SystemInit is configured for a different frequency.
#include "LPC17xx.h"
#define LED1 (1u << 18) /* P1.18 on mbed LPC1768 */
volatile uint32_t tick_count = 0;
/* Name must match the vector table entry in the startup file */
void TIMER0_IRQHandler(void)
{
if (LPC_TIM0->IR & 1u) { /* MR0 interrupt? */
LPC_TIM0->IR = 1u; /* clear by writing 1 */
tick_count++;
if (LPC_GPIO1->FIOPIN & LED1)
LPC_GPIO1->FIOCLR = LED1;
else
LPC_GPIO1->FIOSET = LED1;
}
}
static void timer0_init(uint32_t period_ms)
{
uint32_t pclk = SystemCoreClock / 4u; /* PCLKSEL0 reset = CCLK/4 */
LPC_SC->PCONP |= (1u << 1); /* power Timer0 */
LPC_TIM0->TCR = 0x02; /* reset */
LPC_TIM0->PR = (pclk / 1000u) - 1u; /* 1 ms per TC tick */
LPC_TIM0->MR0 = period_ms - 1u;
LPC_TIM0->MCR = 0x03; /* interrupt + reset on MR0 */
NVIC_SetPriority(TIMER0_IRQn, 10);
NVIC_EnableIRQ(TIMER0_IRQn);
LPC_TIM0->TCR = 0x01; /* start */
}
int main(void)
{
uint32_t snapshot;
SystemCoreClockUpdate(); /* make sure the variable is valid */
LPC_GPIO1->FIODIR |= LED1;
timer0_init(250);
while (1) {
/* Read a shared variable safely */
__disable_irq();
snapshot = tick_count;
__enable_irq();
if (snapshot >= 40u) { /* after 10 seconds, sleep between ticks */
__WFI();
}
}
}
What Makes This Code Portable
The interrupt handler is an ordinary C function. Its name, TIMER0_IRQHandler, matches the weak symbol in the vector table of the CMSIS startup file, so the linker replaces the default infinite-loop handler with yours. No special keyword is needed, unlike the __irq attribute required on ARM7.
NVIC_SetPriority(TIMER0_IRQn, 10) shifts the value 10 into the upper five bits of the priority byte because the device header declared five implemented bits. On a chip with four bits the same call still works, just with a different valid range. The critical section around tick_count uses the CMSIS intrinsics instead of inline assembly, and __WFI() stops the core until the next interrupt to save power.
Only three lines are LPC17xx specific: the PCONP bit, the PCLK divider assumption and the Timer0 register names. Porting to another Cortex-M3 or M4 means changing those lines, while the NVIC, intrinsics and handler structure stay the same.
Common Mistakes
- Misspelling the handler name, for example Timer0_IRQHandler; the code compiles but the default handler runs and the program hangs in an endless loop.
- Using SystemCoreClock before SystemInit or after changing the PLL without calling SystemCoreClockUpdate.
- Mixing an old CMSIS core header with a newer device header, causing duplicate or missing definitions.
- Passing an already shifted priority value such as 0xA0 to NVIC_SetPriority; CMSIS shifts it again.
- Forgetting that NVIC_EnableIRQ only enables the NVIC side; the peripheral's own interrupt enable bit must also be set.
Summary
CMSIS-Core gives every Cortex-M chip the same names for core registers, NVIC functions and intrinsics. A project consists of the ARM core header, the vendor device header, the system file with SystemInit and SystemCoreClock, and a startup file with the vector table. Write handlers with the exact names from the startup file, configure interrupts with NVIC functions, and derive timing from SystemCoreClock to keep drivers portable.