The STM32 Family processors include general purpose timers that have a nice PWM function that can handle four channels of independently controlled duty cycles. In this article I will look at how to set these up for basic use suitable for the majority of applications that need PWM signals.
PWM with the general purpose timers
This is part of a series of articles about the general purpose timers found in the STM32 family of ARM cortex processors. In the preceding parts I introduced the TIM3 timer features, showed you how to identify the timer clock and set up the prescaler and reload register, and how to use the output compare interrupts. You can see all the STM32 TIM3 posts here. Code is written using the Standard Peripheral Library for the STM32F4Discovery board. Timer related portions should run directly on other STM32 family members since they all have a TIM3 or other identical general purpose timer.
A small change for the STM32F4Discovery
This article will switch to using the TIM4 timer simply because it has the output compare pins for TIM4 conveniently connected to four LEDs on the board. That will let me test the code directly on the STM32F4Discovery without wiring up any other LEDs. TIM4 is identical in all respects to TIM3 and everything here will work perfectly well on any of the general purpose timers so long as you take note of the output pins where present on your specific device.
In this article, I am going to look at how the general purpose timers on the STM32 Family of processors can be used to generate up to four channels of PWM, each with its own independent duty cycle.
PWM
Pulse Width Modulation is a way of modifying a signal my changing the proportion of time it is on and off. There are other uses of the term but, for now, I am only interested in signals where the amount of time that the signal is active – the duty cycle – can be varied from 0 to 100%. Signals such as these are used a lot in power control applications like light dimmers and motor speed control. The brightness of your computer screen is almost certainly controlled by a PWM signal.
The STM32 general purpose timers like TIM3 and TIM4 have hardware that makes it easy to generate PWM signals. In fact they have several modes for just this purpose. I will consider only simplest type which is good for the great majority of application. There are four channels available and each can have a different duty cycle although the basic frequency will be the same for each.
Basic PWM mode is similar to the output compare toggle mode except that the output pin is cleared whenever there is a match between the CCRx and the CNT registers and then set again when the counter reloads.
The diagram shows the default behaviour. This is the arrangement that is most intuitive. It is also possible to configure a channel to have an inverted state. That is, it will be set on a match and cleared the reload. This can be useful when driving motors both forwards and backwards.
PWM frequency
The frequency of the PWM signal can be important parameter of the setup. For changing the brightness of an LED, any frequency above a few tens of Hertz will not be seen by the eye. Below that and the light will seem to flicker. Even at relatively high frequencies you may sometimes see spotty trails left by PWM dimmed lights as your vision tracks across them. For motors, it is often a good idea to have PWM frequencies of well above the range of normal hearing. Otherwise, your motor controller will appear to whine a lot. It is someties difficult to distinguish the whining of the motor from the whining of the customers who have to listen to it.
PWM resolution
Another factor to take into account is the minimum resolution you need from the PWM system. That is, how many different steps in duty cycle are needed. The most obvious choices might be 100 steps, corresponding to percentages, or 256 steps, corresponding to the range of values in an 8 bit integer. Perhaps you only want 16 different intensities of a backlight. It is generally OK to have more resolution than you need but to have less might be a problem.
Configure the timer timebase
As with other timer problems, there are a number of constraints to consider when setting up the timer timebase. These are:
TIMER_Frequency – the input clock to the timer module
PWM_Steps – the number of different duty cycles needed
PWM_Frequency – the repetition rate of the PWM signal
These numbers determine the reload register and prescaler values used to configure the timer.
Suppose I want to have 100 different PWM levels and a frequency of 100Hz for a simple LED dimming application. First, I need to calculate the frequency needed to drive the counter
COUNTER_Frequency = PWM_Frequency * PWM_Steps = 100 * 100 = 10000 = 10KHz
Now I need to find a value for the prescaler that will give me a 10kHz clock from the Timer_Frequency
TIMER_Prescale = (TIMER_Frequency / COUNTER_Frequency) – 1 = 72000000/10000 – 1 = 7199
This value is safely within the range of an unsigned 16 bit register so I should be safe to proceed.
The ARR register will get a value that is PWM_Steps – 1 and I am ready to configure the timer timebase.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void timer_clock_init (void) { uint32_t TIMER_Frequency = get_timer_clock_frequency(); uint32_t COUNTER_Frequency = PWM_Steps * PWM_Frequency; uint32_t PSC_Value = (TIMER_Frequency / COUNTER_Frequency) - 1; uint16_t ARR_Value = PWM_Steps - 1; /* make sure the peripheral is clocked */ RCC_APB1PeriphClockCmd (TIMER_PERIPHERAL_CLOCK, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; /* set everything back to default values */ TIM_TimeBaseStructInit (&TIM_TimeBaseStructure); /* only changes from the defaults are needed */ TIM_TimeBaseStructure.TIM_Period = ARR_Value; TIM_TimeBaseStructure.TIM_Prescaler = PSC_Value; TIM_TimeBaseInit (TIMER, &TIM_TimeBaseStructure); } |
PWM configuration
With the timebase sorted out, I can concentrate on configuring the PWM. This turns out to be really easy when using the Standard Peripheral Library. Also, the designers of the chip made sure that the default values for many registers in the timers produce a basic working configuration. All I really need to do is tell the library function that I want to use PWM mode 1 (the simple one) and that I want to connect the output state to the OCx pin. In my example, I have configured one of the channels, the one for the blue LED, to be low polarity and the others to be high polarity. If you run the code, you will see that, with the same value for duty cycle, they have opposite brightness.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void timer_pwm_init (void) { TIM_OCInitTypeDef TIM_OCInitStructure; /* always initialise local variables before use */ TIM_OCStructInit (&TIM_OCInitStructure); /* Common settings for all channels */ TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 0; /* Channel2 - ORANGE LED*/ TIM_OC2Init (TIMER, &TIM_OCInitStructure); /* Channel3 - RED LED*/ TIM_OC3Init (TIMER, &TIM_OCInitStructure); /* Channel4 - BLUE LED*/ /* make this the opposite polarity to the other two */ TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; TIM_OC4Init (TIMER, &TIM_OCInitStructure); } |
Using the PWM
Once enabled and active, the duty cycle for each channel can be set either with a call to the library functions or by setting the CCRx register directly. Either way, the value is updated the next time the counter reloads so that there are no glitches. You are free to write a value to the CCRx register that is outside the range that can be matched by the counter. If you do that, the output will stay in one state or another. If you want the output to stay active or inactive you can simply set a duty cycle at one or the other extreme.
Bonus – flickering flame effect
In the application listing below, I set one LED to fade up while the other fades down thanks to the ease with which the PWM can be set to complementary mode.
The red LED will flicker with a flame-like effect and you may be interested in how this is done. The most obvious choice might be to generate a random duty cycle and set the channel with that. The result is not very pleasing with large excursions in the output intensity and visible high-frequency components that do not look very realistic. A basic property of random numbers is that if you average several of them, you get a new random number with a normal distribution. The more values that you average, the less the variation in the output. Here I average uniformly distributed number to gat an average 50% duty cycle and give that to the PWM channel looking after the red LED.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
#include #include "stm32f4xx.h" #include "systick.h" #define MHz 1000000L #define KHz 1000L /* The LED indicators on the STM32F4Discovery board */ #define LED_PORT GPIOD #define LED_PORT_CLOCK RCC_AHB1Periph_GPIOD #define GREEN_PIN GPIO_Pin_12 #define ORANGE_PIN GPIO_Pin_13 #define RED_PIN GPIO_Pin_14 #define BLUE_PIN GPIO_Pin_15 #define ALL_LED_PINS GREEN_PIN | ORANGE_PIN | RED_PIN | BLUE_PIN #define GREEN_LED LED_PORT,GREEN_PIN #define ORANGE_LED LED_PORT,ORANGE_PIN #define RED_LED LED_PORT,RED_PIN #define BLUE_LED LED_PORT,BLUE_PIN #define ALL_LEDS LED_PORT,ALL_LED_PINS #define TIMER TIM4 #define TIMER_PERIPHERAL_CLOCK RCC_APB1Periph_TIM4 #define TIMER_AF GPIO_AF_TIM4 /* unfortunate globals */ uint32_t PWM_Frequency = 100; uint32_t PWM_Steps = 100; uint32_t get_timer_clock_frequency (void) { RCC_ClocksTypeDef RCC_Clocks; RCC_GetClocksFreq (&RCC_Clocks); uint32_t multiplier; if (RCC_Clocks.PCLK1_Frequency == RCC_Clocks.SYSCLK_Frequency) { multiplier = 1; } else { multiplier = 2; } return multiplier * RCC_Clocks.PCLK1_Frequency; } void timer_clock_init (void) { uint32_t TIMER_Frequency = get_timer_clock_frequency(); uint32_t COUNTER_Frequency = PWM_Steps * PWM_Frequency; uint32_t PSC_Value = (TIMER_Frequency / COUNTER_Frequency) - 1; uint16_t ARR_Value = PWM_Steps - 1; /* make sure the peripheral is clocked */ RCC_APB1PeriphClockCmd (TIMER_PERIPHERAL_CLOCK, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; /* set everything back to default values */ TIM_TimeBaseStructInit (&TIM_TimeBaseStructure); /* only changes from the defaults are needed */ TIM_TimeBaseStructure.TIM_Period = ARR_Value; TIM_TimeBaseStructure.TIM_Prescaler = PSC_Value; TIM_TimeBaseInit (TIMER, &TIM_TimeBaseStructure); } void timer_start (void) { TIM_Cmd (TIMER, ENABLE); } void timer_stop (void) { TIM_Cmd (TIMER, DISABLE); } void timer_pwm_init (void) { TIM_OCInitTypeDef TIM_OCInitStructure; /* always initialise local variables before use */ TIM_OCStructInit (&TIM_OCInitStructure); /* Common settings for all channels */ TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 0; /* Channel2 - ORANGE LED*/ TIM_OC2Init (TIMER, &TIM_OCInitStructure); /* Channel3 - RED LED*/ TIM_OC3Init (TIMER, &TIM_OCInitStructure); /* Channel4 - BLUE LED*/ /* make this the opposite polarity to the other two */ TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; TIM_OC4Init (TIMER, &TIM_OCInitStructure); } // these are the LEDs on the STM32F4Discovery board void board_leds_init (void) { GPIO_InitTypeDef GPIO_InitStructure; /* always initialise local variables before use */ GPIO_StructInit (&GPIO_InitStructure); RCC_AHB1PeriphClockCmd (LED_PORT_CLOCK, ENABLE); /* these pins will be controlled by the CCRx registers */ GPIO_InitStructure.GPIO_Pin = ORANGE_PIN + RED_PIN + BLUE_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_Init (LED_PORT, &GPIO_InitStructure); /* ensure that the pins all start off in a known state */ GPIO_ResetBits (LED_PORT, ORANGE_PIN + RED_PIN + BLUE_PIN); /* this one is used with delay_ms() to act as a timing reference */ GPIO_InitStructure.GPIO_Pin = GREEN_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init (LED_PORT, &GPIO_InitStructure); /* The others get connected to the AF function for the timer */ GPIO_PinAFConfig (GPIOD, GPIO_PinSource13, TIMER_AF); GPIO_PinAFConfig (GPIOD, GPIO_PinSource14, TIMER_AF); GPIO_PinAFConfig (GPIOD, GPIO_PinSource15, TIMER_AF); } // return a fairly gaussian random number in the range 0-99 inclusive int rnd (void) { int intensity = 0; int sampleCount = 10; for (int i = 0; i < sampleCount; i++) { intensity += rand() % 100; } return intensity / sampleCount; } /* the flashing green LED acts as a visual timing reference */ void flash_green_led_forever (void) { int brightness = 0; int increment = 5; while (1) { for (int i = 0; i < 20; i++) { delay_ms (50); TIM_SetCompare3 (TIMER, rnd()); brightness += increment; TIM_SetCompare2 (TIMER, brightness); TIM_SetCompare4 (TIMER, brightness); // note opposite polarity } increment = -increment; GPIO_ToggleBits (GREEN_LED); } } int main (void) { systickInit (1000); board_leds_init(); timer_clock_init(); timer_pwm_init(); timer_start(); flash_green_led_forever(); return 0; // there is no going back but keep the compiler happy } |
Pingback: TIM3 on the STM32 - an introduction - Micromouse Online
first include statement in the last code listing is missing file.
So it is. Thank you.
The line should, I think, be
#include
Pingback: Creating a stand-up countdown buzzer - Pt 2 - Fourtress