Engineering Journal

← all posts

Measuring an LED pole until it browns out (on purpose)

Hardware & PCBPowerInstrumentation

Real current, voltage, and light measurements from an SK6813 LED installation: what FastLED's power limiter actually delivers, a per-pixel power model that tracked whole-pole draw within 3%, and a deliberately induced power supply collapse.


Why

We build LED poles: steel tubes wrapped in six channels of double-row SK6813-mini strip (480 pixels per channel, 2,880 pixels per pole) driven by an ESP32 receiving frames over Ethernet at 60 fps from a Raspberry Pi.

Our worst field failure mode is ESP32 brownout. If the LEDs demand more current than the supply can deliver, the 5 V rail sags, the ESP32's brownout detector fires, and the controller reboots mid-show. The firmware defends against this with FastLED's power limiter, FastLED.setMaxPowerInMilliWatts(30000), but we had reasons to distrust that number: FastLED estimates power from a generic per-LED model, and anecdotally the pole "safely drew 42 W for weeks" with a 30 W setting.

So we built a measurement bench and asked four questions:

  1. What does the FastLED limiter actually limit to?
  2. How much power does a pixel draw, per color channel, per byte value?
  3. Can we predict a whole pattern's power from its pixels?
  4. What does the failure actually look like, and can we make it happen on demand?

The bench

A dedicated Raspberry Pi 4 reads seven I2C sensors through a PCA9548 multiplexer:

Every board is a stock Adafruit part, about $130 the lot:

Raspberry Pi 4

Raspberry Pi 4

bench host

Runs the capture scripts and generates the test frames. Separate from the pole's own controller.

STEMMA QT breakout

STEMMA QT breakout #6365

Pi header to QT

Puts the Pi's I2C on a keyed 4-pin chain. Nothing to mis-seat, nothing to short.

PCA9548 8-channel I2C multiplexer

PCA9548 #5626

8-channel I2C mux

Eight switched I2C channels, so seven sensors that share one address can coexist.

INA260 power sensor

INA260 × 6 #4226

current, voltage, power

One per LED channel, in series with its 5 V feed. Shunt and ADC are on-board.

TSL2591 light sensor

TSL2591 #1980

light

Taped face-down over one LED. Objective lux, immune to camera auto-exposure.

They wire into a STEMMA QT chain with the PCA9548 as the hub of a star: one cable per device, one mux channel per sensor, no address jumpers. The supply feeds the pole's controller and the controller feeds the strips, so each sensor is spliced into an output's 5 V wire: controller end into Vin+, strip end out of Vin−, on the screw terminals. Ground and LED data run straight through, untouched.

Bench topology: the controller's six outputs, each 5 V feed spliced through an INA260, the I2C star off the PCA9548 mux
The full bench topology. Click to open at full resolution.

The pole under test is powered by a Mean Well LPV-100-5 (5 V, 12 A). Test frames were generated directly on the bench Pi and multicast to the controller in the pole's native UDP frame format, giving per-pixel control of all 2,880 pixels with no pattern engine in the loop. The ESP32's serial console and its heartbeat/alert broadcasts were logged throughout.

Two measurement gotchas worth passing on:

Finding 1: a pixel's current is linear, and color doesn't matter

We drove a 60-pixel block through every byte value on each channel separately, measuring channel current with everything else dark.

Three results, all cleaner than we expected:

Those are electrical measurements, one strip's current sensor against the byte we sent, so they say nothing about how much light a watt buys on each channel.

On top of them sits a fixed cost. All six strips dark measure 2.08 A (10.4 W) for the whole pole, which is 0.72 mA per pixel. The lights-off tax is real.

The light output is linear too, on all four channels:

Lux tracks byte value point-for-point (byte 128 gives 0.509 of byte 255's output), and current is linear in byte value too, so within a channel the lux per amp is flat across the range at these timescales. The TSL2591 is photopically weighted, so this is not a statement that red, green, and blue turn watts into photons equally well; that needs a spectrometer, not a lux sensor. If your LEDs look nonlinear, that's your gamma table, not the silicon.

The pattern power model

Those two constants are the whole model. For a frame of RGB bytes:

current_mA = 0.72 x N_pixels  +  0.048 x sum(every channel byte in the frame)

For our pole: I = 2.1 A + 0.048 mA x sum_of_all_bytes. Any pattern's power can now be computed from its pixel data before it ever plays.

How good is it? Our first whole-pole validation run said "within 10-25%, over-predicting," and we had a tidy physical story ready (voltage starvation at the far end of each 480-pixel run). Then we noticed the trap: those measurements ran with the stock FastLED limiter armed, and FastLED's internal power estimate crosses its budget below the point where its hard clamp is obvious. The limiter was silently scaling frames we believed were clean.

Re-measured with the limiter parked at an unreachable 200 W, the whole-pole curve tracks the per-pixel model within 3% up to 9.5 A, and the leftover 2-3% is honest rail sag. That is twelve points of uniform white between byte 2 and byte 18, not a survey of real patterns. Two lessons for the price of one: the model predicts whole-pole draw closely where we checked it, and a power limiter contaminates measurements well below the point where you notice it clamping. If you are characterizing LEDs behind FastLED, park max_power out of reach first or your data quietly measures the limiter instead of the hardware.

Finding 2: what the FastLED limiter actually delivers

With the model in hand, we ramped a solid white frame while stepping setMaxPowerInMilliWatts through 30 W, 45 W, and 60 W, rebooting between settings. A detail worth knowing: FastLED only reads this value at boot. A runtime change silently does nothing to the frame limiter until restart.

Across the three settings we tried the measured clamp is linear in the setting, and nowhere near equal to it:

setting delivered (measured)
30 W 35.0 W
45 W 46.0 W
60 W 56.1 W

delivered = 14.0 W + 0.70 x setting for this pole. Three points is not a linearity test, so the honest evidence for the line is the one held-out check: the middle point was measured after fitting the other two and landed within 1%.

The shape makes sense once you know FastLED's internals: its power model both overestimates per-pixel modulation draw (about 1.4x on SK6813-mini) and subtracts its own quiescent estimate from the budget, double-counting the idle draw. The two errors partially cancel near 50 W, which is presumably why nobody notices until they measure. The takeaway is not "FastLED is broken." It's that the setting is a knob, not a specification, and each LED type and count needs calibrating against measured watts, with a third point held back to check that a line is the right shape at all.

Finding 3: the failure is a cliff

Everything above happens in the safe zone. The real question was the failure.

First surprise: we couldn't brown it out with content alone at sane settings. With the limiter at 60 W, we hit the pole with 21 instant black-to-white steps (seven step sizes, three trials each) reaching 10.7 A measured, the worst dI/dt a pattern could ever produce. The rail never fell below 4.71 V, and 21 more steps delivered as a 100 ms ramp barely changed the dip. A properly calibrated limiter absorbed every content transient we could build; the supply's regulation, not the step rate, is what mattered.

At the stock 30 W setting the margin is even more decisive: five instant black-to-full-white steps (the worst frame any pattern can produce) held the rail at 4.87 V, drawing the calibrated 35 W. Five trials on one supply at shop temperature is not a proof of impossibility, but at the stock setting we could not get content anywhere near the edge.

So we raised the limiter beyond the supply's capability (100 W setting) and escalated:

We never found a gradual danger zone, but we also never went looking inside the gap: "sustains 120% forever" and "collapses in 80 ms" are adjacent steps of our sweep, byte 32 and byte 40, and we swept nothing between them. So the transition is at most one step wide and might be much narrower or a little softer. What is clearly going on is two protection systems facing off: the supply's constant-current overload behavior versus the load's willingness to keep pulling.

The controller's own instrumentation confirmed each event: the firmware counts brownout resets in RTC memory and broadcasts a PoleFX_alert line on the network. Worth actually listening to; ours were being silently dropped by the receiving server (fixed now).

What this means for brownout-proofing

What we concluded, in order of leverage:

  1. Calibrate the software limiter; it's the actual protection. Set max_power so the delivered watts (via the calibration line, not the setting) stay at or below the supply's rating. On this 60 W supply a setting of about 65 W delivers roughly 60 W; we run a 30 W setting (35 W delivered) for margin. With that in place, nothing we could throw at the pole, worst-case instant white included, got near the cliff.
  2. Power the ESP32 through a buck-boost, not an LDO. Our collapse waveform shows the rail parking at 1.6 V during the fault. No 3.3 V LDO survives that, which is why the controller reboots rather than riding through and shedding load. A buck-boost is the right shape of answer, and we're planning the TPS63020, but be careful what it buys: TI specifies it down to 1.8 V of input, with undervoltage lockout at 1.4 to 1.6 V. Our floor is 1.62 V. So the part covers the 80 ms shoulder at 4.55 V and then sits below its own rated input exactly where the fault gets interesting, and whether it holds 3.3 V long enough for firmware to blank the strips is unresolved: it needs this waveform played into the real converter with the real controller load on it, not a part number. If it does stay up, a controller that survives collapse can blank the LEDs, kill the demand, and let the supply recover, turning a reboot loop into a one-frame flicker. That is the payoff we are chasing, not something we have measured.
  3. Per-output load switches (SY6288 or similar) close the loop. With the controller alive through faults, switchable outputs let it shed strips, stagger power-up inrush, and re-admit load gradually after a fault. Our step data says inrush from content isn't the killer, but staged re-admission after collapse is where the switches earn their place.
  4. Model your content if you bypass a limiter. The per-pixel model (0.72 mA/px + 48 µA/px/byte) predicted whole-pole white within 3% up to 9.5 A, which is as far as we checked it. We now compute worst-frame power for patterns at upload time.

Reproducing this

The whole bench is about $130 of sensors: INA260 breakouts on a STEMMA QT chain (no address jumpers needed, one I2C mux channel per sensor), a TSL2591 taped to an LED, and a Raspberry Pi. The most valuable habits: average over PWM ripple, bond the grounds once, verify every firmware reboot actually happened (lossy UDP config protocols will gaslight you), and let the failure happen. The collapse waveform taught us more than every safe measurement combined.

Data, scripts, and figures for this post were captured on a live bench in a single evening. Currents, voltages, and lux are direct readings. Three numbers are not: the 18.7 A at the fatal step is the per-pixel model's prediction for a step nothing measured, delivered = 14.0 W + 0.70 x setting is a fit to three limiter settings, and 0.72 mA per pixel is the measured whole-pole dark current divided by 2,880.