diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa12536..8dc47f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Unreleased: -- fix: RampControl.cpp declared the log2 timer-frequency variables `static`, conflicting with their `extern` declaration in RampGenerator.h. That combination is ill-formed C++ (a static redeclaration cannot follow a non-static/extern one in the same translation unit) and breaks the build for any platform whose TICKS_PER_S isn't exactly 16000000 or 21000000 - e.g. an AVR board not running at 16/21 MHz - the moment SUPPORT_LOG2_TIMER_FREQ_VARIABLES is exercised +- Teensy 4.0/4.1 support (EXPERIMENTAL): new pd_teensy backend, up to 16 steppers via the i.MX RT1062's QuadTimer modules, any digital pin usable as step/dir (see README "Teensy 4.0/4.1" section). Verified on real Teensy 4.0 hardware: single stepper through a DM556 driver board reaching the driver's full 200 kHz rated speed with zero missed steps, and all 16 steppers across all 4 QuadTimer modules running simultaneously with no cross-talk +- fix: teensy_queue.cpp's step-timing ISR was missing a Cortex-M7 `dsb` memory barrier after clearing the QuadTimer interrupt flags, matching one luni64/TeensyStep4's own code has for the same reason - without it, step rate would plateau then the motor would hang as interrupt frequency increased during acceleration +- fix (shared with #372): RampControl.cpp declared the log2 timer-frequency variables `static`, conflicting with their `extern` declaration. Ill-formed C++, breaks the build for any platform whose TICKS_PER_S isn't exactly 16000000 or 21000000 - needed for this port's 9.375 MHz tick rate +- fix (shared with #372): those extern declarations also lived in the wrong header (RampGenerator.h, included after the header that already needs them) - moved into RampCalculator.h +- fix (shared with #372): `log2_timer_freq_div_sqrt_of_2` was computed with a copy-paste of the formula for `log2_timer_freq_square_div_2` (squaring the frequency instead of dividing by sqrt(2)). Caused the stepper to "hang" for minutes on any ramp start/reversal from standstill, unless setLinearAcceleration() happened to be in use (which masks it) - found via real hardware testing on this port 1.3.3: - moveTimed(0, duration): pause uses last direction XOR prepare_revert (default false does not toggle DIR; true pauses in the opposite direction) diff --git a/README.md b/README.md index c5e9f232..7d75e257 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Arduino core 3.1.0 will support ESP-IDF V5.3.0 (based on RC1) ## Overview This is a high speed alternative for the [AccelStepper library](http://www.airspayce.com/mikem/arduino/AccelStepper/). -Supported are avr (ATmega 168/328/P, ATmega2560, ATmega32u4), atmelsam due, Microchip SAMD51, esp32, esp32s2, esp32s3, esp32c3, esp32c6, esp32p4, Raspberry pi pico and pico2. +Supported are avr (ATmega 168/328/P, ATmega2560, ATmega32u4), atmelsam due, Microchip SAMD51, esp32, esp32s2, esp32s3, esp32c3, esp32c6, esp32p4, Raspberry pi pico and pico2, and (experimentally) Teensy 4.0/4.1. For memory footprint information across supported architectures, see [Memory Report](https://github.com/gin66/FastAccelStepper/blob/master/extras/doc/memory_report.md). @@ -96,6 +96,7 @@ The `src/` directory is organized into the following subdirectories: | `pd_pico/` | RP2040/RP2350 pulse driver: PIO-based stepper control | | `pd_sam/` | SAM Due pulse driver: timer-based stepper control | | `pd_samd/` | SAMD51 pulse driver: TCC PWM-based stepper control | +| `pd_teensy/` | Teensy 4.0/4.1 pulse driver: QuadTimer-based stepper control (EXPERIMENTAL) | | `pd_test/` | Test platform pulse driver: PC-based testing simulation | | `fas_queue/` | Queue implementation: command queue management | | `fas_ramp/` | Ramp calculation: acceleration/deceleration curves | @@ -291,6 +292,14 @@ Tested with max two stepper motors with 50 kHz step rate by clazarowitz * SAMD21 is not supported (compile error) * requires an Arduino SAMD51 core (uses the variant pin table); tested with the Adafruit SAMD core +### Teensy 4.0/4.1 (EXPERIMENTAL) + +* up to 16 stepper motors: 4 QuadTimer (TMR) modules x 4 channels each +* step/dir pins are plain GPIO (toggled via `digitalWriteFast()`), not restricted to specific muxed pins - any digital pin works +* two ISR calls per step (rising + falling edge), each hardware-timed by a QuadTimer compare match, the same principle this library's AVR backend uses +* Steppers' command queue depth: 32 +* tested on a real Teensy 4.0 with a DM556 industrial stepper driver: a single axis was speed-swept up to 200 kHz with no missed steps (verified by marking the shaft, since there is no pulse-counter/encoder feedback to check this in software), and all 16 steppers were run simultaneously across all 4 QuadTimer modules with no cross-talk between channels. Pulse width/edge timing has not been checked on a scope or logic analyzer yet - if you have one, please verify and report back what you find + ## Usage The library is in use with A4988, but other driver ICs should work, too. @@ -706,4 +715,5 @@ As mentioned by kthod861 in [Issue #110](https://github.com/gin66/FastAccelStepp - Thanks Sam W for pull request (https://github.com/gin66/FastAccelStepper/pull/351) samx3-due: Fix null result from `tryAllocateQueue()` - Thanks drayde for pull request (https://github.com/gin66/FastAccelStepper/pull/366) Fix ESP-IDF 6 RMT header include - Thanks Loriland for the Microchip SAMD51 port (https://github.com/gin66/FastAccelStepper/issues/317) +- Thanks ARDUTECH0 for the experimental Teensy 4.0/4.1 port (https://github.com/gin66/FastAccelStepper/pull/374) diff --git a/examples/TeensyBringUp/TeensyBringUp.ino b/examples/TeensyBringUp/TeensyBringUp.ino new file mode 100644 index 00000000..c05595af --- /dev/null +++ b/examples/TeensyBringUp/TeensyBringUp.ino @@ -0,0 +1,109 @@ +#include "FastAccelStepper.h" + +// Minimal bring-up/test sketch for the EXPERIMENTAL Teensy 4.0/4.1 backend +// (src/pd_teensy). This backend has NOT been verified on real hardware - +// see the warning at the top of src/pd_teensy/pd_config.h. Use this sketch +// to check it on your board before relying on it in a real project. +// +// What to check: +// 1. Does it compile and upload at all? +// 2. Does the stepper actually move back and forth, the right number of +// steps (watch the position printed to Serial, or count physical steps)? +// 3. Put a scope/logic analyzer on the STEP pin: pulse HIGH time should be +// close to FAS_TEENSY_PULSE_WIDTH_US (default 3us, see pd_config.h), and +// the step-to-step period should ramp smoothly (S-curve corners from +// setLinearAcceleration(), see below) without jitter or missed edges, +// especially near the top speed configured below. +// 4. Try raising setSpeedInHz()/setAcceleration() to find where it actually +// breaks (missed steps, hangs) - the 50k steps/s ceiling in +// _pd_initVars() is an untested guess, not a measurement. +// +// Any digital pin works for step/dir/enable on this backend (unlike +// SAMD/SAM, it's not restricted to specific muxed pins) - change the pin +// numbers below to whatever you've actually wired up. + +#define stepPinStepper 2 +#define dirPinStepper 3 +#define enablePinStepper 4 + +FastAccelStepperEngine engine = FastAccelStepperEngine(); +FastAccelStepper *stepper = NULL; + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 3000) { + // wait for the USB serial monitor, but don't hang forever if none is + // attached + } + Serial.println("START - FastAccelStepper Teensy 4.x bring-up test"); + + engine.init(); + + stepper = engine.stepperConnectToPin(stepPinStepper); + if (stepper) { + Serial.println("HAVE STEPPER"); + stepper->setDirectionPin(dirPinStepper); + stepper->setEnablePin(enablePinStepper); + stepper->setAutoEnable(true); + + stepper->setSpeedInHz(2000); // start conservative, raise later + stepper->setAcceleration(4000); + stepper->setLinearAcceleration(50); // small S-curve corners to check + + Serial.print("TICKS_PER_S="); + Serial.println((uint32_t)TICKS_PER_S); + Serial.print("max_speed getMaxSpeedInHz()="); + Serial.println(stepper->getMaxSpeedInHz()); + Serial.print("enablePin state right after setEnablePin() (expect HIGH=1, disabled): "); + Serial.println(digitalRead(enablePinStepper)); + } else { + while (true) { + Serial.println("NO STEPPER - check stepPinStepper is a valid pin"); + delay(1000); + } + } +} + +uint32_t last_report_ms = 0; + +void loop() { + if (!stepper->isRunning()) { + int32_t pos = stepper->getCurrentPosition(); + Serial.print("Reached standstill at position "); + Serial.println(pos); + + int32_t target = (pos <= 0) ? 3200 : -3200; + Serial.print("Moving to "); + Serial.println(target); + MoveResultCode res = stepper->moveTo(target); + if (!moveIsOk(res)) { + Serial.print("moveTo() error: "); + Serial.println(toString(res)); + } + } + + // Periodic status while running, so you can watch position/speed advance + // in the Serial Monitor without needing a scope for a first sanity check. + uint32_t now = millis(); + if (stepper->isRunning() && (now - last_report_ms >= 200)) { + last_report_ms = now; + Serial.print("pos="); + Serial.print(stepper->getCurrentPosition()); + Serial.print(" target="); + Serial.print(stepper->targetPos()); + Serial.print(" stepsToStop="); + Serial.print(stepper->stepsToStop()); + Serial.print(" speed(mHz)="); + Serial.print(stepper->getCurrentSpeedInMilliHz()); + Serial.print(" rampState="); + Serial.print(stepper->rampState()); + Serial.print(" queueRunning="); + Serial.print(stepper->isQueueRunning()); + Serial.print(" queueEmpty="); + Serial.print(stepper->isQueueEmpty()); + Serial.print(" enablePin="); + Serial.print(digitalRead(enablePinStepper)); + Serial.print(" queueEntries="); + Serial.println(stepper->queueEntries()); + } +} diff --git a/examples/TeensyEnableBlinkTest/TeensyEnableBlinkTest.ino b/examples/TeensyEnableBlinkTest/TeensyEnableBlinkTest.ino new file mode 100644 index 00000000..b5ce014e --- /dev/null +++ b/examples/TeensyEnableBlinkTest/TeensyEnableBlinkTest.ino @@ -0,0 +1,70 @@ +#include "FastAccelStepper.h" + +// Minimal, decisive test of FastAccelStepper's enable-pin control, +// completely decoupled from auto-enable timing and from any driver board - +// wire an LED directly to enablePinStepper (through a resistor to GND) to +// watch it. +// +// This calls the library's own public enableOutputs()/disableOutputs() +// directly, alternating every 2 seconds, printing what it did and what +// digitalRead() sees right after. If the LED doesn't change in sync with +// these prints, the problem is confirmed to be in the library's pin +// control - if it does, the library is fine and something else (timing, +// wiring, or how another sketch used it) was the real cause. + +#define stepPinStepper 2 +#define dirPinStepper 3 +#define enablePinStepper 4 + +FastAccelStepperEngine engine = FastAccelStepperEngine(); +FastAccelStepper *stepper = NULL; + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 3000) { + } + Serial.println("START - enable pin blink test"); + + engine.init(); + stepper = engine.stepperConnectToPin(stepPinStepper); + if (!stepper) { + while (true) { + Serial.println("NO STEPPER - check stepPinStepper is a valid pin"); + delay(1000); + } + } + stepper->setDirectionPin(dirPinStepper); + stepper->setEnablePin(enablePinStepper); + // NOT calling setAutoEnable() - we're driving enable manually here so + // there's no ambiguity about when it fires. + + Serial.print("Right after setEnablePin(), digitalRead(pin)="); + Serial.println(digitalRead(enablePinStepper)); + Serial.println( + "Expect HIGH=1 here (disabled is the safe default) - the LED should " + "be ON now if wired pin->resistor->LED->GND."); +} + +bool enabled = false; +uint32_t lastToggleMs = 0; + +void loop() { + uint32_t now = millis(); + if (now - lastToggleMs >= 2000) { + lastToggleMs = now; + enabled = !enabled; + bool ok; + if (enabled) { + ok = stepper->enableOutputs(); + Serial.print("enableOutputs() -> "); + } else { + ok = stepper->disableOutputs(); + Serial.print("disableOutputs() -> "); + } + Serial.print(ok ? "true" : "false"); + Serial.print(" digitalRead(pin) now = "); + Serial.print(digitalRead(enablePinStepper)); + Serial.println(enabled ? " (expect LOW=0, LED should turn OFF)" + : " (expect HIGH=1, LED should turn ON)"); + } +} diff --git a/examples/TeensyMaxSpeedFinder/TeensyMaxSpeedFinder.ino b/examples/TeensyMaxSpeedFinder/TeensyMaxSpeedFinder.ino new file mode 100644 index 00000000..3d20a36e --- /dev/null +++ b/examples/TeensyMaxSpeedFinder/TeensyMaxSpeedFinder.ino @@ -0,0 +1,255 @@ +#include "FastAccelStepper.h" + +// Finds the real maximum reliable step rate on YOUR board/motor/driver +// combination, for the EXPERIMENTAL Teensy 4.0/4.1 backend (src/pd_teensy). +// +// WHY THIS EXISTS: software alone cannot detect missed steps on an open-loop +// stepper (no encoder feedback) - the queue/ramp generator will happily +// report "move complete" even if the motor physically stalled or skipped +// pulses under load. So this test relies on YOU watching the shaft. +// +// HOW TO USE: +// 1. Mark the motor shaft (piece of tape, paint dot, anything visible) so +// you can see its exact rotational position. +// 2. Upload this sketch and open the Serial Monitor at 115200 baud. +// 3. Type 'y' to start. For each speed level, the motor moves +// forward far enough to actually reach and briefly cruise at that +// speed (see stepsForLeg()), pauses, then moves back the same +// distance - ending exactly where it started IF (and only if) no +// steps were lost in either direction. Watch the mark. +// While it runs, status lines print every 200ms - if those stop +// appearing, the last line printed pinpoints exactly where it got +// stuck; share it. +// 4. After each leg finishes, the sketch asks: +// y - shaft returned to the exact same position -> try the +// next (faster) speed level +// n - shaft did NOT return to the same position (steps were +// lost) -> stop here; the LAST speed that answered 'y' +// is your real, hardware-verified ceiling +// 5. Whatever speed you stopped at, set that as the argument to +// setAbsoluteSpeedLimit() in your real project (see pd_teensy/pd_config.h +// for SUPPORT_UNSAFE_ABS_SPEED_LIMIT_SETTING), with a safety margin below +// it (e.g. 80% of the last good value) - don't run right at the edge. +// +// This tests SPEED only, at a fixed, modest acceleration - it does not by +// itself find the max acceleration your motor/load can handle. If a leg +// fails, it could be losing steps because of the acceleration ramp too fast +// for your motor/load's torque, not the step rate itself. If you suspect +// that, lower ACCELERATION below and re-run from the same speed level. + +#define stepPinStepper 2 +#define dirPinStepper 3 +#define enablePinStepper 4 + +// Distance per leg is computed per speed level (see stepsForLeg()) - a +// FIXED step count can't reach a high target speed at all: too short a +// distance silently turns a high-speed level into a low triangular blip +// that never actually reaches the speed being "tested", making the result +// meaningless. Higher ACCELERATION here means less distance/time needed +// to reach a given speed level (e.g. at 200000 steps/s^2, the 200000 Hz +// level needs ~300000 steps and ~1.5s per leg; lower this and the +// distance/time grows a lot - d,t ~ 1/ACCELERATION for a given speed). +// This is a test default, not a claim about your motor's real max +// acceleration - lower it if you suspect losses are from acceleration +// being too aggressive for your torque, not from the step rate itself. +// Lowered from 200000 to 20000 for round 2: round 1 (200000 steps/s^2) +// found steps lost at 125000 Hz with a hard/fast ramp - this checks +// whether that was a motor torque/acceleration limit rather than a real +// step-rate ceiling, by giving the motor a much gentler ramp to the same +// speeds. Legs will take longer now (t ~ 1/ACCELERATION for a given +// speed) - expect several seconds per leg at the higher levels. +#define ACCELERATION 20000 // steps/s^2 + +// Round 2: 5000-100000 Hz already confirmed good in round 1 (with an even +// harder ramp), so start straight at 100000 to save time. +const uint32_t speedLevels[] = {100000, 125000, 150000, 175000, 200000}; +const uint8_t numLevels = sizeof(speedLevels) / sizeof(speedLevels[0]); + +FastAccelStepperEngine engine = FastAccelStepperEngine(); +FastAccelStepper *stepper = NULL; + +enum class TestState { + waitingToStart, + movingForward, + pausing, + movingBack, + waitingConfirm, + stopped, + done +}; +TestState state = TestState::waitingToStart; +uint8_t level = 0; +uint32_t stateChangeMs = 0; +uint32_t lastReportMs = 0; +int32_t currentLegSteps = 0; + +// Distance needed to accelerate up to speedHz and back down to 0 is +// speedHz^2 / ACCELERATION (both phases together); add 50% more so there +// is an actual cruise segment at speedHz to observe, not just a triangular +// peak that brushes the target speed for an instant. +int32_t stepsForLeg(uint32_t speedHz) { + uint32_t accelDecelSteps = + (uint32_t)(((uint64_t)speedHz * speedHz) / ACCELERATION); + uint32_t steps = accelDecelSteps + accelDecelSteps / 2; + if (steps < 200) { + steps = 200; // sane minimum for the lowest test levels + } + return (int32_t)steps; +} + +void printStatus(const char *label) { + Serial.print(label); + Serial.print(" pos="); + Serial.print(stepper->getCurrentPosition()); + Serial.print(" target="); + Serial.print(stepper->targetPos()); + Serial.print(" stepsToStop="); + Serial.print(stepper->stepsToStop()); + Serial.print(" speed(mHz)="); + Serial.print(stepper->getCurrentSpeedInMilliHz()); + Serial.print(" rampState="); + Serial.print(stepper->rampState()); + Serial.print(" queueRunning="); + Serial.print(stepper->isQueueRunning()); + Serial.print(" queueEmpty="); + Serial.print(stepper->isQueueEmpty()); + Serial.print(" queueEntries="); + Serial.println(stepper->queueEntries()); +} + +void startLeg() { + currentLegSteps = stepsForLeg(speedLevels[level]); + Serial.println(); + Serial.print("=== Testing "); + Serial.print(speedLevels[level]); + Serial.print(" Hz, "); + Serial.print(currentLegSteps); + Serial.println(" steps per leg ==="); + Serial.println("Watch the shaft mark now."); + stepper->setSpeedInHz(speedLevels[level]); + stepper->setAcceleration(ACCELERATION); + MoveResultCode res = stepper->move(currentLegSteps); + Serial.print("move() forward returned: "); + Serial.println(toString(res)); + state = TestState::movingForward; + stateChangeMs = millis(); +} + +void checkSerialForYesNo() { + while (Serial.available()) { + char c = Serial.read(); + if (c == 'y' || c == 'Y') { + if (state == TestState::waitingConfirm) { + Serial.println("-> continuing to next speed level"); + level++; + if (level >= numLevels) { + state = TestState::done; + return; + } + } else { + Serial.println("-> starting"); + } + startLeg(); + return; + } + if (c == 'n' || c == 'N') { + Serial.println( + "-> stopping here. The last speed that printed 'y' is your " + "hardware-verified ceiling."); + state = TestState::stopped; + return; + } + } +} + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 3000) { + } + Serial.println("START - Teensy max-speed finder (non-blocking)"); + + engine.init(); + stepper = engine.stepperConnectToPin(stepPinStepper); + if (!stepper) { + while (true) { + Serial.println("NO STEPPER - check stepPinStepper is a valid pin"); + delay(1000); + } + } + stepper->setDirectionPin(dirPinStepper); + stepper->setEnablePin(enablePinStepper); + stepper->setAutoEnable(true); + + // Raise the ceiling so setSpeedInHz() up to the top test level is even + // accepted - see pd_teensy/pd_config.h, SUPPORT_UNSAFE_ABS_SPEED_LIMIT_SETTING. + // Guarded here because this method only exists on platforms that define + // that macro (e.g. not atmelsam) - the CI build matrix compiles every + // example against every platform. +#if defined(SUPPORT_UNSAFE_ABS_SPEED_LIMIT_SETTING) + stepper->setAbsoluteSpeedLimit( + (uint16_t)fas_max((uint32_t)1, TICKS_PER_S / 400000)); +#endif + + Serial.println( + "Mark the shaft now if you haven't. Type 'y' when ready to " + "start."); +} + +void loop() { + uint32_t now = millis(); + bool wantReport = (now - lastReportMs >= 200); + + switch (state) { + case TestState::waitingToStart: + case TestState::waitingConfirm: + checkSerialForYesNo(); + break; + + case TestState::movingForward: + if (wantReport) { + lastReportMs = now; + printStatus("[fwd]"); + } + if (!stepper->isRunning()) { + Serial.println("Forward leg finished."); + state = TestState::pausing; + stateChangeMs = now; + } + break; + + case TestState::pausing: + if (now - stateChangeMs >= 300) { + MoveResultCode res = stepper->move(-currentLegSteps); + Serial.print("move() back returned: "); + Serial.println(toString(res)); + state = TestState::movingBack; + } + break; + + case TestState::movingBack: + if (wantReport) { + lastReportMs = now; + printStatus("[back]"); + } + if (!stepper->isRunning()) { + Serial.println("Back leg finished."); + Serial.println( + "Did the shaft mark return to EXACTLY the same position as " + "before this leg started? (y/n)"); + state = TestState::waitingConfirm; + } + break; + + case TestState::stopped: + break; + + case TestState::done: + if (wantReport) { + lastReportMs = now; + Serial.println( + "All speed levels passed! Add higher values to speedLevels[] " + "and re-flash to keep narrowing in."); + } + break; + } +} diff --git a/examples/TeensyMoveAndPause/TeensyMoveAndPause.ino b/examples/TeensyMoveAndPause/TeensyMoveAndPause.ino new file mode 100644 index 00000000..97f18731 --- /dev/null +++ b/examples/TeensyMoveAndPause/TeensyMoveAndPause.ino @@ -0,0 +1,77 @@ +#include "FastAccelStepper.h" + +// Move, sit idle for a few seconds, move again - repeating. Unlike +// TeensyBringUp.ino (which starts the next move the instant the previous +// one finishes, so the enable pin never gets a chance to go back to +// "disabled"), this sketch has a real idle gap between moves, so you can +// actually watch the enable pin (and, if wired, an LED on it) switch: +// HIGH (disabled) while idle <-> LOW (enabled) while moving +// (default polarity - LOW enables the motor. See setEnablePin() docs if +// your driver needs the opposite.) + +#define stepPinStepper 2 +#define dirPinStepper 3 +#define enablePinStepper 4 + +#define IDLE_MS 3000 // how long to sit still between moves + +FastAccelStepperEngine engine = FastAccelStepperEngine(); +FastAccelStepper *stepper = NULL; + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 3000) { + } + Serial.println("START - move, pause, move"); + + engine.init(); + stepper = engine.stepperConnectToPin(stepPinStepper); + if (!stepper) { + while (true) { + Serial.println("NO STEPPER - check stepPinStepper is a valid pin"); + delay(1000); + } + } + stepper->setDirectionPin(dirPinStepper); + stepper->setEnablePin(enablePinStepper); + stepper->setAutoEnable(true); + // Disable (almost) as soon as the motor stops, instead of waiting for + // the default delay, so the HIGH/disabled state during the idle gap + // below is easy to see. + stepper->setDelayToDisable(50); + + stepper->setSpeedInHz(2000); + stepper->setAcceleration(4000); +} + +void printEnableState(const char *label) { + Serial.print(label); + Serial.print(" enablePin="); + Serial.println(digitalRead(enablePinStepper)); +} + +void loop() { + int32_t pos = stepper->getCurrentPosition(); + int32_t target = (pos <= 0) ? 3200 : -3200; + + Serial.print("Moving to "); + Serial.print(target); + Serial.println(" ..."); + MoveResultCode res = stepper->moveTo(target, true); // built-in blocking mode + Serial.print("moveTo() returned: "); + Serial.println(toString(res)); + Serial.print("Reached "); + Serial.println(stepper->getCurrentPosition()); + + Serial.print("Idling for "); + Serial.print(IDLE_MS); + Serial.println(" ms - watch the enable pin/LED now."); + uint32_t idleStart = millis(); + while (millis() - idleStart < IDLE_MS) { + static uint32_t lastPrint = 0; + if (millis() - lastPrint >= 500) { + lastPrint = millis(); + printEnableState("[idle]"); + } + } +} diff --git a/examples/TeensyMultiStepperTest/TeensyMultiStepperTest.ino b/examples/TeensyMultiStepperTest/TeensyMultiStepperTest.ino new file mode 100644 index 00000000..c59d53db --- /dev/null +++ b/examples/TeensyMultiStepperTest/TeensyMultiStepperTest.ino @@ -0,0 +1,146 @@ +#include "FastAccelStepper.h" + +// Multi-stepper test for the EXPERIMENTAL Teensy 4.0/4.1 backend +// (src/pd_teensy). Defaults to the FULL claimed capacity - 16 steppers, +// spanning all 4 QuadTimer modules (TMR1..TMR4) - to find out how many +// this board actually drives, not just how many the code claims to +// support. +// +// WHY THIS MATTERS: this backend allocates one QuadTimer channel per +// stepper, 4 channels per module, in connection order. Steppers 0-3 land +// on module 0 (TMR1), 4-7 on module 1 (TMR2), 8-11 on module 2 (TMR3), +// 12-15 on module 3 (TMR4) - each module has its own register block and +// NVIC interrupt vector. A bug specific to a given module, or a resource +// conflict once all 4 are active simultaneously, would only show up once +// every module is actually exercised. +// +// Real motors are NOT required - this tests software correctness (do all +// 16 channels across all 4 modules run independently, with no missed +// deadlines even under the combined interrupt load of all of them at +// once), not torque/missed-steps under load (measured separately per +// axis with examples/TeensyMaxSpeedFinder). Driving raw pins into LEDs, +// a logic analyzer, or nothing at all is enough. +// +// PINS: 16 unique step pins and 16 unique dir pins are needed (step pins +// specifically MUST be unique - each one claims its own QuadTimer +// channel); the enable pin is shared across all 16 here (this library +// supports sharing enable/dir pins between motors) to keep the pin count +// within Teensy 4.0's 40 available pins. Change ENABLE_PIN or the arrays +// below to match your wiring; set NUM_STEPPERS lower if you don't have +// (or don't want to wire up) all 16. +// +// HOW TO USE: +// 1. Set NUM_STEPPERS below (up to 16) and check the pin arrays match +// your wiring (or that the pins are simply safe to toggle/unconnected). +// 2. Upload, open the Serial Monitor at 115200. +// 3. Every stepper immediately starts an independent, continuous +// back-and-forth move at its own speed/acceleration/distance +// (deliberately different per axis, computed from its index so no two +// are ever in lockstep) - this maximizes the chance of exposing any +// cross-talk between channels/modules. +// 4. A status table prints every 500 ms: one row per stepper, showing +// which TMR module/channel it's on, position, live speed, and +// queue/ramp state. Every row should keep updating independently; +// none should ever show a stuck rampState or frozen position while +// queueRunning=1 (that pattern is exactly the "stuck ramp" bug this +// library had - see CHANGELOG). Let it run for several minutes and +// confirm every row - especially the ones on modules 2-4 (index +// >= 4) - keeps moving smoothly the whole time. + +#define NUM_STEPPERS 16 // up to 16 (4 QuadTimer modules x 4 channels) + +// clang-format off +const uint8_t STEP_PIN[16] = { + 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 14, 15, 16, 17, 18, // 13 skipped (onboard LED) +}; +const uint8_t DIR_PIN[16] = { + 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, +}; +// clang-format on +#define ENABLE_PIN 35 // shared by all steppers - this library supports that + +FastAccelStepperEngine engine = FastAccelStepperEngine(); +FastAccelStepper *steppers[NUM_STEPPERS]; +int32_t travelSteps[NUM_STEPPERS]; + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 3000) { + } + Serial.println("START - Teensy multi-stepper test"); + Serial.print("NUM_STEPPERS="); + Serial.println(NUM_STEPPERS); + + engine.init(); + + for (uint8_t i = 0; i < NUM_STEPPERS; i++) { + FastAccelStepper *s = engine.stepperConnectToPin(STEP_PIN[i]); + steppers[i] = s; + Serial.print("stepper "); + Serial.print(i); + Serial.print(" (step pin "); + Serial.print(STEP_PIN[i]); + Serial.print(") -> TMR module "); + Serial.print(i >> 2); + Serial.print(" channel "); + Serial.print(i & 0x03); + Serial.print(": "); + if (!s) { + Serial.println("FAILED to connect - pin busy or out of queues?"); + continue; + } + Serial.println("OK"); + s->setDirectionPin(DIR_PIN[i]); + s->setEnablePin(ENABLE_PIN); + s->setAutoEnable(true); + + // Deliberately different per axis, and not simple multiples of each + // other, so the steppers are never in lockstep - makes any timing + // cross-talk between channels/modules much easier to notice. + uint32_t speedHz = 2000 + (uint32_t)i * 733; + uint32_t accel = 4000 + (uint32_t)i * 1500; + travelSteps[i] = 1800 + (int32_t)(i % 5) * 300; + + s->setSpeedInHz(speedHz); + s->setAcceleration(accel); + s->moveTo(travelSteps[i]); + } + Serial.println(); +} + +uint32_t lastReportMs = 0; + +void loop() { + uint32_t now = millis(); + if (now - lastReportMs < 500) { + return; + } + lastReportMs = now; + + Serial.println( + "ax mod ch pos target speed(mHz) rampState qRun qEmpty " + "qEntries"); + for (uint8_t i = 0; i < NUM_STEPPERS; i++) { + FastAccelStepper *s = steppers[i]; + if (!s) { + continue; + } + if (!s->isRunning()) { + // reverse direction and go again + int32_t target = + (s->getCurrentPosition() <= 0) ? travelSteps[i] : -travelSteps[i]; + s->moveTo(target); + } + char line[110]; + snprintf(line, sizeof(line), + "%2d %1d %1d %8ld %8ld %10ld %9d %4d %6d %8d", (int)i, + (int)(i >> 2), (int)(i & 0x03), (long)s->getCurrentPosition(), + (long)s->targetPos(), (long)s->getCurrentSpeedInMilliHz(), + (int)s->rampState(), (int)s->isQueueRunning(), + (int)s->isQueueEmpty(), (int)s->queueEntries()); + Serial.println(line); + } + Serial.println(); +} diff --git a/extras/scripts/build-platformio.sh b/extras/scripts/build-platformio.sh index 6f513fed..b9c9825d 100755 --- a/extras/scripts/build-platformio.sh +++ b/extras/scripts/build-platformio.sh @@ -41,6 +41,10 @@ do echo $p: Skipping $i for $p due to space constraints continue fi + if [ "$p" = "nanoatmega168" ] && [ "$i" = "pio_dirs/TeensyMultiStepperTest" ]; then + echo $p: Skipping $i for $p due to space constraints + continue + fi echo $p: $i (cd $i;pio run -s -e $p) done diff --git a/library.properties b/library.properties index 11cc1d2c..575b9c80 100644 --- a/library.properties +++ b/library.properties @@ -7,6 +7,6 @@ sentence=A high speed stepper library for Atmega 168/168p/328/328p (nano), 32u4 paragraph=Drive stepper motors with acceleration/deceleration profile up to 50 kSteps/s (Atmega) and 200kSteps/s (esp32). url=https://github.com/gin66/FastAccelStepper repository=https://github.com/gin66/FastAccelStepper.git -architectures=avr,esp32,sam,samd,rp2040,rp2350 +architectures=avr,esp32,sam,samd,rp2040,rp2350,teensy category=Device Control dot_a_linkage=true diff --git a/src/fas_arch/arduino_teensy.h b/src/fas_arch/arduino_teensy.h new file mode 100644 index 00000000..bad5aae8 --- /dev/null +++ b/src/fas_arch/arduino_teensy.h @@ -0,0 +1,14 @@ +#ifndef FAS_ARCH_ARDUINO_TEENSY_H +#define FAS_ARCH_ARDUINO_TEENSY_H + +// Teensy 4.0/4.1 (i.MX RT1062, ARM Cortex-M7), via Teensyduino. +// EXPERIMENTAL - see pd_teensy/pd_config.h and pd_teensy/teensy_queue.cpp. +#define SUPPORT_TEENSY4 + +// this is an arduino platform, so include the Arduino.h header file +#include + +#define fasEnableInterrupts interrupts +#define fasDisableInterrupts noInterrupts + +#endif /* FAS_ARCH_ARDUINO_TEENSY_H */ diff --git a/src/fas_arch/common.h b/src/fas_arch/common.h index bf671ecf..44cdc85d 100644 --- a/src/fas_arch/common.h +++ b/src/fas_arch/common.h @@ -75,6 +75,11 @@ struct queue_end_s { #include "fas_arch/espidf_esp32.h" #include "pd_esp32/pd_config.h" +#elif defined(__IMXRT1062__) +// Teensy 4.0/4.1, via Teensyduino. EXPERIMENTAL - see pd_teensy/pd_config.h. +#include "fas_arch/arduino_teensy.h" +#include "pd_teensy/pd_config.h" + #elif defined(ARDUINO_ARCH_SAM) // SAM-architecture #include "fas_arch/arduino_sam.h" diff --git a/src/fas_queue/stepper_queue.h b/src/fas_queue/stepper_queue.h index 2734c89f..74dd9d1d 100644 --- a/src/fas_queue/stepper_queue.h +++ b/src/fas_queue/stepper_queue.h @@ -13,6 +13,8 @@ #include "pd_avr/avr_queue.h" #elif defined(TEST) #include "pd_test/test_queue.h" +#elif defined(SUPPORT_TEENSY4) +#include "pd_teensy/teensy_queue.h" #elif defined(SUPPORT_SAM) #include "pd_sam/sam_queue.h" #elif defined(SUPPORT_SAMD51) diff --git a/src/fas_ramp/RampCalculator.h b/src/fas_ramp/RampCalculator.h index 52c2f564..325dbec8 100644 --- a/src/fas_ramp/RampCalculator.h +++ b/src/fas_ramp/RampCalculator.h @@ -20,6 +20,13 @@ #define TICKS_TO_US(u32) ((u32) / 21) #else #define SUPPORT_LOG2_TIMER_FREQ_VARIABLES +// Declared here (not in RampGenerator.h, which includes this file before +// its own copy of these declarations) because the inline methods below - +// evaluated as soon as this header is parsed - already need them. +// Defined (non-static, exactly once) in RampControl.cpp. +extern log2_value_t log2_timer_freq; +extern log2_value_t log2_timer_freq_div_sqrt_of_2; +extern log2_value_t log2_timer_freq_square_div_2; #define LOG2_TICKS_PER_S log2_timer_freq #define LOG2_TICKS_PER_S_DIV_SQRT_OF_2 log2_timer_freq_div_sqrt_of_2 #define LOG2_ACCEL_FACTOR log2_timer_freq_square_div_2 diff --git a/src/fas_ramp/RampControl.cpp b/src/fas_ramp/RampControl.cpp index 47afc952..7706539f 100644 --- a/src/fas_ramp/RampControl.cpp +++ b/src/fas_ramp/RampControl.cpp @@ -7,7 +7,7 @@ #include "fas_arch/common.h" #ifdef SUPPORT_LOG2_TIMER_FREQ_VARIABLES -// Definitions for the `extern` declarations in RampGenerator.h. Must have +// Definitions for the `extern` declarations in RampCalculator.h. Must have // external linkage (no `static`): RampGenerator.cpp, a separate translation // unit, also uses these via the LOG2_TICKS_PER_S* macros in RampCalculator.h. log2_value_t log2_timer_freq; @@ -23,8 +23,17 @@ void ramp_rw_s::init() { void init_ramp_module() { #ifdef SUPPORT_LOG2_TIMER_FREQ_VARIABLES log2_timer_freq = log2_from((uint32_t)TICKS_PER_S); - log2_timer_freq_div_sqrt_of_2 = - log2_shr(log2_multiply(log2_timer_freq, log2_timer_freq), 1); + // freq / sqrt(2): -0.5 in log2 space. 1 raw unit = 1/512 (see + // Log2Representation.h), so -0.5 is -256 raw units. Previously this used + // log2_multiply(log2_timer_freq, log2_timer_freq) - a copy-paste of the + // line below, computing freq^2 instead - making this identical to + // log2_timer_freq_square_div_2 and producing wildly wrong (huge) tick + // counts for the small-steps/near-standstill case (calculate_ticks() / + // calculate_ramp_steps()'s non-cubic branch, only reachable when + // setLinearAcceleration() is unused, i.e. s_h == 0). This was the cause + // of the stepper "hanging" for minutes on a ramp start/reversal from + // standstill during Teensy 4.0 bring-up. + log2_timer_freq_div_sqrt_of_2 = (log2_value_t)(log2_timer_freq - 256); log2_timer_freq_square_div_2 = log2_shr(log2_square(log2_timer_freq), 1); #endif } diff --git a/src/fas_ramp/RampGenerator.h b/src/fas_ramp/RampGenerator.h index 7bd38495..21e556d1 100644 --- a/src/fas_ramp/RampGenerator.h +++ b/src/fas_ramp/RampGenerator.h @@ -7,11 +7,9 @@ class FastAccelStepper; -#ifdef SUPPORT_LOG2_TIMER_FREQ_VARIABLES -extern log2_value_t log2_timer_freq; -extern log2_value_t log2_timer_freq_div_sqrt_of_2; -extern log2_value_t log2_timer_freq_square_div_2; -#endif +// log2_timer_freq and friends (when SUPPORT_LOG2_TIMER_FREQ_VARIABLES is +// defined) are declared in RampCalculator.h, included above - that header's +// own inline methods need them declared before this point, not after. class RampGenerator { private: diff --git a/src/pd_teensy/pd_config.h b/src/pd_teensy/pd_config.h new file mode 100644 index 00000000..2ea8589c --- /dev/null +++ b/src/pd_teensy/pd_config.h @@ -0,0 +1,88 @@ +// pd_teensy/pd_config.h - Teensy 4.x (i.MX RT1062) platform configuration +// +// !! EXPERIMENTAL !! +// This backend is adapted from two sources that were verified on real +// hardware: +// - the register-level QuadTimer (TMR) sequence in luni64/TeensyStep4 +// (MIT licensed), a proven stepper library for Teensy 4.x +// - the queue/ramp protocol already shipped here for SAMD51 +// (src/pd_samd), which this backend's structure mirrors +// It has since been tested on a real Teensy 4.0 with a DM556 industrial +// stepper driver: a single axis was speed-swept up to 200 kHz with no +// missed steps (verified by marking the shaft - there is no pulse +// counter/encoder feedback to check this in software), and all 16 +// steppers were run simultaneously across all 4 QuadTimer modules with +// no cross-talk between channels. Pulse width/edge timing has not been +// checked on a scope or logic analyzer yet - if you have one, please +// verify and report back. +// +// This file defines Teensy-4.x-specific constants for the +// FastAccelStepper library: +// - Queue topology (MAX_STEPPER, NUM_QUEUES, QUEUE_LEN) +// - Timing constants (TICKS_PER_S, MIN_CMD_TICKS, delays) +// - Feature flags for Teensy-4.x-specific behavior +// +// Included by fas_arch/common.h during platform dispatch. + +#ifndef PD_TEENSY_CONFIG_H +#define PD_TEENSY_CONFIG_H + +// i.MX RT1062 has 4 QuadTimer (TMR) modules, 4 channels each. Unlike +// SAMD/SAM, the step pin is NOT muxed to the timer's own output - the ISR +// toggles it directly via digitalWriteFast() (see pd_teensy/teensy_queue.cpp), +// so any digital pin can be used, same as ESP32. +#define MAX_STEPPER 16 +#define NUM_QUEUES 16 +#define QUEUE_LEN 32 + +// The QuadTimer modules are clocked from the IPG peripheral clock, which +// Teensyduino configures to 150 MHz regardless of F_CPU/overclocking +// (confirmed via TeensyStep4's TMR.h). FAS_TEENSY_TMR_PRESCALE (0..7) +// divides that by 2^prescale (see teensy_queue.cpp) to get the tick rate: +// 0 -> 150 MHz (65535 ticks =~ 0.44 ms per queue entry) +// 4 -> 9.375 MHz (default: =~ 7 ms per queue entry) +// 7 -> 1.17 MHz (=~ 56 ms per queue entry) +// A smaller prescale gives finer timing resolution (matters most for very +// high step rates and for how smoothly the ramp can approximate its curve), +// at the cost of a shorter max duration per single queue entry - still +// comfortably above the 4 ms ramp tick down to prescale 2 (37.5 MHz, +// =~1.75 ms/entry). Untested: worth sweeping this while measuring actual +// achievable step rate/jitter on a scope, see pd_teensy warning above. +#ifndef FAS_TEENSY_TMR_PRESCALE +#define FAS_TEENSY_TMR_PRESCALE 4 +#endif +#if (FAS_TEENSY_TMR_PRESCALE < 0) || (FAS_TEENSY_TMR_PRESCALE > 7) +#error "FAS_TEENSY_TMR_PRESCALE must be 0..7" +#endif +// +// This does not match the two precomputed fast-path constants in +// RampCalculator.h (16 MHz / 21 MHz), so the generic runtime log2 timer +// frequency path is used automatically (see SUPPORT_LOG2_TIMER_FREQ_VARIABLES +// in RampCalculator.h/RampControl.cpp) - no extra setup needed here. +#define TICKS_PER_S (150000000L >> FAS_TEENSY_TMR_PRESCALE) +#define MIN_CMD_TICKS (TICKS_PER_S / 5000) +#define MIN_DIR_DELAY_US (MIN_CMD_TICKS / (TICKS_PER_S / 1000000)) +#define MAX_DIR_DELAY_US (65535 / (TICKS_PER_S / 1000000)) +#define DELAY_MS_BASE 4 + +#define DEBUG_LED_HALF_PERIOD 50 + +#define noop_or_wait + +#define SUPPORT_QUEUE_ENTRY_END_POS_U16 + +#define NEED_GENERIC_GET_CURRENT_POSITION + +// Lets the application raise/lower the per-stepper max speed ceiling set +// below via setAbsoluteSpeedLimit() - useful here since the default ceiling +// is an untested guess (see teensy_queue.cpp _pd_initVars()). +#define SUPPORT_UNSAFE_ABS_SPEED_LIMIT_SETTING + +// Step pulse high time in microseconds. Kept above DRV8825's 1.9 us +// minimum with margin for the tick-rounding below (2 us truncates to +// under 1.9 us at 9.375 MHz ticks). +#ifndef FAS_TEENSY_PULSE_WIDTH_US +#define FAS_TEENSY_PULSE_WIDTH_US 3 +#endif + +#endif /* PD_TEENSY_CONFIG_H */ diff --git a/src/pd_teensy/teensy_queue.cpp b/src/pd_teensy/teensy_queue.cpp new file mode 100644 index 00000000..6648f5de --- /dev/null +++ b/src/pd_teensy/teensy_queue.cpp @@ -0,0 +1,298 @@ +#include "fas_queue/stepper_queue.h" + +#if defined(SUPPORT_TEENSY4) + +// ============================================================================ +// Teensy 4.x (i.MX RT1062) pulse driver. EXPERIMENTAL - tested on real +// hardware, but pulse width/edge timing not yet scope-verified. See +// pd_teensy/pd_config.h for context. +// +// Register sequence (CTRL/CSCTRL/COMP1/CMPLD1 setup, the "two-phase" toggle +// technique of alternating COMP1 between the pulse width and the remaining +// period) is adapted from luni64/TeensyStep4's TMR.h (MIT licensed), the +// only publicly available, hardware-verified reference found for driving +// these timers this way: +// https://github.com/luni64/TeensyStep4/blob/master/src/TS4/timers/Teensy4/TMR/TMR.h +// The step/dir pin toggling via digitalWriteFast() with a runtime pin +// number is likewise taken from TeensyStep4's stepperbase.h/.cpp, which +// uses that exact pattern from real ISRs. +// +// Queue/ramp protocol integration (grace period before stopping, dead +// period so a late-arriving command is picked up seamlessly, one +// FastAccelStepperEngine::manageSteppers() tick every ~4 ms) mirrors this +// library's own pd_samd backend, adapted from SAMD51's fully-hardware +// double-buffered PWM to this driver's ISR-per-edge model (closer to how +// the AVR backend uses a hardware compare match to time each edge). +// ============================================================================ + +// FAS_TEENSY_TMR_PRESCALE (0..7, default 4) lives in pd_config.h - it also +// derives TICKS_PER_S, so the two must always move together. + +#define PULSE_TICKS \ + ((uint16_t)(((uint32_t)(FAS_TEENSY_PULSE_WIDTH_US) * TICKS_PER_S) / 1000000L)) + +// NVIC priority, 0 (highest) .. 255 (lowest), in steps of 16 (Cortex-M7 on +// i.MX RT1062 implements 4 priority bits; values not a multiple of 16 get +// rounded down by the hardware, so stick to multiples of 16 to keep the +// numbers meaningful). Step edges must preempt the ~4 ms ramp tick (queue +// refill), or a slow/late refill under load could show up as timing jitter +// on the step pin. Untested: worth confirming on a scope under load. +#ifndef FAS_TEENSY_STEP_ISR_PRIORITY +#define FAS_TEENSY_STEP_ISR_PRIORITY 16 +#endif +#ifndef FAS_TEENSY_RAMP_TICK_PRIORITY +#define FAS_TEENSY_RAMP_TICK_PRIORITY 208 +#endif + +static FastAccelStepperEngine* fas_engine = NULL; +static IntervalTimer fas_ramp_timer; + +static void fas_ramp_tick_isr() { + if (fas_engine != NULL) { + fas_engine->manageSteppers(); + } +} + +void fas_init_engine(FastAccelStepperEngine* engine) { + fas_engine = engine; + fas_ramp_timer.begin(fas_ramp_tick_isr, (unsigned int)(DELAY_MS_BASE * 1000)); + fas_ramp_timer.priority(FAS_TEENSY_RAMP_TICK_PRIORITY); +} + +// ---------------------------------------------------------------------------- +// Step generation +// ---------------------------------------------------------------------------- + +// ISR dispatch: [module][channel] -> owning queue +static StepperQueue* queue_for_channel[4][4] = {{NULL}}; + +static IMXRT_TMR_t* const tmr_module_regs[4] = { + (IMXRT_TMR_t*)IMXRT_TMR1_ADDRESS, (IMXRT_TMR_t*)IMXRT_TMR2_ADDRESS, + (IMXRT_TMR_t*)IMXRT_TMR3_ADDRESS, (IMXRT_TMR_t*)IMXRT_TMR4_ADDRESS}; + +static const IRQ_NUMBER_t tmr_module_irq[4] = {IRQ_QTIMER1, IRQ_QTIMER2, + IRQ_QTIMER3, IRQ_QTIMER4}; + +// Consume the queue entry at read_idx `rp` (queue must be non-empty): +// apply a pending direction change, then schedule the immediate next +// compare event - either the rising edge of a step pulse (scheduled +// _pulse_ticks out) or the full duration of a pause entry. +void StepperQueue::primeFromQueue(uint8_t rp) { + struct queue_entry* e = &entry[rp & QUEUE_LEN_MASK]; + if (e->toggle_dir) { + e->toggle_dir = 0; + digitalWriteFast(dirPin, e->dirPinState); + } + if (e->hasSteps) { + _remaining_ticks = (e->ticks > _pulse_ticks) + ? (uint16_t)(e->ticks - _pulse_ticks) + : (uint16_t)1; + digitalWriteFast(_step_pin, HIGH); + _pulse_phase = true; + _regs->COMP1 = _pulse_ticks; + _regs->CMPLD1 = _pulse_ticks; + } else { + _pulse_phase = false; + _regs->COMP1 = e->ticks; + _regs->CMPLD1 = e->ticks; + } + if (e->steps > 1) { + e->steps--; + } else { + read_idx = rp + 1; + } +} + +// Called once per QuadTimer compare-match event for this channel (the +// TCF1 flag itself is cleared by the module-level dispatcher below). +// +// State machine (mirrors TeensyStep4's TmrTimer::ISR, adapted for this +// library's repeat-count queue entries): +// _pulse_phase == true: pin is HIGH. This event is exactly _pulse_ticks +// after the rising edge: lower the pin and schedule the remainder of +// the step period (computed by primeFromQueue() when the pulse started). +// _pulse_phase == false: pin is LOW. This event marks the end of the +// previous inter-step gap (or the very start of a run): look at the +// queue and schedule what's next. +void StepperQueue::handleCompareMatch() { + if (_pulse_phase) { + digitalWriteFast(_step_pin, LOW); + _pulse_phase = false; + _regs->COMP1 = _remaining_ticks; + _regs->CMPLD1 = _remaining_ticks; + return; + } + + uint8_t rp = read_idx; + if (rp == next_write_idx) { + if (_noMoreCommands) { + // Grace period elapsed without new commands: stop. + _regs->CTRL = 0; + _isRunning = false; + _noMoreCommands = false; + return; + } + // Queue drained: stage one dead period (no pulse) so a command + // arriving within it continues the stream seamlessly. + _noMoreCommands = true; + _regs->COMP1 = MIN_CMD_TICKS; + _regs->CMPLD1 = MIN_CMD_TICKS; + return; + } + _noMoreCommands = false; + primeFromQueue(rp); +} + +// The Cortex-M7 has a write buffer, so clearing CSCTRL.TCF1 is not +// guaranteed to have reached the peripheral by the time the ISR returns - +// without a barrier, the NVIC can see the interrupt as still pending and +// the flag-clear can appear to "not have happened" from the next ISR's +// point of view, corrupting the edge-toggle state machine. TeensyStep4's +// TMRModule::ISR() carries the same barrier with the same reasoning. +#define TEENSY_TMR_ISR(N) \ + static void teensyTmrIsr##N() { \ + for (uint8_t ch = 0; ch < 4; ch++) { \ + StepperQueue* q = queue_for_channel[N][ch]; \ + if (q == NULL) { \ + continue; \ + } \ + IMXRT_TMR_CH_t* regs = &tmr_module_regs[N]->CH[ch]; \ + if (regs->CSCTRL & TMR_CSCTRL_TCF1) { \ + regs->CSCTRL &= ~TMR_CSCTRL_TCF1; \ + q->handleCompareMatch(); \ + } \ + } \ + asm volatile("dsb"); \ + } + +TEENSY_TMR_ISR(0) +TEENSY_TMR_ISR(1) +TEENSY_TMR_ISR(2) +TEENSY_TMR_ISR(3) + +static void (*const tmr_module_isr[4])() = {teensyTmrIsr0, teensyTmrIsr1, + teensyTmrIsr2, teensyTmrIsr3}; + +// ---------------------------------------------------------------------------- +// Queue protocol +// ---------------------------------------------------------------------------- + +bool StepperQueue::isValidStepPin(uint8_t step_pin) { + if (step_pin == PIN_UNDEFINED) { + return false; + } + for (uint8_t i = 0; i < NUM_QUEUES; i++) { + if (fas_queue[i]._step_pin == step_pin) { + return false; // already claimed by another queue + } + } + return true; +} + +void StepperQueue::init(uint8_t queue_num, uint8_t step_pin) { + digitalWrite(step_pin, LOW); + pinMode(step_pin, OUTPUT); + + _queue_num = queue_num; + _step_pin = step_pin; + _pulse_ticks = PULSE_TICKS; + _pulse_phase = false; + + uint8_t mod = queue_num >> 2; // 0..3 -> TMR1..TMR4 + uint8_t ch = queue_num & 0x03; // 0..3 + _tmr_module = mod; + _tmr_channel = ch; + _regs = &tmr_module_regs[mod]->CH[ch]; + + _regs->CTRL = 0; + _regs->CNTR = 0; + _regs->LOAD = 0; + _regs->COMP1 = _pulse_ticks; + _regs->CMPLD1 = _pulse_ticks; + _regs->CSCTRL = 0; + _regs->SCTRL = 0; + + queue_for_channel[mod][ch] = this; + + static bool module_isr_attached[4] = {false, false, false, false}; + if (!module_isr_attached[mod]) { + attachInterruptVector(tmr_module_irq[mod], tmr_module_isr[mod]); + NVIC_SET_PRIORITY(tmr_module_irq[mod], FAS_TEENSY_STEP_ISR_PRIORITY); + NVIC_ENABLE_IRQ(tmr_module_irq[mod]); + module_isr_attached[mod] = true; + } + + connect(); +} + +void StepperQueue::connect() { + pinMode(_step_pin, OUTPUT); + digitalWriteFast(_step_pin, LOW); + _connected = true; +} + +void StepperQueue::disconnect() { + digitalWriteFast(_step_pin, LOW); + _connected = false; +} + +void StepperQueue::startQueue() { + if (_isRunning) { + return; + } + uint8_t rp = read_idx; + if (rp == next_write_idx) { + return; // nothing to do (addQueueEntry guards this already) + } + + _isRunning = true; + _noMoreCommands = false; + + _regs->CTRL = 0; + _regs->CNTR = 0; + _regs->LOAD = 0; + _regs->CSCTRL = 0; + _regs->SCTRL = 0; + + primeFromQueue(rp); + + _regs->CSCTRL |= TMR_CSCTRL_TCF1EN; + _regs->CTRL = + TMR_CTRL_CM(1) | TMR_CTRL_PCS(0b1000 | FAS_TEENSY_TMR_PRESCALE) | + TMR_CTRL_LENGTH; +} + +void StepperQueue::forceStop() { + noInterrupts(); + _regs->CTRL = 0; + _isRunning = false; + _noMoreCommands = false; + _pulse_phase = false; + digitalWriteFast(_step_pin, LOW); + read_idx = next_write_idx; + interrupts(); +} + +static uint8_t stepper_allocated_count = 0; + +StepperQueue* StepperQueue::tryAllocateQueue(FastAccelStepperEngine* engine, + uint8_t step_pin) { + (void)engine; + if (!isValidStepPin(step_pin)) { + return nullptr; + } + if (stepper_allocated_count >= MAX_STEPPER) { + return nullptr; + } + for (uint8_t i = 0; i < MAX_STEPPER; i++) { + if (fas_queue[i]._step_pin == PIN_UNDEFINED) { + fas_queue[i]._initVars(); + fas_queue[i].init(i, step_pin); + stepper_allocated_count++; + return &fas_queue[i]; + } + } + return nullptr; +} + +#endif // SUPPORT_TEENSY4 diff --git a/src/pd_teensy/teensy_queue.h b/src/pd_teensy/teensy_queue.h new file mode 100644 index 00000000..f84f66b3 --- /dev/null +++ b/src/pd_teensy/teensy_queue.h @@ -0,0 +1,110 @@ +#ifndef PD_TEENSY_QUEUE_H +#define PD_TEENSY_QUEUE_H + +#include "FastAccelStepper.h" +#include "fas_queue/base.h" +#include "imxrt.h" + +// Teensy 4.x (i.MX RT1062) pulse driver. EXPERIMENTAL - tested on real +// hardware, but pulse width/edge timing not yet scope-verified - see +// pd_config.h for context and pd_teensy/teensy_queue.cpp for the +// implementation and its references. +// +// Each stepper claims one channel of one of the 4 QuadTimer (TMR) modules. +// Unlike SAMD/SAM, the channel's own output pin is not used: the step (and, +// on direction changes, the dir) pin is toggled directly in the ISR via +// digitalWriteFast(), so any digital pin works, not just specific muxed +// ones. The QuadTimer channel is only used to time the two ISR calls a step +// needs (rising edge, then falling edge after the pulse width) precisely, +// the same way this library's AVR backend uses a hardware compare match to +// time each edge instead of busy-waiting. + +class StepperQueue : public StepperQueueBase { + public: +#include "../fas_queue/protocol.h" + + IMXRT_TMR_CH_t* _regs; // this channel's QuadTimer register block + uint8_t _tmr_module; // 0..3 (TMR1..TMR4) - ISR dispatch key + uint8_t _tmr_channel; // 0..3 within the module + uint8_t _queue_num; + uint8_t _step_pin; + uint16_t _pulse_ticks; // step pulse high time, in ticks + uint16_t _remaining_ticks; // ticks from falling edge to next rising edge + bool _pulse_phase; // true: pin is HIGH, next event lowers it + volatile bool _isRunning; + // One dead (pulse-less) period is inserted after the queue drains before + // the timer is stopped, so a command arriving late is picked up seamlessly + // (same grace-period idea as the SAMD backend). + volatile bool _noMoreCommands; + bool _connected; + + inline void _pd_initVars() { + _regs = NULL; + _step_pin = PIN_UNDEFINED; + _pulse_phase = false; + _isRunning = false; + _noMoreCommands = false; + _connected = false; + // UNVERIFIED placeholder ceiling (50k steps/s <=> 20us/step). Each step + // costs two ISR calls here (rising + falling edge), unlike SAMD's one. + // Measure on real hardware and adjust via setAbsoluteSpeedLimit() if + // needed - see SUPPORT_UNSAFE_ABS_SPEED_LIMIT_SETTING in pd_config.h. + max_speed_in_ticks = TICKS_PER_S / 50000; + } + + inline bool isRunning() const { return _isRunning; } + inline bool isReadyForCommands() const { return true; } + + void setDirPin(uint8_t dir_pin, bool _dirHighCountsUp) { + dirPin = dir_pin; + dirHighCountsUp = _dirHighCountsUp; + if ((dir_pin != PIN_UNDEFINED) && ((dir_pin & PIN_EXTERNAL_FLAG) == 0)) { + pinMode(dir_pin, OUTPUT); + } + } + + // ISR-side helpers (defined in teensy_queue.cpp) + void primeFromQueue(uint8_t rp); + void handleCompareMatch(); + + private: + static bool isValidStepPin(uint8_t step_pin); +}; + +// digitalWriteFast() accepts a runtime pin number on Teensyduino (it falls +// back to a fast table lookup, not a compile-time-only fast path) - the +// same call TeensyStep4 uses from its own step/dir ISRs. +#define SET_DIRECTION_PIN_STATE(q, high) \ + digitalWriteFast((q)->dirPin, (high) ? HIGH : LOW) + +#define SET_ENABLE_PIN_STATE(q, pin, high) \ + digitalWrite((pin), (high) ? HIGH : LOW) + +#define AFTER_SET_DIR_PIN_DELAY_US 5 + +inline AqeResultCode StepperQueue::addDirChangePauseToQueue( + const struct stepper_command_s* cmd, bool start, + uint16_t dir_change_delay_ticks) { + if (dir_change_delay_ticks == 0) { + return AQE_OK; + } + if ((cmd->steps == 0) && (cmd->ticks >= dir_change_delay_ticks)) { + return AQE_OK; + } + uint16_t pause_ticks = dir_change_delay_ticks; + if (cmd->steps == 0) { + pause_ticks = dir_change_delay_ticks - cmd->ticks; + } + struct stepper_command_s pause_cmd = { + .ticks = (uint16_t)fas_max(pause_ticks, MIN_CMD_TICKS), + .steps = 0, + .count_up = cmd->count_up}; + AqeResultCode res = addQueueEntry(&pause_cmd, start); + if (res != AQE_OK) { + return res; + } + _injected_pause_ticks = pause_cmd.ticks; + return AQE_DIR_CHANGE_PAUSE_INJECTED; +} + +#endif // PD_TEENSY_QUEUE_H