[Elixir Nerves] Pulse Width Modulation (PWM) for LEDs

I started learning the Nerves IoT framework and 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 blinking the on-board LEDs or nerves-project/nerves_examples/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) 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.

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.

When the frequency (Hz) 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, 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:

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, 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 provides functions that allow us to build and send waveforms with pigpio daemon. There is Pigpiox 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, 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.

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

This project imports two libraries:

Usage

Handmade GenServer-powered PWM Scheduler

# 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 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

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

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 using Nerves.

I might write up for it later.

hello_servo

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.

日本語 | English