NeoPXL8HDR
NeoPXL8 addresses the NeoPixel bandwidth issue, improving frame rates for large LED installations and freeing up more processor time for creating ânext levelâ animation. With more potent microcontrollers now in the form of the RP2040 and ESP32-S3, suddenly we can go next-next-level. BEAST MODE!
The NeoPXL8 library includes an extra class called NeoPXL8HDRÂ (âhigh dynamic rangeâ). You donât need to install an additional library for this, it tags along with the NeoPXL8 installation.
NeoPXL8HDR works a lot like NeoPXL8âin fact the same Arduino sketches can be used as a starting pointâbut then brings bonus cake:
- Temporal dithering provides more intermediate shadesâthousands of brightness levels rather than 256.
- 16-bit color components for nuanced animation; each pixel now allows colors in 48-bit (RGB) or 64-bit (RGBW) formats.
- Gamma correction provides perceptually-linear brightness ramps from the pixelsâitâs built into the library, you no longer need this in your code.
- Frame-to-frame blending creates smooth transitions even when animation frame rates are low.
NeoPXL8HDR works best with a Feather RP2040 or ESP32-S3 (or other RP2040 or ESP32-S3 board, with suitable level shifting). With two processor cores, one can be dedicated fully to NeoPXL8HDR work. Though it can run on an M4 board, the results are less satisfying (CPU time must be split between user code and the library). Other chips, even the M0 that works so well with âclassicâ NeoPXL8, are right out, the HDR additions are just too demanding.
Tempering Expectations
NeoPixels still use 8-bit brightness levels, this doesnât magically make them 16-bit. The illusion is done with temporal dithering, quickly alternating the LEDs between two different levels. The effect is noticeable, and one must ride a fine line between just noticeable and distracting.
Additionally, even the dithered output isnât fully 16-bit. It produces 12 bits by default, and with shorter pixel runs you might manage 13 bits. This is configurable so you can find a balance between precision and distraction. The important idea is that to your code everything appears 16-bit. The last-step dither reduction is fully handled by the library and does not complicate your task.
If your project doesnât require HDR features, use the much-loved NeoPXL8 class! The HDR class consumes inordinate RAM and resources, and it will not automatically make anything look better; one must specifically code to its strengths. Scrolling text? NeoPXL8. Super smooth plasma flame effects? NeoPXL8HDR.
NeoPXL8HDR relies on frequent refreshes to all the NeoPixel strips. Thereâs an associated bandwidth bottleneck to this, ultimately limiting how many pixels can be controlled while maintaining a sufficient refresh rate. This is subjective and there is no hard limitâŠbut around 1,000 pixels (split 8 ways, and give or take a bit) is a sensible ballpark guideline.
Basic Use
If youâve never used either class, read through the NeoPXL8 Arduino Library page first and play with the strandtest example. Get something working on actual hardware and familiarize yourself the concepts and constraints there.
Then, graduating to HDR, letâs look at the strandtest_hdr example included with the NeoPXL8 library, comparing it against the simpler non-HDRÂ strandtest. Open both side-by side in the Arduino IDE; weâll just highlight certain sections here.
The two start out very similar. #include the header file, declare a list of pins. Even the constructor accepts the same arguments, it just has a different name for the HDR vs. traditional NeoPXL8 varieties.
Adafruit_NeoPXL8HDR leds(NUM_LED, pins, NEO_GRB);
Immediately things get strange in the HDR code, where you can see it compiles different code for RP2040 vs. ESP32-S3 vs. M4 (SAMD51) microcontrollers.
A new class functionârefresh()âis introduced. In NeoPixel or NeoPXL8 code, you call the show() function to transmit new color data to the LEDs, done. In NeoPXL8HDR, show() hands off new color data to the library, but doesnât actually update the LEDs. Thatâs now the task of refresh(), which must be called frequently to perform temporal dithering and all the other niceties.
On RP2040, we put a refresh() call inside the loop1() function, which runs again and again on the chipâs second core, as fast as it can go. This requires installing the Earle Philhower RP2040 package in the Arduino Boards Manager, if you havenât already.
On ESP32-S3, refresh() is called in a tight loop in the loop0() function, which the setup() code pins as a separate task on core 0. Arduino codeâŠthe animation logic in this caseâŠalways runs on core 1.
On the M4 board, itâs necessary to set up a timer interrupt (via the Adafruit_ZeroTimer library) to periodically call refresh(). You can see some extra steps needed both above and inside setup().
If you know for a fact that your own project will only ever use one chip or the other, you can trim the extraneous code. If sharing code with others as an open source project, itâs more neighborly to support all the different chips.
Once inside setup(), each classâ begin() function is a little different. NeoPXL8 accepts no arguments, it just runs one specific way. NeoPXL8HDR adds some flair:
bool status = leds.begin(true, 4, true);
The first argument selects whether frame-to-frame blending should be enabled. If you canât generate frequent frames for animation, the library will provide some interpolation for you. This requires an extra 6 (RGB) or 8 (RGBW) bytes of RAM per pixel, atop the libraryâs already voracious needs. This 512-pixel example works fine with it, so itâs set true.
Second argument establishes the temporal dithering resolution. NeoPixels normally provide 256 brightness levels, or 8 bits. The 4 here bumps this up to 12 bits (effectively, but not actually, the extra being dithered). Long chains of NeoPixels are slow to update and canât use a lot of ditheringâŠso you can dial this down (or up for short strips) to balance refresh rate with effective color range. Valid range is 0 (no dithering) to 8 (maximum dithering, but probably too infrequent to be useful).
Third argument is whether to enable double-buffering in the underlying NeoPXL8 library, freeing up the CPU to start on the next refresh sooner. Again, uses more RAM, but this 512-pixel demo is not a problem. This argument is currently ignored on SAMD, only the RP2040 provides this, but itâs present for code compatibility.
begin() returns a status code: true if it was able to allocate the required RAM, false if not. The example sketch ignores this for brevity because we know it fits, but well-behaved neighborly code may want to respond appropriately (perhaps printing a message to the Serial console and/or blinking a boardâs built-in LED).
Both examples then call setBrightness(), but NeoPXL8HDR works a little differently:
leds.setBrightness(65535 / 8, 2.6);
The first argument is a peak brightness level from 0 to 65535 (vs. 0â255 in NeoPixel and NeoPXL8). Everything sent to the LEDs will be scaled in proportion, this is just the top value. Itâs set to 65535 / 8 here (instead of 8191) just as a fancy and hopefully more readable way of saying âone eighth of the maximum brightness,â but the two are equivalent.
Second is a gamma correction factor as a floating-point value. This is a topic extensively covered in other guides already, but the basic idea is to make âin-betweenâ colors more perceptually linear (because 50% duty cycle doesnât look 50% as bright). A value of 2.6 has worked well for us over the years. Itâs subjective though, you can try more or less.
The default gamma value if left un-set is 1.0; linear brightness values have a linear effect on duty cycle, which is not perceptually linear. This is on purpose and by design, so that any NeoPixel or NeoPXL8 code brought over to HDR doesnât suddenly yield surprises; it will look more or less the same until you start adapting your code. Notice the NeoPXL8 strandtest code performs its own gamma correction on every pixel, while strandtest_hdr doesnât have to.
The remainder of these two examplesâstrandtest and strandtest_hdrâis then very similar, with the exception mentioned above that hdr doesnât need to gamma-correct every pixel, itâs done automatically. Both produce eight rows of colored ârain,â but the HDR version looks better especially among the lower brightness levels. Also theyâre both using 8-bit color values despite HDRâs support for 16-bit. More on that later.
Finer setBrightness() Details
First, please, I must reiterate a point from other NeoPixel projects and documentation: setBrightness() never was and never will be intended as an animation effect in itself. Configure it once at startup and leave it beâŠin fact you may get flickering in NeoPXL8HDR if using setBrightness() âlive.â Animated fades should be rendered in code, perhaps with the NeoPixel fill() function.
If called with a single argument (no gamma value), setBrightness() works just like the original NeoPixel/NeoPXL8 version: the brightness value is 8-bit (0-255) and the library will scale it up. This way, old code will work just as it did before. Only when specifying a gamma value, or in more cases listed below, is the brightness level 16 bits.
âBrightness,â in the context of setBrightness(), is not a perceptual brightness, but rather a duty cycle. So even if a gamma value is specified, this doesnât apply to the brightness value. Requesting 32767 (or 127 for the 8-bit variant above) yields a peak brightness with about a 50% duty cycle, which will appear more than half as bright. This is normal and by design, again for maintaining âclassicâ code behavior.
In addition to these two formats:
setBrightness(0-255); setBrightness(0-65535, float gamma);
NeoPXL8HDR offers the ability to set peak red, green, blue and white (for RGBW pixels) independently, which can be used to improve color balance (folks often find that NeoPixels appear a bit blue-tinted when all elements are equally lit):
setBrightness(red, green, blue); setBrightness(red, green, blue, gamma); setBrightness(red, green, blue, white); setBrightness(red, green, blue, white, gamma);
The red, green, blue (and white) values are always 16-bit (0-65535) with these syntaxes; thereâs no 8-bit mode, no need for back-compatibility since classic NeoPixel doesnât offer this. Gamma is always floating-point, 1.0 is linear, and 2.6 works well in practice.
Another reason for defaulting to linear gamma (rather than imposing a value like 2.6) is that some users may want to provide their own more sophisticated color-correction functions. Leave gamma at 1.0 and then use the 16-bit pixel setting functionsâŠ
setPixelColor(pixel, r, g, b); setPixelColor(pixel, 0xXXXXXXXX); unsigned long x = getPixel(pixel);
âŠplus the Color() and ColorHSV() functions for âpackingâ RGB(W) values and working with HSV colors.
All of these functions work as before and use 8-bit (0-255) brightness values. NeoPXL8HDR will upscale these numbers to the 16-bit range (or decimate back down to 8 bits in the getPixel() case).
To work with the full range of 16-bit values, a different set of functions must be usedâŠ
set16(pixel, r, g, b); set16(pixel, r, g, b, w); get16(pixel, &r, &g, &b, &w);
In each case, pixel is a 16-bit pixel index (0â65535) just like before. For the two set functions, r, g, b (and w if present) are now also 16-bit values (0â65535). There is no âpackedâ set16() function as we donât have 64-bit types just yet.
The rgb(w) arguments to get16() are pointers to 16-bit variables (unsigned short or uint16_t). w can be NULL if itâs not used, but the others must point to valid destinations.
Adafruit_NeoPixel provides a getPixels() function which returns a pointer straight into the buffer where 8-bit colors are stored. High-performance code can bypass setPixelColor() and work with the pixel buffer directly, with all the risks that entails.
The Adafruit_NeoPXL8HDR class also provides a getPixels() function, but the color values are 16-bit (function returns a uint16_t *). One perk is that the values here are always in RGB or RGBW order, no need to reorder for different pixel vintages. Same risks still apply though, like anything with pointers, you can clobber things if you go out of range.
Page last edited March 08, 2024
Text editor powered by tinymce.