Blinkenlights 2.0

Ido GendelApril 17, 2024

The Amiga 1000 computer had a little red light, right next to the power switch, but it wasn't just a power indicator. Whenever I – back then, a clueless 12-year-old – tried to run a pirated, cracked game from a worn-out 3.5" floppy, I kept a nervous eye on that light. If it started blinking, it meant that something's gone terribly wrong, the computer is restarting, and I won't be playing that game any time soon. That was my first encounter with "Blinkenlights": informative on/off patterns of light that a system displays to indicate internal states or faults.

The Simplest indications are trivial to implement. For instance, if I have a dedicated LED to indicate that my embedded system is loading, I can just put a "LED.on()" command around the start of the initialization code and a "LED.off()" at the end of it. However, for economic and for product-size reasons, we usually want a single LED to be able to express several different things. On top of that, many system events are too fleeting for human perception, and they'll be easily missed by the user if the indicators displayed them in real time. What's needed is an abstracted representation, based on convention rather than direct correspondence: We want to blink LEDs in various human-perceivable ways.

Blinking one LED is simple too: find an unused timer in your MCU, set it to whatever frequency you wish, and put a "LED.toggle()" call in its Interrupt Service Routine. Done. But what if you have several independent indicator LEDs? Or if you want to signal a Morse S.O.S? (For the younger readers: that'll be a '... --- ...' pattern) The worst solution is to hard-code sequences, because that's a lot of work, and difficult to change later, when a client decides that an S.O.S is too stressful and wants an A-OK instead (that's '.- --- -.-', kids). A better solution is what I call, for lack of a funnier name, "Blinkenscripts".

This article is available in PDF format for easy printing

Blinkenscript Basics

For our current purposes, an LED can only be ON or OFF, so the only LED "action" is a toggle. Therefore, we can prescribe a blink pattern simply by defining an array of integers representing time periods between toggles. Remember that we're dealing with time scales appropriate for human eyes, so the unit of measurement can be coarse – say, 10ms. A single byte can thus represent up to 2.55 seconds! Assuming the initial state of the LED when starting a script is ON, an S.O.S script could look like this:

// Read the numbers as Time On, Time Off, Time On, Time Off [...]
uint8_t blinkScriptSOS[] = {5, 5, 5, 5, 5, 15, 
                            15, 5, 15, 5, 15, 15,
                            5, 5, 5, 5, 5, 0};

We'll also need some data structure to keep track of our position in this script, and a function that will be called roughly every 10ms, update the tracking information and toggle the LED as required. This function can be easily made to handle more than one script and one LED at a time, by monitoring an array of structs, each with its own pin assignment, script reference, script index etc.

But what if we want to repeat certain blinking patterns indefinitely? If we're willing to sacrifice a couple of integer values and use them as "instructions" in the script instead of time counts, we can have for instance the value 0 represent a "Stop" command, and let 255 be a "Start over" command. Alternatively, if you feel this method is a little risky, these meta-commands can be stored outside the script proper, for example in a Boolean variable called "isOneShot". Either way, once we allow looping, we have to make sure that the LED is always at the same state at the beginning, otherwise the ONs and OFFs might get swapped. Perhaps, instead of "LED.toggle()", use "LED.write(1 – positionInScript % 2)".

A Slippery Slope

If you're anything like me, your mind must be giddy right now with the possibilities presented by the concept of script meta-commands. Why stop at One-shot vs. Infinite Loop? We have the power of programming! We could create a clever catch-all script interpreter with support for arbitrary loops, nested loops, variable time units, variables, conditions… to dynamically run every conceivable blink pattern from here to infinity!

Having spent some time on this, my advice is not to bother. For the vast majority of real-world blinkenlights, the simple interpreter described above is sufficient, and obviously much easier on microcontroller resources too. However, don't dismiss the idea altogether, because if used in moderation, it can become handy under different circumstances.

Imagine your board has a buzzer for beeping sounds. That's not too far-fetched – even my washing machine plays a happy little tune before turning off. How can we script such a tune? In the script, in addition to timing data, we'll need sound frequency data. This can be raw Hertz (16-bit, for audible range), or indices for a lookup table with the frequencies of formal music notes. Assuming we use raw data, put everything in one contiguous array of "frequency - ms – frequency – ms …", and use the frequency value 0 to indicate "no sound", what will this script sound like?

// Read the numbers as frequency, time, frequency, time [...]
uint16_t myMusic[] = {392, 100, 0, 100, 392, 100, 0, 100, 392, 100, 0, 100, 
                      311, 500, 0, 1000, 
                      349, 100, 0, 100, 349, 100, 0, 100, 349, 100, 0, 100, 
                      294, 500, 0, 1000};

The script-interpreting code will process two values at a time, and use the parity of the current index to determine whether a value represents Hertz or milliseconds. We could store these two kinds of values in separate arrays, of course, although that may prove less comfortable to edit later.


That method for producing beep-tunes is almost obvious, but it's a good segue from binary-state blinkenscripts into their 21st century incarnation: breathing-LED scripts. A soft, PWM-controlled fading LED looks a lot nicer and a lot more modern than the hard, cold ON/OFFs we saw earlier.

Given the PWM duty cycle changes required for a smooth-looking fade, a script containing every single value will be prohibitively long and cumbersome, especially if our system is based on a "value line" microcontroller; so Instead of storing lots of literal values, we better store only target values, and write our script interpreter so that it will handle the in-between.

Another very useful piece of information that should be made explicit in the script is the rate of change; controlling that will allow us to create both slow and fast fades. Now, here's the trap: say we use an 8-bit resolution PWM module, and to perform a fade-in, we change the duty cycle for our LED from 0 to 255. If we use the previous timing scheme of one action every 10ms, the fade will take over 2.5 seconds. That's way too slow. Even at 1ms it may prove too slow for common scenarios. And we can't speed it up indefinitely because the microcontroller needs to do other things, too.

One of the solutions to this problem is not to go through each and every PWM value; our eyes can't even tell adjacent ones apart, especially in the higher range (Weber-Fechner law). Like the music note lookup table option that I mentioned for the tune script, we can have here a duty cycle lookup table (LUT) with maybe 64 values, strategically spaced for visual continuity and pleasantness. This requires some preparation and testing, but once it's done, we can do a satisfactory fade in as little as 64ms, without putting too much load on the microcontroller.

This advanced script will contain three parameters for each action: target brightness (as LUT index), rate of change (e.g. 1 to 16 ms per step), and time to "hold still" once the target brightness is reached. The script interpreter code for this can be short and relatively simple, but I'll leave that as an exercise for you.

There's an extra benefit to breathing-LED scripts over simple blinkenscripts. Imagine you manufactured a thousand boards, and then the client complains that the darn blue LED is too bright. Instead of replacing a thousand resistors, the script in the firmware can simply be changed to set a lower PWM target.

Even further

Interpreted programming languages in embedded systems are frowned upon, and with good cause. However, as we just saw, a far more limited kind of script interpreter, designed for very specific tasks within the system, can make our code cleaner, easier to modify, less error-prone, and faster do develop (once we have a tested interpreter build into a reusable library).

Scripting UI light patterns is a classic implementation, but with careful modifications, we can script more advanced stuff, too – from breathing LEDs, through an AT command sequence for initializing an external module, to a self-test procedure for the entire system. There's more to blinkenlights than meets the eye!

To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: