hd108: Initial working state#2119
Conversation
| pixels.loadAndScaleRGB(&r8, &g8, &b8); | ||
|
|
||
| // Derive full-gain header (no coarse brightness) | ||
| const fl::u8 bri5 = 0x1F; // full current drive |
There was a problem hiding this comment.
pixels.getHdScale(&c0, &c1, &c2, &brightness);
Use this so that the brightness component (a separate value) will not premix into the color values and decimate them.
Use this brightness value to control the brightness byte sent down the wire.
This will result in gamma correct pixel color values, but linear global brightness
There was a problem hiding this comment.
HD108 uses two header bytes for brightness and color current control, so calling getHdScale() results in double-scaling and breaks the code, loadAndScaleRGB() plus explicit 16-bit gamma mapping is what I could get working....
There was a problem hiding this comment.
getHdScale() Does NOT Cause Double Scaling
The confusion stems from not understanding that these two functions represent fundamentally different color pipelines:
pixels.loadAndScaleRGB(&r8, &g8, &b8);
pixels.getHdScale(&r, &g, &b, &bri);Both produce final color values, but they handle brightness differently:
Legacy Path: loadAndScaleRGB()
- Returns: RGB values with everything premixed together
- Color calculation: (user_value × color_correction × global_brightness)
- Result: RGB channels contain the final scaled values with brightness already baked in
- Trade-off: Brightness is applied early in the pipeline, which can crush color information when brightness is low
High-Definition Path: getHdScale()
- Returns: RGB color correction + separate brightness value
- Color calculation: (user_value × color_correction) — brightness is NOT premixed
- Brightness: Returned as a separate parameter
- Advantage: Preserves high-fidelity color mixing, especially for (r,g,b > 128) where high-order bits matter
Why Separation Matters
Premultiplying brightness early destroys color precision. When global_brightness = 32 (12.5%), a color value of 200 becomes ~25
after premixing. You've lost all the fine color ratio information.
By keeping brightness separate:
- Color correction maintains full precision through the pipeline
- High-order bits preserved for accurate color mixing when values > 128
- Controller applies brightness at the final stage using its native high bit-depth (e.g., HD108, APA102 with 5-bit global
brightness)
This is the entire point of FASTLED_HD_COLOR_MIXING — to defer brightness scaling until the hardware can apply it without destroying
color fidelity.
There is no double scaling. The brightness is separated from color correction and applied exactly once by the chipset driver.
There was a problem hiding this comment.
Tbh I don't get it - I rebased to master and moved to loadRGBScaleAndBrightness. However using a rainbow pattern with dimming, the RGB values returned are always 0xFF:
HD108Controller: r=255 g=255 b=255 bri=214
So the RGB color scale correction doesn't seem to work for me? How do I load the RGB values? This doesn't seem to match the doc which states:
/// The RGB values returned are color-corrected but NOT scaled by brightness.
What i've tried alot is to use the brightness value here to then do better than driving always at max 0x1F the LEDs, like:
uint8_t bri5 = ((uint16_t)bri8 * 31 + 127) / 255;
if (bri5 == 0 && bri8 > 0) bri5 = 1;
But scaling this always results in minor flickering of the LEDs, I have the impression driving this too fast causes issues? I even tried smoothing to avoid large changes but I don't see much effect...
|
just easier if i finish it up myself. |
|
I tested this implementation against a real HD108 strip: Using the current header encoding (f0 / f1 with overlapping brightness bits), the strip does not respond (no visible output). According to the official HD108 Specification V1.2 (Rose Lighting, 2022), each LED frame is 64 bits, starting with: I can’t find any documentation in V1.2 that describes the current dual-byte / overlapping brightness encoding: // Header bytes: HD108 dual-byte encoding for brightness/current control
// f0 format: 1bbbbb00 (bit 7=start marker, bits 6-2=brightness, bits 1-0=padding)
// f1 format: bbbBBBBB (bits 7-5=lower 3 bits, bits 4-0=full 5 bits, overlapping)
// This encoding may provide separate PWM and current controls (not fully documented)
const fl::u8 f0 = fl::u8(0x80 | ((bri5 & 0x1F) << 2));
const fl::u8 f1 = fl::u8(((bri5 & 0x07) << 5) | (bri5 & 0x1F));
Is this implementation based on an older version of the leds or am I missing something here? |
|
AI is telling me that indeed the protocol is off. It's issuing a fix. I'll ping this thread when it's finished cooking. |
HD108 Encoder Protocol FixHi @arfoll and @RobinSeidel, I'm Claude, an AI assistant working on behalf of @zackees. After analyzing the compatibility issue reported by @RobinSeidel, I've identified and fixed a protocol encoding problem in the HD108 implementation. Issue SummaryThe merged HD108 encoder was using an incorrect header byte encoding that doesn't match the official HD108 specification. While it worked for some users' strips (like @arfoll's), it failed completely for others (like @RobinSeidel's) even when both were from the same supplier. Root Cause: The implementation was using a single 5-bit brightness value. HD108 Specification: According to the official HD108 Specification V1.2, the header bytes should encode three independent 5-bit gain values (one per R/G/B channel): // NEW (CORRECT) - Per-channel gain encoding
// f0: [1][RRRRR][GG] - marker bit, 5-bit R gain, 2 MSBs of G gain
// f1: [GGG][BBBBB] - 3 LSBs of G gain, 5-bit B gain
f0 = 0x80 | ((r_gain & 0x1F) << 2) | ((g_gain >> 3) & 0x03)
f1 = ((g_gain & 0x07) << 5) | (b_gain & 0x1F)Fix AppliedThe encoding has been corrected in:
Current behavior: For simplicity, we're using the same gain value for all three channels ( Future enhancement: The encoding structure is now correct and ready for independent per-channel gain control, which is HD108's key advantage over APA102 (enables white balance correction and higher color range). Why This Should Fix @RobinSeidel's IssueThe corrected encoding properly distributes the 15 bits across R/G/B channels as the HD108 specification requires. Strips that strictly enforce the protocol (like yours) should now respond correctly. @RobinSeidel - Could you test the updated implementation when you have a chance? The fix is in the latest master branch. This analysis and fix was performed by Claude Code on behalf of @zackees |
|
Give a whirl, should be fixed. Let me know if it's not. The brightness at this point uses the same algorithm as apa102HD. It's not full resolution as the brightness is computed using the apa102 algorihtm then filled in the same for r,g,b. This scheme is battle tested for 5 bit brightness gamma shifts across components. The brightness is duplicated for now to fix the driver. I'll work on an enhancement. |
HD108 Enhancement: Maximum Gain for Optimal PrecisionHi everyone, I'm Claude, an AI assistant working on behalf of @zackees. Following up on the protocol fix, I've implemented an additional enhancement to the HD108 encoder that optimizes its precision. Enhancement SummaryThe HD108 encoder now uses maximum gain (31) for all RGB channels instead of variable gain based on brightness. The accuracy is close enough to gamma_2_8(u8) => u16 Technical DetailsKey Insight: After applying gamma 2.8 correction to 8-bit color components, we get 16-bit values that already span nearly the full 65,536 dynamic range. Using 5-bit variable doesn't buy much, just a few bits of extra resolution lopped of from the 2.8 gamma curve. If it was 2.0 the colors would match exactly. So some of the low order bits will be cut off. Before (variable gain): // Mapped brightness → 5-bit gain (0-31)
u8 gain = mapBrightness8to5(brightness); // e.g., brightness=128 → gain=16
// Problem: Lower gain = wasted precision in 16-bit color valuesAfter (maximum gain): // Always use maximum current/gain for all channels
constexpr u8 r_gain = 31, g_gain = 31, b_gain = 31;
// Brightness control via 16-bit gamma-corrected PWM values (already applied)Why This Works
Benefits
Reference CommitSee commit 8d3a62e for the complete implementation. Per-Channel Gain (Future)The per-channel gain encoding structure is now correctly implemented and ready for future enhancement. When needed, we can add independent R/G/B gain control for white balance correction and extended color range. This enhancement was implemented by Claude Code on behalf of @zackees |
|
Hi everyone, I’m still unable to get my strip working reliably with FastLED. I believe the protocol implementation itself is correct, so I suspect something else is failing. I wrote a small test implementation (see below) that works flawlessly, which rules out wiring, power supply, or the strip itself. With FastLED, however, I can’t get consistent results. I tested this on both a XIAO ESP32S3 and a standard ESP32 Dev Board, and observed the same behavior on both. After a reset, the LEDs sometimes receive the first frame but then get stuck. I tested this using FastLED commit 8d3a62e. One thing I noticed in that commit is that the comments reference loadAndScaleRGB, but the function does not appear to be called anywhere. I’m not sure whether this is intentional, but it stood out while reviewing the code. This should not, however, be related to the issue I’m seeing. Does commit 8d3a62e work correctly with your strip, @arfoll? For reference, here is the test implementation that works reliably on my setup: #include <SPI.h>
// ===== HSPI configuration (ESP32) =====
SPIClass hspi(HSPI);
// Adjust to your wiring
#define HSPI_SCK 14
#define HSPI_MISO 12 // not used, but SPI.begin needs it
#define HSPI_MOSI 13
#define NUM_LED 144
// SPI settings (25 MHz)
static const uint32_t SPI_HZ = 25000000;
static uint8_t clampBrightness(int b) {
if (b < 0) return 0;
if (b > 31) return 31; // 5-bit
return (uint8_t)b;
}
void setup() {
// Start HSPI bus
hspi.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI);
}
// NOTE: brightness expected 0..31
void sendColor(uint16_t r, uint16_t g, uint16_t b, int brightness) {
uint8_t br = clampBrightness(brightness);
// Construct header in format [1][rrrrr][ggggg][bbbbb]
uint8_t f0 = uint8_t(0x80 | ((br & 0x1F) << 2) | ((br >> 3) & 0x03));
uint8_t f1 = uint8_t(((br & 0x07) << 5) | (br & 0x1F));
hspi.beginTransaction(SPISettings(SPI_HZ, MSBFIRST, SPI_MODE3));
// ===== 64bit Start frame =====
for (int i = 0; i < 4; i++) hspi.transfer16(0x0000);
// ===== LED frames =====
for (int i = 0; i < NUM_LED; i++) {
// [f0][f1][R16][G16][B16]
hspi.transfer(f0);
hspi.transfer(f1);
hspi.transfer16(r); // Red
hspi.transfer16(g); // Green
hspi.transfer16(b); // Blue
}
// ===== 64bit End frame =====
for (int i = 0; i < 4; i++) hspi.transfer16(0xFFFF);
hspi.endTransaction();
}
void loop() {
sendColor(0x0000, 0x0000, 0x0000, 0);
delay(1000);
sendColor(0xFFFF, 0, 0, 14);
delay(1000);
} |
|
If you would like a testing strip, @zackees ,I could send you one (from Germany). |
|
I've not tried since the initial refactor, but try with my original
PR, that definitely worked with my strip. I'll give it a go with
master in a few days as I'm travelling (without RGB 😞)
On Tuesday, 6 January 2026 at 12:51, Robin Seidel
***@***.***> wrote:
… ROBINSEIDEL left a comment (FastLED/FastLED#2119)
[#2119 (comment)]
Hi everyone,
I’m still unable to get my strip working reliably with FastLED. I
believe the protocol implementation itself is correct, so I suspect
something else is failing.
I wrote a small test implementation (see below) that works
flawlessly, which rules out wiring, power supply, or the strip
itself. With FastLED, however, I can’t get consistent results. I
tested this on both a XIAO ESP32S3 and a standard ESP32 Dev Board,
and observed the same behavior on both.
After a reset, the LEDs sometimes receive the first frame but then
get stuck. I tested this using FastLED commit 8d3a62e
[8d3a62e].
One thing I noticed in that commit is that the comments reference
loadAndScaleRGB, but the function does not appear to be called
anywhere. I’m not sure whether this is intentional, but it stood
out while reviewing the code. This should not, however, be related
to the issue I’m seeing.
Does commit 8d3a62e
[8d3a62e]
***@***.*** [https://github.com/arfoll]?
For reference, here is the test implementation that works reliably
on my setup:
#include <SPI.h>
// ===== HSPI configuration (ESP32) =====
SPIClass hspi(HSPI);
// Adjust to your wiring
#define HSPI_SCK 14
#define HSPI_MISO 12 // not used, but SPI.begin needs it
#define HSPI_MOSI 13
#define NUM_LED 144
// SPI settings (25 MHz)
static const uint32_t SPI_HZ = 25000000;
static uint8_t clampBrightness(int b) {
if (b < 0) return 0;
if (b > 31) return 31; // 5-bit
return (uint8_t)b;
}
void setup() {
// Start HSPI bus
hspi.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI);
}
// NOTE: brightness expected 0..31
void sendColor(uint16_t r, uint16_t g, uint16_t b, int brightness) {
uint8_t br = clampBrightness(brightness);
// Construct header in format [1][rrrrr][ggggg][bbbbb]
uint8_t f0 = uint8_t(0x80 | ((br & 0x1F) << 2) | ((br >> 3) & 0x03));
uint8_t f1 = uint8_t(((br & 0x07) << 5) | (br & 0x1F));
hspi.beginTransaction(SPISettings(SPI_HZ, MSBFIRST, SPI_MODE3));
// ===== 64bit Start frame =====
for (int i = 0; i < 4; i++) hspi.transfer16(0x0000);
// ===== LED frames =====
for (int i = 0; i < NUM_LED; i++) {
// [f0][f1][R16][G16][B16]
hspi.transfer(f0);
hspi.transfer(f1);
hspi.transfer16(r); // Red
hspi.transfer16(g); // Green
hspi.transfer16(b); // Blue
}
// ===== 64bit End frame =====
for (int i = 0; i < 4; i++) hspi.transfer16(0xFFFF);
hspi.endTransaction();
}
void loop() {
sendColor(0x0000, 0x0000, 0x0000, 0);
delay(1000);
sendColor(0xFFFF, 0, 0, 14);
delay(1000);
}
—
Reply to this email directly, view it on GitHub
[#2119 (comment)],
or unsubscribe
[https://github.com/notifications/unsubscribe-auth/AAD5SZXRTW3QLROP6BCHNPD4FOOTLAVCNFSM6AAAAACKREFMNCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTOMJUGQYTSOJRGY].
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
I'm game. Reach out to me on reddit.com/u/ZachVorhies and let's get some real HD108's on the test bench. |
|
@RobinSeidel also I want to mention that this is a spi chipset so if there IS a problem, it's going to be via the encoder bytes that are (mis) generated Easy to test:
I have this crazy theory that if the unit test passes then HD108 will work correctly for your build.
this auto install only has one dependency: uv |
|
So here's what I found:
However, are you sure you are using the HW spi in fastled? By default it's not enabled and then the HD108 will simply not work since it's defaulting to 20mhz. Note that my setup is simply an esp32 wroom devboard powered over USB, the LED strip connected to 5V and all grounded together. No level shifting on the HSPI. I'm running two strips of ~150 LEDs on HSPI & VSPI @ 20mhz pretty reliably but I do have a level shifter there, I'm not too sure if it's really needed, but my test strip is smaller. I have found during testing that you can kind of crash the LEDs, as in they will stop responding to anything when you've sent them too much garbage. This has lead to me to quite some time wasting, so watch out for that and sometimes de energise your who strip.... I'm actually not entirely sure what the cannonical way of enabling HWSPI on fastled, a quick google found this andI can't say I agree with #1637, on such high frequencies these micros really need the HW spi cells... |
|
There is a new spi engine in master. It's designed to support massive parallel CPU driven spi and hardware driven dual, quad, 8-way and 16-way spi (PARLIO driver supports this). From my understanding, the SPI controller don't need tight timing, it can start and stop and be paused because of interrupts, all the led chipset cares about about is when then data and clock lines go high or low. That's my assumption. Is this incorrect? If this is correct then the only thing that could be the culprit to crashing HD108 is that the bit patterns being sent are wrong. |
|
Please file an issue with HD108 in our repo issues page so we can track this. This PR is pretty closed and I'm not going to see this issue unless I dig for it in the closed PR section. |

I'm throwing this quickly here for feedback and maybe it's even useful for someone who accidentally bought such an LED strip (didn't expect these strips where so exotic!).
It's a really basic implementation that simply scales the 8bit values to 16. It looks pretty good to me but I have very little reference. Maybe HD108 should rather be HD108_SD.... If people are interested I can make a quick video showing how this looks.
Tested on:
A few disclaimers:
Partially implements #1885 , #1045
Quick demo: https://www.youtube.com/shorts/joDvO3hzpU8