PWM in hardware with STM32 Timer and ChibiOS

The Pulse Width Modulation

The Pulse Width Modulation (also known as PWM) is a digital modulation technique which uses duty-cycle of square waves to encode information. In communication field PWM surrendered to more evolute communication technique which uses more complex waveforms showing better noise rejection ratio and less transmission errors at highest data rate. Nevertheless, PWM is still used in infrared communication where data rate is very low but transmitter and receiver are cheap.

Anyway low rate communication is not the most relevant use case of PWM which is widely used in many fields very far from communication: maybe the most know use case is the control of Switching Mode Power Supply especially when used to drive inductive load like motors. Another interesting use case for PWM is light dimming especially with LEDs.

The PWM is a waveform which can basically switch between two states with a negligible raise/fall time and a constant period. The two states are usually two different voltage levels Vhigh and Vlow. In a general sense the two possible states can be named active and idle. 

The Period can be thus defined as

T=t_{active}+t_{idle}

The Duty Cycle is a ratio often expressed as percentage which can be defined as

Duty=\frac{t_{active}}{t_{active}+t_{idle}}=\frac{t_{active}}{T}

It is not guaranteed that the active state would be Vhigh and idle Vlow (as example if we are using the PWM to switch a P-channel mosfet). Thus we can have two different types of PWM: the Active High (high is active, low is idle) and the Active Low (low is active, high is idle).

A PWM having a duty cycle of 37.5% active high (top) active low (bottom)

The most exploited property of the PWM signal is definitely the proportionality between Duty Cycle and signal mean value. Let us consideran active high PWM like that shown in figure 2. In this case the signal mean value for a periodic signal is defined as

\bar{x}(t)=\frac{1}{T}\int_0^T \! x(t) \, \mathrm{d}t

This integral can be splitted and solved easily

\bar{x}(t)=\frac{1}{T}\int_0^{DT} \! V_{high} \, \mathrm{d}t+\frac{1}{T}\int_{DT}^{T} \! V_{low} \, \mathrm{d}t

\bar{x}(t)=D\cdot{V_{high}}+(1 - D)\cdot{V_{low}}

\bar{x}(t)=V_{low}+D\cdot{(V_{high}-V_{low})}

An active high PWM with details

In the particular case were low state is the reference ground the mean value becomes

\bar{x}(t)=D\cdot{V_{high}}

The STM32 Timer

STM32 Timer (also abbreviated as TIM) is a peripheral which allows to generate PWM signals in hardware and this means once the Timer have been configured and started it can generate a PWM waveform on a certain output PIN without the intervention of the software. Actually the general purpose timer of the STM32 is a peripheral which can be used for a large variety of purposes. Mainly it can be used as reliable timing source, as example to properly schedule tasks in a RTOS like ChibiOS, or to trigger with a precise cadency the ADC peripheral. As said it can be additionally used to generate PWM signals or conversely capture square waves measuring elapsed time between two waveform edges and thus period and duty cycle.

The hardware itself is very complex and offers so many working mode that explain all of them in detail would be difficult and dispersive. Anyway I would like to give you some information about this peripheral and then focus on PWM output mode.

The time base unit

Each STM32 is equipped with a large amount of timers which are identified by a progressive number (TIM1, TIM2, TIM3, and so on). All the timers are independent and they do not share any resources even if they can be synchronised together.

Some timer are known as Advanced Timers as they have some advanced features like the complementary outputs, programmable dead-time and break input to put the timer output in reset or known state. Such features are thought to fulfil Full Bridge control application needs. Usually those timers are the TIM1 and TIM8 but it is not guarantee they are always available on every STM32.

A timer basically is fed with a clock signal and it counts the clock pulses. As the clock speed is know, this basically allows to measure time windows. On STM32 each Timer has in independent counter which can be configured to count upward or downward: basically the counter increases/decreases its value on each clock pulse and the current value can be read accessing the register TIMx Counter (TIMx_CNT). The timer counter register has a 16-bit depth except on a limited number of timers which are equipped with a 32-bit counter register. Those timer, if available, are usually the TIM2 and TIM5.

A larger counter bit-depth guarantees higher resolution at given full scale. To understand what this means lets take a look to some definitions. The timer resolution is the minimum amount of time the timer is able to count. In term of time magnitude, it is equal to clock period

\delta t_{TIM}=T_{clock}=\frac{1}{f_{clock}}

The timer full scale is the maximum amount of time that can be counted. It depends on timer resolution and counter depth

FS_{TIM}=\delta t_{TIM} \cdot 2^{depth}

About clock speed we should take a step back and take a look to clock distribution. On STM32 but more in general on ARM Cortex-M architecture, peripherals are interfaced to memory, core and DMA through the Advanced Peripheral Bus (also shortened as APB). The architecture of this bus is based on the ARM Advanced Microcontroller Bus Architecture (AMBA) which is an open-standard for the connection and management of subsystem in a system-on-chip. Depending on the amount of peripherals and chip size, there could be more than a APB on each micro and usually they are identified by a progressive number (APB1, APB2, APB3 and so on). The main purposes of an APB is to provide clock distribution from the main domain (that of the core, memory and DMA) to the peripheral, as well as provide a memory-mapped register interface. As in STM32 there are dozen of timer, often they are connected on different APB and the maximum clock speed of a timer depends on which APB they are. Even more each APB has is own prescaler to run at a clock speed which is lower or equal to that from the main domain.

Usually on STM32 at least an APB is able to run at the same speed of the ARM Cortex-M core thus at the maximum speed of the whole system. Anyway, to run at maximum speed is not always suitable for all the scenarios. Looking back to previous formula let us consider the case of a STM32F401RE and its TIM9 which is equipped with a 16-bit counter and is connected to APB2 and can be configured to run at 84MHz. At this speed we have an high time resolution which is approximately 11.9 ns but a small full scale in the order of 780 ms.

If we cannot make use of a 32-bit timer the best way to deal with larger time windows would be to reduce clock speed. Even if APB offer a clock prescaler, changing this divider would impact all the peripherals connected to it. To ensure flexibility, each timer is equipped with an independent 16-bit prescaler which can be configured accessing the register TIMx prescaler (TIMx_PSC).

When a timer is running and overflows its maximum value, it rolls over to zero and continues to run. On overflow event the timer launches an interrupt request. Acting on a register known as Timer Auto-Reload Register (TIMx_ARR) is possible to reduce the upper limit and anticipate the rollover. This is actually true only in upperward counting mode when the counter runs from 0 to the value of the TIMx_ARR register then rolls over to 0. In downward counting mode the condition is inverted: the counter runs from the TIMx_ARR value down to 0, generates an interrupt on the underflow event and the rolls over to the TIMx_ARR value continuing to run.

Basically once started the counter continues to run and launch interrupts with a specific frequency. Such kind of interrupt is periodic and we will refer to it as Timer Periodic Interrupt. The frequency of periodic interrupts (at given clock speed) can be changed acting on the Prescaler Register and on the Auto Reload Register value according to the formula

f_{IRQ}=\frac{f_{clk}}{PSC \cdot ARR}

The TIMx_CNT and TIMx_PSC joint with the TIMx_ARR compose the core of the timer which is called Time Base Unit.

The channels

Each STM32 timer is equipped with some repeated sub structures known as Channels. A timer can have up to 4 independent channels which are identified with a progressive number (TIMx_CH1, TIMx_CH2, and so on) and each channel is equipped with additional registers and an I/O line.

The Channel is able to launch additional interrupt when the counter reaches an intermediate value before the overflow/underflow event. This can be done comparing the current counter value with that preloaded in an additional channel register known as Capture/Compare Register (TIMx_CCR1, TIMx_CCR2, etc.). Obviously such value shall be smaller than that stored in the TIMx_ARR. Such kind of interrupts are channel dependent so we will refer to them as Timer Channel Interrupts.

The operation mode of a channel can be changed acting on the Capture/Compare Mode Register (TIMx_CCMR1, TIMx_CCMR2, etc.). When a channel is configured in PWM mode it is able to change the logic level of its I/O line on periodic and channel interrupts. Just remember, to use this feature, we need to reroute some GPIO connections assigning them to TIM (take a look to GPIO article if you are not familiar with). Thus when a Channel is configured in PWM mode active high the timer will switch high the channel line on periodic interrupt and low on channel interrupt. On contrary when it is configured in PWM mode active low the timer will set low on periodic and high on channel.

To clarify how PWM mode works let us do a simple example. Let us assume we would like to generate a PWM with a period of 1 kHz and duty cycle 47.5% active high. To do that let configure the timer to run at a frequency of 1MHz. We can then configure the TIMx_ARR to 1000 having a periodic interrupt every millisecond (which is equal to our PWM frequency e.g. 1 kHz) and the TIMx_CCR1 to 475 (where the ratio TIMx_CCR1/TIMx_ARR is equal to our PWM duty cycle).

What if we reduce the timer speed from 1MHz to 100kHz? Well we could scale all the previous value by 10 but we would lose resolution on duty cycle. In such case indeed the minimum duty  we can set is 1%. Back to the previous example, with these settings, we can set duty cycle to 47% or 48% but not to 47.5%. More in general the duty resolution is given by the formula

Duty_{min}=\frac{f_{PWM} \cdot 100}{f_{Timer}}\%

As each channel has independent Capture\Compare module and shared Auto Reload Register using a timer it possible to generate a PWM signal for each channel having same period but different duty cycle.

Note that on STM32 usually TIM6 and TIM7 have no channel as they are deputy to act as timing source for internal ADC and DAC.

The ChibiOS PWM Driver

The ChibiOS PWM driver exploits the PWM output mode capability of STM32 TIM to generate PWM signal in hardware offering also the chance to intercept periodic and channel interrupts through callbacks. This allows to generate PWM on arbitrary I/O line not necessarily internally connected to a timer channel with software intervention.

Each API of the PWM Driver starts with the prefix “pwm”. Function names are camel-case, pre-processor constants uppercase and variables lowercase.

Differently from other peripherals Timer is the same across all the STM32 subfamilies. Thus we have only TIMv1.

Different driver same approach

We have presented many driver in this article series and again in PWM driver we can notice same certitudes we got used to dealing with ChibiOS. The design patterns are constant and the PWM driver is organized like every other simple driver of ChibiOS\HAL:

  • The whole driver subsystem can be enabled disabled through the proper switch in the halconf.h. The switch name is HAL_USE_PWM.
  • To use the driver we have then to assign  a TIM peripheral  to it acting on mcuconf.h. 
  • Assigning a peripheral to the driver a new object will become available: PWMD1 on TIM1 assignation, PWMD2 on TIM2 and so on.
  • Each PWMDx object represent a driver which implements a Finite State Machine.
  • A driver to be used shall be initialized but this is done automatically on halInit();
  • The driver shall be properly configured before to be used. This requires a call to pwmStart().
  • The pwmStart() receives as usual two parameters: a pointer to the driver and a pointer to its configuration.
  • If the driver is not used it can be stopped through the pwmStop().

The following figure represent the state machine of the driver.

The PWM Driver state machine

Managing peripheral assignation related conflicts

The TIM peripheral can be assigned to many driver like ICU, GPT and ST. Before to assign a timer to the PWM driver, user should take care to check whereas it is already assigned to another driver.

A special care is required by ST which stands for System Tick: the ChibiOS scheduler relies on hardware timer to achieve reliable timing for scheduling purposes. Thus, when available, it uses a 32-bit timer to have a larger resolution independently from clock tree configuration.

A timer is assinged to the ST in the mcuconf.h header through the define STM32_ST_USE_TIMER. The following code as grabbed from the default demo for STM32 Nucleo-64 F401RE and here the ST uses the TIM2. This means basically PWMD2 cannot be used.

To assign TIM2 to the PWM we have to deassign it from ST. By default ST expects a 32-bit resolution timer, and the only alternative is the TIM5. Anyway, if we accept to lose resolution we can also reduce the ST resolution acting on chconf.h and moving CH_CFG_ST_RESOLUTION from 32 to 16

Configuring the PWM

The PWM Driver offers a rich configuration structure which allow to properly configure the STM32 timer to work in PWM mode. These fields are aligned to what we have seen in the previous chapter.

The first parameter is frequency and represent the timer clock speed. In simple words this parameter will be used to compute the prescaler value starting from the APB frequency. Note that while the APB represent an upper limit to the reachable frequency, the timer counter depth provides an lower limit to it.

\frac{f_{APBx}}{2^{depth}} <= f_{clock} <= f_{APBx}

ChibiOS allows to catch invalid frequency specification through the assertions. This debugging mechanism can be enabled in chconf.h and, in case, triggers an error at compile time when the choosen frequency is invalid.

Assertions, like other debugging mechanism, should always be enabled during the development phase and disabled when the application is ready to be deployed.

The second parameter is period and represent the initial PWM period expressed as clock pulses. Once that timer and PWM frequency are know, this value can easily be computed using the formula

period=\frac{f_{TIM}}{f_{PWM}}

period is limited by TIMx_ARR bit depth and invalid value can be catched using assertion as well.

The third parameter callback is a pointer to a callable which the driver will call on periodic interrupt. It can be set to NULL if unused.

The fourth parameter is an array of structures having the same size of the available channels. This size of course depends on specific timer and each element represents the configuration of that channel

The first element of the structure is the channel mode, while the second element is again a callback which will be called on channel interrupt. The following code represent the available channel modes.

In addition to these parameters the driver accept also 3 additional fields which represent three registers of the timer: the Control Register 2 (TIMx_CR2), the Break and Dead Time Register (TIMx_BDTR) and the DMA/interrupt enable register (TIMx_DIER). In standard application their value can be left to 0 but note that the TIMx_BDTR is available only in Advanced Timers: because of that its field can be activated optionally setting the STM32_PWM_USE_ADVANCED preprocessor switch to TRUE mcuconf.h.

The following snippet of code is an example of PWM configuration and related callback

This configuration sets the timer clock to 10 kHz, the PWM initial period to 1S, enables only the Channel 1. The PWM configuration is used on pwmStart specifying which PWM Driver we are actually going to use.

As instance the following code is using the configuration to setup the PWM Driver 1 which relies on TIM1.

PWM driver configuration can be changed on the fly restarting the driver with a new configuration.

Enabling/Disabling PWM channels

The configuration structure does not specifies any information about the duty cycle. Indeed the pwmStart configures the timer but does not output anything. To see something on the I/O line we have to enable the channel. Beside of that, remember that the related I/O line shall be properly configured as Alternate Function.

A PWM channel can be enabled using the a specific API

This API receives a pointer to the PWM driver, a channel identifier and the pulse width expressed as clock pulses. The channel identifier is a progressive number starting from 0 to N-1 whereas N is the number of available channels on that timer.

The driver offers some helper to compute the width starting from a fraction

an angle

or a percentage

This last is very interesting as we usually express the duty in form of percentage. As example the next snippet enables the TIM1_CH1 with a duty of 47.5%

It is possible to change the channel duty cycle on the fly calling the pwmEnableChannel repeatedly but with a different width.

To disable a channel there is another function named pwmDisableChannel. Such function receives only the PWM Driver and the channel identifier.

Changing period on the fly

The PWM period can be changed on the fly with another API which overwrites the configuration applied on pwmStart.

Callbacks and notifications

The PWM driver can trigger a callback on periodic interrupt and a callback on channel interrupt. Such kind of callbacks can be setup acting on PWM configuration. As instance the following code is identical to the previous example with the difference that here we have setup a periodic and a channel callback.

More in detail this configuration exploits callbacks to generate a PWM signal on the an arbitrary GPIO not necessarily interconnected to the timer. In this case the pin have been previously configured as output push-pull.

Note that even if a callback has been set up it is not necessarily called. To enable callbacks we should enable notification using the following APIs

Further readings and Hands-on

Previous and next

This article is part of a series of articles which are meant to be tutorials. I have composed them to be read in sequence. Here the previous and next article of this series:

Be the first to reply at PWM in hardware with STM32 Timer and ChibiOS

Leave a Reply