Embedded HandbookApps
All sections › I. Basics
STM32F103, STM32F407, STM32F401, STM32F072 · level basic · 18 min read
Open in the interactive handbook

1. The microcontroller as such

Before blinking an LED, it helps to understand what exactly you are controlling. This section is about what lies beneath all the others: the core, the memory, the instruction set. Without it the later sections would have to be taken on faith.

Terms used in the documentation

Term Meaning
MCU (microcontroller unit) a microcontroller: core, memory and peripherals on one die
core, processor the core, the processing part
peripheral a peripheral block: a timer, an ADC, a UART
RISC (reduced instruction set computer) a set of simple instructions of equal length
architecture (ARMv7-M) the architecture: a specification of instructions and the execution model
licensee a manufacturer that has bought a licence for the core
core registers (R0–R12, SP, LR, PC, xPSR) the registers of the core
memory map which addresses belong to what
memory management unit (MMU) translates virtual addresses into physical ones; Cortex-M has none
bit-band region, alias region the bit-banding region and its bit-by-bit mirror
Thumb-2 instruction set the Thumb-2 instruction set: 16- and 32-bit instructions mixed together
pipeline: fetch, decode, execute the stages of the instruction pipeline

1.1 What it is in the first place

A microcontroller (MCU, Micro Controller Unit) is a digital chip that gathers on one die everything needed to work on its own:

The key difference from a processor: a processor usually has no Flash and RAM on the die — they are placed outside to make manufacturing cheaper and simpler. A microcontroller is self-sufficient: apply power and it runs.

The second difference matters more to a programmer than the first — the memory management unit (MMU). A processor running Linux or Windows translates addresses: each program has its own virtual address space, the MMU maps it on the fly onto physical memory and keeps the program out of other programs' pages. Processes, memory protection and swapping rest on this. A microcontroller has no MMU: an address in the program is the physical address, and the program sees all the memory at once. Some Cortex-M have an MPU — it forbids access to regions but does not translate addresses (section 11а). That is why ordinary Linux does not run on a Cortex-M (section 191.1); what runs on it is "bare-metal" firmware or an RTOS.

The first single-chip microcontrollers appeared in the 1970s: the TMS1000 from Texas Instruments and the 8048 from Intel.

Two popular core architectures

ARM RISC-V
expansion Advanced RISC Machine reduced instruction set computer
licence proprietary, royalties to ARM Holdings open and free
niche from MCUs to servers and the Apple M1 low-cost MCUs, custom cores

Both are RISC: the instructions are encoded so that they decode simply and execute quickly.


1.2 ARM, Cortex and who does what

It is easy to get confused here, so let us lay it out in layers.

The ARM architecture (ARMv6-M, ARMv7-M, ARMv8-M…) is a specification: the instruction set, the execution model, the memory organisation, the cost of instructions in cycles. Not hardware, but a document. A compiler that can generate code for an architecture can do so for all processors that implement it.

A Cortex-M core is a concrete implementation of the architecture, a ready-made block to be embedded in a die. The Cortex-M3, for example, is designed to ARMv7-M, and the Cortex-M4 to ARMv7E-M: the same architecture plus an extension with DSP instructions. People often simply write "the M4 is ARMv7-M", but the letter E in the name stands precisely for the added digital signal processing instructions.

A microcontroller is the core plus what the silicon vendor has added around it: Flash, RAM, peripherals, buses. This is the work of ST, NXP, TI, Mikron, Milandr and dozens of others. ARM Holdings itself manufactures nothing — it licenses the architecture and the cores.

How a Cortex-M3 core relates to a microcontroller built on it A Cortex-M3 core inside a microcontroller built on it

An important consequence: having the core does not guarantee having the feature. The Cortex-M3 is designed with bit-banding support, but implementing it is the vendor's decision. The STM32F1 implements it, while the Cortex-M7 has none at all. Always check the reference manual of the specific MCU, not the description of the core.,

Three Cortex families

Family Letter What for
Cortex-A Application heavy computing, operating systems (Linux, Android): phones, tablets, single-board computers
Cortex-M eMbedded embedded tasks: IoT, motor control, meters, household appliances, medical equipment
Cortex-R Real-time systems where fault tolerance and a guaranteed response time matter

We are interested in Cortex-M.

Cortex-M3 versus Cortex-M4: what moving to F4 really gives you

The two reference platforms of this handbook are the STM32F103 (Cortex-M3) and the STM32F407/F401 (Cortex-M4). The difference between them is not cosmetic.

Cortex-M3 (F1) Cortex-M4 (F4)
architecture ARMv7-M ARMv7E-M
clock frequency up to 72 MHz up to 180 MHz (F427/F429/F446/F469), 168 MHz (F405/F407), 84–100 MHz (F401/F410/F411)
FPU no present on all STM32F4, single precision
DSP instructions no yes: single-cycle MAC, saturating arithmetic, SIMD
pipeline 3 stages 3 stages
bit-banding yes yes
Flash accelerator prefetch buffer ART Accelerator — an instruction cache, zero wait states
GPIO port bus APB2 AHB1
DMA 7+5 channels, no FIFO 2 controllers × 8 streams, FIFO, burst

What this means in practice.

Floating point. On the M3 every float multiplication is a call to a library function taking dozens of cycles. On the M4 with an FPU it is one instruction. For controllers, filters and sensor reading conversion the difference is an order of magnitude. But the FPU has to be enabled — it is off after reset — and the compiler has to be told -mfpu=fpv4-sp-d16 -mfloat-abi=hard, otherwise you get an M4 that computes like an M3.

DSP instructions. Single-cycle multiply-accumulate, saturating arithmetic (the result does not overflow but stops at the limit) — the things the CMSIS-DSP library is built on: FFT, FIR and IIR filters, matrices. The M3 can compute the same, but several times slower.

ART Accelerator. At 168–180 MHz the Flash physically cannot deliver instructions fast enough — wait states are needed. ART caches instructions and branches so that during linear execution there are practically no waits. That is why the F407 at 168 MHz gives about 210 DMIPS — the same 1.25 DMIPS per megahertz as the F103 at 72 MHz (90 DMIPS): performance grows with frequency even though the Flash needs five wait states. Without ART, at the same 168 MHz the core would spend most of its time waiting for Flash.

⚠️ The main trap of moving F1 → F4 lies not in the core but in the peripherals: different clock buses (AHB1ENR instead of APB2ENR), different base addresses, a different GPIO register model, a different DMA. Code that works with the core — NVIC, SysTick, bit-banding — moves over without changes. Code that works with the peripherals is rewritten almost entirely. The details for each peripheral are in the corresponding sections.


1.3 Core registers

Cortex-M is a load/store machine: arithmetic is performed only on registers, and memory is accessed by separate load and store instructions.

ARM Cortex-M core registers Cortex-M core registers

Register Purpose
R0–R12 general purpose; the compiler uses some of them for its own needs
R13 (SP) the stack pointer, banked — changes when the CPU mode changes
R14 (LR) the return address from a subroutine
R15 (PC) the program counter
xPSR the program status flags

A "banked" SP means that there are physically two stack pointers — MSP and PSP. Exception handlers always run on MSP, while the main code runs on MSP after reset or on PSP if the SPSEL bit is set in the CONTROL register. Privilege is set by a separate bit of the same register (nPRIV), and the choice of stack does not depend on it. Task switching in an RTOS is built on the two stacks: each task has its own PSP, while interrupts run on the shared MSP. More in section 26.

What it looks like in real life. Simple C code:

uint8_t a, b, c;
a = 3;
b = 2;
c = a * b;

compiles into roughly this (Thumb, no optimisation, for Cortex-M4):

movs   r3, #3         ; занести 3 в r3
strb   r3, [r7, #7]   ; сохранить r3 в переменную a
movs   r3, #2         ; занести 2 в r3
strb   r3, [r7, #6]   ; сохранить r3 в переменную b
ldrb   r2, [r7, #7]   ; загрузить a в r2
ldrb   r3, [r7, #6]   ; загрузить b в r3
smulbb r3, r2, r3     ; умножить, результат в r3
strb   r3, [r7, #5]   ; сохранить результат в c

smulbb multiplies the lower 16-bit halves; it is one of the Cortex-M4 DSP instructions. The Cortex-M3 (F103) has no such instruction; in its place there will be mul r3, r2, r3.

Not a single operation is performed "in memory" — everything goes through the registers. Hence a practical conclusion: the fewer unnecessary memory accesses in a hot loop, the faster the code.


1.4 The memory map

A Cortex-M address is 32 bits wide, so there are exactly 2³² = 4 294 967 296 addresses — that is 4 GB: the last byte has the number 0xFFFF FFFF, nothing more fits in 32 bits. ARM divides this space into regions the same way on all Cortex-M from any vendor: code at the bottom, SRAM from 0x2000 0000, peripherals from 0x4000 0000, core registers at the top. Only core code is portable because of this — more on it right below the table. The peripherals inside their region differ for each vendor and even for each STM32 family: a port, a timer or a UART does not move from F1 to F4 without changes (section 1.10).

Cortex-M address space The fixed map of the Cortex-M address space

Range Size What is there
0x0000 0000 … 512 MB code: the vector table, the initial stack pointer, the program
0x2000 0000 … 512 MB internal SRAM
0x4000 0000 … 512 MB peripherals
0x6000 0000 … 2 GB external memory through FSMC/EMI
0xE000 0000 … 512 MB registers of the Cortex core itself (NVIC, SysTick, debug)

The core registers sit at fixed addresses on all Cortex-M — which is why code that works with the NVIC or SysTick moves over without changes.

The code region on STM32

The code region in STM32 Layout of the code region on STM32

Internal Flash on all STM32 lives from address 0x0800 0000. At the same time it is duplicated ("aliased") from address 0x0000 0000 — depending on the boot setting. One and the same function is accessible both as 0x0800 16DC and as 0x0000 16DC.

Then come two service regions:

How much SRAM there really is

The SRAM region begins at 0x2000 0000, but the real end depends on the amount of memory in your chip. For an STM32F103RB with 20 KB:

20 КБ = 20 × 1024 = 0x5000 байт
последний адрес = 0x2000 0000 + 0x4FFF = 0x2000 4FFF

An access beyond this boundary raises a Bus Fault exception.


1.5 Bit-banding

The problem

Changing a single bit of a variable is something you do all the time:

uint8_t temp = 0;
temp |= 0x4;     // установить бит 2
temp &= ~0x4;    // сбросить бит 2

Each such line turns into three instructions — load, modify, store:

ldrb   r3, [r7, #7]
orr.w  r3, r3, #4
strb   r3, [r7, #7]

Hence two troubles. The first is wasted cycles. The second is more serious: between the read and the write an interrupt or a task switch may squeeze in and change the same word — and your write will overwrite the other change. A classic race.

The solution

Bit-banding maps each bit of a special memory region onto a whole word in another, "mirror" region. Write the word — exactly one bit changes, in one operation, without races.

Mapping bits into the alias region Mapping the bits of address 0x2000 0000 into the bit-band region

The conversion formula:

alias_адрес = база_alias + (смещение_в_области × 32) + (номер_бита × 4)

The two regions on Cortex-M3/M4:

What bit-band region alias region
SRAM 0x2000 0000–0x200F FFFF (1 MB) 0x2200 0000–0x23FF FFFF (32 MB)
peripherals 0x4000 0000–0x400F FFFF (1 MB) 0x4200 0000–0x43FF FFFF (32 MB)

The 1 MB ↔ 32 MB ratio is easy to explain: each bit of the source region corresponds to a whole 32-bit word in the alias.

Memory map with the bit-banding regions The bit-banding regions on the overall memory map

Macros

#define BITBAND_SRAM_BASE   0x20000000u
#define ALIAS_SRAM_BASE     0x22000000u
#define BITBAND_SRAM(a, b)  ((ALIAS_SRAM_BASE + (((uint32_t)&(a)) - BITBAND_SRAM_BASE) * 32u + ((b) * 4u)))

#define BITBAND_PERI_BASE   0x40000000u
#define ALIAS_PERI_BASE     0x42000000u
#define BITBAND_PERI(a, b)  ((ALIAS_PERI_BASE + (((uint32_t)(a)) - BITBAND_PERI_BASE) * 32u + ((b) * 4u)))

Blinking the LED on PC13 (STM32F103) without a read-modify-write operation:

volatile uint32_t *pc13 = (uint32_t *)BITBAND_PERI(&GPIOC->ODR, 13);
*pc13 = 1;      /* одна запись — один бит */

Let us check by hand: the GPIOC base on F1 is 0x4001 1000, the ODR offset is 0x0C.

смещение   = 0x4001100C − 0x40000000 = 0x1100C
alias      = 0x42000000 + 0x1100C × 32 + 13 × 4 = 0x4222 01B4

⚠️ Addresses from other people's examples do not carry over. Guides often show the calculation for GPIOA->ODR at address 0x4002 0014 — that is the STM32F4. On the STM32F1 the port bases are different (GPIOA = 0x4001 0800), and the ODR offset is 0x0C, not 0x14. Copy such an address to F1 and you will miss the register. Always compute the address from &GPIOx->ODR, not from a constant.

⚠️ The bit number depends on which address you substituted. For a byte address the bit numbers run 0…7, for a word address 0…31. Both give the same alias address (4 bytes × 32 = 32 bits × 4), but mixing up the ranges is easy: byte 0x400F FFFF with "bit 31" runs past the end of the alias region.

⚠️ Bit-banding does not do away with the read. The bus itself performs a write to the alias as a read-modify-write of the whole register. This protects against interrupts, but not against the peripheral itself. In a register of flags that are cleared by writing a one (EXTI->PR), writing a one to a single bit reads all the pending flags and writes them back as ones — all waiting lines get cleared, the same mistake as with EXTI->PR |= (section 7.9). In a register where a flag is cleared by writing a zero (TIMx->SR), a flag that the hardware raises between the read and the write gets erased. Such flags are cleared by writing the mask directly: EXTI->PR = EXTI_PR_PR3, TIM2->SR = ~TIM_SR_UIF.

⚠️ Bit-banding is not everywhere. It is absent on the Cortex-M0/M0+, absent on the Cortex-M7 (F7, H7) and on ARMv8-M cores — the Cortex-M33 and newer (L5, U5, H5, WBA). Code with bit-banding will not port to these families; there a bit can be changed atomically through BSRR for pins, LDREX/STREX (except M0/M0+) or a critical section.


1.6 Thumb-2 and alignment

Why Thumb was needed

ARM historically has 32-bit instructions: a rich set and good performance, but every instruction takes 4 bytes of Flash. Flash is die area, which means price and power consumption.

Thumb is a 16-bit set, a subset of the most frequently used instructions. They are transparently expanded into 32-bit ones at execution, with no loss of speed. Thumb code takes about 65 % of the size of ARM code. The price is reduced capabilities: only branches can be conditional, and many instructions see only half of the registers.

Thumb-2 mixes 16- and 32-bit instructions in one mode: code density like Thumb, completeness of the set almost like ARM. The Cortex-M3/M4/M7 support it in full, and the M4/M7 add instructions for floating-point numbers.

Alignment

Aligned and unaligned memory access Aligned and unaligned access

Aligned access means that a 32-bit variable sits at an address that is a multiple of 4 (0x2000 0000, 0x2000 0004…), a 16-bit one at a multiple of 2. Early ARMs could not do otherwise, and this fragmented memory: unused bytes were left between variables.

The Cortex-M3/M4/M7 support unaligned access: the LDR/STR instructions and their halfword variants also work with an address that is not a multiple of the size. The compiler still aligns variables and structure fields. Data is packed tightly only in a packed structure (__attribute__((packed))), for example when parsing a packet. An unaligned access costs more — the bus makes two accesses — and is not atomic. The Cortex-M0/M0+ have no such access at all, there you get a HardFault; on the M3/M4 it can be turned into a fault with the UNALIGN_TRP bit in SCB->CCR.

⚠️ Unaligned access is allowed not for all instructions — only LDR, LDRT, LDRH, LDRHT, LDRSH, LDRSHT, STR, STRT, STRH, STRHT. The others give a UsageFault exception on an unaligned address. Remember separately: peripheral registers almost always require whole-word access. For the STM32F1 GPIO, halfword or byte access is forbidden explicitly in the documentation.


1.7 The pipeline

Executing an instruction is not one step: first it has to be fetched from memory (fetch), then decoded (decode), and only then executed (execute). If this is done sequentially, the processor sits idle most of the time.

Three-stage pipeline A three-stage instruction pipeline

The pipeline executes the stages of different instructions in parallel: while one is being executed, the next is being decoded, and a third is being fetched.

Core Stages
Cortex-M0+ 2 (fewer stages mean less energy, which matters for low-power MCUs)
Cortex-M0/M3/M4 3
Cortex-M7 6

The price of the pipeline is branches. When the program takes a conditional branch, the instructions already loaded into the pipeline turn out to be unneeded and are discarded (the "branch shadow"). Hence a practical conclusion for hot loops: fewer unpredictable branches mean denser pipeline work.


1.8 What it is written in

The C language, created by Dennis Ritchie in the 1970s, remains the main tool: more than 80 % of microcontroller system code is written in it.,

Why exactly C:

  1. Compiled and efficient — machine code, high performance, compact programs. In embedded systems, where resources are tightly limited, this is decisive.
  2. Low-level — it lets you work at the register level and optimise for specific hardware.
  3. Portable — code moves between platforms.

A study by the universities of Minho, Nova de Lisboa and Coimbra covering 27 languages showed that C is the most energy-efficient and the fastest in execution time.

C++ is used where graphics and a complex interface are needed — ST's TouchGFX library for high-resolution displays is built on it.


1.9 The same Cortex-M3, but domestic: 1986VE9x

A useful exercise for understanding where the core ends and the vendor begins: the Russian microcontroller 1986VE9x (Milandr) is built on the same ARM Cortex-M3 core as the STM32F1.

1986VE9x STM32F103C8
core Cortex-M3 Cortex-M3
clock frequency up to 80 MHz up to 72 MHz
Flash 128 KB 64/128 KB
RAM 32 KB 20 KB
ADC 2 × 12 bit, up to 0.5 Msps, 16 channels 2 × 12 bit, 1 Msps
DAC 2 × 12 bit not on the C8
oscillators HSI 8 MHz, LSI 40 kHz, HSE 2…16 MHz, LSE 32 kHz HSI 8 MHz, LSI 40 kHz, HSE 4…16 MHz, LSE 32.768 kHz
supply 2.2–3.6 V, built-in 1.8 V regulator 2.0–3.6 V
backup domain yes, with RTC yes, with RTC

Almost everything matches except the names of the peripheral registers: on the Milandr part pin configuration is set by separate registers OE, FUNC, ANALOG, PULL, PWR, not by the MODE/CNF pair as on the STM32F1.

What follows from this. Everything that belongs to the core — registers R0–R15, the memory map, NVIC, SysTick, Thumb-2, the pipeline — carries over one to one. Everything that belongs to the peripherals does not carry over at all. The boundary between "knowledge about ARM" and "knowledge about STM32" runs exactly here.


1.10 Pitfalls

Code from F1 "almost" ports to F4, but the peripherals stay silent. The core is the same, but the clock buses, base addresses, GPIO and DMA differ: core code (NVIC, SysTick) carries over, peripheral code is rewritten (1.2).

A write to an address from someone else's example does nothing. Address 0x4002 0014 is GPIOA->ODR on F4; on F1 the port base and offset are different. The address is taken as the expression &GPIOx->ODR, not as a number (1.5).

Bit-banding goes beyond the region. The bit number for a byte address is 0…7, for a word 0…31; mix them up and you miss the alias region (1.5).

Code with bit-banding builds on F7/H7 but does not work. To the compiler the alias address is an ordinary number, but the Cortex-M0/M0+, M7 and M33 have no such region: the write goes nowhere or ends in a bus fault (1.5).

A UsageFault out of nowhere. Unaligned access is allowed only for LDR/STR and their variants; peripheral registers only as whole words, on F1 GPIO halfword and byte access is forbidden (1.6).


1.11 Summary

Next: section 2 "The STM32 portfolio and choosing a chip" — how to choose the right one from over two thousand microcontrollers and what the letters in the part number mean.

Open in the interactive handbook