I can't help myself with the blinkenlights_

🇺🇦 Resources to help support the people of Ukraine. 🇺🇦
January 12, 2022 @12:20

Background

Starting just before the holidays I found myself back on the kick of working with LEDs. About 7 years ago I started making a little microcontroller based controller to drive LED strips and generate some interesting effects. I've used this in a bunch of situations with WS2812, WS2811 and APA102C based LED strips (Adafruit tends to call these NeoPixel or DotStars) over the years and recently decided to reorganize the code into more generic building blocks. In doing so I moved a bunch of the heavy lifting to a little library which allowed me to step back and do some thinking about how I might want to build up larger effects in the future. Since I do all my microcontroller development in C the natural fit seemed to be to create a sequence of actions as an array of structs. The structs could contain some conditions and a function pointer to be executed. It is also possible to have the condition be a function pointer, enabling the triggerable events so the controller can respond to stimulus or use the random number generator to change up patterns. With this in mind I designed the sequencer. It takes two arrays of structs, one for the sequence and one for the optional events. It turns out that not only was this easy to implement, but there wasn't a tremendous amount of special work needed to support the split memory architecture of the AVR platform. At the time of writing the whole file is right around 100 lines of code.

The Sequencer

I start out defining the types for the function pointers so we know what arguments to expect, and a type for a structure to hold the state information in RAM (the sequence data is stored in program memory).

typedef bool (* cond_func_t)(void);
typedef uint8_t (* seq_func_t)(void);

struct sequence_t {
        uint8_t cnt;
        seq_func_t func;
        bool is_event;
        size_t pos;
};
struct sequence_t seq = { 0, NULL, false, 0 };

Next we have the actual sequencer. The first thing to do is to check to see if we are running one of the events or not. If we are then we run the event. The convention is that all the display functions all return a byte indicating either a delay in milliseconds until the next time they need to be run or a 0 indicating they are done. We store this value for use later.

int
ledfx_sequencer_run(
        const LEDFX_SEQUENCE_MEMBER sequence[],
        const LEDFX_EVENT_MEMBER events[],
        size_t len,
        size_t events_len
) {
        uint8_t ret;

        if (seq.is_event) {
                ret = seq.func();

        } else {

If we aren't running an event then we need to run the current item in the sequence. Here we have to remember that if we are running on the AVR that we need to fetch the actual pointer from program memory and not RAM. This means using the pgm_read_* helpers. Because I test effects using a GUI app built on amd64 this is wrapped in a ifdef.

#ifdef __AVR_ARCH__
                seq_func_t func = pgm_read_ptr(&sequence[seq.pos].func);
                ret = func();
#else
                ret = sequence[seq.pos].func();
#endif
        }

If the display function (either an event triggered or normal sequence event) completes we do some housekeeping prior to returning to the caller. In the case of an event we update our internal state so we know to return to the normal sequence next time we are called. In the case that we've just run a sequence item we update a counter and check to see if we have run this entry enough times, if we have then we reset our counter and move on to the next entry in the sequence. This is also the point we check all the event entries to see if any of them want to interrupt. This is important as there is not enough memory in these systems to save the state of the in-process sequence to be restored after the event, meaning all effects would have to be designed knowing that they something else may change the state of the display memory out from under them.

        if (ret == 0) {
                if (seq.is_event) {
                        seq.func = NULL;
                        seq.is_event = false;
                        return -1;
                }

                seq.cnt++;

#ifdef __AVR_ARCH__
                if ((pgm_read_byte(&sequence[seq.pos].times) - seq.cnt) == 0) {
#else
                if ((sequence[seq.pos].times - seq.cnt) == 0) {
#endif
                        seq.cnt = 0;

                        if (++seq.pos == len) {
                                seq.pos = 0;
                                test_events(events, events_len);
                                return -1;
                        }
                }
        }

Finally we return to the caller the number of milliseconds to wait before calling us again.

        return ret;
}

This means that an effect can be written as simply as...

const LEDFX_SEQUENCE_MEMBER sequence[] PROGMEM = {
        { .times = 1, .func = fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = block_empty },
        { .times = 1, .func = block_fill },
        { .times = E_REPEAT * 12, .func = rotate },
        { .times = 1, .func = empty }
};
/**
 * Run the effect.  We do not return to the loop in main.
 */
int
ledfx_effect_christmas(void)
{

        uint8_t ret = ledfx_sequencer_run(
                sequence,
                NULL,
                sizeof(sequence) / sizeof(sequence[0]),
                0
        );

        if (ret > 0) {
                return ret;
        }

        return F_DELAY;
}

Well, more like this, but you get the point.

Conclusion

I've built a number of effects with this framework now and the christmas effect ran for the entire month of December on an ATTiny85 connected to a 100 LED WS2811 string wrapped around a tree in my front yard. It worked quite nicely and looked rather fetching as well.

I haven't seen any Arduino-style projects that let you build effects quite as simply as this does, hopefully this gives you some ideas of the things you can do with more advanced language constructs.

Subscribe via RSS. Send me a comment.