175 lines
9.0 KiB
Markdown
175 lines
9.0 KiB
Markdown
---
|
|
title: "STM32 For Beginners [4]: PWM"
|
|
date: 2024-10-08T00:36:00+03:00
|
|
draft: false
|
|
summary: "..."
|
|
author: "Rusted Skull"
|
|
series: ["STM32 For Beginners"]
|
|
tags: ["Embedded", "STM32"]
|
|
---
|
|
|
|
Digital outputs are all fun and good, but very few things in actual reality are digital (just "on" or "off") in nature.
|
|
MCUs have a few ways to emulate a truly analog output, with Digital to Analog Converters (DACs) being the most useful one.
|
|
However, in most cases, we can make due with a digital signal driven in a specific manner. This is where PWM comes in.
|
|
|
|
# Generating a PWM, For a LED
|
|
|
|
PWM stands for "Pulse Width Modulation". We will be generating a series of pulses, and modulating, changing, the width
|
|
of those pulses over time. This allows us to do all kinds of fun things in the real world, for example, drive DC motors
|
|
or dim LEDs.
|
|
|
|
The waveform of a typical PWM signal would look something like this:
|
|
|
|
{{< figure src="/media/stm32begin-004-001.png" >}}
|
|
|
|
The waveform has some key properties:
|
|
* T</sub>period</sub> is the time of a full ON-OFF cycle, this is usually static per application,
|
|
* T<sub>ON</sub> is the time during which the output pin outputs a HIGH signal (in the STM32 case, a 3.3 V signal).
|
|
|
|
Not pictured is the frequency (F<sub>PWM</sub>) of the PWM signal. This can be derived from the T<sub>period</sub>: F<sub>PWM</sub> = 1 s / T<sub>period</sub>.
|
|
For, example, if our T<sub>period</sub> = 25 us, then the frequency F</sub>PWM</sub> = 1 s / 25 us = 40 kHz.
|
|
|
|
The other property that's not pictured is the duty cycle (D) of the pulse. The duty cycle represents the ratio
|
|
between the ON and OFF time in a single period: D = T<sub>ON</sub> / T<sub>period</sub>. In the figure above,
|
|
if we continue with the assumption that T<sub>period</sub> = 25 us, then we can say that T<sub>ON</sub> = 8.3 us,
|
|
and thus D = 8.3 / 25 = 0.33 = 33%. The duty cycle is the metric that we will be driving by modifying the T<sub>ON</sub>
|
|
of the signal.
|
|
|
|
An important property of the PWM signal is that, once the frequency of the signal is high enough, in most physical
|
|
systems, the ON-OFF switching will be evened out by the properties of the connected circuitry. This means that the
|
|
observed output voltage U<sub>OUT</sub> can be expressed as the function: U<sub>OUT</sub> = U<sub>VCC</sub> × D.
|
|
Note that U<sub>VCC</sub> is the peak voltage of the signal, in our case, 3.3 V. So for our drawing, the averaged
|
|
output voltage of the PWM signal is: U<sub>OUT</sub> = 3.3 × 0.33 ~= 1.1 V.
|
|
|
|
# Timers
|
|
|
|
Generating a PWM signal on the STM32 series is done via the timer peripheral. In other cases, these may be called
|
|
counters, or timer/counters. But in principle, across microcontroller series and manufacturers, timers work the same:
|
|
they count the number of pulses, and thus, allow you to keep track of time.
|
|
|
|
A timer is effectively configured to run at a given frequency, and it will count. It will either count to its maximum
|
|
limit (either unsigned 32-bit or 16-bit integer's maximum value for STM32s), or until it reaches a configured N<sub>period</sub> value.
|
|
Once that value is reached, usually, the timer is reset and it will either stop or restart, depending on the configuration.
|
|
The functionality of a generic timer is illustrated in the figure below.
|
|
|
|
{{< figure src="/media/stm32begin-004-002.png" >}}
|
|
|
|
As can be seen, by modifying the N<sub>period</sub> value, we can modify the real world T<sub>period</sub> in which the timer
|
|
counts to its reset value.
|
|
|
|
The rate at which the timer counts is determined by F<sub>tick</sub>. For STM32s, the timers are powered by APB1 and APB2 clocks.
|
|
These usually have speeds in the megahertz. Which can be way too big. For this purpose, timers on the STM32 include a frequency divisor:
|
|
a prescaler. This prescaler value will let us slow down the counting to a reasonable point for us. As such, we can say that F<sub>tick</sub> = F<sub>in</sub> / N<sub>prescale</sub>.
|
|
|
|
This also lets us calculate the F<sub>period</sub> = F<sub>tick</sub> / N<sub>period</sub> = F<sub>in</sub> / N<sub>prescale</sub> / N<sub>period</sub>.
|
|
And if we remember that F<sub>period</sub> = 1 s / T<sub>period</sub>, we can calculate the period time as well. From the
|
|
above example, we can say that F<sub>period</sub> = F<sub>PWM</sub>.
|
|
|
|
This lets us tie the period of the PWM to the period of the timer. But we also need a midway point for inverting the signal.
|
|
STM32 timers call this value a "compare" value: it's an arbitrary value during which we can do _something_ in. For generating
|
|
a PWM, the STM32 hardware uses the compare value as the point at which the the pulse inverts its value. This means, we can
|
|
combine the two previous figures as follows:
|
|
|
|
{{< figure src="/media/stm32begin-004-003.png" >}}
|
|
|
|
## Configuring a Timer
|
|
|
|
We now know that we need to configure a timer to generate PWM. Great.
|
|
|
|
From before, we know that we also have to choose two parameters: N<sub>prescale</sub> and N<sub>period</sub>.
|
|
What you choose as N<sub>period</sub> will dictate the range in which you can adjust the PWM "value" in code.
|
|
For example, setting a period of 100 will let you input the PWM "power" as a number between 0 and 100.
|
|
Setting it to some other, more random value, will make the logic harder. So, pick a sane period. In our case,
|
|
100 will do.
|
|
|
|
We thus know that N<sub>period</sub> = 100 and we also know that F<sub>in</sub> = 8 MHz and we wish our output
|
|
frequency to be F<sub>PWM</sub> = 40 kHz. This lets us calculate N<sub>prescale</sub> as the last unknown in our
|
|
configuration.
|
|
|
|
If F<sub>PWM</sub> = F<sub>in</sub> / N<sub>prescale</sub> / N<sub>period</sub>
|
|
|
|
then N<sub>prescale</sub> = F<sub>in</sub> / F<sub>PWM</sub> / N<sub>period</sub>
|
|
|
|
thus N<sub>prescale</sub> = 8e6 / 40e3 / 1e2 = 2.
|
|
|
|
**Note**: due to the digital nature of our work, and with the number 0 counting as a "1" for mathematical purposes,
|
|
both N<sub>period</sub> and N<sub>prescale</sub> will need to be input as the calculate value - 1. So we're finally
|
|
left with:
|
|
|
|
N<sub>period</sub> = 99, and N<sub>prescaler</sub> = 1.
|
|
|
|
Now we go over into CubeMX. For illustration purposes, we'll be applying the PWM to the LED that's on the dev board.
|
|
This will have the effect of letting us dim the LED's brightness. Fortunately, the STM32F303k8 dev board has a PWM
|
|
channel attached to the LED pin PB3. We can check this by clicking the pin and seeing if there's any TIMx_CHy functions
|
|
attached to it. In the case of PB3, we can see that there's TIM2_CH2, which means it's connected to Timer 2's channel 2.
|
|
|
|
{{< video src="/media/stm32begin-004-004.mp4" type="video/mp4" preload="auto" >}}
|
|
|
|
Our next step is to enable TIM2 by setting its "Clock Source" to "Internal Clock", and setting "Channel2" to "PWM Generation
|
|
CH2".
|
|
|
|
{{< video src="/media/stm32begin-004-005.mp4" type="video/mp4" preload="auto" >}}
|
|
|
|
With that done, we now have to set the counter values. We have to set:
|
|
* **Prescaler (PSC - 16 bits value)** to our calculated N<sub>prescale</sub> (1),
|
|
* **Counter Period (AutoReload Register - 32 bits value)** to N<sub>period</sub> (99),
|
|
* set **auto-reload preload** to "Enabled".
|
|
|
|
{{< video src="/media/stm32begin-004-006.mp4" type="video/mp4" preload="auto" >}}
|
|
|
|
With this done, generate the code as you would normally.
|
|
|
|
## Controlling the PWM
|
|
|
|
We've configured the timer, the PWM output, now to start it and play with its duty cycle.
|
|
|
|
After code generation, we go to main. The first thing we have to do is actually start the timer
|
|
and start the PWM generation. This is done as such:
|
|
|
|
```cpp
|
|
/* USER CODE BEGIN 2 */
|
|
HAL_TIM_Base_Start(&htim2);
|
|
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
|
|
/* USER CODE END 2 */
|
|
```
|
|
|
|
The code should be placed in user code section 2, before the while loop but after the MX_x_Init functions.
|
|
|
|
* `HAL_TIM_Base_Start(&htimx)` will start the actual timer itself, and
|
|
* `HAL_TIM_PWM_Start(&htimx, TIM_CHANNEL_y)` will start the PWM for that timer on the specified channel.
|
|
|
|
If you're using multiple PWM channels from the same timer, you will have to call `HAL_TIM_PWM_Start` for each
|
|
channel that you're using.
|
|
|
|
To set the PWM to a given duty cycle value, we would use the macro function `__HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, n)`.
|
|
Where the `n` is a value between 0 and N<sub>period</sub>. This effectively sets us our duty cycle as well.
|
|
|
|
To dim the LED from off to full brightness in sequence, we could do something like this, for example:
|
|
|
|
```cpp
|
|
/* Infinite loop */
|
|
/* USER CODE BEGIN WHILE */
|
|
while (1)
|
|
{
|
|
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0);
|
|
HAL_Delay(400);
|
|
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 20);
|
|
HAL_Delay(400);
|
|
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 40);
|
|
HAL_Delay(400);
|
|
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 60);
|
|
HAL_Delay(400);
|
|
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 80);
|
|
HAL_Delay(400);
|
|
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 99); // because our period was 99 and not 100.
|
|
HAL_Delay(400);
|
|
/* USER CODE END WHILE */
|
|
|
|
/* USER CODE BEGIN 3 */
|
|
}
|
|
/* USER CODE END 3 */
|
|
```
|
|
|
|
Bootload the code, and off we go. We now have a functional PWM useful for dimming a LED... Or driving motors. Which we'll
|
|
cover next.
|