STM32 Tutorial #3: PWM driven LED

Timers are a very important topic for embedded systems. For example, they are used for many things like recurring events, counting time and output signals. In this tutorial we will control the light intensity of the onboard led time dependent. To clarify, this means that we will use one timer to generate the PWM signal and another one for changing the PWM values.

What is PWM!?

One easy explanation is that Pulse Width Modulation (PWM) is a technique to output analog values from a digital output. A normal GPIO output can just output high or low. Because of this it is only possible to generate analog values by the use of time. Using PWM, the pin gets turned on and off alternately so that in average the output voltage is somewhere between high and low.

Duty cycle and PWM frequency are the most important parameters for us. The fraction of the on-time over the period time is called duty cycle. For example, a duty cycle of 20% means that a signal is on for 20% of the time and off for 80% of the time. The PWM frequency specifies the time period of the signal.

If you want to read more about PWM please follow this link.

Timer Settings

PWM Timer

Before we open STM32CubeMX, we define the timer settings needed by our application. One Timer deals with the PWM signal. In our application we want to use a variable that can have values between 0 and 100. This variable defines the duty cycle for the PWM signal. Furthermore, let us define to use a PWM frequency of 20kHz because then there is certainly no flickering happening. A second timer shall change the duty cycle by 5% every 50ms. Additionally, the direction should automatically toggle. This creates a light effect that toggles the led every second by fading slowly up and down.

Now that we specified the behavior, we can start the code generation tool STM32CubeMX and create a new project with the correct microcontroller. How this is done can be found in the former blog post here. Now, on the pinout, click on PB3 and choose “TIM2_CH2”.

STM32CubeMX Timer Setting

After this we click on Timers – TIM2 and select “PWM Generation CH2” at Channel 2. Additionally, choose “Internal Clock” at Clock Source.

STM32CubeMX Timer Channel PWM Selection

Now we have to specify the timer settings. In the clock configuration we can see that the clock for the timers is 8 MHz. If we now need a PWM frequency of 20 kHz we need to divide the clock by 400 (8e6 Hz / 20e3 Hz). Additionally, we want to set our duty cycle with the numbers 0 – 100. Therefore, set the counter period to 100. Now the prescaler calculates to 4 (400 / 100). The prescaler multiplied by the counter period now show the value 400 as needed above.

STM32CubeMX Timer PWM Settings

Now change to the NVIC settings and enable “TIM2 global interrupt”. We use this interrupt afterwards to dynamically set the duty cycle.

STM32CubeMX Timer Interrupt Setting

Fading Timer

Next, click on Timers – TIM1 and enable “Internal Clock” as clock source. We specified a time period of 50 ms above which translates to a frequency of 20 Hz. Because of this we need a prescaler of 400,000 (8e6 Hz / 20 Hz). The highest number the prescaler can get is 65,535 so we have to split it up. I chose a prescaler value of 800 and a counter period of 500.

STM32CubeMX Timer 1 Settings

After making these changes, click on NVIC settings and enable the “TIM1 update and TIM16 interrupts”.

STM32CubeMX Timer 1 Interrupt Settings

Now all settings in STM32CubeMX are ready. Enter your project related data to the project manager and press “Generate Code”. After this step open the created project in SW4STM32 or any IDE you chose.

Adding User Code

When we are working with generated code you should be careful to stay in the user code sections. Because of these sections it is relatively easy to find the correct location of the code I am presenting here as they are named in the same way. For example, search for “BEGIN PV” if you search the place for the first code snippet below.

main.c

First we create a new Variable in the file main.c called ui8TimPulse. This represents the duty cycle of the PWM signal.

/* USER CODE BEGIN PV */
uint8_t ui8TimPulse = 50; // set duty cycle to 50% initially
/* USER CODE END PV */

Next, in the same file we have to start the timers 1 and 2 as well as the PWM. Below you can see how this is done. HAL_TIM_Base_Start_IT starts the timebase of the timer specified in the argument. It also enables the interrupts used in this timer. HAL_TIM_PWM_Start starts the PWM specific part of the timer. TIM_CHANNEL_2 is corresponding to the GPIO pin chosen in STM32CubeMX.

  /* USER CODE BEGIN 2 */
 HAL_TIM_Base_Start_IT(&htim1);
 HAL_TIM_Base_Start_IT(&htim2);
 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
 /* USER CODE END 2 */

main.h

In main.h we add some constants to avoid magic numbers. The constant PWM_MIN defines the minimum possible value, the PWM_MAX is the maximum possible PWM value and the PWM_MIN_CHANGE is the percentage of every increment or decrement of the duty cycle.

/* USER CODE BEGIN EC */
#define PWM_MIN 4
#define PWM_MAX 100
#define PWM_MIN_CHANGE 5
/* USER CODE END EC */

stm32f3xx_it.c

Next we declare the ui8TimPulse variable and define a new enumeration in the stm32f3xx_it.c file. The enumeration is used to specify the current direction.

/* USER CODE BEGIN EV */
extern uint8_t ui8TimPulse;
enum{
	UP=0,
	DOWN
}eDir;
/* USER CODE END EV */

We stay in this file and move down to the TIM2_IRQHandler function. Here is the perfect place for setting the new duty cycle on every timer event. TIM2->CCR2 gives direct access to the capture compare register 2 of timer 2. This is used to set the duty cycle of the PWM signal. The variable htim2.Init.Period includes the initialized counter period value from above.

  /* USER CODE BEGIN TIM2_IRQn 0 */
 TIM2->CCR2 = (htim2.Init.Period * ui8TimPulse) / 100u;
 /* USER CODE END TIM2_IRQn 0 */

The code above would be enough to light the LED at a constant intensity defined at the definition of the ui8TimPulse variable. Instead of this we want to change the duty cycle dynamically.

The function TIM1_UP_TIM16_IRQHandler is called when the capture compare condition is met. In our case this happens every 50 ms. On every interrupt we first check for direction. Thereafter, we check if we have reached the end. If this is the case we change the direction, if not we increment/decrement the duty cycle.

  /* USER CODE BEGIN TIM1_UP_TIM16_IRQn 0 */
 if (eDir == UP) {
  if( ui8TimPulse >= PWM_MAX )
  eDir = DOWN;
  else
  ui8TimPulse += PWM_MIN_CHANGE;
 } else {
  if ( ui8TimPulse <= PWM_MIN  )
  eDir = UP;
  else
  ui8TimPulse -= PWM_MIN_CHANGE;
 }
 /* USER CODE END TIM1_UP_TIM16_IRQn 0 */

Now the program is ready to be compiled and programmed onto the microcontroller. If everything went fine you should see the same output as in the video below.

Conclusion

This blog post explained the very basics of PWM and the different usage of timers. As already shown in the GPIO tutorial, all of the configuration of the timers was done in STM32CubeMX. Only the start of the timers and our user application was done directly in the code. This combination makes it very fast and easy to produce working code.

As always, if you have any questions please ask in the comments, I will answer them as soon as possible. Also, if there are any improvements, please tell me.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.