Lucky Resistor
Menu
  • Home
  • Learn
    • Learn C++
    • Product Photography for Electronics
      • Required Equipment and Software
    • Soldering for Show
  • Projects
  • Libraries
  • Applications
  • Shop
  • About
    • About Me
    • Contact
    • Stay Informed
  •  
Menu

How to Write Custom Snowflake Patterns

Posted on 2019-12-072019-12-10 by Lucky Resistor

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
    • Double-Buffering
    • Frame-Synchronization
    • Non-linear PWM
      • The Performance Problem of Linear PWM
      • Non-Linear PWM
    • Frame-Based Animation System
    • Fixed Point Math
  • Where to Start
    • The Structure of a Scene
    • Enable Testing Mode
  • The Frame
  • Add an Index-Based Animation
  • Conclusion
  • More Posts

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:

\text{pwm} = \frac{\text{level}}{\text{num of levels}}^3 * \text{num of levels}

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:

  1. A const variable cFrameCount with the number of frames in the scene.
  2. The initialize function, which is called once before a scene is displayed.
  3. 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.

Note: It is important to always add the f suffix to define float literals.
Note: Always wrap the floating point numbers with 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

Perfect Snowflake Panels from Eurocircuits

Perfect Snowflake Panels from Eurocircuits

Eurocircuits delivered some perfect panels for the snow flake board. Each panel has five snow flake boards on it. This was an impressive good job from from Eurocircuits. There are a quite number of challenges ...
Read More
Perfect Prototype Boards from Eurocircuits

Perfect Prototype Boards from Eurocircuits

Today prototype boards from Eurocircuits arrived. The quality of this boards is outstanding. I do not know any other board house which delivers this incredible quality of boards. They really thrive for perfection ...
Read More
Snowflake Decoration Available on the Pimoroni Store

Snowflake Decoration Available on the Pimoroni Store

We have some great news about my Snowflake Decoration: Starting today, you can buy an assembled version of the project from Pimoroni that can be shipped worldwide. Pimoroni did an excellent job on this project ...
Read More
Recreating the Human Perception of the Snowflake Sparkling Effect

Recreating the Human Perception of the Snowflake Sparkling Effect

The sparkling lights on the real snowflake decoration are stunning and beautiful. Yet, it seems to be impossible to capture the effect with a video. After some experiments, I created a new video with a ...
Read More
Programming the Snowflake Decoration

Programming the Snowflake Decoration

In this brief article, I will discuss the requirements to flash a custom firmware to your Pimoroni snowflake decoration. I assume you have some programming knowledge and have previously worked with an Arduino Uno or ...
Read More
How to Wire the Snowflake Decoration

How to Wire the Snowflake Decoration

If you own an original snowflake kit or produced boards from the sources, you have to wire them by yourself. The pre-assembled kit from Pimoroni comes with some nice flex-cables, but you may want to ...
Read More

1 thought on “How to Write Custom Snowflake Patterns”

  1. Andy says:
    2019-12-08 at 13:34

    Thanks for this, lots of very useful information.

    Reply

Leave a Reply Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Stay Updated

Join me on Mastodon!

Top Posts & Pages

  • How and Why to use Namespaces
  • Storage Boxes System for 3D Print
  • Use Enum with More Class!
  • Circle Pattern Generator
  • Real Time Counter and Integer Overflow
  • Circle Pattern Generator
  • Logic Gates Puzzles
  • C++ Templates for Embedded Code
  • C++ Templates for Embedded Code (Part 2)
  • Logic Gates Puzzle 101

Latest Posts

  • The Importance of Wall Profiles in 3D Printing2023-02-12
  • The Hinges and its Secrets for Perfect PETG Print2023-02-07
  • Better Bridging with Slicer Guides2023-02-04
  • Stronger 3D Printed Parts with Vertical Perimeter Linking2023-02-02
  • Logic Gates Puzzle 1012023-02-02
  • Candlelight Emulation – Complexity with Layering2023-02-01
  • Three Ways to Integrate LED Light Into the Modular Lantern2023-01-29
  • The 3D Printed Modular Lantern2023-01-17

Categories

  • 3D Printing
  • Build
  • Common
  • Fail
  • Fun
  • Learn
  • Projects
  • Puzzle
  • Recommendations
  • Request for Comments
  • Review
  • Software
Copyright (c)2022 by Lucky Resistor. All rights reserved.
 

Loading Comments...