Electronics
Halloween Jack O’ Lantern V2
After the success of my previous project, the Halloween, Jack O’ Lantern – many of you asked for something even more dynamic, with smoother flame transitions and more LED action.
Introduction
After the success of my previous project, the Halloween, Jack O’ Lantern – many of you asked for something even more dynamic, with smoother flame transitions and more LED action. So here it is: Ultimate Fire Effect, a fully reworked version with improved hardware and software, driven by the tiny but mighty ATtiny85.
This project expands on the original flickering candle by adding six independently controlled LEDs, more realistic flickering, and even an optional magical blue flame effect inspired by Harry Potter. It’s perfect for pumpkins, lanterns, fantasy props, or spooky Halloween scenes.
Hardware Overview
I stuck with the ATtiny85 for this version, it’s compact, affordable, power-efficient, and powerful enough for individual PWM control.
New in this version:
- 6* individually controlled LEDs (3 red, 3 yellow)
- PWM flickering on all pins (PB0–PB5)
- More lifelike flame simulation
- Optional: blue flame effect
- Custom PCB for easy and compact assembly
*Optional: default 5 LED’s, Pin PB5 (pin 1) is the default RESET pin. → If you want to use it as a regular output, you must set the “RSTDISBL” fuse using avrdude.
Be aware that this makes reprogramming via ISP more difficult, you will need High Voltage programmer.
Bill of Materials
| Quantity | Component | |
|---|---|---|
| 1 | Microchip ATtiny85 | ![]() |
| 6 | 200Ω Resistors | |
| 3 | Red 5mm LEDs | ![]() |
| 3 | Yellow 5mm LEDs | ![]() |
| 1 | 3×AA Battery Holder with switch | ![]() |
| 3 | AA Batteries | ![]() |
| 1 | Custom PCB (see below) |
Circuit Diagram
All six LED’s are connected to individual PWM pins on the ATtiny85. The schematic shows the basic wiring, and the matching PCB layout keeps everything neat.
See the images below:
Schematic:

PCB Layout:


Code, Halloween V2.0.0
This version uses per-LED fading with randomness and occasional spark bursts for realism. Smooth transitions are achieved with logarithmic fading.
View the full source code
/**
Copyright 2025 Achim Pieters | StudioPieters®
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information visit https://www.studiopieters.nl
Ultimate Fire Effect – Realistic Flame for ATtiny85
- D4 (PB4/OC1B) remains hardware PWM
- D2, D3 and optionally D6 (PB5/reset): soft-PWM with 3-bit @ ~125 Hz + dithering
- Per-LED phase shift to reduce visible pulse synchronization
- Slightly faster animation (10 ms) and extra smoothing for D2/D3/D4
- Extra life for YELLOW: micro-flicker + slow sway + soft yellow sparks
Color mapping:
PB5=red, PB4=yellow, PB3=yellow, PB2=red, PB1=yellow, PB0=red
**/
#include <Arduino.h>
#include <avr/pgmspace.h>
#define NUM_LEDS 6
// Pins (DigiSpark/ATtiny85): Arduino 0..5 => PB0..PB5
const uint8_t ledPins[NUM_LEDS] = {0, 1, 2, 3, 4, 5};
// true = YELLOW (bright), false = RED (subtle)
// Order: PB0, PB1, PB2, PB3, PB4, PB5
// Requested: PB5=R, PB4=Y, PB3=Y, PB2=R, PB1=Y, PB0=R
const uint8_t isYellow[NUM_LEDS] = {0, 1, 0, 1, 1, 0};
// ---- Fast helpers -------------------------------------------------
#define CONSTRAIN8(x) ((uint8_t)((x) > 255 ? 255 : ((x) < 0 ? 0 : (x))))
static inline uint8_t pinToMask(uint8_t arPin) {
return (uint8_t)(1u << arPin);
}
// Soft-PWM pin selection: D2, D3 and optionally D6(PB5)
static inline bool isSoftPinNum(uint8_t arPin) {
return (arPin == 2 || arPin == 3 || arPin == 5); // drop 5 if PB5 is still RESET
}
// ---- Tunables (unchanged logic) -----------------------------------
const uint8_t Y_MIN = 140, Y_MAX = 255;
const uint8_t R_MIN = 50, R_MAX = 150;
const uint8_t RED_TARGET_MAX = 175;
const uint8_t RED_SPARK_MAX = 165;
// “1/f”-like base drift
const uint16_t BASE_UPDATE_MIN_MS = 22, BASE_UPDATE_MAX_MS = 47;
const uint8_t BASE_SMOOTH = 2;
const int8_t GROUP_JITTER_AMP = 5;
const uint8_t JITTER_PROB = 8;
// Per-LED offsets
const int8_t LED_OFFSET_AMP_Y = 22;
const int8_t LED_OFFSET_AMP_R = 14;
const uint16_t LED_OFFSET_MIN_MS = 120, LED_OFFSET_MAX_MS = 260;
const uint8_t LED_OFFSET_SMOOTH = 16;
// Wind
const int8_t WIND_AMP = 18;
const uint16_t WIND_UPDATE_MS = 220;
const uint8_t WIND_SMOOTH = 24;
// Sparks
const uint16_t SPARK_CHANCE = 3500;
const uint16_t SPARK_HOLD_MS = 70;
// Extra smoothing for soft pins (D2/D3) and D4 (hardware)
const uint8_t EXTRA_SMOOTH_SOFT = 6;
const uint8_t EXTRA_SMOOTH_D4 = 4;
// --- NEW: Extra life for YELLOW ---
const uint8_t Y_FLICKER_AMP = 10; // ± around 0
const uint16_t Y_FLICKER_INT_MS = 14;
const uint8_t Y_FLICKER_SMOOTH = 6;
const int8_t Y_SWAY_AMP = 14; // peak amplitude
// Store in PROGMEM to save RAM
const uint8_t Y_SWAY_SPEEDS_P[NUM_LEDS] PROGMEM = {2, 1, 2, 3, 1, 2};
const uint16_t Y_SPARK_DECAY_MS = 90; // soft tail for yellow
// ---- State ---------------------------------------------------------
uint8_t baseVal = 200, baseTarget = 200;
uint16_t nextBaseInterval = 30;
unsigned long lastBaseUpdate = 0;
int8_t windVal = 0, windTarget = 0;
unsigned long lastWindUpdate = 0;
int8_t groupJitter = 0;
int8_t ledOffset[NUM_LEDS] = {0};
int8_t ledOffsetTarget[NUM_LEDS] = {0};
uint16_t nextOffsetInterval[NUM_LEDS] = {0};
unsigned long lastOffsetUpdate[NUM_LEDS] = {0};
uint8_t currentPWM[NUM_LEDS] = {0};
uint8_t targetPWM[NUM_LEDS] = {0};
unsigned long lastSpark[NUM_LEDS] = {0};
// Yellow-only layers
int8_t yFlickVal[NUM_LEDS] = {0};
int8_t yFlickTarget[NUM_LEDS] = {0};
unsigned long yFlickLast[NUM_LEDS] = {0};
uint8_t ySwayPhase[NUM_LEDS] = {0};
// ---- Soft PWM (3-bit + dithering) ---------------------------------
#define SOFT_BITS 3
#define SOFT_TOP ((1 << SOFT_BITS) - 1) // 7
volatile uint8_t softDuty[NUM_LEDS]; // 0..7 (read in ISR)
volatile uint8_t softPhase = 0; // 0..7
uint8_t softPinMask[NUM_LEDS];
uint8_t isSoft[NUM_LEDS];
uint8_t softPinMaskAll = 0;
uint8_t ditherAcc[NUM_LEDS]; // 0..7
uint8_t softPhaseOffset[NUM_LEDS]; // per-LED phase shift
// Precomputed per-pin smoothing divisor
uint8_t pinSmoothDiv[NUM_LEDS];
// ---- Fast PRNG (xorshift16) ---------------------------------------
// Much cheaper than Arduino random(); no modulo/div.
static uint16_t rngState = 0xA5C3u;
static inline uint8_t rng8() {
uint16_t x = rngState;
x ^= x << 7;
x ^= x >> 9;
x ^= x << 8;
rngState = x;
return (uint8_t)x;
}
static inline uint16_t rng16() { // combine two steps
uint16_t x = rngState;
x ^= x << 7; x ^= x >> 9; x ^= x << 8;
rngState = x;
return x;
}
static inline uint8_t rnd8_range(uint8_t a, uint8_t b_inclusive) {
uint8_t span = (uint8_t)(b_inclusive - a + 1);
return (uint8_t)(a + (rng8() % span));
}
static inline uint16_t rnd16_range(uint16_t a, uint16_t b_inclusive) {
uint16_t span = (uint16_t)(b_inclusive - a + 1);
return (uint16_t)(a + (rng16() % span));
}
static inline int8_t rndSigned(int8_t amp) {
// returns [-amp, +amp]
// map rng8 0..255 -> -amp..+amp without division
uint8_t r = rng8(); // 0..255
int16_t v = (int16_t)((r >> 1)); // 0..127
v = (v * amp) >> 6; // ~scale to 0..~2*amp
// center and randomize sign bit
return (r & 1) ? (int8_t)v : (int8_t)(-v);
}
// ---- Math helpers --------------------------------------------------
static inline uint8_t approach8(uint8_t cur, uint8_t tgt, uint8_t div) {
if (cur == tgt) return cur;
int16_t d = (int16_t)tgt - (int16_t)cur;
int16_t step = d / (int16_t)div;
if (step == 0) step = (d > 0) ? 1 : -1;
int16_t out = (int16_t)cur + step;
return (uint8_t)(out < 0 ? 0 : (out > 255 ? 255 : out));
}
// Soft tone mapping for red: gamma ~2 and ~0.6 scale (3/5)
static inline uint8_t mapRed(uint8_t v) {
uint16_t g = ((uint16_t)v * (uint16_t)v) / 255; // gamma ~2
g = (g * 3) / 5; // ≈0.60
return (uint8_t)(g > 255 ? 255 : g);
}
// 8-bit triangle: 0..255 -> 0..254..0
static inline uint8_t tri8(uint8_t p) {
return (p & 0x80) ? (uint8_t)(255 - ((p & 0x7F) << 1)) : (uint8_t)((p & 0x7F) << 1);
}
// ---- Timer1 Overflow ISR ------------------------------------------
ISR(TIMER1_OVF_vect) {
uint8_t phase = (uint8_t)(softPhase + 1);
if (phase > SOFT_TOP) phase = 0;
softPhase = phase;
if (softPinMaskAll) {
PORTB &= ~softPinMaskAll; // all low
// per-LED phase shift
for (uint8_t i = 0; i < NUM_LEDS; i++) {
if (!isSoft[i]) continue;
uint8_t localPhase = (uint8_t)((softPhase + softPhaseOffset[i]) & SOFT_TOP);
if (softDuty[i] > localPhase) PORTB |= softPinMask[i];
}
}
}
// ---- Setup ---------------------------------------------------------
void setup() {
// Seed PRNG a bit (millis not running yet; mix PINB noise)
rngState ^= (uint16_t)analogRead(0) ^ (uint16_t)(PINB << 8) ^ 0x5A5A;
for (uint8_t i = 0; i < NUM_LEDS; i++) {
pinMode(ledPins[i], OUTPUT);
// Soft/HW bookkeeping
uint8_t soft = isSoftPinNum(ledPins[i]) ? 1 : 0;
isSoft[i] = soft;
softPinMask[i] = pinToMask(ledPins[i]);
ditherAcc[i] = 0;
// Spread pulse phases per LED
softPhaseOffset[i] = (uint8_t)(rng8() & SOFT_TOP);
if (soft) {
softPinMaskAll |= softPinMask[i];
softDuty[i] = 0;
PORTB &= ~softPinMask[i]; // init LOW
}
// Per-LED offsets
ledOffset[i] = 0;
ledOffsetTarget[i] = 0;
lastOffsetUpdate[i] = millis();
nextOffsetInterval[i] = rnd16_range(LED_OFFSET_MIN_MS, LED_OFFSET_MAX_MS);
// Yellow layers
yFlickVal[i] = 0;
yFlickTarget[i]= 0;
yFlickLast[i] = millis();
ySwayPhase[i] = rng8();
// Precompute smoothing divisor per pin
uint8_t p = ledPins[i];
uint8_t s = 3;
if (p == 2 || p == 3) s = EXTRA_SMOOTH_SOFT; // D2, D3
else if (p == 4) s = EXTRA_SMOOTH_D4; // D4
pinSmoothDiv[i] = s;
}
lastBaseUpdate = millis();
nextBaseInterval = rnd16_range(BASE_UPDATE_MIN_MS, BASE_UPDATE_MAX_MS);
lastWindUpdate = millis();
windTarget = rndSigned(WIND_AMP);
// Kick Timer1 via OC1B and enable overflow interrupt
analogWrite(4, 0); // D4/OC1B duty = 0 (no light)
// Fast PWM on OC1B, prescaler = 1, TOP = 255
TCCR1 = _BV(CS10) | _BV(PWM1B) | _BV(COM1B1);
OCR1C = 255;
TIMSK |= _BV(TOIE1);
}
// ---- Loop ----------------------------------------------------------
void loop() {
const unsigned long now = millis();
// --- slow group base ---
if ((uint16_t)(now - lastBaseUpdate) >= nextBaseInterval) {
lastBaseUpdate = now;
nextBaseInterval = rnd16_range(BASE_UPDATE_MIN_MS, BASE_UPDATE_MAX_MS);
baseTarget = rnd8_range(110, 230);
}
baseVal = approach8(baseVal, baseTarget, BASE_SMOOTH);
// --- wind ---
if ((uint16_t)(now - lastWindUpdate) >= WIND_UPDATE_MS) {
lastWindUpdate = now;
windTarget = rndSigned(WIND_AMP);
}
if (windVal != windTarget) {
int16_t d = (int16_t)windTarget - (int16_t)windVal;
windVal += (int8_t)(d / (int16_t)WIND_SMOOTH);
if (d && (d / (int16_t)WIND_SMOOTH) == 0) windVal += (d > 0 ? 1 : -1);
}
// --- very subtle group jitter ---
if ((rng8() % JITTER_PROB) == 0) groupJitter = rndSigned(GROUP_JITTER_AMP);
// --- per-LED offsets ---
for (uint8_t i = 0; i < NUM_LEDS; i++) {
if ((uint16_t)(now - lastOffsetUpdate[i]) >= nextOffsetInterval[i]) {
lastOffsetUpdate[i] = now;
nextOffsetInterval[i]= rnd16_range(LED_OFFSET_MIN_MS, LED_OFFSET_MAX_MS);
const int8_t amp = isYellow[i] ? LED_OFFSET_AMP_Y : LED_OFFSET_AMP_R;
ledOffsetTarget[i] = rndSigned(amp);
}
int16_t od = (int16_t)ledOffsetTarget[i] - (int16_t)ledOffset[i];
ledOffset[i] += (int8_t)(od / (int16_t)LED_OFFSET_SMOOTH);
if (od && (od / (int16_t)LED_OFFSET_SMOOTH) == 0) ledOffset[i] += (od > 0 ? 1 : -1);
}
// --- yellow-only micro-flicker + slow sway ---
for (uint8_t i = 0; i < NUM_LEDS; i++) {
if (!isYellow[i]) continue;
if ((uint16_t)(now - yFlickLast[i]) >= Y_FLICKER_INT_MS) {
yFlickLast[i] = now;
yFlickTarget[i]= rndSigned(Y_FLICKER_AMP);
}
int16_t fd = (int16_t)yFlickTarget[i] - (int16_t)yFlickVal[i];
yFlickVal[i] += (int8_t)(fd / (int16_t)Y_FLICKER_SMOOTH);
if (fd && (fd / (int16_t)Y_FLICKER_SMOOTH) == 0) yFlickVal[i] += (fd > 0 ? 1 : -1);
// speed per LED from PROGMEM
ySwayPhase[i] += pgm_read_byte(&Y_SWAY_SPEEDS_P[i]); // overflow ok
}
// --- outputs ---
for (uint8_t i = 0; i < NUM_LEDS; i++) {
// spark (yellow gets soft decay)
if ((rng16() % SPARK_CHANCE) == 0) {
lastSpark[i] = now ? now : 1;
targetPWM[i] = isYellow[i] ? 255 : RED_SPARK_MAX;
}
uint8_t sparkActive = 0;
uint16_t sparkWindow = SPARK_HOLD_MS;
if (isYellow[i]) sparkWindow = (uint16_t)(SPARK_HOLD_MS + Y_SPARK_DECAY_MS);
if (lastSpark[i] && (uint16_t)(now - lastSpark[i]) < sparkWindow) {
sparkActive = 1;
if (isYellow[i]) {
uint16_t elapsed = (uint16_t)(now - lastSpark[i]);
if (elapsed > SPARK_HOLD_MS) {
uint16_t t = (uint16_t)(elapsed - SPARK_HOLD_MS);
uint8_t fall = (t >= Y_SPARK_DECAY_MS) ? 0
: (uint8_t)(((uint16_t)(Y_SPARK_DECAY_MS - t) * 255) / Y_SPARK_DECAY_MS);
uint8_t decTgt = (uint8_t)(200 + ((uint16_t)55 * fall) / 255); // 255 -> ~200
if (targetPWM[i] > decTgt) targetPWM[i] = decTgt;
}
}
}
if (!sparkActive) {
lastSpark[i] = 0;
int16_t base = (int16_t)baseVal + (int16_t)windVal + (int16_t)groupJitter;
if (base < 0) base = 0; else if (base > 255) base = 255;
const uint8_t lo = isYellow[i] ? Y_MIN : R_MIN;
const uint8_t hi = isYellow[i] ? Y_MAX : R_MAX;
uint16_t v = (uint16_t)lo + (((uint16_t)(hi - lo) * (uint8_t)base) / 255);
int16_t withOffset = (int16_t)v + (int16_t)ledOffset[i];
if (isYellow[i]) {
int16_t swaySigned = (int16_t)tri8(ySwayPhase[i]) - 127; // -127..+127
swaySigned = (swaySigned * (int16_t)Y_SWAY_AMP) / 127;
withOffset += (int16_t)yFlickVal[i] + swaySigned;
}
if (!isYellow[i] && withOffset > RED_TARGET_MAX) withOffset = RED_TARGET_MAX;
if (withOffset < 0) withOffset = 0;
if (withOffset > 255) withOffset = 255;
targetPWM[i] = (uint8_t)withOffset;
}
// smoothing per pin (precomputed)
currentPWM[i] = approach8(currentPWM[i], targetPWM[i], pinSmoothDiv[i]);
// final value (after red gamma)
uint8_t out8 = isYellow[i] ? currentPWM[i] : mapRed(currentPWM[i]);
if (isSoft[i]) {
// 8-bit -> 3-bit + temporal dithering
uint8_t baseN = out8 >> (8 - SOFT_BITS); // 0..7
uint8_t remN = out8 & ((1 << (8 - SOFT_BITS)) - 1);// 0..31
uint8_t remScaled = remN >> (5 - SOFT_BITS); // 0..7
uint8_t acc = (uint8_t)(ditherAcc[i] + remScaled);
if (acc > SOFT_TOP) {
acc -= (SOFT_TOP + 1);
if (baseN < SOFT_TOP) baseN++;
}
ditherAcc[i] = acc;
noInterrupts();
softDuty[i] = baseN;
interrupts();
} else {
// Hardware PWM for D0, D1, D4
analogWrite(ledPins[i], out8);
}
}
// faster update for smoother perception
delay(10);
}
Optional, Halloween V2.1.0
Blue Flame, Harry Potter Style
Want a mysterious blue flame effect instead? Try this cool-burning version for a more magical, fantasy-style look.
Build the same harde ware but replace the Red and Yellow LED’s for All Neon blue LED’s.

View the full source code
/**
Copyright 2025 Achim Pieters | StudioPieters®
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information visit https://www.studiopieters.nl
ATtiny85 – Blue Wizard Fire (HP Cinematic)
5x neon-blue LEDs on PB0..PB4 (D0..D4), NOT on RESET/PB5.
Core: ATTinyCore (Spence Konde). No external libraries.
What you get:
- Deep blue flame as the base (organic)
- Crackle (sparking crackles)
- Whoosh (fast flare-up with afterglow)
- SpellBurst (staccato pulses marching across the LEDs)
- PatronusWave (center-out wave, clearly visible)
- Global swell (musical swell)
Everything is non-blocking and layers nicely.
**/
const bool ACTIVE_HIGH = true; // LED to GND -> true, to VCC -> false
const uint8_t LED_PINS[] = {0,1,2,3,4}; // D0..D4 = PB0..PB4
const uint8_t NUM_LEDS = sizeof(LED_PINS);
// ====== “Cinematic” settings (tweak here) ======
uint8_t MAX_BRIGHT = 230; // go big! blue is bright — set 255 if desired
uint8_t MIN_BRIGHT = 6; // never completely off
uint8_t INTENSITY = 100; // 60..140 – global multiplier in %
const uint8_t LOOP_MS = 10; // faster cadence
const uint8_t GAMMA2 = 1; // 1 = gamma~2 enabled, 0 = off
// ===== Flame “personality” =====
struct Flame {
uint16_t t;
uint8_t speed;
uint8_t base;
uint8_t range;
uint8_t seed;
} flames[NUM_LEDS];
// ===== RNG =====
static uint16_t xr = 0xA1C3;
uint16_t r16(){
uint16_t x=xr; x^=x<<7; x^=x>>9; x^=x<<8; return xr=x;
}
uint8_t r8(uint8_t a,uint8_t b){
return a + (uint8_t)(r16() % (uint16_t)(b-a+1));
}
// ===== Smooth value-noise 0..255 =====
uint8_t noise(uint16_t t, uint8_t s){
uint8_t x0 = (uint8_t)(t>>8), x1 = x0+1;
auto h=[](uint8_t x,uint8_t s) -> uint8_t {
uint8_t v=x; v^=s*37U+17U; v=(v^61U)^(v>>3); v*=9U; v^=(v<<8); return v;
};
uint16_t a=h(x0,s), b=h(x1,s);
uint8_t f8 = (uint8_t)(t&0xFF);
uint16_t f=f8, f2=(f*f)>>8, f3=(f2*f)>>8;
uint16_t fade=((3*f2)>255 ? 255 : (3*f2)) - (2*f3);
if((int16_t)fade<0) fade=0; if(fade>255) fade=255;
return (uint8_t)(((256-fade)*a + fade*b)>>8);
}
inline uint8_t g2(uint8_t x){
uint16_t v=x; v=(v*v)>>8; return (uint8_t)v;
}
inline void ledWrite(uint8_t pin, uint8_t val){
if(!ACTIVE_HIGH) val=255-val; analogWrite(pin,val);
}
inline uint8_t clamp8(int16_t v){
if(v<0) return 0; if(v>255) return 255; return (uint8_t)v;
}
// ===== Overlays =====
uint8_t crackle[NUM_LEDS]; // fast crackle
const uint8_t CRACKLE_DECAY = 24;
const uint8_t CRACKLE_CHANCE = 35; // ~1/35 per tick per LED (quite often)
uint8_t whoosh = 0; // global boost with slower decay
const uint8_t WHOOSH_DECAY = 3;
uint32_t nextWhooshAt=0;
bool burstActive=false;
uint8_t burstStep=0, burstRepeats=0; // staccato train
uint32_t nextBurstAt=0;
bool waveActive=false;
int16_t wavePos=0; // 0..(NUM_LEDS-1)*256
int16_t waveSpeed=240; // fast
uint8_t waveStrength=160; // highly visible
bool swellActive=false;
uint16_t swellPh=0, swellSpd=900;
uint8_t swellAmt=80;
uint32_t nextSwellAt=0;
// ===== helpers for triggers =====
void scheduleWhoosh(uint32_t now){
nextWhooshAt = now + (400 + (r16()%1800));
}
void scheduleBurst (uint32_t now){
nextBurstAt = now + (900 + (r16()%2500));
}
void scheduleSwell (uint32_t now){
nextSwellAt = now + (700 + (r16()%2200));
}
void startWhoosh(){
whoosh = r8(120, 200);
} // strong flare-up
void startBurst(){
burstActive=true; burstStep=0; burstRepeats=r8(2,4);
}
void startWave (){
waveActive=true; wavePos = (NUM_LEDS/2)*256; // start in the center
waveSpeed = 200 + (r16()%200);
waveStrength = r8(130, 200);
}
void startSwell(){
swellActive=true; swellPh=0; swellSpd = 700 + (r16()%1400); swellAmt=r8(60,120);
}
// ===== setup =====
void setup(){
for(uint8_t i=0; i<NUM_LEDS; i++) { pinMode(LED_PINS[i],OUTPUT); ledWrite(LED_PINS[i],0); }
for(uint8_t i=0; i<NUM_LEDS; i++) {
flames[i].t = r16();
flames[i].speed = r8(3,8);
flames[i].base = r8(18,34);
flames[i].range = r8(120,170); // greater dynamic range
flames[i].seed = r8(1,250);
crackle[i]=0;
}
uint32_t now=millis();
scheduleWhoosh(now); scheduleBurst(now); scheduleSwell(now);
// start immediately with a Patronus wave so you get a “wow” right away
startWave();
}
// ===== loop =====
void loop(){
uint32_t now = millis();
// triggers
if((int32_t)(now-nextWhooshAt)>=0) { startWhoosh(); scheduleWhoosh(now); }
if((int32_t)(now-nextBurstAt )>=0) { startBurst(); scheduleBurst(now); }
if((int32_t)(now-nextSwellAt)>=0) { startSwell(); scheduleSwell(now); }
// a visible “Patronus” wave every now and then
if(!waveActive && (r16() & 0x03FF)==0) startWave();
for(uint8_t i=0; i<NUM_LEDS; i++) {
Flame &f = flames[i];
// — Base flame (two layers of noise, more contrast) —
uint8_t n1 = noise(f.t, f.seed);
uint8_t n2 = noise(f.t + 17000U, f.seed ^ 0x9B);
uint16_t mix = ((uint16_t)n1*180 + (uint16_t)n2*76) >> 8; // 70/30
int16_t raw = f.base + ((uint16_t)f.range * mix >> 8);
// — Crackle: frequent, short, and bright —
if((r16() % CRACKLE_CHANCE) == 0) {
uint8_t add = r8(120, 220); // really bright
uint16_t c = crackle[i] + add; crackle[i] = (c>240 ? 240 : (uint8_t)c);
}
if(crackle[i]) { raw += crackle[i]; crackle[i] = (crackle[i]>CRACKLE_DECAY) ? (crackle[i]-CRACKLE_DECAY) : 0; }
// — Whoosh (global flare-up) —
if(whoosh) { raw += whoosh; whoosh = (whoosh>WHOOSH_DECAY) ? (whoosh-WHOOSH_DECAY) : 0; }
// — SpellBurst: staccato pulse train that marches —
if(burstActive) {
// burstStep determines which LED gets the “hit”, then echoes
uint8_t lead = (burstStep % NUM_LEDS);
int8_t d = (int8_t)i - (int8_t)lead;
int16_t boost = 0;
if(d==0) boost = 200; // main hit
else if(d==1 || d==-1) boost = 120; // direct echo
else if(d==2 || d==-2) boost = 60; // trailing tail
raw += boost;
}
// — PatronusWave: center-out wave with a wide lobe —
if(waveActive) {
int16_t here = (int16_t)i*256;
int16_t d = here - wavePos; if(d<0) d=-d;
int16_t width = 380; // width of the lobe
if(d < width) {
// bell-ish (triangle-ish) shape -> clearly visible
raw += (int16_t)((uint32_t)(width - d) * waveStrength / width);
}
}
// — Swell (slow musical swell) —
if(swellActive) {
uint8_t t8 = (uint8_t)(swellPh>>8);
uint16_t t=t8, t2=(t*t)>>8, t3=(t2*t)>>8;
uint16_t ease=((3*t2)>255 ? 255 : (3*t2)) - (2*t3); // 0..255
raw += (uint16_t)(swellAmt * ease) >> 8;
}
// global intensity + clamping
raw = (raw * INTENSITY) / 100;
if(raw > MAX_BRIGHT) raw = MAX_BRIGHT;
if(raw < MIN_BRIGHT) raw = MIN_BRIGHT;
uint8_t v = (GAMMA2 ? g2((uint8_t)raw) : (uint8_t)raw);
ledWrite(LED_PINS[i], v);
// advance time (slightly faster than before)
f.t += (uint16_t)(f.speed * 7U);
}
// wave progress
if(waveActive) {
wavePos += waveSpeed;
if(wavePos > (int16_t)((NUM_LEDS-1)*256 + 420)) waveActive=false;
}
// burst progress (staccato, fast)
static uint32_t lastBurstStep=0;
if(burstActive && (now - lastBurstStep) >= 55) { // 55 ms per step
lastBurstStep++; // exact value isn’t important
burstStep++;
if(burstStep >= NUM_LEDS) {
burstStep = 0;
if(burstRepeats) burstRepeats--;
else burstActive=false;
}
}
// swell progress
if(swellActive) {
uint16_t prev = swellPh; swellPh += swellSpd;
if(swellPh < prev) swellActive=false; // done after overflow
}
delay(LOOP_MS);
}
Programming & Assembly
Program the ATtiny85 using a USBasp, TinyUSB programmer, or even an Arduino-as-ISP. Then insert it into the PCB, connect the LED’s as shown, and power it with three AA batteries.
Once powered on:
- V2.0.0 delivers warm red-yellow flickering with glowing bursts.
- V2.1.0 provides cool, subtle pulses with rare magical flashes.
Note: Pin PB5 (pin 1) is the default RESET pin. → If you want to use it as a regular output, you must set the “RSTDISBL” fuse using avrdude.
Be aware that this makes reprogramming via ISP more difficult, you will need High Voltage programming.
FREE PCB Download
Want to build it yourself? You can download the full KiCad files, Gerbers, and HEX code from my GitHub:
Or orde your PCB’s fast and easy at PCBway here.
Final Thoughts
With just a handful of components and some code magic, you can create an enchanting fire effect that’s both safe and reusable. Whether you choose the fiery red or the magical blue version, this is a perfect Halloween or cosplay prop for any DIY enthusiast.
Happy Halloween, and stay spooky!





