embeddingcpp/content/posts/005-stm32begin-004-pwm.md
erki 9a6bb39669
All checks were successful
Build and push latest / publish (push) Successful in 1m19s
Add 005
2024-11-12 20:00:02 +02:00

175 lines
8.9 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> &times; 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 &times; 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 (unsigned 32-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.