In this article, I will discuss how to write custom patterns for your snowflake decoration. This is a rather advanced topic, and there are a number of prerequisites that are required and listed below. Nevertheless, it is rewarding to you see your very own pattern displayed on the decoration.
- You must solder a 5-pin programming header to all snowflake boards you wish to reprogram, which is not included in the kit from Pimoroni.
- An SWD programmer is required to write the firmware to the microcontroller.
- Established C++ language knowledge.
If you are still with me at this point 😆, please read “Programming the Snowflake Decoration” if you have not already done so. The article describes the basic setup of the toolchain required for reprogramming.
First, Some Basics
Blinking a single LED is easy — displaying a smooth animation is not.
In the firmware, I use a number of techniques to optimize the display:
- Double-buffering
- Frame-synchronization
- Non-linear PWM
- Frame-based animation system
- Fixed point math
Double-Buffering
You want to implement double-buffering to prevent incomplete data from being displayed. This is a very simple technique, and is most effective with frame synchronization.
Check the Display.hpp
and Display.cpp
files from the snowflake firmware. This code implements software PWM using an interrupt to control the brightness of all 19 LEDs. The code reads a value from a buffer with 19 bytes. Each byte represents the brightness-level for a single LED.
Without double-buffering, you will experience many undesired side effects.
Imagine this situation: All LEDs are off and you are about to turn them all on to full brightness. You start to write the full brightness values to the buffer, but your code is interrupted by the display interrupt. Only the first ten values were written to the full brightness, and the nine remaining are still zero.
The first ten LEDs are turned on by the display interrupt handler code. After the interrupt, the processor returns to your code and writes the remaining nine values. For a short period, there was an incomplete frame visible on the decoration.
If this happens once every minute, it is likely that nobody will notice it – but if this happens 20-30 times per second, the displayed animation will look rather strange. In certain situations, if the animation code has even a slightly different frequency than the display code, you will see noticeable side effects on the display.
Double-buffering solves this problem.
Double-buffering helps solve this problem. You can simply use two buffers. One buffer is displayed and one is prepared for display. If all writing for one buffer is complete, the display will then switch to the new buffer.
Frame-Synchronization
We implement frame-synchronization to switch between the two buffers at a defined point in time.

This illustration displays a program that switches an LED between 50% and 90% brightness. If the switching is not synchronized, you get a mix between the two PWM values.
At first glance, you may think this is rather useful. It will add intermediate values to the animation, which smoothes quick changes. If you just slowly blend between brightness levels, this can be the case. Yet, if you try to create sparkling effects, these intermediate values will kill the effect.
Non-linear PWM
Thanks to rhodopsin in the rods of the retina in your eyes, your brain can detect very small amounts of light. To optimize night vision, the perception of brightness levels is not linear, but somewhat logarithmic.
If you drive an LED using PWM and measure the emitted light in lumens, you will obtain linear results. An LED emitting 500lm when fully powered will emit 250lm with a 50% PWM ratio.
However, your perception of this brightness is very different. You will observe a large difference between 1% and 2%, but it will be difficult to differentiate between 90% and 91%.
If you look at this problem superficially, the solution is simple. You can simply create a conversion table to adapt to the logarithmic perception.
Let’s do some 8-bit math: You would like to have 64 brightness levels, from zero as black to 64 as fully on. To prevent any visible flickering, you run the software PWM at a high frequency, which supports 64 PWM levels.
0: 0.0000 0 1: 0.0002 0 2: 0.0020 0 3: 0.0066 0 4: 0.0156 0 5: 0.0305 0 6: 0.0527 0 7: 0.0837 0 8: 0.1250 0 9: 0.1780 0 10: 0.2441 0 11: 0.3250 0 12: 0.4219 0 13: 0.5364 1 ... 60: 52.7344 53 61: 55.4153 55 62: 58.1855 58 63: 61.0466 61 64: 64.0000 64
This table was generated using the following Python script:
import math level_count = 64 for level in range(level_count + 1): pwm = math.pow(level / level_count, 3) * level_count print(f'{level:4d}: {pwm:8.3f} {round(pwm):3d}')
Adjusting the brightness perception requires a formula such as:
As you can see, this leads to a long series of zero values at the beginning of the table. You would need at least a 16bit resolution for your LED PWM implementation to get the required precision.
0: 0x00000 1: 0x00001 2: 0x00002 3: 0x00007 4: 0x00010 ... 60: 0x0d2f0 61: 0x0dda9 62: 0x0e8be 63: 0x0f430 64: 0x10000
The Performance Problem of Linear PWM
For linear PWM, you set a timer to a regular interval. An interrupt is triggered in the defined interval which controls the LED outputs.
This illustration shows a very simplified system with only 9 different brightness levels. The red bars at the top of the timeline represents the executed interrupt code.
This is the more straightforward way to implement PWM on a microcontroller if there is no dedicated PWM peripheral for all outputs. In the case of DMA support, the interrupt code is replaced by a large bit-table — but the core principle remains the same.
To avoid perceptible flickering, the PWM frequency should be greater than 400Hz. If you want to provide 256 brightness levels at this frequency, the interrupt must be called 102’400 times per second, at 102.4kHz.
Now, consider a microcontroller executing ten instructions per microsecond (e.g. running at 10MHz). At 102.4kHz, it could only execute approximate 90 instructions in the interrupt – and this would use the full capacity of the processor without any room for other code.
To solve the performance problem, you can either lower the overall frequency of 400Hz, and risk perceptible flickering, or lower the number of levels with poor stepping in the dim brightness levels.
Pros
- Simple implementation.
- Not very time-sensitive.
Cons
- Brightness adaptation required.
- More brightness levels required due to the adaptation.
- Many brightness levels are not used because of the adaptation.
- Uses a large amount of CPU time for the interrupt if not implemented via DMA.
Non-Linear PWM
With non-linear PWM, you utilize a precise timer for the interrupt dynamically.
At the start of each interrupt, a new dynamic timing to the next interrupt is set, which creates a logarithmic timing behaviour.
As you can see, at very low levels, the timing between the interrupts would be too short without overlapping. This is solved by merging the first levels into one initial interrupt block.
Pros
- Dim brightness levels are possible.
- No adaptation of levels required.
- Fewer brightness levels required.
- Less interrupt code executed.
Cons
- Complex implementation.
- Time-sensitive interrupt.
Frame-Based Animation System
Many different patterns (called Scene
in the code) are integrated in the firmware. In Random Mode, one pattern will blend smoothly to another. This is possible using a highly abstract, frame-based animation system.
Code generating only an effect (scene, pattern) must provide a sequence of frames with the new states for all 19 LEDs. Each call has to provide the next frame in the sequence, just like a video stream.
This makes the scene code completely independent from the rest of the system, meaning it is free from any dependencies. Effects can then be implemented in a very simple, abstract way.
The animation system in the snowflake decoration runs at ~32 frames per second, allowing for very smooth effects.
Fixed Point Math
Cortex-M0+ chips have no hardware support for floating-point numbers. For best performance, you must stick with integer calculations. However, there is a good compromise between these two worlds, called fixed-point math.
The class Fixed16
is located in my HAL layer, and is also contained in the snowflake firmware. It implements a 32-bit fixed-point value with a 16-bit fraction part. You can use it like any floating-point value, but calculations are completed nearly as fast as integer math.
Using this class, we can represent brightness levels as values between 0.0
and 1.0
, where 0.0
is off and 1.0
is full-on. This has many benefits:
- It is an abstract system in which you do not need to know the actual number of brightness levels of the system.
- Because of this abstraction, any effect can easily be ported to another implementation with more or fewer brightness levels.
- Calculations to mix and blend frames are simple and precise.
- Interpolation between values, used for any smooth transition, is straightforward.
Where to Start
Be sure to download the latest version of the firmware, 1.2.2. It contains a template scene for a quick start.
The files src/scene/User.hpp
and src/scene/User.cpp
contain the User
scene, which is already integrated into the firmware. Go ahead and modify the code in these files for your first experiments.
The Structure of a Scene
An effect or pattern is called a scene in the firmware. A scene is no class, but rather a well-defined interface in a namespace.
namespace scene { namespace User { /// The number of frames for this scene /// const FrameIndex cFrameCount = 1216; /// The function to initialize this scene. /// void initialize(SceneData *data, uint8_t entropy); /// The function to get a frame from this scene. /// Frame getFrame(SceneData *data, FrameIndex frameIndex); } }
Each scene is found in the namespace scene
and defines its own namespace with the name of the scene. In this case, the second namespace is called User
.
The interface is defined in the header file User.hpp
and must consist of the following three elements:
- A const variable
cFrameCount
with the number of frames in the scene. - The
initialize
function, which is called once before a scene is displayed. - The
getFrame
function, which is called ~32 times per second to obtain the next frame of the scene.
namespace scene { namespace User { void initialize(SceneData*, uint8_t) { // empty } Frame getFrame(SceneData*, FrameIndex frameIndex) { Frame frame; for (uint8_t i = 0; i < Display::cLedCount; ++i) { const auto shiftedValue = static_cast<uint8_t>((frameIndex + i) % Display::cLedCount); frame.pixelValue[i] = PixelValue::normalFromRange<uint8_t>(0, Display::cLedCount-1, shiftedValue); } return frame; } } }
Here, you see a minimal example implementation in User.cpp
. In this example, the initialize
function is not used and remains empty.
In the getFrame
function, a black frame is created with the line Frame frame;
. Next, the for
loop assigns new values to this frame which is returned in the last line.
The parameter frameIndex
of the getFrame
function is incremented for each call. In the current example code, from zero to 1215
, then the counter will start over at zero. The number of counted frame indexes is modified using the cFrameCount
constant in the header file.
Enable Testing Mode
Open the file Configuration.hpp
. You will see that it contains various configuration variables for the firmware. Now, search for the following line:
const bool cStartWithUserScene = false;
Set the value to true
:
const bool cStartWithUserScene = true;
If you compile the firmware and run it on the snowflake board, it will start directly with the User
scene.
The Frame
Remove the example code from the getFrame
function and replace it with this code:
Frame getFrame(SceneData*, FrameIndex frameIndex) { Frame frame; frame.pixelValue[0] = PixelValue(0.1f); frame.pixelValue[5] = PixelValue(0.5f); frame.pixelValue[9] = PixelValue(1.0f); return frame; }
The LED indexes in each frame are arranged as shown in the following figure:
By assigning a PixelValue
to individual frame indices, you set the brightness of these LEDs. A pixel value lies within the range of 0.0f
and 1.0f
. Here, 0.0f
is black and 1.0f
is maximum brightness.
f
suffix to define float
literals.PixelValue(…)
. This will convert the floating point number into a fixed point number at the compile time. Add an Index-Based Animation
The simplest way to create an animation is by using the frame index, on which you will base the animation:
Frame getFrame(SceneData*, FrameIndex frameIndex) { Frame frame; frame.pixelValue[frameIndex % Display::cLedCount] = PixelValue(0.75f); return frame; }
Conclusion
Implementing custom animations is relatively straightforward thanks to the extensive animation framework that already exists in the firmware. You can create numerous interesting effects, even with just a few lines of code.
Go ahead and start experimenting with simple animations and take a look at the existing scenes, which use slightly different animation techniques. In the next part of this series, I will explain some of these in more detail.
If you have any questions, missed any information, or simply want to provide feedback, feel free to comment below or reach out to us on Twitter!
More Posts

How to Wire the Snowflake Decoration

Perfect Snowflake Panels from Eurocircuits

Snowflake Configuration

Programming the Snowflake Decoration

Snowflake Assembly Video

Thanks for this, lots of very useful information.