Home ▸ STM32 Tutorials ▸ STM32 Timers Series ▸ How to Generate PWM Output

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.

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 timer diagram showing the counter rising to ARR, the CCR compare level, and the PWM output switching when the counter crosses CCR.

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.

Three PWM waveforms with 25%, 50%, and 75% duty cycle, showing how the ON time grows as the duty cycle increases.

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.

STM32CubeMX clock configuration for the BluePill with an 8 MHz crystal, PLL, and a 72 MHz system clock.

TIM1 Timer Settings for PWM

The image below shows the TIM1 configuration used in this project.

STM32CubeMX TIM1 parameter settings with PWM Generation CH1, prescaler 71, counter period 99, and PWM mode 1.

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.

STM32 PWM frequency calculation using a 72 MHz timer clock, a prescaler of 72, and a period of 100.

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.

STM32CubeMX DMA settings for TIM1 CH1 with memory-to-peripheral direction and half word data width.

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 timer prescaler register diagram showing that the register value plus one sets the clock division factor.

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 ARR

A 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.

Logic analyzer capture of an STM32 PWM signal at 10 kHz with a 30% duty cycle.

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.

Logic analyzer capture of STM32 PWM in DMA Normal mode showing one burst of pulses with increasing duty cycle.

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.

Zoomed logic analyzer view of STM32 PWM pulses with different duty cycles and a fixed 10 kHz frequency.

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.

Logic analyzer capture of STM32 PWM in DMA Circular mode showing repeating waveforms with rising duty cycle.

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.

Logic analyzer capture showing STM32 PWM Circular DMA output stopping automatically after three waveforms.

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.



About the Author
Arun Rawat
Arun Rawat
Embedded Systems Engineer · Founder, ControllersTech

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.

Subscribe
Notify of

22 Comments
Newest
Oldest Most Voted
takeo
1 year ago

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 !

Diego Ulises
2 years ago

Para que utilizas interrupciones! solo quiero enviar una señal pwm solo eso. You code while is empty not interrpt

Patrick Lee
2 years ago

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.

Bjoern
2 years ago

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

Bjoern
Reply to  Bjoern
2 years ago

Yeah, I got it running.
Unfortunately I selected “Output Compare CHx” instead of “PWM Generation CHx”.

Dat
2 years ago

I want to see how you wire up the complete circuit and what are the necessary links

Mord
2 years ago

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?

Mord
2 years ago

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?

embedded_eng
2 years ago

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.

aio
4 years ago

You are the best! Thanks for all of your work.

Michaël
4 years ago

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 ?

Michaël
Reply to  Arun Rawat
4 years ago

Thanks, I fixed the problem – another question, to bring the frequency to 100 kHz, the ARR to 10 instead of 100 ?

Olivier
6 years ago

That works like a charm !

Graham Gillett
6 years ago

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?

Abhishek Gupta
8 years ago

How to generate a center aligned pwm.

NGerb
Reply to  Abhishek Gupta
4 years ago

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.

×

Don’t Miss Future STM32 Tutorials

Join thousands of developers getting free guides, code examples, and updates.