From 87a1cd0ea0b0682a600cdb87d554f37a6d59cb06 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Fri, 8 May 2020 17:52:24 +0200 Subject: [PATCH] Change PWM updated to the latest version of Arduino PR #7231 --- tasmota/CHANGELOG.md | 1 + tasmota/core_esp8266_waveform.cpp | 366 ++++++++++++------------ tasmota/core_esp8266_wiring_digital.cpp | 4 +- tasmota/core_esp8266_wiring_pwm.cpp | 34 +-- 4 files changed, 198 insertions(+), 207 deletions(-) diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index ebfd0e3aec66..5a35b9b6d6ba 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -10,6 +10,7 @@ - Change HAss discovery by Federico Leoni (#8370) - Change default PWM Frequency to 977 Hz from 223 Hz - Change minimum PWM Frequency from 100 Hz to 40 Hz +- Change PWM updated to the latest version of Arduino PR #7231 ### 8.2.0.5 20200425 diff --git a/tasmota/core_esp8266_waveform.cpp b/tasmota/core_esp8266_waveform.cpp index c39670686701..1ff0a3687bcc 100644 --- a/tasmota/core_esp8266_waveform.cpp +++ b/tasmota/core_esp8266_waveform.cpp @@ -47,29 +47,23 @@ extern "C" { // Internal-only calls, not for applications -extern void _setPWMPeriodCC(uint32_t cc); +extern void _setPWMFreq(uint32_t freq); extern bool _stopPWM(int pin); -extern bool _setPWM(int pin, uint32_t cc); +extern bool _setPWM(int pin, uint32_t val, uint32_t range); extern int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles); // Maximum delay between IRQs #define MAXIRQUS (10000) -// Set/clear GPIO 0-15 by bitmask -#define SetGPIO(a) do { GPOS = a; } while (0) -#define ClearGPIO(a) do { GPOC = a; } while (0) - // Waveform generator can create tones, PWM, and servos typedef struct { uint32_t nextServiceCycle; // ESP cycle timer when a transition required uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop - uint32_t timeHighCycles; // Currently running waveform period + uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles) uint32_t timeLowCycles; // - uint32_t desiredHighCycles; // Currently running waveform period - uint32_t desiredLowCycles; // - uint32_t gotoTimeHighCycles; // Copied over on the next period to preserve phase - uint32_t gotoTimeLowCycles; // - uint32_t lastEdge; // + uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal + uint32_t desiredLowCycles; // + uint32_t lastEdge; // Cycle when this generator last changed } Waveform; class WVFState { @@ -82,7 +76,7 @@ class WVFState { uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation - int32_t waveformToChange = -1; + uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI uint32_t waveformNewHigh = 0; uint32_t waveformNewLow = 0; @@ -91,8 +85,8 @@ class WVFState { // Optimize the NMI inner loop by keeping track of the min and max GPIO that we // are generating. In the common case (1 PWM) these may be the same pin and // we can avoid looking at the other pins. - int startPin = 0; - int endPin = 0; + uint16_t startPin = 0; + uint16_t endPin = 0; }; static WVFState wvfState; @@ -107,7 +101,7 @@ static WVFState wvfState; static ICACHE_RAM_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); @@ -138,21 +132,22 @@ static ICACHE_RAM_ATTR void forceTimerInterrupt() { constexpr int maxPWMs = 8; // PWM machine state -typedef struct { +typedef 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 uint8_t pin[maxPWMs + 1]; 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 PWMState *pwmUpdate = nullptr; // Set by main code, cleared by ISR -static uint32_t pwmPeriod = microsecondsToClockCycles(1000000UL) / 1000; - +static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / 1000; +// If there are no more scheduled activities, shut down Timer 1. +// Otherwise, do nothing. static ICACHE_RAM_ATTR void disableIdleTimer() { if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) { ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); @@ -162,62 +157,78 @@ static ICACHE_RAM_ATTR void disableIdleTimer() { } } +// Notify the NMI that a new PWM state is available through the mailbox. +// Wait for mailbox to be emptied (either busy or delay() as needed) +static ICACHE_RAM_ATTR void _notifyPWM(PWMState *p, bool idle) { + p->pwmUpdate = nullptr; + pwmState.pwmUpdate = p; + MEMBARRIER(); + forceTimerInterrupt(); + while (pwmState.pwmUpdate) { + if (idle) { + delay(0); + } + MEMBARRIER(); + } +} + +static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); // Called when analogWriteFreq() changed to update the PWM total period -void _setPWMPeriodCC(uint32_t cc) { - if (cc == pwmPeriod) { - return; +void _setPWMFreq(uint32_t freq) { + // Convert frequency into clock cycles + uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; + + // Simple static adjustment to bring period closer to requested due to overhead +#if F_CPU == 80000000 + cc -= microsecondsToClockCycles(2); +#else + cc -= microsecondsToClockCycles(1); +#endif + + if (cc == _pwmPeriod) { + return; // No change } + + _pwmPeriod = cc; + if (pwmState.cnt) { - // Adjust any running ones to the best of our abilities by scaling them - // Used FP math for speed and code size - uint64_t oldCC64p0 = ((uint64_t)pwmPeriod); - uint64_t newCC64p16 = ((uint64_t)cc) << 16; - uint64_t ratio64p16 = (newCC64p16 / oldCC64p0); PWMState p; // The working copy since we can't edit the one in use - p = pwmState; - uint32_t ttl = 0; - for (uint32_t i = 0; i < p.cnt; i++) { - uint64_t val64p16 = ((uint64_t)p.delta[i]) << 16; - uint64_t newVal64p32 = val64p16 * ratio64p16; - p.delta[i] = newVal64p32 >> 32; - ttl += p.delta[i]; + p.cnt = 0; + for (uint32_t i = 0; i < pwmState.cnt; i++) { + auto pin = pwmState.pin[i]; + _addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles); } - p.delta[p.cnt] = cc - ttl; // Final cleanup exactly cc total cycles // Update and wait for mailbox to be emptied - pwmUpdate = &p; - MEMBARRIER(); - forceTimerInterrupt(); - while (pwmUpdate) { - delay(0); - // No mem barrier. The external function call guarantees it's re-read - } + initTimer(); + _notifyPWM(&p, true); + disableIdleTimer(); } - pwmPeriod = cc; } // Helper routine to remove an entry from the state machine -static ICACHE_RAM_ATTR void _removePWMEntry(int pin, PWMState *p) { - uint32_t i; - - // Find the pin to pull out... - for (i = 0; p->pin[i] != pin; i++) { /* no-op */ } - auto delta = p->delta[i]; - - // Add the removed previous pin delta to preserve absolute position - p->delta[i+1] += delta; - - // Move everything back one - for (i++; i <= p->cnt; i++) { - p->pin[i-1] = p->pin[i]; - p->delta[i-1] = p->delta[i]; +// and clean up any marked-off entries +static void _cleanAndRemovePWM(PWMState *p, int pin) { + uint32_t leftover = 0; + uint32_t in, out; + for (in = 0, out = 0; in < p->cnt; in++) { + if ((p->pin[in] != pin) && (p->mask & (1<pin[in]))) { + p->pin[out] = p->pin[in]; + p->delta[out] = p->delta[in] + leftover; + leftover = 0; + out++; + } else { + leftover += p->delta[in]; + p->mask &= ~(1<pin[in]); + } } - // Remove the pin from the active list - p->mask &= ~(1<cnt--; + p->cnt = out; + // Final pin is never used: p->pin[out] = 0xff; + p->delta[out] = p->delta[in] + leftover; } -// Called by analogWrite(0/100%) to disable PWM on a specific pin + +// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%)) ICACHE_RAM_ATTR bool _stopPWM(int pin) { if (!((1<= _pwmPeriod) { + _stopPWM(pin); + digitalWrite(pin, HIGH); + return; } - // And add it to the list, in order - if (p.cnt >= maxPWMs) { - return false; // No space left - } else if (p.cnt == 0) { + + if (p.cnt == 0) { // Starting up from scratch, special case 1st element and PWM period p.pin[0] = pin; p.delta[0] = cc; - p.pin[1] = 0xff; - p.delta[1] = pwmPeriod - cc; - p.cnt = 1; - p.mask = 1<= (int)i; j--) { + p.pin[j + 1] = p.pin[j]; + p.delta[j + 1] = p.delta[j]; + } int off = cc - ttl; // The delta from the last edge to the one we're inserting p.pin[i] = pin; p.delta[i] = off; // Add the delta to this new pin p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant - p.cnt++; - p.mask |= 1<= maxPWMs) { + return false; // No space left + } + + _addPWMtoList(p, pin, val, range); // Set mailbox and wait for ISR to copy it over - pwmUpdate = &p; - MEMBARRIER(); initTimer(); - forceTimerInterrupt(); - while (pwmUpdate) { - delay(0); - } + _notifyPWM(&p, true); + disableIdleTimer(); return true; } @@ -311,22 +343,22 @@ int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t time uint32_t mask = 1<= 0) { + // Make sure no waveform changes are waiting to be applied + while (wvfState.waveformToChange) { delay(0); // Wait for waveform to update // No mem barrier here, the call to a global function implies global state updated } + wvfState.waveformNewHigh = timeHighCycles; + wvfState.waveformNewLow = timeLowCycles; + MEMBARRIER(); + wvfState.waveformToChange = mask; + // The waveform will be updated some time in the future on the next period for the signal } else { // if (!(wvfState.waveformEnabled & mask)) { wave->timeHighCycles = timeHighCycles; + wave->desiredHighCycles = timeHighCycles; wave->timeLowCycles = timeLowCycles; - wave->desiredHighCycles = wave->timeHighCycles; - wave->desiredLowCycles = wave->timeLowCycles; + wave->desiredLowCycles = timeLowCycles; wave->lastEdge = 0; - wave->gotoTimeHighCycles = wave->timeHighCycles; - wave->gotoTimeLowCycles = wave->timeLowCycles; // Actually set the pin high or low in the IRQ service to guarantee times wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1); wvfState.waveformToEnable |= mask; MEMBARRIER(); @@ -355,10 +387,10 @@ void setTimer1Callback(uint32_t (*fn)()) { // Speed critical bits #pragma GCC optimize ("O2") + // Normally would not want two copies like this, but due to different // optimization levels the inline attribute gets lost if we try the // other version. - static inline ICACHE_RAM_ATTR uint32_t GetCycleCountIRQ() { uint32_t ccount; __asm__ __volatile__("rsr %0,ccount":"=a"(ccount)); @@ -380,8 +412,13 @@ int ICACHE_RAM_ATTR stopWaveform(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 - if (wvfState.waveformEnabled & (1UL << pin)) { - wvfState.waveformToDisable = 1UL << pin; + uint32_t mask = 1<> (turbo ? 0 : 1)) #endif -#define ENABLE_ADJUST // Adjust takes 36 bytes -#define ENABLE_FEEDBACK // Feedback costs 68 bytes -#define ENABLE_PWM // PWM takes 160 bytes - -#ifndef ENABLE_ADJUST - #undef adjust - #define adjust(x) (x) -#endif - static ICACHE_RAM_ATTR void timer1Interrupt() { // Flag if the core is at 160 MHz, for use by adjust() @@ -435,19 +463,12 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1; // Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one) wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled); -#ifdef ENABLE_PWM - } else if (!pwmState.cnt && pwmUpdate) { + } else if (!pwmState.cnt && pwmState.pwmUpdate) { // Start up the PWM generator by copying from the mailbox pwmState.cnt = 1; pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0 pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop! // No need for mem barrier here. Global must be written by IRQ exit -#endif - } else if (wvfState.waveformToChange >= 0) { - wvfState.waveform[wvfState.waveformToChange].gotoTimeHighCycles = wvfState.waveformNewHigh; - wvfState.waveform[wvfState.waveformToChange].gotoTimeLowCycles = wvfState.waveformNewLow; - wvfState.waveformToChange = -1; - // No need for memory barrier here. The global has to be written before exit the ISR. } bool done = false; @@ -455,35 +476,32 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { do { nextEventCycles = microsecondsToClockCycles(MAXIRQUS); -#ifdef ENABLE_PWM // PWM state machine implementation if (pwmState.cnt) { - uint32_t now = GetCycleCountIRQ(); - int32_t cyclesToGo = pwmState.nextServiceCycle - now; + int32_t cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); if (cyclesToGo < 0) { if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new - if (pwmUpdate) { - // Do the memory copy from temp to global and clear mailbox - pwmState = *(PWMState*)pwmUpdate; - pwmUpdate = nullptr; - } - GPOS = pwmState.mask; // Set all active pins high - // GPIO16 isn't the same as the others - if (pwmState.mask & (1<<16)) { - GP16O = 1; - } - pwmState.idx = 0; + if (pwmState.pwmUpdate) { + // Do the memory copy from temp to global and clear mailbox + pwmState = *(PWMState*)pwmState.pwmUpdate; + } + GPOS = pwmState.mask; // Set all active pins high + if (pwmState.mask & (1<<16)) { + GP16O = 1; + } + pwmState.idx = 0; } else { - do { - // Drop the pin at this edge - GPOC = 1<expiryCycle - now; if (expiryToGo < 0) { // Done, remove! - wvfState.waveformEnabled &= ~mask; if (i == 16) { GP16O = 0; - } else { - ClearGPIO(mask); - } + } + GPOC = mask; + wvfState.waveformEnabled &= ~mask; continue; } } @@ -528,39 +544,33 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { wvfState.waveformState ^= mask; if (wvfState.waveformState & mask) { if (i == 16) { - GP16O = 1; // GPIO16 write slow as it's RMW - } else { - SetGPIO(mask); + GP16O = 1; } - if (wave->gotoTimeHighCycles) { + GPOS = mask; + + if (wvfState.waveformToChange & mask) { // Copy over next full-cycle timings - wave->timeHighCycles = wave->gotoTimeHighCycles; - wave->desiredHighCycles = wave->gotoTimeHighCycles; - wave->timeLowCycles = wave->gotoTimeLowCycles; - wave->desiredLowCycles = wave->gotoTimeLowCycles; - wave->gotoTimeHighCycles = 0; - } else { -#ifdef ENABLE_FEEDBACK - if (wave->lastEdge) { - desired = wave->desiredLowCycles; - timeToUpdate = &wave->timeLowCycles; - } + wave->timeHighCycles = wvfState.waveformNewHigh; + wave->desiredHighCycles = wvfState.waveformNewHigh; + wave->timeLowCycles = wvfState.waveformNewLow; + wave->desiredLowCycles = wvfState.waveformNewLow; + wave->lastEdge = 0; + wvfState.waveformToChange = 0; + } + if (wave->lastEdge) { + desired = wave->desiredLowCycles; + timeToUpdate = &wave->timeLowCycles; } -#endif nextEdgeCycles = wave->timeHighCycles; } else { if (i == 16) { - GP16O = 0; // GPIO16 write slow as it's RMW - } else { - ClearGPIO(mask); + GP16O = 0; } -#ifdef ENABLE_FEEDBACK + GPOC = mask; desired = wave->desiredHighCycles; timeToUpdate = &wave->timeHighCycles; -#endif nextEdgeCycles = wave->timeLowCycles; } -#ifdef ENABLE_FEEDBACK if (desired) { desired = adjust(desired); int32_t err = desired - (now - wave->lastEdge); @@ -569,7 +579,6 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { *timeToUpdate += err; } } -#endif nextEdgeCycles = adjust(nextEdgeCycles); wave->nextServiceCycle = now + nextEdgeCycles; nextEventCycles = min_u32(nextEventCycles, nextEdgeCycles); @@ -599,7 +608,6 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { // Do it here instead of global function to save time and because we know it's edge-IRQ T1L = nextEventCycles >> (turbo ? 1 : 0); - TEIE |= TEIE1; // Edge int enable } }; diff --git a/tasmota/core_esp8266_wiring_digital.cpp b/tasmota/core_esp8266_wiring_digital.cpp index d3a8fa737661..199fbabd89eb 100644 --- a/tasmota/core_esp8266_wiring_digital.cpp +++ b/tasmota/core_esp8266_wiring_digital.cpp @@ -34,9 +34,9 @@ extern "C" { // Internal-only calls, not for applications -extern void _setPWMPeriodCC(uint32_t cc); +extern void _setPWMFreq(uint32_t freq); extern bool _stopPWM(int pin); -extern bool _setPWM(int pin, uint32_t cc); +extern bool _setPWM(int pin, uint32_t val, uint32_t range); extern void resetPins(); volatile uint32_t* const esp8266_gpioToFn[16] PROGMEM = { &GPF0, &GPF1, &GPF2, &GPF3, &GPF4, &GPF5, &GPF6, &GPF7, &GPF8, &GPF9, &GPF10, &GPF11, &GPF12, &GPF13, &GPF14, &GPF15 }; diff --git a/tasmota/core_esp8266_wiring_pwm.cpp b/tasmota/core_esp8266_wiring_pwm.cpp index 984ce92db17a..5189fa8f573a 100644 --- a/tasmota/core_esp8266_wiring_pwm.cpp +++ b/tasmota/core_esp8266_wiring_pwm.cpp @@ -29,13 +29,11 @@ extern "C" { // Internal-only calls, not for applications -extern void _setPWMPeriodCC(uint32_t cc); +extern void _setPWMFreq(uint32_t freq); extern bool _stopPWM(int pin); -extern bool _setPWM(int pin, uint32_t cc); +extern bool _setPWM(int pin, uint32_t val, uint32_t range); -static uint32_t analogMap = 0; static int32_t analogScale = PWMRANGE; -static uint16_t analogFreq = 1000; extern void __analogWriteRange(uint32_t range) { if (range > 0) { @@ -43,17 +41,15 @@ extern void __analogWriteRange(uint32_t range) { } } - extern void __analogWriteFreq(uint32_t freq) { - if (freq < 40) { // Arduino sets a minimum of 100Hz, waiting for them to change this one. - analogFreq = 40; + if (freq < 40) { + freq = 40; } else if (freq > 60000) { - analogFreq = 60000; + freq = 60000; } else { - analogFreq = freq; + freq = freq; } - uint32_t analogPeriod = microsecondsToClockCycles(1000000UL) / analogFreq; - _setPWMPeriodCC(analogPeriod); + _setPWMFreq(freq); } extern void __analogWrite(uint8_t pin, int val) { @@ -61,28 +57,14 @@ extern void __analogWrite(uint8_t pin, int val) { return; } - uint32_t analogPeriod = microsecondsToClockCycles(1000000UL) / analogFreq; - _setPWMPeriodCC(analogPeriod); if (val < 0) { val = 0; } else if (val > analogScale) { val = analogScale; } - analogMap &= ~(1 << pin); - uint32_t high = (analogPeriod * val) / analogScale; - uint32_t low = analogPeriod - high; pinMode(pin, OUTPUT); - if (low == 0) { - _stopPWM(pin); - digitalWrite(pin, HIGH); - } else if (high == 0) { - _stopPWM(pin); - digitalWrite(pin, LOW); - } else { - _setPWM(pin, high); - analogMap |= (1 << pin); - } + _setPWM(pin, val, analogScale); } extern void analogWrite(uint8_t pin, int val) __attribute__((weak, alias("__analogWrite")));