Pulse Width Modulation (PWM) is easy, isn't it? - Turning it off and on again

Part of Uwe Kleine-König's work at Pengutronix is to review PWM (Pulse Width Modulation) drivers. In addition, he also sometimes refactors existing drivers and the Linux kernel PWM subsystem in general.

Since Uwe has seen lots of PWM drivers, he decided to give a talk at the 2023 FOSDEM in Brussels where he explained what PWM is, how the use-cases influence the kernel PWM subsystem's design, gave advice to driver authors and showed common pitfalls.

This blog post summarizes the knowledge Uwe shared in his talk as a starting point for new driver authors (and users of existing drivers).

How to toggle pins automatically (fast)

Pulse Width Modulation, or PWM for short, refers to a periodic square wave signal and is used for applications such as

  • dimming (or blinking) LEDs
  • driving display backlights at a controllable brightness level
  • controlling motor speeds (e.g. a fan)
  • remote controls

Of course, you could theoretically toggle GPIOs in software as fast as possible to generate a PWM signal, but for most applications, this would not only cause quite some CPU load, but also introduce quite some unwanted jitter in the PWM signal due to interruptions of the task performing the toggling by higher priority tasks.

Instead, most SoCs have dedicated hardware units, which basically increment a counter at each tick of a clock source and drive outputs based on the counter value. Once a preconfigured counter value is reached, a pin is automatically switched on, without any additional software interaction. When another preconfigured value is reached, the pin is automatically switched off, the counter is cleared and everything automatically starts all over again.

How shall I compare thee to another PWM

Since a PWM is, in a nutshell, just a square wave signal, we can describe it with only a few properties:

  • period [ns], i.e. the time between rising edges
  • duty_cycle [ns], i.e. active time in a period
  • polarity (normal or inverted)
  • enable / disable

For some use cases it can be useful to set an inverted polarity, which swaps the active and inactive level. For example, this is useful for a backlight that has a higher brightness for lower duty cycles.

When a PWM is enabled, the square wave waveform is generated, when it is disabled, the waveform generation is stopped.

Kernel view on a PWM

If a driver in the Linux kernel is a PWM consumer, i.e. it needs to parameterize a PWM, the previous description of a PWM signal is used to pass the request to the PWM device driver, which in turn has to calculate and write the corresponding values to the PWM hardware.

The PWM Subsystem bundles the properties in a structure, which is called the pwm_state. As periods and duty_cycles tend to be rather short and kernel interfaces tend to be built around integer numbers, nanoseconds ([ns]) are used as units of time:

struct pwm_state {
    u64 period;     // [ns]
    u64 duty_cycle; // [ns]
    enum pwm_polarity polarity;
    bool enabled
    ...
}

This structure is then passed to or returned by the API functions

...
int (*apply)(struct pwm_device *pwm, const struct pwm_state *state);
int (*get_state)(struct pwm_device *pwm, struct pwm_state *state);
...

of a PWM device driver.

When calling its .apply() function, the PWM device driver will calculate the required register values from the pwm_state properties and write them to the hardware.

The .get_state() callback does the exact opposite of .apply(): it reads out the hardware registers and recalculates a pwm_state from the actual hardware values.

Dealing with hardware is... hard

In an ideal world, the hardware would configure the requested output exactly and reading it back with .get_state() would then yield an exact copy of the state passed to .apply().

In reality, the hardware often cannot be configured to the exact values requested when setting a state, thus calling .get_state() will return a different state from the last values set with .apply().

This has a multitude of reasons, some of which Uwe explained in his talk.

Accuracy of values in API and in hardware

The hardware counter can only count integer multiples of the time quantum q of the clock driving it. The quantum q is the period length of the clock (i.e. the time between two rising edges) and represents the smallest amount of time that can be adjusted.

Let us for example assume, that we drive our PWM hardware with a 13333 kHz clock. The quantum q for this clock therefore is 75.001875... ns.

If a PWM consumer now requests a period of 30000 ns, the driver must select the multiple of the quantum q which best approximates the requested value. For our example, this would be either 399 q (= 29925.748 ns) or 400 q (= 30000.750 ns).

So no matter which of the two settings the driver chooses it will introduce a small, but inevitable error.

Of course, this principle applies to the duty cycle calculation in the same way.

Time vs. frequency

While for blinking or dimming LEDs, the exact frequency of the PWM is not of much interest, some applications such as motor control require a PWM's frequency to be set as precisely as possible. Other applications, such as transmitters for infrared remote controls, rather require the timing of the PWM to be as precise as possible.

In some corner cases, the rounding strategy determines which of the requirements is met more precisely.

Assume again an input clock of 13333 kHz, which gives a quantum q of 75.001875... ns. As duty cycle and period are multiples of q, a requested frequency of 1161587 Hz (period = 860.891 ns) can be approximated by either 11 q or 12 q.

If optimizing for a closer match on the period, i.e. optimizing for time, 11 q is the better match at 825.021 ns (-35.871 ns off) than 12 q at 900.023 ns (+39.131 ns off).

If optimizing for frequency, 1 / 11 q is the worse match at 1212090.909 Hz (+50503.909 Hz off), since 1 / 12 q provides the better match at 1111083.333 Hz (-50503.667 Hz off).

As usually the driver author does not know the PWM consumer's requirements, it is considered best practice to always round down period values, as this gives at least a consistent behaviour over different drivers.

To further improve the situation for PWM consumers, Uwe suggests to introduce a new .round_state() callback. .round_state() would take a state, calculate which register values would be written in a real .apply() call, and instead of actually applying the state to the hardware (as .apply() would), hands the values back to the calculations implemented in .get_state().

This way, PWM consumers can determine what the actual result of an .apply() call would be for a given state, which allows them to determine the request that fits their needs as closely as possible.

Precision of integer math

As the values in a struct pwm_state are integer values, special attention has to be paid to the order in which mathematical operations are executed.

Assume a requested period of 30000 ns, where the driver has to calculate its period steps as

period_steps = clkrate / NSEC_PER_SEC * period

If the division is executed as the first operation, its result will round to 0, and no matter what period is requested, period_steps will always be 0.

While in this case it is rather obvious that the division needs to be the last operation executed, most occurrences in real world drivers are a lot more subtle.

As a general rule of thumb: always divide in the last step, and only divide once.

Another issue arises from the fact that the clock framework reports clock rates in Hz rounded to an integer value.

State transitions

When configuring the hardware for a new struct pwm_state by calling the driver's .apply() function, the different hardware implementations of PWMs on different SoCs exhibit a vast set of different behaviours, such as

  • applying the new state after the current period is finished
    • provides a pretty signal
    • new signal is applied delayed, depending on current position in period and period duration
    • the apply function may either block or return while the old state is still active
  • immediately starting a new period with the new values
    • causes glitches in the signal
    • new setting active when .apply() returns
  • mixed settings
    • e.g. one cycle with the new period, but with the old duty cycle
    • usually sensitive to runtime timing behaviour
  • hardware must be disabled for reconfiguration
    • during the reconfiguration, the PWM level is hardware specific

Further hardware limitations

Some hardware implementations cannot output a duty_cycle of 0 or a duty_cycle = period, i.e. outputting a constant high or low signal.

There is hardware that only supports a fixed period, on some SoCs multiple PWM units share a common period as they connect to a common counter.

As some hardware has write-only registers for configuring the PWM units, their drivers do not support .get_state().

For more hardware limitations, just check the Linux kernel yourself:

sed -rn '/Limitations:/,/\*\/?$/p' drivers/pwm/*.c

Switch it off and off again: how (not) to shut off a PWM

Most developers assume that for configuring a PWM to generate a fixed low signal level at the output you can simply

pwm_get_state(mypwm, &state);
state.enabled = false;         // <---- Wrong !
state.duty_cycle = 0;
pwm_state_apply(mypwm, &state);

while this will in most cases generate the intended signal, in some cases it won't.

There are hardware implementations that don't emit the inactive level when disabled, some

  • freeze the output level at the current output pin state (depending on the current position in the cycle); or
  • set the output in a high impedance state;
  • set a (fixed) constant low or a constant high signal at the output thus ignoring the configured polarity;
  • going to the new output level immediately or after having completed the current period.

Instead, setting the duty_cycle to 0 will produce the desired fixed output level (at least for hardware that allows setting a duty_cycle of 0):

pwm_get_state(mypwm, &state);
state.enabled = true;
state.duty_cycle = 0;
pwm_state_apply(mypwm, &state);

As this is a state change nevertheless, the hardware-specific behaviour for setting the new state as described in the last section applies.

Advice to driver authors

At the end of his talk, Uwe gave some advice to driver authors.

Besides the already mentioned advice to always round down, and to do divisions as the last step in a chain of calculations, Uwe suggests to:

  • enable the PWM_STATE kernel option during tests. This debugging feature compares the HW state before and after a call to .apply() and complains if either the old state was a better match for the request than the new state, or if the new state is determined using unexpected rounding.
  • properly document the hardware's properties,
  • properly document state transition behaviour as 'Limitations',
  • link to the hardware manual, which simplifies reviews.

Title Image by PiccoloNamek, CC BY-SA 3.0, via Wikimedia Commons


Further Readings

2022 at Pengutronix

At Pengutronix and in the embedded Linux world in general, exciting things happen all year round, but a carry in the date field is a great opportunity to sit back and talk about it. In the broad categories of kernel, open source software, hardware and public relations we want to tell you what happened at Pengutronix in 2022.


Pengutronix Kernel Contributions in 2021

2022 has started, and although Corona had a huge impact on our workflow, the Pengutronix team again made quite some contributions to the Linux kernel. The last kernel release in 2020 was 5.10, the last one in 2021 was 5.15, so let's have a look at what happened in between.


umpf - Git on a New Level

Modern software development is commonly accompanied by a version control system like Git in order to have changes to the source code being documented in a traceable manner. Furthermore, any version state should be easily reproducible at any time. However, for work on complex projects such as the BSP ("Board Support Package") of an embedded system with its multiple development aspects, the simple stacking of the individual changes on top of each other does not scale.


FOSDEM 2023

The Pengutronix team is on it's way to FOSDEM in Brussels! We are looking forward to many interesting discussions with developers of different open source software components - be it the Linux kernel, Debian, KiDAC, FreeCAD etc ...


Pengutronix at Embedded World 2022

Welcome to our booth at the Embedded World 2022 in Nürnberg!


CLT-2022: Voll verteilt!

Unter dem Motto "Voll verteilt" finden die Chemnitzer Linux Tage auch 2022 im virtuellen Raum statt. Wie auch im letzten Jahr, könnt ihr uns in der bunten Pixelwelt des Workadventures treffen und auf einen Schnack über Linux, Open Source, oder neue Entwicklungen vorbei kommen.


Pengutronix at FOSDEM 2022

"FOSDEM is a free event for software developers to meet, share ideas and collaborate. Every year, thousands of developers of free and open source software from all over the world gather at the event in Brussels." -- FOSDEM