STM32 PWM Output with HAL: TIM1 Setup, Duty Cycle Control and DMA Examples
Pulse width modulation (PWM) lets a microcontroller control the power delivered to a load by using only a digital output pin. It is used to dim LEDs, set motor speed, position servos, and produce simple audio signals. On an STM32, the timer peripheral generates the waveform in hardware, so the CPU stays free for other work while the output runs.
This tutorial shows how to generate PWM on an STM32 using TIM1, STM32CubeMX, and the HAL library. It covers three methods: basic output without DMA, DMA in Normal mode for a single sequence of duty cycles, and DMA in Circular mode for a waveform that repeats continuously. All examples run on an STM32F103C8 (BluePill), and every result was captured with a logic analyzer.
This is the first tutorial in the STM32 Timer Series.
Table of contents
STM32 PWM Basics: Frequency and Duty Cycle
PWM works by switching a digital signal on and off at a fixed rate. The connected device responds to the average value of the signal instead of each individual pulse, so the result behaves like an adjustable analog level.
Two values describe every PWM signal. The frequency is the number of complete on-off cycles that occur each second. The duty cycle is the share of each cycle during which the signal stays on, and it is normally given as a percentage.
A 50% duty cycle means the signal is on for half of each cycle, so the load receives about half of the maximum power. A 75% duty cycle delivers more power because the signal stays on longer, and a 25% duty cycle delivers less. Changing the duty cycle is therefore enough to control the behavior of the load.
Common uses include LED dimming, motor speed control, and heater power regulation. The switching happens fast enough that the eye or the mechanical system sees a smooth result instead of individual pulses.
PWM is also efficient. The output is always either fully on or fully off, so very little energy is lost as heat in the driver. Resistors and linear regulators, which burn off the excess power, cannot match this efficiency.
How STM32 Timers Generate PWM Signals
STM32 timers can produce PWM directly in hardware. They run independently of the CPU, which allows accurate waveforms with almost no processing load.
Three timer settings are involved in most PWM setups.
- Up-counting mode: The counter starts at 0 and counts up to the value stored in the Auto-Reload Register (ARR). After it reaches that value, it returns to 0 and starts again.
- PWM Mode 1: The output stays HIGH while the counter is below the compare value (CCR) and switches LOW once the counter passes it.
- PWM Mode 2: The output behaves in the opposite way. It stays LOW while the counter is below CCR and goes HIGH afterward.
The diagram below shows how the counter, ARR, and CCR work together. The blue line is the counter rising to ARR, the red line is the CCR value that decides when the output changes state, and the green waveform is the resulting PWM signal.
STM32 PWM Frequency and Duty Cycle Formulas
The PWM frequency depends on the timer clock, the prescaler (PSC), and the auto-reload value (ARR).
fPWM = ftimer / ((PSC + 1) × (ARR + 1))
The duty cycle depends on the compare value (CCR) and ARR.
Duty (%) = CCR / (ARR + 1) × 100
For example, with ARR = 999 and CCR = 500, the duty cycle is about 50%, so the signal is HIGH for half of each cycle and LOW for the other half. The image below shows the same waveform at 25%, 50%, and 75% duty cycle.
STM32 PWM Setup in CubeMX
This project uses the STM32F103C8 (BluePill). The setup starts with the clock, then moves to the timer and DMA.
Clock Configuration for TIM1
The board uses an external 8 MHz crystal, and the PLL raises the system clock to the 72 MHz maximum. TIM1 sits on the APB2 bus, and the APB2 timer clock is also 72 MHz, so TIM1 receives 72 MHz.
TIM1 Timer Settings for PWM
The image below shows the TIM1 configuration used in this project.
The main settings are listed here.
- Clock Source: Internal Clock. The timer counts using the internal APB2 clock, which gives accurate timing without external parts.
- Channel1: PWM Generation CH1. This option turns TIM1 channel 1 into a PWM output. The option named Output Compare CH1 looks similar but does not produce PWM.
- Prescaler (PSC) = 72-1. The timer clock is divided by 72, so the counter runs at 1 MHz.
- Counter Mode: Up. The counter runs from 0 to ARR and then restarts.
- Counter Period (ARR) = 100-1. The counter has 100 steps per cycle, from 0 to 99.
- Mode: PWM mode 1. The output is HIGH while the counter is below the pulse value and LOW otherwise.
- Pulse (CCR1) = 0. The initial duty cycle is zero, so the output stays LOW until the code sets a new value.
- Pin: PA8 (TIM1_CH1). CubeMX configures this pin as an alternate function output, and the PWM signal appears here.
These settings give the following frequency.
fPWM = 72 MHz / (72 × 100) = 10 kHz
Because ARR + 1 equals 100, each CCR value maps directly to a duty cycle percentage, which keeps the later examples easy to follow.
For STM32F4 devices, the STM32F4 PWM calculator can work out the PSC and ARR values for a target frequency.
DMA Settings for PWM
The DMA settings below are needed for the two DMA examples later in this tutorial.
Any DMA stream that supports the selected timer channel can be used. The direction must be set to Memory to Peripheral, because DMA reads the duty cycle values from memory and writes them into the timer’s CCR register. Memory increment must be enabled so DMA steps through the array, and peripheral increment must stay disabled so that every value is written to the same CCR register.
The data width is set to Half Word (16 bits, uint16_t), since the CCR registers of most STM32 timers are 16 bits wide.
DMA offers two modes for this job.
- Normal mode: DMA moves the array to the timer once and then stops. This suits a single burst of pulses, and the transfer must be started again for another burst.
- Circular mode: DMA returns to the start of the array after the last value and keeps going until it is stopped. This suits repeating patterns such as sine or triangle waves, and the CPU does not need to restart anything.
Why PSC and ARR Use Minus One
The PSC and ARR registers store a value that is one lower than the real division factor or count length. A PSC register value of 71 divides the clock by 72, and an ARR register value of 999 gives a counter that runs through 1000 steps. This is the reason the formulas add 1 to both values, and it is also the reason CubeMX examples are often written as 72-1 and 100-1.
STM32 PWM Code Without DMA
When CubeMX generates the project, it creates an initialization function named MX_TIM1_Init(). This function applies the PSC, ARR, and PWM mode settings, and it also sets the PA8 pin to its alternate function so the signal reaches the pin.
Start the PWM Output
The PWM output is started with a single HAL call.
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);In this call, htim1 is the timer handle created by CubeMX, and TIM_CHANNEL_1 selects the channel. Timers with more channels use TIM_CHANNEL_2, TIM_CHANNEL_3, and TIM_CHANNEL_4 in the same way, and each channel needs its own call.
Change the PWM Duty Cycle at Runtime
The duty cycle is changed by writing a new value to the CCR register of the channel.
TIM1->CCR1 = 30; // CCR value must not be larger than ARRA higher CCR value gives a longer pulse and a higher duty cycle, while a lower value gives a shorter pulse. HAL provides an equivalent macro, __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, value), which works with any channel. With ARR set to 99, a CCR value of 30 gives a 30% duty cycle.
Complete Example Code
The main function below raises the duty cycle from 0% to 100% in steps of 10%, with a delay of 500 ms between steps.
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM1_Init();
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
while (1)
{
for (int duty = 0; duty <= 100; duty += 10)
{
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty);
HAL_Delay(500);
}
}
}A CCR value of 100 is larger than ARR, so the output stays HIGH for the whole cycle, which is a duty cycle of 100%.
Result on the Logic Analyzer
The capture below was taken with a USB logic analyzer while CCR1 was set to 30.
The measured signal has a frequency of 10 kHz, so each cycle lasts 100 microseconds. The signal stays HIGH for 30 microseconds and LOW for 70 microseconds in every cycle, which confirms the 30% duty cycle. The pulse spacing and width are consistent from cycle to cycle.
The animation below shows the duty cycle stepping from 0% to 100% every 500 ms.
STM32 PWM with DMA in Normal Mode
In Normal mode, an array of pulse width values is stored in memory, and DMA copies the values one by one into the timer’s CCR register. The duty cycle therefore changes by itself without any CPU involvement. After the last value is transferred, DMA stops, and the output stays at the final duty cycle.
This method reduces the CPU load because the duty cycle updates are handled entirely by DMA.
uint16_t pwmData[11];
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM1_Init();
pwmData[0] = 10; pwmData[1] = 20; pwmData[2] = 30;
pwmData[3] = 40; pwmData[4] = 50; pwmData[5] = 60;
pwmData[6] = 70; pwmData[7] = 80; pwmData[8] = 90;
pwmData[9] = 100; pwmData[10] = 0;
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, (uint32_t *)pwmData, 11);
while (1)
{
}
}Because ARR is 99, each array value matches a duty cycle percentage, so a value of 10 gives 10% and a value of 20 gives 20%. The final value of 0 forces the output LOW when the sequence ends. The function HAL_TIM_PWM_Start_DMA also starts the PWM channel, so a separate call to HAL_TIM_PWM_Start is not needed.
Result of DMA Normal Mode
The capture below shows the complete output. It is a single burst of pulses with increasing duty cycles, and the frequency stays at the 10 kHz that was set in CubeMX.
The next image shows the duty cycles in detail. They range from 10% (a 10 microsecond pulse) up to 100%, which appears as a constant HIGH level and cannot be shown as a pulse.
STM32 PWM with DMA in Circular Mode
Circular mode uses the same array-based method, but DMA starts over from the first value after it reaches the end of the array. The pattern therefore repeats without any action from the CPU, which makes this mode a good fit for sine waves, triangle waves, and other repeating patterns. The DMA channel must be set to Circular mode in CubeMX, as described in the DMA settings section above.
uint16_t pwmData[13];
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM1_Init();
pwmData[0] = 10; pwmData[1] = 20; pwmData[2] = 30;
pwmData[3] = 40; pwmData[4] = 50; pwmData[5] = 60;
pwmData[6] = 70; pwmData[7] = 80; pwmData[8] = 90;
pwmData[9] = 100; pwmData[10] = 0; pwmData[11] = 0;
pwmData[12] = 0;
HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, (uint32_t *)pwmData, 13);
while (1)
{
}
}The array ends with three zeros, which hold the output LOW for a short time at the end of each pass. This gap makes it easy to see where one repetition ends and the next begins on the logic analyzer.
Result of DMA Circular Mode
The capture below shows the continuous output. In every repetition the duty cycle rises from 10% to 100%, and the output then stays LOW for the length of the three zero values.
Stop Circular DMA After a Set Number of Cycles
When DMA reaches the end of the array, it raises an interrupt that calls HAL_TIM_PWM_PulseFinishedCallback. This callback is a convenient place to stop the transfer or to run other code once a pass is complete.
int count = 0;
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
count++;
if (count == 3)
{
HAL_TIM_PWM_Stop_DMA(htim, TIM_CHANNEL_1);
}
}The counter increases by one each time the callback runs. When it reaches 3, HAL_TIM_PWM_Stop_DMA stops the transfer, so the output completes three full passes and then ends.
A more practical use of Circular mode is covered in the tutorial on STM32 PWM using DMA.
Conclusion
This tutorial covered PWM generation on an STM32 using TIM1, CubeMX, and HAL. It explained the frequency and duty cycle formulas, showed how to change the duty cycle from code, and used DMA in Normal and Circular modes to play back a sequence of duty cycles.
Once the relationship between PSC, ARR, and CCR is clear, any frequency and duty cycle can be produced. DMA goes one step further by removing the CPU from the duty cycle updates, which is useful for smooth waveforms and motor control patterns.
The next part of the series covers measuring a PWM input signal, which shows how to read the frequency and duty cycle of a signal coming from a sensor or an RC receiver.
Download Project Here
Download the CubeMX project files and HAL source code — all tested on real hardware.
If you find this useful, you can support us with a Donation as well.
Browse More STM32 Timer Tutorials
STM32 Timers (Part 3): How to use the Timer Encoder Mode
STM32 Input Capture: Measure Frequency & Pulse Width
STM32 Timer Synchronization: Slave Trigger Mode with CubeMX & HAL
STM32 Timers (Part 6): Timer Synchronization for 3-Phase PWM Generation
STM32 Timers (Part 7): Timer synchronization using Slave Reset mode
Arun is an embedded systems engineer with 10+ years of experience in STM32, ESP32, and AVR microcontrollers. He created ControllersTech to share practical tutorials on embedded software, HAL drivers, RTOS, and hardware design — grounded in real industrial automation experience.
Recommended Tools
Essential dev tools
Categories
Browse by platform













hello
im usin stm32f407vet6 to control bldc motor for my graduation project
idk whats the teacher sayin about the frequency must be 5kHz he said use only PWM and dont use the adc the sys frequency is 84MHz
can u plz help in configuring the card also help on codin !
This article explains the exact thing, how to generate PWM with some fixed frequency. Please read it, or watch the video.
Para que utilizas interrupciones! solo quiero enviar una señal pwm solo eso. You code while is empty not interrpt
Where did I used interrupt ?
Great example! Worked fine for me for anyone who can’t get it working
1) Make sure PWM Timer is enabled.
2) Make sure CCR value is correct.
3) Double check Timer clock settings.
Thanks for your tutorial.
I switched from Microchip to STM32 and everything is a little bit different^^
I use the Board Nucleo G474RE and got the PWM-Output working. So I wanted to use more outputs with the same PWM-Timer and set the CCR for Channel 2, 3 and 4 of TIM1.
Unfortunately I get only an output on channel 1.
I tried this:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1 | TIM_CHANNEL_2 | TIM_CHANNEL_3 | TIM_CHANNEL_4);
this:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
and this: //makes no sense for me…
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1 & TIM_CHANNEL_2 & TIM_CHANNEL_3 & TIM_CHANNEL_4);
with the CCR-Settings:
TIM1->CCR1 = 120; // PWM OUTPUT is on PIN A5 (PC0)
TIM1->CCR2 = 120; // PWM OUTPUT is on PIN A4 (PC1)
TIM1->CCR3 = 120; // PWM OUTPUT is on opposite Pin of A5 (PC2)
TIM1->CCR4 = 120; // PWM OUTPUT is on opposite Pin of A4 (PC3)
best regards
Yeah, I got it running.
Unfortunately I selected “Output Compare CHx” instead of “PWM Generation CHx”.
I want to see how you wire up the complete circuit and what are the necessary links
Hi I want to use example but with an interrupt, can I modify CCRx while using interrupt? if not I can just use the example as is. But will I get accurate timing with the cpu running other parts of the code if I don’t use interrupt?
Hi I want to use example but with an interrupt? can I modify CCRx while using interrupt? if not I can just use the example as is. But will I get accurate timing with the cpu running other parts of the code if I don’t use interrupt?
I used the same configuration, but the last example didnt work either. There is no signal output at TIM_CH1. The others worked as expected.
I use STM32F410RB board.
There is nothing in this example, so “not working” is not possible.
Check your CCR value, also if you have started the timer in PWM mode.
Also make sure you are measuring the output from the right pin.
You are the best! Thanks for all of your work.
I am using the STM32F401RE with the same code and same configuration, I confirmed the oscillator is 8Mhz and check that TIM1 is on APB1 – when I connect my logic analyzer I see nothing, no PWM signal.
is there something I can check to figure out what I am missing ?
2 things.
Thanks, I fixed the problem – another question, to bring the frequency to 100 kHz, the ARR to 10 instead of 100 ?
yes
That works like a charm !
I am running a Nucleo-64 with a STM32F401RE on it and the PWM frequency, as clocked by APB1, is nothing compared to what it should be. Say APB1 clock frequency is 84MHz and the pre-scale is 20 I get 19.84MHz on the PWM output?
In the clock setup make sure that input frequency is 8 MHz and not 25 MHz
How to generate a center aligned pwm.
Because the PWM is a periodic signal it is center aligned.
But it is easier to to read the time if you don’t view it as center aligned.