PWM signal generation tutorial

PWM tutorial

In this tutorial, you will learn how to generate a PWM signal using STM32 Cube IDE to control a servo motor.

Prerequisites

What is a PWM signal?

Pulse Width Modulation (PWM), is a technique used to control the power delivered to an electrical device.

In most digital systems, there are only two voltage levels: a high state (1) and a low state (0). There are no intermediate levels to vary the voltage (e.g., 0.5). To overcome this limitation, PWM works by varying the duration for which the signal is high while keeping the frequency constant.

Duty cycle

Over a given period, the proportion of time the signal is high is called the duty cycle. By changing the duty cycle, we modify the average voltage applied to the component, allowing us to control a device with variable power.

This principle is used to control many devices, such as RGB LEDs, motor drivers, and so on.

In the context of controlling a servo motor, the PWM signal determines the angular position of the servo. By adjusting the pulse width, we can control the servo motor’s orientation.

Wiring and Configuration

For this tutorial, we will use a NUCLEO-L476RG board. The circuit diagram for a servo motor is shown below:

Wiring

  • Two wires connected to the servo’s +5V and GND (usually red for power and brown or black for ground).
  • The third wire should be connected to a pin on the board capable of providing a PWM signal (most pins are labeled). In this example, we connect it to pin D5.

The next steps takes place on CubeIDE: create a new project and open CubeMX (to access the .ioc file).

Identify which pin corresponds to D5. The mapping between “Dx” and “PAx” pins can be found online in the pinout documentation. For D5, it corresponds to PB4.

Configure the pin to use a timer and its available channel (in the form “TIMx_CHx”). For the NUCLEO L476RG, the only available configuration for D5 is TIM3_CH1.

PWM pin assignment

In the left panel, set the following parameters:

  • In the “System Core” section, select RCC.

    • Set the High Speed Clock (HSE) to Crystal/Ceramic Resonator.

    System core

  • In the “Timers” section, select the timer you configured.

    • Set the Clock Source to Internal Clock.
    • Set the chosen channel to PWM Generation CHx (here, PWM Generation CH1).

    Timer configuration

Frequency Parameters

In the top Clock Configuration tab, you can see the frequencies at which the microcontroller timers operate. For the NUCLEO L476RG, it’s 80 MHz.

Clock configuration

Howaver, according to the SG90 servo datasheet, the signal frequency should be around 50 Hz. The timer’s current frequency is far from 50 Hz, but fortunately, it is possible to divide the timer frequency to reach the desired value.

To do this, the following formula is used:

mathematical expression or equation

Where:

  • mathematical expression or equation is the timer frequency.
  • mathematical expression or equation is the frequency of the bus feeding the timer (here 80 MHz).
  • mathematical expression or equation (prescaler) is a register that divides the timer’s input frequency.
  • mathematical expression or equation (auto-reload register) defines the timer’s maximum count.

Knowing mathematical expression or equation , you just need to find a combination of PSC and ARR to get mathematical expression or equation Hz. This can be done manually or using online calculators.

In this example, a suitable combination is mathematical expression or equation and mathematical expression or equation .

Did you know?

Several combinations can achieve 50 Hz. Note that the higher the value of mathematical expression or equation , the better the PWM resolution.

Once mathematical expression or equation and mathematical expression or equation are determined, enter these values in TIM3 under Parameter Settings:

PSC & ARR assignment

Duty Cycle

The mathematical expression or equation parameter is used to control the duty cycle of the PWM signal. In the same way as for dividing the frequency, there is a formula to calculate the value to enter in the mathematical expression or equation register based on the desired duty cycle mathematical expression or equation :

mathematical expression or equation

For example, to set a 50% duty cycle with my values:

mathematical expression or equation

Enter this value in the timer parameters and generate the code:

Pulse assignment

Code integration

Before running the servo, you need to write a few lines in main.c. In the int main(void){} function, add:

main.c
 94MX_USART2_UART_Init();
 95MX_TIM3_Init();
 96/* USER CODE BEGIN 2 */
 97HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
 98/* USER CODE END 2 */
 99
100/* Infinite loop */

HAL_TIM_PWM_Start(&htimX, TIM_CHANNEL_X); starts the PWM signal (replacing the “X” with the timer and channel values used).

Warning

Be sure to write your code between the tags “/* USER CODE BEGIN */” and “/* USER CODE END */”. Otherwise, your code will be deleted if you regenerate your files!

If you compile and upload the code to the board, you will notice that the servomotor… does not turn! This is because the servomotor is held in the position defined by the duty cycle, so you would need to vary this to get it to turn.

Bonus

If you have an oscilloscope, you can still observe the PWM signal from your pin:

Oscilloscope display

Thanks to the display tiles, we can clearly see that the signal period is 20 ms (i.e., 50 Hz). The duty cycle is mathematical expression or equation .

Modifying the duty cycle

Rather than struggling to modify the mathematical expression or equation value manually by editing the .ioc file, it is possible to dynamically modify the pulse width directly in the code.

The macro __HAL_TIM_SetCompare(&htimX, TIM_CHANNEL_X, pulse) allows you to modify the duty cycle value of the channel associated with the selected timer. pulse is generally defined to represent the pulse width in microseconds.

The servomotor documentation states that:

Position “0°” (1.5 ms pulse) is middle, “90°” (~2ms pulse) is all the way to the right, “-90°” (~1ms pulse) is all the way to the left

With these values, it is possible to directly deduce the pulse values to be applied to the CCR register.

Here is a quick test to write in the while(1){} loop to change the angular position of the servomotor every second:

main.c
 97HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
 98/* USER CODE END 2 */
 99
100/* Infinite loop */
101/* USER CODE BEGIN WHILE */
102while(1)
103{
104  // -90°
105  __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, 1000);
106  HAL_Delay(1000);
107
108  // 0°
109  __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, 1500);
110  HAL_Delay(1000);
111
112  // 90°
113  __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, 2000);
114  HAL_Delay(1000);
115  /* USER CODE END WHILE */
116
117  /* USER CODE BEGIN 3 */
118}

There you go!

Moving servomotor

Going further

Feel free to test different values to see how the servomotor behaves. Why not create a function that allows you to convert angle values directly into pulses? And of course, this signal generation process works with all compatible components. For example, you can easily adjust the brightness of an LED based on the pulse width you send it.


Credits