How to measure PWM signal in STM32
PWM (Pulse Width Modulation) signals are commonly used in embedded systems to represent information such as speed, position, or sensor data. In many real-world applications, it is not enough to generate a PWM signal — we also need to measure it. This includes finding the signal’s frequency and duty cycle accurately using the microcontroller.
In this second tutorial in the STM32 Timer Series, we will learn how to read a PWM input signal using an STM32 microcontroller. We will configure the timer in PWM input mode using STM32CubeMX and then use HAL functions to capture the signal’s high time and period. By the end of this tutorial, you will understand how to calculate the duty cycle and frequency of an incoming PWM signal and use this technique in applications such as motor feedback, RC receiver input, and signal monitoring.

PWM Signal Basics and PWM Input Working in STM32
What Is a PWM Signal?
PWM (Pulse Width Modulation) is a digital signal that alternates between HIGH and LOW states, but instead of varying voltage, it varies time.
A PWM signal is defined by three main parameters:
- Period – Total time taken for one complete cycle
- Frequency – How often the signal repeats (Frequency = 1 / Period)
- Duty Cycle – Percentage of time the signal stays HIGH in one period
Why these matter
- Duty cycle often represents power, speed, or position
- Frequency defines how fast the control or data updates
- Many peripherals (ESCs, motors, RC receivers) encode information using PWM
This makes PWM a very common signal type that needs to be measured, not just generated.
How PWM Input Works in STM32
STM32 microcontrollers measure PWM signals using timers, which are high-resolution hardware counters.
Role of Timers
- Timers continuously count at a known clock frequency
- The counter value represents elapsed time
- This allows precise time measurement between signal edges
What Is Input Capture?
- Input Capture is a timer mode where the counter value is latched automatically
- The capture happens when a selected signal edge occurs:
- Rising edge
- Falling edge
Measuring Duty Cycle and Frequency
STM32 uses both edges of the PWM signal:
- Rising edge → Falling edge
- Measures HIGH time
- Rising edge → Next rising edge
- Measures total period
Using these two values:
- Duty Cycle = (High Time / Period) × 100
- Frequency = Timer Clock / Period
This method is fully hardware-assisted, accurate, and efficient, making it ideal for reading PWM signals in real-time applications before moving on to the firmware implementation.
STM32 CubeMX Configuration
Clock Configuration
The image below shows the STM32 clock configuration.

The Nucleo-F446 has 8MHz crystal shared between ST-Link and the main MCU. We will use this 8MHz crystal along with PLL to generate the system clock.
- The system is running at 180 MHz
- The APB1 Timer clock (TIMER 2 Clock) is at 90 MHz
- The APB2 Timer Clock (TIMER 1 Clock) is at 180 MHz
Timer 1 Configuration (PWM out for Testing)
To measure the input PWM signal, we need a PWM signal. I am going to use the TIM1 to generate one. This will be useful as we will already know the pulse width and frequency, making it easier for us to debug any issue with the measured pulse.

- We already know that the APB2 Timer Clock is running at 180 MHz.
- Using the Auto Reload Value (ARR) of 1800, along with the prescaler of 1 (0 in the configuration) will set the PWM output Frequency at 100 KHz.
- The channel 1 is being used as the PWM Out channel.
- The duty cycle part will be covered in the code itself.
I have already covered how to generate the PWM with STM32. If you have any issues understanding these parameters, check out the linked tutorial.
Timer 2 Configuration (PWM Input)
Timer 2 will be used to measure the incoming signal. The image below shows the Timer 2 configuration for PWM input mode.
- The clock source is internal clock.
- The combined channels has been selected in the PWM Input mode on channel 1
- Note that only one Pin (PA0) has been selected as the input Pin, it is actually the channel 1 pin
- The channel 2 is internally connected to the channel 1, and this is to measure the duty cycle
- The prescalar is 0, so the timer clock will be same as APB 1 Timer clock, that is 90 MHz
- The polarity for the channel 1 is set to the rising edge, and it is set to falling edge for the channel 2.
- The IC Selection for the Channel 1 is direct, and here we are going to connect our input signal (PA0)
- The IC selection for the channel 2 is inDirect, and it is internally connected to the channel 1.
- The rest of the configuration is explained below
Internal Clock Division
This field indicates the division ratio between the timer clock (CK_INT) frequency and the dead-time and sampling clock (tDTS) used by the dead-time generators and the digital filters (ETR, TIx)
In the main setup, I have set it to NO DIVISION. This means the Sampling Clock will be same as the Internal clock, i.e. 90 MHz
Prescaler Division Ratio
This bit basically defines how often we want to do the capture on the input signal.
The CubeMX did not let me configure this part. But I will configure it later in the main code.
Setting this to 8 events would make it easier for the rest of the code to run. This way the captures will be limited, and less number of interrupts will be triggered.
Input Filter
This basically defines the sampling frequency. Also how many events does it take to validate a transaction.
The IC Filter configures the Sampling Frequency. It also configures the low pass filter, but I will update the information about that some other day.
For now I am choosing the filter as 0, so the Fsample = FDTS , which will be same as the internal clock.
STM32 Code to measure PWM Input
Generate PWM signal
Before we capture the PWM input signal, we need to generate it using Timer 1. Inside the main function, we will simply start the Timer 1.
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
TIM1->CCR1 = 900;Here I am starting Timer1 in the PWM mode. The Capture Compare Value is set to 900. With the ARR of 1800, this will generate a pulse with the duty cycle of 50% [(900/1800)*100].
We will measure this signal using the Timer 2 PWM Input Mode.
Start Timer 2
Inside the main function, we will start the TIM2 in the input capture mode. We need to start both the channels, even though the signal pin is connected to only one of them.
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // main channel
HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_2); // indirect channel- Here I am starting the input capture for the Channel 1 in the interrupt mode, and channel 2 in the normal mode.
- This way the interrupt handler will only be called, when the rising edge gets captured. And that much is enough to get it working.
Once the rising edge is captured, the Input Capture callback will be called, and we will write the rest of the code inside it.
Input Capture Callback
We will do our entire calculation inside the HAL_TIM_IC_CaptureCallback function.
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) // If the interrupt is triggered by channel 1
{
// Read the IC value
ICValue = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
if (ICValue != 0)
{
// calculate the Duty Cycle
Duty = (HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2) *100)/ICValue;
Frequency = 90000000/ICValue;
}
}
}- First we will check if the interrupt is called by the channel 1. This is just a formality, since we are using only one of the channel in interrupt mode.
- Then we will read the Input capture value and store it in the variable
ICValue. - In the first capture, this value will be 0. You will see this in the figure below
- In the second capture, we will get some non zero value, and this will be the period of the incoming signal.
- We will also read the capture for the falling edge. This will be the time, for which the signal was HIGH.
- Signal High Time / Period will give us the duty cycle of the signal.
- Also, the timer clock / ICValue gives us the frequency of the signal.
The calculation used in the code above is explained below with the figure from the reference manual.
As I mentioned above, the CubeMX did not let me configure the IC Prescaler. So I am configuring it in the code itself. I made a small change in the Timer 2 Configuration. TIM_ICPSC_DIV1 to TIM_ICPSC_DIV8.
This allows us to capture the signal once after every 8 events. This is used to slow down the interrupt generation rate is the signal frequency is very high.
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV8; // This was set to TIM_ICPSC_DIV1
sConfigIC.ICFilter = 0;
if (HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}Result of the PWM input Capture
The image below shows the result captured on the STM32CubeIDE Debugger console.
The Frequency is around 200 KHz, and the Duty Cycle is 49%. There is a limit of how much you can measure, and I did perform some experiments to check that.
You can see the video to understand this limitation.
VIDEO Tutorial
STM32 PWM Input Tutorial Video
This tutorial explains how to read and measure a PWM input signal using STM32 microcontrollers. Along with the written guide, this video walks through the timer configuration in CubeMX, input capture setup, and the HAL code used to calculate the duty cycle and frequency of an incoming PWM signal. Watch the video to clearly understand how PWM input works at both the hardware and code level.
Watch the VideoConclusion
In this tutorial, we covered how to read a PWM input signal using an STM32 microcontroller by configuring the timer in input capture mode. We discussed the basics of PWM signals, explained how STM32 timers use rising and falling edges to measure time, and walked through the CubeMX configuration and HAL-based code needed to capture the signal accurately. By measuring the high time and total period, we were able to calculate both the duty cycle and frequency of the incoming PWM signal.
Understanding PWM input is extremely useful in many real-world embedded applications where external devices communicate using PWM. This technique can be applied to read signals from RC receivers, motor controllers, sensors, and feedback systems without additional hardware. Since the measurement is handled by the timer peripheral, it is both precise and efficient, making it suitable for real-time applications and forming a strong foundation for more advanced timer-based features in STM32 projects.
Checkout More STM32 Timer Tutorials
STM32 TIMERS #3. How to use the ENCODER Mode
STM32 Timer #4: Input Capture Tutorial | Measure Frequency & Pulse Width
STM32 TIMERS #5. Timer synchronization || Slave Trigger mode
STM32 TIMERS #6. Timer synchronization || Generate 3 Phase PWM
STM32 TIMERS #7. Timer synchronization || Slave Reset mode
STM32 TIMERS #8. Make 48 bit Counter by Cascading Timers
STM32 Timers #9. One Pulse Mode
STM32 PWM Input Project Download
Info
You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.
STM32 PWM Input FAQs
Yes, but it is inaccurate and CPU-intensive. Timer input capture provides precise and hardware-assisted measurements.
The timer may miss edges if the clock or prescaler is not configured correctly, leading to incorrect measurements.
Yes, most STM32 timers support multiple channels, allowing multiple PWM signals to be measured simultaneously.
No, edge capture is handled by hardware, so measurements remain accurate even under high CPU load.
It works, but unstable signals may require input filtering or signal conditioning for reliable results.







As I understand timers 1, 8, 9, 10, 11 are connected to APB 2 and 2, 3, 4, 5, 6, 7, 12, 13, 14 to APB 1
Yes you are correct, that is a typing mistake. I will fix it.
When I use the ADC value to dynamically change the pulse value of Timer 1 Channel 1 (
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, adc_value );), why doesn’t the duty cycle change? That is,HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2)does not change along with the ADC value.I observed the PWM generated by Timer 1 Channel 1 using a logic analyzer, and it indeed changes according to the ADC value.
chibba chibba
If I change the prescaler of TIM2, can I measure lower frequcy PWM
Is it possible to simulate this example with Proteus (PWM/Timers) for F103 or F401 ?
Thank you for all STM32 tutorials
Hi, do u know how to setup it by DMA?
Input pin Only connected to channel 2.
If u are getting Ic_VAl2 as 0, use channel 3 in indirect mode. However i will advice not to use this. There are 2 more posts on how to measure pulse width and frequency. Use them.. i was supposed to delete this post but forgot.
Sir, In your code IC_Val2, is not getting updated. Hence duty cycle is zero. Also How falling is detected?
Did you followed the steps correctly? As shown above it detects falling edge. I wouldn’t prefer this method though. Look for another tutorial “measure pulse width using input capture”. That is more effective