Maximum current - Feature

Hi,

some of you maybe know about the software WLED you can run on ESP32s.
they have a feature i find very useful. it is “Maximum current”:with it i can have a 5m strip but only power it with a 2A power supply and configure 2A max current.
This way i never need to worry about having a too weak power supply for my projects.

Hey @mephisto! Thanks for bringing this up.

I think there was a community discussion about this once before, and I think the main issue preventing this from being easy on Pixelblaze is an internal architecture decision.

Pixelblaze doesn’t buffer it’s computation internally. That means that as soon as the first pixel in a frame is computed, it can be sent out to the strip. This pipelining has some speed and architectural advantages. Unfortunately it also prevents you from knowing how to scale the first pixel’s intensity to satisfy power consumption constraints, because you need to know the planned intensity of all pixels in the frame before selecting a scale factor for them all.

It might be also feasible to always scale the next frame based on the prior frame’s values and a short term moving average. I wonder if a larger capacitor across the rails would help for this situation too.

A second and more minor challenge was that different models of pixels have slightly different maximum current draws. But that’s something we could allow people to measure themselves, or apply a conservative estimate, like 60 mA for 5 V pixels.

Thanks for the suggestion and telling us what you find useful in other controllers!

thank you for explaining this in so much detail.

this makes sense. how do you go about calculating power using for your projects. using the full 60mA seems excessive.

In my patterns that use a framebuffer (i.e. an array of pixel values that I populate, and stream out in the render() function) and are on an amperage-limited power supply, if the total T of all values is above some maximum M, I multiply all of the values by M/T.

I don’t calculate the amps or anything, and I don’t scale it differently for R/G/B. I just lower M until the power supply stops popping its breaker. :smiley:

i think i understand what you mean, but i have no idea about how to do this.
could you share a example?
i am not good at programming (yet) but i am working on it :sweat_smile:

This isn’t a simple example, but … well … you just have to pay attention to how the fb_ arrays are treated and the code commented with “Calculate the sum of all pixels in the framebuffer … and scale them all down if necessary!”

There are language features that would make a lot of this more concise … you are fortunate that I haven’t used them so you’ll have a better chance of understanding the code. :wink:

Code
// This pattern is designed for a "sword" with an LED strip folded in half
// and demonstrates:
// - non-render-ending HSVtoRGB function
// - RGB framebuffer accumulation of HSVtoRGB results
// - Framebuffer attenuation to avoid pulling too many amps from the battery

export var accelerometer

export var x = 0
export var y = 0
export var z = 0

export var total

// configuration
var max_total = 12 // maximum total value to prevent overload
var streak_count = 20 // streak count

// tuning
var scale_acceleration = 50.0 // acceleration scale to ~ [-1,1]
var scale_speed = 12.0 // speed scale to ~10x
var scale_brightness = 8.0 // scale rgb by 1/scale_brightness before summing

// streak state storage
var s_age = array(streak_count)
var s_hue = array(streak_count)
var s_spd = array(streak_count)
var s_len = array(streak_count)

var hPc = pixelCount/2 // half pixelCount

// output framebuffer
var fb_Red = array(hPc)
var fb_Green = array(hPc)
var fb_Blue = array(hPc)

// other stuff I was hacking on
// export var angle = 0
// export var max_spd = 0

var r, g, b // filled by HSVtoRGB

function HSVtoRGB(h, s, v) {
    var i, f, p, q, t;
    i = floor(h * 6);
    f = h * 6 - i;
    p = v * (1 - s);
    q = v * (1 - f * s);
    t = v * (1 - (1 - f) * s);
    im6 = i % 6
    if (im6 == 0) {
      r = v; g = t; b = p;
    } else { if (im6 == 1) {
      r = q; g = v; b = p;
    } else { if (im6 == 2) {
      r = p; g = v; b = t;
    } else { if (im6 == 3) {
      r = p; g = q; b = v;
    } else { if (im6 == 4) {
      r = t; g = p; b = v;
    } else {if (im6 == 5) {
      r = v; g = p; b = q;
    }}}}}}
}

export function beforeRender(delta) {
  // current acceleration, with a 1-sample smoothing window
  var nx = clamp((x + (accelerometer[2]*scale_acceleration))/2.0,-1,1)
  var ny = clamp((y + (accelerometer[0]*scale_acceleration))/2.0,-1,1)
  var nz = clamp((z + (accelerometer[1]*scale_acceleration))/2.0,-1,1)

  // angle = acos(((x*nx)+(y*ny)+(z*nz))/(sqrt(x*x+y*y+z*z)*sqrt(nx*nx+ny*ny+nz*nz)))

  // clear framebuffer
  for (n = 0; n < hPc; n++) {
    fb_Red[n] = 0
    fb_Green[n] = 0
    fb_Blue[n] = 0
  }

  // create a new streak if change in acceleration is low
  if (abs(sqrt(x*x+y*y+z*z) - sqrt(nx*nx+ny*ny+nz*nz)) < 0.25) {
    s_new = 1
  }

  // render streaks
  for (n = 0; n < streak_count; n++) {
    // if we are creating a new streak, and this slot is empty, fill it
    if (s_new && s_age[n] == 0) {
      s_age[n] = 1/1024
      s_hue[n] = random(1) // or angle/PI // or (abs(nx-ny))
      s_spd[n] = 1 + scale_speed * sqrt(abs(nz * nx * ny) * (1 + random(2)))
      s_len[n] = max(12 - s_spd[n] + random(3), 3)
      // max_spd = max(max_spd, s_spd[n])
      s_new = 0
    }

    if (s_age[n] > 0) {
      len = s_len[n]
      pix = s_spd[n] / scale_speed * s_age[n] / 6
      if ((pix-len) >= hPc) {
        s_age[n] = 0
        if (s_new) { n-- } // render this streak
      } else {
        s_age[n] += delta
        hue = s_hue[n]
        remaining = len
        for (pn = ceil(pix); remaining > 0 && pn >= 0; remaining--) {
          if (pn < hPc) {
            fade = remaining/len
            HSVtoRGB(hue, sqrt(clamp(1.5 - fade,0,1)), pow(clamp((1-pn/hPc) * fade * fade,0,1),1.5) )
            fb_Red[pn] += r/scale_brightness
            fb_Green[pn] += g/scale_brightness
            fb_Blue[pn] += b/scale_brightness
          }
          pn--
        }
      }
    }
  }

  // Calculate the sum of all pixels in the framebuffer ...
  total = 0
  for (n = 0; n < hPc; n++) {
    fb_Red[n] = min(1,fb_Red[n])
    fb_Green[n] = min(1,fb_Green[n])
    fb_Blue[n] = min(1,fb_Blue[n])
    total += fb_Red[n] + fb_Green[n] + fb_Blue[n]
  }
  // ... and scale them all down if necessary!
  if (total > max_total) {
    adjust = total / max_total
    for (n = 0; n < hPc; n++) {
      fb_Red[n] *= adjust
      fb_Green[n] *= adjust
      fb_Blue[n] *= adjust
    }
  }

  // Store current values for smoothing on next frame
  x = nx
  y = ny
  z = nz
}

export function render(index) {
  // The PB is at one end of the strip, and the strip is folded in half
  // Use "^hPc - [...]" instead of "[...] - 1$" if PB is at the other end
  pindex = ( (index < hPc) ? (hPc - index) : (index - hPc + 1) ) - 1

  rgb(fb_Red[pindex], fb_Green[pindex], fb_Blue[pindex])
}
1 Like

Sorceror’s example is great example because it does it correctly with R, G, and B components. To try to simplify it even more and show the essential idea, I wrote this slightly more minimal example.

/*
  Limiting current using a framebuffer.
  
  Imagine the original pattern was:
  
  export function render(index) {
    pct = index / pixelCount
    v = perlinTurbulence(pct * 8, time(.1), 0, 4, 1, 2)
    hsv(0, 0, v) // Literal white noise
  }
  
  Instead, we'll calculate it all in beforeRender(), sum it up, and scale output
  such that very intense output above 20% of all-white is scaled down.
  
  This example is simplified to ignore how non-white RGB colors 
  draw different amounts.
*/

values = array(pixelCount)
export var currentLimitingFactor = 1 // Value to scale final output. Should be in 0..1.
var intensityLimit = .2 * pixelCount // 20% of max current limit

export function beforeRender(delta) {
  frameIntensitySum = 0
  
  for (i = 0; i < pixelCount; i++) {
    pct = i / pixelCount
    var thisValue = perlinTurbulence(pct * 8, time(.1), 0, 4, 1, 2)
    thisValue = clamp(thisValue, 0, 1) // limit outliers to their real current draw
    
    values[i] = thisValue
    frameIntensitySum = frameIntensitySum + thisValue
  }

  currentLimitingFactor = 1
  
  if (frameIntensitySum > intensityLimit) { // Over intensityLimit currrent draw
    currentLimitingFactor = intensityLimit / frameIntensitySum
  }
}

export function render(index) {
  v = values[index]

  hsv(0, 0, v * currentLimitingFactor) 
}
1 Like

What I do is set the Global Brightness to a low value, sometimes as low as 15 or 20% otherwise will trip the thermal breaker i use to limit overdriving the Power source. for example, i use 2048 led matrix that will suck way more than 100A and burn into a puff of smoke if you would let it. (Manufacturer says limit a panel of 256 to 7A, but the LEDs will consume a lot more) it’s powered by a 12A supply. with a 7A thermal limiter (circuit breaker) and an old school meter (for power consumption observation) and find running through most patterns works great at about 15-20%. If i set it higher, eventually i will find its tripped the breaker and need to reset it. i wrote a power test pattern and found the consumption is not quite linear and be careful if you set it to run all of the lights and the pattern locks up, it can be hard to reset, and need to get the board to crash and crash a few times to recover.

1 Like