The ARM Cortex M0 is one of the most popular 32-bit microcontroller cores used in a wide range of embedded systems and IoT devices. Its low power consumption, small silicon footprint and ease of use make it an ideal choice for resource constrained applications. This article provides a step-by-step guide to programming the Cortex M0 in C and assembly language.
Introduction to the Cortex M0 Architecture
The Cortex M0 is a 32-bit RISC processor optimized for low-power embedded applications. Key features include:
- 3 stage pipeline allowing efficient execution of sequential instructions
- Operates at up to 50MHz clock speed
- Built-in memory protection unit and low power sleep modes
- Supports Thumb-2 instruction set with compact 16-bit and 32-bit instructions
- Includes SysTick timer, NVIC and debug support
The processor contains a single execution pipeline and does not include cache memory. It can be implemented as a standalone core or with an additional memory protection unit for supported operating systems.
Programming Model
The Cortex M0 programming model is very simple compared to more complex application processors. Key components include:
- Registers – 16 general purpose 32-bit registers R0-R15. R13-R15 have special functions.
- Memory – Linear byte addressed memory map. Flash and SRAM memory areas.
- Bus Interface – AHB-Lite interface connects processor to memories and peripherals.
- NVIC – Nested Vectored Interrupt Controller handles interrupt processing.
- SysTick – 24-bit system timer and counter for task scheduling.
- Debug – Debug Access Port and breakpoint unit for debugging.
All memory and peripherals are mapped into the linear memory address space. Special register addresses are used to access the core system registers and peripherals.
Toolchain Setup
To build Cortex M0 projects you will need:
- Compiler – For example GNU ARM GCC, ARM Compiler 6, IAR or Keil compiler
- Debugger/Programmer – Segger J-Link, ST-Link, CMSIS-DAP or other supported probe
- IDE/Editor – such as Eclipse, IAR EWARM, Keil uVision, VS Code
- SDK/Libraries – Include vendor specific HAL and peripherals libraries
The compiler and libraries provide support for the ARM Thumb-2 instruction set. The debugger connects to the Cortex SWD debug port to flash and debug programs. Many vendors provide low cost evaluation boards to get started.
Creating a Bare Metal C Project
A simple bare metal C project for Cortex M0 contains just a few files:
- Startup code – Low level SystemInit() and ResetHandler() functions
- Main C file – User application code with main() function
- Linker script – Links code with hardware memory map
- GCC Makefiles – Build configurations for compiling and linking
The startup code performs early system setup before calling main(). ResetHandler() is the first code executed on reset or power on. It initializes the stack pointer, BSS data section and calls SystemInit() and main().
SystemInit() configures the system clock, PLLs and external memories. It lives in a vendor specific Cortex M0 CMSIS library.
The main C file contains your main application code. This gets compiled into an object file and linked with other objects and libraries to produce the final executable.
The linker script describes the target memory layout. It combines code and data into the correct Flash and RAM memory regions.
Makefiles contain recipes for compiling source files and linking into the final binary. Overall this provides a simple project framework for building Cortex M0 applications.
Cortex M0 Register Usage
Here is how the 16 Cortex M0 registers are commonly used in C programs:
- R0-R3 – Used to pass function arguments and return values.
- R4-R7 – Temporary Usage inside functions.
- R8 – Holds the Stack Pointer (SP).
- R9 – Static Base Pointer (BP) for local stack frame.
- R10-R11 – More temporary registers preserved across function calls.
- R12 – Holds the Intra-Procedure-call scratch register (IP).
- R13 – Stack Pointer (SP). R13 contains current stack top address.
- R14 – Link Register (LR) holds return program counter on function calls.
- R15 – Program Counter (PC). Points to current instruction being executed.
Beyond R12 the remaining registers have dedicated roles. The stack grows down from high address to low. On function calls R4-R11, R14 and the LR are automatically pushed to stack. Understanding the purpose of each register is useful when writing assembly code.
Assembly Programming
The Cortex M0 assembly language is based on the Thumb-2 instruction set. Here is a simple asm program:
.global Reset_Handler
.section .text
Reset_Handler:
LDR R0, =0x40001000
LDR R1, [R0]
BX LR
This loads an address into R0, reads from that address into R1, then returns via the link register. Some key points:
- Instructions are 32-bit and 16-bit Thumb-2 format
- .text directive emits code into the text section
- Reset_Handler is the processor entry point on reset
- Registers R0-R15 are directly accessible
- LDR and STR load/store registers from memory
With assembly you have complete control over the hardware and peripherals. But higher level C code is generally easier to develop complex applications.
Interrupts and ISRs
The Cortex M0 NVIC allows handling interrupts in C with ISR functions:
void TIM2_IRQHandler(void)
{
// clear interrupt flag
TIM2->SR = ~TIM_SR_CC1IF;
// interrupt handling code
}
TIM2_IRQHandler will run when the timer 2 interrupt occurs. The SR clear flag must be used to reset the interrupt. Key steps:
- Enable interrupt in peripheral and NVIC
- Write ISR function with same name as the IRQn
- Clear interrupt flags
- Add ISR to the vector table
The vector table maps interrupts to functions and is normally defined in startup code. With interrupts Cortex M0 processing can be event driven.
Debugging with Breakpoints
One of the most useful tools for embedded debugging is source code breakpoints. These allow pausing the processor at a particular line of C code. For example:
int main(void)
{
int i = do_something();
sum += do_calculation(i); // set breakpoint here
return 0;
}
A debugger like GDB can set a software breakpoint on the sum line. Execution pauses when that line is hit. All processor registers and peripherals are then visible.
Breakpoints are set by replacing code with a special BKPT instruction. The debugger transparently handles this. External debug probes stop the core on the breakpoint and handle debug communications.
Debugging with Device Registers
The Cortex M0 debug access port (DAP) provides read/write access to many internal registers. Important ones include:
- DHCSR – Debug module control/status.
- DCRSR – Debug core register selector.
- DCRDR – Debug core register data.
- DEMCR – Debug exception and monitor control.
These allow examining core register contents while stepped or at breakpoints. The DHCSR.C_DEBUGEN bit enables halting debug. DEMCR enables the debug monitor for ARMv6 compatibility.
This low level debug access supplements breakpoint debugging by inspecting internal state. Combined they form a powerful Cortex M0 debugging solution.
Using the SysTick Timer
The SysTick timer is a simple countdown timer useful for creating periodic interrupts. For example to generate a 1ms tick:
void SysTick_Config(uint32_t ticks)
{
SysTick->LOAD = ticks - 1;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
int main(void) {
SysTick_Config(SystemCoreClock/1000);
while (1) {
// 1ms tick handler
}
}
This loads the reload value into LOAD, resets the current value with VAL, then enables the timer and interrupt. On each 1ms timeout the SysTick handler will run.
From there timers, delays, timeout handlers and more can be built. The SysTick peripheral is one of the most useful features of the Cortex M0 core.
Wakeup from Sleep Modes
To lower power the Cortex M0 supports sleep modes where the core halts until woken up. For example with a GPIO pin interrupt:
// enable wake on GPIO pin interrupt
PWR->CSR |= PWR_CSR_EWUP;
// enter sleep
SCB->SCR = SCB_SCR_SLEEPDEEP_Msk;
__WFI();
// wakes on GPIO interrupt
void EXTI0_IRQHandler()
{
// handle wake
}
The EWUP bit enables wake from IRQs. __WFI() halts the core. When the GPIO interrupt hits the handler runs waking the processor.
Sleep modes stop unused peripherals and the core allowing very low power. Combined with interrupts or DMA they enable efficient always-on sensing applications.
Conclusion
The Cortex M0 enables building lightweight embedded and IoT applications in both C and assembly language. Its Thumb-2 instruction set provides excellent code density in a simple but powerful RISC architecture. Features like debug, SysTick and sleep modes enable real world functionality.
With the core fundamentals covered here, you can now leverage the thousands of M0 based microcontrollers for your own projects.