-
Notifications
You must be signed in to change notification settings - Fork 13.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Keep PWM phases constant #7057
Keep PWM phases constant #7057
Conversation
See esp8266#7054, PWM channels phases can change over time causing visible flickering on LED drivers (Tasmota). This fix makes sure the PWM pulses are kept in sync, phases constant whatever the delay of the NMI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm traveling this week and don't have my logic analyzer or board handy to test, but I think the changes proposed here are on-the-whole beneficial. Let me throw some waveforms out and see what I can see when I get back this weekend.
@@ -256,23 +256,24 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { | |||
// Check for toggles | |||
int32_t cyclesToGo = wave->nextServiceCycle - now; | |||
if (cyclesToGo < 0) { | |||
cyclesToGo = -((-cyclesToGo) % (wave->nextTimeHighCycles + wave->nextTimeLowCycles)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logically, I get what you're trying to here. But it seems like this is a no-op unless we have overshot by an entire waveform cycle, no? If it's been less than a waveform cycle then the mod will be a no-op. The mod operator is a relatively expensive one, might consider just ignoring the case for code speed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, this should be a no-op most of the time. It's a safeguard because if we overshoot one or multiple wafeforms, the PWM will oscillate at 500KHz (1ms up + 1ms down) until the required number of cycles is reached. I don't know if it's even possible, or if 500KHz for a very short amount of time is an issue at all.
We could also add a while (-cyclesToGo > wave->nextTimeHighCycles + wave->nextTimeLowCycles) { cyclesToGo += wave->nextTimeHighCycles + wave->nextTimeLowCycles)};
but that would also add code size to the precious IRAM.
I'm pretty sure this would never occur with PWM, only if someone sets a very short waveform. You can probably drop this for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you comment it out and make a note in the code about why we're skipping, so if something pops up later we can see the reasoning behind the choice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed this line of code for nom but leaved the two variants in comments.
wave->nextServiceCycle = now + wave->nextTimeHighCycles; | ||
nextEventCycles = min_u32(nextEventCycles, wave->nextTimeHighCycles); | ||
wave->nextServiceCycle = now + wave->nextTimeHighCycles + cyclesToGo; | ||
nextEventCycles = min_u32(nextEventCycles, min_u32(wave->nextTimeHighCycles + cyclesToGo, 1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What this and the other branch seem to be doing is to be preserving the waveform phase at expense of period jitter.
Could it be that the pulsing you're seeing on some LED boards is due to periods of some pins slightly increasing (when the total on-time is so small that a few 10s of 80mhz instructions would actually increase the %on-time appreciably)? And this, by introducing a feedback on the period makes things average out by continually adding jitter to the last-checked pin so that over large amounts of time the cyclesToGo
overrun is subtracted off of the hi/low time?
I think it's a fair thing to do, given that when cyclesToGo < 0 then bad stuff has already happened, actually. Feels like a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, if cyclesToGo
is already significantly negative, then we already missed the right timing for the coming edge. Either we keep timing of the next edge, at the expense of phase shifting (original code), or we compensate the missed timing and shorten the next portion to keep phase stable (new version).
Missing one cycle is not visible, whereas shifting the phase creates a visible artefact.
Also min_u32(wave->nextTimeHighCycles + cyclesToGo, 1)
is to make sure that we trigger the change at the next microsecond. My first version without the min_u32
resulted in missing an edge at some rare occasion and creating flashes. It looks safe with this now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry to interfere, but from my work on the concurring PR #7022 I believe recomputing the nextServiceCycle based on now
, instead of incrementally, is at the heart of the problem. I am also not even giving the Timer1 a head start anymore, because any delay until a specific pin is reached should in general remain the same for each transition.
So, in the context of this here PR, what happens if you don't do the cyclesToGo compensation you are implementing, but instead of
wave->nextServiceCycle = now + wave->nextTimeHighCycles;
use
wave->nextServiceCycle += wave->nextTimeHighCycles;
? (Same for low cycle)
Never mind cycle timings that are too short to run reliably.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@s-hadinger I'm sorry, though mitigated by fix and feature, the IRAM size is actually larger in my PR, from master
IROM *: 228428 - code in flash (default or ICACHE_FLASH_ATTR)
IRAM *: 27400 \ 32768 - code in IRAM (ICACHE_RAM_ATTR, ISRs...)
DATA *: 1252 ) - initialized variables (global, static) in RAM\HEAP
RODATA *: 680 ) \ 81920 - constants (global, static) in RAM\HEAP
BSS *: 25168 ) - zeroed variables (global, static) in RAM\HEAP
to PR #7022
IROM *: 228460 - code in flash (default or ICACHE_FLASH_ATTR)
IRAM *: 27492 \ 32768 - code in IRAM (ICACHE_RAM_ATTR, ISRs...)
DATA *: 1252 ) - initialized variables (global, static) in RAM\HEAP
RODATA *: 680 ) \ 81920 - constants (global, static) in RAM\HEAP
BSS *: 25240 ) - zeroed variables (global, static) in RAM\HEAP
A prominent fix is that 100% duty or off cycle now work without pulsating on the output.
A specified duration now also leaves the output at the state it is at that moment, so no more force to off when stopped during duty cycle. One can always force thereafter via digitalWrite(pin, <state>)
, after all. One might call this a breaking change, I consider it a bug fix.
Harder to fix without further increasing RAM usage is maintaining phase when duty cycle changes, but period not: (duty_before+off_before) == (duty_new + off_new). Is this something you consider a requirement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@s-hadinger I think this PR should not be merged, on the grounds that
min_u32(nextEventCycles, min_u32(wave->nextTimeHighCycles + cyclesToGo, 1));
is near 100% of the time 1, but otherwise always 0.
A quick inspection seems to indicate that this causes another forced loop-iteration, executing
} else {
uint32_t deltaCycles = wave->nextServiceCycle - now;
nextEventCycles = min_u32(nextEventCycles, deltaCycles);
}
which more or less revokes the effect of the change in this PR a few CPU cycles later.
So whatever is observed as an improvement is a side-effect of iterating inside the NMI handler.
Am I seriously missing something here headscratch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A prominent fix is that 100% duty or off cycle now work without pulsating on the output.
Err, how would a 100% duty cycle actually get into this section of code, though? The Tone is by definition a 50% cycle and analogWrite checks for 100% duty cycle and just does digitalWrite
s to avoid any CPU use at all.
A specified duration now also leaves the output at the state it is at that moment, so no more force to off when stopped during duty cycle. One can always force thereafter via
digitalWrite(pin, <state>)
, after all. One might call this a breaking change, I consider it a bug fix.
That would be a breaking change. The original code special-cased it and there are probably HW installations out there depending on this behavior. A 3.0 tweak, not a 2.x one.
Harder to fix without further increasing RAM usage is maintaining phase when duty cycle changes, but period not: (duty_before+off_before) == (duty_new + off_new). Is this something you consider a requirement?
The API specifies a frequency and has no guarantees about phase relationships. You can't know what phases there are, in general, since the app code is running async to the timer...
min_u32(nextEventCycles, min_u32(wave->nextTimeHighCycles + cyclesToGo, 1));
As @dok-net says, this does always equal 0 or 1. I think you meant 1
to be microsecondsToClockCycles(1)
. However, that still means it hits the NMI at 1us and not only on edges which will eat CPU like mad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I know is that min_u32(nextEventCycles, wave->nextTimeHighCycles + cyclesToGo);
does not work, because can introduce a negative number causing an edge transition to be skipped. So there's maybe a more elegant way to force it to be non-negative.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@s-hadinger a much reworked version is in #7022 now. Changing duty cycle maintains phase if period duration remains the same. Runtime is honored precisely, not only on next incidental timer event. Overall stability, correctness and performance fixes, "works for me" (buzzer, servo, PWM PC fan control). I think that's good for a start.
I don't have measuring equipment to graph timings etc, so basically, my observations that the PR is a major improvement for my use cases is all I can provide in the way of proofing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Err, how would a 100% duty cycle actually get into this section of code, though?
First, in the case of direct use of waveform :-) Second, loss of phase for analogWrite()
ing min or max values seemed unacceptable to me (magnitude: bug), this here PR seems to confirm that sentiment. I think an explicit from-sketch digitalWrite()
to fully stop the PWM generator is correct use.
That would be a breaking change. The original code special-cased it and there are probably HW installations out there depending on this behavior.
Hem. stopWaveform()
in master just removes the pin from signal generator, effectively, asynchronously, leaving the pin in a random state, right? OTOH, startWaveform()
to set a finish time to an already running waveform cancels the generation at an unpredictable time in master, but is well defined in PR #7022 - at least per my objective. In master, this has nasty effects like in PR #7084 whenever the duty cycle is cut short, for instance. If the expectation is that startWaveform()
with a running time is always used on first call for a given inactive pin, and the running time is a multiple of period, then there is no functional change between master and #7022.
Is there a need to stay compatible to broken uses?
@s-hadinger, I'm still not convinced that analogWrite phase relationships need to be guaranteed, but this is an indirect way of getting there. A more direct and more easily guaranteed way would be to go back and use the original concept and install a timer callback at 1/analoghz which sets all analogwrite pins to 1 at t=0 then pulls pins down as appropriate. By construction all analog pins would then go high at exactly the same nanosecond (module GPIO16). That was what @me-no-dev 's original code did. |
@earlephilhower Don't get me wrong, I actually don't think that all PWM should start at the same nanosecond. Ideally it would be better to spread them in time which would stress less the power supply. We could even think of an additional parameter to The only feature needed is that relative phases between PWM channels remain constant in time - which is NOT the case with the current implementation. |
Hi @s-hadinger, I'm engaged in the project WLED and added support for analog (5050) LED strips. We have the same flicker especially at low brightness. I did try really a lot and it definitively has nothing to do with the PSU (neither the power nor the quality). I really like to test your PR #7057 but did not "replace" the arduino core before. I did include
in the platformio.ini
Is this the right way doing it ? However: the effect is kind of "slower" but there is still a slight color variation if you use RGB-only strips and try to emulate a warm white like
and the refresh is programmed to be not faster than every 15 ms. |
Just copy core_esp8266_waveform.cpp in your project. The linker will take your local version over the Arduino lib. What do you mean refresh every 15ms. Are you calling analogWrite every 15ms? We don't refresh in Tasmota unless the value changed. |
Hi @s-hadinger, WLED is a NeoPixel LED driver project and the support for analog LED stripes was added later. I will try the local ...unfortunately this did not have the desired effect for me. |
Hi @s-hadinger, hi @earlephilhower I can confirm: the flicker is gone ! So the desired effect is there. |
@Def3nder Have you checked the complete discussion to this PR? You've also just bought into thrashing TIMER1 at 7 or 8µs interval :-) |
Hi @dok-net, yes, I did - let me say "try to understand" the discussion. Let me do this and then I report back what the effect on analog LEDs is. |
@Def3nder great, thanks. My PR has turned into such a major rewrite that any proof that it doesn't break previous contracts for major deployments, plus provides noticeable improvements in operation and additional features, is a must, for it to go anywhere. My little "Tone sounds better and servos and PWM fans jitter a bit less" may not cut it alone. |
@Def3nder Thanks for reporting. The flicker being gone shows that phases are kept constant while calling analogWrite at sustained frequency. I will not be able to test before next week. |
Hi @s-hadinger, hi @dok-net, hi @earlephilhower, summary: details: the same repeated for PR #7057: Just as a comparison this had been the situation before: YouTube If we just look at the Osziloskope screens, PR #7022 looks more "constant", however this cannot be seen with the eyes only. I need to say that our PWM usage in the project is a kind of special: we update the value every 15ms at 880Hz PWM frequency, because we "grab" the colors from a NeoPixelLibrary for digital LED strips. Now my final questions would be:
|
@Def3nder I suspect that setting the PWM value every 15 ms adds some unnecessary stress to keeping phase constant. Did you consider comparing the pwm value to the previous one and avoiding repeated analogWrite when it's not necessary? |
Hi @s-hadinger, yes - it's definitivly worth doing that. So we considder the actual implementation as a kind of stress-test. The code (PR #7022) does run "live" in one of my WLED ceiling LED strips and the LEDs are switched on it does reboot after about 6 to 12 minutes. I did repeat it like 10 times and every time I switch on the lights it will crash after a short time. The crash is not dependant on the number of channels used for PWM: it crashes even when using the white channel on 50% only. So: PR #7022 is not doing well when analogWrite is call every 15ms. I will do the same test with PR #7057 this evening. |
@Def3nder I've checked the sources over and again, it's highly unlikely that |
Do you have the stack trace of the crash? It would tell us more and whether it's WDT |
Hi @s-hadinger, I started with the other test: PR #7057 with all the other unchanged: this did not crash for hours. When no WLED effect is running we have about 4000 loops per second - If a WDT would trigger the reset because of enabling PR #7022, this would mean that 4 analogWrite (RGB + W channel) would take too long - hmmm - let's see what the dump says. The device currently is in my dauther's room (and she sleeps), so I will grab it tomorrow and plug it to USB to connect the serial monitor with PR #7022. Then I will post a crash dump. By the way: I hooked up my big oscilloscope: this is the unpatched version: YouTube (the two channel constantly change phases) |
Hi @s-hadinger, you were right: the watchdog restarted the ESP:
But the question is: why does he do this only with PR #7022 implemented and in no other cases ? So I do not have any clue how a WDT can be triggered. |
I suppose that the new code takes too long, either in analogWrite or in the interrupt handler. @dok-net what do you think? |
I was going to suggest something to try, based on the same assumption that the timings in my PR cause the loop() to slow down too much. We should definitely start working with and posting MCVEs here, otherwise it's all easily just hearsay. For a quick check, you can, before taking the long route above, check two things. // Firing timer too soon, the NMI occurs before ISR has returned.
// But, must fire timer early to reach deadlines for waveforms.
if (nextTimerCcys < microsecondsToClockCycles(4))
nextTimerCcys = microsecondsToClockCycles(2);
else
nextTimerCcys -= microsecondsToClockCycles(2); and change the minimum trigger rate, like if (nextTimerCcys < microsecondsToClockCycles(11))
nextTimerCcys = microsecondsToClockCycles(8);
else
nextTimerCcys -= microsecondsToClockCycles(3); |
Hi @dok-net, I did not use the whole Arduino Core with included PR#7022 - I only included the files changed in the PR and added them locally. I did the same with PR #7057. This is what I did: Then I did add the three files from PR #7022 to the dependencies subfolder, so PlatformIO will notice and compile the files. Just to be sure I added an include in the main file:
That's all. Comming to your question: WLED does not use any hardware timers, but it uses the NeoPixelBus library. But I just checked this - the NeoPixelBus library has no occurence of I will test your code changes ... EDIT: so here comes the test. First of all: we switched from [email protected].2 to [email protected].3 and the WDT reset is gone with PR #7022 (both versions: without the code change and with it). Differences:
So, the good thing: no WDT reboot when using latest Arduino Core. Like the Tasmota Team we also did include the fixed |
@Def3nder Thank you for the very detailed analysis. Not using platform.io myself, I can't make any sense of their version numbering. |
@dok-net I'm not aware of the thrashing you are mentioning. I removed The latest version has lower IRAM impact, and shouldn't do any thrashing. Did I miss something else? |
@s-hadinger With regard to thrashing, @earlephilhower mentioned this, too:
@Def3nder You have me confused, how can you use PR #7022 and yet, at the same time, say
? Which I consider a bad idea, because now ANY change to core_esp8266_waveform.cpp in core is ignored completely unless manually patched into the local copy. Why not use git, then at least conflicts get reported on merge. Besides, it's not fixed, it thrashes inside the ISR's loop and between IRQs per @earlephilhower's and my analysis. |
@dok-net there is no core patch necessary to override a Arduino core file with PlatformIO. |
Hi @dok-net, as soon as a solution to the PWM phases is implemented in the Arduino Core, we will just remove the local copy of Until then the local version does work well. This is the advantage of using PlatformIO: you can include part of a library locally and the linker will care about prefering local copies. I hope that my test did help somehow and that you all find a good solution for the Arduino library 😃 |
@Def3nder unt @s-hadinger
For your info, if you run the PWM at non-harmonic multiples of (1/(1E-6 * 1023)) then you're missing steps in the PWM range. At low brightness that might look like an obvious intensity change (or flicker) as the duty cycle changes, where more steps would smooth that out. Just for fun, you might try increasing your PWM to 977Hz and see what that does for the flicker with the current git. I haven't pulled s-hadinger's changes yet, but I'm about to do so. I only have a short 2 meter length of RGBW LEDs and no decent demo code to test them with yet so I can't see what you are seeing. I don't know if crystal frequency (the accuracy of 80MHz) makes a difference to the PWM timing, or if it's based solely off of the number of ets_timer clocks. I'd guess it's a fixed divisor. That 977Hz should be a stable number with all of the edges, from testing I've done with 3 different boards. Here's the note from my observation on PR7022:
The variable in that is 1us, 2us, 3us, 4us etc. There are some missing steps in the lowest end of the range no matter the PWM frequency, but it's down in the 1% duty cycle range. The LEDs are effectively off at that level, I'd imagine. |
@Def3nder, @s-hadinger Please let me point out once more that this should and can be easily fixed: @earlephilhower said:
I suggest the effected code fragments be changed to this to keep the ISR from thrashing, and to reduce some obfuscation, which saves us 12 bytes in IRAM cache as a nice side effect: static inline ICACHE_RAM_ATTR int32_t max_32(int32_t a, int32_t b) {
if (a < b) {
return b;
}
return a;
} wave->nextServiceCycle += wave->nextTimeHighCycles;
nextEventCycles = min_u32(nextEventCycles,
max_32(wave->nextTimeHighCycles + cyclesToGo, microsecondsToClockCycles(1))); and wave->nextServiceCycle += wave->nextTimeLowCycles;
nextEventCycles = min_u32(nextEventCycles,
max_32(wave->nextTimeLowCycles + cyclesToGo, microsecondsToClockCycles(1))); respectively. As a side note, the typing of ...Cycles as unsigned is universally wrong, which causes unsigned arithmetic despite a negative cyclesToGo, breaking stuff in unexpected places - which I missed in the first version of this code fragment, too. Probably 1 cycle instead of microseconds would be OK, or 0, the way above just prevents |
@earlephilhower To be fair, I've amended this PR, please review the code fragments above, and if you approve, please ask the PR's author to adopt them. My servo is very (not perfectly) quiet with this. |
@dok-net Thanks a lot, this is indeed a very good improvement. Let me adopt it in this PR. @Tech-TX I agree that 977Hz would be better than 880Hz, to make sure not to skip any step. For LEDs this is less of an issue, because we apply Gamma correction meaning that we don't actually use all steps. Any non-linearity due to skipped steps is actually not visible. |
Code changes suggested by @dok-net
What I see in this latest commit looks OK. Channel-to-channel phase jitter is ~12.6us, and that includes the 0 and 1023 endpoints where it has to stop and restart the PWM. I don't have a 4-channel scope any longer so I can't see it with the equivalent of an RGBW LED strip. Servo jitter looks to be a little better than current master. |
@Tech-TX @s-hadinger I'm inclined to close this in favor of #7192 once my comment there about drift gets addressed. It is my understanding that #7192 is a strict improvement on current master, and that it doesn't introduce any new issue. |
This last change was introduced in the latest Tasmota version. When wifi connection happens, the PWM interrupt handler is given much less CPU windows. The current code tries to keep phase constant sacrificing PWM level. During Wifi connection this sometimes almost shut downs PWM. When the interrupt handler has a delay of more than 25% of the overall cycle, it switches back to previous mode, favoring PWM level rather than keep phase constant. This result in slight dimming. |
@s-hadinger If phase has any meaning in your use case, the latest commits don't make things any better. Correcting the overshoot would have to be proportional, I believe, so for your 25% in one cycle, you'd have to add 25% to the other cycle (duty/idle), no just let the phase drift. But if you keep throwing special handling at the ISR, it will bloat and become slower all the time. |
Sure, this is still a quick and dirty patch to make sure I'm back to the original behavior when wifi connection happens. I'm patiently waiting for the definitive solution from you or Earle. I tested all three solutions and they all had the same flaw during wifi connection. Agree on the I'm sharing here how Tasmota code has patched PWM, if this helps others. |
@s-hadinger any objection to closing this in deference to #7022 or #7231 ? |
Sure |
Fixes #7054, PWM channels phases can change over time causing visible flickering on LED drivers (Tasmota).
This fix makes sure the PWM pulses are kept in sync, phases constant whatever the delay of the NMI.