From d48b3baa28e3db02414214ed20d1ae4d595f36d5 Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Fri, 6 Apr 2018 18:38:18 -0700 Subject: [PATCH 1/6] Move to shared TIMER1 infrastructure, add Steppers Remove and rewrite all the parts of the core/libraries using TIMER1 and consolidate into a single, shared waveform generation interrupt structure. Tone, analogWrite(), Servo all now just call into this shared resource to perform their tasks. This setup enables multiple tones, analogWrites, servos, and stepper motors to be controlled with reasonable accuracy. It uses both TIMER1 and the internal ESP cycle counter to handle timing of waveform edges. TIMER1 is used in non-reload mode and only edges cause interrupts. The interrupt is started and stopped as required, minimizing overhead when these features are not being used. Adds in control of DRV8825 or A4988 interfaces stepper motors to the timer infrastructure. The interrupt handles acceleration and velocity of stepper pulses including jerk (dA/dt) calculations, but it is up to the user application to do path planning and generate S-curve accelerations. All stepper DIR pins are connected together, and then the STEP from each stepper interface board is routed to an individual pin. For motions which involve multiple steppers, there is an option to wait before beginning the next stepper motion until all active steppers have completed their moves (i.e. moving to an X, Y where there may be 1-2us delay between the X arrival and Y arrival). A generic "startWaveform(pin, high-US, low-US, runtime-US)" and "stopWaveform(pin)" allow for further types of interfaces. Using a simple USB DSO shows that with only a single channel running, a "tone(1000)" generates a waveform of 999.8Hz (500.1us per high/low). Running additional generators or steppers will potentially increase the jitter. The implementation also is limited in the time of the shortest pulse it can generate (mostly an issue for analogWrite at values <1% or >99% of the scale). Presently measurements of ~2-3us are seen on the shortest of pulses. There is no optimization to allow generating two edges on a single interrupt, which leads to this minimum pulse width. The current implementation was written focusing on readability and flexibility, not squeezing every cycle out of interrupt loops. There are probably many places where the code could be optimized (after it's fully validated!) to reduce CPU load and timing errors. --- cores/esp8266/Tone.cpp | 119 +---- cores/esp8266/base64.cpp | 0 cores/esp8266/base64.h | 0 cores/esp8266/core_esp8266_waveform.c | 557 ++++++++++++++++++++ cores/esp8266/core_esp8266_waveform.h | 77 +++ cores/esp8266/core_esp8266_wiring_digital.c | 9 +- cores/esp8266/core_esp8266_wiring_pwm.c | 238 ++------- libraries/Servo/src/Servo.cpp | 129 +++++ libraries/Servo/src/Servo.h | 24 +- libraries/Servo/src/esp8266/Servo.cpp | 308 ----------- libraries/Servo/src/esp8266/ServoTimers.h | 225 -------- 11 files changed, 840 insertions(+), 846 deletions(-) mode change 100755 => 100644 cores/esp8266/base64.cpp mode change 100755 => 100644 cores/esp8266/base64.h create mode 100644 cores/esp8266/core_esp8266_waveform.c create mode 100644 cores/esp8266/core_esp8266_waveform.h create mode 100644 libraries/Servo/src/Servo.cpp delete mode 100644 libraries/Servo/src/esp8266/Servo.cpp delete mode 100644 libraries/Servo/src/esp8266/ServoTimers.h diff --git a/cores/esp8266/Tone.cpp b/cores/esp8266/Tone.cpp index fb7837ee46..95a291ff45 100644 --- a/cores/esp8266/Tone.cpp +++ b/cores/esp8266/Tone.cpp @@ -3,7 +3,7 @@ A Tone Generator Library for the ESP8266 - Copyright (c) 2016 Ben Pirt. All rights reserved. + Original Copyright (c) 2016 Ben Pirt. All rights reserved. This file is part of the esp8266 core for Arduino environment. This library is free software; you can redistribute it and/or @@ -22,115 +22,36 @@ */ #include "Arduino.h" -#include "pins_arduino.h" +#include "core_esp8266_waveform.h" -#define AVAILABLE_TONE_PINS 1 -const uint8_t tone_timers[] = { 1 }; -static uint8_t tone_pins[AVAILABLE_TONE_PINS] = { 255, }; -static long toggle_counts[AVAILABLE_TONE_PINS] = { 0, }; -#define T1INDEX 0 +// Which pins have a tone running on them? +static uint32_t _toneMap = 0; -void t1IntHandler(); - -static int8_t toneBegin(uint8_t _pin) { - int8_t _index = -1; - - // if we're already using the pin, reuse it. - for (int i = 0; i < AVAILABLE_TONE_PINS; i++) { - if (tone_pins[i] == _pin) { - return i; - } +void tone(uint8_t _pin, unsigned int frequency, unsigned long duration) { + if (_pin > 16) { + return; } - // search for an unused timer. - for (int i = 0; i < AVAILABLE_TONE_PINS; i++) { - if (tone_pins[i] == 255) { - tone_pins[i] = _pin; - _index = i; - break; - } + if (frequency == 0) { + noTone(_pin); + return; } - return _index; -} - -// frequency (in hertz) and duration (in milliseconds). -void tone(uint8_t _pin, unsigned int frequency, unsigned long duration) { - int8_t _index; - - _index = toneBegin(_pin); - - if (_index >= 0) { - // Set the pinMode as OUTPUT - pinMode(_pin, OUTPUT); - - // Alternate handling of zero freqency to avoid divide by zero errors - if (frequency == 0) - { - noTone(_pin); - return; - } - - // Calculate the toggle count - if (duration > 0) { - toggle_counts[_index] = 2 * frequency * duration / 1000; - } else { - toggle_counts[_index] = -1; - } - - // set up the interrupt frequency - switch (tone_timers[_index]) { - case 0: - // Not currently supported - break; - - case 1: - timer1_disable(); - timer1_isr_init(); - timer1_attachInterrupt(t1IntHandler); - timer1_enable(TIM_DIV1, TIM_EDGE, TIM_LOOP); - timer1_write((clockCyclesPerMicrosecond() * 500000) / frequency); - break; - } + uint32_t halfCycle = 500000L / frequency; + if (halfCycle < 100) { + halfCycle = 100; } -} - -void disableTimer(uint8_t _index) { - tone_pins[_index] = 255; - switch (tone_timers[_index]) { - case 0: - // Not currently supported - break; - - case 1: - timer1_disable(); - break; + if (startWaveform(_pin, halfCycle, halfCycle, (uint32_t) duration * 1000)) { + _toneMap |= 1 << _pin; } } void noTone(uint8_t _pin) { - for (int i = 0; i < AVAILABLE_TONE_PINS; i++) { - if (tone_pins[i] == _pin) { - tone_pins[i] = 255; - disableTimer(i); - break; - } - } - digitalWrite(_pin, LOW); -} - -ICACHE_RAM_ATTR void t1IntHandler() { - if (toggle_counts[T1INDEX] != 0){ - // toggle the pin - digitalWrite(tone_pins[T1INDEX], toggle_counts[T1INDEX] % 2); - toggle_counts[T1INDEX]--; - // handle the case of indefinite duration - if (toggle_counts[T1INDEX] < -2){ - toggle_counts[T1INDEX] = -1; - } - }else{ - disableTimer(T1INDEX); - digitalWrite(tone_pins[T1INDEX], LOW); + if (_pin > 16) { + return; } + stopWaveform(_pin); + _toneMap &= ~(1 << _pin); + digitalWrite(_pin, 0); } diff --git a/cores/esp8266/base64.cpp b/cores/esp8266/base64.cpp old mode 100755 new mode 100644 diff --git a/cores/esp8266/base64.h b/cores/esp8266/base64.h old mode 100755 new mode 100644 diff --git a/cores/esp8266/core_esp8266_waveform.c b/cores/esp8266/core_esp8266_waveform.c new file mode 100644 index 0000000000..389837b4aa --- /dev/null +++ b/cores/esp8266/core_esp8266_waveform.c @@ -0,0 +1,557 @@ +/* + esp8266_waveform - General purpose waveform generation and stepper motor + control, supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + + The code idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds). TIMER1 is set to 1-shot + mode and is always loaded with the time until the next edge of any live + waveforms or Stepper motors. + + Up to one waveform generator or stepper driver per pin supported. + + Each waveform generator is synchronized to the ESP cycle counter, not the + timer. This allows for removing interrupt jitter and delay as the counter + always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next low->high waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + The stepper driver supports a constant jerk (da/dt, in ticks/sec^3) to + produce a smooth acceleration curve and as well as a constant initial + acceleration and velocity and number of pulses. + + The stepper driver can also force all steppers to wait for completion + by the use of a SYNC option (i.e. when completing a move where the X and + Y need to hit at one point before moving to the next X and Y). + + The user application is responsible for actually calculating the proper + motion profiles (a general-purpose S-curve planner is left as an exercise + for the reader). + + The steppers should be wired using an A4988 or DRV8825 controller and + with a single, shared direction pin. + + Everywhere in the code where "cycles" is used, it means ESP.getCycleTime() + cycles, not TIMER1 cycles (which may be 2 CPU clocks @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include +#include "core_esp8266_waveform.h" + +// Map the IRQ stuff to standard terminology +#define cli() ets_intr_lock() +#define sei() ets_intr_unlock() + +// Maximum delay between IRQs +#define MAXIRQUS (10000) + +// If the cycles from now to an event are below this value, perform it anyway since IRQs take longer than this +#define CYCLES_FLUFF (200) + +// Macro to get count of predefined array elements +#define countof(a) ((size_t)(sizeof(a)/sizeof(a[0]))) + +// Set/clear *any* GPIO +#define SetGPIOPin(a) do { if (a < 16) { GPOS |= (1<high transition + unsigned gpioPin : 4; // Check gpioPin16 first + unsigned nextTimeHighCycles : 28; + unsigned gpioPin16 : 1; // Special case for weird IO16 + unsigned state : 1; + unsigned enabled : 1; + unsigned nextTimeLowCycles : 28; +} Waveform; + +// These can be accessed in interrupts, so ensure to bracket access with SEI/CLI +static Waveform waveform[] = { + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // GPIO0 + {0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, // GPIO1 + {0, 0, 0, 0, 2, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 3, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 4, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 5, 0, 0, 0, 0, 0}, + // GPIOS 6-11 not allowed, used for flash + {0, 0, 0, 0, 12, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 13, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 14, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 15, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 1, 0, 0, 0} +}; // GPIO16 + + +// Maximum umber of moves per stepper queue +#define STEPPERQUEUESIZE 16 + +// Stepper generator can send # of steps with given velocity and linear acceleration +// Can do any piecewise linear acceleration profile +typedef struct { + float j_2; // Jerk value/2, pulses/sec/sec/sec + float a0; // Initial acceleration, pulses/sec/sec + float v0; // Initial velocity, pulses/sec + unsigned pulses : 16; // Total # of pulses to emit + unsigned sync : 1; // Wait for all channels to have a sync before popping next move + unsigned dir : 1; // CW=0 or CCW=1 +} Motion; + +// Minimum clock cycles per step. +#define MINSTEPCYCLES (4000) +// Maxmimum clock cycles per step. +#define MAXSTEPCYCLES (1000000000) + +// Pre-allocated circular buffer +typedef struct { + Motion * move; + uint32_t nextEventCycles; + + // Copied from head for fast access + uint16_t pulses; + uint32_t cumCycles; + float j_2; + float a0; + float v0; + unsigned sync : 1; + unsigned dir : 1; + + unsigned gpioPin : 4; + unsigned finished : 1; + + uint8_t readPtr; + uint8_t writePtr; + uint8_t validEntries; +} StepperQueue; + +static volatile StepperQueue *stepQ = NULL; +static volatile uint8_t stepQCnt = 0; +static uint8_t stepDirPin = 16; // The weird one + +// Helper functions +static inline ICACHE_RAM_ATTR uint32_t MicrosecondsToCycles(uint32_t microseconds) { + return clockCyclesPerMicrosecond() * microseconds; +} + +static inline ICACHE_RAM_ATTR uint32_t min_u32(uint32_t a, uint32_t b) { + if (a < b) { + return a; + } + return b; +} + +static inline ICACHE_RAM_ATTR void ReloadTimer(uint32_t a) { + timer1_write(a); +} + +static inline ICACHE_RAM_ATTR uint32_t GetCycleCount() { + uint32_t ccount; + __asm__ __volatile__("esync; rsr %0,ccount":"=a"(ccount)); + return ccount; +} + +// Interrupt on/off control +static ICACHE_RAM_ATTR void timer1Interrupt(); +static uint8_t timerRunning = false; +static uint32_t lastCycleCount = 0; // Last ESP cycle counter on running the interrupt routine + +static void initTimer() { + timer1_disable(); + timer1_isr_init(); + timer1_attachInterrupt(timer1Interrupt); + lastCycleCount = GetCycleCount(); + timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); + timerRunning = true; +} + +static void deinitTimer() { + timer1_attachInterrupt(NULL); + timer1_disable(); + timer1_isr_init(); + timerRunning = false; +} + +// Called by the IRQ to move the next Motion to the head +static inline ICACHE_RAM_ATTR void PopStepper(int i) { + StepperQueue *q = (StepperQueue *)&stepQ[i]; + if (q->validEntries == 0) { + q->sync = false; + q->finished = true; + q->nextEventCycles = 0; + return; + } + q->finished = false; + + Motion *head = &q->move[q->readPtr]; + q->pulses = head->pulses; + q->cumCycles = 0; + q->j_2 = head->j_2; + q->a0 = head->a0; + q->v0 = head->v0; + q->sync = head->sync; + q->dir = head->dir; + q->nextEventCycles = 0; // (uint32_t)((clockCyclesPerMicrosecond()*1000000.0) / q->v0); + q->readPtr = (q->readPtr + 1) & (STEPPERQUEUESIZE - 1); + q->validEntries--; +} + +// Called by the user to detach a stepper and free memory +int removeStepper(int pin) { + sei(); + for (int i = 0; i < stepQCnt; i++) { + if (stepQ[i].gpioPin == pin) { + memmove((void*)&stepQ[i], (void*)&stepQ[i + 1], (stepQCnt - i - 1) * sizeof(stepQ[0])); + stepQ = (StepperQueue*)realloc((void*)stepQ, (stepQCnt - 1) * sizeof(stepQ[0])); + stepQCnt--; + cli(); + return true; + } + } + return false; +} + +// Add a stepper move, return TRUE on success, FALSE on out of space +// Calling application needs to ensure IRQS are disabled for the call! +static int PushStepper(int gpioPin, const Motion *nextMove) { + StepperQueue *q = NULL; + int i; + sei(); + // Determine which queue it should be on, or maybe add one if needed + for (i = 0; i < stepQCnt; i++) { + if (stepQ[i].gpioPin == gpioPin) { + q = (StepperQueue *)&stepQ[i]; + break; + } + } + if (q == NULL) { + // Make the stepper move array + Motion *move = (Motion*)malloc(sizeof(stepQ[0].move) * STEPPERQUEUESIZE); + if (!move) { + cli(); + return false; + } + + // Add a queue + StepperQueue *newStepQ = (StepperQueue*)realloc((void*)stepQ, (stepQCnt + 1) * sizeof(stepQ[0])); + if (!newStepQ) { + cli(); + free(move); + return false; + } + stepQ = newStepQ; + q = (StepperQueue*) & (stepQ[stepQCnt]); // The one just added + memset(q, 0, sizeof(*q)); + q->move = move; + q->readPtr = 0; + q->writePtr = 0; + q->validEntries = 0; + q->gpioPin = gpioPin; + q->finished = true; + i = stepQCnt; + stepQCnt++; + } + // Have queue ready, can we fit this new one in? + if (q->validEntries == STEPPERQUEUESIZE) { + return false; + } + + // Store and record + q->move[q->writePtr] = *nextMove; // Copy actual values + q->validEntries++; + q->writePtr = (q->writePtr + 1) & (STEPPERQUEUESIZE - 1); + if (!timerRunning) { + initTimer(); + ReloadTimer(10); // Cause an interrupt post-haste + } + if (!q->sync) { + PopStepper(i); // If there's only this in the queue and we're not waiting for sync, start it up + } + cli(); + return true; +} + +// Called by user to add a PWL move to the queue, returns false if there is no space left +int pushStepperMove(int pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0) { + Motion m; + m.pulses = pulses; + m.j_2 = j / 2.0; + m.a0 = a0; + m.v0 = v0; + m.sync = sync ? 1 : 0; + m.dir = dir ? 1 : 0; + return PushStepper(pin, &m); +} + +// Assign a pin to stepper DIR +int setStepperDirPin(int pin) { + if (pin > 16) { + return false; + } + stepDirPin = pin; + 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. +int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS) { + Waveform *wave = NULL; + for (size_t i = 0; i < countof(waveform); i++) { + if (((pin == 16) && waveform[i].gpioPin16) || ((pin != 16) && (waveform[i].gpioPin == pin))) { + wave = (Waveform*) & (waveform[i]); + break; + } + } + if (!wave) { + return false; + } + sei(); + wave->nextTimeHighCycles = MicrosecondsToCycles(timeHighUS); + wave->nextTimeLowCycles = MicrosecondsToCycles(timeLowUS); + wave->timeLeftCycles = MicrosecondsToCycles(runTimeUS); + if (!wave->enabled) { + wave->state = 1; + digitalWrite(pin, 1); + wave->timeHighCycles = MicrosecondsToCycles(timeHighUS); + wave->timeLowCycles = MicrosecondsToCycles(timeLowUS); + wave->nextEventCycles = wave->timeHighCycles; + wave->enabled = 1; + if (!timerRunning) { + initTimer(); + } + ReloadTimer(10); // Cause an interrupt post-haste + } + cli(); + return true; +} + +// Stops a waveform on a pin +int stopWaveform(uint8_t pin) { + for (size_t i = 0; i < countof(waveform); i++) { + if (((pin == 16) && waveform[i].gpioPin16) || ((pin != 16) && (waveform[i].gpioPin == pin))) { + sei(); + waveform[i].enabled = 0; + int cnt = stepQCnt; + for (size_t i = 0; i < countof(waveform); i++) { + cnt += waveform[i].enabled ? 1 : 0; + } + if (!cnt) { + deinitTimer(); + } + cli(); + return true; + } + } + return false; +} + + + +// Send pulses for specific direction. +// Stepper direction pin needs to be set before calling (helps ensure setup time) +static ICACHE_RAM_ATTR void AdvanceSteppers(uint32_t deltaCycles, int dir) { + static uint16_t toClear = 0; // Store last call's pins to allow us to meet hold time by clearing on the processing of the other dir + uint16_t pulseGPIO = 0; + for (size_t i = 0; i < stepQCnt; i++) { + StepperQueue *q = (StepperQueue*)&stepQ[i]; + if (q->dir != dir || q->finished) { + continue; + } + q->cumCycles += deltaCycles; + uint32_t newNextEventCycles = q->nextEventCycles - deltaCycles; + if ((deltaCycles >= q->nextEventCycles) || (newNextEventCycles <= CYCLES_FLUFF)) { + // If there are no more pulses in the current motion, try to pop next one here + if (!q->pulses) { + if (!q->sync) { + PopStepper(i); + if (!q->pulses) { + // We tried to pop, but there's nothing left, done! + continue; + } + // We will generate the first pulse of the next motion later on in this loop + } else { + // Sync won't allow us to advance here. The main loop will have to + // call this loop *again* after all are processed the first time + // if we are sync'd. + q->nextEventCycles = 0; // Don't look at this for timing + continue; + } + } + pulseGPIO |= 1 << q->gpioPin; + q->pulses--; + + // Forgive me for going w/FP. The dynamic range for fixed math would require many 64 bit multiplies + static const float cycPerSec = 1000000.0 * clockCyclesPerMicrosecond(); + static const float secPerCyc = 1.0 / (1000000.0 * clockCyclesPerMicrosecond()); + float t = q->cumCycles * secPerCyc; + float newVel = ((q->j_2 * t) + q->a0) * t + q->v0; + uint32_t newPeriodCycles = (uint32_t)(cycPerSec / newVel); + if (newPeriodCycles < MINSTEPCYCLES) { + newPeriodCycles = MINSTEPCYCLES; + } else if (newPeriodCycles > MAXSTEPCYCLES) { + newPeriodCycles = MAXSTEPCYCLES; + } + q->nextEventCycles = newPeriodCycles; + } else { + q->nextEventCycles = newNextEventCycles; + } + } + ClearGPIO(toClear); + SetGPIO(pulseGPIO); + toClear = pulseGPIO; +} + + +static ICACHE_RAM_ATTR uint32_t ProcessSteppers(uint32_t deltaCycles) { + ClearGPIOPin(stepDirPin); + AdvanceSteppers(deltaCycles, 0); + SetGPIOPin(stepDirPin); + AdvanceSteppers(deltaCycles, 1); + + // Check for sync, and if all set and 0 steps clear it + bool haveSync = true; + bool wantSync = false; + for (int i = 0; i < stepQCnt; i++) { + haveSync &= stepQ[i].sync && stepQ[i].finished; + wantSync |= stepQ[i].sync; + } + + if (wantSync && haveSync) { // Sync requested, and hit + for (int i = 0; i < stepQCnt; i++) { + PopStepper(i); + stepQ[i].nextEventCycles = 1; // Cause the pulse to fire immediately + } + // Hokey, but here only now could we know it was time to fire everyone again + ClearGPIOPin(stepDirPin); + AdvanceSteppers(deltaCycles, 0); + SetGPIOPin(stepDirPin); + AdvanceSteppers(deltaCycles, 1); + } + // When's the next event? + uint32_t nextEventCycles = MicrosecondsToCycles(MAXIRQUS); + for (size_t i = 0; i < stepQCnt; i++) { + if (stepQ[i].nextEventCycles) { + nextEventCycles = min_u32(nextEventCycles, stepQ[i].nextEventCycles); + } + } + + return nextEventCycles; +} + +static ICACHE_RAM_ATTR void timer1Interrupt() { + uint32_t curCycleCount = GetCycleCount(); + uint32_t deltaCycles = curCycleCount - lastCycleCount; + lastCycleCount = curCycleCount; + + for (size_t i = 0; i < countof(waveform); i++) { + Waveform *wave = &waveform[i]; + + // If it's not on, ignore! + if (!wave->enabled) { + continue; + } + + // Check for timed-out waveforms + if (wave->timeLeftCycles) { + uint32_t newTimeLeftCycles = wave->timeLeftCycles - deltaCycles; + // Check for unsigned underflow with new > old + if ((deltaCycles >= wave->timeLeftCycles) || (newTimeLeftCycles <= CYCLES_FLUFF)) { + // Done, remove! + wave->enabled = false; + if (wave->gpioPin16) { + GP16O &= ~1; + } else { + ClearGPIO(1 << wave->gpioPin); + } + } else { + wave->timeLeftCycles = newTimeLeftCycles; + } + } + + // Check for toggles + uint32_t newNextEventCycles = wave->nextEventCycles - deltaCycles; + if ((deltaCycles >= wave->nextEventCycles) || (newNextEventCycles <= CYCLES_FLUFF)) { + wave->state = !wave->state; + if (wave->state) { + if (wave->gpioPin16) { + GP16O |= 1; + } else { + SetGPIO(1 << wave->gpioPin); + } + wave->nextEventCycles = wave->timeHighCycles; + wave->timeHighCycles = wave->nextTimeHighCycles; + } else { + if (wave->gpioPin16) { + GP16O &= ~1; + } else { + ClearGPIO(1 << wave->gpioPin); + } + wave->nextEventCycles = wave->timeLowCycles; + wave->timeLowCycles = wave->nextTimeLowCycles; + } + } else { + wave->nextEventCycles = newNextEventCycles; + } + } + + uint32_t nextEventCycles = MicrosecondsToCycles(MAXIRQUS); + for (size_t i = 0; i < countof(waveform); i++) { + if (waveform[i].enabled) { + nextEventCycles = min_u32(nextEventCycles, waveform[i].nextEventCycles); + } + } + + if (stepQCnt) { + nextEventCycles = min_u32(nextEventCycles, ProcessSteppers(deltaCycles)); + } + + // Adjust back by the time we spent in here + deltaCycles = GetCycleCount() - lastCycleCount; + // Add in IRQ delay, from measurements on idle system + #if F_CPU == 160000000 + deltaCycles += MicrosecondsToCycles(1) + (MicrosecondsToCycles(1) >> 1); + #else + deltaCycles += MicrosecondsToCycles(3); + #endif + if (nextEventCycles > deltaCycles) { + nextEventCycles -= deltaCycles; + } else { + nextEventCycles = CYCLES_FLUFF; + } + + // Keep next call within sane min/max time + if (nextEventCycles < CYCLES_FLUFF) { + nextEventCycles = CYCLES_FLUFF; + } else if (nextEventCycles > MicrosecondsToCycles(MAXIRQUS)) { + nextEventCycles = MicrosecondsToCycles(MAXIRQUS); + } + #if F_CPU == 160000000 + nextEventCycles = nextEventCycles >> 1; + #endif + ReloadTimer(nextEventCycles); +} + diff --git a/cores/esp8266/core_esp8266_waveform.h b/cores/esp8266/core_esp8266_waveform.h new file mode 100644 index 0000000000..9dcd6d4d34 --- /dev/null +++ b/cores/esp8266/core_esp8266_waveform.h @@ -0,0 +1,77 @@ +/* + esp8266_waveform - General purpose waveform generation and stepper motor + control, supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + + The code idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds). TIMER1 is set to 1-shot + mode and is always loaded with the time until the next edge of any live + waveforms or Stepper motors. + + Up to one waveform generator or stepper driver per pin supported. + + Each waveform generator is synchronized to the ESP cycle counter, not the + timer. This allows for removing interrupt jitter and delay as the counter + always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next low->high waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + The stepper driver supports a constant jerk (da/dt, in ticks/sec^3) to + produce a smooth acceleration curve and as well as a constant initial + acceleration and velocity and number of pulses. + + The stepper driver can also force all steppers to wait for completion + by the use of a SYNC option (i.e. when completing a move where the X and + Y need to hit at one point before moving to the next X and Y). + + The user application is responsible for actually calculating the proper + motion profiles (a general-purpose S-curve planner is left as an exercise + for the reader). + + The steppers should be wired using an A4988 or DRV8825 controller and + with a single, shared direction pin. + + Everywhere in the code where "cycles" is used, it means ESP.getCycleTime() + cycles, not TIMER1 cycles (which may be 2 CPU clocks @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include + +#ifndef __ESP8266_WAVEFORM_H +#define __ESP8266_WAVEFORM_H + +#ifdef __cplusplus +extern "C" { +#endif + +int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS); +int stopWaveform(uint8_t pin); + +int setStepperDirPin(int pin); +int pushStepperMove(int pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0); +int removeStepper(int pin); + +#ifdef __cplusplus +} +#endif + + +#endif + diff --git a/cores/esp8266/core_esp8266_wiring_digital.c b/cores/esp8266/core_esp8266_wiring_digital.c index 9d88ba847e..4d4af6325b 100644 --- a/cores/esp8266/core_esp8266_wiring_digital.c +++ b/cores/esp8266/core_esp8266_wiring_digital.c @@ -24,13 +24,12 @@ #include "c_types.h" #include "eagle_soc.h" #include "ets_sys.h" - -extern void pwm_stop_pin(uint8_t pin); +#include "core_esp8266_waveform.h" uint8_t esp8266_gpioToFn[16] = {0x34, 0x18, 0x38, 0x14, 0x3C, 0x40, 0x1C, 0x20, 0x24, 0x28, 0x2C, 0x30, 0x04, 0x08, 0x0C, 0x10}; extern void __pinMode(uint8_t pin, uint8_t mode) { - pwm_stop_pin(pin); + stopWaveform(pin); if(pin < 16){ if(mode == SPECIAL){ GPC(pin) = (GPC(pin) & (0xF << GPCI)); //SOURCE(GPIO) | DRIVER(NORMAL) | INT_TYPE(UNCHANGED) | WAKEUP_ENABLE(DISABLED) @@ -80,7 +79,7 @@ extern void __pinMode(uint8_t pin, uint8_t mode) { } extern void ICACHE_RAM_ATTR __digitalWrite(uint8_t pin, uint8_t val) { - pwm_stop_pin(pin); + stopWaveform(pin); if(pin < 16){ if(val) GPOS = (1 << pin); else GPOC = (1 << pin); @@ -91,7 +90,7 @@ extern void ICACHE_RAM_ATTR __digitalWrite(uint8_t pin, uint8_t val) { } extern int ICACHE_RAM_ATTR __digitalRead(uint8_t pin) { - pwm_stop_pin(pin); + stopWaveform(pin); if(pin < 16){ return GPIP(pin); } else if(pin == 16){ diff --git a/cores/esp8266/core_esp8266_wiring_pwm.c b/cores/esp8266/core_esp8266_wiring_pwm.c index 671eaa4145..153f30ee37 100644 --- a/cores/esp8266/core_esp8266_wiring_pwm.c +++ b/cores/esp8266/core_esp8266_wiring_pwm.c @@ -1,7 +1,9 @@ /* pwm.c - analogWrite implementation for esp8266 - Copyright (c) 2015 Hristo Gochkov. All rights reserved. + Use the shared TIMER1 utilities to generate PWM signals + + Original Copyright (c) 2015 Hristo Gochkov. All rights reserved. This file is part of the esp8266 core for Arduino environment. This library is free software; you can redistribute it and/or @@ -18,204 +20,58 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "wiring_private.h" -#include "pins_arduino.h" -#include "c_types.h" -#include "eagle_soc.h" -#include "ets_sys.h" - -#ifndef F_CPU -#define F_CPU 800000000L -#endif - -struct pwm_isr_table { - uint8_t len; - uint16_t steps[17]; - uint32_t masks[17]; -}; -struct pwm_isr_data { - struct pwm_isr_table tables[2]; - uint8_t active;//0 or 1, which table is active in ISR -}; +#include +#include "core_esp8266_waveform.h" -static struct pwm_isr_data _pwm_isr_data; -uint32_t pwm_mask = 0; -uint16_t pwm_values[17] = {0,}; -uint32_t pwm_freq = 1000; -uint32_t pwm_range = PWMRANGE; +static uint32_t analogMap = 0; +static int32_t analogScale = 255; +static uint16_t analogFreq = 1000; -uint8_t pwm_steps_changed = 0; -uint32_t pwm_multiplier = 0; - -int pwm_sort_array(uint16_t a[], uint16_t al) -{ - uint16_t i, j; - for (i = 1; i < al; i++) { - uint16_t tmp = a[i]; - for (j = i; j >= 1 && tmp < a[j-1]; j--) { - a[j] = a[j-1]; - } - a[j] = tmp; - } - int bl = 1; - for(i = 1; i < al; i++) { - if(a[i] != a[i-1]) { - a[bl++] = a[i]; - } - } - return bl; +extern void __analogWriteRange(uint32_t range) { + if (range > 0) { + analogScale = range; + } } -uint32_t pwm_get_mask(uint16_t value) -{ - uint32_t mask = 0; - int i; - for(i=0; i<17; i++) { - if((pwm_mask & (1 << i)) != 0 && pwm_values[i] == value) { - mask |= (1 << i); - } - } - return mask; +extern void __analogWriteFreq(uint32_t freq) { + if (freq < 100) { + analogFreq = 100; + } else if (freq > 40000) { + analogFreq = 40000; + } else { + analogFreq = freq; + } } -void prep_pwm_steps() -{ - if(pwm_mask == 0) { - return; +extern void __analogWrite(uint8_t pin, int val) { + if (pin >= 16) { + return; + } + uint32_t analogPeriod = 1000000L / analogFreq; + 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; + if (low == 0) { + stopWaveform(pin); + digitalWrite(pin, HIGH); + } else if (high == 0) { + stopWaveform(pin); + digitalWrite(pin, LOW); + } else { + if (startWaveform(pin, high, low, 0)) { + analogMap |= (1 << pin); } - - int pwm_temp_steps_len = 0; - uint16_t pwm_temp_steps[17]; - uint32_t pwm_temp_masks[17]; - uint32_t range = pwm_range; - - if((F_CPU / ESP8266_CLOCK) == 1) { - range /= 2; - } - - int i; - for(i=0; i<17; i++) { - if((pwm_mask & (1 << i)) != 0 && pwm_values[i] != 0) { - pwm_temp_steps[pwm_temp_steps_len++] = pwm_values[i]; - } - } - pwm_temp_steps[pwm_temp_steps_len++] = range; - pwm_temp_steps_len = pwm_sort_array(pwm_temp_steps, pwm_temp_steps_len) - 1; - for(i=0; i0; i--) { - pwm_temp_steps[i] = pwm_temp_steps[i] - pwm_temp_steps[i-1]; - } - - pwm_steps_changed = 0; - struct pwm_isr_table *table = &(_pwm_isr_data.tables[!_pwm_isr_data.active]); - table->len = pwm_temp_steps_len; - ets_memcpy(table->steps, pwm_temp_steps, (pwm_temp_steps_len + 1) * 2); - ets_memcpy(table->masks, pwm_temp_masks, pwm_temp_steps_len * 4); - pwm_multiplier = ESP8266_CLOCK/(range * pwm_freq); - pwm_steps_changed = 1; -} - -void ICACHE_RAM_ATTR pwm_timer_isr() //103-138 -{ - struct pwm_isr_table *table = &(_pwm_isr_data.tables[_pwm_isr_data.active]); - static uint8_t current_step = 0; - TEIE &= ~TEIE1;//14 - T1I = 0;//9 - if(current_step < table->len) { //20/21 - uint32_t mask = table->masks[current_step] & pwm_mask; - if(mask & 0xFFFF) { - GPOC = mask & 0xFFFF; //15/21 - } - if(mask & 0x10000) { - GP16O = 0; //6/13 - } - current_step++;//1 - } else { - current_step = 0;//1 - if(pwm_mask == 0) { //12 - table->len = 0; - return; - } - if(pwm_mask & 0xFFFF) { - GPOS = pwm_mask & 0xFFFF; //11 - } - if(pwm_mask & 0x10000) { - GP16O = 1; //5/13 - } - if(pwm_steps_changed) { //12/21 - _pwm_isr_data.active = !_pwm_isr_data.active; - table = &(_pwm_isr_data.tables[_pwm_isr_data.active]); - pwm_steps_changed = 0; - } - } - T1L = (table->steps[current_step] * pwm_multiplier);//23 - TEIE |= TEIE1;//13 -} - -void pwm_start_timer() -{ - timer1_disable(); - ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); - ETS_FRC_TIMER1_NMI_INTR_ATTACH(pwm_timer_isr); - timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); - timer1_write(1); -} - -void ICACHE_RAM_ATTR pwm_stop_pin(uint8_t pin) -{ - if(pwm_mask){ - pwm_mask &= ~(1 << pin); - if(pwm_mask == 0) { - ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); - timer1_disable(); - timer1_isr_init(); - } - } -} - -extern void __analogWrite(uint8_t pin, int value) -{ - bool start_timer = false; - if(value == 0) { - digitalWrite(pin, LOW); - prep_pwm_steps(); - return; - } - if((pwm_mask & (1 << pin)) == 0) { - if(pwm_mask == 0) { - memset(&_pwm_isr_data, 0, sizeof(_pwm_isr_data)); - start_timer = true; - } - pinMode(pin, OUTPUT); - digitalWrite(pin, LOW); - pwm_mask |= (1 << pin); - } - if((F_CPU / ESP8266_CLOCK) == 1) { - value = (value+1) / 2; - } - pwm_values[pin] = value % (pwm_range + 1); - prep_pwm_steps(); - if(start_timer) { - pwm_start_timer(); - } -} - -extern void __analogWriteFreq(uint32_t freq) -{ - pwm_freq = freq; - prep_pwm_steps(); -} - -extern void __analogWriteRange(uint32_t range) -{ - pwm_range = range; - prep_pwm_steps(); + } } -extern void analogWrite(uint8_t pin, int val) __attribute__ ((weak, alias("__analogWrite"))); -extern void analogWriteFreq(uint32_t freq) __attribute__ ((weak, alias("__analogWriteFreq"))); -extern void analogWriteRange(uint32_t range) __attribute__ ((weak, alias("__analogWriteRange"))); +extern void analogWrite(uint8_t pin, int val) __attribute__((weak, alias("__analogWrite"))); +extern void analogWriteFreq(uint32_t freq) __attribute__((weak, alias("__analogWriteFreq"))); +extern void analogWriteRange(uint32_t range) __attribute__((weak, alias("__analogWriteRange"))); diff --git a/libraries/Servo/src/Servo.cpp b/libraries/Servo/src/Servo.cpp new file mode 100644 index 0000000000..305d2c82a5 --- /dev/null +++ b/libraries/Servo/src/Servo.cpp @@ -0,0 +1,129 @@ +/* +Servo library using shared TIMER1 infrastructure + +Original Copyright (c) 2015 Michael C. Miller. All right reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#if defined(ESP8266) + +#include +#include +#include "core_esp8266_waveform.h" + +// similiar to map but will have increased accuracy that provides a more +// symetric api (call it and use result to reverse will provide the original value) +int improved_map(int value, int minIn, int maxIn, int minOut, int maxOut) +{ + const int rangeIn = maxIn - minIn; + const int rangeOut = maxOut - minOut; + const int deltaIn = value - minIn; + // fixed point math constants to improve accurancy of divide and rounding + const int fixedHalfDecimal = 1; + const int fixedDecimal = fixedHalfDecimal * 2; + + return ((deltaIn * rangeOut * fixedDecimal) / (rangeIn) + fixedHalfDecimal) / fixedDecimal + minOut; +} + +//------------------------------------------------------------------- +// Servo class methods + +Servo::Servo() +{ + _attached = false; + _valueUs = DEFAULT_PULSE_WIDTH; + _minUs = MIN_PULSE_WIDTH; + _maxUs = MAX_PULSE_WIDTH; +} + +Servo::~Servo() { + detach(); +} + + +uint8_t Servo::attach(int pin) +{ + return attach(pin, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH); +} + +uint8_t Servo::attach(int pin, uint16_t minUs, uint16_t maxUs) +{ + if (!_attached) { + digitalWrite(pin, LOW); + pinMode(pin, OUTPUT); + _pin = pin; + _attached = true; + } + + // keep the min and max within 200-3000 us, these are extreme + // ranges and should support extreme servos while maintaining + // reasonable ranges + _maxUs = max((uint16_t)250, min((uint16_t)3000, maxUs)); + _minUs = max((uint16_t)200, min(_maxUs, minUs)); + + write(_valueUs); + + return pin; +} + +void Servo::detach() +{ + if (_attached) { + stopWaveform(_pin); + _attached = false; + digitalWrite(_pin, LOW); + } +} + +void Servo::write(int value) +{ + // treat values less than 544 as angles in degrees (valid values in microseconds are handled as microseconds) + if (value < MIN_PULSE_WIDTH) { + // assumed to be 0-180 degrees servo + value = constrain(value, 0, 180); + // writeMicroseconds will contrain the calculated value for us + // for any user defined min and max, but we must use default min max + value = improved_map(value, 0, 180, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH); + } + writeMicroseconds(value); +} + +void Servo::writeMicroseconds(int value) +{ + _valueUs = value; + if (_attached) { + startWaveform(_pin, _valueUs, REFRESH_INTERVAL - _valueUs, 0); + } +} + +int Servo::read() // return the value as degrees +{ + // read returns the angle for an assumed 0-180, so we calculate using + // the normal min/max constants and not user defined ones + return improved_map(readMicroseconds(), MIN_PULSE_WIDTH, MAX_PULSE_WIDTH, 0, 180); +} + +int Servo::readMicroseconds() +{ + return _valueUs; +} + +bool Servo::attached() +{ + return _attached; +} + +#endif diff --git a/libraries/Servo/src/Servo.h b/libraries/Servo/src/Servo.h index d4bf861baf..6b19a477bd 100644 --- a/libraries/Servo/src/Servo.h +++ b/libraries/Servo/src/Servo.h @@ -1,6 +1,6 @@ /* Servo.h - Interrupt driven Servo library for Esp8266 using timers - Copyright (c) 2015 Michael C. Miller. All right reserved. + Original Copyright (c) 2015 Michael C. Miller. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -23,13 +23,6 @@ // The servos are pulsed in the background using the value most recently // written using the write() method. // -// This library uses timer0 and timer1. -// Note that timer0 may be repurposed when the first servo is attached. -// -// Timers are seized as needed in groups of 12 servos - 24 servos use two -// timers, there are only two timers for the esp8266 so the support stops here -// The sequence used to sieze timers is defined in timers.h -// // The methods are: // // Servo - Class for manipulating servo motors connected to Arduino pins. @@ -58,15 +51,7 @@ #define DEFAULT_PULSE_WIDTH 1500 // default pulse width when servo is attached #define REFRESH_INTERVAL 20000 // minumim time to refresh servos in microseconds -// NOTE: to maintain a strict refresh interval the user needs to not exceede 8 servos -#define SERVOS_PER_TIMER 12 // the maximum number of servos controlled by one timer -#define MAX_SERVOS (ServoTimerSequence_COUNT * SERVOS_PER_TIMER) - -#if defined(ESP8266) - -#include "esp8266/ServoTimers.h" - -#else +#if !defined(ESP8266) #error "This library only supports esp8266 boards." @@ -76,6 +61,7 @@ class Servo { public: Servo(); + ~Servo(); uint8_t attach(int pin); // attach the given pin to the next free channel, sets pinMode, returns channel number or 0 if failure uint8_t attach(int pin, uint16_t min, uint16_t max); // as above but also sets min and max values for writes. void detach(); @@ -85,9 +71,11 @@ class Servo int readMicroseconds(); // returns current pulse width in microseconds for this servo (was read_us() in first release) bool attached(); // return true if this servo is attached, otherwise false private: - uint8_t _servoIndex; // index into the channel data for this servo + bool _attached; + uint8_t _pin; uint16_t _minUs; uint16_t _maxUs; + uint16_t _valueUs; }; #endif diff --git a/libraries/Servo/src/esp8266/Servo.cpp b/libraries/Servo/src/esp8266/Servo.cpp deleted file mode 100644 index 676aab2cc6..0000000000 --- a/libraries/Servo/src/esp8266/Servo.cpp +++ /dev/null @@ -1,308 +0,0 @@ -/* -Copyright (c) 2015 Michael C. Miller. All right reserved. - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#if defined(ESP8266) - -#include -#include - - -#define INVALID_SERVO 255 // flag indicating an invalid servo index - -const uint32_t c_CycleCompensation = 4; // compensation us to trim adjust for digitalWrite delays - -#define INVALID_PIN 63 // flag indicating never attached servo - -struct ServoInfo { - uint8_t pin : 6; // a pin number from 0 to 62, 63 reserved - uint8_t isActive : 1; // true if this channel is enabled, pin not pulsed if false - uint8_t isDetaching : 1; // true if this channel is being detached, maintains pulse integrity -}; - -struct ServoState { - ServoInfo info; - volatile uint16_t usPulse; -}; - -#if !defined (SERVO_EXCLUDE_TIMER0) -ServoTimer0 s_servoTimer0; -#endif - -#if !defined (SERVO_EXCLUDE_TIMER1) -ServoTimer1 s_servoTimer1; -#endif - -static ServoState s_servos[MAX_SERVOS]; // static array of servo structures - -static uint8_t s_servoCount = 0; // the total number of attached s_servos - - -// inconvenience macros -#define SERVO_INDEX_TO_TIMER(servoIndex) ((ServoTimerSequence)(servoIndex / SERVOS_PER_TIMER)) // returns the timer controlling this servo -#define SERVO_INDEX(timerId, channel) ((timerId * SERVOS_PER_TIMER) + channel) // macro to access servo index by timer and channel - -// similiar to map but will have increased accuracy that provides a more -// symetric api (call it and use result to reverse will provide the original value) -// -int improved_map(int value, int minIn, int maxIn, int minOut, int maxOut) -{ - const int rangeIn = maxIn - minIn; - const int rangeOut = maxOut - minOut; - const int deltaIn = value - minIn; - // fixed point math constants to improve accurancy of divide and rounding - const int fixedHalfDecimal = 1; - const int fixedDecimal = fixedHalfDecimal * 2; - - return ((deltaIn * rangeOut * fixedDecimal) / (rangeIn) + fixedHalfDecimal) / fixedDecimal + minOut; -} - -//------------------------------------------------------------------------------ -// Interrupt handler template method that takes a class that implements -// a standard set of methods for the timer abstraction -//------------------------------------------------------------------------------ -template -static void Servo_Handler(T* timer) ICACHE_RAM_ATTR; - -template -static void Servo_Handler(T* timer) -{ - uint8_t servoIndex; - - // clear interrupt - timer->ResetInterrupt(); - - if (timer->isEndOfCycle()) { - timer->StartCycle(); - } - else { - servoIndex = SERVO_INDEX(timer->timerId(), timer->getCurrentChannel()); - if (servoIndex < s_servoCount && s_servos[servoIndex].info.isActive) { - // pulse this channel low if activated - digitalWrite(s_servos[servoIndex].info.pin, LOW); - - if (s_servos[servoIndex].info.isDetaching) { - s_servos[servoIndex].info.isActive = false; - s_servos[servoIndex].info.isDetaching = false; - } - } - timer->nextChannel(); - } - - servoIndex = SERVO_INDEX(timer->timerId(), timer->getCurrentChannel()); - - if (servoIndex < s_servoCount && - timer->getCurrentChannel() < SERVOS_PER_TIMER) { - timer->SetPulseCompare(timer->usToTicks(s_servos[servoIndex].usPulse) - c_CycleCompensation); - - if (s_servos[servoIndex].info.isActive) { - if (s_servos[servoIndex].info.isDetaching) { - // it was active, reset state and leave low - s_servos[servoIndex].info.isActive = false; - s_servos[servoIndex].info.isDetaching = false; - } - else { - // its an active channel so pulse it high - digitalWrite(s_servos[servoIndex].info.pin, HIGH); - } - } - } - else { - if (!isTimerActive(timer->timerId())) { - // no active running channels on this timer, stop the ISR - finISR(timer->timerId()); - } - else { - // finished all channels so wait for the refresh period to expire before starting over - // allow a few ticks to ensure the next match is not missed - uint32_t refreshCompare = timer->usToTicks(REFRESH_INTERVAL); - if ((timer->GetCycleCount() + c_CycleCompensation * 2) < refreshCompare) { - timer->SetCycleCompare(refreshCompare - c_CycleCompensation); - } - else { - // at least REFRESH_INTERVAL has elapsed - timer->SetCycleCompare(timer->GetCycleCount() + c_CycleCompensation * 2); - } - } - - timer->setEndOfCycle(); - } -} - -static void handler0() ICACHE_RAM_ATTR; -static void handler0() -{ - Servo_Handler(&s_servoTimer0); -} - -static void handler1() ICACHE_RAM_ATTR; -static void handler1() -{ - Servo_Handler(&s_servoTimer1); -} - -static void initISR(ServoTimerSequence timerId) -{ -#if !defined (SERVO_EXCLUDE_TIMER0) - if (timerId == ServoTimerSequence_Timer0) - s_servoTimer0.InitInterrupt(&handler0); -#endif -#if !defined (SERVO_EXCLUDE_TIMER1) - if (timerId == ServoTimerSequence_Timer1) - s_servoTimer1.InitInterrupt(&handler1); -#endif -} - -static void finISR(ServoTimerSequence timerId) ICACHE_RAM_ATTR; -static void finISR(ServoTimerSequence timerId) -{ -#if !defined (SERVO_EXCLUDE_TIMER0) - if (timerId == ServoTimerSequence_Timer0) - s_servoTimer0.StopInterrupt(); -#endif -#if !defined (SERVO_EXCLUDE_TIMER1) - if (timerId == ServoTimerSequence_Timer1) - s_servoTimer1.StopInterrupt(); -#endif -} - -// returns true if any servo is active on this timer -static boolean isTimerActive(ServoTimerSequence timerId) ICACHE_RAM_ATTR; -static boolean isTimerActive(ServoTimerSequence timerId) -{ - for (uint8_t channel = 0; channel < SERVOS_PER_TIMER; channel++) { - if (s_servos[SERVO_INDEX(timerId, channel)].info.isActive) { - return true; - } - } - return false; -} - -//------------------------------------------------------------------- -// Servo class methods - -Servo::Servo() -{ - if (s_servoCount < MAX_SERVOS) { - // assign a servo index to this instance - _servoIndex = s_servoCount++; - // store default values - s_servos[_servoIndex].usPulse = DEFAULT_PULSE_WIDTH; - - // set default _minUs and _maxUs incase write() is called before attach() - _minUs = MIN_PULSE_WIDTH; - _maxUs = MAX_PULSE_WIDTH; - - s_servos[_servoIndex].info.isActive = false; - s_servos[_servoIndex].info.isDetaching = false; - s_servos[_servoIndex].info.pin = INVALID_PIN; - } - else { - _servoIndex = INVALID_SERVO; // too many servos - } -} - -uint8_t Servo::attach(int pin) -{ - return attach(pin, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH); -} - -uint8_t Servo::attach(int pin, uint16_t minUs, uint16_t maxUs) -{ - ServoTimerSequence timerId; - - if (_servoIndex < MAX_SERVOS) { - if (s_servos[_servoIndex].info.pin == INVALID_PIN) { - pinMode(pin, OUTPUT); // set servo pin to output - digitalWrite(pin, LOW); - s_servos[_servoIndex].info.pin = pin; - } - - // keep the min and max within 200-3000 us, these are extreme - // ranges and should support extreme servos while maintaining - // reasonable ranges - _maxUs = max((uint16_t)250, min((uint16_t)3000, maxUs)); - _minUs = max((uint16_t)200, min(_maxUs, minUs)); - - // initialize the timerId if it has not already been initialized - timerId = SERVO_INDEX_TO_TIMER(_servoIndex); - if (!isTimerActive(timerId)) { - initISR(timerId); - } - s_servos[_servoIndex].info.isDetaching = false; - s_servos[_servoIndex].info.isActive = true; // this must be set after the check for isTimerActive - } - return _servoIndex; -} - -void Servo::detach() -{ - if (s_servos[_servoIndex].info.isActive) { - s_servos[_servoIndex].info.isDetaching = true; - } -} - -void Servo::write(int value) -{ - // treat values less than 544 as angles in degrees (valid values in microseconds are handled as microseconds) - if (value < MIN_PULSE_WIDTH) { - // assumed to be 0-180 degrees servo - value = constrain(value, 0, 180); - // writeMicroseconds will contrain the calculated value for us - // for any user defined min and max, but we must use default min max - value = improved_map(value, 0, 180, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH); - } - writeMicroseconds(value); -} - -void Servo::writeMicroseconds(int value) -{ - // ensure channel is valid - if ((_servoIndex < MAX_SERVOS)) { - // ensure pulse width is valid - value = constrain(value, _minUs, _maxUs); - - s_servos[_servoIndex].usPulse = value; - } -} - -int Servo::read() // return the value as degrees -{ - // read returns the angle for an assumed 0-180, so we calculate using - // the normal min/max constants and not user defined ones - return improved_map(readMicroseconds(), MIN_PULSE_WIDTH, MAX_PULSE_WIDTH, 0, 180); -} - -int Servo::readMicroseconds() -{ - unsigned int pulsewidth; - if (_servoIndex != INVALID_SERVO) { - pulsewidth = s_servos[_servoIndex].usPulse; - } - else { - pulsewidth = 0; - } - - return pulsewidth; -} - -bool Servo::attached() -{ - return s_servos[_servoIndex].info.isActive; -} - -#endif diff --git a/libraries/Servo/src/esp8266/ServoTimers.h b/libraries/Servo/src/esp8266/ServoTimers.h deleted file mode 100644 index 90a19e1651..0000000000 --- a/libraries/Servo/src/esp8266/ServoTimers.h +++ /dev/null @@ -1,225 +0,0 @@ -/* - Copyright (c) 2015 Michael C. Miller. All right reserved. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - */ - -// -// Defines for timer abstractions used with Servo library -// -// ServoTimerSequence enumerates the sequence that the timers should be allocated -// ServoTimerSequence_COUNT indicates how many timers are available. -// -enum ServoTimerSequence { - -#if !defined (SERVO_EXCLUDE_TIMER0) - ServoTimerSequence_Timer0, -#endif - -#if !defined (SERVO_EXCLUDE_TIMER1) - ServoTimerSequence_Timer1, -#endif - - ServoTimerSequence_COUNT -}; - - -#if !defined (SERVO_EXCLUDE_TIMER0) - -struct ServoTimer0 -{ -public: - ServoTimer0() - { - setEndOfCycle(); - } - - - uint32_t usToTicks(uint32_t us) const - { - return (clockCyclesPerMicrosecond() * us); // converts microseconds to tick - } - uint32_t ticksToUs(uint32_t ticks) const - { - return (ticks / clockCyclesPerMicrosecond()); // converts from ticks back to microseconds - } - - void InitInterrupt(timercallback handler) - { - timer0_isr_init(); - timer0_attachInterrupt(handler); - } - - void ResetInterrupt() {}; // timer0 doesn't have a clear interrupt - - void StopInterrupt() - { - timer0_detachInterrupt(); - } - - void SetPulseCompare(uint32_t value) - { - timer0_write(ESP.getCycleCount() + value); - } - - void SetCycleCompare(uint32_t value) - { - timer0_write(_cycleStart + value); - } - - uint32_t GetCycleCount() const - { - return ESP.getCycleCount() - _cycleStart; - } - - - void StartCycle() - { - _cycleStart = ESP.getCycleCount(); - _currentChannel = 0; - } - - int8_t getCurrentChannel() const - { - return _currentChannel; - } - - void nextChannel() - { - _currentChannel++; - } - - void setEndOfCycle() - { - _currentChannel = -1; - } - - bool isEndOfCycle() const - { - return (_currentChannel == -1); - } - - ServoTimerSequence timerId() const - { - return ServoTimerSequence_Timer0; - } - -private: - volatile uint32_t _cycleStart; - volatile int8_t _currentChannel; -}; - -#endif - - -#if !defined (SERVO_EXCLUDE_TIMER1) - -#define TIMER1_TICKS_PER_US (APB_CLK_FREQ / 1000000L) - -struct ServoTimer1 -{ -public: - ServoTimer1() - { - setEndOfCycle(); - } - - - uint32_t usToTicks(uint32_t us) const - { - return (TIMER1_TICKS_PER_US / 16 * us); // converts microseconds to tick - } - uint32_t ticksToUs(uint32_t ticks) const - { - return (ticks / TIMER1_TICKS_PER_US * 16); // converts from ticks back to microseconds - } - - void InitInterrupt(timercallback handler) - { - timer1_isr_init(); - timer1_attachInterrupt(handler); - timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); - timer1_write(usToTicks(REFRESH_INTERVAL)); - } - - void ResetInterrupt() {}; // timer1 doesn't have a clear interrupt - - void StopInterrupt() - { - timer1_detachInterrupt(); - } - - void SetPulseCompare(uint32_t value) - { - _cycleTicks += value; - timer1_write(value); - } - - void SetCycleCompare(uint32_t value) - { - if (value <= _cycleTicks) - { - value = 1; - } - else - { - value -= _cycleTicks; - } - timer1_write(value); - } - - uint32_t GetCycleCount() const - { - return _cycleTicks; - } - - - void StartCycle() - { - _cycleTicks = 0; - _currentChannel = 0; - } - - int8_t getCurrentChannel() const - { - return _currentChannel; - } - - void nextChannel() - { - _currentChannel++; - } - - void setEndOfCycle() - { - _currentChannel = -1; - } - - bool isEndOfCycle() const - { - return (_currentChannel == -1); - } - - ServoTimerSequence timerId() const - { - return ServoTimerSequence_Timer1; - } - -private: - volatile uint32_t _cycleTicks; - volatile int8_t _currentChannel; -}; - -#endif From 2b1c0f01c47762aab56f13416acd5d51e45e2d24 Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Sat, 14 Apr 2018 18:06:53 -0700 Subject: [PATCH 2/6] Support busy-looping for small phase resolution The closest time between interrupts is about 3us, so rework the waveform generator to support phases down to 1us, while limiting the time in the IRQ handler to avoid WDT or WiFi errors. Waveforms now are based off of absolute cycle, not delta-cycles, and the GPIOs are converted from a bit to a mask (to save clocks in the IRQ) costing about 40 more bytes in global storage and reducing the minimum period down from 3.5us to 1.20us at 160MHz. Steppers still based off of delta-times. --- cores/esp8266/core_esp8266_waveform.c | 208 ++++++++++++++------------ cores/esp8266/core_esp8266_waveform.h | 6 +- 2 files changed, 117 insertions(+), 97 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform.c b/cores/esp8266/core_esp8266_waveform.c index 389837b4aa..b688508aef 100644 --- a/cores/esp8266/core_esp8266_waveform.c +++ b/cores/esp8266/core_esp8266_waveform.c @@ -55,6 +55,9 @@ #include #include "core_esp8266_waveform.h" +// Need speed, not size, here +#pragma GCC optimize ("O3") + // Map the IRQ stuff to standard terminology #define cli() ets_intr_lock() #define sei() ets_intr_unlock() @@ -63,7 +66,7 @@ #define MAXIRQUS (10000) // If the cycles from now to an event are below this value, perform it anyway since IRQs take longer than this -#define CYCLES_FLUFF (200) +#define CYCLES_FLUFF (100) // Macro to get count of predefined array elements #define countof(a) ((size_t)(sizeof(a)/sizeof(a[0]))) @@ -77,34 +80,36 @@ // Waveform generator can create tones, PWM, and servos typedef struct { - uint32_t nextEventCycles; + uint32_t nextServiceCycle; uint32_t timeHighCycles; uint32_t timeLowCycles; uint32_t timeLeftCycles; // To ensure stable change, only copy these over on low->high transition - unsigned gpioPin : 4; // Check gpioPin16 first - unsigned nextTimeHighCycles : 28; - unsigned gpioPin16 : 1; // Special case for weird IO16 + uint16_t gpioMask; + uint16_t gpio16Mask; +// unsigned gpioPin : 4; // Check gpioPin16 first unsigned state : 1; + unsigned nextTimeHighCycles : 31; +// unsigned gpioPin16 : 1; // Special case for weird IO16 unsigned enabled : 1; - unsigned nextTimeLowCycles : 28; + unsigned nextTimeLowCycles : 31; } Waveform; // These can be accessed in interrupts, so ensure to bracket access with SEI/CLI static Waveform waveform[] = { - {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // GPIO0 - {0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, // GPIO1 - {0, 0, 0, 0, 2, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 3, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 4, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 5, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 1<<0, 0, 0, 0, 0, 0}, // GPIO0 + {0, 0, 0, 0, 1<<1, 0, 0, 0, 0, 0}, // GPIO1 + {0, 0, 0, 0, 1<<2, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 1<<3, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 1<<4, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 1<<5, 0, 0, 0, 0, 0}, // GPIOS 6-11 not allowed, used for flash - {0, 0, 0, 0, 12, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 13, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 14, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 15, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0, 0, 1, 0, 0, 0} -}; // GPIO16 + {0, 0, 0, 0, 1<<12, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 1<<13, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 1<<14, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 1<<15, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 1, 0, 0, 0, 0} // GPIO16 +}; // Maximum umber of moves per stepper queue @@ -164,8 +169,20 @@ static inline ICACHE_RAM_ATTR uint32_t min_u32(uint32_t a, uint32_t b) { return b; } +static inline ICACHE_RAM_ATTR uint32_t min_s32(int32_t a, int32_t b) { + if (a < b) { + return a; + } + return b; +} + static inline ICACHE_RAM_ATTR void ReloadTimer(uint32_t a) { - timer1_write(a); + // Below a threshold you actually miss the edge IRQ, so ensure enough time + if (a > 32) { + timer1_write(a); + } else { + timer1_write(32); + } } static inline ICACHE_RAM_ATTR uint32_t GetCycleCount() { @@ -220,7 +237,7 @@ static inline ICACHE_RAM_ATTR void PopStepper(int i) { } // Called by the user to detach a stepper and free memory -int removeStepper(int pin) { +int removeStepper(uint8_t pin) { sei(); for (int i = 0; i < stepQCnt; i++) { if (stepQ[i].gpioPin == pin) { @@ -231,6 +248,7 @@ int removeStepper(int pin) { return true; } } + cli(); return false; } @@ -295,7 +313,11 @@ static int PushStepper(int gpioPin, const Motion *nextMove) { } // Called by user to add a PWL move to the queue, returns false if there is no space left -int pushStepperMove(int pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0) { +int pushStepperMove(uint8_t pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0) { + if (pin > 15) { + // Only GPIO 0...15 allowed + return false; + } Motion m; m.pulses = pulses; m.j_2 = j / 2.0; @@ -307,7 +329,7 @@ int pushStepperMove(int pin, int dir, int sync, uint16_t pulses, float j, float } // Assign a pin to stepper DIR -int setStepperDirPin(int pin) { +int setStepperDirPin(uint8_t pin) { if (pin > 16) { return false; } @@ -321,7 +343,7 @@ int setStepperDirPin(int pin) { int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS) { Waveform *wave = NULL; for (size_t i = 0; i < countof(waveform); i++) { - if (((pin == 16) && waveform[i].gpioPin16) || ((pin != 16) && (waveform[i].gpioPin == pin))) { + if (((pin == 16) && waveform[i].gpio16Mask==1) || ((pin != 16) && (waveform[i].gpioMask == 1<nextTimeLowCycles = MicrosecondsToCycles(timeLowUS); wave->timeLeftCycles = MicrosecondsToCycles(runTimeUS); if (!wave->enabled) { - wave->state = 1; - digitalWrite(pin, 1); - wave->timeHighCycles = MicrosecondsToCycles(timeHighUS); - wave->timeLowCycles = MicrosecondsToCycles(timeLowUS); - wave->nextEventCycles = wave->timeHighCycles; + wave->state = 0; + // Actually set the pin high or low in the IRQ service to guarantee times + wave->timeHighCycles = MicrosecondsToCycles(timeHighUS) - 30; // Sub off some of the codepath time + wave->timeLowCycles = MicrosecondsToCycles(timeLowUS) - 30; // Sub off some of the codepath time + wave->nextServiceCycle = GetCycleCount() + MicrosecondsToCycles(1); wave->enabled = 1; if (!timerRunning) { initTimer(); } - ReloadTimer(10); // Cause an interrupt post-haste + ReloadTimer(MicrosecondsToCycles(1)); // Cause an interrupt post-haste } cli(); return true; @@ -352,7 +374,7 @@ int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t // Stops a waveform on a pin int stopWaveform(uint8_t pin) { for (size_t i = 0; i < countof(waveform); i++) { - if (((pin == 16) && waveform[i].gpioPin16) || ((pin != 16) && (waveform[i].gpioPin == pin))) { + if (((pin == 16) && waveform[i].gpio16Mask) || ((pin != 16) && (waveform[i].gpioMask == 1<enabled) { + continue; + } + + // Check for toggles + now = GetCycleCount(); + if (now >= wave->nextServiceCycle) { + wave->state = !wave->state; + if (wave->state) { + SetGPIO(wave->gpioMask); + if (wave->gpio16Mask) { + GP16O |= wave->gpio16Mask; // GPIO16 write slow as it's RMW + } + wave->nextServiceCycle = now + wave->timeHighCycles; + wave->timeHighCycles = wave->nextTimeHighCycles; + nextEventCycles = min_u32(nextEventCycles, wave->timeHighCycles); + } else { + ClearGPIO(wave->gpioMask); + if (wave->gpio16Mask) { + GP16O &= ~wave->gpio16Mask; + } + wave->nextServiceCycle = now + wave->timeLowCycles; + wave->timeLowCycles = wave->nextTimeLowCycles; + nextEventCycles = min_u32(nextEventCycles, wave->timeLowCycles); + } + } else { + uint32_t deltaCycles = wave->nextServiceCycle - now; + nextEventCycles = min_u32(nextEventCycles, deltaCycles); + } + } + } while (--cnt && (nextEventCycles < MicrosecondsToCycles(4))); + uint32_t curCycleCount = GetCycleCount(); uint32_t deltaCycles = curCycleCount - lastCycleCount; lastCycleCount = curCycleCount; + // Check for timed-out waveforms out of the high-frequency toggle loop for (size_t i = 0; i < countof(waveform); i++) { Waveform *wave = &waveform[i]; - - // If it's not on, ignore! - if (!wave->enabled) { - continue; - } - - // Check for timed-out waveforms if (wave->timeLeftCycles) { - uint32_t newTimeLeftCycles = wave->timeLeftCycles - deltaCycles; // Check for unsigned underflow with new > old - if ((deltaCycles >= wave->timeLeftCycles) || (newTimeLeftCycles <= CYCLES_FLUFF)) { + if (deltaCycles >= wave->timeLeftCycles) { // Done, remove! wave->enabled = false; - if (wave->gpioPin16) { - GP16O &= ~1; - } else { - ClearGPIO(1 << wave->gpioPin); - } + ClearGPIO(wave->gpioMask); + GP16O &= ~wave->gpio16Mask; } else { + uint32_t newTimeLeftCycles = wave->timeLeftCycles - deltaCycles; wave->timeLeftCycles = newTimeLeftCycles; } } - - // Check for toggles - uint32_t newNextEventCycles = wave->nextEventCycles - deltaCycles; - if ((deltaCycles >= wave->nextEventCycles) || (newNextEventCycles <= CYCLES_FLUFF)) { - wave->state = !wave->state; - if (wave->state) { - if (wave->gpioPin16) { - GP16O |= 1; - } else { - SetGPIO(1 << wave->gpioPin); - } - wave->nextEventCycles = wave->timeHighCycles; - wave->timeHighCycles = wave->nextTimeHighCycles; - } else { - if (wave->gpioPin16) { - GP16O &= ~1; - } else { - ClearGPIO(1 << wave->gpioPin); - } - wave->nextEventCycles = wave->timeLowCycles; - wave->timeLowCycles = wave->nextTimeLowCycles; - } - } else { - wave->nextEventCycles = newNextEventCycles; - } - } - - uint32_t nextEventCycles = MicrosecondsToCycles(MAXIRQUS); - for (size_t i = 0; i < countof(waveform); i++) { - if (waveform[i].enabled) { - nextEventCycles = min_u32(nextEventCycles, waveform[i].nextEventCycles); - } } if (stepQCnt) { nextEventCycles = min_u32(nextEventCycles, ProcessSteppers(deltaCycles)); } - // Adjust back by the time we spent in here - deltaCycles = GetCycleCount() - lastCycleCount; - // Add in IRQ delay, from measurements on idle system #if F_CPU == 160000000 - deltaCycles += MicrosecondsToCycles(1) + (MicrosecondsToCycles(1) >> 1); - #else - deltaCycles += MicrosecondsToCycles(3); - #endif - if (nextEventCycles > deltaCycles) { - nextEventCycles -= deltaCycles; + if (nextEventCycles <= 5 * MicrosecondsToCycles(1)) { + nextEventCycles = MicrosecondsToCycles(1) / 2; } else { - nextEventCycles = CYCLES_FLUFF; + nextEventCycles -= 5 * MicrosecondsToCycles(1); } - - // Keep next call within sane min/max time - if (nextEventCycles < CYCLES_FLUFF) { - nextEventCycles = CYCLES_FLUFF; - } else if (nextEventCycles > MicrosecondsToCycles(MAXIRQUS)) { - nextEventCycles = MicrosecondsToCycles(MAXIRQUS); - } - #if F_CPU == 160000000 nextEventCycles = nextEventCycles >> 1; + #else + if (nextEventCycles <= 6 * MicrosecondsToCycles(1)) { + nextEventCycles = MicrosecondsToCycles(1) / 2; + } else { + nextEventCycles -= 6 * MicrosecondsToCycles(1); + } #endif + ReloadTimer(nextEventCycles); } diff --git a/cores/esp8266/core_esp8266_waveform.h b/cores/esp8266/core_esp8266_waveform.h index 9dcd6d4d34..5f618b2769 100644 --- a/cores/esp8266/core_esp8266_waveform.h +++ b/cores/esp8266/core_esp8266_waveform.h @@ -64,9 +64,9 @@ extern "C" { int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS); int stopWaveform(uint8_t pin); -int setStepperDirPin(int pin); -int pushStepperMove(int pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0); -int removeStepper(int pin); +int setStepperDirPin(uint8_t pin); +int pushStepperMove(uint8_t pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0); +int removeStepper(uint8_t pin); #ifdef __cplusplus } From 330f9cd6766a091a7510b51f934350a50459ab87 Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Tue, 17 Apr 2018 14:39:00 -0700 Subject: [PATCH 3/6] Save 88 bytes, make 1us period exact, allow GP16 stepper Remove unneeded curTimeHigh/curTimeLow from the waveform structure, saving 88 bytes (11 pins x 8bytes) of heap. Also reduces inner loop in the IRQ by an couple stores. Also add measured offset to the waveform generation request, enabling a 1/1000 reqest to produce a 1.083us high pulse, and a 999/1000 to give a 1.083 low period, and the cycle time to be exactly 1.00ms over the range. IRQ doesn't need to be disabled on waveform changes as the code order between the irq and start/stop is safe. Allow GPIO16 as a stepper out pin --- cores/esp8266/core_esp8266_waveform.c | 112 ++++++++++++-------------- 1 file changed, 53 insertions(+), 59 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform.c b/cores/esp8266/core_esp8266_waveform.c index b688508aef..0ccc16ec5a 100644 --- a/cores/esp8266/core_esp8266_waveform.c +++ b/cores/esp8266/core_esp8266_waveform.c @@ -80,35 +80,30 @@ // Waveform generator can create tones, PWM, and servos typedef struct { - uint32_t nextServiceCycle; - uint32_t timeHighCycles; - uint32_t timeLowCycles; - uint32_t timeLeftCycles; - // To ensure stable change, only copy these over on low->high transition - uint16_t gpioMask; - uint16_t gpio16Mask; -// unsigned gpioPin : 4; // Check gpioPin16 first - unsigned state : 1; - unsigned nextTimeHighCycles : 31; -// unsigned gpioPin16 : 1; // Special case for weird IO16 - unsigned enabled : 1; - unsigned nextTimeLowCycles : 31; + uint32_t nextServiceCycle; // ESP cycle timer when a transition required + uint32_t timeLeftCycles; // For time-limited waveform, how many ESP cycles left + uint16_t gpioMask; // Mask instead of value to speed IRQ loop + uint16_t gpio16Mask; // Mask instead of value to speed IRQ loop + unsigned state : 1; // Current state of this pin + unsigned nextTimeHighCycles : 31; // Copy over low->high to keep smooth waveform + unsigned enabled : 1; // Is this GPIO generating a waveform? + unsigned nextTimeLowCycles : 31; // Copy over high->low to keep smooth waveform } Waveform; // These can be accessed in interrupts, so ensure to bracket access with SEI/CLI static Waveform waveform[] = { - {0, 0, 0, 0, 1<<0, 0, 0, 0, 0, 0}, // GPIO0 - {0, 0, 0, 0, 1<<1, 0, 0, 0, 0, 0}, // GPIO1 - {0, 0, 0, 0, 1<<2, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 1<<3, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 1<<4, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 1<<5, 0, 0, 0, 0, 0}, + {0, 0, 1<<0, 0, 0, 0, 0, 0}, // GPIO0 + {0, 0, 1<<1, 0, 0, 0, 0, 0}, // GPIO1 + {0, 0, 1<<2, 0, 0, 0, 0, 0}, + {0, 0, 1<<3, 0, 0, 0, 0, 0}, + {0, 0, 1<<4, 0, 0, 0, 0, 0}, + {0, 0, 1<<5, 0, 0, 0, 0, 0}, // GPIOS 6-11 not allowed, used for flash - {0, 0, 0, 0, 1<<12, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 1<<13, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 1<<14, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 1<<15, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0, 1, 0, 0, 0, 0} // GPIO16 + {0, 0, 1<<12, 0, 0, 0, 0, 0}, + {0, 0, 1<<13, 0, 0, 0, 0, 0}, + {0, 0, 1<<14, 0, 0, 0, 0, 0}, + {0, 0, 1<<15, 0, 0, 0, 0, 0}, + {0, 0, 0, 1, 0, 0, 0, 0} // GPIO16 }; @@ -137,20 +132,20 @@ typedef struct { uint32_t nextEventCycles; // Copied from head for fast access - uint16_t pulses; - uint32_t cumCycles; - float j_2; - float a0; - float v0; - unsigned sync : 1; - unsigned dir : 1; - - unsigned gpioPin : 4; - unsigned finished : 1; - - uint8_t readPtr; - uint8_t writePtr; - uint8_t validEntries; + uint16_t pulses; // Pulses remaining + uint32_t cumCycles; // The "t" in our equations + float j_2; // j/2 (jerk divided by 2.0) + float a0; // Initial constant acceleration + float v0; // Initial constant velocity + unsigned sync : 1; // Wait for all steppers to finish before advancing + unsigned dir : 1; // CCW or CW + + unsigned finished : 1; // Done with all moves, on next hit pop another motion + unsigned gpioPin : 5; // Allow all GPIOs, we're going to be slow no matter what + + uint8_t readPtr; // Read queue index + uint8_t writePtr; // Push queue spot + uint8_t validEntries; // How many entries present } StepperQueue; static volatile StepperQueue *stepQ = NULL; @@ -254,9 +249,10 @@ int removeStepper(uint8_t pin) { // Add a stepper move, return TRUE on success, FALSE on out of space // Calling application needs to ensure IRQS are disabled for the call! -static int PushStepper(int gpioPin, const Motion *nextMove) { +static int PushStepper(uint8_t gpioPin, const Motion *nextMove) { StepperQueue *q = NULL; int i; + // gpioPin already validated in calling function sei(); // Determine which queue it should be on, or maybe add one if needed for (i = 0; i < stepQCnt; i++) { @@ -314,10 +310,10 @@ static int PushStepper(int gpioPin, const Motion *nextMove) { // Called by user to add a PWL move to the queue, returns false if there is no space left int pushStepperMove(uint8_t pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0) { - if (pin > 15) { - // Only GPIO 0...15 allowed + if (pin > 16) { return false; } + Motion m; m.pulses = pulses; m.j_2 = j / 2.0; @@ -351,15 +347,12 @@ int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t if (!wave) { return false; } - sei(); - wave->nextTimeHighCycles = MicrosecondsToCycles(timeHighUS); - wave->nextTimeLowCycles = MicrosecondsToCycles(timeLowUS); + wave->nextTimeHighCycles = MicrosecondsToCycles(timeHighUS) - 70; // Take out some time for IRQ codepath + wave->nextTimeLowCycles = MicrosecondsToCycles(timeLowUS) - 70; // Take out some time for IRQ codepath wave->timeLeftCycles = MicrosecondsToCycles(runTimeUS); if (!wave->enabled) { wave->state = 0; // Actually set the pin high or low in the IRQ service to guarantee times - wave->timeHighCycles = MicrosecondsToCycles(timeHighUS) - 30; // Sub off some of the codepath time - wave->timeLowCycles = MicrosecondsToCycles(timeLowUS) - 30; // Sub off some of the codepath time wave->nextServiceCycle = GetCycleCount() + MicrosecondsToCycles(1); wave->enabled = 1; if (!timerRunning) { @@ -367,7 +360,6 @@ int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t } ReloadTimer(MicrosecondsToCycles(1)); // Cause an interrupt post-haste } - cli(); return true; } @@ -375,7 +367,6 @@ int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t int stopWaveform(uint8_t pin) { for (size_t i = 0; i < countof(waveform); i++) { if (((pin == 16) && waveform[i].gpio16Mask) || ((pin != 16) && (waveform[i].gpioMask == 1<dir != dir || q->finished) { @@ -443,8 +433,14 @@ static ICACHE_RAM_ATTR void AdvanceSteppers(uint32_t deltaCycles, int dir) { q->nextEventCycles = newNextEventCycles; } } - ClearGPIO(toClear); - SetGPIO(pulseGPIO); + ClearGPIO(toClear & 0xffff); + if (toClear & 0x80000) { + GP16O &= ~1; // RMW is slow, only do if needed + } + SetGPIO(pulseGPIO & 0xffff); + if (pulseGPIO & 0x80000) { + GP16O |= 1; // RMW is slow, only do if needed + } toClear = pulseGPIO; } @@ -513,17 +509,15 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { if (wave->gpio16Mask) { GP16O |= wave->gpio16Mask; // GPIO16 write slow as it's RMW } - wave->nextServiceCycle = now + wave->timeHighCycles; - wave->timeHighCycles = wave->nextTimeHighCycles; - nextEventCycles = min_u32(nextEventCycles, wave->timeHighCycles); + wave->nextServiceCycle = now + wave->nextTimeHighCycles; + nextEventCycles = min_u32(nextEventCycles, wave->nextTimeHighCycles); } else { ClearGPIO(wave->gpioMask); if (wave->gpio16Mask) { GP16O &= ~wave->gpio16Mask; } - wave->nextServiceCycle = now + wave->timeLowCycles; - wave->timeLowCycles = wave->nextTimeLowCycles; - nextEventCycles = min_u32(nextEventCycles, wave->timeLowCycles); + wave->nextServiceCycle = now + wave->nextTimeLowCycles; + nextEventCycles = min_u32(nextEventCycles, wave->nextTimeLowCycles); } } else { uint32_t deltaCycles = wave->nextServiceCycle - now; From dfc7a72a72e9776b94745dee8f7031d8e73e1fa1 Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Tue, 24 Apr 2018 19:47:46 -0700 Subject: [PATCH 4/6] Remove stepper control, add callback function Remove the stepper code due to its size. If it's included here it will take a large amount of IRAM in *all* sketches, even those which do not use it. Add in a traditional callback option, which will allow Stepper to be put in as a library, only being linked when necessary. --- cores/esp8266/core_esp8266_waveform.c | 286 ++------------------------ cores/esp8266/core_esp8266_waveform.h | 21 +- 2 files changed, 32 insertions(+), 275 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform.c b/cores/esp8266/core_esp8266_waveform.c index 0ccc16ec5a..07f4775deb 100644 --- a/cores/esp8266/core_esp8266_waveform.c +++ b/cores/esp8266/core_esp8266_waveform.c @@ -4,7 +4,7 @@ Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. - The code idea is to have a programmable waveform generator with a unique + The core idea is to have a programmable waveform generator with a unique high and low period (defined in microseconds). TIMER1 is set to 1-shot mode and is always loaded with the time until the next edge of any live waveforms or Stepper motors. @@ -106,51 +106,8 @@ static Waveform waveform[] = { {0, 0, 0, 1, 0, 0, 0, 0} // GPIO16 }; +static uint32_t (*timer1CB)() = NULL;; -// Maximum umber of moves per stepper queue -#define STEPPERQUEUESIZE 16 - -// Stepper generator can send # of steps with given velocity and linear acceleration -// Can do any piecewise linear acceleration profile -typedef struct { - float j_2; // Jerk value/2, pulses/sec/sec/sec - float a0; // Initial acceleration, pulses/sec/sec - float v0; // Initial velocity, pulses/sec - unsigned pulses : 16; // Total # of pulses to emit - unsigned sync : 1; // Wait for all channels to have a sync before popping next move - unsigned dir : 1; // CW=0 or CCW=1 -} Motion; - -// Minimum clock cycles per step. -#define MINSTEPCYCLES (4000) -// Maxmimum clock cycles per step. -#define MAXSTEPCYCLES (1000000000) - -// Pre-allocated circular buffer -typedef struct { - Motion * move; - uint32_t nextEventCycles; - - // Copied from head for fast access - uint16_t pulses; // Pulses remaining - uint32_t cumCycles; // The "t" in our equations - float j_2; // j/2 (jerk divided by 2.0) - float a0; // Initial constant acceleration - float v0; // Initial constant velocity - unsigned sync : 1; // Wait for all steppers to finish before advancing - unsigned dir : 1; // CCW or CW - - unsigned finished : 1; // Done with all moves, on next hit pop another motion - unsigned gpioPin : 5; // Allow all GPIOs, we're going to be slow no matter what - - uint8_t readPtr; // Read queue index - uint8_t writePtr; // Push queue spot - uint8_t validEntries; // How many entries present -} StepperQueue; - -static volatile StepperQueue *stepQ = NULL; -static volatile uint8_t stepQCnt = 0; -static uint8_t stepDirPin = 16; // The weird one // Helper functions static inline ICACHE_RAM_ATTR uint32_t MicrosecondsToCycles(uint32_t microseconds) { @@ -207,130 +164,21 @@ static void deinitTimer() { timerRunning = false; } -// Called by the IRQ to move the next Motion to the head -static inline ICACHE_RAM_ATTR void PopStepper(int i) { - StepperQueue *q = (StepperQueue *)&stepQ[i]; - if (q->validEntries == 0) { - q->sync = false; - q->finished = true; - q->nextEventCycles = 0; - return; - } - q->finished = false; - - Motion *head = &q->move[q->readPtr]; - q->pulses = head->pulses; - q->cumCycles = 0; - q->j_2 = head->j_2; - q->a0 = head->a0; - q->v0 = head->v0; - q->sync = head->sync; - q->dir = head->dir; - q->nextEventCycles = 0; // (uint32_t)((clockCyclesPerMicrosecond()*1000000.0) / q->v0); - q->readPtr = (q->readPtr + 1) & (STEPPERQUEUESIZE - 1); - q->validEntries--; -} - -// Called by the user to detach a stepper and free memory -int removeStepper(uint8_t pin) { - sei(); - for (int i = 0; i < stepQCnt; i++) { - if (stepQ[i].gpioPin == pin) { - memmove((void*)&stepQ[i], (void*)&stepQ[i + 1], (stepQCnt - i - 1) * sizeof(stepQ[0])); - stepQ = (StepperQueue*)realloc((void*)stepQ, (stepQCnt - 1) * sizeof(stepQ[0])); - stepQCnt--; - cli(); - return true; - } - } - cli(); - return false; -} - -// Add a stepper move, return TRUE on success, FALSE on out of space -// Calling application needs to ensure IRQS are disabled for the call! -static int PushStepper(uint8_t gpioPin, const Motion *nextMove) { - StepperQueue *q = NULL; - int i; - // gpioPin already validated in calling function - sei(); - // Determine which queue it should be on, or maybe add one if needed - for (i = 0; i < stepQCnt; i++) { - if (stepQ[i].gpioPin == gpioPin) { - q = (StepperQueue *)&stepQ[i]; - break; - } - } - if (q == NULL) { - // Make the stepper move array - Motion *move = (Motion*)malloc(sizeof(stepQ[0].move) * STEPPERQUEUESIZE); - if (!move) { - cli(); - return false; +// Set a callback. Pass in NULL to stop it +void setTimer1Callback(uint32_t (*fn)()) { + timer1CB = fn; + if (!timerRunning && fn) { + initTimer(); + } else if (timerRunning && !fn) { + int cnt = 0; + for (size_t i = 0; i < countof(waveform); i++) { + cnt += waveform[i].enabled ? 1 : 0; } - - // Add a queue - StepperQueue *newStepQ = (StepperQueue*)realloc((void*)stepQ, (stepQCnt + 1) * sizeof(stepQ[0])); - if (!newStepQ) { - cli(); - free(move); - return false; + if (!cnt) { + deinitTimer(); } - stepQ = newStepQ; - q = (StepperQueue*) & (stepQ[stepQCnt]); // The one just added - memset(q, 0, sizeof(*q)); - q->move = move; - q->readPtr = 0; - q->writePtr = 0; - q->validEntries = 0; - q->gpioPin = gpioPin; - q->finished = true; - i = stepQCnt; - stepQCnt++; - } - // Have queue ready, can we fit this new one in? - if (q->validEntries == STEPPERQUEUESIZE) { - return false; - } - - // Store and record - q->move[q->writePtr] = *nextMove; // Copy actual values - q->validEntries++; - q->writePtr = (q->writePtr + 1) & (STEPPERQUEUESIZE - 1); - if (!timerRunning) { - initTimer(); - ReloadTimer(10); // Cause an interrupt post-haste - } - if (!q->sync) { - PopStepper(i); // If there's only this in the queue and we're not waiting for sync, start it up - } - cli(); - return true; -} - -// Called by user to add a PWL move to the queue, returns false if there is no space left -int pushStepperMove(uint8_t pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0) { - if (pin > 16) { - return false; } - - Motion m; - m.pulses = pulses; - m.j_2 = j / 2.0; - m.a0 = a0; - m.v0 = v0; - m.sync = sync ? 1 : 0; - m.dir = dir ? 1 : 0; - return PushStepper(pin, &m); -} - -// Assign a pin to stepper DIR -int setStepperDirPin(uint8_t pin) { - if (pin > 16) { - return false; - } - stepDirPin = pin; - return true; + ReloadTimer(MicrosecondsToCycles(1)); // Cause an interrupt post-haste } // Start up a waveform on a pin, or change the current one. Will change to the new @@ -368,7 +216,7 @@ int stopWaveform(uint8_t pin) { for (size_t i = 0; i < countof(waveform); i++) { if (((pin == 16) && waveform[i].gpio16Mask) || ((pin != 16) && (waveform[i].gpioMask == 1<dir != dir || q->finished) { - continue; - } - q->cumCycles += deltaCycles; - uint32_t newNextEventCycles = q->nextEventCycles - deltaCycles; - if ((deltaCycles >= q->nextEventCycles) || (newNextEventCycles <= CYCLES_FLUFF)) { - // If there are no more pulses in the current motion, try to pop next one here - if (!q->pulses) { - if (!q->sync) { - PopStepper(i); - if (!q->pulses) { - // We tried to pop, but there's nothing left, done! - continue; - } - // We will generate the first pulse of the next motion later on in this loop - } else { - // Sync won't allow us to advance here. The main loop will have to - // call this loop *again* after all are processed the first time - // if we are sync'd. - q->nextEventCycles = 0; // Don't look at this for timing - continue; - } - } - pulseGPIO |= 1 << q->gpioPin; - q->pulses--; - - // Forgive me for going w/FP. The dynamic range for fixed math would require many 64 bit multiplies - static const float cycPerSec = 1000000.0 * clockCyclesPerMicrosecond(); - static const float secPerCyc = 1.0 / (1000000.0 * clockCyclesPerMicrosecond()); - float t = q->cumCycles * secPerCyc; - float newVel = ((q->j_2 * t) + q->a0) * t + q->v0; - uint32_t newPeriodCycles = (uint32_t)(cycPerSec / newVel); - if (newPeriodCycles < MINSTEPCYCLES) { - newPeriodCycles = MINSTEPCYCLES; - } else if (newPeriodCycles > MAXSTEPCYCLES) { - newPeriodCycles = MAXSTEPCYCLES; - } - q->nextEventCycles = newPeriodCycles; - } else { - q->nextEventCycles = newNextEventCycles; - } - } - ClearGPIO(toClear & 0xffff); - if (toClear & 0x80000) { - GP16O &= ~1; // RMW is slow, only do if needed - } - SetGPIO(pulseGPIO & 0xffff); - if (pulseGPIO & 0x80000) { - GP16O |= 1; // RMW is slow, only do if needed - } - toClear = pulseGPIO; -} - - -static ICACHE_RAM_ATTR uint32_t ProcessSteppers(uint32_t deltaCycles) { - ClearGPIOPin(stepDirPin); - AdvanceSteppers(deltaCycles, 0); - SetGPIOPin(stepDirPin); - AdvanceSteppers(deltaCycles, 1); - - // Check for sync, and if all set and 0 steps clear it - bool haveSync = true; - bool wantSync = false; - for (int i = 0; i < stepQCnt; i++) { - haveSync &= stepQ[i].sync && stepQ[i].finished; - wantSync |= stepQ[i].sync; - } - - if (wantSync && haveSync) { // Sync requested, and hit - for (int i = 0; i < stepQCnt; i++) { - PopStepper(i); - stepQ[i].nextEventCycles = 1; // Cause the pulse to fire immediately - } - // Hokey, but here only now could we know it was time to fire everyone again - ClearGPIOPin(stepDirPin); - AdvanceSteppers(deltaCycles, 0); - SetGPIOPin(stepDirPin); - AdvanceSteppers(deltaCycles, 1); - } - // When's the next event? - uint32_t nextEventCycles = MicrosecondsToCycles(MAXIRQUS); - for (size_t i = 0; i < stepQCnt; i++) { - if (stepQ[i].nextEventCycles) { - nextEventCycles = min_u32(nextEventCycles, stepQ[i].nextEventCycles); - } - } - - return nextEventCycles; -} - static ICACHE_RAM_ATTR void timer1Interrupt() { uint32_t nextEventCycles; #if F_CPU == 160000000 @@ -547,8 +296,8 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { } } - if (stepQCnt) { - nextEventCycles = min_u32(nextEventCycles, ProcessSteppers(deltaCycles)); + if (timer1CB) { + nextEventCycles = min_u32(nextEventCycles, timer1CB()); } #if F_CPU == 160000000 @@ -568,4 +317,3 @@ static ICACHE_RAM_ATTR void timer1Interrupt() { ReloadTimer(nextEventCycles); } - diff --git a/cores/esp8266/core_esp8266_waveform.h b/cores/esp8266/core_esp8266_waveform.h index 5f618b2769..3e9844af22 100644 --- a/cores/esp8266/core_esp8266_waveform.h +++ b/cores/esp8266/core_esp8266_waveform.h @@ -4,7 +4,7 @@ Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. - The code idea is to have a programmable waveform generator with a unique + The core idea is to have a programmable waveform generator with a unique high and low period (defined in microseconds). TIMER1 is set to 1-shot mode and is always loaded with the time until the next edge of any live waveforms or Stepper motors. @@ -61,17 +61,26 @@ extern "C" { #endif +// Start or change a waveform of the specified high and low times on specific pin. +// If runtimeUS > 0 then automatically stop it after that many usecs. +// Returns true or false on success or failure. int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS); +// Stop a waveform, if any, on the specified pin. +// Returns true or false on success or failure. int stopWaveform(uint8_t pin); -int setStepperDirPin(uint8_t pin); -int pushStepperMove(uint8_t pin, int dir, int sync, uint16_t pulses, float j, float a0, float v0); -int removeStepper(uint8_t pin); +// Add a callback function to be called on *EVERY* timer1 trigger. The +// callback returns the number of microseconds until the next desired call. +// However, since it is called every timer1 interrupt, it may be called +// again before this period. It should therefore use the ESP Cycle Counter +// to determine whether or not to perform an operation. +// Pass in NULL to disable the callback and, if no other waveforms being +// generated, stop the timer as well. +// Make sure the CBN function has the ICACHE_RAM_ATTR decorator. +void setTimer1Callback(uint32_t (*fn)()); #ifdef __cplusplus } #endif - #endif - From cd8de435873d2d871e9228ac39bb513e7fe838de Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Mon, 14 May 2018 20:54:18 -0700 Subject: [PATCH 5/6] Fix comments to reflect removed stepper code --- cores/esp8266/core_esp8266_waveform.c | 25 +++++-------------------- cores/esp8266/core_esp8266_waveform.h | 27 ++++++--------------------- 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/cores/esp8266/core_esp8266_waveform.c b/cores/esp8266/core_esp8266_waveform.c index 07f4775deb..0454c768b2 100644 --- a/cores/esp8266/core_esp8266_waveform.c +++ b/cores/esp8266/core_esp8266_waveform.c @@ -1,39 +1,24 @@ /* - esp8266_waveform - General purpose waveform generation and stepper motor - control, supporting outputs on all pins in parallel. + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. The core idea is to have a programmable waveform generator with a unique high and low period (defined in microseconds). TIMER1 is set to 1-shot mode and is always loaded with the time until the next edge of any live - waveforms or Stepper motors. + waveforms. - Up to one waveform generator or stepper driver per pin supported. + Up to one waveform generator per pin supported. Each waveform generator is synchronized to the ESP cycle counter, not the timer. This allows for removing interrupt jitter and delay as the counter always increments once per 80MHz clock. Changes to a waveform are - contiguous and only take effect on the next low->high waveform transition, + contiguous and only take effect on the next waveform transition, allowing for smooth transitions. This replaces older tone(), analogWrite(), and the Servo classes. - The stepper driver supports a constant jerk (da/dt, in ticks/sec^3) to - produce a smooth acceleration curve and as well as a constant initial - acceleration and velocity and number of pulses. - - The stepper driver can also force all steppers to wait for completion - by the use of a SYNC option (i.e. when completing a move where the X and - Y need to hit at one point before moving to the next X and Y). - - The user application is responsible for actually calculating the proper - motion profiles (a general-purpose S-curve planner is left as an exercise - for the reader). - - The steppers should be wired using an A4988 or DRV8825 controller and - with a single, shared direction pin. - Everywhere in the code where "cycles" is used, it means ESP.getCycleTime() cycles, not TIMER1 cycles (which may be 2 CPU clocks @ 160MHz). diff --git a/cores/esp8266/core_esp8266_waveform.h b/cores/esp8266/core_esp8266_waveform.h index 3e9844af22..24ce91fb36 100644 --- a/cores/esp8266/core_esp8266_waveform.h +++ b/cores/esp8266/core_esp8266_waveform.h @@ -1,39 +1,24 @@ /* - esp8266_waveform - General purpose waveform generation and stepper motor - control, supporting outputs on all pins in parallel. + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. The core idea is to have a programmable waveform generator with a unique high and low period (defined in microseconds). TIMER1 is set to 1-shot mode and is always loaded with the time until the next edge of any live - waveforms or Stepper motors. + waveforms. - Up to one waveform generator or stepper driver per pin supported. + Up to one waveform generator per pin supported. Each waveform generator is synchronized to the ESP cycle counter, not the timer. This allows for removing interrupt jitter and delay as the counter always increments once per 80MHz clock. Changes to a waveform are - contiguous and only take effect on the next low->high waveform transition, + contiguous and only take effect on the next waveform transition, allowing for smooth transitions. This replaces older tone(), analogWrite(), and the Servo classes. - The stepper driver supports a constant jerk (da/dt, in ticks/sec^3) to - produce a smooth acceleration curve and as well as a constant initial - acceleration and velocity and number of pulses. - - The stepper driver can also force all steppers to wait for completion - by the use of a SYNC option (i.e. when completing a move where the X and - Y need to hit at one point before moving to the next X and Y). - - The user application is responsible for actually calculating the proper - motion profiles (a general-purpose S-curve planner is left as an exercise - for the reader). - - The steppers should be wired using an A4988 or DRV8825 controller and - with a single, shared direction pin. - Everywhere in the code where "cycles" is used, it means ESP.getCycleTime() cycles, not TIMER1 cycles (which may be 2 CPU clocks @ 160MHz). @@ -76,7 +61,7 @@ int stopWaveform(uint8_t pin); // to determine whether or not to perform an operation. // Pass in NULL to disable the callback and, if no other waveforms being // generated, stop the timer as well. -// Make sure the CBN function has the ICACHE_RAM_ATTR decorator. +// Make sure the CB function has the ICACHE_RAM_ATTR decorator. void setTimer1Callback(uint32_t (*fn)()); #ifdef __cplusplus From fcb9c05b74a74d2be64a4192a7429372f9035ddb Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Thu, 24 May 2018 07:35:56 -0700 Subject: [PATCH 6/6] Add a tone(float) method, set pin to out in tone() Allow tone to take a double/float as wekk as an integer frequency. Set the pin to an output on a tone() call to match original code. --- cores/esp8266/Arduino.h | 1 + cores/esp8266/Tone.cpp | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/cores/esp8266/Arduino.h b/cores/esp8266/Arduino.h index 5424d3baf3..b274f17121 100644 --- a/cores/esp8266/Arduino.h +++ b/cores/esp8266/Arduino.h @@ -279,6 +279,7 @@ unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout = 100000 unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout = 1000000L); void tone(uint8_t _pin, unsigned int frequency, unsigned long duration = 0); +void tone(uint8_t _pin, double frequency, unsigned long duration = 0); void noTone(uint8_t _pin); // WMath prototypes diff --git a/cores/esp8266/Tone.cpp b/cores/esp8266/Tone.cpp index 95a291ff45..a3345bb612 100644 --- a/cores/esp8266/Tone.cpp +++ b/cores/esp8266/Tone.cpp @@ -27,26 +27,49 @@ // Which pins have a tone running on them? static uint32_t _toneMap = 0; -void tone(uint8_t _pin, unsigned int frequency, unsigned long duration) { + +static void _startTone(uint8_t _pin, uint32_t high, uint32_t low, unsigned long duration) { if (_pin > 16) { return; } + pinMode(_pin, OUTPUT); + + high = std::max(high, (uint32_t)100); + low = std::max(low, (uint32_t)100); + + if (startWaveform(_pin, high, low, (uint32_t) duration * 1000)) { + _toneMap |= 1 << _pin; + } +} + + +void tone(uint8_t _pin, unsigned int frequency, unsigned long duration) { if (frequency == 0) { noTone(_pin); - return; + } else { + uint32_t period = 1000000L / frequency; + uint32_t high = period / 2; + uint32_t low = period - high; + _startTone(_pin, high, low, duration); } +} - uint32_t halfCycle = 500000L / frequency; - if (halfCycle < 100) { - halfCycle = 100; - } - if (startWaveform(_pin, halfCycle, halfCycle, (uint32_t) duration * 1000)) { - _toneMap |= 1 << _pin; +// Separate tone(float) to hopefully not pull in floating point libs unless +// it's called with a float. +void tone(uint8_t _pin, double frequency, unsigned long duration) { + if (frequency < 1.0) { // FP means no exact comparisons + noTone(_pin); + } else { + double period = 1000000.0 / frequency; + uint32_t high = (uint32_t)((period / 2.0) + 0.5); + uint32_t low = (uint32_t)(period + 0.5) - high; + _startTone(_pin, high, low, duration); } } + void noTone(uint8_t _pin) { if (_pin > 16) { return;