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:

SymptomLikely Clock Problem
UART prints garbage charactersBaud rate is wrong because SYSCLK isn't what you think
SPI communication fails randomlyAPB clock doesn't divide cleanly to SPI baud rate
Timers run at wrong speedTimer clock prescaler assumes a different input frequency
USB doesn't enumerateUSB requires exactly 48 MHz — no tolerance
Everything works but is painfully slowRunning on default 64 MHz HSI instead of 480 MHz PLL
Program crashes on startupPLL 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

ParameterDefault (HSI)Full Speed (HSE + PLL)
SourceHSI 64 MHzHSE 25 MHz
SYSCLK64 MHz480 MHz
AHB64 MHz240 MHz
APB164 MHz120 MHz
APB264 MHz120 MHz
USB capableNoYes (with PLL divider)
Crystal requiredNoYes

Debugging Clock Problems

If things aren't working after changing clock settings, check these in order:

  1. Does your board actually have an HSE crystal? Check the schematic. If there's no crystal, HSE configuration will hang at startup.
  2. 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.
  3. Are the PLL dividers within valid ranges? The VCO frequency (after multiply, before final divide) must be within the chip's specified range.
  4. 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.