Many peripheral problems do not look like clock problems at first.
UART baud rate is slightly wrong. SPI fails when the rate is raised. I2C occasionally times out. A timer period drifts. ADC sampling does not match the expected rate. The CPU runs at a high frequency, but peripheral register access is still slow. A more subtle case: everything works before sleep, but the first peripheral access after wakeup fails, or a UART works in the bootloader and then stops working once the operating system takes over.
These symptoms often connect to the same underlying fact: modules inside a chip do not all use one shared frequency directly.
The most useful first model for a clock tree is this: a clock tree takes one or more base clock sources, then selects, multiplies, divides, gates, and distributes them to the CPU, buses, memory, timers, and peripherals. Software sees frequency, timeouts, and transfer rates; the hardware depends on every step in that path being selected, calculated, enabled, and stable.
crystal / internal RC / low-speed RTC clock
-> PLL multiplication or locking
-> mux selects a clock source
-> divider scales the frequency
-> gate enables or disables a module clock
-> CPU / bus / timer / UART / SPI / ADC / USB
This article is not about how to program a specific RCC, CCU, or clock-controller register block. It is about a more basic question: why the same idea, “clock,” affects performance, peripherals, timing, low power, and boot debugging at the same time.
Frequency Is Not Only CPU Frequency
Engineering discussions often say that an MCU runs at 120 MHz or an SoC runs at 1 GHz. That usually means the CPU frequency or one main clock frequency, but many other frequencies exist inside the system.
Common clocks include:
- external crystal or internal RC clocks
- PLL output clocks
- CPU core clocks
- bus clocks such as AHB, APB, or AXI
- peripheral functional clocks
- peripheral bus-access clocks
- timer counter clocks
- RTC or low-power clocks
- dedicated reference clocks for audio, USB, Ethernet, or display blocks
A high CPU frequency does not mean every peripheral is fast. A UART controller may sit on a lower-frequency APB bus. A timer may use an independent timer clock. An RTC may use a 32.768 kHz low-speed crystal.
So when debugging “why is it slow” or “why is the timing wrong,” CPU frequency is not enough. The real question is: which clock does the target module actually receive?
Why It Is a Tree
A chip usually does not give each module its own independent crystal. More often, it starts with a small number of clock sources and distributes derived clocks to different modules.
That distribution path commonly contains several kinds of nodes:
- oscillator: a base source such as an external crystal or internal RC
- PLL: generates a higher or specific frequency from an input reference
- mux: selects one clock source from several candidates
- divider: divides the frequency by an integer or fractional ratio
- gate: disables a module clock when the module is not used
- clock domain: a hardware region that shares a clock and timing boundary
It looks like a tree because one upstream clock can feed many downstream branches. Changing an upstream PLL may affect the CPU, buses, and multiple peripherals at once. Closing one gate usually affects only one module or one domain.
That is why clock configuration is risky: a change that looks local can alter the operating conditions of many downstream blocks.
A PLL Generates Frequency, Not Free Speed
External crystals are often modest frequencies such as 8 MHz, 12 MHz, or 24 MHz. When the system needs a higher CPU or bus frequency, it usually uses a PLL.
A PLL generates a higher or more suitable output frequency from an input reference. But it is not magic that becomes usable immediately after a register write. In practice, you need to care about:
- whether the input clock is stable
- whether the PLL settings are within the chip limits
- whether the PLL has locked
- whether the system waits before switching clock sources
- whether flash wait states, voltage, and bus dividers match the new frequency
If the system switches to a PLL before it is locked, it may crash immediately. If the CPU frequency is raised without adjusting flash wait states, instruction fetches may fail. If a bus exceeds a peripheral specification, the peripheral may become intermittent.
A clock tree is not an isolated performance switch. It is part of the operating conditions together with voltage, flash timing, bus limits, and peripheral specifications.
Bus Clocks Define Register Access and Peripheral Boundaries
Many peripherals involve at least two kinds of clocks.
One is the bus-access clock, used by the CPU to read and write peripheral registers. The other is the functional clock, used by the peripheral’s internal state machine, sampling, shifting, or protocol timing.
For example:
CPU
-> bus clock
-> UART register access
UART functional clock
-> baud-rate generator
-> TX/RX sampling and shifting
If the bus clock is off, register access may time out or return invalid values. If the functional clock is off, register writes may appear to succeed, but the state machine does not run and status bits never change.
This explains many cases where the register configuration looks correct but the peripheral does not work. Software writes registers; the hardware still needs its clock domains to be running.
Divider Error Becomes Protocol Error
Peripheral rates usually are not equal to the clock-source frequency. They are derived from a parent clock divided by one or more divider values.
UART baud rate, SPI SCLK, I2C SCL, PWM period, ADC sample rate, and timer tick can all come from a parent clock divided by hardware.
If the target frequency is not exactly divisible, the hardware can only choose a nearby value. Small error may be tolerable; larger error becomes a protocol problem:
- UART sampling points drift
- SPI setup and hold margins fail at high speed
- I2C SCL high and low times violate requirements
- PWM period or duty cycle differs from expectation
- ADC sampling time is too short and readings become unstable
- timer periods disagree with application assumptions
So “configured as 115200” does not necessarily mean the line is exactly 115200 baud. A stricter judgment needs the parent clock, divider algorithm, rounding behavior, and allowed error.
Timers Depend on Hardware Counter Clocks
Applications see sleep(1), a 100 ms timeout, or a 10 Hz periodic task. Underneath, some hardware counter must advance time.
That counter’s input clock comes from the clock tree. Depending on the platform, it may be:
- a CPU-related system tick
- an independent timer clock
- an ARM generic timer
- an RTC low-speed clock
- a low-power timer
If the system changes a parent clock but does not update the timer conversion, timeouts drift. If the system enters low power and a normal timer stops while only the RTC keeps running, wakeup time now depends on a different clock path.
This is one layer below the timer article. The timer article explains wait paths and scheduling semantics; the clock-tree model explains where the hardware time base comes from, whether it keeps running during sleep, and whether it remains trustworthy after frequency changes.
Clock Gating Saves Power and Creates Failures
Clock gating has a simple purpose: when a module is unused, stop its clock to reduce dynamic power.
This is common in low-power devices. The CPU may keep running while a UART, SPI, I2C, ADC, or sensor-interface clock is disabled. Before the next access, the system or driver enables it again.
The problem is that gating changes hardware state:
- whether registers remain accessible after the clock is off
- whether the state machine pauses or resets
- whether FIFO, DMA, or sampling state is retained
- whether the clock needs time to stabilize after enable
- whether the peripheral must be reinitialized after resume
If firmware or a driver treats “registers are still visible” as “hardware is still running,” wakeup failures, stuck status bits, stalled DMA, and intermittent peripheral silence become likely.
Clock gating is not a transparent pause. It is part of the hardware operating condition.
Clock-Domain Changes Add Synchronization Boundaries
Different blocks inside an SoC may live in different clock domains. CPU, bus, peripheral, DMA, display, audio, and wireless blocks do not necessarily share one clock.
Crossing clock domains requires synchronization logic. Hardware handles most details, but software still sees boundaries:
- some status bits require waiting for synchronization
- a module must be idle before changing its clock
- reset release and clock enable must follow a fixed order
- DMA or FIFO state cannot be frequency-switched at arbitrary moments
- low-power and run domains have wakeup latency between them
That is why chip manuals often say to disable a module, change a divider, wait for a status bit, and then re-enable it. These steps are not ceremony; they prevent state from being damaged across clock-domain boundaries.
Boot Clocks Are Often Not Final Clocks
After power-on, a system usually does not start at its final high-frequency configuration.
Early startup code often uses a safe default clock such as an internal RC oscillator or a low-frequency external crystal. After power, flash, SRAM or DRAM, and PLL conditions are ready, the system switches to the target frequency.
This creates several boot-debugging traps:
- a peripheral that works in the bootloader may not use the same frequency later
- early UART baud rate depends on the default clock and must be recalculated after switching
- DRAM, flash, bus dividers, and CPU frequency have ordering constraints
- the operating system may reconfigure the clock controller after handoff
- Device Tree or board configuration errors may override a working bootloader state
So “it worked in the bootloader” does not mean the clock tree remains correct. Each stage in the boot path may take ownership and reconfigure clocks.
Linux, RTOS, and Bare Metal See Different Layers
Bare-metal code often programs clock-controller registers directly. An RTOS may initialize system clocks in the BSP or HAL, then attach ticks, peripheral drivers, and low power to that setup. Linux usually manages clocks through the clock framework, clocks properties in Device Tree, and clock providers.
The layers differ, but the hardware facts are the same:
- a peripheral needs an available parent clock
- dividers must produce a protocol-acceptable frequency
- gates must be enabled before access
- frequency changes must follow the hardware sequence
- low-power recovery must restore operating conditions
The Linux driver call clk_prepare_enable() is only one API expression of this hardware path. The clock tree itself is lower-level than any one interface.
How to Debug Clock-Tree Problems
When facing frequency, peripheral, timing, or low-power issues, split the path first.
First, identify which parent clock the target module actually uses. Do not look only at CPU frequency; look at the parent clock for the peripheral, timer, or bus.
Second, confirm that PLLs and muxes are stable and correctly selected. Check input sources, PLL lock, clock-source switching, and fallback paths.
Third, calculate whether dividers really produce the target frequency. Compute the actual rate and error, especially for UART, audio, USB, ADC, PWM, and timers.
Fourth, verify that related gates are enabled during access and operation. Distinguish bus clocks from functional clocks, and check that runtime PM or suspend/resume did not disable them.
Fifth, make sure frequency-change order follows the manual. Check module idle state, synchronized status bits, and the order of reset, power, clock, and register configuration.
Sixth, identify the current boot or low-power stage. Bootloader, kernel, RTOS, application, and post-resume clock configuration may differ.
These questions get closer to the real cause than asking only whether a register was written.
What to Remember in Practice
A clock tree is not background artwork in a chip manual. It decides how fast each module in the system runs, which modules are running, and which modules merely still have visible registers.
CPU frequency, bus frequency, peripheral baud rate, timer precision, low-power wakeup, and boot-stage behavior can all come from different branches of the same clock tree.
So when a peripheral does not work, a rate is wrong, timeouts look strange, wakeup fails, or boot stages behave differently, do not ask only whether the code configured a register. Better questions are:
- where does this clock come from
- which PLLs, muxes, dividers, and gates does it pass through
- what is the actual frequency
- at this stage, is it stable, enabled, and used by the target module
- after low power or frequency changes, were those conditions restored
Treat the clock tree as an operating condition, not background knowledge. Many intermittent MCU and SoC problems become explainable only after that.