1.8. Clock settings
1. Introduction
Clock settings are of primary importance when you start a new microcontroller project. The way you configure the clocking scheme has direct impact on peripheral programing, application performance and power consumption. This tutorial details the default configuration and provides a function to setup clock for maximum performance and stability. You absolutely need to understand and master the concepts introduced here.
2. STM32 Clock architecture
Open your blink3 project and edit the main.c file. Add a call to the SystemCoreClockUpdate() at the beginning of the main() function. The SystemCoreClockUpdate() is implemented in system_stm32f0xx.c and is part of the CMSIS package. What it does is an update of the global variable SystemCoreClock using current settings to calculate actual core clock frequency.
/*
* main.c
*
* Created on: 15 mai 2021
* Author: Laurent
*/
#include "stm32f0xx.h"
int main()
{
uint32_t i;
SystemCoreClockUpdate(); // <-- Add this line
// Start GPIOA clock
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
// Configure PA5 as output
GPIOA->MODER &= ~GPIO_MODER_MODER5_Msk;
GPIOA->MODER |= (0x01 <<GPIO_MODER_MODER5_Pos);
while(1)
{
// LED toggle
GPIOA->ODR ^= GPIO_ODR_5;
// Wait
for(i=0; i<100000; i++);
}
}
Save , build , and fire a debug session .
In the Expressions view, add the SystemCoreClock global variable, then step over the SystemCoreClockUpdate() function and watch the value of SystemCoreClock:
SystemCoreClock = 8000000, so actual CPU frequency is 8MHz. The device datasheet reveals that SMT32F072 runs fine up to 48MHz, so we have room for a 6× faster CPU. How can we do that?
- By understanding the clock scheme of the device.
- By writing a function to configure the clock scheme for 48MHz operation.
3. Understanding the Clock scheme
CubeIDE provides a handy graphical view of the clock scheme. It is only available when you create project using STM32 templates and HAL libraries. You can open the first blink project back, and then open blink.ioc file into the graphical editor and then select the Clock Configuration tab.
The above picture represents the full hardware architecture of the MCU internal clock system. We find:
- Internal clock sources (RC oscillators) → Available at no additional costs, but neither very precise, nor very stable across the temperature range
- High Speed : HSI, HSI48, HSI14 → default RC-based oscillator for CPU and peripherals.
- Low Speed : LSI → always used with the watchdog timer.
- Inputs for external clock sources (Crystal oscillators) → Required if clock precision and stability are required
- High Speed : HSE → the de facto input for high-speed crystal-based oscillator for CPU and peripherals fast and stable clocking.
- Low Speed : LSE → mostly used with RTC.
- Multiplexers for clock routing (one input among n is connected to the output)
- Frequency dividers (prescalers)
- Frequency multiplier (PLL)
By fine tuning all these boxes, you can achieve a large amount of different clock configurations. Why is it that complex? Because clock and power consumption are tightly linked, and you want the most of the MCU performance, with as low current as possible.
Hence, in power-aware embedded systems, basic approaches are:
- We only clock the hardware the application needs
- We lower the clock frequency as much as we can to meet performance requirements
- We use Low-Power modes (sleep, stop, standby) whenever we can
4. Understanding the default configuration
In our blink3 project, so far, the clock configuration is done at startup via a call to the SystemInit() function. Note that SystemInit() is called from the startup script prior calling main(). Therefore a clock configuration can be performed before your program actually starts executing.
Yet, as you can see, the SystemInit() function (in system_stm32f0xx.c) has been left empty, and that's up to you to customize it.
/**
* @brief Setup the microcontroller system
* @param None
* @retval None
*/
void SystemInit(void)
{
/* NOTE :SystemInit(): This function is called at startup just after reset and
before branch to main program. This call is made inside
the "startup_stm32f0xx.s" file.
User can setups the default system clock (System clock source, PLL Multiplier
and Divider factors, AHB/APBx prescalers and Flash settings).
*/
}
As a result, the 8MHz we observe for the system core clock comes from the default hardware clock configuration at startup (reset state), which in turn is a consequence of the registers that configure the above clock scheme reset states . These registers are part of the RCC (Reset & Clock Control) peripheral. Refer to the the Reference Manual to fully understand the table below.
RESET STATE | BITS | EFFECT | |
RCC_CR | 0x0000 XX83 |
|
|
RCC_CFGR | 0x0000 0000 |
|
|
The corresponding path is highlighted below. This is the configuration you have if you do nothing.
Note that configuration at startup only relies on internal (and therefore available) resources. Doing so guaranties that software always start executing at power on, regardless the availability of external clock sources.
5. Writing a new clock configuration function
Assume we want to push the processor to its 48MHz limit, and then use HSE as the clock source for more stability.
The HSE hardware supports two different configurations (modes):
The Oscillator Mode: In this mode, HSE is connected to a crystal/capacitor network. HSE drives this network (unstable closed-loop amplifier) in order to produce oscillations
The Bypass Mode: In this mode, HSE receives a clock from an external source on the board. HSE does nothing but letting that clock passing through it.
Looking at the board schematics, we see that the 2 options have been anticipated on the PCB. We can either use:
the X3/C33/C34 network with HSE in Oscillator Mode
or the ST-Link MCO (Master Clock Output) clock source with HSE in Bypass Mode
But looking closer on the board itself, you will see that X3/C33/C34 and R35, R37 are not actually fitted. Footprints are there, but if you want it, you have to buy the parts and then find yourself a soldering iron...
So we're left with the second option, i.e. using the ST-Link MCO as our HSE source, with HSE in Bypass Mode. The user manual also states that ST-Link MCO is a fixed 8MHz frequency clock.
As we want to achieve a 48MHz CPU clock frequency from the 8MHz external clock source, we need something that multiplies frequencies. This is the purpose of the PLL (Phase Locked Loop) circuit. Using a ×6 multiplication factor within the PLL, we can obtain the targeted 48MHz for the CPU and peripherals.
In summary, the configuration we need to implement is:
Practically, this will be achieved by writing a function that does (in this order):
- Enable HSE in Bypass Mode and make sure it is READY (should already be)
- Choose HSE as the selected input on the PLL Source Mux
- Set the PLL prescaler PREDiv to /1 and the multiplication factor PLLMul to ×6
- Start the PLL and make sure it is READY
- Make sure AHB Prescaler and APB Prescaler are set such that system (CPU & peripherals) will cope with the new frequency
- Switch the System Clock Mux intput, from HSI to PLLCLK → At this moment only will the system clock get boosted.
6. Let's boost the system
Using the blink3 project, open main.c in the editor. Copy/paste the following function below the main() function. Comments should help you understand what the code does but basically, it just sets the RCC peripheral as above discussed.
In addition, it sets PA8 pin as MCO (Master Clock Output). This is not mandatory. It only provides a convenient way to measure the internal clock frequency with an oscilloscope. Yet, in order to cope with oscilloscopes bandwidth, the 48MHz is divided by /16 so that MCO pin actually exhibits a 3MHz square signal.
/*
* Clock configuration for the Nucleo STM32F072RB board
* HSE input Bypass Mode -> 8MHz
* SYSCLK, AHB, APB1 -> 48MHz
* PA8 as MCO with /16 prescaler -> 3MHz
*
* Laurent Latorre - 05/08/2017
*/
static void SystemClock_Config()
{
uint32_t HSE_Status;
uint32_t PLL_Status;
uint32_t SW_Status;
uint32_t timeout = 0;
timeout = 1000000;
// Start HSE in Bypass Mode
RCC->CR |= RCC_CR_HSEBYP;
RCC->CR |= RCC_CR_HSEON;
// Wait until HSE is ready
do
{
HSE_Status = RCC->CR & RCC_CR_HSERDY_Msk;
timeout--;
} while ((HSE_Status == 0) && (timeout > 0));
// Select HSE as PLL input source
RCC->CFGR &= ~RCC_CFGR_PLLSRC_Msk;
RCC->CFGR |= (0x02 <<RCC_CFGR_PLLSRC_Pos);
// Set PLL PREDIV to /1
RCC->CFGR2 = 0x00000000;
// Set PLL MUL to x6
RCC->CFGR &= ~RCC_CFGR_PLLMUL_Msk;
RCC->CFGR |= (0x04 <<RCC_CFGR_PLLMUL_Pos);
// Enable the main PLL
RCC-> CR |= RCC_CR_PLLON;
// Wait until PLL is ready
do
{
PLL_Status = RCC->CR & RCC_CR_PLLRDY_Msk;
timeout--;
} while ((PLL_Status == 0) && (timeout > 0));
// Set AHB prescaler to /1
RCC->CFGR &= ~RCC_CFGR_HPRE_Msk;
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
//Set APB1 prescaler to /1
RCC->CFGR &= ~RCC_CFGR_PPRE_Msk;
RCC->CFGR |= RCC_CFGR_PPRE_DIV1;
// Enable FLASH Prefetch Buffer and set Flash Latency
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY;
/* --- Until this point, MCU was still clocked by HSI at 8MHz ---*/
/* --- Switching to PLL at 48MHz Now! Fasten your seat belt! ---*/
// Select the main PLL as system clock source
RCC->CFGR &= ~RCC_CFGR_SW;
RCC->CFGR |= RCC_CFGR_SW_PLL;
// Wait until PLL becomes main switch input
do
{
SW_Status = (RCC->CFGR & RCC_CFGR_SWS_Msk);
timeout--;
} while ((SW_Status != RCC_CFGR_SWS_PLL) && (timeout > 0));
/* --- Here we go! ---*/
/*--- Use PA8 as MCO output at 48/16 = 3MHz ---*/
// Set MCO source as SYSCLK (48MHz)
RCC->CFGR &= ~RCC_CFGR_MCO_Msk;
RCC->CFGR |= RCC_CFGR_MCOSEL_SYSCLK;
// Set MCO prescaler to /16 -> 3MHz
RCC->CFGR &= ~RCC_CFGR_MCOPRE_Msk;
RCC->CFGR |= RCC_CFGR_MCOPRE_DIV16;
// Enable GPIOA clock
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
// Configure PA8 as Alternate function
GPIOA->MODER &= ~GPIO_MODER_MODER8_Msk;
GPIOA->MODER |= (0x02 <<GPIO_MODER_MODER8_Pos);
// Set to AF0 (MCO output)
GPIOA->AFR[1] &= ~(0x0000000F);
GPIOA->AFR[1] |= (0x00000000);
// Update SystemCoreClock global variable
SystemCoreClockUpdate();
}
Then declare the SystemClock_Config() function prototype at the beginning of main.c and insert a call to this function at the beginning of main() :
static void SystemClock_Config (void); // <-- Function prototype
int main()
{
uint32_t i;
SystemClock_Config(); // <-- Call to the clock configuration function
// Start GPIOA clock
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
// Configure PA5 as output
GPIOA->MODER &= ~GPIO_MODER_MODER5_Msk;
GPIOA->MODER |= (0x01 <<GPIO_MODER_MODER5_Pos);
while(1)
{
// LED toggle
GPIOA->ODR ^= GPIO_ODR_5;
// Wait
for(i=0; i<100000; i++);
}
}
Save , build , and fire a debug session .
At the beginning of main(), the SystemCoreClock variable should still be 8000000 (8MHz). Then step over the SystemClock_Config() function and watch the Expression view.
The SystemCoreClock variable should be now 48000000 (48MHz). Well done!
Run the program and watch the LED... blinking has become pretty nervous, hasn't it?
Probe PA8 pin with an oscilloscope. You should see a 3MHz clock (as a result of 48MHz /16):
Then suspend the execution and keep probing PA8. What do you see? Can you explain?
You can terminate the debug session and switch back to the C/C++ perspective.
7. Summary
In this tutorial, we introduced the clock setup in the STM32. User settings define clock frequencies for the CPU and for the various peripherals (buses). This is a matter of primary importance:
- You can't configure peripherals, especially timers and communication peripherals if you don't know what is their clock frequency.
- The clock frequency can be changed on-the-fly, depending on the application requirements or the available power at a given moment.
- As you can see, there are several clock domains in the MCU. Fine tuning each domain to the minimum required frequency, at every moment of the application life, is a key for power savings.
At this moment, we tuned clock for maximum operating frequency. Therefore, we considered the performance before the power consumption. All along the subsequent tutorials, we will assume that performance is the priority and keep using that clock configuration, but keep in mind that it is not power friendly. A good approach would be to start a development at maximum speed, then measure what is really needed for the application to perform flawlessly, and finally to reduce the frequency to what is just needed.