Dithering support & bugfix in UI

Thanks to @dedehai & @zalatnaicsongor
This commit is contained in:
Blaz Kristan 2024-08-30 15:21:16 +02:00
parent c51ce2eec7
commit 0d035a08d6
3 changed files with 63 additions and 39 deletions

View File

@ -6,6 +6,7 @@
#include <IPAddress.h> #include <IPAddress.h>
#ifdef ARDUINO_ARCH_ESP32 #ifdef ARDUINO_ARCH_ESP32
#include "driver/ledc.h" #include "driver/ledc.h"
#include "soc/ledc_struct.h"
#endif #endif
#include "const.h" #include "const.h"
#include "pin_manager.h" #include "pin_manager.h"
@ -392,7 +393,7 @@ void BusDigital::cleanup(void) {
#define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM
#else #else
// ESP32: 20 bit (but in reality we would never go beyond 16 bit as the frequency would be to low) // ESP32: 20 bit (but in reality we would never go beyond 16 bit as the frequency would be to low)
#define MAX_BIT_WIDTH 20 #define MAX_BIT_WIDTH 14
#endif #endif
#endif #endif
@ -413,11 +414,13 @@ BusPwm::BusPwm(BusConfig &bc)
analogWriteRange((1<<_depth)-1); analogWriteRange((1<<_depth)-1);
analogWriteFreq(_frequency); analogWriteFreq(_frequency);
#else #else
// for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer
_ledcStart = pinManager.allocateLedc(numPins); _ledcStart = pinManager.allocateLedc(numPins);
if (_ledcStart == 255) { //no more free LEDC channels if (_ledcStart == 255) { //no more free LEDC channels
pinManager.deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); pinManager.deallocateMultiplePins(pins, numPins, PinOwner::BusPwm);
return; return;
} }
if (_needsRefresh) _depth = 8; // fixed 8 bit depth with 4 bit dithering (ESP8266 has no hardware to support dithering)
#endif #endif
for (unsigned i = 0; i < numPins; i++) { for (unsigned i = 0; i < numPins; i++) {
@ -501,38 +504,62 @@ uint32_t BusPwm::getPixelColor(uint16_t pix) const {
void BusPwm::show() { void BusPwm::show() {
if (!_valid) return; if (!_valid) return;
const unsigned numPins = getPins(); const unsigned numPins = getPins();
const unsigned maxBri = (1<<_depth); const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8)
// use CIE brightness formula // use CIE brightness formula to fit (or approximate linearity of) human eye perceived brightness
unsigned pwmBri = (unsigned)_bri * 100; // the formula is based on 12 bit resolution as there is no need for greater precision
if (pwmBri < 2040) unsigned pwmBri = (unsigned)_bri * 100; // enlarge to use integer math for linear response
pwmBri = ((pwmBri << _depth) + 115043) / 230087; //adding '0.5' before division for correct rounding if (pwmBri < 2040) {
else { // linear response for values [0-20]
pwmBri = ((pwmBri << 12) + 115043) / 230087; //adding '0.5' before division for correct rounding
} else {
// cubic response for values [21-255]
pwmBri += 4080; pwmBri += 4080;
float temp = (float)pwmBri / 29580.0f; float temp = (float)pwmBri / 29580.0f;
temp = temp * temp * temp * maxBri; temp = temp * temp * temp * 4095.0f;
pwmBri = (unsigned)temp; pwmBri = (unsigned)temp;
} }
// pwmBri is in range [0-4095]
// determine phase shift
[[maybe_unused]] unsigned phaseOffset = maxBri / numPins; // (maxBri is at _depth resolution)
// we will be phase shifting every channel by fixed amount (i times /2 or /3 or /4 or /5)
// phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type (with 180° phase)
// CCT additive blending must be 0 (WW & CW must not overlap) in such case
// for all other cases it will just try to "spread" the load on PSU
for (unsigned i = 0; i < numPins; i++) { for (unsigned i = 0; i < numPins; i++) {
unsigned scaled = (_data[i] * pwmBri) / 255; unsigned scaled = (_data[i] * pwmBri) / 255;
if (_reversed) scaled = maxBri - scaled; // adjust "scaled" value (to fit resolution bounds)
if (_depth < 12 && !_needsRefresh) scaled >>= 12 - _depth; // normalize scaled value (if not using dithering)
else if (_depth > 12) scaled <<= _depth - 12; // scale to _depth if using >12 bit
if (_reversed) scaled = maxBri - scaled;
#ifdef ESP8266 #ifdef ESP8266
analogWrite(_pins[i], scaled); analogWrite(_pins[i], scaled);
#else #else
unsigned channel = _ledcStart + i; unsigned channel = _ledcStart + i;
// determine phase shift POC for PWM CCT (credit @dedehai) if (_type == TYPE_ANALOG_2CH && Bus::getCCTBlend() == 0) {
// phase shifting (180°) is only available for PWM CCT LED type if _needsRefresh is true (UI hack) // pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer
// and CCT blending is 0 (WW & CW must not overlap) unsigned briLimit = phaseOffset << (_needsRefresh*4); // expand limit if using dithering (_depth==8, scaled is at 12 bit)
// this will allow using H-bridge to drive reverse-polarity CCT LED strip (2 wires) if (scaled >= briLimit) scaled = briLimit - 1; // safety check & 1 pulse dead time when brightness is at 50%
// NOTE/TODO: if this has no side effects we may forego UI hack and the need for _needsRefresh }
// we may even use phase shift to evenly distribute power across different pins unsigned gr = channel/8; // high/low speed group
if (_type == TYPE_ANALOG_2CH && _needsRefresh && Bus::getCCTBlend() == 0) { // hacked to determine if phase shifted PWM is requested unsigned ch = channel%8; // group channel
unsigned maxDuty = (maxBri / numPins); // numPins is 2 if (_needsRefresh) {
if (scaled >= maxDuty) scaled = maxDuty - 1; // safety check & add dead time of 1 pulse when brightness is at 50% // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor)
ledc_set_duty_and_update((ledc_mode_t)(channel / 8), (ledc_channel_t)(channel % 8), scaled, maxDuty*i); // https://github.com/Aircoookie/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1)
} else // directly write to LEDC struct as there is no HAL exposed function for dithering
ledcWrite(channel, scaled); // duty has 20 bit resolution with 4 fractional bits (24 bits in total)
// _depth is 8 bit in this case (and maxBri==256), scaled is still at 12 bit
LEDC.channel_group[gr].channel[ch].duty.duty = scaled; // write full 12 bit value (4 dithering bits)
LEDC.channel_group[gr].channel[ch].hpoint.hpoint = phaseOffset*i; // phaseOffset is at _depth resolution (8 bit)
ledc_update_duty((ledc_mode_t)gr, (ledc_channel_t)ch);
} else {
// scaled will be [0-((1<<_depth)-1)] and hpoint evenly distributed
ledc_set_duty_and_update((ledc_mode_t)gr, (ledc_channel_t)ch, scaled, phaseOffset*i);
//ledcWrite(channel, scaled);
}
#endif #endif
} }
} }

View File

@ -533,7 +533,11 @@
#ifdef ESP8266 #ifdef ESP8266
#define WLED_PWM_FREQ 880 //PWM frequency proven as good for LEDs #define WLED_PWM_FREQ 880 //PWM frequency proven as good for LEDs
#else #else
#define WLED_PWM_FREQ 19531 #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK
#define WLED_PWM_FREQ 9765 // XTAL clock is 40MHz (this will allow 12 bit resolution)
#else
#define WLED_PWM_FREQ 19531 // APB clock is 80MHz
#endif
#endif #endif
#endif #endif

View File

@ -5,9 +5,9 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>LED Settings</title> <title>LED Settings</title>
<script> <script>
var d=document,laprev=55,maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=4096,maxL=1333,maxCO=10,maxLbquot=0; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32 var d=document,laprev=55,maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxLbquot=0; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
var oMaxB=1; var oMaxB=1;
d.ledTypes = []; // filled from GetV() d.ledTypes = [/*{i:22,c:1,t:"D",n:"WS2812"},{i:42,c:6,t:"AA",n:"PWM CCT"}*/]; // filled from GetV()
d.um_p = []; d.um_p = [];
d.rsvd = []; d.rsvd = [];
d.ro_gpio = []; d.ro_gpio = [];
@ -60,7 +60,7 @@
x.className = error ? "error":"show"; x.className = error ? "error":"show";
clearTimeout(timeout); clearTimeout(timeout);
x.style.animation = 'none'; x.style.animation = 'none';
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900); timeout = setTimeout(()=>{ x.className = x.className.replace("show", ""); }, 2900);
} }
function bLimits(b,v,p,m,l,o=5,d=2,a=6) { function bLimits(b,v,p,m,l,o=5,d=2,a=6) {
// maxB - max buses (can be changed if using ESP32 parallel I2S) // maxB - max buses (can be changed if using ESP32 parallel I2S)
@ -69,7 +69,7 @@
// maxV - min virtual buses // maxV - min virtual buses
// maxPB - max LEDs per bus // maxPB - max LEDs per bus
// maxM - max LED memory // maxM - max LED memory
// maxL - max LEDs // maxL - max LEDs (will serve to determine ESP >1664 == ESP32)
// maxCO - max Color Order mappings // maxCO - max Color Order mappings
oMaxB = maxB = b; maxD = d, maxA = a, maxV = v; maxM = m; maxPB = p; maxL = l; maxCO = o; oMaxB = maxB = b; maxD = d, maxA = a, maxV = v; maxM = m; maxPB = p; maxL = l; maxCO = o;
} }
@ -237,16 +237,8 @@
p0d = "Data "+p0d; p0d = "Data "+p0d;
break; break;
case 'A': // PWM analog case 'A': // PWM analog
switch (gT(t).t.length) { // type length determines number of GPIO used if (gT(t).t.length > 1) p0d = "GPIOs:";
case 1: break; off = "Dithering";
case 2: off = "Phase shift";
if (d.Sf["CB"].value != 0) gId(`rf${n}`).checked = 0; // disable phase shifting
gId(`rf${n}`).disabled = (d.Sf["CB"].value != 0); // prevent changes
// fallthrough
default: p0d = "GPIOs:"; break;
}
// PWM CCT allows phase shifting
gId(`dig${n}f`).style.display = (gT(t).t.length != 2) ? "none" : "inline";
break; break;
case 'N': // network case 'N': // network
p0d = "IP address:"; p0d = "IP address:";
@ -259,7 +251,7 @@
gId("p1d"+n).innerText = p1d; gId("p1d"+n).innerText = p1d;
gId("off"+n).innerText = off; gId("off"+n).innerText = off;
// secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off) // secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off)
let pins = Math.min(gT(t).t.length,1) + 3*isNet(t); // fixes network pins to 4 let pins = Math.max(gT(t).t.length,1) + 3*isNet(t); // fixes network pins to 4
for (let p=1; p<5; p++) { for (let p=1; p<5; p++) {
var LK = d.Sf["L"+p+n]; var LK = d.Sf["L"+p+n];
if (!LK) continue; if (!LK) continue;
@ -294,7 +286,7 @@
gId("dig"+n+"c").style.display = (isAna(t)) ? "none":"inline"; // hide count for analog gId("dig"+n+"c").style.display = (isAna(t)) ? "none":"inline"; // hide count for analog
gId("dig"+n+"r").style.display = (isVir(t)) ? "none":"inline"; // hide reversed for virtual gId("dig"+n+"r").style.display = (isVir(t)) ? "none":"inline"; // hide reversed for virtual
gId("dig"+n+"s").style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide skip 1st for virtual & analog gId("dig"+n+"s").style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide skip 1st for virtual & analog
gId("dig"+n+"f").style.display = (isDig(t) || isPWM(t)) ? "inline":"none"; // hide refresh (PWM hijacks reffresh for phase shifting) gId("dig"+n+"f").style.display = (isDig(t) || (isPWM(t) && maxL>2048)) ? "inline":"none"; // hide refresh (PWM hijacks reffresh for dithering on ESP32)
gId("dig"+n+"a").style.display = (hasW(t)) ? "inline":"none"; // auto calculate white gId("dig"+n+"a").style.display = (hasW(t)) ? "inline":"none"; // auto calculate white
gId("dig"+n+"l").style.display = (isD2P(t) || isPWM(t)) ? "inline":"none"; // bus clock speed / PWM speed (relative) (not On/Off) gId("dig"+n+"l").style.display = (isD2P(t) || isPWM(t)) ? "inline":"none"; // bus clock speed / PWM speed (relative) (not On/Off)
gId("rev"+n).innerHTML = isAna(t) ? "Inverted output":"Reversed"; // change reverse text for analog else (rotated 180°) gId("rev"+n).innerHTML = isAna(t) ? "Inverted output":"Reversed"; // change reverse text for analog else (rotated 180°)
@ -916,7 +908,8 @@ Swap: <select id="xw${s}" name="XW${s}">
<br> <br>
Calculate CCT from RGB: <input type="checkbox" name="CR"><br> Calculate CCT from RGB: <input type="checkbox" name="CR"><br>
CCT IC used (Athom 15W): <input type="checkbox" name="IC"><br> CCT IC used (Athom 15W): <input type="checkbox" name="IC"><br>
CCT additive blending: <input type="number" class="s" min="0" max="100" name="CB" onchange="UI()" required> % CCT additive blending: <input type="number" class="s" min="0" max="100" name="CB" onchange="UI()" required> %<br>
<i class="warn">WARNING: When using H-bridge for reverse polarity (2-wire) CCT LED strip<br><b>make sure this value is 0</b>.<br>(ESP32 variants only, ESP8266 does not support H-bridges)</i>
</div> </div>
<h3>Advanced</h3> <h3>Advanced</h3>
Palette blending: Palette blending: