[Elixir/Nerves] Pulse Width Modulation (PWM) for LEDs
Tags: elixir, nerves, iot, raspberrypi
Published: 2020-12-04 23:05:01

[日本語](https://qiita.com/mnishiguchi/items/4bdf88acf0ab0e8e2c7e) | English

I started learning the [Nerves](https://www.nerves-project.org/) IoT framework and [Raspberry Pi](https://en.wikipedia.org/wiki/Raspberry_Pi) this year. I really enjoy controlling hardware with my Elixir program.

I suppose that most beginners start with the official examples [nerves-project/nerves_examples/blinky](https://github.com/nerves-project/nerves_examples/tree/main/blinky) blinking the on-board LEDs or [nerves-project/nerves_examples/hello_gpio](https://github.com/nerves-project/nerves_examples/tree/main/hello_gpio) blinking an LED on a breadboard though GPIO. Then one will think what to do next.

In my case, I got curious about [Pulse-width modulation (PWM)](https://en.wikipedia.org/wiki/Pulse-width_modulation) and started to extend my LED blinking using PWM.

## PWM for LEDs

First I started learning what it is. I saw a bunch of articles and youtube videos for understanding the basics of it.

I like this image below from Wikipedia because it explains very concisely what PWM essentially is.

[![](https://upload.wikimedia.org/wikipedia/commons/b/b8/Duty_Cycle_Examples.png)](https://en.wikipedia.org/wiki/Pulse-width_modulation)

Long story short, it is about controlling the ratio of on-time to off-time in a period.

In the context of controlling an LED through GPIO, we can do two things using PWM, which was intriguing to me when I first learned it.

### 1. to blink an LED with various on/off timings

When the [frequency (Hz)](https://en.wikipedia.org/wiki/Hertz) is low for example 1-2Hz, we can actually see the LED switching on and off on a regular basis.
So by changing the [duty cycle](https://en.wikipedia.org/wiki/Duty_cycle), we can change the timing of the blinking. It is fun.

### 2. to change the brightness of an LED

As the frequency goes higher for example up to 100Hz, we no longer can observe the blinking beyond a certain point. With such a frequency,
the duty cycle acts like the brightness. In other words, we can consider the area of the graph of the waves as the brightness. They say this technique of converting digital signal into analog is quite common and is applied to many different things such as controlling a servo motor.

## Hardware and Software PWM

As I googled around and asked a few people questions, I learned there are two types of PWM: hardware and software.

### Software PWM

It seems like the software PWM should not be used in high frequency as I read many sources such as:
- [elixir-circuits/circuits_gpio README](https://github.com/elixir-circuits/circuits_gpio#i-tried-turning-on-and-off-a-gpio-as-fast-as-i-could-why-was-it-slow)
- [Frank Hunleth's answer in Elixir Forum](https://elixirforum.com/t/rpi4-hardware-pwm-dtoverlay-need-kernel-module/33264/2?u=mnishiguchi)

At first I did not understand but it totally makes sense. If hardware can do it, why not? Also they say Elixir's processes run in millisecond precision. So I can see there is a limitation on calculating precise waves in high frequency.

### Harware PWM

Obviously the Hardware PWM is dependent on the hardware. So different devices may have different capabilities. According to [Raspberry Pi's GPIO usage documentation](https://www.raspberrypi.org/documentation/usage/gpio/), here are the pins PWM is avaliable on:

- Software PWM: all pins
- Hardware PWM: GPIO12, GPIO13, GPIO18, GPIO19

For Raspberry Pi, the [`Pigpiox.Pwm` module](https://hexdocs.pm/pigpiox/Pigpiox.Pwm.html#content) provides functions that allow us to build and send waveforms with [pigpio daemon](http://abyz.me.uk/rpi/pigpio/pigpiod.html).
There is [Pigpiox](https://hexdocs.pm/pigpiox/Pigpiox.Pwm.html#summary) Elixir library, which is a wrapper around pigpiod for the Raspberry Pi.

If our target board does not support PWM, we can connect it to a [PWM/Servo Driver board like this](https://www.adafruit.com/product/815), then we can control PWM through the serial communication.

## Experiments

With that all said, still implementing my custom software PWM sounded fun so I did it anyway. Here is my code [mnishiguchi/nerves_hello_pwm](https://github.com/mnishiguchi/nerves_hello_pwm).

```
git clone https://github.com/mnishiguchi/nerves_hello_pwm
```

This project imports two libraries:
- [elixir-circuits/circuits_gpio](https://github.com/elixir-circuits/circuits_gpio) - defacto standard Elixir library for GPIO
- [tokafish/pigpiox](https://github.com/tokafish/pigpiox) -  a wrapper around pigpiod for the Raspberry Pi, which allows us to access hardware PWM.

### Usage

#### Handmade GenServer-powered PWM Scheduler

```elixir
# A GPIO pin for an LED.
gpio_pin = 12

# Get a reference to the LED.
{:ok, led_ref} = Circuits.GPIO.open(gpio_pin, :output)

# Start a scheduler with on/off callback functions and initial settings for the
# period (frequency in Hz and duty cycle in percentage).
NervesHelloPwm.PwmScheduler.start_link(%{
  id: gpio_pin,
  frequency: 1,
  duty_cycle: 50,
  on_fn: fn -> Circuits.GPIO.write(led_ref, 1)  end,
  off_fn: fn -> Circuits.GPIO.write(led_ref, 0) end
})

# Change the on/off ratio to 4:1.
NervesHelloPwm.PwmScheduler.change_period(gpio_pin, 1, 80)

# Change the frequency to 2Hz (2x faster than 1Hz).
NervesHelloPwm.PwmScheduler.change_period(gpio_pin, 2, 80)

# Stop the scheduler.
NervesHelloPwm.PwmScheduler.stop(gpio_pin)
** (EXIT from #PID<0.1202.0>) shell process exited with reason: shutdown
```

The PWM scheduler processes are registered with an ID so it is possible to start multiple PWM scheduler processes as long as IDs are unique.

Since Elixir's [`Process.send_after`](https://hexdocs.pm/elixir/Process.html#send_after/4) function is millisecond precision, there is a limitation on the pulse precision. So I impose the maximum frequency of 100Hz (10ms / period) on `NervesHelloPwm.PwmScheduler`, which is probably fast enough to dim the brightness of an LED.

If we need faster and more precise PWM, we should definitely consider alternative approaches, such as accessing the target device's built-in hardware PWM, using an external PWM driver board.

#### Hardware PWM using the [tokafish/pigpiox's Pwm functions](https://hexdocs.pm/pigpiox/Pigpiox.Pwm.html#content)

Hardware PWM feels smoother in changing the brightness of an LED.

```elixir
gpio = 12
frequency = 800
Pigpiox.Pwm.hardware_pwm(gpio, frequency, 1_000_000) # 100%
Pigpiox.Pwm.hardware_pwm(gpio, frequency, 500_000)   # 50%
Pigpiox.Pwm.hardware_pwm(gpio, frequency, 100_000)   # 10%
Pigpiox.Pwm.hardware_pwm(gpio, frequency, 10_000)    # 1%
```

## PWM using a servo driver

Now I am planning to play with [Adafruit 16-Channel PWM/Servo HAT for Raspberry Pi](https://learn.adafruit.com/adafruit-16-channel-pwm-servo-hat-for-raspberry-pi) using Nerves.

I might write up for it later.

![hello_servo](https://user-images.githubusercontent.com/7563926/103154972-9dd43380-4769-11eb-9764-1884492eef82.gif)

## Finally

It took me a while to understand what to do with PWM using Nerves; however it was a great opportunity to learn tons of new things and practice Elixir/Nerves programming. And most importantly, the Elixir/Nerves communities are enthusiastic and helpful, which is awesome. I will contribute back with what I learn.