Effects Advanced - AlbertGBarber/PixelSpork GitHub Wiki

Jump to

Multiple Effects at the Same Time:

All effects have a showNow variable. If false, then the effect will still update, but it will skip displaying the physical pixels (the color data is still written to FastLED's led color buffer array, but it does not call FastLED show()). Writing out pixel data takes time, particularly when you have a large number of pixels. If you are using multiple simultaneous effects, disabling showNow on all but one of them can minimize how often you write out pixel data. Note that this assumes your effects are running on different segment sets, if you want multiple effects on the same segment set, see here. Likewise, MAKE SURE THAT YOUR EFFECT WITH "showNow" SET "true" IS THE LAST EFFECT TO BE UPDATED IN A CYCLE, otherwise you'll display the color data before all your effects have updated!

You need to be careful disabling showNow with multiple simultaneous effects that have different update rates. It is not as simple as just setting the fastest effect to show only, because this can lead to your slower effects actually updating too slowly. Consider two effects with update rates of 60 and 80ms. If you set only the 60ms effect to show, the 80ms effect will actually wait 120ms before the first frame is filled in!

To fix this, you need to set an update rate that hits both effects' update times. Unfortunately, the only way I can see to do this is by setting the effects' rates to be multiples of each other, ie set the fast effect to 40ms, or the slow to 120ms. The alternative is you just have to accept a lower overall frame rate as each effect updates independently. You can also experiment with the JustShow utility, which writes to your strip at a fixed rate.

Parallel Output:

This is more of a general note, rather than specific to effects, but it seemed to fit best here.

FastLED supports using multiple strips across multiple pins. There's a number of reasons why you might do this, such as mixing different strip types, or ease of assembly, but the number one reason is performance. Writing to an LED takes time, and Fast LED has to write to all your LEDs whenever you update, so longer strips means more time writing (~30ms for 1000 LEDs!). Ideally, by spreading your LED strips across multiple pins, you can update them all at once in parallel. FastLED supports this feature, but it seems to be micro-controller dependent. You can read more about it here, but you may need to do some Googling for your Micro-controller -- the FastLED wiki is not always up-to-date?

Pixel Spork should support FastLED's parallel output, but only the "one array, many strips" method, which aggregates all your LED strips into one continuous array. You then tell FastLED where each strip starts and what pin it's on during setup. You can see examples of this here. Note that currently, I've not tested parallel output, but it should work in principal.

Managing Dynamic Memory and Fragmentation:

Dynamic Memory:

Whenever you create a new variable, effect, palette, etc, some of your micro-controller's memory needs to be allocated to store it. For most things, the memory is allocated at compile time. The Arduino IDE will tell you how much programming (Flash) and dynamic memory (SRAM) you're using after compilation. However, sometimes you don't know the size of a variable at compile time, or you need to be able to change its size during runtime. Consider an effect where we turn a set of LEDs on and off one at a time (see the Fairy Lights effect). To track the twinkling LED locations, we us an array. When we create the effect, we tell it how many LEDs we want to light up, so the compiler knows how large the location array will be and how much memory to reserve for it.

But what if we want to change the number of twinkling LEDs?

To have a variable number of twinkling LEDs, we also need a variable length location array. This means that we have to be able to re-size the array on the fly, and, consequently, change how much dynamic memory it's taking up. Your micro-controller only has a limited amount of dynamic memory, and there are no safeguards about running out of it during runtime. If we ask for too many LEDs, our program may just crash, or it may over-write existing memory leading to unexpected behavior. The compiler will not warn us about this -- it only checks the memory usage at compile time, it does not try to predict how much memory we might need in total. If your code seems to be randomly crashing, or doing weird stuff, a memory issue is usually the culprit, be it related to space, fragmentation, or miss-handling of pointers.

You can read more about micro-controller memory here.

How do I Know if a Variable is Dynamically Sized (or Controls Something that is)?

If an effect uses a dynamically sized array or object, it should be noted on the effect's wiki page, usually after the effect's description (See Fairy Lights for an example of this). Also, in general, any patterns created by the effect (usually using a setPaletteAsPattern() function) will by dynamic. Overall, if an effect requires you to use a function to change a setting, that setting probably relates to something that's dynamically created. If you're ever in doubt, you can always search the effect's code for malloc, which is used to dynamically allocate memory.

How Can We Manage Dynamic Memory?

  1. Don't change any dynamically sized effect/utility variables. This basically side steps the issue by keeping everything static, so the memory usage reported by the compiler should be close to the runtime usage.

  2. Compile with dynamically sized variables set to their maximum. Effects always create their dynamic objects when they're initialized, with the objects memory being reported by the compiler, so by starting an effect's dynamic objects at their maximum size, you can confirm that you have enough space for them. For example, if I know that I won't have more than 20 twinkling LEDs, I can initialize my twinkle effect with the number of twinkles set to 20. This will reserve the memory for the 20 twinkles at compile time. I can later lower the number of twinkles without worrying about memory usage. This also has the added benefit of reducing memory fragmentation (see below). Note that you should always cap any dynamic size related variables to a maximum value.

  3. If you try (2.) above, but the compiler reports you don't have enough space (or you are close to the max) you'll need to get creative by:

    • Upgrading to a micro-controller with more SRAM. Sometimes the most boring answer is the easiest. I highly recommend swapping to a 32bit micro-controller over a typical 8bit Arduino. At the time of writing (2023), the Wemos D1 Mini remains my pick for most projects, being small, very cheap, but with loads of memory (4MB SRAM vs 8KB for an Arduino Nano). Its pins are even 5V tolerant (for inputs, except A0), so moving from a 5V Arduino is relativity painless (if you need the more analog pins, you'll need an IO multiplexer). You could also consider an ESP32 or Teensy board, just be sure that it's compatible with FastLED.

    • Limiting memory usage by only using what you need when you need it. This only applies if you have multiple effects. If you only have a single effect, you're stuck with the options above. In most of my projects, I have a number of effects that I'm cycling through. I may not have enough memory to compile all my effects at their maximum size at the same time, but since I'm not using all the effects at once, I can reuse the same pieces of memory for each effect one at a time. The easiest way to do this is by using temporary effects.

    • Directly managing effects' memory. If you can't or don't want to use a temporary effect, you can directly manage the dynamic portion of an effect by releasing it when you don't need it. THIS IS A DANGEROUS TECHNIQUE THAT REQUIRES UNDERSTANDING THE EFFECT'S CODE. You should only do this if you fully understand the effect and know exactly what you are doing! To release the dynamic memory, you must free() effect's dynamic memory pointers. You can easily find what you need to free() by checking the effect's destructor (~). WARNING, before freeing palette and pattern pointers, double check that the effect has a function to re-create them. Most effects create dynamic palettes in their constructors, so you shouldn't free them (they should be small anyway). DO NOT try updating the effect after freeing the objects, this will almost certainly cause a crash.

      When you want to use the effect again, you'll need to re-create the dynamic memory objects. Each effect should have a function that will build the objects, usually something like setNum<X>() where is the number of twinkles, a length, etc. Before you call the function be sure to set alwaysResizeObj_PS to true to force the object to be created (see fragmentation below for more info). Set it back to false once you're done.

Fragmentation:

Most micro-controllers' memory behaves like a big array. When an effect creates an object dynamically it asks the micro-controller to reserve a section of the memory array for the object. The micro-controller then searches for a section of the array that is large enough, and reports the start of the section to the effect. As time goes on, more of the memory array will be filled in, with new objects being slotted into whatever space is available. This is fine until we start de-allocating memory, which frees blocks of the memory array. If a new object is allocated to a freed section, it may not fill the section completely, leaving a gap or hole. The gap may be too small to practically fit anything, and over time, as we allocate and free more objects, the memory can become full of these holes. It becomes fragmented. Suddenly, the micro-controller is asked to create a new object (possibly an object it had been able to create during a previous loop() cycle) but now, because of the holes, there isn't a large enough section of free space. At this point the program crashes.

You can read more about memory fragmentation here.

Unfortunately, a lot of effects require some amount of dynamic allocation. To help avoid fragmentation, by default in Pixel Spork, an effect's objects are only re-allocated if they need more memory. For example, lets say I start an effect with 20 twinkles (the effect creates an array to track their locations). I later increase the number of twinkles to 30, this will cause the array to be re-sized and re-allocated up to 30. Later still, I decrease the twinkles to 10. Internally, the effect tracks the decrease in twinkles, but because the twinkle array is already large enough it is not re-sized. It is not shrunk to 10 -- it remains at size 30. This helps avoid memory fragmentation by re-allocating memory only when absolutely necessary. All effects setup their dynamic objects when they are created, so you should always try to initialize your effects with any dynamic variables at their maximum (as determined by you, not their numerical limit!).

Limited re-sizing is controlled by alwaysResizeObj_PS, a global Pixel Spork variable. It is default to false, but when set true, any dynamically created objects will always be re-sized and re-allocated. Note that this applies to any and all dynamic objects across the whole library. You can change alwaysResizeObj_PS on the fly, so you can toggle it, setup an object, and then toggle it back.

Note that overall, you to use more memory than your program actually needs at any given time. I chose this way because catching memory fragmentation is extremely tedious, causing random crashes, possibly hours after the program started. At least with memory, the compiler reports the initial memory usage, so you can get a feel for how much space you're using.

If you are having memory space issues, see point 3. of the dynamic memory section above.