From b89932c64d858dea58229f22bf6ca4b61dcf9cdd Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Thu, 29 Apr 2021 22:30:39 +0200 Subject: [PATCH 01/12] Pin has uint8_t, not int type. --- cores/esp8266/core_esp8266_waveform.h | 7 +++---- cores/esp8266/core_esp8266_waveform_phase.cpp | 5 +++-- cores/esp8266/core_esp8266_waveform_pwm.cpp | 17 ++++++++--------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform.h b/cores/esp8266/core_esp8266_waveform.h index d3d303f99f..040e737bf8 100644 --- a/cores/esp8266/core_esp8266_waveform.h +++ b/cores/esp8266/core_esp8266_waveform.h @@ -110,11 +110,10 @@ int stopWaveform(uint8_t pin); // Make sure the CB function has the IRAM_ATTR decorator. void setTimer1Callback(uint32_t (*fn)()); - // Internal-only calls, not for applications -extern void _setPWMFreq(uint32_t freq); -extern bool _stopPWM(uint8_t pin); -extern bool _setPWM(int pin, uint32_t val, uint32_t range); +void _setPWMFreq(uint32_t freq); +bool _stopPWM(uint8_t pin); +bool _setPWM(uint8_t pin, uint32_t val, uint32_t range); #ifdef __cplusplus } diff --git a/cores/esp8266/core_esp8266_waveform_phase.cpp b/cores/esp8266/core_esp8266_waveform_phase.cpp index 4240ccb3c1..966f4bb546 100644 --- a/cores/esp8266/core_esp8266_waveform_phase.cpp +++ b/cores/esp8266/core_esp8266_waveform_phase.cpp @@ -55,8 +55,8 @@ extern "C" void enablePhaseLockedWaveform (void) // No-op calls to override the PWM implementation extern "C" void _setPWMFreq_weak(uint32_t freq) { (void) freq; } -extern "C" IRAM_ATTR bool _stopPWM_weak(int pin) { (void) pin; return false; } -extern "C" bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { (void) pin; (void) val; (void) range; return false; } +extern "C" IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { (void) pin; return false; } +extern "C" bool _setPWM_weak(uint8_t pin, uint32_t val, uint32_t range) { (void) pin; (void) val; (void) range; return false; } // Timer is 80MHz fixed. 160MHz CPU frequency need scaling. @@ -116,6 +116,7 @@ static IRAM_ATTR void timer1Interrupt(); // Non-speed critical bits #pragma GCC optimize ("Os") +static void initTimer() __attribute__((noinline)); static void initTimer() { timer1_disable(); ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); diff --git a/cores/esp8266/core_esp8266_waveform_pwm.cpp b/cores/esp8266/core_esp8266_waveform_pwm.cpp index f85ccb76aa..f232302b49 100644 --- a/cores/esp8266/core_esp8266_waveform_pwm.cpp +++ b/cores/esp8266/core_esp8266_waveform_pwm.cpp @@ -97,7 +97,7 @@ static WVFState wvfState; static IRAM_ATTR void timer1Interrupt(); static bool timerRunning = false; -static __attribute__((noinline)) void initTimer() { +static void initTimer() { if (!timerRunning) { timer1_disable(); ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); @@ -169,8 +169,7 @@ static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { } } -static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); - +static void _addPWMtoList(PWMState &p, uint8_t pin, uint32_t val, uint32_t range); // Called when analogWriteFreq() changed to update the PWM total period extern void _setPWMFreq_weak(uint32_t freq) __attribute__((weak)); @@ -216,7 +215,7 @@ void _setPWMFreq(uint32_t freq) { // Helper routine to remove an entry from the state machine // and clean up any marked-off entries -static void _cleanAndRemovePWM(PWMState *p, int pin) { +static void _cleanAndRemovePWM(PWMState *p, uint8_t pin) { uint32_t leftover = 0; uint32_t in, out; for (in = 0, out = 0; in < p->cnt; in++) { @@ -265,7 +264,7 @@ IRAM_ATTR bool _stopPWM(uint8_t pin) { return _stopPWM_bound(pin); } -static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range) { +static void _addPWMtoList(PWMState &p, uint8_t pin, uint32_t val, uint32_t range) { // Stash the val and range so we can re-evaluate the fraction // should the user change PWM frequency. This allows us to // give as great a precision as possible. We know by construction @@ -311,8 +310,8 @@ static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range) { } // Called by analogWrite(1...99%) to set the PWM duty in clock cycles -extern bool _setPWM_weak(int pin, uint32_t val, uint32_t range) __attribute__((weak)); -bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { +extern bool _setPWM_weak(uint8_t pin, uint32_t val, uint32_t range) __attribute__((weak)); +bool _setPWM_weak(uint8_t pin, uint32_t val, uint32_t range) { stopWaveform(pin); PWMState p; // Working copy p = pwmState; @@ -342,8 +341,8 @@ bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { return true; } -static bool _setPWM_bound(int pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); -bool _setPWM(int pin, uint32_t val, uint32_t range) { +static bool _setPWM_bound(uint8_t pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); +bool _setPWM(uint8_t pin, uint32_t val, uint32_t range) { return _setPWM_bound(pin, val, range); } From 2776d16836f9356fb0847518219d04c29e47ffc8 Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Thu, 29 Apr 2021 22:51:08 +0200 Subject: [PATCH 02/12] Can't image stopWaveform is called but PWM shall not stop. startWaveform already implied stopping PWM before. --- cores/esp8266/Tone.cpp | 5 ----- cores/esp8266/core_esp8266_waveform_pwm.cpp | 3 +++ cores/esp8266/core_esp8266_wiring_digital.cpp | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/cores/esp8266/Tone.cpp b/cores/esp8266/Tone.cpp index b4dc93c642..601b1df51d 100644 --- a/cores/esp8266/Tone.cpp +++ b/cores/esp8266/Tone.cpp @@ -30,11 +30,6 @@ static void _startTone(uint8_t _pin, uint32_t high, uint32_t low, uint32_t durat return; } - // Stop any analogWrites (PWM) because they are a different generator - _stopPWM(_pin); - // If there's another Tone or startWaveform on this pin - // it will be changed on-the-fly (no need to stop it) - pinMode(_pin, OUTPUT); high = std::max(high, (uint32_t)microsecondsToClockCycles(25)); // new 20KHz maximum tone frequency, diff --git a/cores/esp8266/core_esp8266_waveform_pwm.cpp b/cores/esp8266/core_esp8266_waveform_pwm.cpp index f232302b49..a2664f98bf 100644 --- a/cores/esp8266/core_esp8266_waveform_pwm.cpp +++ b/cores/esp8266/core_esp8266_waveform_pwm.cpp @@ -435,6 +435,9 @@ IRAM_ATTR int stopWaveform_weak(uint8_t pin) { if (!timerRunning) { return false; } + + _stopPWM(pin); // Make sure there's no PWM live here + // If user sends in a pin >16 but <32, this will always point to a 0 bit // If they send >=32, then the shift will result in 0 and it will also return false uint32_t mask = 1<<pin; diff --git a/cores/esp8266/core_esp8266_wiring_digital.cpp b/cores/esp8266/core_esp8266_wiring_digital.cpp index 6b42c3ee36..015c21bdbf 100644 --- a/cores/esp8266/core_esp8266_wiring_digital.cpp +++ b/cores/esp8266/core_esp8266_wiring_digital.cpp @@ -83,7 +83,6 @@ extern void __pinMode(uint8_t pin, uint8_t mode) { extern void IRAM_ATTR __digitalWrite(uint8_t pin, uint8_t val) { stopWaveform(pin); // Disable any Tone or startWaveform on this pin - _stopPWM(pin); // and any analogWrites (PWM) if(pin < 16){ if(val) GPOS = (1 << pin); else GPOC = (1 << pin); From 98537bd150194d88c2140fb2dd51086810d4a11b Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Sun, 2 May 2021 11:50:10 +0200 Subject: [PATCH 03/12] let analogWrite coexist with open-drain mode --- cores/esp8266/core_esp8266_wiring_digital.cpp | 2 +- cores/esp8266/core_esp8266_wiring_pwm.cpp | 26 ++++++++++++------- libraries/Servo/src/Servo.cpp | 3 +-- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/cores/esp8266/core_esp8266_wiring_digital.cpp b/cores/esp8266/core_esp8266_wiring_digital.cpp index 015c21bdbf..b04310dc74 100644 --- a/cores/esp8266/core_esp8266_wiring_digital.cpp +++ b/cores/esp8266/core_esp8266_wiring_digital.cpp @@ -82,7 +82,7 @@ extern void __pinMode(uint8_t pin, uint8_t mode) { } extern void IRAM_ATTR __digitalWrite(uint8_t pin, uint8_t val) { - stopWaveform(pin); // Disable any Tone or startWaveform on this pin + stopWaveform(pin); // Disable any Tone, startWaveform, or analogWrite on this pin if(pin < 16){ if(val) GPOS = (1 << pin); else GPOC = (1 << pin); diff --git a/cores/esp8266/core_esp8266_wiring_pwm.cpp b/cores/esp8266/core_esp8266_wiring_pwm.cpp index 5bd5416d70..8ab0f296a1 100644 --- a/cores/esp8266/core_esp8266_wiring_pwm.cpp +++ b/cores/esp8266/core_esp8266_wiring_pwm.cpp @@ -44,7 +44,14 @@ extern void __analogWriteFreq(uint32_t freq) { } extern void __analogWrite(uint8_t pin, int val) { - analogWriteMode(pin, val, false); + if (pin > 16) { + return; + } + bool openDrain = false; + if (analogMap & 1UL << pin) { + openDrain = GPC(pin) & (1 << GPCD); + } + analogWriteMode(pin, val, openDrain); } extern void __analogWriteMode(uint8_t pin, int val, bool openDrain) { @@ -59,23 +66,22 @@ extern void __analogWriteMode(uint8_t pin, int val, bool openDrain) { } if (analogMap & 1UL << pin) { - // Per the Arduino docs at https://www.arduino.cc/reference/en/language/functions/analog-io/analogwrite/ - // val: the duty cycle: between 0 (always off) and 255 (always on). - // So if val = 0 we have digitalWrite(LOW), if we have val==range we have digitalWrite(HIGH) - analogMap &= ~(1 << pin); + const bool isOpenDrain = GPC(pin) & (1 << GPCD); + if (isOpenDrain != openDrain) { + GPC(pin) ^= (1 << GPCD); + } } else { - if(openDrain) { - pinMode(pin, OUTPUT_OPEN_DRAIN); - } else { - pinMode(pin, OUTPUT); - } + pinMode(pin, openDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } uint32_t high = (analogPeriod * val) / analogScale; uint32_t low = analogPeriod - high; // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) int phaseReference = __builtin_ffs(analogMap) - 1; + // Per the Arduino docs at https://www.arduino.cc/reference/en/language/functions/analog-io/analogwrite/ + // val: the duty cycle: between 0 (always off) and 255 (always on). + // So if val = 0 we have digitalWrite(LOW), if we have val==range we have digitalWrite(HIGH) if (_setPWM(pin, val, analogScale)) { analogMap |= (1 << pin); } else if (startWaveformClockCycles(pin, high, low, 0, phaseReference, 0, true)) { diff --git a/libraries/Servo/src/Servo.cpp b/libraries/Servo/src/Servo.cpp index 1d19f62683..f5c410083b 100644 --- a/libraries/Servo/src/Servo.cpp +++ b/libraries/Servo/src/Servo.cpp @@ -116,8 +116,7 @@ void Servo::writeMicroseconds(int value) _servoMap &= ~(1 << _pin); // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) int phaseReference = __builtin_ffs(_servoMap) - 1; - if (startWaveform(_pin, _valueUs, REFRESH_INTERVAL - _valueUs, 0, phaseReference)) - { + if (startWaveform(_pin, _valueUs, REFRESH_INTERVAL - _valueUs, 0, phaseReference)) { _servoMap |= (1 << _pin); } } From 36b2bd3dd248cb2a011b83ace0784297603d41a1 Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Sun, 2 May 2021 12:40:33 +0200 Subject: [PATCH 04/12] Don't force timer on PWM update as this is pulled by ISR, but only implemented on next PWM period begin anyway. --- cores/esp8266/core_esp8266_waveform_pwm.cpp | 45 +++++++++++---------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform_pwm.cpp b/cores/esp8266/core_esp8266_waveform_pwm.cpp index a2664f98bf..3402159ee9 100644 --- a/cores/esp8266/core_esp8266_waveform_pwm.cpp +++ b/cores/esp8266/core_esp8266_waveform_pwm.cpp @@ -128,7 +128,7 @@ static IRAM_ATTR void forceTimerInterrupt() { constexpr int maxPWMs = 8; // PWM machine state -typedef struct PWMState { +struct PWMState { uint32_t mask; // Bitmask of active pins uint32_t cnt; // How many entries uint32_t idx; // Where the state machine is along the list @@ -136,13 +136,12 @@ typedef struct PWMState { uint32_t delta[maxPWMs + 1]; uint32_t nextServiceCycle; // Clock cycle for next step struct PWMState *pwmUpdate; // Set by main code, cleared by ISR -} PWMState; +}; static PWMState pwmState; static uint32_t _pwmFreq = 1000; static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / _pwmFreq; - // If there are no more scheduled activities, shut down Timer 1. // Otherwise, do nothing. static IRAM_ATTR void disableIdleTimer() { @@ -158,6 +157,7 @@ static IRAM_ATTR void disableIdleTimer() { // Wait for mailbox to be emptied (either busy or delay() as needed) static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { p->pwmUpdate = nullptr; + MEMBARRIER(); pwmState.pwmUpdate = p; MEMBARRIER(); forceTimerInterrupt(); @@ -242,8 +242,7 @@ IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { return false; // Pin not actually active } - PWMState p; // The working copy since we can't edit the one in use - p = pwmState; + PWMState p = pwmState; // The working copy since we can't edit the one in use // In _stopPWM we just clear the mask but keep everything else // untouched to save IRAM. The main startPWM will handle cleanup. @@ -264,7 +263,7 @@ IRAM_ATTR bool _stopPWM(uint8_t pin) { return _stopPWM_bound(pin); } -static void _addPWMtoList(PWMState &p, uint8_t pin, uint32_t val, uint32_t range) { +static void _addPWMtoList(PWMState& p, uint8_t pin, uint32_t val, uint32_t range) { // Stash the val and range so we can re-evaluate the fraction // should the user change PWM frequency. This allows us to // give as great a precision as possible. We know by construction @@ -278,7 +277,8 @@ static void _addPWMtoList(PWMState &p, uint8_t pin, uint32_t val, uint32_t range // Clip to sane values in the case we go from OK to not-OK when adjusting frequencies if (cc == 0) { cc = 1; - } else if (cc >= _pwmPeriod) { + } + else if (cc >= _pwmPeriod) { cc = _pwmPeriod - 1; } @@ -286,9 +286,10 @@ static void _addPWMtoList(PWMState &p, uint8_t pin, uint32_t val, uint32_t range // Starting up from scratch, special case 1st element and PWM period p.pin[0] = pin; p.delta[0] = cc; - // Final pin is never used: p.pin[1] = 0xff; + // Final pin is never used: p.pin[1] = 0xff; p.delta[1] = _pwmPeriod - cc; - } else { + } + else { uint32_t ttl = 0; uint32_t i; // Skip along until we're at the spot to insert @@ -532,39 +533,39 @@ static IRAM_ATTR void timer1Interrupt() { // PWM state machine implementation if (pwmState.cnt) { - int32_t cyclesToGo; + int32_t pwmCyclesToGo; do { - cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); - if (cyclesToGo < 0) { + pwmCyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); + if (pwmCyclesToGo <= 0) { if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new if (pwmState.pwmUpdate) { // Do the memory copy from temp to global and clear mailbox - pwmState = *(PWMState*)pwmState.pwmUpdate; + pwmState = *pwmState.pwmUpdate; } GPOS = pwmState.mask; // Set all active pins high if (pwmState.mask & (1<<16)) { GP16O = 1; } pwmState.idx = 0; - } else { + } + else { do { // Drop the pin at this edge - if (pwmState.mask & (1<<pwmState.pin[pwmState.idx])) { - GPOC = 1<<pwmState.pin[pwmState.idx]; - if (pwmState.pin[pwmState.idx] == 16) { - GP16O = 0; - } + GPOC = 1<<pwmState.pin[pwmState.idx]; + if (pwmState.pin[pwmState.idx] == 16) { + GP16O = 0; } pwmState.idx++; // Any other pins at this same PWM value will have delta==0, drop them too. } while (pwmState.delta[pwmState.idx] == 0); } // Preserve duty cycle over PWM period by using now+xxx instead of += delta - cyclesToGo = adjust(pwmState.delta[pwmState.idx]); - pwmState.nextServiceCycle = GetCycleCountIRQ() + cyclesToGo; + pwmCyclesToGo = adjust(pwmState.delta[pwmState.idx]); + pwmState.nextServiceCycle = GetCycleCountIRQ() + pwmCyclesToGo; } nextEventCycle = earliest(nextEventCycle, pwmState.nextServiceCycle); - } while (pwmState.cnt && (cyclesToGo < 100)); + // PWM can starve the generic waveform generator if pwmCyclesToGo remains below 100 + } while (pwmState.cnt && pwmCyclesToGo < 100); } for (auto i = wvfState.startPin; i <= wvfState.endPin; i++) { From ff0782c405fc950588200f93d1a8f50cae5043cf Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Sun, 2 May 2021 15:27:35 +0200 Subject: [PATCH 05/12] Correct shutdown sequence for phase-sync waveform generator in analogWrite. --- cores/esp8266/core_esp8266_wiring_pwm.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cores/esp8266/core_esp8266_wiring_pwm.cpp b/cores/esp8266/core_esp8266_wiring_pwm.cpp index 8ab0f296a1..5fc6e026e8 100644 --- a/cores/esp8266/core_esp8266_wiring_pwm.cpp +++ b/cores/esp8266/core_esp8266_wiring_pwm.cpp @@ -83,9 +83,18 @@ extern void __analogWriteMode(uint8_t pin, int val, bool openDrain) { // val: the duty cycle: between 0 (always off) and 255 (always on). // So if val = 0 we have digitalWrite(LOW), if we have val==range we have digitalWrite(HIGH) if (_setPWM(pin, val, analogScale)) { - analogMap |= (1 << pin); - } else if (startWaveformClockCycles(pin, high, low, 0, phaseReference, 0, true)) { - analogMap |= (1 << pin); + if (val > 0 && val < analogScale) analogMap |= (1 << pin); + } else { + const bool detach = (val == 0 || val == analogScale); + // To go steady LOW or HIGH, let the waveform run into next duty cycle, if any. Then stop. + if (startWaveformClockCycles(pin, high, low, static_cast<uint32_t>(detach), phaseReference, 0, true)) { + if (detach) { + delay((1000 + analogFreq) / analogFreq); + stopWaveform(pin); + } else { + analogMap |= (1 << pin); + } + } } } From 293f767b4ca6b0b3bbbe08defdd7ddf07282e127 Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Mon, 3 May 2021 10:49:59 +0200 Subject: [PATCH 06/12] Correction to IRQ latency and ISR delta logic. --- cores/esp8266/core_esp8266_waveform_phase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cores/esp8266/core_esp8266_waveform_phase.cpp b/cores/esp8266/core_esp8266_waveform_phase.cpp index 966f4bb546..1dd4a98643 100644 --- a/cores/esp8266/core_esp8266_waveform_phase.cpp +++ b/cores/esp8266/core_esp8266_waveform_phase.cpp @@ -309,7 +309,7 @@ static IRAM_ATTR void timer1Interrupt() { uint32_t now = ESP.getCycleCount(); uint32_t isrNextEventCcy = now; while (busyPins) { - if (static_cast<int32_t>(isrNextEventCcy - now) > IRQLATENCYCCYS) { + if (static_cast<int32_t>(isrNextEventCcy - now) > IRQLATENCYCCYS + DELTAIRQCCYS) { waveform.nextEventCcy = isrNextEventCcy; break; } From 50ded373f982119b7190c8b81ace55ba0e65ecdb Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Mon, 3 May 2021 11:20:29 +0200 Subject: [PATCH 07/12] Early firing the ISR causes interruption to other waveforms, let it pick up new starts at next regular interval. --- cores/esp8266/core_esp8266_waveform_phase.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform_phase.cpp b/cores/esp8266/core_esp8266_waveform_phase.cpp index 1dd4a98643..3e3dae89f0 100644 --- a/cores/esp8266/core_esp8266_waveform_phase.cpp +++ b/cores/esp8266/core_esp8266_waveform_phase.cpp @@ -195,10 +195,7 @@ int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCc if (!waveform.timer1Running) { initTimer(); } - else if (T1V > IRQLATENCYCCYS) { - // Must not interfere if Timer is due shortly - timer1_write(IRQLATENCYCCYS); - } + // The ISR pulls updates on next PWM interval } else { wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI From f17ba7fb39ce364c400bca0d4e6f62eac99cf8a3 Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Mon, 3 May 2021 15:20:20 +0200 Subject: [PATCH 08/12] Stopping of the same pin always wins over starting. --- cores/esp8266/core_esp8266_waveform_phase.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform_phase.cpp b/cores/esp8266/core_esp8266_waveform_phase.cpp index 3e3dae89f0..c56bacabdf 100644 --- a/cores/esp8266/core_esp8266_waveform_phase.cpp +++ b/cores/esp8266/core_esp8266_waveform_phase.cpp @@ -78,7 +78,7 @@ constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ? enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, INIT = 3}; // Waveform generator can create tones, PWM, and servos -typedef struct { +struct Waveform { uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. If WaveformMode::INIT, temporarily holds positive phase offset ccy count 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 @@ -88,7 +88,7 @@ typedef struct { WaveformMode mode; 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 -} Waveform; +}; namespace { @@ -116,7 +116,6 @@ static IRAM_ATTR void timer1Interrupt(); // Non-speed critical bits #pragma GCC optimize ("Os") -static void initTimer() __attribute__((noinline)); static void initTimer() { timer1_disable(); ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); @@ -126,7 +125,7 @@ static void initTimer() { timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste } -static void IRAM_ATTR deinitTimer() { +static IRAM_ATTR void deinitTimer() { ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); timer1_disable(); timer1_isr_init(); @@ -195,7 +194,7 @@ int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCc if (!waveform.timer1Running) { initTimer(); } - // The ISR pulls updates on next PWM interval + // The ISR pulls updates on the next waveform interval } else { wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI @@ -265,7 +264,7 @@ static IRAM_ATTR void timer1Interrupt() { const bool isCPU2X = CPU2X & 1; if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) { // Handle enable/disable requests from main app. - waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off + waveform.enabled = (waveform.enabled | waveform.toSetBits) & ~waveform.toDisableBits; // Set the requested waveforms on/off // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) waveform.toDisableBits = 0; } From 5de25bde5e3a947bc6f0f275ba6725451f5f8ef8 Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Wed, 19 May 2021 10:17:54 +0200 Subject: [PATCH 09/12] Clean code. --- cores/esp8266/core_esp8266_wiring_pwm.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cores/esp8266/core_esp8266_wiring_pwm.cpp b/cores/esp8266/core_esp8266_wiring_pwm.cpp index 5fc6e026e8..07eac4e907 100644 --- a/cores/esp8266/core_esp8266_wiring_pwm.cpp +++ b/cores/esp8266/core_esp8266_wiring_pwm.cpp @@ -83,7 +83,9 @@ extern void __analogWriteMode(uint8_t pin, int val, bool openDrain) { // val: the duty cycle: between 0 (always off) and 255 (always on). // So if val = 0 we have digitalWrite(LOW), if we have val==range we have digitalWrite(HIGH) if (_setPWM(pin, val, analogScale)) { - if (val > 0 && val < analogScale) analogMap |= (1 << pin); + if (val > 0 && val < analogScale) { + analogMap |= (1 << pin); + } } else { const bool detach = (val == 0 || val == analogScale); // To go steady LOW or HIGH, let the waveform run into next duty cycle, if any. Then stop. From 6dbd371a0324b160b81353a5d3af9f2db793d854 Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Mon, 31 May 2021 17:57:42 +0200 Subject: [PATCH 10/12] Prevent initial bad clock drift calculation on timer init. --- cores/esp8266/core_esp8266_waveform_phase.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cores/esp8266/core_esp8266_waveform_phase.cpp b/cores/esp8266/core_esp8266_waveform_phase.cpp index c56bacabdf..9d200fa56c 100644 --- a/cores/esp8266/core_esp8266_waveform_phase.cpp +++ b/cores/esp8266/core_esp8266_waveform_phase.cpp @@ -122,6 +122,7 @@ static void initTimer() { ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); waveform.timer1Running = true; + waveform.nextEventCcy = ESP.getCycleCount() + IRQLATENCYCCYS; timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste } @@ -229,6 +230,7 @@ IRAM_ATTR int stopWaveform_weak(uint8_t pin) { std::atomic_thread_fence(std::memory_order_release); // Must not interfere if Timer is due shortly if (T1V > IRQLATENCYCCYS) { + waveform.nextEventCcy = ESP.getCycleCount() + IRQLATENCYCCYS; timer1_write(IRQLATENCYCCYS); } while (waveform.toDisableBits) { From d8893b09c886c089e5bc7c6b8fb366c66c7fd2a2 Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Sun, 6 Jun 2021 11:05:35 +0200 Subject: [PATCH 11/12] stopWaveform in _setPWM caused unconditional _stopPWM. --- cores/esp8266/core_esp8266_waveform_pwm.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform_pwm.cpp b/cores/esp8266/core_esp8266_waveform_pwm.cpp index 3402159ee9..5fd3925703 100644 --- a/cores/esp8266/core_esp8266_waveform_pwm.cpp +++ b/cores/esp8266/core_esp8266_waveform_pwm.cpp @@ -160,7 +160,9 @@ static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { MEMBARRIER(); pwmState.pwmUpdate = p; MEMBARRIER(); - forceTimerInterrupt(); + if (idle) { + forceTimerInterrupt(); + } while (pwmState.pwmUpdate) { if (idle) { esp_yield(); @@ -313,7 +315,10 @@ static void _addPWMtoList(PWMState& p, uint8_t pin, uint32_t val, uint32_t range // Called by analogWrite(1...99%) to set the PWM duty in clock cycles extern bool _setPWM_weak(uint8_t pin, uint32_t val, uint32_t range) __attribute__((weak)); bool _setPWM_weak(uint8_t pin, uint32_t val, uint32_t range) { - stopWaveform(pin); + const uint32_t mask = 1<<pin; + if (wvfState.waveformEnabled & mask) { + stopWaveform(pin); + } PWMState p; // Working copy p = pwmState; // Get rid of any entries for this pin @@ -368,7 +373,7 @@ int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t _stopPWM(pin); // Make sure there's no PWM live here - uint32_t mask = 1<<pin; + const uint32_t mask = 1<<pin; MEMBARRIER(); if (wvfState.waveformEnabled & mask) { // Make sure no waveform changes are waiting to be applied @@ -441,7 +446,7 @@ IRAM_ATTR int stopWaveform_weak(uint8_t pin) { // If user sends in a pin >16 but <32, this will always point to a 0 bit // If they send >=32, then the shift will result in 0 and it will also return false - uint32_t mask = 1<<pin; + const uint32_t mask = 1<<pin; if (wvfState.waveformEnabled & mask) { wvfState.waveformToDisable = mask; // Cancel any pending updates for this waveform, too. @@ -569,7 +574,7 @@ static IRAM_ATTR void timer1Interrupt() { } for (auto i = wvfState.startPin; i <= wvfState.endPin; i++) { - uint32_t mask = 1<<i; + const uint32_t mask = 1<<i; // If it's not on, ignore! if (!(wvfState.waveformEnabled & mask)) { From 9c612a9c86fa59ddca4f19e70dcefdc0893153ff Mon Sep 17 00:00:00 2001 From: "Dirk O. Kaar" <dok@dok-net.net> Date: Sat, 10 Dec 2022 23:25:54 +0100 Subject: [PATCH 12/12] Prevent inlining of `initTimer()` --- cores/esp8266/core_esp8266_waveform_pwm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cores/esp8266/core_esp8266_waveform_pwm.cpp b/cores/esp8266/core_esp8266_waveform_pwm.cpp index 5fd3925703..6c591366f5 100644 --- a/cores/esp8266/core_esp8266_waveform_pwm.cpp +++ b/cores/esp8266/core_esp8266_waveform_pwm.cpp @@ -97,7 +97,7 @@ static WVFState wvfState; static IRAM_ATTR void timer1Interrupt(); static bool timerRunning = false; -static void initTimer() { +static __attribute__((noinline)) void initTimer() { if (!timerRunning) { timer1_disable(); ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);