Lesson 1 of 5 · 20 min
Cortex-M and the memory map
On an Arduino, the core hides almost everything between your code and the silicon. On an STM32 you meet the machine directly, and the single most useful mental model is this: the whole chip is one flat address space, and every peripheral is just a few memory locations. This course uses the NUCLEO-L476RG throughout: an STM32L476RG with a Cortex-M4F core running up to 80 MHz, 1 MB of flash and 128 KB of SRAM. Everything in this lesson applies to a NUCLEO-F103RB (Cortex-M3) too, but its GPIO registers are laid out differently, which is why we standardise on the L476.
ARM, ST and who makes what
ARM does not sell chips. It licenses a CPU design, the Cortex-M core, and companies such as ST wrap it with their own flash, SRAM, clocks and peripherals. So an STM32 is two things glued together:
- The core (Cortex-M4): instruction set (Thumb-2), registers R0 to R15, the NVIC interrupt controller, SysTick, and the debug hardware. Identical across all vendors.
- The vendor silicon: GPIO, timers, UART, ADC and so on. These differ between ST, NXP and others, and even between STM32 families.
That split explains why a document called the Cortex-M4 Generic User Guide describes the NVIC, while a separate Reference Manual (RM0351 for the L476) describes GPIO and timers. You will use both.
One address space
A 32-bit core can address 2^32 bytes = 4 GiB. ARM fixes the rough layout, ST fills in the details:
| Region | Start address | Size on L476RG | What lives there |
|---|---|---|---|
| Flash | 0x0800 0000 | 1 MB | Your program and constants |
| SRAM2 | 0x1000 0000 | 32 KB | Fast RAM, kept on in standby if enabled |
| SRAM1 | 0x2000 0000 | 96 KB | Variables, heap, stack |
| Peripherals (APB1) | 0x4000 0000 | Timers 2 to 7, USART2, I2C | |
| Peripherals (APB2) | 0x4001 0000 | SYSCFG, EXTI, TIM1, USART1 | |
| Peripherals (AHB1) | 0x4002 0000 | DMA, RCC | |
| Peripherals (AHB2) | 0x4800 0000 | GPIOA to GPIOH | |
| Core peripherals | 0xE000 E000 | SysTick, NVIC, SCB |
Check the arithmetic: 1 MB is 0x10 0000 bytes, so the last flash byte is 0x0800 0000 + 0x10 0000 - 1 = 0x080F FFFF. SRAM1 is 96 KB = 0x1 8000 bytes, so it ends at 0x2001 7FFF. A peripheral register has no special instruction: GPIOA output data register sits at 0x4800 0000 + 0x14 = 0x4800 0014, and writing a word there changes pin voltages. In C that is a pointer dereference, which is exactly what the CMSIS header hides behind the name GPIOA->ODR.
The vector table and the reset sequence
The first words of the program are not instructions. They are a table of addresses, the vector table. Each entry is 4 bytes, so exception number n lives at offset 4n:
| Offset | Entry |
|---|---|
| 0x00 | Initial main stack pointer value |
| 0x04 | Reset handler address |
| 0x08 | NMI |
| 0x0C | HardFault |
| 0x10, 0x14, 0x18 | MemManage, BusFault, UsageFault |
| 0x3C | SysTick |
| 0x40 onward | Peripheral interrupts, IRQ0, IRQ1, and so on |
When power arrives or NRST is released, the core does only two things before running any of your code: it loads the word at address 0x0000 0000 into the stack pointer, then loads the word at 0x0000 0004 into the program counter. On the L476 with BOOT0 low, flash at 0x0800 0000 is also aliased to address 0 so those reads hit your table. A typical value for the stack pointer is 0x2001 8000, the first address past the end of SRAM1, because the Cortex-M stack grows downward: the first push writes to 0x2001 7FFC.
The reset vector has its lowest bit set, for example 0x0800 0189. That bit does not select an address, it says "Thumb state". The code really starts at 0x0800 0188. Cortex-M runs only Thumb, so a vector with bit 0 clear would raise a fault.
What a startup file does
The reset handler is the first C-like code that runs, and it has chores that a desktop OS would normally do for you:
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;
void Reset_Handler(void) {
uint32_t *src = &_sidata; // initial values stored in flash
for (uint32_t *dst = &_sdata; dst < &_edata; ) {
*dst++ = *src++; // copy .data from flash to SRAM
}
for (uint32_t *dst = &_sbss; dst < &_ebss; ) {
*dst++ = 0; // zero .bss
}
SystemInit(); // FPU enable, vector table offset
__libc_init_array(); // C library and C++ constructors
main();
for (;;) {} // main must never return
}
Why the copy? int count = 5; must be writable, so it has to live in SRAM, but SRAM is empty at power-up. The linker therefore stores the initial value in flash and the startup code copies it across. Uninitialised globals and static variables are promised to start at zero, which is the .bss loop. The startup file also fills the vector table, and gives every interrupt a weak default handler that loops forever, so you override one just by defining a function with the right name. The linker script supplies the symbols such as _sdata and says where flash and RAM are: FLASH at 0x08000000 length 1024K, RAM at 0x20000000 length 96K.
ST-Link and SWD
The Nucleo board carries a second chip, the ST-Link, which is a USB-to-debug adapter. It talks to the target over SWD (Serial Wire Debug): just two signals, SWDIO and SWCLK, plus ground and reset. Through those two wires the debugger can halt the core, read and write any address in the map (including flash programming), set hardware breakpoints, and single-step. The ST-Link also provides a virtual serial port and, optionally, the SWO trace pin you will use for printf in lesson 5.
Blue pins: analog inputs. Orange pins: digital I/O.
Check yourself
At reset, which word does the Cortex-M core load into the program counter?
Check yourself
Why does Reset_Handler copy a block from flash to SRAM before calling main?