Flickering-flame LED effect using MicroPython on a Raspberry Pi Pico

Python Microcontrollers Electrical engineering

8 September, 2024



I was recently watching a Wyloch's Armory video about making volcanic terrain for tabletop RPGs. At one point he uses an electronic tealight candle with a flicker effect (used in place of a regular tealight candle to simulate a dancing flame) to light up a smoke cloud with volcanic eruption effects, and it looked incredible.

I thought it'd be fun to replicate this effect without just buying a ready-made candle. It seemed well above my abilities to replicate it solely with an electrical circuit (and I feel obliged at this point to issue a PSA to remind the internet that "flicker" and "blink" are not synonms). I ended up stumbling upon a video by FriendlyWire, who uses a PIC microcontroller and pulse-width modulation to achieve pretty much exactly what I was looking for: a simple circuit with an LED that flickers like a real flame.

Taking inspiration from those two videos, this post describes how I used a Raspberry Pi Pico microcontroller, a simple MicroPython script, and a tiny electrical circuit to replicate the flickering effect of a flame. Here's what it looks like on the table, sitting inside the hollowed-out model volcano column:

flickering-flame-volcano

The MicroPython script is available on my GitLab.

Prerequisites

Electrical circuit

If you've followed along with the official guide, you should be able to control an external LED using a circuit like this (credit: Raspberry Pi):

circuit-diagram

We don't need to modify this circuit in any way, we just need to modify the script!

MicroPython script

Here's the entirety of the MicroPython script:

from machine import Pin, PWM
from random import randint
from time import sleep

max_brightness = 65535
min_brightness = int(round(max_brightness / 2, 0))
max_sleep_time = 1000
min_sleep_time = 1

pwm = PWM(Pin(15), freq=1000)

while True:
    pwm.duty_u16(randint(min_brightness, max_brightness))
    sleep(randint(min_sleep_time, max_sleep_time) / 10000)

This is using pulse-width modulation (PWM) to control the voltage sent to the LED in a pseudo-random way. There are two elements of randomness here: the brightness of the LED itself, and the time that the LED spends at a given brightness. There are infinite ways to replicate the flickering of a flame, and you can probably come up with something better than this pretty easily, but it works well for my purposes and I love its simplicity, so I stuck with it. Let's look at exactly what is happening here.

After importing our dependencies, we define some constants:

We then instantiate the PWM object with pwm = PWM(Pin(15), freq=1000). We're using GPIO pin 15 to control the LED (which is the same pin used by the Getting started guide), and we set the frequency to 1000 Hz. Frequency in the context of PWM is how often to switch the power on and off; probably anything faster than the human eye can see is okay, but 1000 seems to be a common value.

It's probably time for a little digression into pulse-width modulation. We control the perceived brightness of the LED by changing the duty cycle, which is the proportion of time that the LED is on. The higher the proportion of time that the LED is on, the brighter it appears, and vice versa. This is how the brightness of lights is typically controlled: When you turn the dial on a dimmer switch, you aren't changing the voltage or current sent to the LED, you're just changing the proportion of time that power is supplied to the LED. As long as the frequency of power switches is much faster than we can perceive, we just interpret a lower proportion of "on" time as a dimmer light. If voltage is supplied to the LED 90% of the time, it's bright; if voltage is supplied only 20% of the time, it's dim.

When we call pwm.duty_u16(randint(min_brightness, max_brightness)), we're randomising the duty cycle of the LED, and therefore randomising the perceived brightness.

sleep(randint(min_sleep_time, max_sleep_time) / 10000) then pauses the script for a random time between 10 microseconds and 10 milliseconds. This gives a bit more noise to the brightness changes, which I think makes it look a bit more realistic as a flickering flame.

Finally, we run the whole thing in while loop, repeating indefinitely. And that's basically it: Transfer the code to your Pico, and the LED should flicker like a flame.

Battery power

I built this circuit to fit inside a model volcano, so it needed to be battery-powered. Fortunately, it's really simple to run a Pico off batteries.

The Pico can run off anywhere between 1.8 and 5.5 V. Since the GPIO pins deliver 3.3 V, I thought using two or three 1.5 V batteries inside a battery box would be sufficient.

Connect the negative terminal to a ground pin (you have several options as shown in the pin-out diagram; I chose pin 3). Connect the positive terminal to pin 39, the VSYS pin. As long as you've named your MicroPython script main.py, the Pico will run it on startup, so it should start flickering immediately. Here's the final circuit:

final-circuit

References



0 comments

Leave a comment