Skip to content

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.

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 != 1

HSI 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 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.)

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.

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:

ParameterValueResult
PLLM8VCO input = 8 MHz / 8 = 1 MHz
PLLN336VCO output = 1 MHz × 336 = 336 MHz
PLLP2 (field value 00)SYSCLK = 336 / 2 = 168 MHz
PLLQ7USB 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.

  1. 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.
  2. Set the voltage regulator scale. At 168 MHz the internal regulator must be in Scale 1 mode (PWR_CR bit VOS). 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 (APB1ENR bit 28, PWREN) before you can write it.
  3. 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.
  4. Configure the PLL and enable it, wait for PLLRDY. The PLL can only be configured while it is off. PLLRDY means the loop has locked.
  5. 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.
  6. Switch SW to the PLL and wait for SWS to confirm. SW is the request; SWS is the hardware telling you the switch has happened.
  7. Update SystemCoreClock. Software that computes baud rates or SysTick reloads reads this variable instead of hard-coding 168000000.

The relevant register bits (RM0090):

RegisterFieldBitsMeaning
RCC_CRHSEON / HSERDY16 / 17enable HSE / HSE stable
RCC_CRPLLON / PLLRDY24 / 25enable PLL / PLL locked
RCC_PLLCFGRPLLM5:0input divider, 2 to 63
RCC_PLLCFGRPLLN14:6multiplier, 50 to 432
RCC_PLLCFGRPLLP17:1600=/2, 01=/4, 10=/6, 11=/8
RCC_PLLCFGRPLLSRC220 = HSI, 1 = HSE
RCC_PLLCFGRPLLQ27:24USB divider, 2 to 15
RCC_CFGRSW / SWS1:0 / 3:200 HSI, 01 HSE, 10 PLL
RCC_CFGRHPRE7:40xxx = /1
RCC_CFGRPPRE1 / PPRE212:10 / 15:13100 = /2, 101 = /4
FLASH_ACRLATENCY2:0wait states
FLASH_ACRPRFTEN, ICEN, DCEN8, 9, 10prefetch, I-cache, D-cache
PWR_CRVOS141 = 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.

clock.h
#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
clock.c
#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 false and 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.
  • PLLCFGR is read-modify-write. Its reset value is 0x24003010, 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_ACR back. 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, not SW. Reading back SW only tells you what you asked for. SWS tells 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.

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.

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.

clock.c (continued)
#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.

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):

BitFlagSet by
31LPWRRSTFillegal entry to Stop/Standby (low-power management reset)
30WWDGRSTFwindow watchdog
29IWDGRSTFindependent watchdog
28SFTRSTFsoftware reset (AIRCR.SYSRESETREQ)
27PORRSTFpower-on or power-down reset
26PINRSTFthe NRST pin went low
25BORRSTFbrown-out: supply dipped below the BOR threshold
24RMVFwrite 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 SYSRESETREQ in SCB->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:

clock.c (continued)
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 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.

clock.c (continued)
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.

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:

clock.c (continued)
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.

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.

RegisterAddressUse
DEMCR0xE000EDFCbit 24 TRCENA enables the DWT and ITM blocks
DWT_CTRL0xE0001000bit 0 CYCCNTENA starts the counter
DWT_CYCCNT0xE0001004the count
clock.c (continued)
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 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.

main.c
#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 main touches RCC_CSR, but in a larger system something might reset or clear state, so capture the evidence before doing anything else.
  • next_toggle += 500u rather than next_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.
  • BSRR for LEDs. Writing a 1 to bit n sets pin n, writing a 1 to bit n + 16 clears 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 LATENCY or disable DCEN and watch g_loop_cycles change.

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 = 168000000

0x14000000 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.

Most clock bugs produce no error; something else just behaves wrongly. Recognise the symptoms:

SymptomLikely cause
UART prints garbage at every baud rateCode 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 rateForgot the timer clock doubling rule
Everything runs about 3× too slow, consistentlyHSE_VALUE mismatch: code assumes a 25 MHz crystal (common in ST examples), board has 8 MHz
Hangs at boot, never blinksWaiting forever on HSERDY with no crystal fitted, or on a peripheral flag with the peripheral clock off
Random crashes right after clock switchFlash latency set after the switch, or not at all
USB enumerates on one PC, not another48 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.

  • 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_ACR latency before the switch, lower it only after slowing down.
  • Waiting on HSERDY with no timeout. Fix: bound the loop and fall back to HSI with an error indication.
  • Assigning RCC->PLLCFGR a constant and clobbering reserved bits. Fix: clear and set only the fields you own.
  • Comparing timestamps with < on raw millis() values. Fix: subtract first, (uint32_t)(now - start) < duration or (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 write RMVF.
  1. Change PLLN to 240 and PLLQ to 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.)
  2. Remove the FLASH_ACR write from clock_init_168mhz() and run on hardware. Describe what happens. Then set LATENCY to 5 but clear ICEN and DCEN and compare g_loop_cycles with the cached version.
  3. Disassemble clock.o with arm-none-eabi-objdump -d clock.o and find SysTick_Handler. Count its instructions and confirm it does a load, an add, and a store to s_ms. Then remove volatile from s_ms, rebuild delay_ms at -O2, and find in the disassembly the infinite loop the compiler produced.
  4. Write uint32_t pclk1_hz(void) that computes PCLK1 from SystemCoreClock and the PPRE1 field of RCC_CFGR, and uint32_t tim_apb1_hz(void) that applies the doubling rule. Test it by hand for PPRE1 values 0b000, 0b100 and 0b101.
  5. Write a host test for time_reached(): replace millis() 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.

Question 1:With SYSCLK = 168 MHz, AHB /1 and APB1 /4, what frequency does TIM2 count at with its prescaler at 0?

Question 2:A driver writes GPIOD->MODER and reads it back in the debugger as 0x00000000. What is the most likely cause?

Question 3:millis() returns 0xFFFFFF00 and code computes deadline = millis() + 0x200. Which deadline-reached test correctly evaluates to false at this moment?

Question 4:Why must flash wait states be increased before switching SYSCLK to 168 MHz rather than after?

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 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_CSR records why the chip reset. Read it once at boot, save it, clear it with RMVF.
  • Compare times by subtracting first: (uint32_t)(now - start) < duration. Raw comparisons break at wraparound.
  • DWT->CYCCNT gives single-cycle timing on the M3/M4/M7; enable it through DEMCR.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.