Changing the Clock Frequency STM32F1 via Registers

Posted by Eddie on March 09, 2020
For the most part a lot of us tend to run the microcontroller at it's max clock frequency because what is the point of having a 100 MHz microcontroller only to run it at 1 MHz? Well the most obvious answer is POWER! But there is also the less often considered concept of stray capacitance and inductance which increases in high speed circuits. But then again the high speed signals are all inside your microcontroller, right? Well yes but your microcontroller is not immune to the above phenomena. However that could be a post all of its own and I might get around to doing that.
But enough chatter if you are here it is because you want to know how to change the System Clock of your precious blue pill like a real champ and not by clicking buttons in CUBEMX. I will however use CUBEMX for visual representation of what is going on because it helps visualize the clock tree a bit less daunting than the RM. 

First I want to list the steps required to accomplish the task at hand as well as show the registers and bits that are relevant to each task. 


  • Turn on the HSE (High Speed External)
  • Activate prefetch buffer
  • Change wait states/Latency
  • Configure RCC and PLL settings while the PLL is off
  • Turn on PLL
  • Change clock source
Some of these steps require that we wait for the specific setting to take effect. It is also a good idea to implement something to handle an error on any of these steps. I will only implement during the first task of enabling the HSE.

Enabling the HSE

The high speed external clock source can be anything from a crystal to a MEMs oscillator and even the MCO (main clock output) of another microcontroller, which is a technique used in Discovery and Nucleo boards. 
To enable the HSE which we assume is  properly routed to the correct pins etc.. we simply enable the corresponding bit in the RCC register found in the reference manual of the F1. However it is very important that we wait for the HSE to stabilize, we do this by polling the bit next to it HSERDY (HSE Ready) We can sit in a while loop until it is ready or use a timeout counter. The point is that if it does not become ready we cannot move on and must handle that situation accordingly in our code.


The code for this looks like so: 
 //turn on external crystal
 RCC->CR |= RCC_CR_HSEON;

 //wait for HSE crystal be stable
 while(!(RCC->CR & RCC_CR_HSERDY))
  ;
 


Activate Prefetch Buffer and set Latency

This next step (prefetch) is not entirely necessary but if you are running that bad-boy at full speed this will definitely make a difference in instruction execution.
According to the Programming manual for the STM32F1: 
"The implementation of this prefetch buffer makes a faster CPU execution possible as the
CPU fetches one word at a time with the next word readily available in the prefetch buffer.
This implies that the acceleration ratio will be of the order of 2 assuming that the code is
aligned at a 64-bit boundary for the jumps. "

As stated above we will need to dig into the programming manual to find the necessary bits for the next 2 steps.

A 1 in the bit 4 position will activate the prefetch buffer which ideally will be set anyways since the reset value of this register is 0x30 ( 0011-0000). The prefetch also has a status bit next to it that you can check to see if it is enabled, I did not do so in my code but that does not mean that you should not.
The other highlighted bits set the latency required for proper flash operations, because remember that flash or any external memory can only be read and written so fast.. and our microcontroller is usually faster than that. The available settings for those bits can bee seen below. In my example I am going to set the clock to 28 MHz so I will use one wait state.
The code for this looks like so:
 //activate prefetch buffer but it should already be on
 FLASH->ACR |= FLASH_ACR_PRFTBE;

 // Flash 1 wait state 
 FLASH->ACR &= ~FLASH_ACR_LATENCY); //reset just to be sure
 FLASH->ACR |= 0x1;    

Configure RCC and PLL Settings

This section is now the heart of changing our clock frequency. I will use the image below to help visualize what we are doing. Below is a screenshot of CUBEMX clock tree,  I have configured it to be 28MHz and highlighted some areas that will become relevant when we edit the specific bit in the registers, so I will reference this image the the numbered highlights.
Number 1 in the image above was what we did in the first step by enabling the HSE. The second thing we did with the Latency and Prefetch is not acknowledged in the clock tree. Lets move on to configuring the PLL.
Here is the CFGR register which helps us accomplish all the settings we need.

First thing I do is reset the PLLSRC (bit 16) and PLLXTPRE (bit 17) and PLLMULL (bits 18-21) just in case there are some other values there, I want to set it to a know value of 0x00. I will explain what those bits are in the coming sentences.
Looking at our Clock tree diagram you see Number 2 has a value of "/1" this divided by one, in other words no real division, means our HSE of 8MHz will not get divided and the PLL will see all 8 Mhz.
In the STM32F1 this value can be "/1" or "/2" and it is controlled by the PLLXTPRE bit. Below is the explanation of that bit which is a bit confusing in the RM
Basically it says this PLLXTPRE bit is the same as bit 0 in the CFGR2 register, (but in the STM32F1 there is no CFGR2 register) then it says if the bits in CFGR2 are not set (which they are not because we dont have such a register) then this bit controls PREDIV1 (STEP2  in clock Tree)
If this PLLXTPRE bit is set to 1 then the HSE is divided by 2 ("/2"). and if this PLLXTPRE bit is set to 0 then HSE is not divided("/1") . DO NOT CONFUSE BIT SET TO 1 MEANS "/1"  anything divided by 1 means no division so that would be PLLXPRESET to 0

Next we set the PLL source which is Number 3 clock tree image. And it is bit 16 in the register. This tells the PLL multiplexer we are using the HSE passing through PREDIVE1 (which we configured above)  as the source to the PLL

Next we set the PLL multiplier to 7 which is Number 4 in the clock tree and bits 18-21 in the register.

Then we set the AHB prescaler which is Number 6 in the clock tree and bits 0 to  3 in the register, this will be Divide by 2.

Then we set the APB1 prescaler which is Number 7 in the clock tree and bits 8 to 10 in the register. This will be divide by 1 or "no division"

and then we set the APB2 prescaler which is Number 8 in the clock tree and bits 11 to 13 in the register. This will also be divide by one or no division.

The code looks like so:
 //configure RCC and PLL settings while PLL is off
 RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |  RCC_CFGR_PLLMULL); //reset
 
 RCC->CFGR &= ~(RCC_CFGR_PLLXTPRE);  //PLLXTPRE bit set to 0
 RCC->CFGR |= RCC_CFGR_PLLSRC;  //pll source
 RCC->CFGR |= RCC_CFGR_PLLMULL7;  //pll miultiplier 
 RCC->CFGR |= RCC_CFGR_HPRE_DIV2; //AHB prescaler
 RCC->CFGR |= RCC_CFGR_PPRE1_DIV1; //APB1 presacaler 
 RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; //APB2 prescaler
 

Once that is done we have to turn on the PLL which is done in the CR register of the RCC , which is the first register image posted above. This is done using bit 24, and we MUST wait for the PLL to be ready before using it by polling ther PLLRDY bit (PLL ready)
 //turn on PLL
 RCC->CR |= RCC_CR_PLLON; 
 while (!(RCC->CR & RCC_CR_PLLRDY)) 
  ;
 

Once the PLL is on we can select it as a source to the System Clock Multiplexer  which is Number 5 in the clock tree image and bits 0 to 1 in the CFGR register. and we also must wait for that to take effect by polling the SWS bits in the register..
 //set pll as clock source
 RCC->CFGR &= ~(RCC_CFGR_SW);
 RCC->CFGR |= RCC_CFGR_SW_PLL;
 while (!(RCC->CFGR & RCC_CFGR_SWS_PLL)) 
  ;

Now the HCLK is set to 28MHz and SYSCLK is 56 MHz , all of our peripherals run off the HCLK as seen in the clock tree. One last thing we must to is let the CMSIS core functions know that we have changed the clock because some of them depends on an accurate value for the variable called  SystemCoreClock  We can do this by calling a simple functions that looks at all the registers we just configured and it figures out the clock speed.
 SystemCoreClockUpdate();

This function must be called before you call the SysTick_Config function.. In fact if you are changing the clock freq this should be the first thing you do followed by the SystemCoreClockUpdate function. Because all your other things like UART baudrate and SPI I2C clocks all depend on you calculating the baud rate based on a correct number.

Putting it all together for you guys that love to copy and paste:
NOTE: this is just the raw code to get the job done with no error checking, what happens when the HSE is never ready? you'll be stuck in that while loop. So make sure you make your code robust in that respect.

void makeItGoFast(void)
{


 //turn on external crystal
 RCC->CR |= RCC_CR_HSEON;

 //wait for HSE crystal be stable
        //there are better ways to handle this but ill use simple polling
 while(!(RCC->CR & RCC_CR_HSERDY))
  ;
 
 
 //activate prefetch buffer but it should already be on
 FLASH->ACR |= FLASH_ACR_PRFTBE;

 // Flash 2 wait state 
 FLASH->ACR &= ~(FLASH_ACR_LATENCY);  //reset just to be sure
 FLASH->ACR |= (uint32_t)0x1;    

 
 //configure RCC and PLL settings while PLL is off
 RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |  RCC_CFGR_PLLMULL);  //reset
 
 RCC->CFGR &= ~(RCC_CFGR_PLLXTPRE);   //PLLXTPRE bit set to 0
 RCC->CFGR |= RCC_CFGR_PLLSRC;   //pll source
 RCC->CFGR |= RCC_CFGR_PLLMULL7;  //pll miultiplier 
 RCC->CFGR |= RCC_CFGR_HPRE_DIV2;  //AHB prescaler
 RCC->CFGR |= RCC_CFGR_PPRE1_DIV1;  //APB1 presacaler 
 RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;  //APB2 prescaler
 
  
 //turn on PLL
 RCC->CR |= RCC_CR_PLLON; 
 while (!(RCC->CR & RCC_CR_PLLRDY)) ;
 
 //set pll as clock source
 RCC->CFGR &= ~(RCC_CFGR_SW);  //reset just in case 
 RCC->CFGR |= RCC_CFGR_SW_PLL;
 while (!(RCC->CFGR & RCC_CFGR_SWS_PLL)) ;

 SystemCoreClockUpdate();
 
}