4. Clock Tree

Reference: RM0390 Reference Manual, Sections 6.2 and 6.3

1. Clock Sources

Crystal Oscillator (HSE)

This is an external clock source. Crystal oscillators provide high stability and low drift, making them ideal for tasks that require precise timing. They are commonly used in real-time clock applications and high-frequency range tasks.

On many STM32 Nucleo boards, the High-Speed External (HSE) oscillator is connected to an 8 MHz crystal that is part of the ST-Link debugger/programmer circuitry. This setup allows you to use HSE without needing to add your own external crystal. However, it's worth noting that if you wish to operate the microcontroller independently of the ST-Link, you'll need to provide your own external oscillator or switch back to using the internal HSI.

RC Oscillator (HSI)

This is an internal clock source. RC (Resistor-Capacitor) oscillators are generally less precise than crystal oscillators but have the advantage of not requiring external components. They are well-suited for tasks that don't need pinpoint timing accuracy.

This is the default clock source for the STM32F4xx microcontrollers.

Phase-Locked Loop (PLL)

Also an internal clock source, a PLL takes an input frequency and multiplies it to generate higher frequencies, which are then used by different parts of the system. This allows for a versatile range of clock speeds and is useful when you need variable, yet stable, frequencies for different tasks.

Summary

Feature Crystal Oscillator RC Oscillator Phase-Locked Loop (PLL)
Source External Internal Internal
Stability High Low to Medium Medium to High
Drift Low Medium to High Low
Precision High Low Medium to High
Complexity Medium Low High
External Components Required None None
Frequency Range Fixed, but broad Fixed Variable
Use Cases Real-time clock, high-frequency tasks General tasks not requiring precise timing Versatile; adaptable to different system needs

2. Clock Tree

clock_tree_zoom_in

Examining the clock tree above closely reveals the following path: System Clock MUX -> SYSCLK -> AHB Prescaler -> HCLK. From here, HCLK branches out to several modules. It directly powers the Cortex core, the AHB bus, memory, and DMA. Additionally, HCLK feeds into the APB1 and APB2 buses through the APB1 Prescaler and APB2 Prescaler, respectively. These APB buses then continue to various peripheral modules.

SYSCLK vs. HCLK vs. FCLK (Cortex Clock)

  • SYSCLK: The System Clock (SYSCLK) serves as the primary clock source for the microcontroller. It can be sourced from various inputs like the internal HSI, external HSE, or a PLL. SYSCLK determines the clock speed for the AHB bus after passing through the AHB Prescaler.

  • HCLK: The High-Speed Clock (HCLK) is essentially SYSCLK after it has been divided by the AHB Prescaler. HCLK is crucial because it feeds several critical components such as the Cortex core, the AHB bus, memory interfaces, and DMA controllers.

  • FCLK: The Cortex Clock (FCLK) is the clock source specifically for the processor core. Generally, FCLK is directly derived from HCLK, meaning they often run at the same frequency. However, under certain low-power scenarios or other special conditions, FCLK may differ from HCLK.

The full clock tree is shown below:

clock_tree


3. RCC Registers

Overview

You can select the clock source in a STM32 Project (not an empty project):

ide_clock_config

As you can see, the multiplexer (MUX) in the middle is used to select the clock source. The MUX is controlled by the RCC (Reset and Clock Control) peripheral. The RCC peripheral is used to enable and disable clocks for different peripherals. It also has a clock security system (CSS) that can be used to detect clock failures.

For example, as you can see in the figure above, the HSE option in the MUX is grayed out. We need to go to "Pinout & Configuration" to enable the HSE:

enable_hse

  • RCC Clock Control Register (RCC_CR): This register is used to enable or disable internal and external oscillators like the High-Speed Internal (HSI) or High-Speed External (HSE) oscillators. It also provides flags to indicate if the oscillators are stable.

  • RCC Clock Configuration Register (RCC_CFGR): This register is mainly used to select the system clock source and set the AHB, APB1, and APB2 prescalers. It also contains settings for the microcontroller clock output. In the Clock Configuration window of STM32CubeIDE, you can easily enable or disable the HSI and HSE oscillators in the GUI without having to manually set the bits in this RCC_CFGR register.

  • RCC Clock Interrupt Register (RCC_CIR): This register is used to enable or disable and clear RCC-related interrupts, like clock ready or PLL ready interrupts.

  • RCC AHBx Peripheral Clock Enable Register (RCC_AHBxENR): These registers are used to enable or disable the clocks to AHB peripherals.

  • RCC APBx Peripheral Clock Enable Register (RCC_APBxENR): Like the AHB equivalent, these registers enable or disable the clocks to APB peripherals.

... and so on. There are many more RCC registers, but these are the most important ones to know.

Peripheral Clock Enable Registers (RCC_AHBxENR, RCC_APBxENR)

By default, all peripheral clocks are disabled to conserve power. So, the first thing to do when using a peripheral is to enable its clock. This is done by setting the appropriate bit in the RCC peripheral's peripheral clock enable register.

For example, if I want to modify the ADC CRI register (ADC_CR1), just doing the following won't work:

#include <stdint.h>

#define ADC_BASE_ADDR       0x40012000UL
#define ADC_CR1_OFFSET      0x04UL
#define ADC_CR1_ADDR        (ADC_BASE_ADDR + ADC_CR1_OFFSET)

int main(void)
{
    uint32_t *pADC_CR1 = (uint32_t *)ADC_CR1_ADDR;
    *pADC_CR1 = 0x00000000UL; // This won't work!

    for(;;);
}

This is because the ADC peripheral clock is disabled by default. We need to enable it first. We must first figure which bus the ADC peripheral is connected to, so that we can find the right RCC peripheral clock enable register. Let's navigate to the memory map in the reference manual:

adc_memory_map

As we can see, the ADC1-3 are located in the APB2 bus. So, we need to find the RCC_APB2ENR register:

RCC_APB2ENR

We can see that ADC1EN is the 8th bit in the register. So, we need to set this bit to enable the ADC1 peripheral clock. Let's modify our code to do this:

#include <stdint.h>

// New code
#define RCC_BASE_ADDR       0x40023800UL
#define RCC_APB2ENR_OFFSET  0x44UL
#define RCC_APB2ENR_ADDR    (RCC_BASE_ADDR + RCC_APB2ENR_OFFSET)

#define ADC_BASE_ADDR       0x40012000UL
#define ADC_CR1_OFFSET      0x04UL
#define ADC_CR1_ADDR        (ADC_BASE_ADDR + ADC_CR1_OFFSET)

int main(void)
{
    uint32_t *pRCC_APB2ENR = (uint32_t *)RCC_APB2ENR_ADDR;
    uint32_t *pADC_CR1 = (uint32_t *)ADC_CR1_ADDR;

    *pRCC_APB2ENR |= (1 << 8); // Enable ADC1 peripheral clock
    *pADC_CR1 = 0x00000000UL; // This will work now!

    for(;;);
}

By the way, the base addresses can be found in the memory map of the reference manual.


4. Exercise: HSI Measurement

Let's write a program to output HSI clock on an output pin and measure it using a logic analyzer.

Steps to Output HSI Clock

  1. Select the desired clock for the MCO (Master Clock Output) pin in the RCC_CFGR register. In this case, we want to output the HSI clock, so we need to set the MCO1 bit to 01 (HSI clock selected) in RCC_CFGR.

  2. Output the MCO1 signal via an output pin on the board. In this case, we want to output the MCO1 signal on PA8 (More on this later).

Enabling MCO1 using the STM32CubeIDE GUI

As you can see in the clock configuration window, there are two MCO pins: MCO1 and MCO2.

MCO

However, you can see that it is grayed out. We need to go to the "Pinout & Configuration" tab to enable it:

enable_MCO1

Now, we can select the HSI clock for MCO1. Remember, MCO1 is a signal inside the microcontroller. We need to connect it to an output pin to measure it with a logic analyzer.

Enabling MCO1 using the RCC_CFGR register

We can also enable MCO1 by setting the MCO1 bit in RCC_CFGR. The RCC_CFGR register is located at 0x40023808. The MCO1 bit is the 21st and 22nd bits in the register.

RCC_CFGR

And we need to set them both to zero to select the HSI clock:

HSI_bits

So far, the code looks like this:

#include <stdint.h>

#define RCC_BASE_ADDR       0x40023800UL
#define RCC_CFGR_OFFSET     0x08UL
#define RCC_CFGR_ADDR       (RCC_BASE_ADDR + RCC_CFGR_OFFSET)



int main(void)
{
    // 1. Configure the RCC_CFGR register to output HSI clock on MCO1
    uint32_t *pRCC_CFGR = (uint32_t *)RCC_CFGR_ADDR;
    *pRCC_CFGR &= ~(0x3 << 21);

    for(;;);
}

Now we need to think about how to connect the MCO1 signal to an output pin. Refer to the datasheet of the STM32F446RE for the alternate function mapping of the GPIO pins. We can see that GPIOA pin 8 (PA8) has the alternate function MCO1.

AF_table

So, we need to set the alternate function of PA8 to MCO1. We need to do so with the MODER register. The MODER register is located at 0x40020000. The MODER register is 32 bits wide, and each pin has 2 bits. So, PA8 is the 16th and 17th bits in the register. We need to set them to 10 to select the alternate function (AF) mode.

GPIOx_MODER

However, there are many AF modes. As we see from the AF table above, MCO1 is AF0. So, we need to set the AF bits to 0000 to select AF0. We need to do that using the GPIOA_AFRL register. It is responsible for setting the AF modes of pins 8 to 15. It has an offset of 0x24, and we need to set the 0th to 3rd bits to 0000.

GPIOx_AFRH

Let's modify our code to do this:

#include <stdint.h>

#define RCC_BASE_ADDR       0x40023800UL
#define RCC_CFGR_OFFSET     0x08UL
#define RCC_CFGR_ADDR       (RCC_BASE_ADDR + RCC_CFGR_OFFSET)
#define RCC_AHB1ENR_OFFSET  0x30UL
#define RCC_AHB1ENR_ADDR    (RCC_BASE_ADDR + RCC_AHB1ENR_OFFSET)

#define GPIOA_BASE_ADDR     0x40020000UL
#define GPIOA_MODER_OFFSET  0x00UL
#define GPIOA_MODER_ADDR    (GPIOA_BASE_ADDR + GPIOA_MODER_OFFSET)
#define GPIOA_AFRH_OFFSET   0x24UL
#define GPIOA_AFRH_ADDR     (GPIOA_BASE_ADDR + GPIOA_AFRH_OFFSET)


int main(void)
{
    // 1. Configure the RCC_CFGR register to output HSI clock on MCO1
    uint32_t *pRCC_CFGR = (uint32_t *)RCC_CFGR_ADDR;
    *pRCC_CFGR &= ~(0x3 << 21);

    // 2. Configure PA8 to AF0 mode to behave as MCO1
    // 2.a. Enable the clock to GPIOA
    uint32_t *pRCC_AHB1ENR = (uint32_t *)RCC_AHB1ENR_ADDR;
    *pRCC_AHB1ENR |= (1 << 0);

    // 2.b. Configure PA8 to AF mode
    uint32_t *pGPIOA_MODER = (uint32_t *)GPIOA_MODER_ADDR;
    *pGPIOA_MODER &= ~(0x3 << 16); // clear
    *pGPIOA_MODER |= (0x2 << 16); // set

    // 2.c. Configure the AF register to set the mode 0 for PA8
    uint32_t *pGPIOA_AFRH = (uint32_t *)GPIOA_AFRH_ADDR;
    *pGPIOA_AFRH &= ~(0xF << 0); // clear

    for(;;);
}

Let's build and run the code. Using a logic analyzer (I am using a Saleae Logic Pro 8), we can hook channel 0 to PA8 (and the ground) to measure the frequency of the HSI clock:

nucleo64_pinout

The output looks like this:

logic_analyzer_output1

64 ns? That's 15.625 MHz. That's not right. Our logic analyzer might not be fast enough to measure the frequency. Let's try to divide the HSICLK by 4. We can do that by setting the MCO1PRE bit in RCC_CFGR.

MCO1PRE

The new code now looks like this:

#include <stdint.h>

#define RCC_BASE_ADDR       0x40023800UL
#define RCC_CFGR_OFFSET     0x08UL
#define RCC_CFGR_ADDR       (RCC_BASE_ADDR + RCC_CFGR_OFFSET)
#define RCC_AHB1ENR_OFFSET  0x30UL
#define RCC_AHB1ENR_ADDR    (RCC_BASE_ADDR + RCC_AHB1ENR_OFFSET)

#define GPIOA_BASE_ADDR     0x40020000UL
#define GPIOA_MODER_OFFSET  0x00UL
#define GPIOA_MODER_ADDR    (GPIOA_BASE_ADDR + GPIOA_MODER_OFFSET)
#define GPIOA_AFRH_OFFSET   0x24UL
#define GPIOA_AFRH_ADDR     (GPIOA_BASE_ADDR + GPIOA_AFRH_OFFSET)

int main(void)
{
    // 1. Configure the RCC_CFGR register to output HSI clock on MCO1
    uint32_t *pRCC_CFGR = (uint32_t *)RCC_CFGR_ADDR;
    *pRCC_CFGR &= ~(0x3 << 21); // Clear MCO1 source bits
    *pRCC_CFGR &= ~(0x7 << 24); // Clear the MCO1 prescaler bits
    *pRCC_CFGR |= (0x6 << 24);  // Set MCO1 prescaler bits to divide by 4

    // 2. Configure PA8 to AF0 mode to behave as MCO1
    // 2.a. Enable the clock to GPIOA
    uint32_t *pRCC_AHB1ENR = (uint32_t *)RCC_AHB1ENR_ADDR;
    *pRCC_AHB1ENR |= (1 << 0); // Enable GPIOA clock

    // 2.b. Configure PA8 to AF mode
    uint32_t *pGPIOA_MODER = (uint32_t *)GPIOA_MODER_ADDR;
    *pGPIOA_MODER &= ~(0x3 << 16); // clear existing bits for PA8
    *pGPIOA_MODER |= (0x2 << 16);  // set AF mode for PA8

    // 2.c. Configure the AF register to set the mode 0 for PA8
    uint32_t *pGPIOA_AFRH = (uint32_t *)GPIOA_AFRH_ADDR;
    *pGPIOA_AFRH &= ~(0xF << 0);  // clear existing bits for PA8 in AFRH
    *pGPIOA_AFRH |= (0x0 << 0);   // set AF0 for PA8

    for(;;);
}

Let's build and run the code. The output looks like this:

logic_analyzer_output2

Now, the frequency is 4.032 MHz. It is still not exactly 4 MHz, but it is close enough. The reason why it is not exactly 4 MHz is because the HSI clock is not exactly 16 MHz. It is 16 MHz +/- 1%. So, the HSI clock can be anywhere between 15.84 MHz and 16.16 MHz. Also, our logic analyzer might not be fast enough to measure the frequency accurately.

Note that if you want to output the HSE clock, you need to enable the HSE clock first:

#include <stdint.h>

#define RCC_BASE_ADDR           0x40023800UL
#define RCC_CFGR_OFFSET         0x08UL
#define RCC_CR_OFFSET           0x00UL
#define RCC_AHB1ENR_OFFSET      0x30UL

#define RCC_CFGR_ADDR           (RCC_BASE_ADDR + RCC_CFGR_OFFSET)
#define RCC_CR_ADDR             (RCC_BASE_ADDR + RCC_CR_OFFSET)
#define RCC_AHB1ENR_ADDR        (RCC_BASE_ADDR + RCC_AHB1ENR_OFFSET)

#define GPIOA_BASE_ADDR         0x40020000UL
#define GPIOA_MODER_OFFSET      0x00UL
#define GPIOA_AFRH_OFFSET       0x24UL

#define GPIOA_MODER_ADDR        (GPIOA_BASE_ADDR + GPIOA_MODER_OFFSET)
#define GPIOA_AFRH_ADDR         (GPIOA_BASE_ADDR + GPIOA_AFRH_OFFSET)

int main(void)
{
    /***** Start of new code *****/

    // 1. Enable HSE and wait for it to stabilize
    uint32_t *pRCC_CR = (uint32_t *)RCC_CR_ADDR;
    *pRCC_CR |= (1 << 16);
    while (!(*pRCC_CR & (1 << 17)));

    // 2. Switch system clock to HSE
    uint32_t *pRCC_CFGR = (uint32_t *)RCC_CFGR_ADDR;
    *pRCC_CFGR |= (1 << 0);

    /******* End of new code *****/

    // 3. Configure MCO1 to output HSE and set prescaler to 4
    *pRCC_CFGR |= (1 << 22);
    *pRCC_CFGR |= (1 << 25) | (1 << 26);

    // 4. Configure PA8 to AF0 to operate as MCO1
    // 4.a. Enable GPIOA clock
    uint32_t *pRCC_AHB1ENR = (uint32_t *)RCC_AHB1ENR_ADDR;
    *pRCC_AHB1ENR |= (1 << 0);

    // 4.b. Set PA8 to AF mode
    uint32_t *pGPIOA_MODER = (uint32_t *)GPIOA_MODER_ADDR;
    *pGPIOA_MODER &= ~(0x3 << 16);
    *pGPIOA_MODER |= (0x2 << 16);

    // 4.c. Set AF0 for PA8
    uint32_t *pGPIOA_AFRH = (uint32_t *)GPIOA_AFRH_ADDR;
    *pGPIOA_AFRH &= ~(0xF << 0);

    for(;;);
}