Pulse-width modulation: using PWM to build a breathing nightlight and alarm

R. X. Seger
Sep 22, 2016 · 9 min read

Pulse-width modulation is a technique for varying the width of pulses to encode a signal. On the Raspberry Pi and other embedded computers, PWM is available as an output mode on the general-purpose I/O ports, controlled in either hardware or software.

In this article I’ll use PWM to control LED brightness, developing a nightlight with a continuously varying brightness by varying the duty cycle over time, when there is little ambient light as sensed by a photodiode over an SPI-based analog-to-digital converter. When the nightlight toggles, it will momentarily sound a magnetic transducer, also using PWM.

LED Brightness

RPIO and pigpio

First I found the RPIO.PWM library for Python, seemed promising. However it wasn’t installed by default in Raspbian. Installed with sudo pip install RPIOwith. Attempting to use this module, greeted with an error:

import RPIO._GPIO as _GPIO
SystemError: This module can only be run on a Raspberry Pi!

Found at https://github.com/metachris/RPIO/issues/53, there is a newer fork of RPIO for Raspberry Pi version 2 and later, but a developer suggested his alternative, pigpio (note: pi-gpio, not pig-pio). This module comes with Raspbian and can be readily used out of the box, first run the daemon:

sudo pigpiod

then access it via Python:


Hardware vs Software PWM

Raspberry Pi’s SoC supports hardware PWM, accessible via pi.hardware_PWM() using pigpio. Only a handful of specific GPIO pins are usable for PWM:

  • 12 PWM channel 0 All models but A and B
  • 13 PWM channel 1 All models but A and B
  • 18 PWM channel 0 All models
  • 19 PWM channel 1 All models but A and B
  • 40 PWM channel 0 Compute module only
  • 41 PWM channel 1 Compute module only
  • 45 PWM channel 1 Compute module only
  • 52 PWM channel 0 Compute module only
  • 53 PWM channel 1 Compute module only

and they all share the same channel, so if you try to configure multiple pins for PWM, they’ll all be driven identically.

Testing hardware PWM based on the example from the documentation:

sudo pigpiod
import pigpio
pi.hardware_PWM(12, 800, 1e6*0.25) # 800Hz 25% dutycycle

This caused an audible tone to be emitted, with a pitch varying based on the given frequency. An annoying whine, but becomes inaudible (ultrasonic?) at higher frequencies, tested up to 30e6 as the documentation recommends as the upper limit (“Frequencies above 30 MHz are unlikely to work.”).

Software PWM is more flexible: it can be used on any GPIO pin, at the downside of increased CPU usage. I’ll accept this trade-off:

pi.set_PWM_dutycycle(12, 255*0.25)

Both of these calls set GPIO 12 (equivalent to board pin #32) to 25% duty cycle — outputting high 25% of the time (0% = all low, 100% = all high).

Control with PWM

What use is a signal that is high 25% of the cycle and low 75%? One use case is controlling motor speed. However I’ll be using it to control the apparent brightness of an LED.

Incandescent lamps can have their brightness adjusted by lowering voltage, using for example a dimmer:

Although variable-voltage devices are used for various purposes, the term dimmer is generally reserved for those intended to control light output from resistive incandescent, halogen…

e.g. a variable resistor: a potentiometer, aka rheostat.

However, light-emitting diodes as a semiconductor device have a minimum current and forward voltage drop, not lending themselves well to voltage-controlled brightness control. Instead, the LED can be switched off and on faster than human persistence of vision, with a duty cycle varied to control the perceived brightness. This strategy also has the advantage it is easily integrated with the Raspberry Pi, no extra digital-to-analog (DAC) hardware is needed; PWM already works with the Pi.

To drive the LED at 50% brightness:

pi.set_PWM_dutycycle(12, 255*0.50)

I happened to wire my LED active-low, so 255*0.10 will set the LED to 90% brightness, and 255*0.90 to 10% brightness, for example.

Updating the nightlight

RPi.GPIO to pigpio

Remember the “nightlight” built from photodiode, ADC, and LED in SPI interfacing experiments: EEPROMs, Bus Pirate, ADC/OPT101 with Raspberry Pi? The light was set on/off as follows:

def set_light(on):
GPIO.output(LED_Y, not on) # active-low

This can be enhanced to allow variable brightness using PWM:

import pigpio
pi = pigpio.pi()

def set_light(brightness):
pi.set_PWM_dutycycle(LED_Y_BCM, 255 * (1 - brightness))

Then change set_light(False) → set_light(0.0), and set_light(True) → set_light(1.0). The multiplication by 255 is needed since the software PWM library accepts a duty cycle from 0–255 corresponding to 0–100%, and the 1- to account for the active-low wiring of the LED.


pi.get_PWM_frequency(12) shows the frequency defaults to 800 Hz. If the duty cycle controls the brightness, then what does the frequency control?

Visible flicker, the “refresh rate”, see: persistence of vision. CRT monitors refersh at ~85 Hz to reduce visible flickering. For LED dimming, Digikey How to Dim a LED recommends 200 Hz or greater.

At 100 Hz, flickering started to become noticeable to me. Note that with software PWM, some jitter may occur as the CPU is busy doing other tasks. 800 Hz should suffice. Much higher has no discernible effect for LED dimming, but will become important later for other purposes.

Edge triggering

The naive initial implementation of nightlight.py simply read the ADC in a loop, and turned the LED on or off if the voltage reached a threshold:

while True:
v = readadc(7)
if v > V_LIGHT: set_light(0.0)
elif v < V_DARK: set_light(1.0)

this continuously drives the LED, given the input received by polling. Often a better approach is to trigger on rising/falling edges. This technique was covered using interrupts in Interrupt-driven I/O on Raspberry Pi 3 with LEDs and pushbuttons: rising/falling edge-detection using RPi.GPIO, but the ADC I am using does not provide interrupts.

Nonetheless, edge-triggering is still possible. The ADC is polled and an internal state is kept:

if v > V_LIGHT: is_light = True
elif v < V_DARK: is_light = False

then set_light() is only called on a state transition:

if is_light != was_light:
if is_light: set_light(0.0)
else: set_light(1.0)

This technique has some advantages we’ll see soon.

Breathing, linearly

Some old electronic devices provided a pleasant “breathing” effect, as if the light was snoozing. This is straightforward to implement linearly as follows:

if not is_light:
counter += direction
if counter < min_counter or counter > max_counter: direction = -direction

where min_counter = 0.1, max_counter = 0.9, counter is initialized to max_counter, and direction to -0.05 (all these can be adjusted to taste).

But this breathing does not feel very natural, turns out it is a triangle wave:

Notice the sharp edges? It would be nice to smooth them out.

Easing curves

To do so, we could learn about easing curves. Useful references:

Easings.net’s quick reference shows a wealth of available curves:

although it is focused on JavaScript/CSS web development. For hardware you’re on our own, but can use these easings for reference. I particularly like the easeInOutQuad, which can be edited on cubic-bezier.com. Cubic Bézier curves, with control points 0.25, 0.1, 0.25, 1. Simplifying The Math Behind the Bézier Curve, one-dimensional bezier curves, cubic:

y = A*(1-x)³+3*B*(1-x)²*x+3*C*(1-x)*x²+D*x³

But this complexity is not needed for our application. A sine function is sufficient, with appropriate scaling and translation for ±1.0:

Translating to Python:

import math
brightness = (math.sin(counter * math.pi * 1.5) / 2) + 0.5

Video demo showing the brightness varying by a sine wave over time:


The breathing sine-wave PWM-controlled nightlight LED is cool and all, but that’s not all you can do with PWM. Previously I salvaged a magnetic transducer from Building an H-Bridge from a salvaged Uninterruptible Power Supply, but lacked the tools and knowledge to use it — until now.


The datasheet for the WT-1201 P transducer (powered at voltage 1.5V(1–2V), impedance 16±4.5 Ω, frequency 2.4± 0.2kHz) says “W” in the part number indicates it is washable:

The datasheet also suggests an application of “Washing Machine”, among others. But that’s unrelated. Dave Tweed on StackExchange “Remove After Washing” on Piezo Buzzer explains, washable actually refers to industrial PCB assembly:

The industrial PCB assembly process usually leaves residues — mostly soldering flux — on the circuit board. One step in the process is to wash the board (by dipping or spraying) with a solvent to remove those residues for long-term reliability and for the sake of appearance.

Some devices (such as sound or pressure transducers) have openings for their functioning, and their performance would be adversely affected if the solvent or the residues got washed into the opening and lodged there. Therefore, such devices often have a sticker that covers the opening(s) that should not be removed until after the washing.

The more you know…

Magnetic Transducer vs Piezoelectric Buzzer

You may be familiar with piezoelectric buzzers. The PAC-WT-1202 on the other hand is a magnetic transducer. The same company produces both types. This is the construction per the datasheet:

The vibrating disk is pulled by the magnetic coil, oscillating to generate an audible sound. In contrast, piezoelectric buzzers employ the piezoelectric effect. What’s better? Depends on your use case, but TrippLite decided to use a magnetic transducer in their UPS alarm for some reason.

Transistor driver

As an inductive load (= has a coil), the transducer shouldn’t be powered directly from the Raspberry Pi’s GPIO ports. But we want to control it through the GPIO port. A transistor can be used for this purpose.

I used a D1786R NPN transistor, 10 kΩ resistor on the base wired to GPIO 19 (board pin #35), with a 1N5404 diode on the collector (as a flyback diode) and the transducer in parallel, as shown:

Note: the datasheet said 1–2V, I gave it 5V, it hasn’t blown up yet. YMMV.

PWMing the Transducer

Applying constant voltage to the transducer has no audible effect. Some transducers have built-in drivers, but not this one. To have it emit a tone you need to drive it with alternating current.

Pulse-width modulation is a convenient means to do this. Here the frequency becomes important, as it corresponds to the sound wave frequency. Give the transducer a square wave at 2400 Hz:

import pigpio
pi = pigpio.pi()
pi.set_PWM_frequency(19, 2400)
pi.set_PWM_dutycycle(19, 255/2)

The buzzer emits a loud tone, as expected.

Although the transducer’s frequency is rated at 2400 ± 200 Hz = 2200–2600 Hz, it can be driven at other frequencies, albeit at lower volume. pigpio rounds 2400 down to 2000, and 2600 also down to 2000, no difference. The next value up is 4000 Hz. Highest pigpio allows for software PWM is 8000, hardware PWM can go higher but human hearing frequency ranges from 20–20,000 Hz. Try 2000, 1000, 800, 500, 200, 100, 80, 50, 10 (clicks). Frequency response from datasheet:


An alarm clock is a possible application of this buzzer, correlating with daylight or sunrise time. A full implementation of an alarm clock, with sleep/wake, snooze, etc., will have to wait for another time. For this article I’m going to add a momentary tone on the nightlight’s edge transitions, like so:

if is_light != was_light:

buzzer_started = time.time()

if buzzer_started and time.time() - buzzer_started > BUZZER_DURATION:
buzzer_stated = None

where set_buzzer() calls pi.set_PWM_frequency() and pi.set_PWM_dutycycle(), with 0 and 0 to turn off, or a reasonable frequency and 50% duty cycle (255*0.50) to turn on. Experimented with 2000 Hz, near the rated frequency of this transducer, but found it annoying; turned down to 10 Hz, a more muted clicking sound, still audible but less obnoxious.


Pulse-width modulation is essential for interfacing software to hardware. In this article we saw how it can be used to vary the brightness of an LED and to emit sound using a magnetic transducer, culminating in a simple example of a soothing nightlight with auditory output on edge transitions.

The complete source code for this nightlight is available on GitHub:

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade