10000 Re-implement PWM generator logic by earlephilhower · Pull Request #7231 · esp8266/Arduino · GitHub
[go: up one dir, main page]

Skip to content

Re-implement PWM generator logic #7231

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

Merged
merged 68 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
2c010d6
Re-implement PWM generator logic
earlephilhower Apr 19, 2020
122ca23
Merge branch 'master' into realpwm
earlephilhower Apr 19, 2020
118103b
Adjust running PWM when analogWriteFreq changed
earlephilhower Apr 19, 2020
5cd145e
Also preserve phase of running tone/waveforms
earlephilhower Apr 19, 2020
4ce623d
Clean up signed/unsigned mismatch, 160MHz operat'n
earlephilhower Apr 19, 2020
3a6f6c1
Turn off PWM on a Tone or digitalWrite
earlephilhower Apr 20, 2020
27ee6f8
Remove hump due to fixed IRQ delta
earlephilhower Apr 21, 2020
5faf6be
Speed PWM generator by reordering data struct
earlephilhower Apr 22, 2020
94af195
Remove if() that could never evaluate TRUE
earlephilhower Apr 24, 2020
1182cd0
Add error feedback to waveform generation
earlephilhower Apr 27, 2020
7ee7d19
Move _stopPWM and _removePWMEntry to IRAM
earlephilhower Apr 27, 2020
c12e961
Avoid long wait times when PWM freq is low
earlephilhower Apr 27, 2020
143b6ae
Merge branch 'master' into realpwm
earlephilhower Apr 28, 2020
a783621
Fix bug where tone/pwm could happen on same pin
earlephilhower Apr 28, 2020
ad3076c
Adjust for random 160MHZ operation
earlephilhower Apr 28, 2020
dc32961
Clean up leftover debugs in ISR
earlephilhower Apr 28, 2020
ec62ee3
Subtract constant-time overhead for PWM, add 60khz
earlephilhower Apr 28, 2020
a41890b
Fix GPIO16 not toggling properly.
earlephilhower Apr 28, 2020
42bbede
Remove constant offset to PWM period
earlephilhower Apr 29, 2020
79a7f7d
Remove volatiles, replace with explicit membarrier
earlephilhower Apr 29, 2020
cbff7a3
Consolidate data into single structure
earlephilhower Apr 29, 2020
4196bf8
Factor out common timer shutdown code
earlephilhower Apr 29, 2020
14f4416
Remove unneeded extra copy on PWM start
earlephilhower Apr 29, 2020
2002a5d
Factor out common edge work in waveform loop
earlephilhower Apr 29, 2020
5b1f288
Factor out waveform phase feedback loop math
earlephilhower Apr 29, 2020
f42696c
Reduce PWM size by using 32b count, indexes
earlephilhower Apr 29, 2020
2b0dde4
GP16O is a 1-bit register, just write to it
earlephilhower Apr 29, 2020
b0e818f
Merge branch 'master' into realpwm
earlephilhower Apr 29, 2020
dfaa9ce
Increase PWM linearity in low/high regions
earlephilhower May 3, 2020
3909ada
Remove redundant GetCycleCount (non-IRQ)
earlephilhower May 3, 2020
413cd17
Factor out common timer setup operations
earlephilhower May 3, 2020
07f5ff1
Fix clean-waveform transition, lock to tone faster
earlephilhower May 3, 2020
539d0d4
Reduce code size ~145 bytes
earlephilhower May 4, 2020
df51b21
Reduce IRAM by pushing more work to _setPWM
earlephilhower May 4, 2020
867b181
Fix typo in PWM pin 1->0 transition
earlephilhower May 5, 2020
f757778
Combine cleanup and pin remove, save 50 bytes IROM
earlephilhower May 5, 2020
9424090
Remove unused analogMap, toneMap
earlephilhower May 5, 2020
c8b53ef
Save IRAM/heap by adjusting WVF update struct
earlephilhower May 5, 2020
e6b7aa1
Don't duplicate PWM period calculation
earlephilhower May 5, 2020
28645ff
Factor out common PWM update code
earlephilhower May 5, 2020
3ee638c
Merge branch 'master' into realpwm
earlephilhower May 5, 2020
174d19e
Clean up old comments
earlephilhower May 5, 2020
a910eae
Fix indent, remove some unneeded if-else branches
earlephilhower May 5, 2020
179b9d6
Fix regression when analogWrite done cold
earlephilhower May 5, 2020
e44171f
Save 16b of IRAM by not re-setting edge intr bit
earlephilhower May 6, 2020
7fe9a2d
Allow on-the-fly PWM frequency changes
earlephilhower May 7, 2020
e7cb533
Adjust for fixed overhead on PWM period
earlephilhower May 8, 2020
083560d
Fix value reversal when analogWrite out of range
earlephilhower May 8, 2020
e421d81
Merge branch 'master' into realpwm
earlephilhower May 9, 2020
5be4961
8000 Don't optimize the satopWaveform call
earlephilhower May 10, 2020
051008a
Avoid side effects in addPWMtoList
earlephilhower May 13, 2020
6692418
Adjust PWM period as fcn of # of PWM pins
earlephilhower May 18, 2020
606c5cd
Merge branch 'master' into realpwm
earlephilhower Jun 4, 2020
524f047
Fix occasional Tone artifacts
earlephilhower Jun 6, 2020
361d4a2
Reduce CPU usage and enhance low range PWM output
earlephilhower Jun 6, 2020
975fe12
Merge branch 'master' into realpwm
earlephilhower Jun 7, 2020
9e48706
Update min IRQ time to remove humps in PWM linearity
earlephilhower Jun 7, 2020
565f21f
Remove minor bump at high PWM frequencies
earlephilhower Jun 7, 2020
272dc9d
Undo the 160->80 frequency adjust
earlephilhower Jun 8, 2020
8f9af5d
Merge branch 'master' into realpwm
earlephilhower Jul 13, 2020
2961933
Merge branch 'master' into realpwm
earlephilhower Aug 23, 2020
e5afab0
Merge branch 'master' into realpwm
earlephilhower Aug 29, 2020
e5ba217
Update core_esp8266_wiring_pwm.cpp
earlephilhower Aug 29, 2020
f911754
Update core_esp8266_wiring_pwm.cpp
earlephilhower Aug 29, 2020
1b34278
Merge branch 'master' into realpwm
d-a-v Oct 26, 2020
55e8abb
Merge branch 'master' of https://github.com/esp8266/Arduino into realpwm
earlephilhower Nov 20, 2020
4cc3d8a
Fix Servo shutdown changes which caused trouble with Servo::detach()
earlephilhower Nov 20, 2020
a353909
Servo shutdown tweak in PWM path
earlephilhower Nov 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Re-implement PWM generator logic
Add special-purpose PWM logic to preserve alignment of PWM signals for
things like RGB LEDs.

Keep a sorted list of GPIO changes in memory.  At time 0 of the PWM
cycle, set all pins to high.  As time progresses bring down the
additional pins as their duty cycle runs out.  This way all PWM signals
are time aligned by construction.

This also reduces the number of PWM interrupts by up to 50%.  Before,
both the rising and falling edge of a PWM pin required an interrupt (and
could shift arround accordingly).  Now, a single IRQ sets all PWM rising
edges (so 1 no matter how many PWM pins) and individual interrupts
generate the falling edges.

The code favors duty cycle accuracy over PWM period accuracy (since PWM
is simulating an analog voltage it's the %age of time high that's the
critical factor in most apps, not the refresh rate).  Measurements give
it about 35% less total error over full range at 20khz than master.

@me-no-dev used something very similar in the original PWM generator.
  • Loading branch information
earlephilhower committed Apr 19, 2020
commit 2c010d69c2dd174bf7fcbb7cbca0380af47e879f
180 changes: 175 additions & 5 deletions cores/esp8266/core_esp8266_waveform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
#include <Arduino.h>
#include "ets_sys.h"
#include "core_esp8266_waveform.h"

#include "user_interface.h"
extern "C" {

// Maximum delay between IRQs
Expand Down Expand Up @@ -68,7 +68,6 @@ static volatile uint32_t waveformToDisable = 0; // Message to the NMI handler to

static uint32_t (*timer1CB)() = NULL;


// Non-speed critical bits
#pragma GCC optimize ("Os")

Expand Down Expand Up @@ -108,6 +107,133 @@ void setTimer1Callback(uint32_t (*fn)()) {
}
}

// PWM implementation using special purpose state machine
//
// Keep an ordered list of pins with the delta in cycles between each
// element, with a terminal entry making up the remainder of the PWM
// period. With this method sum(all deltas) == PWM period clock cycles.
//
// At t=0 set all pins high and set the timeout for the 1st edge.
// On interrupt, if we're at the last element reset to t=0 state
// Otherwise, clear that pin down and set delay for next element
// and so forth.

constexpr int maxPWMs = 8;

// PWM edge definition
typedef struct {
unsigned int pin : 8;
unsigned int delta : 24;
} PWMEntry;

// PWM machine state
typedef struct {
uint32_t mask; // Bitmask of active pins
uint8_t cnt; // How many entries
uint8_t idx; // Where the state machine is along the list
PWMEntry edge[maxPWMs + 1]; // Include space for terminal element
uint32_t nextServiceCycle; // Clock cycle for next step
} PWMState;

static PWMState pwmState;
static volatile PWMState *pwmUpdate = nullptr; // Set by main code, cleared by ISR
static int pwmPeriod = (1000000L * system_get_cpu_freq()) / 1000;

// Called when analogWriteFreq() changed to update the PWM total period
void _setPWMPeriodCC(int cc) {
pwmPeriod = cc;
// TODO - should we drop all waveforms?
}

// Helper routine to remove an entry from the state machine
static void _removePWMEntry(int pin, PWMState *p) {
if (!((1<<pin) & p->mask)) {
return;
}

int delta = 0;
int i;
for (i=0; i < p->cnt; i++) {
if (p->edge[i].pin == pin) {
delta = p->edge[i].delta;
break;
}
}
// Add the removed previous pin delta to preserve absolute position
p->edge[i+1].delta += delta;
// Move everything back one and clean up
for (i++; i <= p->cnt; i++) {
p->edge[i-1] = p->edge[i];
}
p->mask &= ~(1<<pin);
p->cnt--;
}

// Called by analogWrite(0/100%) to disable PWM on a specific pin
bool _stopPWM(int pin) {
if (!((1<<pin) & pwmState.mask)) {
return false; // Pin not actually active
}

PWMState p; // The working copy since we can't edit the one in use
p = pwmState;
_removePWMEntry(pin, &p);
// Update and wait for mailbox to be emptied
pwmUpdate = &p;
while (pwmUpdate) {
delay(0);
}
// Possibly shut doen the timer completely if we're done
if (!waveformEnabled && !pwmState.cnt && !timer1CB) {
deinitTimer();
}
return true;
}

// Called by analogWrite(1...99%) to set the PWM duty in clock cycles
bool _setPWM(int pin, int cc) {
PWMState p; // Working copy
p = pwmState;
// Get rid of any entries for this pin
_removePWMEntry(pin, &p);
// And add it to the list, in order
if (p.cnt >= maxPWMs) {
return false; // No space left
} else if (p.cnt == 0) {
// Starting up from scratch, special case 1st element and PWM period
p.edge[0].pin = pin;
p.edge[0].delta = cc;
p.edge[1].pin = 0xff;
p.edge[1].delta = pwmPeriod - cc;
p.cnt = 1;
p.mask = 1<<pin;
} else {
int ttl=0;
int i;
// Skip along until we're at the spot to insert
for (i=0; (i <= p.cnt) && (ttl + p.edge[i].delta < cc); i++) {
ttl += p.edge[i].delta;
}
// Shift everything out by one to make space for new edge
memmove(&p.edge[i + 1], &p.edge[i], (1 + p.cnt - i) * sizeof(p.edge[0]));
int off = cc - ttl; // The delta from the last edge to the one we're inserting
p.edge[i].pin = pin;
p.edge[i].delta = off; // Add the delta to this new pin
p.edge[i + 1].delta -= off; // And subtract it from the follower to keep sum(deltas) constant
p.cnt++;
p.mask |= 1<<pin;
}
// Set mailbox and wait for ISR to copy it over
pwmUpdate = &p;
if (!timerRunning) {
initTimer();
timer1_write(microsecondsToClockCycles(10));
}
while (pwmUpdate) { delay(0); }
return true;
}


// Start up a waveform on a pin, or change the current one. Will change to the new
// waveform smoothly on next low->high transition. For immediate change, stopWaveform()
// first, then it will immediately begin.
Expand Down Expand Up @@ -189,7 +315,7 @@ int ICACHE_RAM_ATTR stopWaveform(uint8_t pin) {
while (waveformToDisable) {
/* no-op */ // Can't delay() since stopWaveform may be called from an IRQ
}
if (!waveformEnabled && !timer1CB) {
if (!waveformEnabled && !pwmState.cnt && !timer1CB) {
deinitTimer();
}
return true;
Expand All @@ -216,7 +342,7 @@ static ICACHE_RAM_ATTR void timer1Interrupt() {
uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14);

if (waveformToEnable || waveformToDisable) {
// Handle enable/disable requests from main app.
// Handle enable/disable requests from main app
waveformEnabled = (waveformEnabled & ~waveformToDisable) | waveformToEnable; // Set the requested waveforms on/off
waveformState &= ~waveformToEnable; // And clear the state of any just started
waveformToEnable = 0;
Expand All @@ -225,12 +351,56 @@ static ICACHE_RAM_ATTR void timer1Interrupt() {
startPin = __builtin_ffs(waveformEnabled) - 1;
// Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one)
endPin = 32 - __builtin_clz(waveformEnabled);
} else if (!pwmState.cnt && pwmUpdate) {
// Start up the PWM generator by copying from the mailbox
pwmState = *(PWMState*)pwmUpdate;
pwmUpdate = nullptr;
pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop!
pwmState.idx = pwmState.cnt; // Cause it to start at t=0
}

bool done = false;
if (waveformEnabled) {
if (waveformEnabled || pwmState.cnt) {
do {
nextEventCycles = microsecondsToClockCycles(MAXIRQUS);

// PWM state machine implementation
if (pwmState.cnt) {
uint32_t now = GetCycleCountIRQ();
int32_t cyclesToGo = pwmState.nextServiceCycle - now;
if (cyclesToGo <= 10) {
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 & 0x100) {
GP16O |= 1;
}
pwmState.idx = 0;
} else {
do {
// Drop the pin at this edge
GPOC = 1<<pwmState.edge[pwmState.idx].pin;
// GPIO16 still needs manual work
if (pwmState.edge[pwmState.idx].pin == 16) {
GP16O &= ~1;
}
pwmState.idx++;
// Any other pins at this same PWM value will have delta==0, drop them too.
} while (pwmState.edge[pwmState.idx].delta == 0);
}
// Preserve duty cycle over PWM period by using now+xxx instead of += delta
pwmState.nextServiceCycle = now + pwmState.edge[pwmState.idx].delta;
cyclesToGo = pwmState.nextServiceCycle - now;
if (cyclesToGo<0) cyclesToGo=0;
}
nextEventCycles = min_u32(nextEventCycles, cyclesToGo);
3372 }

for (int i = startPin; i <= endPin; i++) {
uint32_t mask = 1<<i;

Expand Down
7 changes: 7 additions & 0 deletions cores/esp8266/core_esp8266_waveform.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ int stopWaveform(uint8_t pin);
// Make sure the CB function has the ICACHE_RAM_ATTR decorator.
void setTimer1Callback(uint32_t (*fn)());



// Internal-only calls, not for applications
extern void _setPWMPeriodCC(int cc);
extern bool _stopPWM(int pin);
extern bool _setPWM(int pin, int cc);

#ifdef __cplusplus
}
#endif
Expand Down
11 changes: 6 additions & 5 deletions cores/esp8266/core_esp8266_wiring_pwm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ extern void __analogWriteRange(uint32_t range) {
}
}


extern void __analogWriteFreq(uint32_t freq) {
if (freq < 100) {
analogFreq = 100;
Expand All @@ -52,6 +53,7 @@ extern void __analogWrite(uint8_t pin, int val) {
return;
}
uint32_t analogPeriod = (1000000L * system_get_cpu_freq()) / analogFreq;
_setPWMPeriodCC(analogPeriod);
if (val < 0) {
val = 0;
} else if (val > analogScale) {
Expand All @@ -63,15 +65,14 @@ extern void __analogWrite(uint8_t pin, int val) {
uint32_t low = analogPeriod - high;
pinMode(pin, OUTPUT);
if (low == 0) {
stopWaveform(pin);
_stopPWM(pin);
digitalWrite(pin, HIGH);
} else if (high == 0) {
stopWaveform(pin);
_stopPWM(pin);
digitalWrite(pin, LOW);
} else {
if (startWaveformCycles(pin, high, low, 0)) {
analogMap |= (1 << pin);
}
_setPWM(pin, high);
analogMap |= (1 << pin);
}
}

Expand Down
0