The Clock System
Every single operation inside your microcontroller happens on the tick of a clock. Reading a GPIO pin, sending a byte over UART, converting an analog voltage — all of it is synchronized to clock edges. If you get the clocks wrong, nothing works right, and the symptoms are baffling.
Why Clocks Matter
Imagine you're talking to a friend over the phone, but your phone plays the audio at 1.5x speed. You'd hear garbled nonsense. That's exactly what happens when two devices try to communicate at different clock speeds — the bits arrive at the wrong time, and the receiver interprets garbage.
Here's the harsh reality of what happens when clocks are misconfigured:
| Symptom | Likely Clock Problem |
|---|---|
| UART prints garbage characters | Baud rate is wrong because SYSCLK isn't what you think |
| SPI communication fails randomly | APB clock doesn't divide cleanly to SPI baud rate |
| Timers run at wrong speed | Timer clock prescaler assumes a different input frequency |
| USB doesn't enumerate | USB requires exactly 48 MHz — no tolerance |
| Everything works but is painfully slow | Running on default 64 MHz HSI instead of 480 MHz PLL |
| Program crashes on startup | PLL misconfigured, CPU tries to run at invalid frequency |
🧠 Think About It: USB requires a clock accurate to 0.25%. The internal HSI oscillator drifts by about 1%. That's why USB simply will not work without an external crystal or a specially calibrated clock. Precision matters.
Clock Sources
STM32 chips have multiple clock sources, each with different characteristics. You choose based on your accuracy, speed, and power requirements.
HSI — High-Speed Internal
The HSI is an on-chip RC oscillator, typically 64 MHz on H7 series (8 or 16 MHz on other families). It's the default clock source — your chip runs on HSI from the moment it powers up, before any of your code executes.
- Accuracy: ~1% drift over temperature and voltage changes
- Advantage: Always available, no external components needed
- Limitation: Too imprecise for USB, unreliable for high baud rate UART over long periods
HSE — High-Speed External
The HSE uses an external crystal or oscillator, commonly 8 MHz or 25 MHz. This is your precision clock source.
- Accuracy: Better than 0.01% (50 ppm or less for a typical crystal)
- Advantage: Precise enough for USB, reliable UART at any baud rate
- Limitation: Requires a crystal soldered to the board (most dev boards include one)
LSI — Low-Speed Internal
A low-power internal oscillator running at roughly 32 kHz. It's imprecise but cheap in terms of power.
- Primary use: Independent Watchdog Timer (IWDG)
- Accuracy: ~5-10% — quite rough, but fine for "reset if firmware is stuck" timers
LSE — Low-Speed External
An external 32.768 kHz crystal — that oddly specific number divides evenly into 1 second (32768 = 2^15), making it perfect for timekeeping.
- Primary use: Real-Time Clock (RTC)
- Accuracy: Excellent, usually within a few seconds per month
- Limitation: Requires a dedicated crystal (most boards include one)
💡 Fun Fact: The 32.768 kHz frequency isn't arbitrary — it's exactly 2^15 Hz. A simple 15-stage binary counter can divide it down to exactly 1 Hz. This is the same crystal used in wristwatches since the 1970s.
The PLL — Frequency Multiplier
Your crystal runs at 25 MHz, but your CPU wants 480 MHz. How do you get there? With a Phase-Locked Loop (PLL) — a circuit that multiplies a low input frequency up to a high output frequency.
The PLL has three stages:
Input (HSE) Divide (DIVM) Multiply (DIVN) Divide (DIVP) Output
25 MHz → /5 = 5 MHz → ×192 = 960 MHz → /2 = 480 MHz → SYSCLK
The intermediate VCO (Voltage-Controlled Oscillator) frequency must stay within a valid range (usually 192–960 MHz for H7). You pick the dividers to land within that range while hitting your target output.
🧠 Think About It: Why not just use a 480 MHz crystal directly? Because high-frequency crystals are expensive, fragile, and consume more power. It's far more practical to use a cheap low-frequency crystal and multiply it up electronically.
The Clock Tree
The PLL output doesn't go directly to every peripheral. Instead, it feeds through a tree of dividers (prescalers) that provide appropriate frequencies to different bus domains.
┌──────────────────────────────────────────┐
│ Clock Tree (H743) │
└──────────────────────────────────────────┘
HSI (64 MHz) ──┐
├──→ [PLL1] ──→ SYSCLK (up to 480 MHz)
HSE (25 MHz) ──┘ │
├──→ AHB Bus (up to 240 MHz)
│ │
│ ├──→ APB1 (up to 120 MHz)
│ │ └─ UART, I2C, TIM2-7
│ │
│ ├──→ APB2 (up to 120 MHz)
│ │ └─ SPI1, TIM1/8, USART1
│ │
│ └──→ APB3, APB4 ...
│
LSI (32 kHz) ──────────────────────→ IWDG
LSE (32.768 kHz) ──────────────────→ RTC
Each APB bus has a divider. When you configure a peripheral's baud rate or frequency, the HAL calculates it relative to that peripheral's bus clock, not SYSCLK. This is why getting the clock tree right is so important — everything downstream depends on it.
Embassy Clock Configuration
Using Defaults
The simplest configuration uses the internal HSI oscillator with default settings. This is what you get with one line:
#![allow(unused)] fn main() { let p = embassy_stm32::init(Default::default()); }
This works for LED blinking and basic GPIO, but you're leaving performance on the table and can't use USB.
Full Speed Configuration (H743 with 25 MHz Crystal)
For real projects, you'll want to configure the clocks explicitly. Here's how to get the STM32H743 running at its full 480 MHz using a 25 MHz external crystal:
#![allow(unused)] fn main() { use embassy_stm32::Config; use embassy_stm32::rcc::*; let mut config = Config::default(); // Use the 25 MHz external crystal config.rcc.hse = Some(Hse { freq: embassy_stm32::time::Hertz(25_000_000), mode: HseMode::Oscillator, }); // Configure PLL1: 25 MHz / 5 × 192 / 2 = 480 MHz config.rcc.pll1 = Some(Pll { source: PllSource::HSE, prediv: PllPreDiv::DIV5, // 25 MHz / 5 = 5 MHz mul: PllMul::MUL192, // 5 MHz × 192 = 960 MHz (VCO) divp: Some(PllDiv::DIV2), // 960 / 2 = 480 MHz → SYSCLK divq: Some(PllDiv::DIV4), // 960 / 4 = 240 MHz → can feed peripherals divr: None, }); config.rcc.sys = Sysclk::PLL1_P; // Use PLL1 P output as system clock config.rcc.ahb_pre = AHBPrescaler::DIV2; // AHB = 480 / 2 = 240 MHz config.rcc.apb1_pre = APBPrescaler::DIV2; // APB1 = 240 / 2 = 120 MHz config.rcc.apb2_pre = APBPrescaler::DIV2; // APB2 = 240 / 2 = 120 MHz config.rcc.apb3_pre = APBPrescaler::DIV2; // APB3 = 240 / 2 = 120 MHz config.rcc.apb4_pre = APBPrescaler::DIV2; // APB4 = 240 / 2 = 120 MHz let p = embassy_stm32::init(config); }
Every line has a purpose. The comments show the math — you should always be able to trace the frequency from input crystal to final bus clock by hand.
💡 Fun Fact: The STM32H7 actually has three PLLs (PLL1, PLL2, PLL3). PLL1 drives the CPU. PLL2 and PLL3 can independently clock peripherals like SAI (audio), ADC, or USB at their own precise frequencies. It's like having three independent frequency synthesizers on one chip.
Clock Configuration Summary
| Parameter | Default (HSI) | Full Speed (HSE + PLL) |
|---|---|---|
| Source | HSI 64 MHz | HSE 25 MHz |
| SYSCLK | 64 MHz | 480 MHz |
| AHB | 64 MHz | 240 MHz |
| APB1 | 64 MHz | 120 MHz |
| APB2 | 64 MHz | 120 MHz |
| USB capable | No | Yes (with PLL divider) |
| Crystal required | No | Yes |
Debugging Clock Problems
If things aren't working after changing clock settings, check these in order:
- Does your board actually have an HSE crystal? Check the schematic. If there's no crystal, HSE configuration will hang at startup.
- Is the crystal frequency correct? WeAct H743 uses 25 MHz. Discovery boards often use 8 MHz. Using the wrong value silently corrupts every frequency downstream.
- Are the PLL dividers within valid ranges? The VCO frequency (after multiply, before final divide) must be within the chip's specified range.
- Did you set the flash wait states? Higher clock speeds require more wait states for flash memory access. Embassy handles this automatically, but it's good to know.
What's Next?
Now that your clocks are ticking at the right speed, it's time to learn how to respond to events without wasting CPU cycles. The next chapter covers interrupts and how Embassy turns them into clean, composable async tasks.