Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 25

Configuring Clocks with RCC Drivers

Recommended resources

Hello! Welcome back.

In our last lesson, we explored the architecture of the microcontroller's clock tree, understanding the roles of oscillators, the PLL, and prescalers. We discussed what these components are and why they are arranged in a specific way.

Today, we transition from theory to practice. Our goal is to implement a Reset and Clock Control (RCC) driver function to configure system and peripheral clocks. You have experience with high-level tools and AUTOSAR's Mcu module for clock configuration; this lesson will take you down to the register level to see how it's actually done in bare-metal C. This is a fundamental skill for any low-level driver developer and a frequent topic in technical interviews.

1. The Goal: From Low to High Speed

After a reset, most microcontrollers start up using a safe, low-speed internal oscillator (like the HSI on STM32 devices, typically running at 8 or 16 MHz). This ensures the device boots reliably. Our task is to write a function that reconfigures the system to run at its maximum speed, using the more accurate High-Speed External (HSE) crystal and the Phase-Locked Loop (PLL).

To do this, we'll primarily interact with a few key registers within the RCC peripheral:

  • RCC_CR (Clock Control Register): Used to turn oscillators (HSI, HSE, PLL) on and off and to check their "ready" status flags.
  • RCC_CFGR (Clock Configuration Register): The main configuration hub. Used to select the system clock source, configure the PLL multiplication and division factors, and set the prescalers for the AHB and APB buses.
  • FLASH_ACR (Flash Access Control Register): Used to configure the Flash memory's wait states, which is crucial when increasing the CPU clock speed.

Let's look at the bit fields of the RCC_CFGR register to get a feel for what we'll be manipulating.

RCC_CFGR Register Bitfield Map
This image shows the various fields within the RCC_CFGR register. We will be focusing on the SW, SWS, HPRE, PPRE, PLLSRC, and PLLMUL fields to orchestrate our clock configuration.

2. The Step-by-Step Configuration Sequence

Configuring the system clock is a precise sequence of operations. Performing these steps out of order can lead to an unstable system or cause the microcontroller to hang. Let's walk through the standard procedure to switch from the HSI to a high-speed, PLL-driven HSE clock.

Clock Configuration in STM32

The article 'Clock Configuration in STM32' provides a clear, step-by-step guide for this process at a conceptual level. We will follow this sequence in our implementation.

Please read the sections 'Set Flash Latency', 'APB divisor and HSE divisor', 'Turn on HSE', 'PLL configuration', and 'Core clock source selection'. This will give you a complete overview of the procedure we are about to implement.

Based on this sequence, here is our implementation plan:

  1. Adjust Flash Latency: Increase the number of wait states for Flash memory access.
  2. Enable HSE and wait for it to be ready.
  3. Configure Bus Prescalers and the PLL.
  4. Enable PLL and wait for it to be ready.
  5. Switch the System Clock (SYSCLK) source to the PLL.

Let's implement each step in C.

Step 1: Configure Flash Latency

When the CPU clock speed (HCLK) increases, the Flash memory might not be able to deliver data fast enough. To prevent incorrect instruction fetches, we must insert wait states. The number of wait states required depends on the target HCLK frequency and the microcontroller's voltage range. You must consult the device's datasheet for the correct value.

For example, to run an STM32F4xx at 168 MHz, you typically need 5 wait states.

// Assume FLASH_ACR is a pointer to the Flash Access Control Register
// #define FLASH_ACR_LATENCY_5WS (0x5U)

// 1. Set the appropriate number of wait states in the FLASH_ACR register.
//    We use a read-modify-write operation to only change the latency bits.
uint32_t reg = *FLASH_ACR;
reg &= ~(0xFU); // Clear the latency bits (bits 3:0)
reg |= (5);      // Set 5 wait states
*FLASH_ACR = reg;

// Wait until the new latency is applied by checking the latency bits.
while ((*FLASH_ACR & 0xFU) != 5);

Step 2: Enable the High-Speed External (HSE) Oscillator

Next, we turn on the external crystal oscillator. Hardware needs time to start up and stabilize. We must not proceed until the hardware confirms it's ready.

// Assume RCC_CR is a pointer to the RCC Clock Control Register
// #define RCC_CR_HSEON  (1U << 16) // Bit 16: HSEON
// #define RCC_CR_HSERDY (1U << 17) // Bit 17: HSERDY

// 2. Enable the HSE oscillator.
*RCC_CR |= RCC_CR_HSEON;

// Wait until the HSE oscillator is stable.
// This loop will run until the HSERDY bit is set by hardware.
while (!(*RCC_CR & RCC_CR_HSERDY));

Step 3: Configure the PLL and Bus Prescalers

This is the most involved step. We need to set all the dividers and multipliers to achieve our target frequency. Let's aim to configure an STM32F407 to run at 168 MHz from an 8 MHz HSE crystal.

From the previous lesson, we know the formula is:
VCO_output = (HSE_IN / M) * N
SYSCLK = VCO_output / P

The VCO input (HSE_IN / M) must be between 1 and 2 MHz. Let's choose M = 8.
VCO_input = 8 MHz / 8 = 1 MHz (This is valid).

Now, we calculate N and P for a 168 MHz SYSCLK.
Let's target a VCO_output of 336 MHz.
336 MHz = 1 MHz * N -> N = 336.
168 MHz = 336 MHz / P -> P = 2.

Our parameters are: M=8, N=336, P=2.

Mastering Microcontroller: Clocking MCU by Internal PLL (Running MCU at Full Speed)

This video provides a great walkthrough of this calculation and the subsequent register configuration. Watching it will help solidify the process.

Watch from the beginning to 05:30. Pay attention to how the PLL M, N, and P values are calculated to get 168 MHz and how the AHB and APB prescalers are configured to stay within their limits.

Now, let's write the code to configure the RCC_CFGR register. We also set the prescalers:

  • AHB Prescaler (HPRE): Set to 1, so HCLK = SYSCLK / 1 = 168 MHz.
  • APB1 Prescaler (PPRE1): Set to 4, so PCLK1 = HCLK / 4 = 42 MHz (max is 42 MHz).
  • APB2 Prescaler (PPRE2): Set to 2, so PCLK2 = HCLK / 2 = 84 MHz (max is 84 MHz).
// Assume RCC_CFGR is a pointer to the RCC Clock Configuration Register
// PLL and Prescaler bit definitions for RCC_CFGR
#define RCC_CFGR_PLLSRC_HSE (1U << 22)
#define RCC_CFGR_PPRE1_DIV4 (0x5U << 10) // 101: HCLK divided by 4
#define RCC_CFGR_PPRE2_DIV2 (0x4U << 13) // 100: HCLK divided by 2

// Assume RCC_PLLCFGR is a pointer to the RCC PLL Configuration Register
#define M_VAL 8
#define N_VAL 336
#define P_VAL 2 // P_VAL is represented by 0b00 for /2 in the register
#define P_REG_VAL 0 // ((P_VAL / 2) - 1) gives the register value

// 3. Configure the main PLL.
//    First, ensure the PLL is OFF before configuring it.
*RCC_CR &= ~(1U << 24); // Clear PLLON bit

// Configure PLL source, M, N, and P values.
*RCC_PLLCFGR = (M_VAL << 0) | (N_VAL << 6) | (P_REG_VAL << 16) | RCC_CFGR_PLLSRC_HSE;

// Configure the AHB, APB1, and APB2 prescalers.
*RCC_CFGR |= RCC_CFGR_PPRE1_DIV4 | RCC_CFGR_PPRE2_DIV2; // HPRE is 0 for DIV1

Step 4: Enable the PLL

Just like with the HSE, we turn the PLL on and wait for it to lock.

// #define RCC_CR_PLLON  (1U << 24) // Bit 24: PLLON
// #define RCC_CR_PLLRDY (1U << 25) // Bit 25: PLLRDY

// 4. Enable the main PLL.
*RCC_CR |= RCC_CR_PLLON;

// Wait until the PLL is locked and stable.
while (!(*RCC_CR & RCC_CR_PLLRDY));

Step 5: Select the PLL as System Clock Source

This is the final step: we tell the system to stop using the HSI and start using our newly configured PLL output. Again, we must wait for the hardware to confirm the switch has occurred.

// #define RCC_CFGR_SW_PLL  (0x2U << 0)  // Select PLL as system clock
// #define RCC_CFGR_SWS_PLL (0x2U << 2)  // PLL used as system clock status

// 5. Select the main PLL as the system clock source.
*RCC_CFGR |= RCC_CFGR_SW_PLL;

// Wait until the PLL is being used as the system clock source.
// The SWS bits are read-only and reflect the current clock source.
while ((*RCC_CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL);
Test your understanding!

A colleague is debugging a board that uses a 16 MHz external crystal. Their SystemClock_Config function appears to hang. You inspect their code and see the following sequence:

  1. Set Flash latency to 2 wait states.
  2. Set SW bits in RCC_CFGR to select PLL as the system clock.
  3. Enable HSE by setting HSEON.
  4. Wait for HSERDY.
  5. Configure RCC_PLLCFGR with M, N, P values.
  6. Enable PLL by setting PLLON.
  7. Wait for PLLRDY.

What is the fundamental error in this sequence?

Show answer

The fundamental error is in step 2. The code attempts to switch the system clock to the PLL before the PLL has even been configured or enabled. The correct procedure is to prepare and enable the target clock source (HSE and then the PLL) first, and only then perform the final switchover by setting the SW bits. Trying to switch to an unstable or disabled clock source will cause the system to fail.


3. Verifying the Configuration

After running your configuration function, how can you be sure the clocks are set correctly? You could use an oscilloscope to probe the MCO (Microcontroller Clock Output) pin if configured. Programmatically, most vendor libraries (like ST's HAL) provide functions to calculate the current clock frequencies based on the register values.

STM32 Clock Configuration Guide: Understanding the ...

This article shows a practical example of how to use HAL functions to read back the configured clock frequencies. This is an excellent way to verify your configuration at runtime.

Read the section 'How to Check the Current Clock Frequencies' and review the example code. This demonstrates a robust way to debug and validate your clock setup.


4. Enabling Peripheral Clocks

With the system clock running at full speed, the final step before using any peripheral (like GPIO, UART, or a timer) is to enable its individual clock. This practice, known as clock gating, is a key power-saving feature. If a peripheral's clock is disabled, its registers are inaccessible.

Each peripheral's clock is enabled by setting a corresponding bit in one of the RCC enable registers (e.g., RCC_AHB1ENR, RCC_APB1ENR, RCC_APB2ENR).

// Example: Enable the clock for GPIO Port A, which is on the AHB1 bus.
// Assume RCC_AHB1ENR is a pointer to the corresponding register.
#define RCC_AHB1ENR_GPIOAEN (1U << 0)

*RCC_AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

You did this in the GPIO lesson, and now you can see how it fits into the broader RCC system.


Interview Preparation

  • Question: "Walk me through the high-level steps you would take in your C code to configure an STM32 to run at its maximum clock speed from an external crystal."

    • Your Answer: "First, I'd increase the Flash memory latency to support the higher CPU speed. Then, I'd enable the High-Speed External oscillator (HSE) and enter a loop, polling the HSERDY flag until the crystal is stable. After that, I would configure the PLL—setting its source to HSE and programming the M, N, and P division/multiplication factors to generate the target frequency. I'd also configure the AHB and APB prescalers to ensure bus clocks don't exceed their limits. Next, I'd enable the PLL and poll the PLLRDY flag. Finally, once the PLL is locked, I would change the system clock switch bits to select the PLL as the SYSCLK and wait for the hardware to confirm the switch by checking the SWS status bits."
  • Question: "When you enable an oscillator or a PLL, why is it critical to wait for a 'ready' flag before proceeding?"

    • Your Answer: "Physical components like crystal oscillators and PLLs don't stabilize instantly. The 'ready' flag (HSERDY, PLLRDY) is a hardware signal indicating that the clock source has reached a stable frequency and phase. Attempting to use a clock before it's stable can lead to unpredictable behavior, incorrect instruction execution, or a complete system hang. Polling this flag ensures a safe and orderly clock startup sequence."
  • Question: "You've switched the system clock to a higher frequency. What is a critical register you should have configured before this switch, and why?"

    • Your Answer: "The Flash Access Control Register (FLASH_ACR) is critical. You must configure the appropriate number of wait states for the Flash memory before increasing the CPU clock speed. The CPU runs much faster than the Flash memory can respond. Without the correct wait states, the CPU would try to fetch instructions faster than the Flash can provide them, leading to corrupted data or a HardFault."
  • Question: "What is clock gating and why is it important in embedded systems?"

    • Your Answer: "Clock gating is the practice of selectively enabling or disabling the clock to individual peripherals. In a complex microcontroller, many peripherals may not be in use at any given time. By default, their clocks are 'gated' off in the RCC. To use a peripheral, you must explicitly enable its clock via a bit in an RCC enable register, like RCC_AHB1ENR. This is a critical power-saving technique, as it prevents power from being consumed by the switching activity in unused parts of the chip."

Conclusion

You have now successfully bridged the gap between the theoretical architecture of a clock tree and its practical, bare-metal implementation. This is a significant milestone in low-level driver development.

Key Takeaways:

  • Clock configuration follows a strict sequence: Flash Latency -> Enable Source -> Configure PLL/Prescalers -> Enable PLL -> Switch SYSCLK.
  • Polling ready flags (HSERDY, PLLRDY, SWS) is mandatory to ensure hardware has stabilized before proceeding.
  • Peripheral clocks must be explicitly enabled via clock gating registers before the peripheral can be accessed.
  • The entire process is a careful orchestration of writes to the FLASH_ACR, RCC_CR, RCC_CFGR, and RCC_PLLCFGR registers.

Next Lesson Preview:
Now that we can generate a stable, high-frequency clock, we can start using it to drive time-sensitive peripherals. In our next lesson, we will implement a driver for the SysTick timer to generate periodic interrupts. This forms the basis for time-basing, creating software delays, and is the default heartbeat for most Real-Time Operating Systems.