Skip to main content
Back to articles
2026 / 02
| 5 min read

Blinking fireflies on an FPGA

Five flashing LEDs, independent phase counters and the timing problem that appeared when their updates began sharing a pipeline.

fpga electronics artificial intelligence experiments
On this page

I wanted to find out whether I could use AI to program an FPGA and get something running that I could actually see on the hardware. Blinking lights seemed like a reasonable place to start. The job was small enough to follow from what I wanted the board to do, through the tools that turned it into a circuit, to lights blinking in front of me.

That first version blinked. With it working, I started reading about firefly synchronization in nature and thinking about adding more dynamics to the flashes. An FPGA seemed like an interesting place to explore independent things influencing one another: each firefly could keep its own state, advance at its own rate and respond when another one flashed.

The version published in February goes further than the initial blinking experiment. It has five pulse-coupled oscillators, LED brightness control, audio and serial telemetry on an iCEstick board. Moving the five phase values into block RAM let them share an updater, but it also introduced a coupling bug: which flash a firefly could respond to depended on when the pipeline visited it. To see why, we need to follow how one phase advances and how a neighbour’s flash changes it.

Keeping five different times

Each firefly has a 32-bit phase value. Add a small number to it repeatedly and it eventually wraps back to zero; the position within that cycle tells the circuit when to flash. Giving the five phases slightly different increments makes their natural periods differ.

They share one 12 MHz hardware clock. Independence here means separate phase state and different rates of advance, rather than five unrelated electrical clocks. In the current HDL, the phase values live in block RAM and a shared pipeline processes one firefly per clock, so each phase is updated 2.4 million times per second.

The uncoupled period follows from the size of the counter and how often it is advanced:

period = 2^32 / (updates per second × phase increment)
FireflyIncrement per updateCalculated uncoupled period
011751.523 s
111851.510 s
211951.498 s
312051.485 s
412151.473 s

Those values are calculated from the HDL constants. They aren’t measured blink intervals once the fireflies begin affecting one another. That interaction changes when a phase reaches the flashing part of its cycle.

What a flash does to another phase

A firefly flashes during the last eighth of its cycle. When it enters that region, the other fireflies may advance their phases a little, depending on where they are in their own cycles:

Position in the cycleLEDResponse to another firefly starting a flash
0% to below 50%DarkIgnore it
50% to below 87.5%DarkAdvance the phase
87.5% to below 100%Flashing and fadingIgnore it

At the strongest coupling setting, one other firefly starting a flash advances a receptive phase by one thirty-second of a cycle, or 3.125%. Several simultaneous starts contribute several kicks. The circuit cycles through four coupling strengths and periodically scatters the phases using a deterministic pseudorandom sequence, allowing the interaction to start again from different positions.

That gives each flash a specific effect. A firefly approaching its own flash can be pulled forward, while one that has only just begun a new cycle is left alone. This was inspired by reading about pulse-coupled fireflies, but the implemented rule has linear phase counters, differing natural frequencies and a gated response. It isn’t a reproduction of the biological system or a verification of the formal Mirollo–Strogatz result.

Sharing the updater changed who saw a flash

The shared pipeline saves duplicating the phase-update machinery, but it also means the five phases are examined in sequence. A complete round takes five clocks, about 0.417 microseconds. That’s very short compared with a one-and-a-half-second blink, yet the order still matters if an event is only visible during one slot.

Moving the phases into block RAM introduced exactly that problem. The coupling fix in the history records an asymmetry in which firefly 0 only coupled to firefly 4. The correction was to take one snapshot of the newly flashing fireflies and hold it for a complete round:

if (fly_proc == 3'd4) begin
    rise_latch <= in_flash & ~was_flash;
    was_flash  <= in_flash;
end

in_flash & ~was_flash picks out the phases that have entered the flash region since the previous snapshot. Keeping that vector in rise_latch means all five pipeline slots see the same set of events, with each firefly excluding itself from the count. The pipeline still visits them one at a time; their view of which flashes occurred no longer depends on that visit order.

There wasn’t a single great aha moment in this project. Getting the first lights blinking answered the original hardware question, and the synchronization reading gave me more to build on. By the time there were interacting phases, a lamp alone was also a fairly limited way to inspect them. The implementation sends a 16-byte serial packet containing a marker followed by a phase and brightness value for each firefly, roughly eleven times a second. That gives the host something to plot or display alongside the LEDs, including differences that are hard to judge by watching the flashes.