Series / Working with STM32 Peripherals / Understanding STM32 Clocks Enough to Build Projects

Understanding STM32 Clocks Enough to Build Projects

Learn the practical STM32 clock concepts needed for timers, UART, I2C, SPI, delays, and peripheral bring-up without turning the clock tree into a reference manual.

On this page

The previous articles used GPIO for visible input and output: blinking the user LED and reading the user button.

GPIO can make clocks feel less important because simple input and output often work with the default clock configuration. Timers, UART, I2C, and SPI are less forgiving. Their behavior depends directly on clock frequencies, prescalers, and timing assumptions.

This article explains enough of the STM32 clock system to build projects without turning the clock tree into a reference manual.

What This Article Covers

This article covers:

  • Why STM32 parts have multiple clocks.
  • The difference between core, bus, and peripheral clocks.
  • Practical clock names such as SYSCLK, HCLK, PCLK1, and PCLK2.
  • What generated SystemClock_Config() code is responsible for.
  • How clock settings affect timers, UART, I2C, SPI, and delays.
  • How to inspect clock frequencies in the debugger.

It does not cover every STM32L4 clock source, low-power mode, PLL option, or clock mux. Those details matter in some projects, but most early peripheral work needs a smaller set of concepts.

Why Microcontrollers Have Clock Trees

A desktop application rarely cares how the CPU clock reaches each peripheral. Embedded firmware does.

An STM32 has a clock tree because different parts of the chip have different timing needs:

  • The CPU core needs a clock to execute instructions.
  • Buses need clocks to move data between the core, memory, and peripherals.
  • GPIO, timers, UART, I2C, SPI, ADC, and other peripherals need clocks to operate.
  • Low-power modes may turn off or slow down parts of the chip.
  • Some peripherals need accurate timing, while others only need a functional clock.

The clock tree is the routing and division system that turns oscillator sources into usable clocks across the chip.

You do not need to memorize the whole tree before writing firmware. You do need to know which clock a peripheral depends on and how to check whether the configured frequency matches your assumptions.

Practical Clock Sources

STM32 parts can use several clock sources. Names vary by family, but these are common concepts:

  • MSI: an internal multi-speed oscillator used on many STM32L parts.
  • HSI: an internal high-speed oscillator.
  • HSE: an external high-speed oscillator or clock input.
  • PLL: a phase-locked loop that multiplies or divides a source clock.
  • LSE: a low-speed external oscillator, often a 32.768 kHz crystal for RTC use.
  • LSI: a low-speed internal oscillator.

For project work, the most important question is not "Which source names exist?" It is "Which source is my project actually using, and what frequencies result from it?"

CubeMX-generated clock code often answers that by configuring the oscillator source, PLL, prescalers, and flash latency in one function.

The Clocks You Will Actually Check

The practical derived clocks to recognize are:

  • SYSCLK: the system clock selected after oscillator and PLL configuration.
  • HCLK: the AHB clock used by the core, memory system, DMA, and AHB peripherals.
  • PCLK1: the APB1 peripheral clock.
  • PCLK2: the APB2 peripheral clock.

Many peripherals are attached to APB buses. If a UART, timer, or SPI peripheral is on APB1, its timing depends on PCLK1 or a clock derived from it. If it is on APB2, it depends on PCLK2 or a clock derived from it.

The CPU frequency and peripheral bus frequency are not always the same. Prescalers can divide clocks between the system, AHB, APB1, and APB2 domains.

That distinction matters. A timer configured with a prescaler based on the wrong clock will run at the wrong rate. A UART baud rate calculation based on the wrong bus clock may produce unreliable serial output.

What SystemClock_Config Does

CubeMX projects commonly generate a function named SystemClock_Config().

That function usually does several important jobs:

  • Enables and configures oscillator sources.
  • Configures the PLL when used.
  • Selects the system clock source.
  • Sets AHB and APB prescalers.
  • Configures flash latency for the selected frequency.
  • Updates HAL and CMSIS clock state used by later code.

In HAL-based firmware, a typical startup path looks like:

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    // Peripheral initialization follows.

    while (1)
    {
    }
}

HAL_Init() prepares HAL state and sets up the default time base. SystemClock_Config() then moves the MCU into the project's intended clock configuration.

Some early examples may appear to work without an explicit clock configuration because the MCU starts from a default internal clock. Do not treat that as a project architecture. Once timers and serial buses matter, the clock configuration should be deliberate.

HAL_Delay Depends on Time Base Assumptions

The LED and button examples used HAL_Delay() for simple timing.

HAL_Delay() depends on the HAL tick. By default, that tick is usually driven by SysTick at a 1 ms rate.

If the system clock changes and the HAL tick is not configured correctly, delays can be wrong or can stop working. That is one reason clock setup belongs early in the firmware startup path.

For basic examples, HAL_Delay(500) is fine. For project timing, do not confuse "a delay function exists" with "the clock tree is understood."

How Clocks Affect Upcoming Peripherals

Timers depend on input clock frequency, prescaler values, and auto-reload values. If the timer clock assumption is wrong, a 1 ms tick may become something else.

UART depends on a peripheral clock to generate the baud rate. A wrong clock can produce serial output that looks like garbage or fails entirely.

I2C timing depends on peripheral clock and timing-register configuration. Wrong assumptions can break bus speed, setup time, hold time, or rise-time margins.

SPI uses clock prescalers to generate the serial clock. If the source clock is not what you think, the SPI bus may run too fast or slower than expected.

This is why clocks appear before the timer, UART, I2C, and SPI articles in the series.

Inspect Clock Values in Firmware

Add a small debug check after clock initialization:

volatile uint32_t sysclk_hz;
volatile uint32_t hclk_hz;
volatile uint32_t pclk1_hz;
volatile uint32_t pclk2_hz;

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    sysclk_hz = HAL_RCC_GetSysClockFreq();
    hclk_hz = HAL_RCC_GetHCLKFreq();
    pclk1_hz = HAL_RCC_GetPCLK1Freq();
    pclk2_hz = HAL_RCC_GetPCLK2Freq();

    while (1)
    {
    }
}

The variables are marked volatile so they remain easy to inspect while debugging.

Build the firmware, start the VSCode debugger, stop after the assignments, and inspect the values.

Compare them with the clock configuration you expect from CubeMX or from your hand-written clock setup.

Use the Debugger as a Clock Sanity Check

The debugger cannot prove every electrical timing requirement, but it can quickly catch wrong assumptions.

Useful checks:

  • Confirm SystemClock_Config() is called.
  • Step over the function and confirm it returns.
  • Inspect sysclk_hz, hclk_hz, pclk1_hz, and pclk2_hz.
  • Compare the values against the CubeMX clock configuration.
  • Confirm later peripheral setup code uses the expected bus clock.

If the values are not what you expected, do not start tuning timer or UART settings yet. Fix the clock configuration first.

Common Mistakes

The first common mistake is assuming the CPU clock equals every peripheral clock. APB prescalers can make peripheral bus clocks different from the core clock.

The second is forgetting that timers may have special clock behavior when APB prescalers are used. Always check the reference manual for the timer clock path when precision matters.

The third is not calling SystemClock_Config() after generating or writing it. The code may exist in the project but never run.

The fourth is changing PLL or prescaler settings without checking flash latency. Flash wait-state configuration must be valid for the selected clock frequency and voltage range.

The fifth is relying on HAL_Delay() before the HAL time base is configured as expected.

The sixth is assuming an external oscillator exists because a different board had one. Board-level oscillator availability must be checked in the Nucleo documentation and schematic.

The seventh is regenerating CubeMX code and not noticing that clock settings changed.

What This Proves

Inspecting clock values proves that the firmware can report the clock frequencies HAL believes are active.

It also proves that the startup path reaches the clock configuration and that the values used by later HAL peripheral code can be checked in the debugger.

That is enough for the next project step: configuring timers with deliberate timing assumptions.

What This Does Not Prove

This does not prove every oscillator is accurate.

It does not measure clock output on a pin, validate UART baud accuracy with a logic analyzer, or prove low-power clock behavior. It also does not replace the STM32L4 reference manual when you need exact clock-tree details.

The point is practical: before using timers and serial buses, know which clocks your firmware thinks it is running from.

Next Steps

The next article uses timers on STM32.

It will take the clock concepts from this article and turn them into a concrete timer configuration for delays, periodic work, and project timing foundations.