This is a tool to speed up development of animated patterns for WS2812B RGB LEDs that are supported by the Adafruit NeoPixel library for Arduino. It consists of a library that has the same interface as the NeoPixel library but that, instead of writing to actual WS2812B LEDs, renders the LEDs on screen using OpenGL. In addition, there's simple adapter code and implementations of a couple of the most common functions used in Arduino sketches (delay()
, random()
and millis()
).
This allows an Arduino sketch that uses only the Adafruit NeoPixel library and the supported functions to be compiled, linked and run on the PC, enabling a shorter development cycle and easy debugging for WS2812B blinkenlight projects.
After implementing the desired effects on the PC, the sketch can be copied unchanged to the Arduino IDE for programming one of the Atmel AVR MCUs supported by the Adafruit NeoPixel library.
Strip, Grid and Ring layouts are supported for the on-screen rendering of the LEDs. For instance a layout resembling the 24 LED Adafruit NeoPixel Ring, can be set up by selecting the Ring layout with 24 pixels. The selection is done by calling setPixelLayout()
in the sketch. Only the emulator has the method, so the preprocessor is used for skipping the call when building for an MCU. This is demonstrated in the included sketch.
This was used for developing some NeoPixel patterns for a DigiSpark ATtiny85. The DigiSpark is a convenient platform for running battery powered blinky lights but is not so convenient for development as it must be unplugged and plugged back into the USB port each time the code is updated (which can easily be hundreds of times when tweaking parameters for animated effects, etc). Hence this small app.
As Douglas Adams might have said, getting things up and running with this app is almost, but not quite, entirely unlike getting things running on an Arduino. No part of the Arduino IDE or libraries are used in this project. It's just a regular PC app written in C++ and based on FreeGLUT that has been set up in such a way that one of the .cpp
files can also be used as an Arduino sketch. The app should compile and run on any platform where FreeGLUT is available, such as Linux, Mac and Windows.
These instructions should work on Ubuntu, Mint and other Debian based systems. Tested on Linux Mint 18.3 64-bit.
$ sudo apt-get install build-essential
$ sudo apt-get install freeglut3-dev
Grab the code directly from this repository:
$ git clone <copy and paste the clone URL from the top of this page>
$ cd ws2812b-neopixel-emulator
You also need OpenGL drivers. These are specific to your graphics card and you probably already have them. If they appear to be missing, it's worth a try to set up the Mesa drivers:
$ sudo apt-get install mesa-common-dev (*install these only if you need them*)
$ mkdir cmake-build
$ cd cmake-build
$ cmake ..
$ make
$ ../bin/emulator
-
Watch the blinkies on screen. Then exit by closing the window or hitting Ctrl-C in the shell.
-
Tweak arduino_sketch.cpp to make your own animations.
-
If working with multiple sketches, set the filename of the one to build in the
SKETCH
variable, close to the top inCMakeLists.txt
. -
Debug with a regular debugger or use
printf()
statements (the output appears in the shell). -
When done, copy paste the
arduino_sketch.cpp
code into the Arduino IDE and write it to your device.
-
The
setBrightness()
call is ignored by the emulator and thegetBrightness()
call always returns the maximum brightness value of 255. As described on the NeoPixel library home page,setBrightness()
is "lossy" and intended for use only insetup()
, not in animations. Since physical LEDs are brighter than a computer monitor even on low brightness settings, the LEDs are most accurately represented by rendering them with maximum brightness. By having the emulator ignoresetBrightness()
, the call can be used in the sketch to set the brightness of the physical LEDs without affecting the accuracy of the representation in the emulator and without the developer having to adjust the value when switching between the emulator and physical LEDs. -
delay()
should always be called in the animation loop, typically after each call topixels.show()
. A delay of 20ms corresponds to a maximum refresh rate of 50Hz (1 / 0.02) and is typically a good value to start with. Refreshing the LEDs more often (delay value lower than 20ms) will probably not cause animations to look any smoother. To speed up the animation it's probably better to increase the step size than the refresh rate. -
The emulator does not take into account that code runs more slowly on the device than on the PC. On the PC, virtually all the time will be spent in
delay()
calls but on the device, a non-significant portion of time is typically spent executing the program. The difference causes the sketch to run faster on the PC. The possible difference becomes larger the shorter thedelay()
is.
If the sketch works in the emulator but not on the device, some possible reasons are:
-
The NEOPIXEL_PIN value is ignored by the emulator but determines which pin on the MCU is used for communicating with the physical LEDs. It must be set to the pin to which the LEDs are connected.
-
The supported MCUs are memory limited, some severely so. For instance, the Atmel ATtiny85 has only 512 bytes of RAM. When controlling 60 LEDs with the ATtiny85 (with the LED RGB values alone taking up 180 bytes (3 * 60)), the included sketches use almost all available memory. Adding more functionality without removing something will typically cause them to run out of memory and crash on the device. There's no practical limit to how much memory the sketch can use when running on the PC.
-
If the colors are different between emulator and device, try changing the color channel ordering in the NeoPixel driver object instantiation. Possibilities are NEO_RGB, NEO_GRB and NEO_BRG.
-
When compiling for the PC, the size of
int
is 32-bit (on both 32- and 64-bit CPUs and OSes). When compiling for the Atmel AVR processor architecture, it's 16-bit. This causesint
types and expressions where types are implicitly promoted toint
to be much more likely to overflow on the MCU. For instance,uint8_t a = 100; uint32_t b = a * 1000;
will give the correct result ofb = 100,000
on the PC but a truncated result ofb = 31,072
on the MCU, even though a 32-bitint
is used for holding the result on both platforms. This happens because, before taking part in an arithmetic operation, both operands must be of the same size and the result is calculated using that size. If the operands are not of the same size, they're promoted to the size of the largest operand or toint
if the largest operand is smaller thanint
. In this example, the literal1000
is implicitlyint
, soa
is promoted toint
. The multiplication is then performed using the resolution ofint
. On the PC, the result of the operation fits in a 32-bitint
and is then directly stored inb
. On the MCU, the result does not fit in a 16-bitint
, causing an overflow. The truncated result is then expanded to 32-bits and stored inb
. The fix for this is to make sure that at least one of the operands is the same size or larger than the size required for holding the result. In the example, this can be done by specifying1000
as a 32-bitint
by appendingUL
to the number or by casting one of the operands to 32-bit.The compiler breaks expressions involving multiple operands into subexpressions and the rules above apply to each subexpression. For instance,
uint32_t a = 50; uint8_t b = 100; uint32_t c = a + b * 1000;
will, on a machine with 16-bitint
, overflow in theb * 1000
subexpression even thougha
is a 32-bitint
. First,b
is promoted toint
and the multiplication is performed withint
resolution, yielding a truncatedint
result. Then the truncated result is promoted to 32-bit to matcha
, then added toa
, yielding a 32-bit result which is stored directly inc
.
The main difference between an Arduino sketch and a regular .cpp
file is that sketches have automatically generated prototypes. We use makeheaders for this task. As BSD licensed code can be included in MIT licensed projects, makeheaders.c
is included directly in this project.