STM32 Timers Applications: Timer Cascading

Cascading two 16-bit timers on the STM32 allows you to build a fully synchronized 32-bit extended timer, overcoming the counting limitations of a single 16-bit peripheral. By configuring one timer as the master and the other as its hardware-driven slave, both counters operate as one continuous 32-bit unit—ideal for long-duration timing, precise period measurements, and high-resolution event capture.

In this guide, we shall cover the following:

  • Introduction.
  • Theory.
  • STM32CubeMX setup.
  • Importing project to STM32CubeIDE.
  • Firmware.
  • Results.

1. Introduction:

In many embedded applications, timing precision and extended measurement ranges are absolutely essential. Although STM32 microcontrollers provide high-performance general-purpose timers, most of these peripherals are limited to a 16-bit counter width, meaning they can represent a maximum count of 65,535 before overflowing. For tasks that require long-duration measurements, high-resolution timestamping, wide-range frequency analysis, real-time profiling, pulse counting, or long-window event accumulation, a 16-bit counter is usually insufficient. Reaching longer timing intervals typically requires either reducing the timer clock (losing resolution) or manually handling overflow interrupts—which increases CPU load and creates timing uncertainty at high clock speeds.

To overcome these limitations without compromising timing resolution or relying on software-driven overflow handling, we can cascade two 16-bit timers into one virtual 32-bit timer. This hardware-driven chaining allows Timer A to overflow and automatically clock Timer B, creating a continuous 32-bit count that rolls over after 4,294,967,295 counts. This technique preserves the full speed and accuracy of the timer clock while enabling timing windows that are 65,536 times longer than a single 16-bit counter can achieve. Cascading timers is therefore a powerful method for building extended-range counters, precise period measurement units, long-interval timestamp generators, and low-latency totalizers—without burdening the core processor.

2. Theory:

1. What Does Cascading Two Timers Mean?

In STM32 architecture, “cascading” or “chaining” timers refers to a master–slave configuration where one timer’s update event (UEV) acts as an input trigger or external clock for another timer. When the master timer completes a full 16-bit cycle and overflows, it issues a trigger output (TRGO). This TRGO is routed internally through the microcontroller’s timer interconnect matrix and is used as a clock source for the slave timer. Each overflow of the master results in one increment of the slave counter.

This results in a combined 32-bit counter, where:

  • The master timer counts the low 16 bits.
  • The slave timer counts the high 16 bits.
  • Together they form:
Combined_32bit_Value = (Slave_Count << 16) | Master_Count

STM32 Timer Interconnect: Trigger and Clock Sharing

STM32 timers include a flexible internal routing system called TIMx Internal Trigger (ITRx).
Each timer has dedicated internal trigger lines:

  • ITR0
  • ITR1
  • ITR2
  • ITR3

These lines allow one timer’s TRGO to be used as:

  • External clock mode 1 source
  • Trigger for reset
  • Trigger for gating
  • Trigger for synchronization of start/stop events

In cascaded 32-bit mode, the slave timer is set to External Clock Mode 1 (SMS = 111), using the master timer’s TRGO as its input clock.

Master Timer (Low-Order 16 Bits)

The master timer operates normally as a 16-bit up-counter.
Its configuration:

  • PSC (prescaler) determines timer tick frequency
  • ARR sets the period
  • Update event occurs every time the counter rolls over
  • TRGO (Trigger Output) is configured to emit on update event

Since ARR = 0xFFFF for a full 16-bit cycle, every overflow equals exactly 65,536 ticks.

Slave Timer (High-Order 16 Bits)

The slave timer is configured in:

External Clock Mode 1

  • SMS = 111 (external clock mode)
  • TS = ITRx (selecting TRGO from the master)
  • PSC = 0 (slave must count each incoming tick)
  • ARR = 0xFFFF

This means:

  • Each overflow of the master increments the slave by 1
  • The slave overflows only after (65,536 × 65,536) ticks
  • Total count range = 4,294,967,296 ticks

Result: True Hardware 32-Bit Counter

Because the slave increments only on master overflow, the combined counter behaves like a native 32-bit timer:

  • No interrupts required
  • No software overhead
  • Perfect synchronization
  • Zero jitter
  • Full hardware speed

3. STM32CubeMX Setup:

Open STM32CubeMX as start a new project as follows:

Search for your STM32 MCU, select the MCU and click on Start New Project as follows:

ext, from system core, select RCC and select either Crystal / Ceramic oscillator in case your board has external oscillator or bypass the oscillator in case of Nucleo board. Since this guide uses STM32F446 Nucleo-64, bypass oscillator will be used as follows:

Next, from Clock configuration, set the input frequency to 8MHz, PLL source to HSE and set HCLK frequency to 180MHz. Press enter to set the correct parameters.

Next, from pinout and configuration, select TIM1 from timers and set Channel and Input capture direct mode as follows:

Next, configure the timer as follows:

  • Set prescaler to 18-1. This will reduce the timer frequency to 10MHz.
  • Set counter period to 10000-1, this will set maximum frequency of 10KHz.
  • Set trigger Event selection to Update event.

Next, enable TIM1 capture compare interrupt as follows:

Next, enable TIM3 and set Slave mode to External Clock Mode 1 and trigger source to ITR0.

Leave the parameters as is.

From Project Manager:

Give the project a name, set the location, set toolchain/IDE to STM32CubeIDE and click on Generate Code.

Thats all for STM32CubeMX.

4. Import Project to STM32CubeIDE:

Open STM32CubeIDE, select your workspace and click on Launch.

From the IDE, click File and select STM32 Project Create/Import as follows:

Next, from Import STM32 Project, select STM32CubeMX/STM32CubeIDE Project and click on Next as follows:

Next, select the folder that contains the .ioc file and click on Finish as follows:

5. Firmware Development:

Open the main.c file.

In user code begin PV, declare the following variables:

volatile uint8_t Is_First_Captured=0;

volatile uint16_t counter1,counter3;

float frequency;
  • Is_First_Capture is related to detecting the first rising edge of the pulse.
  • counter1 and counter 3 to store the value of counter for TIM1 and TIM3 respectively.
  • frequency is the calculated frequency.

In user code begin 0, declare the following function:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)

{

	if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
		{
			if (Is_First_Captured==0) // if the first rising edge is not captured
			{
				TIM1->CNT = 0;
				TIM3->CNT = 0;
				Is_First_Captured = 1;  // set the first captured as true
			}

			else   // If the first rising edge is captured, now we will capture the second edge
			{
				counter1 = TIM1->CNT;
				counter3 = TIM3->CNT;

				frequency = ((float)10000000)/(float)((counter3*10000)+counter1);

				Is_First_Captured = 0; // set it back to false
			}
		}

}

For more information about input capture, please refer to this guide.

In user code begin 2 in main function, start timer 1 in capture mode and timer 3 in base mode as follows:

  HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
  HAL_TIM_Base_Start(&htim3);

Thats all for the guide.

Build the project and start a debugging session and add frequency to the live expression.

Note: I will use OLED to display the frequency, for how to interface the OLED display, you can refer to this guide.

6. Results:

10KHz Pulse signal

100KHz pulse signal.

200KHz pulse signal:

0.5Hz pulse signal:

Please note that you can read up to 300KHz. Beyond this, we need to use DMA.

Happy coding 😉

Add Comment

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