Part 2 · C Meets the Metal33 min read
Clocks, Reset, and Timing
Every peripheral on a microcontroller runs off a clock, and every number you compute for a baud rate, a timer period, or a delay depends on knowing exactly what that clock is. Get it wrong and nothing crashes: the UART just prints garbage, the PWM runs at the wrong frequency, and delay_ms(1000) lasts ten seconds. In this lesson you will walk the STM32F407 clock tree, raise the core from the 16 MHz reset default to 168 MHz using the external crystal, learn how to tell why the chip last reset, and build the timekeeping primitives (millis(), delay_ms(), a cycle counter) that the rest of the course uses.
The clock tree
Section titled “The clock tree”A microcontroller does not have “a clock”. It has several clock sources, a PLL that multiplies one of them up, and a tree of dividers that feeds different buses at different speeds. On the STM32F407 the parts you need are:
+-----------+ HSI 16 MHz RC ---->| | | SW mux |----> SYSCLK (max 168 MHz) HSE 4-26 MHz -+-->| | | (8 MHz xtal on | +-----------+ v Discovery) | ^ +---------+ | | | AHB |---> HCLK: core, DMA, GPIO, SRAM, flash (max 168) | +---------+ | /1../512| +-->| PLL | +---------+ (or HSI) ------->| /M *N /P| | | /Q |--> 48 MHz +--> APB1 /1,2,4,8,16 --> PCLK1 (max 42 MHz) +---------+ USB/SDIO | timers on APB1: x2 if prescaler != 1 +--> APB2 /1,2,4,8,16 --> PCLK2 (max 84 MHz) timers on APB2: x2 if prescaler != 1HSI is an internal RC oscillator at 16 MHz. It needs no external parts and starts in microseconds, so the chip always boots from it. Its accuracy is about 1% at room temperature and drifts with temperature, which is good enough for blinking LEDs but marginal for UART and useless for USB.
HSE is the external oscillator: a crystal across the OSC_IN/OSC_OUT pins (8 MHz on the Discovery board) or an external clock signal fed into OSC_IN in bypass mode. A crystal is accurate to tens of parts per million. It takes a millisecond or so to start oscillating, which is why there is a ready flag you must wait on.
The main PLL takes HSI or HSE, divides it by M to get a VCO input between 1 and 2 MHz (ST recommends 2 MHz for lowest jitter, 1 MHz also works), multiplies it by N to get a VCO frequency between 100 and 432 MHz, then divides by P (2, 4, 6 or 8) for the system clock and by Q for the 48 MHz USB/SDIO clock.
SYSCLK is chosen by the SW field of RCC_CFGR from HSI, HSE or the PLL. It then passes through the AHB prescaler to become HCLK, which clocks the core, memories, DMA and the GPIO ports. Two further prescalers produce PCLK1 for the APB1 bus (max 42 MHz: USART2, I2C, TIM2 to TIM7, …) and PCLK2 for APB2 (max 84 MHz: USART1, SPI1, ADC, TIM1, TIM8, …).
The timer clock doubling rule
Section titled “The timer clock doubling rule”The timers do not simply run at PCLK. The rule on the STM32F4 is: if the APB prescaler is 1, the timers on that bus get PCLK; if the prescaler is anything else, the timers get 2 × PCLK. With the standard 168 MHz configuration (APB1 divided by 4 to 42 MHz), TIM2 to TIM7 therefore count at 84 MHz, not 42 MHz. Every “my timer runs at half speed” bug traces back to forgetting this. (Some later F4 parts add a TIMPRE bit that changes the rule to 4×; the F407 does not have it.)
Peripheral clock gating
Section titled “Peripheral clock gating”After reset, almost every peripheral clock is switched off. This saves power and is the single most common reason a beginner’s first driver does nothing. The enable bits live in RCC_AHB1ENR (offset 0x30), RCC_APB1ENR (0x40) and RCC_APB2ENR (0x44).
What happens when you write to a peripheral whose clock is off? On the STM32F4 the write is silently dropped and reads return zero. There is no fault. You can write GPIOD->MODER all day and the LED never lights, and if you look at it in the debugger it reads 0x00000000 no matter what you wrote. That last symptom is the tell: a register that reads back zero right after you wrote a non-zero value almost always means its clock is not enabled.
RCC->AHB1ENR |= (1u << 3); /* GPIODEN: bit 3, GPIOA is bit 0 ... GPIOI is bit 8 */(void)RCC->AHB1ENR; /* read back: forces the write to complete */GPIOD->MODER = (GPIOD->MODER & ~(3u << 24)) | (1u << 24);The read-back on the second line is not superstition. ST’s errata sheet documents that there is a delay of a couple of bus cycles between setting an enable bit and the peripheral actually being clocked. The core’s write to RCC goes into a write buffer and the very next instruction can reach the peripheral before the clock arrives, so the first access is lost. Reading the RCC register back stalls the core until the write has landed, which covers the delay. The (void) cast tells the compiler (and the reader) that the value is deliberately discarded, and because RCC->AHB1ENR is volatile, the read cannot be optimized away.
Running at 168 MHz, step by step
Section titled “Running at 168 MHz, step by step”The goal: 8 MHz HSE in, 168 MHz SYSCLK, 168 MHz HCLK, 42 MHz PCLK1, 84 MHz PCLK2, 48 MHz for USB.
The PLL numbers:
| Parameter | Value | Result |
|---|---|---|
PLLM | 8 | VCO input = 8 MHz / 8 = 1 MHz |
PLLN | 336 | VCO output = 1 MHz × 336 = 336 MHz |
PLLP | 2 (field value 00) | SYSCLK = 336 / 2 = 168 MHz |
PLLQ | 7 | USB clock = 336 / 7 = 48 MHz |
M = 4, N = 168 would give a 2 MHz VCO input and the same result with slightly lower jitter; both are common. We use 8/336 because the arithmetic reads cleanly.
The order of operations matters, and every step has a reason.
- Turn on HSE and wait for
HSERDY. The crystal needs time to start. Switching to it before it is stable gives the PLL garbage to lock to. - Set the voltage regulator scale. At 168 MHz the internal regulator must be in Scale 1 mode (
PWR_CRbitVOS). On the F405/F407 this is the reset value, but setting it explicitly documents the dependency and protects you if the code is ported to an F401 or F411, where the default differs. The PWR peripheral needs its clock (APB1ENRbit 28,PWREN) before you can write it. - Set the bus prescalers. AHB /1, APB1 /4, APB2 /2. Do this before switching SYSCLK so the buses are never driven above their maximum, not even for one cycle.
- Configure the PLL and enable it, wait for
PLLRDY. The PLL can only be configured while it is off.PLLRDYmeans the loop has locked. - Set flash wait states. Flash cannot be read in one cycle at 168 MHz. At 2.7 to 3.6 V the reference manual requires 5 wait states (
LATENCY = 5) at this frequency. Enable the prefetch buffer and the instruction and data caches (the “ART accelerator”) at the same time so the wait states mostly disappear. This must happen before the switch: if the core starts fetching at 168 MHz with 0 wait states, it reads corrupted instructions and crashes in creative ways. - Switch
SWto the PLL and wait forSWSto confirm.SWis the request;SWSis the hardware telling you the switch has happened. - Update
SystemCoreClock. Software that computes baud rates or SysTick reloads reads this variable instead of hard-coding 168000000.
The relevant register bits (RM0090):
| Register | Field | Bits | Meaning |
|---|---|---|---|
RCC_CR | HSEON / HSERDY | 16 / 17 | enable HSE / HSE stable |
RCC_CR | PLLON / PLLRDY | 24 / 25 | enable PLL / PLL locked |
RCC_PLLCFGR | PLLM | 5:0 | input divider, 2 to 63 |
RCC_PLLCFGR | PLLN | 14:6 | multiplier, 50 to 432 |
RCC_PLLCFGR | PLLP | 17:16 | 00=/2, 01=/4, 10=/6, 11=/8 |
RCC_PLLCFGR | PLLSRC | 22 | 0 = HSI, 1 = HSE |
RCC_PLLCFGR | PLLQ | 27:24 | USB divider, 2 to 15 |
RCC_CFGR | SW / SWS | 1:0 / 3:2 | 00 HSI, 01 HSE, 10 PLL |
RCC_CFGR | HPRE | 7:4 | 0xxx = /1 |
RCC_CFGR | PPRE1 / PPRE2 | 12:10 / 15:13 | 100 = /2, 101 = /4 |
FLASH_ACR | LATENCY | 2:0 | wait states |
FLASH_ACR | PRFTEN, ICEN, DCEN | 8, 9, 10 | prefetch, I-cache, D-cache |
PWR_CR | VOS | 14 | 1 = Scale 1 |
FLASH_ACR is at 0x40023C00 (offset 0 of the FLASH interface) and PWR_CR is at 0x40007000. Neither is in our register header, so the code defines them directly.
The clock module
Section titled “The clock module”#ifndef CLOCK_H#define CLOCK_H
#include <stdbool.h>#include <stdint.h>
extern uint32_t SystemCoreClock; /* HCLK in Hz, updated by clock_init_168mhz() */
bool clock_init_168mhz(void); /* false: HSE failed, still running on HSI */void clock_mco_enable(void); /* HSE on PA8, SYSCLK/4 on PC9 */void systick_init_1ms(void);uint32_t millis(void);void delay_ms(uint32_t ms);bool time_reached(uint32_t deadline);void dwt_init(void);uint32_t dwt_cycles(void);void system_reset(void);
#endif#include <stdbool.h>#include <stdint.h>#include "stm32f407_regs.h"#include "clock.h"
/* Registers not in stm32f407_regs.h */#define FLASH_ACR (*(volatile uint32_t *)0x40023C00u)#define PWR_CR (*(volatile uint32_t *)0x40007000u)#define DEMCR (*(volatile uint32_t *)0xE000EDFCu)#define DWT_CTRL (*(volatile uint32_t *)0xE0001000u)#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004u)
#define RCC_CR_HSEON (1u << 16)#define RCC_CR_HSERDY (1u << 17)#define RCC_CR_PLLON (1u << 24)#define RCC_CR_PLLRDY (1u << 25)#define RCC_PLLCFGR_FIELDS ((0x3Fu << 0) | (0x1FFu << 6) | (3u << 16) | \ (1u << 22) | (0xFu << 24))#define RCC_PLLCFGR_PLLSRC_HSE (1u << 22)#define RCC_CFGR_SW_Msk (3u << 0)#define RCC_CFGR_SW_PLL (2u << 0)#define RCC_CFGR_SWS_Msk (3u << 2)#define RCC_CFGR_SWS_PLL (2u << 2)#define RCC_CFGR_PRE_Msk ((0xFu << 4) | (7u << 10) | (7u << 13))#define RCC_CFGR_PPRE1_DIV4 (5u << 10)#define RCC_CFGR_PPRE2_DIV2 (4u << 13)#define RCC_APB1ENR_PWREN (1u << 28)#define PWR_CR_VOS (1u << 14)#define FLASH_ACR_LATENCY_Msk (7u << 0)#define FLASH_ACR_LATENCY_5WS (5u << 0)#define FLASH_ACR_PRFTEN (1u << 8)#define FLASH_ACR_ICEN (1u << 9)#define FLASH_ACR_DCEN (1u << 10)#define SYSTICK_CTRL_ENABLE (1u << 0)#define SYSTICK_CTRL_TICKINT (1u << 1)#define SYSTICK_CTRL_CLKSOURCE (1u << 2)#define DEMCR_TRCENA (1u << 24)#define DWT_CTRL_CYCCNTENA (1u << 0)#define SCB_AIRCR_VECTKEY (0x5FAu << 16)#define SCB_AIRCR_PRIGROUP_Msk (7u << 8)#define SCB_AIRCR_SYSRESETREQ (1u << 2)
uint32_t SystemCoreClock = 16000000u; /* HSI after reset */static volatile uint32_t s_ms; /* written by SysTick ISR */
bool clock_init_168mhz(void){ /* 1. Start the crystal and wait, with a timeout. */ RCC->CR |= RCC_CR_HSEON; for (uint32_t t = 0; !(RCC->CR & RCC_CR_HSERDY); t++) { if (t > 500000u) { RCC->CR &= ~RCC_CR_HSEON; /* give up, stay on HSI */ return false; } }
/* 2. Regulator Scale 1 (needed above 144 MHz). */ RCC->APB1ENR |= RCC_APB1ENR_PWREN; (void)RCC->APB1ENR; PWR_CR |= PWR_CR_VOS;
/* 3. AHB /1, APB1 /4 (42 MHz), APB2 /2 (84 MHz). */ RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_PRE_Msk) | RCC_CFGR_PPRE1_DIV4 | RCC_CFGR_PPRE2_DIV2;
/* 4. PLL: 8 MHz / 8 * 336 / 2 = 168 MHz, /7 = 48 MHz. */ RCC->PLLCFGR = (RCC->PLLCFGR & ~RCC_PLLCFGR_FIELDS) | (8u << 0) /* PLLM */ | (336u << 6) /* PLLN */ | (0u << 16) /* PLLP = /2 */ | RCC_PLLCFGR_PLLSRC_HSE | (7u << 24); /* PLLQ */ RCC->CR |= RCC_CR_PLLON; while (!(RCC->CR & RCC_CR_PLLRDY)) { }
/* 5. Flash: 5 wait states, prefetch and caches on. */ FLASH_ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_LATENCY_5WS; while ((FLASH_ACR & FLASH_ACR_LATENCY_Msk) != FLASH_ACR_LATENCY_5WS) { }
/* 6. Switch SYSCLK to the PLL and wait for the hardware to confirm. */ RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW_Msk) | RCC_CFGR_SW_PLL; while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL) { }
/* 7. Tell the rest of the firmware. */ SystemCoreClock = 168000000u; return true;}Walking through the parts that are easy to get wrong:
- The HSE timeout. A crystal can fail: a cracked part, a bad solder joint, wrong load capacitors. If you wait forever, the board is a brick that does not even blink. Returning
falseand staying on HSI lets the firmware report the fault. The count of 500000 is a crude loop bound; at a few cycles per iteration and 16 MHz it is on the order of a hundred milliseconds, far longer than a healthy crystal needs. PLLCFGRis read-modify-write. Its reset value is0x24003010, and bit 29 is a reserved bit that reads as 1. The rule for reserved bits is “keep at reset value”, so we clear only the five fields we own and OR in new values rather than assigning a constant.- Reading
FLASH_ACRback. The reference manual says to check that the new latency has been taken into account by reading the register. Usually it is immediate, but the check costs nothing. SWS, notSW. Reading backSWonly tells you what you asked for.SWStells you what the clock mux actually selected.
There is no timeout on PLLRDY and SWS. Once HSE is running, those always complete in microseconds on working silicon. Production code often adds timeouts anyway and treats expiry as a fatal hardware fault.
SystemCoreClock
Section titled “SystemCoreClock”ST’s CMSIS startup code defines a global uint32_t SystemCoreClock and a function that recomputes it from the RCC registers. We keep the variable and set it directly. The point is that drivers never hard-code a frequency:
SysTick->LOAD = SystemCoreClock / 1000u - 1u; /* good */SysTick->LOAD = 167999u; /* breaks the day someone changes the clock */The same applies to peripheral clocks. A UART driver needs PCLK1, not SYSCLK. A robust codebase exposes pclk1_hz() and pclk2_hz() helpers computed from the prescaler fields; to keep this lesson short, the later lessons simply document that they assume PCLK1 = 42 MHz and PCLK2 = 84 MHz.
Verifying the clock with MCO
Section titled “Verifying the clock with MCO”You configured the clock. How do you know it worked? Measure it. The STM32F407 can route internal clocks to two pins, MCO1 on PA8 and MCO2 on PC9, both as alternate function 0. A scope or frequency counter on those pins tells you the truth regardless of what your code believes.
#define RCC_CFGR_MCO_Msk ((3u << 21) | (7u << 24) | (7u << 27) | (3u << 30))#define RCC_CFGR_MCO1_HSE (2u << 21) /* MCO1 source = HSE */#define RCC_CFGR_MCO1PRE_1 (0u << 24) /* no division */#define RCC_CFGR_MCO2_SYSCLK (0u << 30) /* MCO2 source = SYSCLK */#define RCC_CFGR_MCO2PRE_4 (6u << 27) /* divide by 4 */
void clock_mco_enable(void){ RCC->AHB1ENR |= (1u << 0) | (1u << 2); /* GPIOA, GPIOC */ (void)RCC->AHB1ENR;
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_MCO_Msk) | RCC_CFGR_MCO1_HSE | RCC_CFGR_MCO1PRE_1 | RCC_CFGR_MCO2_SYSCLK | RCC_CFGR_MCO2PRE_4;
/* PA8 and PC9: alternate function (MODER = 10), AF0 is the reset value. */ GPIOA->MODER = (GPIOA->MODER & ~(3u << 16)) | (2u << 16); GPIOA->OSPEEDR |= (3u << 16); /* very high speed */ GPIOC->MODER = (GPIOC->MODER & ~(3u << 18)) | (2u << 18); GPIOC->OSPEEDR |= (3u << 18);}After clock_init_168mhz(), PA8 shows 8 MHz (the raw crystal) and PC9 shows 168 / 4 = 42 MHz. If PC9 shows 4 MHz, you are still on HSI. If PA8 is dead, the crystal is not running. The reference manual recommends selecting the MCO sources right after reset, before enabling HSE and the PLL, because changing the mux while clocks run can glitch the output, which is why main calls this function first.
Reset sources and RCC_CSR
Section titled “Reset sources and RCC_CSR”A firmware engineer’s first question about a field failure is “why did it reset?”. The STM32F407 records the answer in RCC_CSR (offset 0x74, address 0x40023874):
| Bit | Flag | Set by |
|---|---|---|
| 31 | LPWRRSTF | illegal entry to Stop/Standby (low-power management reset) |
| 30 | WWDGRSTF | window watchdog |
| 29 | IWDGRSTF | independent watchdog |
| 28 | SFTRSTF | software reset (AIRCR.SYSRESETREQ) |
| 27 | PORRSTF | power-on or power-down reset |
| 26 | PINRSTF | the NRST pin went low |
| 25 | BORRSTF | brown-out: supply dipped below the BOR threshold |
| 24 | RMVF | write 1 to clear all the flags above |
Two subtleties. First, the flags accumulate until cleared. If you never write RMVF, a board that was powered on, then watchdog-reset, shows both PORRSTF and IWDGRSTF. Read the register once at boot, save it, then clear it. Second, PINRSTF is set by almost every reset, because on the STM32 internal reset sources also drive the NRST pin low. So the logic is “check the specific flags first, and treat PINRSTF alone as a genuine external reset”. Likewise a power-on sets PORRSTF together with BORRSTF and PINRSTF.
The reset sources themselves:
- Power-on / power-down (POR/PDR): supply crosses the POR threshold.
- Brown-out (BOR): supply sags below a programmable threshold set in the option bytes. Without BOR enabled, a slowly drooping battery can leave the core running at a voltage where flash reads are unreliable.
- NRST pin: the reset button on the Discovery board, or the debugger.
- Software reset: your code writes
SYSRESETREQinSCB->AIRCR. - Watchdogs: the IWDG or WWDG expired because the firmware stopped feeding them.
- Low-power reset: entering Standby or Stop when option bytes forbid it.
What happens on each, how to log the cause across reboots, and how to recover from faults belongs to Watchdogs, faults, and recovering from them. Here you need the mechanism for a software reset:
void system_reset(void){ __asm volatile ("dsb" ::: "memory"); /* finish pending writes */ SCB->AIRCR = SCB_AIRCR_VECTKEY | (SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) | SCB_AIRCR_SYSRESETREQ; __asm volatile ("dsb" ::: "memory"); for (;;) { /* reset takes a few cycles */ }}AIRCR ignores any write whose upper half is not the key 0x05FA, a guard against stray writes resetting the chip. We preserve the PRIGROUP field so the reset request does not also change interrupt priority grouping in the cycles before reset takes effect. The trailing loop matters: the reset is not instantaneous, and without it the function would return and execute whatever follows.
SysTick: a 1 ms heartbeat
Section titled “SysTick: a 1 ms heartbeat”SysTick is a 24-bit down-counter built into every Cortex-M core. When it reaches zero it reloads from LOAD and, if enabled, raises the SysTick exception. It is the standard time base for bare-metal code and for most RTOSes.
For a 1 ms tick at 168 MHz from the processor clock, LOAD = 168000 - 1. The minus one is because the counter counts LOAD, LOAD-1, ..., 1, 0 before reloading, which is LOAD + 1 clock cycles. The maximum LOAD is 0xFFFFFF (16777215), so the longest single period at 168 MHz is about 99.9 ms.
void systick_init_1ms(void){ SysTick->LOAD = SystemCoreClock / 1000u - 1u; SysTick->VAL = 0u; /* any write clears the counter */ SysTick->CTRL = SYSTICK_CTRL_CLKSOURCE /* processor clock, not HCLK/8 */ | SYSTICK_CTRL_TICKINT /* raise the exception */ | SYSTICK_CTRL_ENABLE;}
void SysTick_Handler(void){ s_ms++;}
uint32_t millis(void){ return s_ms;}s_ms is volatile because it is modified by an interrupt handler and read by main-loop code; without it, the compiler may hoist the read out of a polling loop. A 32-bit aligned load is a single ldr instruction on Cortex-M, so main can never see half an update. (If you made the counter 64-bit, that would no longer be true; Interrupts done right covers this.) The handler name SysTick_Handler must match the name in your startup file’s vector table, which it does if you use the startup code from Startup code and the vector table.
SysTick’s priority defaults to 0, the highest. That is fine for a counter increment. When you have real interrupts, you usually want SysTick at a low priority; the priority register for system exceptions is covered in Interrupts done right.
Wraparound-safe time comparison
Section titled “Wraparound-safe time comparison”millis() wraps from 0xFFFFFFFF to 0 after 2^32 ms, about 49.7 days. That sounds like “never” until a device in the field has been running for seven weeks and every timeout in the system fires at once, or never fires again. The wrong way:
/* BROKEN near wraparound */uint32_t deadline = millis() + 100u;while (millis() < deadline) {}If millis() is 0xFFFFFFF0, then deadline wraps to 0x54. millis() < 0x54 is false immediately, so the wait ends instantly. Or worse, flip the numbers and it waits 49 days.
The fix uses modular arithmetic. Unsigned subtraction in C is defined to wrap modulo 2^32, so now - start is the true elapsed time as long as fewer than 2^32 ms have passed:
void delay_ms(uint32_t ms){ uint32_t start = millis(); while ((uint32_t)(millis() - start) < ms) { }}
/* True once `deadline` is now or in the past. Valid for deadlines within +/- 24.8 days of the current time. */bool time_reached(uint32_t deadline){ return (int32_t)(millis() - deadline) >= 0;}delay_ms compares an elapsed duration, which is always safe. time_reached compares against an absolute deadline: the difference now - deadline is reinterpreted as signed, so “slightly before” (a huge unsigned number like 0xFFFFFFFE) becomes -2, and “slightly after” becomes a small positive number. Converting an out-of-range uint32_t to int32_t is implementation-defined in C, but GCC, Clang and every Arm compiler define it as two’s complement wrap, and this idiom is used in the Linux kernel (time_after()) for exactly this reason.
With 1 ms resolution, delay_ms(1) waits somewhere between 0 and 1 ms depending on where in the current tick you started. If you need “at least N ms”, wait N + 1.
Why busy-wait delays are wrong in production
Section titled “Why busy-wait delays are wrong in production”delay_ms() is fine for bring-up and for a few milliseconds during initialization (waiting for a sensor to power up). In application logic it is a design error:
- The CPU does nothing useful while it spins. A button press, a UART byte, a sensor deadline: all ignored unless an interrupt handles them.
- It burns power. The core runs at full speed instead of sleeping in
WFI. - It composes badly. Two features that each “wait 500 ms” cannot run at the same time.
The production pattern is the one in main below: record a deadline, go do other work, check time_reached() each pass through the loop. State machines and event-driven firmware turns this into a full architecture.
The even worse cousin is the calibrated loop, for (volatile uint32_t i = 0; i < 1000000; i++). Its duration depends on clock speed, flash wait states, optimization level and compiler version. It is acceptable for the first blinky and nowhere else.
Cycle-accurate timing with DWT
Section titled “Cycle-accurate timing with DWT”Milliseconds are too coarse to measure how long a function takes. The Cortex-M4 has a Data Watchpoint and Trace unit with a free-running 32-bit cycle counter, CYCCNT, that increments once per core clock. At 168 MHz that is 5.95 ns resolution, and it wraps every 2^32 / 168e6 ≈ 25.6 seconds.
| Register | Address | Use |
|---|---|---|
DEMCR | 0xE000EDFC | bit 24 TRCENA enables the DWT and ITM blocks |
DWT_CTRL | 0xE0001000 | bit 0 CYCCNTENA starts the counter |
DWT_CYCCNT | 0xE0001004 | the count |
void dwt_init(void){ DEMCR |= DEMCR_TRCENA; /* power up the trace blocks first */ DWT_CYCCNT = 0u; DWT_CTRL |= DWT_CTRL_CYCCNTENA;}
uint32_t dwt_cycles(void){ return DWT_CYCCNT;}Measure with the same wraparound-safe subtraction: uint32_t dt = dwt_cycles() - t0; is correct for any interval under 25.6 s. Two caveats. The measurement includes interrupts that fired in between, so a measurement that is occasionally ten times longer usually means SysTick or another ISR ran. And the DWT is a debug component: on the Cortex-M7 you must also unlock it through a lock access register, and on Cortex-M0/M0+ there is no CYCCNT at all. Simulators often do not model it cycle-accurately, so treat Renode numbers as approximate.
The complete example
Section titled “The complete example”The program below brings everything together. It records the reset cause, raises the clock to 168 MHz, outputs MCO for verification, blinks the green LED with a non-blocking deadline, measures a piece of code with the DWT, and triggers a software reset when you press the user button. After the reset, the blue LED shows that the last reset was a software reset.
#include <stdbool.h>#include <stdint.h>#include "stm32f407_regs.h"#include "clock.h"
#define RCC_CSR_RMVF (1u << 24)#define RCC_CSR_SFTRSTF (1u << 28)#define RCC_CSR_IWDGRSTF (1u << 29)
#define LED_GREEN 12u#define LED_ORANGE 13u#define LED_RED 14u#define LED_BLUE 15u
volatile uint32_t g_reset_flags; /* inspect with gdb: p/x g_reset_flags */volatile uint32_t g_loop_cycles; /* inspect with gdb: p g_loop_cycles */
static void led_on(uint32_t pin) { GPIOD->BSRR = 1u << pin; }static void led_off(uint32_t pin) { GPIOD->BSRR = 1u << (pin + 16u); }
static uint32_t checksum(const uint8_t *p, uint32_t n){ uint32_t sum = 0; for (uint32_t i = 0; i < n; i++) { sum = ((sum << 1) | (sum >> 31)) ^ p[i]; } return sum;}
int main(void){ /* Read and clear the reset cause before anything else. */ g_reset_flags = RCC->CSR; RCC->CSR |= RCC_CSR_RMVF;
clock_mco_enable(); /* before HSE/PLL: avoids glitches */ bool hse_ok = clock_init_168mhz(); systick_init_1ms(); dwt_init();
/* LEDs PD12..PD15 as outputs; PA0 (button) is input after reset. */ RCC->AHB1ENR |= (1u << 0) | (1u << 3); (void)RCC->AHB1ENR; GPIOD->MODER = (GPIOD->MODER & ~(0xFFu << 24)) | (0x55u << 24);
if (!hse_ok) { led_on(LED_RED); /* crystal failed: running on HSI */ } if (g_reset_flags & RCC_CSR_SFTRSTF) { led_on(LED_BLUE); } if (g_reset_flags & RCC_CSR_IWDGRSTF) { led_on(LED_ORANGE); }
uint32_t next_toggle = millis(); bool green = false;
for (;;) { if (time_reached(next_toggle)) { next_toggle += 500u; /* drift-free: based on the deadline */ green = !green; if (green) { led_on(LED_GREEN); } else { led_off(LED_GREEN); } }
uint32_t t0 = dwt_cycles(); (void)checksum((const uint8_t *)0x08000000u, 1024u); /* first 1 KB of flash */ g_loop_cycles = dwt_cycles() - t0;
if (GPIOA->IDR & 1u) { /* user button, active high */ delay_ms(20u); /* crude debounce, fine here */ system_reset(); } }}Points worth noticing:
- Reset flags first. Nothing else in
maintouchesRCC_CSR, but in a larger system something might reset or clear state, so capture the evidence before doing anything else. next_toggle += 500urather thannext_toggle = millis() + 500u. If the loop is late by 3 ms one time, the next deadline is still on the original 500 ms grid, so the blink does not drift over hours.BSRRfor LEDs. Writing a 1 to bitnsets pinn, writing a 1 to bitn + 16clears it, with no read-modify-write. GPIO in depth explains why this matters.- Reading flash as data. The checksum reads the first kilobyte of the image through the data cache, which makes the cycle count a useful experiment: change
LATENCYor disableDCENand watchg_loop_cycleschange.
Build it with startup.c, link.ld and the Makefile from Blinky from nothing, adding clock.c to the source list. Load it, let it run a few seconds, halt it in gdb and inspect:
(gdb) p/x g_reset_flags$1 = 0x14000000(gdb) p g_loop_cycles$2 = 7194(gdb) p SystemCoreClock$3 = 1680000000x14000000 is SFTRSTF | PINRSTF (bits 28 and 26) after pressing the user button: the software reset also pulsed the NRST pin, exactly as described above. The cycle count you see will differ with compiler version and optimization; what matters is that it is stable from run to run. Under Renode the reset flags and cycle counts depend on how completely the model implements them, so verify these two on hardware.
Common clock bugs
Section titled “Common clock bugs”Most clock bugs produce no error; something else just behaves wrongly. Recognise the symptoms:
| Symptom | Likely cause |
|---|---|
| UART prints garbage at every baud rate | Code assumes a PCLK that is not the real one (forgot clock init, or assumed APB1 = 84 MHz) |
| Timer runs at exactly half or double the expected rate | Forgot the timer clock doubling rule |
| Everything runs about 3× too slow, consistently | HSE_VALUE mismatch: code assumes a 25 MHz crystal (common in ST examples), board has 8 MHz |
| Hangs at boot, never blinks | Waiting forever on HSERDY with no crystal fitted, or on a peripheral flag with the peripheral clock off |
| Random crashes right after clock switch | Flash latency set after the switch, or not at all |
| USB enumerates on one PC, not another | 48 MHz clock not exact (PLLQ wrong) or running from HSI |
The HSE_VALUE row deserves emphasis. Vendor code computes the PLL settings from a macro that states the crystal frequency. ST’s generic examples often default it to 25 MHz while the Discovery board has an 8 MHz crystal. The PLL then locks at a third of the intended frequency and everything still “works”, just slowly and with wrong baud rates. When in doubt, look at MCO with a scope.
Common mistakes
Section titled “Common mistakes”- Writing to a peripheral before enabling its RCC clock. Fix: enable the clock first and read the enable register back.
- Assuming the timer clock equals PCLK1. Fix: apply the doubling rule; with APB1 /4, APB1 timers run at 84 MHz.
- Switching SYSCLK to the PLL before raising flash wait states. Fix: set
FLASH_ACRlatency before the switch, lower it only after slowing down. - Waiting on
HSERDYwith no timeout. Fix: bound the loop and fall back to HSI with an error indication. - Assigning
RCC->PLLCFGRa constant and clobbering reserved bits. Fix: clear and set only the fields you own. - Comparing timestamps with
<on rawmillis()values. Fix: subtract first,(uint32_t)(now - start) < durationor(int32_t)(now - deadline) >= 0. - Using
delay_ms()inside application logic. Fix: use deadlines checked from the main loop. - Never clearing
RCC_CSR. Fix: read once at boot, store, then writeRMVF.
Exercises
Section titled “Exercises”- Change
PLLNto 240 andPLLQto 5 with everything else the same. Compute SYSCLK and the USB clock by hand, then predict the MCO2 frequency on PC9. Is the USB clock still exactly 48 MHz? (Answer: SYSCLK 120 MHz, USB 48 MHz, MCO2 30 MHz.) - Remove the
FLASH_ACRwrite fromclock_init_168mhz()and run on hardware. Describe what happens. Then setLATENCYto 5 but clearICENandDCENand compareg_loop_cycleswith the cached version. - Disassemble
clock.owitharm-none-eabi-objdump -d clock.oand findSysTick_Handler. Count its instructions and confirm it does a load, an add, and a store tos_ms. Then removevolatilefroms_ms, rebuilddelay_msat-O2, and find in the disassembly the infinite loop the compiler produced. - Write
uint32_t pclk1_hz(void)that computes PCLK1 fromSystemCoreClockand thePPRE1field ofRCC_CFGR, anduint32_t tim_apb1_hz(void)that applies the doubling rule. Test it by hand forPPRE1values0b000,0b100and0b101. - Write a host test for
time_reached(): replacemillis()with a fake that returns a settable value, and check deadlines just before, at, and after the 32-bit wraparound. Make the test fail first by using the broken<comparison.
Check yourself
Section titled “Check yourself”Question 1:With SYSCLK = 168 MHz, AHB /1 and APB1 /4, what frequency does TIM2 count at with its prescaler at 0?
PCLK1 is 42 MHz, and because the APB1 prescaler is not 1 the timers on APB1 get twice PCLK1, which is 84 MHz.
Question 2:A driver writes GPIOD->MODER and reads it back in the debugger as 0x00000000. What is the most likely cause?
Writes to an unclocked peripheral are silently ignored and reads return zero, so a register that refuses to hold a value points at a missing clock enable.
Question 3:millis() returns 0xFFFFFF00 and code computes deadline = millis() + 0x200. Which deadline-reached test correctly evaluates to false at this moment?
deadline wraps to 0x100, so a plain >= is already true. The signed difference is 0xFFFFFE00, which is -512 as int32_t, correctly meaning the deadline is 512 ms away.
Question 4:Why must flash wait states be increased before switching SYSCLK to 168 MHz rather than after?
Flash needs several cycles per access at 168 MHz. Running even a few instructions at full speed with too few wait states returns corrupted instruction words.
Question 5:After pressing the reset button on a running board, which RCC_CSR flags do you expect to be set (assuming they were cleared at the previous boot)?
The NRST pin sets PINRSTF. Internal resets such as software reset also pulse NRST, so they set PINRSTF together with their own flag; PINRSTF alone means an external reset.
Key takeaways
Section titled “Key takeaways”- The chip boots on the 16 MHz HSI. Reaching 168 MHz takes HSE, Scale 1 regulator, bus prescalers, PLL lock, 5 flash wait states, then the switch, in that order.
- Every peripheral needs its RCC enable bit set first. An unclocked peripheral ignores writes and reads zero, with no fault.
- Timers on an APB bus run at 2 × PCLK whenever that bus prescaler is not 1.
RCC_CSRrecords why the chip reset. Read it once at boot, save it, clear it withRMVF.- Compare times by subtracting first:
(uint32_t)(now - start) < duration. Raw comparisons break at wraparound. DWT->CYCCNTgives single-cycle timing on the M3/M4/M7; enable it throughDEMCR.TRCENA.- Verify clocks with a scope on MCO, not by trusting your configuration code.
Next: GPIO in depth takes the pins you have been toggling and explains the electrical reality behind them, from push-pull outputs to external interrupts.