Slightly reduce PWM jankiness

This commit is contained in:
Will Miles 2024-09-28 23:12:03 -04:00
parent cc87b32206
commit fe4b668107
2 changed files with 27 additions and 8 deletions

View File

@ -104,18 +104,20 @@ constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ?
// for INFINITE, the NMI proceeds on the waveform without expiry deadline. // for INFINITE, the NMI proceeds on the waveform without expiry deadline.
// for EXPIRES, the NMI expires the waveform automatically on the expiry ccy. // for EXPIRES, the NMI expires the waveform automatically on the expiry ccy.
// for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES. // for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES.
// for UPDATEPHASE, the NMI recomputes the target timings
// for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY. // for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY.
enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, INIT = 3}; enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, UPDATEPHASE = 3, INIT = 4};
// Waveform generator can create tones, PWM, and servos // Waveform generator can create tones, PWM, and servos
typedef struct { typedef struct {
uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. If WaveformMode::INIT, temporarily holds positive phase offset ccy count uint32_t nextPeriodCcy; // ESP clock cycle when a period begins.
uint32_t endDutyCcy; // ESP clock cycle when going from duty to off uint32_t endDutyCcy; // ESP clock cycle when going from duty to off
int32_t dutyCcys; // Set next off cycle at low->high to maintain phase int32_t dutyCcys; // Set next off cycle at low->high to maintain phase
int32_t adjDutyCcys; // Temporary correction for next period int32_t adjDutyCcys; // Temporary correction for next period
int32_t periodCcys; // Set next phase cycle at low->high to maintain phase int32_t periodCcys; // Set next phase cycle at low->high to maintain phase
uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count
WaveformMode mode; WaveformMode mode;
uint32_t phaseCcy; // positive phase offset ccy count
int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin
bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings
} Waveform; } Waveform;
@ -200,15 +202,15 @@ int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCc
wave.adjDutyCcys = 0; wave.adjDutyCcys = 0;
wave.periodCcys = periodCcys; wave.periodCcys = periodCcys;
wave.autoPwm = autoPwm; wave.autoPwm = autoPwm;
wave.alignPhase = (alignPhase < 0) ? -1 : alignPhase;
wave.phaseCcy = phaseOffsetCcys;
std::atomic_thread_fence(std::memory_order_acquire); std::atomic_thread_fence(std::memory_order_acquire);
const uint32_t pinBit = 1UL << pin; const uint32_t pinBit = 1UL << pin;
if (!(waveform.enabled & pinBit)) { if (!(waveform.enabled & pinBit)) {
// wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR // wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR
wave.nextPeriodCcy = phaseOffsetCcys;
wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count
wave.mode = WaveformMode::INIT; wave.mode = WaveformMode::INIT;
wave.alignPhase = (alignPhase < 0) ? -1 : alignPhase;
if (!wave.dutyCcys) { if (!wave.dutyCcys) {
// If initially at zero duty cycle, force GPIO off // If initially at zero duty cycle, force GPIO off
if (pin == 16) { if (pin == 16) {
@ -232,11 +234,16 @@ int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCc
else { else {
wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI
std::atomic_thread_fence(std::memory_order_release); std::atomic_thread_fence(std::memory_order_release);
wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count
if (runTimeCcys) { if (runTimeCcys) {
wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count
wave.mode = WaveformMode::UPDATEEXPIRY; wave.mode = WaveformMode::UPDATEEXPIRY;
std::atomic_thread_fence(std::memory_order_release); std::atomic_thread_fence(std::memory_order_release);
waveform.toSetBits = 1UL << pin; waveform.toSetBits = 1UL << pin;
} else if (alignPhase) {
// @willmmiles new feature
wave.mode = WaveformMode::UPDATEPHASE; // recalculate start
std::atomic_thread_fence(std::memory_order_release);
waveform.toSetBits = 1UL << pin;
} }
} }
std::atomic_thread_fence(std::memory_order_acq_rel); std::atomic_thread_fence(std::memory_order_acq_rel);
@ -292,12 +299,13 @@ static inline IRAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X
} }
static IRAM_ATTR void timer1Interrupt() { static IRAM_ATTR void timer1Interrupt() {
const uint32_t isrStartCcy = ESP.getCycleCount();
int32_t clockDrift = isrStartCcy - waveform.nextEventCcy;
// ----- @willmmiles begin patch ----- // ----- @willmmiles begin patch -----
nmiCrashWorkaround(); nmiCrashWorkaround();
// ----- @willmmiles end patch ----- // ----- @willmmiles end patch -----
const uint32_t isrStartCcy = ESP.getCycleCount();
int32_t clockDrift = isrStartCcy - waveform.nextEventCcy;
const bool isCPU2X = CPU2X & 1; const bool isCPU2X = CPU2X & 1;
if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) { if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) {
// Handle enable/disable requests from main app. // Handle enable/disable requests from main app.
@ -328,6 +336,15 @@ static IRAM_ATTR void timer1Interrupt() {
wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X); wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X);
wave.mode = WaveformMode::EXPIRES; wave.mode = WaveformMode::EXPIRES;
break; break;
// @willmmiles new feature
case WaveformMode::UPDATEPHASE:
// in WaveformMode::UPDATEPHASE, we recalculate the targets without adjusting the state
if (wave.alignPhase >= 0 && waveform.enabled & (1UL << wave.alignPhase)) {
auto& align_wave = waveform.pins[wave.alignPhase];
// Go back one cycle
wave.nextPeriodCcy = align_wave.nextPeriodCcy - scaleCcys(align_wave.periodCcys, isCPU2X) + scaleCcys(wave.phaseCcy, isCPU2X);
wave.endDutyCcy = wave.nextPeriodCcy + scaleCcys(wave.dutyCcys, isCPU2X);
}
default: default:
break; break;
} }
@ -355,11 +372,13 @@ static IRAM_ATTR void timer1Interrupt() {
Waveform& wave = waveform.pins[pin]; Waveform& wave = waveform.pins[pin];
/* @willmmiles - wtf? We don't want to accumulate drift
if (clockDrift) { if (clockDrift) {
wave.endDutyCcy += clockDrift; wave.endDutyCcy += clockDrift;
wave.nextPeriodCcy += clockDrift; wave.nextPeriodCcy += clockDrift;
wave.expiryCcy += clockDrift; wave.expiryCcy += clockDrift;
} }
*/
uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy; uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy;
if (WaveformMode::EXPIRES == wave.mode && if (WaveformMode::EXPIRES == wave.mode &&

View File

@ -612,7 +612,7 @@ void BusPwm::show() {
if (_reversed) duty = maxBri - duty; if (_reversed) duty = maxBri - duty;
#ifdef ESP8266 #ifdef ESP8266
stopWaveform(_pins[i]); //stopWaveform(_pins[i]); // can cause the waveform to miss a cycle. instead we risk crossovers.
startWaveformClockCycles(_pins[i], duty, analogPeriod - duty, 0, i ? _pins[0] : -1, hPoint, false); startWaveformClockCycles(_pins[i], duty, analogPeriod - duty, 0, i ? _pins[0] : -1, hPoint, false);
#else #else
unsigned channel = _ledcStart + i; unsigned channel = _ledcStart + i;