Turn on your heartlight
Let it shine wherever you go
Let it make a happy glow
For all the world to see

A little fire for your heart! But there’s no actual flame here, no heat…instead, LEDs and a microcontroller make a virtual flame that dances like the real thing. (In fact it’s playing a tiny video loop of a real candle.)

We planned this animated flame simply as a piece of high-tech jewelry, but other uses are possible…a fire or lantern effect on cosplay props, a safer alternative to having candles on the table or as part of a holiday display, or a unique night light for finding your way to the bathroom at night.

It’s a simple circuit that can be built either as a pendant or as a tabletop piece. Fair warning though: the pendant variant is tricky…it’s a very tight space requiring ace soldering skills and some unconventional methods. As a tabletop piece, perhaps in a glass votive, or maybe you have a cosplay prop in mind, it’s much more forgiving.

Parts from Adafruit:

Other Required Items:

  • Soldering iron and related paraphernalia
  • Narrow-gauge wire. Especially if building the pendant…we’re huge fans of our 30 AWG silicone-coated stranded wire for tightly-packed projects like this one!
  • Hobby knife and/or file.
  • If making the pendant design:
    • 3D printer and transparent filament
    • Two (2) #2-56 x 3/8" screws (or M2 x 10mm)
    • Small screwdriver
    • Flush cutters
    • Necklace chain or lanyard cord
    • 5-minute epoxy or E6000 glue (do not use JB-Weld, it's conductive)
  • If NOT making the pendant:
    • Glass votive candle holder or other container

Before going shopping or committing to any parts or materials, read through the whole guide first to see what’s needed and what you might already have, or for ideas on what you might improvise.

If you’ll be making the pendant using our design, let’s do the 3D printing first…then you can get a head start on the electronics while the printer’s working.

If making a votive candle or something else of your own design, you can skip ahead to the next page.

The 3D files for this project are hosted on Thingiverse. They’re tiny and should fit on any 3D printer.

Transparent or translucent PLA is recommended so the light can filter through. Do not use ABS filament for this project — it tends to shrink slightly and the space inside this case is already very tight.

Print each piece as a separate job (the results are cleaner, no stringing between parts) in high-quality mode with 25% infill. Clean up with files and/or sandpaper afterward. If you rinse off the sanding dust under a faucet, wait until the parts are completely dry before installing any electronics.

The file "spacer.stl" is only used temporarily during assembly and can be printed at normal or draft quality.

Test fit each part in the case before assembling or soldering anything. You may need to file away some cruft or even clip off one of the little board-supporting nubbins to ensure it all fits inside.

Let’s get the flame code installed on the board before soldering anything. That way the electronics can all be tested before sealing everything inside the case.

If this is your first time using the Pro Trinket microcontroller, you’ll want to begin with our guide for setting that up. The Pro Trinket works a little differently from “normal” Arduinos are requires some extra installation and a different upload procedure:

Introducing Pro Trinket

To confirm that you have the driver installed and IDE properly configured, load the basic Arduino “blink” example sketch and try uploading to the board. If it won’t cooperate, work carefully through each of the steps in the guide linked above.

Do not continue until you have the “blink” sketch successfully working on the Pro Trinket board.

Then you can download the fire pendant sketch from Github - with the code below, select Download: Project Zip to get both FirePendant.ino and the animation file data.h.

// SPDX-FileCopyrightText: 2019 Phillip Burgess/paintyourdragon for Adafruit Industries
//
// SPDX-License-Identifier: MIT

//--------------------------------------------------------------------------
// Animated flame for Adafruit Pro Trinket.  Uses the following parts:
//   - Pro Trinket microcontroller (adafruit.com/product/2010 or 2000)
//     (#2010 = 3V/12MHz for longest battery life, but 5V/16MHz works OK)
//   - Charlieplex LED Matrix Driver (2946)
//   - Charlieplex LED Matrix (2947, 2948, 2972, 2973 or 2974)
//   - 350 mAh LiPoly battery (2750)
//   - LiPoly backpack (2124)
//   - SPDT Slide Switch (805)
//
// This is NOT good "learn from" code for the IS31FL3731; it is "squeeze
// every last byte from the Pro Trinket" code.  If you're starting out,
// download the Adafruit_IS31FL3731 and Adafruit_GFX libraries, which
// provide functions for drawing pixels, lines, etc.  This sketch also
// uses some ATmega-specific tricks and will not run as-is on other chips.
//--------------------------------------------------------------------------

#include <Wire.h>           // For I2C communication
#include "data.h"           // Flame animation data
#include <avr/power.h>      // Peripheral control and
#include <avr/sleep.h>      // sleep to minimize current draw

#define I2C_ADDR 0x74       // I2C address of Charlieplex matrix

uint8_t        page = 0;    // Front/back buffer control
const uint8_t *ptr  = anim; // Current pointer into animation data
uint8_t        img[9 * 16]; // Buffer for rendering image

// UTILITY FUNCTIONS -------------------------------------------------------

// The full IS31FL3731 library is NOT used by this code. Instead, 'raw'
// writes are made to the matrix driver.  This is to maximize the space
// available for animation data.  Use the Adafruit_IS31FL3731 and
// Adafruit_GFX libraries if you need to do actual graphics stuff.

// Begin I2C transmission and write register address (data then follows)
void writeRegister(uint8_t n) {
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(n);
  // Transmission is left open for additional writes
}

// Select one of eight IS31FL3731 pages, or Function Registers
void pageSelect(uint8_t n) {
  writeRegister(0xFD); // Command Register
  Wire.write(n);       // Page number (or 0xB = Function Registers)
  Wire.endTransmission();
}

// SETUP FUNCTION - RUNS ONCE AT STARTUP -----------------------------------

void setup() {
  uint8_t i, p, byteCounter;

  power_all_disable(); // Stop peripherals: ADC, timers, etc. to save power
  power_twi_enable();  // But switch I2C back on; need it for display
  DIDR0 = 0x0F;        // Digital input disable on A0-A3

  // The Arduino Wire library runs I2C at 100 KHz by default.
  // IS31FL3731 can run at 400 KHz.  To ensure fast animation,
  // override the I2C speed settings after init...
  Wire.begin();                            // Initialize I2C
  TWSR = 0;                                // I2C prescaler = 1
  TWBR = (F_CPU / 400000 - 16) / 2;        // 400 KHz I2C
  // The TWSR/TWBR lines are AVR-specific and won't work on other MCUs.

  pageSelect(0x0B);                        // Access the Function Registers
  writeRegister(0);                        // Starting from first...
  for(i=0; i<13; i++) Wire.write(10 == i); // Clear all except Shutdown
  Wire.endTransmission();
  for(p=0; p<2; p++) {                     // For each page used (0 & 1)...
    pageSelect(p);                         // Access the Frame Registers
    writeRegister(0);                      // Start from 1st LED control reg
    for(i=0; i<18; i++) Wire.write(0xFF);  // Enable all LEDs (18*8=144)
    for(byteCounter = i+1; i<0xB4; i++) {  // For blink & PWM registers...
      Wire.write(0);                       // Clear all
      if(++byteCounter >= 32) {            // Every 32 bytes...
        byteCounter = 1;                   // End I2C transmission and
        Wire.endTransmission();            // start a new one because
        writeRegister(i);                  // Wire buf is only 32 bytes.
      }
    }
    Wire.endTransmission();
  }

  // Enable the watchdog timer, set to a ~32 ms interval (about 31 Hz)
  // This provides a sufficiently steady time reference for animation,
  // allows timer/counter peripherals to remain off (for power saving)
  // and can power-down the chip after processing each frame.
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Deepest sleep mode (WDT wakes)
  noInterrupts();
  MCUSR  &= ~_BV(WDRF);
  WDTCSR  =  _BV(WDCE) | _BV(WDE);     // WDT change enable
  WDTCSR  =  _BV(WDIE) | _BV(WDP0);    // Interrupt enable, ~32 ms
  interrupts();
  // Peripheral and sleep savings only amount to about 10 mA, but this
  // may provide nearly an extra hour of run time before battery depletes.
}

// LOOP FUNCTION - RUNS EVERY FRAME ----------------------------------------

void loop() {
  uint8_t  a, x1, y1, x2, y2, x, y;

  power_twi_enable();
  // Datasheet recommends that I2C should be re-initialized after enable,
  // but Wire.begin() is slow.  Seems to work OK without.

  // Display frame rendered on prior pass.  This is done at function start
  // (rather than after rendering) to ensire more uniform animation timing.
  pageSelect(0x0B);    // Function registers
  writeRegister(0x01); // Picture Display reg
  Wire.write(page);    // Page #
  Wire.endTransmission();

  page ^= 1; // Flip front/back buffer index

  // Then render NEXT frame.  Start by getting bounding rect for new frame:
  a = pgm_read_byte(ptr++);     // New frame X1/Y1
  if(a >= 0x90) {               // EOD marker? (valid X1 never exceeds 8)
    ptr = anim;                 // Reset animation data pointer to start
    a   = pgm_read_byte(ptr++); // and take first value
  }
  x1 = a >> 4;                  // X1 = high 4 bits
  y1 = a & 0x0F;                // Y1 = low 4 bits
  a  = pgm_read_byte(ptr++);    // New frame X2/Y2
  x2 = a >> 4;                  // X2 = high 4 bits
  y2 = a & 0x0F;                // Y2 = low 4 bits

  // Read rectangle of data from anim[] into portion of img[] buffer
  for(x=x1; x<=x2; x++) { // Column-major
    for(y=y1; y<=y2; y++) img[(x << 4) + y] = pgm_read_byte(ptr++);
  }

  // Write img[] to matrix (not actually displayed until next pass)
  pageSelect(page);    // Select background buffer
  writeRegister(0x24); // First byte of PWM data
  uint8_t i = 0, byteCounter = 1;
  for(uint8_t x=0; x<9; x++) {
    for(uint8_t y=0; y<16; y++) {
      Wire.write(img[i++]);      // Write each byte to matrix
      if(++byteCounter >= 32) {  // Every 32 bytes...
        Wire.endTransmission();  // end transmission and
        writeRegister(0x24 + i); // start a new one (Wire lib limits)
      }
    }
  }
  Wire.endTransmission();

  power_twi_disable(); // I2C off (see comment at top of function)
  sleep_enable();
  interrupts();
  sleep_mode();        // Power-down MCU.
  // Code will resume here on wake; loop() returns and is called again
}

ISR(WDT_vect) { } // Watchdog timer interrupt (does nothing, but required)

A few things to be aware of regarding this sketch:

  • This is NOT good learning code for basic use of the LED matrix! Normally you’ll want to use the Adafruit_IS31FL3731 and Adafruit_GFX libraries (which provide drawing functions for pixels, lines, etc.).
  • The sketch is very specifically optimized for the Pro Trinket and won’t likely work on other boards.
  • It runs equally well on a 3V or 5V Pro Trinket…if you already have a 5V Pro Trinket around, that’s fine and you can use it…the 3V board is just a tiny bit more power-efficient and the battery will last a little longer. You may need to remove the part where we change I2C speed to 400 KHz but only do that if 400 KHz doesn't work on the Trinket 3.3V!

Open the FirePendant sketch in the Arduino IDE and upload it to the Pro Trinket board same way you did the “blink” sketch earlier…watch for the “Done Uploading” message. Nothing will happen, of course…we haven’t wired anything together yet…but having the code already in place makes troubleshooting easier later.

The circuit is super simple…just six wires…but the tiny enclosure necessitates some unusual build steps. We’ll get into the details on the next page, but for now, in simplified schematic form, this is what we’re aiming for:

Some points of interest to remember:

  • Do not assemble the LED matrix and driver board yet…some unusual steps are required if making the pendant design.
  • For the pendant, the LiPoly Backpack is flipped and installed “sidecar style” rather than the usual position where it’s stacked on top.
  • A couple of wires need to be soldered directly to pads on the back of the Pro Trinket rather than the usual through-hole vias.
  • There’s a trace that needs to be cut on the LiPoly Backpack to enable the power switch. This is shown on the next page.

Prep Work

Optional but recommended: using a tiny bit of 5-minute epoxy or E6000 glue, reinforce the point where the wires connect to the battery. Allow to dry completely before continuing.

I’ve gotten in the habit of doing this the moment new batteries arrive, before even starting projects with them.

Using a hobby knife or a pointed file, scratch away the trace between these two points on the LiPoly Backpack board.

This enables use of a power switch, rather than being always-on.

If making a pendant, confirm that each of the electronic components fit in their corresponding spots inside the case without undue force. Use a file to scratch away any protruberances that interfere.

Optional but recommended: splay the “wings” of the switch just slightly so it fits more snugly into its spot inside the case.

Also, I like to trim the legs (using flush cutters) to about half their normal length. Not required, but gives just a little more working room inside the case.

Wiring & Soldering

Any wire lengths mentioned here are for the pendant assembly. If making a votive or other holder, it’s fine (and probably helpful) to use longer wires, whatever length you need.

For reference, here’s that circuit schematic again of what we’re aiming for:

Cut two pieces of wire about 1.5" (40mm) long and strip and tin both ends.

Tin two legs of the switch and connect wires here. Use the center leg and then either one of the two outer legs; the opposite one won’t be used.

Solder the opposite ends of these two wires to the LiPoly Backpack board, to the pins which had the scratched-away trace between them.

Trim away any wire protruding from the underside of the LiPoly board. This is especially important for the pendant…it needs to be nearly flat. A slight protrusion is a normal product of soldering and will fit inside the case, but gloppy soldering here will cause trouble.

Adding heat-shrink tubing to the switch legs before soldering to the LiPoly board is totally optional, but I like to do this for durability. Use it if you got it!

Snap off a 3-pin section of row pin header and solder it to the pro Trinket’s BUS, G and BAT+ pins.

The LiPoly Backpack is installed over this…BUT…instead of sitting atop the Pro Trinket like normal, turn the board over (keeping the same pin alignment…see photos) and solder it hanging off the side.

Make sure the header is perpendicular and the boards are aligned. Start with just the middle “G” pin, re-heat as necessary to get everything aligned, then solder the two outer pins. Let the solder flow into the vias, do not glop it up on the surface!

After soldering, trim away the pins protruding on both sides using flush cutters. We want this as flat as possible.

Snap off two 9-pin sections of row pin header. Solder these to the A1-A9 and B1-B9 pins on the Charlieplex driver board as shown. A solderless breadboard can be handy for holding these straight while soldering. The other 8 pins don’t require connections.

Set the 3D-printed spacer piece on the back side of this board, then lay the LED array on top of it. Hold it snug with a rubber band, tape or a couple clothespins, then solder the pins on this side.

The spacer is required for the pendant (the battery will fit into this space later). For a votive candle or other design, you can sandwich the boards directly without the spacer…unless you want the battery in that spot.

After soldering, use flush cutters to trim the pins on both sides: the LED face and the component face.

BE EXTREMELY CAREFUL AS YOU DO THIS. Do not clip off any LEDs! Also, tiny bits of metal will go flying…wear safety glasses and/or point it away from you when cutting…and make sure those bits don’t end up landing in the circuit somewhere.

For a really low profile, an alternate method is to solder only the middle pin on each side, trim all the pins flush, then finish the rest of the soldering.

This requires really ace soldering though…properly-tinned iron, heating each spot, adding just a little more solder and letting it flow in. If you’re heavy-handed or a “melt-and-wipe” solderer, that isn’t gonna cut it. Watch Collin’s Lab: Soldering for pointers!

Cut four more wires about 1.5" (40 mm) long, then strip just a small amount of insulation and tin each end. They don’t need to be all color-coded like this, but it does make things easier…use it if you got it.

On the back of the Pro Trinket board is a spot for an optional JST connector. Tin the + and pads there, then solder two of the wires to those pads as shown.

The other two wires connect to pins A4 and A5.

These four wires then connect to the matrix driver as shown in the circuit diagram or using the table below. These wires snake up from “between” the LED/matrix sandwich and are soldered on the component side, then trimmed flush.

Pro Trinket Pin

Matrix Driver Pin

+ (JST pad on back)

VCC

– (JST pad on back)

GND

A4

SDA

A5

SCL

If building pendant: push the spacer out from between the boards using a small screwdriver, then slide the battery into this space in the direction shown. Plug the battery into the LiPoly Backpack board.

For non-pendant builds: the battery doesn’t need to fit there unless you want it…just plug it into the backpack.

Test Run

Flick the switch to the “on” position and see what you get! It’s normal for the Pro Trinket to flash its red LED for about 5 seconds, then the sketch will start, and you should see the animation playing out on the LED matrix.

If it does NOT run:

  • Is the battery charged? Try plugging in USB to power the circuit that way.
  • Did you upload the FirePendant sketch to the Pro Trinket board as explained on the “Software” page?
  • Double-check the connections between the two boards. Did you get SDA and SCL crossed?
  • Look around for any cold joints, solder bridges, or bits of conductive detritus that may have fallen in when clipping pins flush.

If everything works, switch it off and we’ll finish sealing this up.

If it still doesn’t work…first, confirm you can get the basic Arduino “blink” sketch uploaded. That’ll help confirm whether it’s a hardware or software problem. Then you can ask for help in the Adafruit Forums. It’s extremely helpful if you can provide a couple well-lit and in-focus photos that clearly show all the connections.

Do not continue until you have the flame animation playing on the LED matrix.

These steps mostly apply to the pendant design. If you’re making a votive or other installation, you’re pretty much done with the guide and just need to devise your own mounting scheme (hot glue, tape, or your own 3D-printed support)…but skip down to the bottom of this page for notes about charging.

Apply a small piece of tape to the back of the Pro Trinket board, covering the JST pads and wires. The matrix will sit very close atop this and we don’t want any shorts if something is flexed around.

It doesn’t need to be fancy Kapton tape like this…ordinary masking tape or a couple layers of office sticky tape will work just fine.

Press the power switch into its space in the back of the case, with the slide bit poking out the hole.

A little bit of glue helps secure this in place…5-minute epoxy is ideal, or E6000 if you’re patient…but in either case, do not get any glue inside the switch (those two little notches on the side lead straight in). Use a toothpick and apply glue near the side flanges or down near the legs.

Make sure the switch is straight while the glue sets up…pinch it with something if need be. E6000 needs at least a couple hours to really firm up, so you’ll be taking a few breaks if that’s what you’ve got. Even “5-minute” epoxy should be given more than 5 minutes…15 or 20 is a good start.

The Pro Trinket and LiPoly Backpack are next…various notches and nubbins line up with the mounting holes on these boards, but you’ll need a few small dabs of glue to keep them permanently in place.

Make sure no wires are tangled underneath the boards! Get that all straightened out, then glue the boards down.

Hold this in place with tape or rubber bands or a clothespin or such while the glue dries.

The LED matrix sandwich then sits on top, also with mounting nubbins and a couple dabs of glue. Use tape or rubber bands to hold it while that dries.

It’s normal that the matrix “leans” along the length of the case…it’s designed that way, slightly tapered. But make sure it’s straight in the other direction.

Once the glue is dry, pinch a kink at the midpoint of the battery wire, then tuck this underneath the other electronics…it’s a tight squeeze, but should all fit.

Fit the case front over the matrix, then add two screws (#2-56 x 3/8" or M2 x 10mm).

The screw sockets aren’t threaded…3D printing lacks the resolution for that…you can either add threads using a tapping tool if you have one, or just grind the screws into the plastic ’til they bite, and tighten until the back fits securely and the screw heads are flush below the surface of the case.

If you accidentally strip the threads…well, more glue. No biggie.

Test it once again, making sure everything still works.

Add a fancy necklace chain or a simple nylon or leather cord, whatever suits your style!

Fully charged, the 350 mAh LiPoly battery should provide about 4 hours run time. When it’s nearly depleted, the flame may flash a few times before turning off completely.

To charge, switch it off and plug in a USB microB cable. LEDs on the edge indicate the battery charging status: a red LED indicates charging, while a green LED on the corner (not the one near the USB plug) lets you know the battery is full. A 100% charge from a fully-depleted battery will also take about 4 hours.

If you’re installing this in something other than the pendant, you have the option of using a higher-capacity battery (500, 1000 mAh or more!) with a proportionally longer run time. If you go this route, there are two pads on the back of the LiPoly backpack that can be bridged with a dot of solder to enable a faster 500 mA charging rate. Do this only if using a 500 mAh battery or larger!

This guide was first published on Apr 15, 2016. It was last updated on Mar 27, 2024.