Blog for my various projects, experiments, and learnings

“Bare Metal” STM32 Programming (Part 5): Timer Peripherals and the System Clock

As you start to create microcontroller projects with more complicated logic, it probably won’t take long before you to want to add some sort of time-based functionality. How should you ask your chip to do something on a schedule?

These days, we don’t have to count clock cycles in ‘delay’ methods. The STM32 line of chips have a variety of “timer” peripherals available, and they are flexible enough to use for all kinds of different things. The “advanced control” timer peripheral is particularly complicated and I won’t try to cover it in this quick overview, but the basic and general-purpose timers are easy to get started with for simple counters and interrupts.

In this tutorial, we will write code to enable a hardware interrupt triggered by a timer peripheral, and use that to blink an LED. Just like the last couple of posts in this series, the STM32F031K6 and STM32L031K6 chips will be used to demonstrate these concepts. Since this tutorial will only use a single LED, you won’t need anything other than an affordable “Nucleo” board with one of those chips and a micro-USB cable:

"Nucleo-32" Board with Blinking LED

You don’t even need a breadboard!

You can double-check that the clock speeds are what we expect by counting the LED’s on/off timings against a clock. It should change about 60 times every minute with the timer set to 1 second, although the internal oscillator is not quite as precise as an external “HSE” crystal oscillator would be. As usual, an example project demonstrating this code is available in a Github repository.

The System Clock

Before you actually write code to initialize a timer, you need to understand a little bit about the STM32’s core system clock. These timer peripherals use the system’s core clock signal to derive their timings by default, so it is important to know how fast the chip is running when you configure them.

To explain the system clock’s initialization in more detail, let’s look at some basic logic to get the chip running at its maximum speed; this can go at the beginning of the ‘main’ C method, if we’re okay with the initial assembly startup code running at the chip’s default speed. The F0 and L0 lines have slightly different clock configurations, but I will highlight those differences when they come up.

Clock Sources

The core clock signal is what drives the program logic on a microcontroller. Every time that the clock signal switches between a high and low signal level, an instruction can be run by the processor. There are several types of clock source that you can use, including some “external” sources like crystal oscillators or signal generators.

When the STM32 first starts up or is reset, the chip automatically selects an 8MHz “HSI” (High-Speed Internal) oscillator on an STM32F0 chip, or a 2.1MHz “MSI” (Multi-Speed Internal) oscillator on an STM32L0 chip. STM32L0 chips also have a 16MHz HSI oscillator, but it is turned off by default. In this tutorial, we’ll use the HSI oscillators to drive the STM32’s “PLL” (Phase-Locked Loop) clock at the maximum speed allowed on the chip – 48MHz for an STM32F0 or 32MHz for an STM32L0.

Don’t worry if you don’t remember all of those acronyms – you can ‘Ctrl+F’ for them in the datasheets or reference manuals when you need a refresher. Even the 3-letter ones like “HSI” and “PLL” are usually pretty unique. It’s a good idea to save your chips’ reference manuals and datasheets for future reference; here is the manual for most STM32F0 chips, and here is the manual for STM32L0x1 chips.

Besides searching for keywords, the table of contents is also a good starting point, and you can find a lot of good information about the chip’s clock sources in the “Clocks” section of the RCC chapter (Section 6.2 in the F0 manual and Section 7.2 in the L0 manual.)

Flash Memory Latency

As you probably know by now, the STM32 stores its programs in nonvolatile ‘Flash’ memory for most simple projects. This is similar to the sort of memory used in USB thumb drives, and it sounds like the chips can reliably read data from it at a maximum speed of about 24MHz. What that means is that if our chip will be running faster than 24MHz, we need to tell it to slow down for one or more ‘wait states’ when it needs to retrieve data from memory.

On STM32F0 chips, we can do that by setting the LATENCY bits in the FLASH_ACR (Access Control Register) to the number of ‘wait states’ that we want to use. For <=24MHz, we can use the default 0 wait states. For 24-48MHz, we need to use at least 1 wait state. For 48-72MHz, at least 2 wait states are required. It goes on like that, but since the fastest speed on these chips is 48MHz, we can use a single wait state for both the STM32L0 and STM32F0 lines.

There is also a PRFTBE bit, which enables the “Flash Prefetch Buffer”. This fetches a handful of instructions at once from the Flash memory, to blunt the impact of waiting on the 24MHz flash memory access. It is a very small buffer on these small chips, but every little bit helps, so I’ll use it.

The STM32L0 line is a bit different, but it mostly has the same core options. The LATENCY (‘number of wait states’) field is just a single bit, and the “Enable Prefetch” bit is called PRFTEN. There is also a PRE_READ bit in STM32L0 chips, which looks like it tells the chip to start fetching the next instruction from Flash before the current one is done executing.

So here is some example code for setting up the FLASH_ACR register for a 32-48MHz clock speed:

#ifdef VVC_F0
  // Reset the Flash 'Access Control Register', and
  // then set 1 wait-state and enable the prefetch buffer.
  // (The device header files only show 1 bit for the F0
  //  line, but the reference manual shows 3...)
  FLASH->ACR &= ~(0x00000017);
  FLASH->ACR |=  (FLASH_ACR_LATENCY |
                  FLASH_ACR_PRFTBE);
#elif VVC_L0
  // Set the Flash ACR to use 1 wait-state
  // and enable the prefetch buffer and pre-read.
  FLASH->ACR |=  (FLASH_ACR_LATENCY |
                  FLASH_ACR_PRFTEN |
                  FLASH_ACR_PRE_READ);
#endif

Remember that the VVC_F0 or VVC_L0 options are set in the Makefile depending on which line of chip the program is being built for.

Frequency Multiplication and Division

To get a specific frequency out of the Phase-Locked Loop peripheral, we need to tell it a few things about the signal that we want to generate:

  1. What clock source should the PLL use for its base signal?
  2. How much should the PLL multiply that signal’s frequency by?
  3. (Sometimes optional) How much should the PLL divide that signal’s frequency by?

Which Clock Source?

The STM32F0 line has four options for the PLL clock source:

  • HSI / 2: The PLL’s core clock source is set to the HSI oscillator divided by 2, which is 4MHz. With this option selected, you don’t need to select a specific division factor.
  • HSI / PREDIV: Similar to the HSI / 2 option, but the HSI signal is divided by the value in the PREDIV bits of RCC_CFGR2.
  • HSE / PREDIV: The “HSE” (High-Speed External) oscillator is used as the PLL’s base signal, after being divided by the PREDIV bits of RCC_CFGR2.
  • HSI48 / PREDIV: Some of the more advanced STM32F0 chips have a 48MHz HSI oscillator built in, and that can be selected as a core source for the PLL. But the STM32F031K6 used in this tutorial does not have an HSI48 oscillator.

I’ll use the HSI / 2 signal, to get a nice and round 4MHz signal which we can multiply by 12 to get 48MHz.

The STM32L0 line has two options for the PLL clock source:

  • HSI16: The 16MHz “HSI” oscillator is used as the PLL core clock source, divided by a factor set by the PLLDIV bits in the RCC_CFGR register.
  • HSE: The “HSE” oscillator is used as the PLL core clock source, after being divided by a factor set by the PLLDIV bits.

I’ll use the HSI16 signal, divide by the minimum factor of 2, and then multiply that by 4 to get 32MHz.

Signal Multiplication and Division

In the STM32F0 line, the PREDIV values in the RCC_CFGR2 register are used to automatically divide the selected clock signal by a certain factor. Using the HSI / 2 option for the PLL’s clock source lets us ignore these bits by using a constant division factor of 2, though – see the “RCC” peripheral section of your chip’s reference manual for more details.

In the STM32L0 line, there is no RCC_CFGR2 register and no PREDIV bits. Instead, the PLL source is always divided by the PLLDIV bits in the RCC_CFGR register. The lowest allowable PLLDIV value is 2, so we can’t simply take the HSI oscillator’s 16MHz signal and multiply it by 2 to get 32MHz.

In both the STM32F0 and STM32L0 lines, once the division settings are chosen, the final PLL signal is created by multiplying the post-division frequency by the PLLMUL bit settings in the RCC_CFGR register. The actual bit values do not necessarily correspond to the same multiplication factors across STM32 lines, so check the “RCC Registers” section of your chip’s reference manual if you want to better understand the device header files’ macro definitions.

That might sound like a complicated process. but the basic equation is very simple:

PLL_frequency = (input_frequency / PLL_division) * PLL_multiplication

On the STM32F031K6 our values will be:

48MHz = (8MHz / 2) * 12

And on the STM32L031K6 it will be:

32MHz = (16MHz / 2) * 4

Initializing and Enabling the PLL

Following the above steps, here is some example C code to initialize the PLL to 48MHz on the STM32F031K6, or 32MHz on the STM32L031K6.

#ifdef VVC_F0
  // Configure the PLL to (HSI / 2) * 12 = 48MHz.
  // Use a PLLMUL of 0xA for *12, and keep PLLSRC at 0
  // to use (HSI / PREDIV) as the core source. HSI = 8MHz.
  RCC->CFGR  &= ~(RCC_CFGR_PLLMUL |
                  RCC_CFGR_PLLSRC);
  RCC->CFGR  |=  (RCC_CFGR_PLLSRC_HSI_DIV2 |
                  RCC_CFGR_PLLMUL12);
  // Turn the PLL on and wait for it to be ready.
  RCC->CR    |=  (RCC_CR_PLLON);
  while (!(RCC->CR & RCC_CR_PLLRDY)) {};
  // Select the PLL as the system clock source.
  RCC->CFGR  &= ~(RCC_CFGR_SW);
  RCC->CFGR  |=  (RCC_CFGR_SW_PLL);
  while (!(RCC->CFGR & RCC_CFGR_SWS_PLL)) {};
  // Set the global clock speed variable.
  core_clock_hz = 48000000;
#elif VVC_L0
  // Enable the HSI oscillator, since the L0 series boots
  // to the MSI one.
  RCC->CR    |=  (RCC_CR_HSION);
  while (!(RCC->CR & RCC_CR_HSIRDY)) {};
  // Configure the PLL to use HSI16 with a PLLDIV of
  // 2 and PLLMUL of 4.
  RCC->CFGR  &= ~(RCC_CFGR_PLLDIV |
                  RCC_CFGR_PLLMUL |
                  RCC_CFGR_PLLSRC);
  RCC->CFGR  |=  (RCC_CFGR_PLLDIV2 |
                  RCC_CFGR_PLLMUL4 |
                  RCC_CFGR_PLLSRC_HSI);
  // Enable the PLL and wait for it to stabilize.
  RCC->CR    |=  (RCC_CR_PLLON);
  while (!(RCC->CR & RCC_CR_PLLRDY)) {};
  // Select the PLL as the system clock source.
  RCC->CFGR  &= ~(RCC_CFGR_SW);
  RCC->CFGR  |=  (RCC_CFGR_SW_PLL);
  while (!(RCC->CFGR & RCC_CFGR_SWS_PLL)) {};
  // Set the global clock speed variable.
  core_clock_hz = 32000000;
#endif

Notice how the device header macro definitions are used. In the STM32F0 logic, the RCC_CFGR_PLLMUL macro covers all of the bits in the PLLMUL field, so resetting its bitwise opposite sets the entire field to 0 in the register. Then, the RCC_CFGR_PLLMUL12 setting has the value for multiplying by 12 in the specific chip being used.

It is possible that those settings will be represented by different values on a chip other than the STM32F031K6, so using the device header files’ definitions instead of raw hex values helps you to write code which works across the whole line of STM32F0 chips, and sometimes even the whole family of STM32s.

Once the clock speed is set up, it is a good idea to store the current system clock frequency in a global variable; I’ll use a uint32_t core_clock_hz in this tutorial.

Using a Timer Peripheral

With the core system clock set to a known frequency, we can use the chip’s built-in “timer” peripherals to trigger actions at fairly precise timing intervals. The timer peripherals can perform a variety of time-based actions, and there are good explanations in the reference manuals. I would recommend reading about the basic and general-purpose timers before braving the complicated “Advanced Control” timer (called TIM1).

You can also check your chip’s reference manual for more information about the timer peripherals. TIM2, which I’ll use in this tutorial, is covered in Section 16 of the STM32L0 reference manual and/or Section 18 of the STM32F0 reference manual.

Enabling the Peripheral

While I’ll use the TIM2 general-purpose timer as an example, it looks like the other “general-purpose” timers like TIM14, TIM16, and TIM17 are exactly the same. The STM32F031K6 has all three of those available, but the STM32L031K6 does not.

The first step is to enable the peripheral in its RCC register. TIM2 and TIM14 are in the APB1 clock domain, so we’ll use APB1ENR to reset them and APB1RSTR to reset them. If you were using TIM16 or TIM17, you’d want to use the APB2 registers instead. And the NVIC interrupts are set up in the same way that we set up the EXTI ‘button press’ interrupts in the last tutorial:

// Enable the TIM2 clock.
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// Enable the NVIC interrupt for TIM2.
NVIC_SetPriority(TIM2_IRQn, 0x03);
NVIC_EnableIRQ(TIM2_RQn);

Setting a ‘Counter’ Interrupt

With the peripherals enabled and the NVIC hardware interrupt enabled, it is fairly simple to set a timer interrupt to trigger every N milliseconds using the timer’s counter. You’ll only need to use 6 timer registers:

  • CR1: ‘Control Register 1’ – this register is used to enable and disable the peripheral.
  • CNT: ‘Counter Register’ – this register hold’s the timer’s current counter value. It counts up from 0 once the timer is started.
  • PSC: ‘Prescaler Register’ – this register will hold the timer’s prescaler. A prescaler value of N will tick the timer’s counter register up by one every N+1 clock cycles.
  • ARR: ‘Autoreload Register’ – the autoreload value is the ‘period’ of the timer. An autoreload value of N will cause the timer to trigger an update event every time the CNT register counts up to N.
  • EGR: ‘Event Generation Register’ – Setting the UG bit in this registers resets all of the timer’s counters and tells it to use the currently-set prescaler/autoreload values.
  • DIER: ‘DMA/Interrupt Enable Register’ – Setting the UIE bit in this register sets the timer to trigger a hardware interrupt when an update event occurs. Usually, that happens when the ‘Counter’ register matches the ‘Autoreload’ value.
  • SR: ‘Status Register’ – The UIF flag is set in this register when a timer’s hardware interrupt triggers, and must be cleared before another one can occur.

We’ll set the ‘Prescaler’ so that the timer counts up every millisecond, and then set the ‘Autoreload’ value to however many milliseconds the timer should measure. Then we’ll set the EGR_UG bit to apply those settings, the DIER_UIE bit to enable an interrupt, and start the timer:

void start_timer(TIM_TypeDef *TIMx, uint16_t ms) {
  // Start by making sure the timer's 'counter' is off.
  TIMx->CR1 &= ~(TIM_CR1_CEN);
  // Next, reset the peripheral. (This is where a HAL can help)
  if (TIMx == TIM2) {
    RCC->APB1RSTR |=  (RCC_APB1RSTR_TIM2RST);
    RCC->APB1RSTR &= ~(RCC_APB1RSTR_TIM2RST);
  }
  #ifdef VVC_F0
  else if (TIMx == TIM14) {
    RCC->APB1RSTR |= (RCC_APB2RSTR_TIM14RST);
    RCC->APB1RSTR &= ~(RCC_APB2RSTR_TIM14RST);
  }
  else if (TIMx == TIM16) {
    RCC->APB2RSTR |=  (RCC_APB2RSTR_TIM16RST);
    RCC->APB2RSTR &= ~(RCC_APB2RSTR_TIM16RST);
  }
  else if (TIMx == TIM17) {
    RCC->APB2RSTR |=  (RCC_APB2RSTR_TIM17RST);
    RCC->APB2RSTR &= ~(RCC_APB2RSTR_TIM17RST);
  }
  #endif
  // Set the timer prescaler/autoreload timing registers.
  // (These are 16-bit timers, so this won't work with >65MHz.)
  TIMx->PSC   = core_clock_hz / 1000;
  TIMx->ARR   = ms;
  // Send an update event to reset the timer and apply settings.
  TIMx->EGR  |= TIM_EGR_UG;
  // Enable the hardware interrupt.
  TIMx->DIER |= TIM_DIER_UIE;
  // Enable the timer.
  TIMx->CR1  |= TIM_CR1_CEN;
}

Note that these timers are 16-bit, so the maximum values allowed in the Prescaler and Autoreload registers is 65,535. That means that this timer method won’t work for clock speeds higher than about 65MHz, or delays longer than about a minute (60,000 milliseconds).

Blinking the Onboard LED

You can initialize the on-board LED pin just like in the past couple of tutorials, and toggle it in the timer interrupt using the ^ (“XOR”) operator. Remember that the SR ‘Status’ register needs to be cleared to let future interrupts trigger, so this is what an example interrupt handler might look like:

void TIM2_IRQ_handler(void) {
  // Handle a timer 'update' interrupt event
  if (TIM2->SR & TIM_SR_UIF) {
    TIM2->SR &= ~(TIM_SR_UIF);
    // Toggle the LED output pin.
    GPIOB->ODR ^= (1 << LED_PIN);
  }
}

You can see how these methods are declared and used in the example Github repository, but it’s fairly simple once you understand the registers. Also, stopping one of these timers after it is started is very easy:

void stop_timer(TIM_TypeDef *TIMx) {
  // Turn off the timer.
  TIMx->CR1 &= ~(TIM_CR1_CEN);
  // Clear the 'pending update interrupt' flag, just in case.
  TIMx->SR  &= ~(TIM_SR_UIF);
}

Conclusions

Besides performing actions on a consistent schedule, these timer peripherals are also a good way to make fairly accurate ‘delay’ or ‘sleep’ methods. You can start a timer for the time that you want to delay, wait for a variable to be set in the interrupt handler, and then stop the timer. Or, you can skip setting up the hardware interrupt and check the timer’s CNT register in a while loop.

Timers are a fundamental part of many embedded systems, so it’s good to get a basic feel for these peripherals early on. As usual, an example project demonstrating the code in this tutorial is available on Github.

I’m hoping to cover some more advanced timer functionality like PWM and input capture in a future post. I’d also like to learn and write a bit more about the ‘SysTick’ peripheral, which is a simple 24-bit timer available on most ARM Cortex-M cores. It seems like a good way to run recurring logic on a schedule, and I think that it might be a common way for RTOS systems to drive schedule timings.

Leave a Reply

Your email address will not be published. Required fields are marked *