From 2c9bb4acd839b2e80c5ca1fcbf44e842c74a0f6f Mon Sep 17 00:00:00 2001 From: AndrewwSS <76648798+AndrewwSSS@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:46:18 +0200 Subject: [PATCH 1/4] docs: link scara-arm; drop project nicknames from comments --- README.md | 3 ++- platformio.ini | 9 +++------ test/test_ramp/test_ramp.cpp | 5 ++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a22e78c..8c9ce1d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ A header-only Arduino / PlatformIO library that drives a fleet of servos and stepper axes without blocking `loop()`: endstops, two-pass homing, speed ramps, soft limits and idle power release, with the pulse generation swappable per axis. -It runs two robotic arms and a CNC-shield test rig. +It drives [scara-arm](https://github.com/AndrewwSSS/scara-arm), a 3D-printed +SCARA robot, along with a servo arm and a test rig. diff --git a/platformio.ini b/platformio.ini index a49d5d2..b46f43b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,9 +1,6 @@ -; Host-side unit tests for the library: pio test -e native -; -; The headers compile against test/fake/Arduino.h instead of a real core, so the -; stepping, endstop and homing logic can be checked on the Mac without a board. -; Sketches that use the library (Ruka, BigHardDick) never read this file - they -; build the library from library.json against their own board. +; Host-side unit tests: pio test -e native +; Builds against test/fake/Arduino.h instead of a real core. Projects using the +; library ignore this file and build it from library.json. [platformio] default_envs = native diff --git a/test/test_ramp/test_ramp.cpp b/test/test_ramp/test_ramp.cpp index 8abf3f7..ec9815b 100644 --- a/test/test_ramp/test_ramp.cpp +++ b/test/test_ramp/test_ramp.cpp @@ -1,6 +1,5 @@ -// Characterisation of LinearRamp, on its own and as a Stepper actually paces -// pulses with it. Uses the ramp the BigHardDick rig runs: 1000 us down to a -// 200 us cruise over 1000 steps. +// LinearRamp on its own and as a Stepper paces pulses with it, using a +// 1000 us start, a 200 us cruise and 1000 ramp steps. #include #include From 232c045206939b7e1183058e083dca975a41f616 Mon Sep 17 00:00:00 2001 From: AndrewwSS <76648798+AndrewwSSS@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:53:57 +0200 Subject: [PATCH 2/4] docs: shorten engine and profile comments; move design notes to docs/ --- docs/engines.md | 36 +++++++++++++++++--- docs/ramping.md | 25 ++++++++++++++ src/EngineStepper.h | 24 ++++---------- src/FasEngine.h | 45 +++++++------------------ src/LinearRamp.h | 45 +++++-------------------- src/PolledEngine.h | 18 +++------- src/SoftEngine.h | 29 +++++----------- src/StepDirStepper.h | 16 +++------ src/StepEngine.h | 78 +++++++++++++------------------------------- src/StepProfile.h | 20 ++++-------- 10 files changed, 129 insertions(+), 207 deletions(-) diff --git a/docs/engines.md b/docs/engines.md index f9b389a..3fe53d9 100644 --- a/docs/engines.md +++ b/docs/engines.md @@ -40,10 +40,38 @@ A polled engine is only as steady as the loop that calls it: anything that holds (`stepsOnItsOwn()`) keeps going in between, and homing then checks on the axis once a millisecond instead of spinning on `update()`. -`FasEngine` is such an engine. Its STEP pulse is half the step interval, so it -takes no pulse width, and its ramp is the profile's `accelerationFor()`. A move -that reverses a running one ramps down first and then goes back, where a polled -engine reverses at full speed. See [`examples/FasEngine`](../examples/FasEngine/FasEngine.ino). +The engine, not the `Stepper`, owns the position, the target and the ramp: +an engine that steps on its own is the only part that knows where the axis is +between two `update()` calls. Positions are signed step counts from the base +position. + +## FasEngine + +`FasEngine` is an engine that steps on its own. FastAccelStepper makes its +pulses in hardware on the ESP32 (MCPWM, counted by PCNT), so the pace holds +however busy `loop()` is. See [`examples/FasEngine`](../examples/FasEngine/FasEngine.ino). + +- **Pulse width:** the STEP pulse is half the step interval, so there is no + pulse width to set. A TB6600's 5 µs is met by any interval of 10 µs or more. +- **DIR setup:** the library's direction-change delay, which rounds + `MC_DIR_SETUP_US` up to its own minimum (200 µs on the ESP32). It is only + spent on a reversal. +- **Ramp:** the library ramps from one constant acceleration, the profile's + `accelerationFor()`. + +Where FastAccelStepper and `StepEngine` behave differently, `FasEngine` bridges +the gap: + +- **Reversals:** the library ignores a move that reverses a running one. Here + the axis ramps down first, and `service()` sends the new move once it stops. + A polled engine reverses at full speed. +- **Immediate stops:** `stopNow()` drops the library's queue at once, instead of + letting about 20 ms of queued steps run out, which at speed would be a hundred + steps onto a switch. +- **The fleet's pulse gate** does not exist in the library. A move commanded + while it is shut waits; a move running when it shuts stops and resumes later. +- **One library instance** runs every axis. `FasEngine` starts it on first use, + so an axis can be built from its pins alone, like any other engine. ## Driver timing diff --git a/docs/ramping.md b/docs/ramping.md index 9c5267a..20dd335 100644 --- a/docs/ramping.md +++ b/docs/ramping.md @@ -34,6 +34,31 @@ Two things behave differently once a profile is attached: snap-action switch is only repeatable at a constant low speed, which is the whole reason the search has a slow second pass. +## How `LinearRamp` shapes a move + +The interval goes linearly from `startIntervalUs` down to the axis's own +interval over the first `rampSteps` steps, and back up over the last +`rampSteps`. It is linear in *interval*, not in speed, so the pull away from +rest is brisker than the approach to cruise. That is a cheap approximation: it +needs no division per step. + +The shape comes from two curves, one measured from each end of the move, and the +slower of the two wins at every step. A move too short to reach cruise speed +simply peaks where the curves cross, with no special case. + +- `startIntervalUs` must be larger (slower) than the axis's own interval; + otherwise there is nothing to ramp and the profile has no effect. +- Keep `startIntervalUs * rampSteps` within 32 bits. +- `setRampSteps()` and `setStartInterval()` work while running, which is how a + ramp is tuned on a bench. The instance is shared, so a change affects every + axis that uses it. +- For engines that ramp in hardware, `accelerationFor()` turns the ramp into one + constant acceleration, (v_top² − v_start²) / (2 · rampSteps) in steps/s². It + is approximate, because the ramp is linear in interval, but moves start, + cruise and end in the same places: 1000 µs down to 200 µs over 1000 steps + gives 12,000 steps/s². It uses integer maths only, so AVR builds don't pull in + floating point. + To write another profile - constant acceleration, an S-curve, a lookup table - implement `StepProfile::intervalFor()`; see `LinearRamp.h`. diff --git a/src/EngineStepper.h b/src/EngineStepper.h index e25114d..8c2a517 100644 --- a/src/EngineStepper.h +++ b/src/EngineStepper.h @@ -2,27 +2,19 @@ #include #include "Stepper.h" -// Holds an EngineStepper's engine. It is a base class listed before Stepper, -// and bases are built in the order they are listed, so the engine is whole -// before Stepper is handed a reference to it - and outlives Stepper on the way -// down. +// Holds the engine in a base listed before Stepper: bases are constructed in +// order, so the engine exists before Stepper takes a reference to it. template struct OwnedEngine { - // By value: engine arguments are pins and intervals, and AVR has no - // std::forward. + // By value: the arguments are small, and AVR has no std::forward. template explicit OwnedEngine(Args... args) : ownEngine(args...) {} Engine ownEngine; }; -// A Stepper that owns its engine, of any kind: one `new` gives a whole axis. -// The arguments after stepsPerRevolution go to the engine's constructor: -// -// Stepper* x = new EngineStepper(3200, /*step*/ 2, /*dir*/ 5); -// -// Use a plain Stepper instead when the engine has to live somewhere else, such -// as at file scope next to the endstops. +// A Stepper that owns its engine. Arguments after stepsPerRevolution go to the +// engine's constructor: new EngineStepper(3200, stepPin, dirPin). template class EngineStepper : private OwnedEngine, public Stepper { public: @@ -31,13 +23,11 @@ class EngineStepper : private OwnedEngine, public Stepper { : OwnedEngine(engineArgs...), Stepper(this->ownEngine, stepsPerRevolution) {} - // The base holds a reference to this object's own engine, which a copy - // would leave pointing at the original. + // Stepper references this object's own engine; a copy would point at the original. EngineStepper(const EngineStepper&) = delete; EngineStepper& operator=(const EngineStepper&) = delete; - // The engine as its own type, for what only that engine offers. Through a - // Stepper& the same call gives the StepEngine interface. + // The engine as its concrete type, for engine-specific calls. Engine& getEngine() { return this->ownEngine; } diff --git a/src/FasEngine.h b/src/FasEngine.h index 3bbc8fc..7929e97 100644 --- a/src/FasEngine.h +++ b/src/FasEngine.h @@ -9,9 +9,7 @@ #include "Log.h" #include "StepEngine.h" -// FastAccelStepper runs every axis from one engine object, started once. It -// lives here rather than in the sketch so an axis can be built from its pins -// alone, like any other engine. +// The single FastAccelStepper engine shared by all axes, started on first use. inline FastAccelStepperEngine& fasEngine() { static FastAccelStepperEngine engine; static bool started = false; @@ -22,23 +20,9 @@ inline FastAccelStepperEngine& fasEngine() { return engine; } -// Makes pulses with FastAccelStepper: in hardware on the ESP32 (MCPWM, counted -// by PCNT), so the pace holds however busy loop() is, and the ramp is computed -// by the library from one acceleration (StepProfile::accelerationFor()). -// -// The STEP pulse is half the step interval, so there is no pulse width to set: -// a TB6600's 5 us is met by any interval of 10 us or more. DIR setup is the -// library's direction-change delay, which rounds MC_DIR_SETUP_US up to its own -// minimum (200 us on the ESP32), paid only per reversal. -// -// Where the library and StepEngine differ, this class bridges: -// -// - A move that reverses a running one is ignored by the library. Here it -// waits: the axis ramps down, and service() sends the move once it stops. -// - stopNow() drops the library's queue at once instead of letting ~20 ms of -// queued steps run out, which at speed is a hundred steps onto a switch. -// - The fleet's gate does not exist in the library. A move commanded while -// it is shut waits, and a move running when it shuts stops and resumes. +// A StepEngine on FastAccelStepper: pulses and ramps in hardware on the ESP32. +// Bridges the library's differences (reversals, immediate stops, the pulse +// gate) as described in docs/engines.md. class FasEngine : public StepEngine { public: // Steps/s^2 used when there is no profile: full speed within a step or two. @@ -60,8 +44,7 @@ class FasEngine : public StepEngine { // --- Motion -------------------------------------------------------------- - // Without a library stepper (see begin()) nothing can move, so nothing is - // taken on. + // Ignored if begin() found no free library stepper. void moveTo(long step) override { if (!fas) return; @@ -84,8 +67,7 @@ class FasEngine : public StepEngine { int8_t moving = runningDirection(); if (moving == 0) return; - // Where the library will come to rest: its stopping distance on from - // here, but never past the end of the move it is running. + // Where the library will stop: its stopping distance, capped at the move's end. long here = fas->getCurrentPosition(); long end = fas->targetPos(); long stopAt = here + moving * (long)fas->stepsToStop(); @@ -108,8 +90,7 @@ class FasEngine : public StepEngine { return fas ? fas->getCurrentPosition() : targetStep; } - // Once the library has stopped with nothing waiting, wherever it stopped - // is the target: a ramped stop or a refused move ends there. + // Once stopped with nothing pending, the stop position is the target. long target() const override { if (fas && !pending && !fas->isRunning()) return fas->getCurrentPosition(); return targetStep; @@ -156,8 +137,7 @@ class FasEngine : public StepEngine { // --- Work ---------------------------------------------------------------- - // Sends a move that was waiting for a reversal to ramp down or for the - // gate to open. Never makes a pulse itself. + // Sends a pending move once a reversal has ramped down or the gate opens. bool service() override { if (pending) send(); return false; @@ -177,8 +157,8 @@ class FasEngine : public StepEngine { pending = true; } - // The library's own handle, for what StepEngine does not cover. nullptr - // before begin(), or when the library had no stepper left. + // The library's handle for features StepEngine lacks; nullptr before begin() + // or if no stepper was free. FastAccelStepper* stepper() const { return fas; } @@ -205,9 +185,8 @@ class FasEngine : public StepEngine { int8_t moving = runningDirection(); if (moving != 0 && way != moving) { - // The library would ignore it: ramp down, and try again from - // service() once stopped. Asked to stop right here, stop dead, - // as a polled engine would. + // The library ignores reversals: ramp down and retry from service(). + // A move to the current position stops dead, as a polled engine would. if (way == 0) stopNow(); else fas->stopMove(); return; diff --git a/src/LinearRamp.h b/src/LinearRamp.h index 7491fb7..ce3f9fd 100644 --- a/src/LinearRamp.h +++ b/src/LinearRamp.h @@ -3,31 +3,9 @@ #include "Stepper.h" -// Eases a move in and out instead of starting and stopping at full speed. -// -// A stepper can only be pulled in from rest below some step rate, set by the -// rotor and the load. Command it faster and the rotor stops following the -// field: it buzzes, slips, and the counter goes on counting steps the axis -// never took. Without a ramp the top speed of an axis is therefore whatever it -// can start at, which is well below what it can sustain - the ramp is what -// buys the difference. -// -// The interval walks linearly from startIntervalUs down to the axis's own -// interval over rampSteps steps, and back up over the last rampSteps of the -// move. Linear in interval rather than in speed, so the pull away from rest is -// brisker than the approach to cruise; that is the cheap approximation, and it -// costs no division per step. The shape is decided by the pair of curves, one -// measured from each end of the move, so a move too short to reach cruise -// simply peaks where they cross - there is no special case for it. -// -// One instance can drive a whole fleet: it holds no per-move state. -// -// LinearRamp ramp(4000, 300); // ease in over 300 steps, from 4000us -// axes.setProfiles(&ramp); -// -// startIntervalUs must be slower (larger) than the axis's own interval, or -// there is nothing to ramp and the profile stands aside. Keep -// startIntervalUs * rampSteps inside 32 bits, which any sane pair is. +// Eases a move in and out: the interval goes linearly from startIntervalUs to +// the axis's own interval over rampSteps at each end. Stateless, so one +// instance can serve a fleet. Shape and limits: docs/ramping.md. class LinearRamp : public StepProfile { public: LinearRamp(unsigned long startIntervalUs, uint16_t rampSteps) @@ -47,14 +25,9 @@ class LinearRamp : public StepProfile { return rampSteps; } - // The acceleration that covers the same change of speed over the same - // rampSteps: (v_top^2 - v_start^2) / (2 * rampSteps), each speed being - // 1,000,000 / interval in steps/s. Not exact, because this ramp is linear - // in interval rather than in speed, but the move starts, cruises and ends - // in the same places. 1000us down to 200us over 1000 steps is 12,000. - // - // Integers only, so the Uno does not pull in float maths for a method only - // the ESP32 calls. Capped at the largest uint32_t rather than wrapping. + // (v_top^2 - v_start^2) / (2 * rampSteps), with v = 1e6 / interval steps/s. + // Approximate (the ramp is linear in interval). Integer-only, so AVR builds + // avoid float maths; saturates at UINT32_MAX. uint32_t accelerationFor(unsigned long cruiseUs) const override { if (cruiseUs == 0 || startIntervalUs <= cruiseUs) return 0; // nothing to ramp @@ -64,8 +37,7 @@ class LinearRamp : public StepProfile { return (a > UINT32_MAX) ? UINT32_MAX : (uint32_t)a; } - // Tunable on the fly, which is how a ramp length gets found on a bench. - // Shared instance, so this moves every axis using it. + // Takes effect at once for every axis sharing this instance. void setRampSteps(uint16_t steps) { rampSteps = steps ? steps : 1; } @@ -83,8 +55,7 @@ class LinearRamp : public StepProfile { } private: - // The interval k steps into the ramp: startIntervalUs at k = 0, the axis's - // own interval once k reaches rampSteps. + // The interval k steps into the ramp. unsigned long at(long k, unsigned long cruiseUs) const { if (startIntervalUs <= cruiseUs) return cruiseUs; // nothing to ramp if (k <= 0) return startIntervalUs; diff --git a/src/PolledEngine.h b/src/PolledEngine.h index aed7408..2d565a4 100644 --- a/src/PolledEngine.h +++ b/src/PolledEngine.h @@ -2,14 +2,8 @@ #include #include "SoftEngine.h" -// Makes pulses on a step+dir driver's pins, each one timed against micros() -// when update() comes round: A4988, DRV8825, TB6600, DM542 and the like. They -// differ only in timing, which is the pulse width below and the DIR setup -// above. -// -// Works on any board, with no timer or interrupt. The price is that the pace is -// only as steady as the caller's loop: anything that holds loop() up holds the -// next pulse up with it. +// Step+dir pulses timed against micros() from update(), on any board. The pace +// is only as steady as the caller's loop. See docs/engines.md. class PolledEngine : public SoftEngine { public: static constexpr uint8_t NO_ENABLE_PIN = 0xFF; @@ -48,8 +42,7 @@ class PolledEngine : public SoftEngine { return false; } - // The driver's EN input is active low. Releasing it frees the shaft and - // stops the coils heating + // EN is active LOW. Disabling frees the shaft and stops the coils heating. void enable() override { if (enablePin != NO_ENABLE_PIN) digitalWrite(enablePin, LOW); } @@ -67,9 +60,8 @@ class PolledEngine : public SoftEngine { } protected: - // tick() pulses straight after this, so a change of direction waits out - // the setup time here. Without it a slow driver takes the first step of a - // reversal the old way, and the counter drifts a step per reversal. + // Waits out the DIR setup time on a reversal; without it a slow driver takes + // the first step the old way and the count drifts. void applyDirection(bool forward) override { int8_t dir = forward ? 1 : 0; if (dir == lastForward) return; diff --git a/src/SoftEngine.h b/src/SoftEngine.h index 6756a55..1641c8d 100644 --- a/src/SoftEngine.h +++ b/src/SoftEngine.h @@ -2,18 +2,12 @@ #include #include "StepEngine.h" - -// The part of a StepEngine that is the same whenever our own code makes the -// pulses, whatever calls it: position, target, the move's starting point for -// the ramp, the endstop inhibit, and tick(), which decides whether the next -// pulse is due. PolledEngine calls tick() from update(); TimerEngine will call -// it from a hardware timer. +// Shared base for engines whose pulses are timed by our own code: position, +// target, ramp, endstop inhibit, and tick(), which emits a pulse when one is due. class SoftEngine : public StepEngine { public: void moveTo(long step) override { - // A move commanded while the axis is already running composes onto the - // one in flight, so it keeps that move's starting point: restarting the - // ramp would slow a running axis back down to its starting speed. + // Mid-move, keep the original start so the ramp does not restart. if (currentStep == targetStep) moveStartStep = currentStep; targetStep = step; @@ -54,8 +48,7 @@ class SoftEngine : public StepEngine { return currentStep != targetStep; } - // Settable after construction because homing switches between a fast seek - // and a slow, repeatable re-approach. + // Changed at runtime by homing (fast seek, slow re-approach). void setStepInterval(unsigned long us) override { stepIntervalUs = us; } @@ -64,8 +57,6 @@ class SoftEngine : public StepEngine { return stepIntervalUs; } - // The profile's idea of the pace here, or the flat interval when there is - // no profile. unsigned long currentInterval() const override { if (!profile_) return stepIntervalUs; @@ -77,8 +68,7 @@ class SoftEngine : public StepEngine { stepIntervalUs); } - // The profile is borrowed, not owned - it is expected to outlive the axis, - // which a file-scope instance in the sketch does. + // Borrowed: the profile must outlive the axis. void setProfile(const StepProfile* p) override { profile_ = p; } @@ -106,16 +96,14 @@ class SoftEngine : public StepEngine { inhibited(0), pulsesAllowed(true) {} - // Emits at most one step pulse, if one is due at now, and says whether it - // did. Never waits. + // Emits at most one pulse if one is due at `now`; returns whether it did. bool tick(unsigned long now) { if (currentStep == targetStep) return false; if (!pulsesAllowed) return false; int8_t dir = (targetStep > currentStep) ? 1 : -1; - // Driving further onto a pressed endstop crashes the axis. Cancel the - // move; moving away from the switch stays allowed. + // Never drive further onto a pressed endstop; moving away is allowed. if (dir == inhibited) { stopNow(); return false; @@ -136,8 +124,7 @@ class SoftEngine : public StepEngine { lastStepAt = 0; } - // Sets the driver's direction input ahead of a pulse. forward means the - // step counter is about to increase. + // Sets DIR before a pulse; forward means the step count increases. virtual void applyDirection(bool forward) = 0; // Emits exactly one step pulse. diff --git a/src/StepDirStepper.h b/src/StepDirStepper.h index f5b67e9..ba925f2 100644 --- a/src/StepDirStepper.h +++ b/src/StepDirStepper.h @@ -3,16 +3,9 @@ #include "EngineStepper.h" #include "PolledEngine.h" -// A Stepper on any step+dir driver (A4988, DRV8825, TB6600, DM542 and the -// like), with its own PolledEngine built in: the one-line way to get an axis, -// and the shape every sketch written before the engine split already uses. -// It is an EngineStepper with the constructor sketches already -// call. To drive the axis from another engine, use EngineStepper with that -// engine instead. -// -// Microstepping is set by jumpers, MS pins or DIP switches on the driver, not -// from here, so stepsPerRevolution must be given as full steps x microsteps (a -// 1.8 deg/step motor at 1/16 is 200 x 16 = 3200). +// An EngineStepper for any step+dir driver (A4988, DRV8825, +// TB6600, DM542). Microstepping is set on the driver, so stepsPerRevolution is +// full steps x microsteps (200 x 16 = 3200 for a 1.8 deg motor at 1/16). class StepDirStepper : public EngineStepper { public: static constexpr uint8_t NO_ENABLE_PIN = PolledEngine::NO_ENABLE_PIN; @@ -28,8 +21,7 @@ class StepDirStepper : public EngineStepper { : EngineStepper(stepsPerRevolution, stepPin, dirPin, enablePin, stepIntervalUs, stepPulseUs) {} - // The driver's EN input is active low. Releasing it frees the shaft and - // stops the coils heating + // EN is active LOW. Disabling frees the shaft and stops the coils heating. void enable() { getEngine().enable(); } diff --git a/src/StepEngine.h b/src/StepEngine.h index cc0f92f..ef6b173 100644 --- a/src/StepEngine.h +++ b/src/StepEngine.h @@ -2,36 +2,15 @@ #include #include "StepProfile.h" -// How long DIR must have settled before the next STEP edge, in microseconds. -// Only paid when the direction actually changes, never per step. 5 us covers -// the opto-isolated drivers (TB6600, DM542), which are the slow ones +// DIR setup time before the next STEP edge, in microseconds; spent only on a +// direction change. 5 us covers the slowest (opto-isolated) drivers. #ifndef MC_DIR_SETUP_US #define MC_DIR_SETUP_US 5 #endif - - -// A StepEngine is responsible for making the actual step pulses. -// A Stepper decides what the axis should -// do - where to go, which end is blocked by a pressed switch, when it has -// arrived - and hands the doing to one of these. -// -// The split exists because there is more than one good way to make pulses: -// -// PolledEngine times each pulse against micros() from update(). Any board; -// the pace is only as steady as the caller's loop. -// FasEngine FastAccelStepper on the ESP32: pulses come from hardware. -// TimerEngine an ESP32 hardware timer running our own stepping code. -// -// It works in moves, not pulses: the engine owns position, target and the -// ramp, because an engine that steps on its own (FasEngine, TimerEngine) is -// the only thing that knows where the axis is between two update() calls. The -// Stepper never counts steps itself; it asks. -// -// Positions are in steps from the base position and may be negative. -// -// Borrowed, not owned, like an Endstop or a StepProfile: declare it at file -// scope in the sketch so it outlives the axis. +// Makes an axis's step pulses and owns its position, target and ramp; the +// Stepper decides what the axis does. Positions are signed steps from the base +// position. Borrowed by the Stepper, so it must outlive it. See docs/engines.md. class StepEngine { public: virtual ~StepEngine() = default; @@ -41,20 +20,17 @@ class StepEngine { // --- Motion -------------------------------------------------------------- - // Absolute move. A move commanded while one is in flight composes onto - // it: the ramp carries on from where it is rather than starting over. + // Absolute move. A move commanded mid-move continues the current ramp. virtual void moveTo(long step) = 0; - // Cancels the move in flight: the current position becomes the target, and - // nothing more is sent. What an endstop hit wants, ramp or no ramp. + // Stops immediately: the current position becomes the target. virtual void stopNow() = 0; - // Brings the axis to rest over the profile's stopping distance instead of - // dropping it dead. Same as stopNow() without a profile, and never extends - // a move that was already shorter than the stopping distance. + // Stops over the profile's stopping distance, never past the move's end. + // Same as stopNow() without a profile. virtual void stopRamped() = 0; - // Redefines where the axis believes it is, and makes that the target too. + // Redefines the current position and makes it the target. virtual void setPosition(long step) = 0; virtual long position() const = 0; @@ -65,13 +41,11 @@ class StepEngine { // --- Pace ---------------------------------------------------------------- - // The flat interval between pulses, or the top speed when a profile is - // attached. + // The flat interval between pulses, or the cruise interval under a profile. virtual void setStepInterval(unsigned long us) = 0; virtual unsigned long stepInterval() const = 0; - // The interval the axis is running at right now, which under a ramp is not - // the one it was asked for. + // The interval right now, which differs from stepInterval() while ramping. virtual unsigned long currentInterval() const = 0; // Borrowed; nullptr for a flat interval. @@ -80,36 +54,28 @@ class StepEngine { // --- Endstops ------------------------------------------------------------ - // Which way the axis must not travel: +1 (towards MAX), -1 (towards MIN) or - // 0 for neither. The Stepper decides from its switches and says so on every - // update(); the engine enforces it, so an engine that steps on its own can - // refuse a pulse between two update() calls. A move heading the forbidden - // way is cancelled, as stopNow() would. Moving the other way stays allowed. + // Direction blocked by a pressed endstop: +1 (MAX), -1 (MIN) or 0. Set by + // the Stepper on every update() and enforced here, so an engine that steps + // on its own refuses pulses between updates. A move that way is cancelled. virtual void inhibit(int8_t direction) = 0; // --- Work ---------------------------------------------------------------- - // Called on every Stepper::update(). A polled engine makes at most one - // pulse here, and returns true when it did. An engine that steps on its own - // has little or nothing to do, and returns false. + // Called on every Stepper::update(). Returns true if it made a pulse; a + // polled engine makes at most one. virtual bool service() = 0; - // True when pulses happen without service() being called - from a timer or - // from hardware. Homing and the fleet use it to decide whether to spin on - // update() or to sleep while a move runs. + // True when pulses come from a timer or hardware rather than service(). virtual bool stepsOnItsOwn() const = 0; - // The fleet's gate: while it is shut no pulse may go out, though moves can - // still be commanded and wait. StepperManager shuts it whenever the drivers - // cannot take a pulse - released by the idle timer, or still waking after - // EN came back - because a pulse sent then is a step the counter claims but - // the axis never took. Open by default, so an axis outside a fleet runs. + // The fleet's pulse gate. While shut, moves wait and no pulse goes out: + // StepperManager shuts it while the drivers are released or waking, when a + // pulse would be counted but not taken. Open by default. virtual void allowPulses(bool allowed) = 0; // --- Driver enable ------------------------------------------------------- - // For an engine that owns its driver's EN pin. Most axes share one EN line - // that the fleet owns, and leave these as the no-ops they are by default. + // For an engine that owns its driver's EN pin; no-ops by default. virtual void enable() {} virtual void disable() {} }; diff --git a/src/StepProfile.h b/src/StepProfile.h index 1ca3114..147fbf6 100644 --- a/src/StepProfile.h +++ b/src/StepProfile.h @@ -1,15 +1,9 @@ #pragma once #include - -// A speed profile: how long to wait before the next pulse, given where the move -// has got to. Attach one to a Stepper and it decides the pace; attach none (the -// default) and the axis runs at one flat interval, as it always has. -// -// Stateless and const on purpose - the move's state belongs to the axis, so a -// single profile instance can be shared by every axis in a fleet, and costs -// each of them nothing but the pointer. Concrete profiles live in their own -// headers (LinearRamp.h); a sketch that wants none includes none. +// A speed profile: the interval before the next pulse, given the move's +// progress. Stateless, so one instance can serve a whole fleet. See +// docs/ramping.md. class StepProfile { public: virtual ~StepProfile() = default; @@ -20,13 +14,11 @@ class StepProfile { virtual unsigned long intervalFor(long stepsDone, long stepsLeft, unsigned long cruiseUs) const = 0; - // Steps needed to come to rest from full speed, for stopRamped(). 0 says - // the axis can stop dead. + // Steps needed to stop from full speed, for stopRamped(); 0 stops dead. virtual long stopDistance() const { return 0; } - // The same profile as one constant acceleration, in steps/s^2, for engines - // that ramp in hardware from a single number (FastAccelStepper). 0 says - // there is no ramp: start and stop at full speed. + // The profile as one constant acceleration in steps/s^2, for engines that + // ramp in hardware. 0 means no ramp. virtual uint32_t accelerationFor(unsigned long cruiseUs) const { (void)cruiseUs; return 0; From 1d94ac554d3ccb86a714f43726befd48c00d259e Mon Sep 17 00:00:00 2001 From: AndrewwSS <76648798+AndrewwSSS@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:09:15 +0200 Subject: [PATCH 3/4] docs: shorten axis, homing and manager comments; document soft limits --- docs/homing.md | 49 +++++++++++++++++++++++++ src/AngularMotor.h | 24 ++++--------- src/Endstop.h | 40 +++++---------------- src/Homing.h | 71 ++++++++---------------------------- src/Log.h | 17 +++------ src/MotorManager.h | 25 ++++--------- src/PCA9685Servo.h | 14 ++------ src/ServoManager.h | 9 ++--- src/ServoMotor.h | 8 ++--- src/Stepper.h | 86 +++++++++++--------------------------------- src/StepperManager.h | 67 +++++++++++----------------------- 11 files changed, 139 insertions(+), 271 deletions(-) diff --git a/docs/homing.md b/docs/homing.md index 6cc0785..a3a4121 100644 --- a/docs/homing.md +++ b/docs/homing.md @@ -44,3 +44,52 @@ stage failed (`HOMING_NEVER_CLOSED`, `HOMING_NEVER_RELEASED`, `HOMING_NOT_FOUND` `HOMING_NO_ENDSTOP`). `isHomed(id)` reports the outcome until `setOrigin()` redefines step 0. Without the manager, `homeAxis(stepper, cfg)` does the same on a single axis whose driver is already energised. + +### How the search behaves + +- **Starting on the switch:** the seek pass is skipped and the axis goes straight + to the back-off. The base position always comes from a fresh closing edge + during the slow re-approach. +- **Back-off:** the axis retreats in `backoffSteps` increments until the switch + releases, not by one fixed distance, because it may start further past the + trip point than any single increment (pushed by hand, or left there by an + earlier failed run). The retreat is bounded by `maxSteps`, so a contact stuck + closed fails instead of driving the axis off the far end. +- **Both switches pressed:** if the switch at the other end stops the retreat, + the axis is pinned between two "pressed" switches. That is a wiring fault, and + homing reports it instead of retrying. +- **A move in flight** is cancelled before the search starts, so it is not added + to the seek. +- **The speed profile** is detached during the search and restored for the move + to the rest step. +- **Soft limits** are switched off during the search, which has to run past them + to find the switch. They are armed when homing succeeds, and restored as they + were when it fails. +- **No reached callback** fires for the search's own stops, only for moves the + sketch commands. +- **Engines that step on their own** are checked once a millisecond during the + search instead of spinning on `update()`, leaving the CPU to other tasks. + +## Endstop debounce + +A snap-action switch chatters for a few milliseconds around the transition. +`Endstop` acts on the first edge it sees and then ignores the pin until the +contacts settle: waiting for a stable level would delay detection and let the +axis overshoot. A homing run reads the pin directly (`resync()`), which opens no +ignore window, so a press right after it is seen at once. + +## Soft limits + +Soft limits are the range of steps an axis may be sent to: + +```cpp +axes.setLimits(0, /*minStep*/ 0, /*maxStep*/ 20000); +``` + +- A move whose target is outside the range is refused and logged; the axis keeps + its previous target. An axis already outside the range may still be sent back + towards it, so it never gets stuck. +- Limits only mean something once step 0 is a known place, so they are enforced + only when **armed**: after a successful `home()`, or after `setOrigin()` + defines step 0 by hand. +- A move already running past a new limit is cut short at the limit. diff --git a/src/AngularMotor.h b/src/AngularMotor.h index 936c8e8..bf7c567 100644 --- a/src/AngularMotor.h +++ b/src/AngularMotor.h @@ -3,8 +3,7 @@ #include "Log.h" -// Common interface for any angle-driven actuator (hobby servo, PCA9685-driven -// servo, geared stepper acting as a joint, ...). +// Base for angle-driven actuators (hobby servo, PCA9685 servo, geared joint). class AngularMotor { public: enum State { @@ -17,8 +16,7 @@ class AngularMotor { // Attaches/initializes the underlying hardware and drives it to defaultAngle. virtual void begin() = 0; - // Advances the motor towards targetAngle. Must be called repeatedly (e.g. - // from loop()). + // Moves towards targetAngle; call from loop(). virtual void update() { if (currentAngle == targetAngle) { setState(HOLD); @@ -53,8 +51,7 @@ class AngularMotor { return this->currentAngle; } - // Per-motor switch for the DEBUG and TRACE messages. Has no effect unless - // MC_LOG_LEVEL compiles them in - see Log.h. + // Per-motor DEBUG/TRACE output, if MC_LOG_LEVEL compiles it in. void toggleDebug() { this->debugEnabled = !this->debugEnabled; } @@ -67,9 +64,7 @@ class AngularMotor { return this->state; } - // True exactly once, the first time this is polled after the motor - // settles into HOLD. Lets a manager fire a "reached" callback without - // keeping its own shadow copy of every motor's previous state. + // True once, on the first poll after the motor settles into HOLD. bool reachedTarget() { bool r = justReached; justReached = false; @@ -77,9 +72,7 @@ class AngularMotor { } protected: - // rotationDelay is the time between 1-degree steps in the default update() - // loop (lower = faster sweep speed). Subclasses with their own update() - // override can ignore it. + // rotationDelay: ms between 1-degree steps in the default update(). explicit AngularMotor( uint16_t defaultAngle, uint16_t rotationDelay = DEFAULT_ROTATION_DELAY, @@ -102,9 +95,7 @@ class AngularMotor { static constexpr uint16_t MAX_ANGLE = 180; static constexpr uint16_t DEFAULT_ROTATION_DELAY = 20; // ms - // Derived classes report motion state through here instead of writing - // `state` directly, so the MOVING -> HOLD edge (used by reachedTarget()) - // is only ever detected in one place. + // The only writer of `state`, so the HOLD edge is detected in one place. void setState(State newState) { if (newState == HOLD && state != HOLD) { justReached = true; @@ -112,8 +103,7 @@ class AngularMotor { state = newState; } - // Physically drives the actuator to the given angle. Called from - // update() on every step and from begin() to reach the initial position. + // One hardware write to the given angle. virtual void writeAngle(uint16_t angle) = 0; bool debugEnabled = false; diff --git a/src/Endstop.h b/src/Endstop.h index bf14e5f..ab4a019 100644 --- a/src/Endstop.h +++ b/src/Endstop.h @@ -1,30 +1,17 @@ #pragma once #include -// A limit switch at one end of an axis's travel: an Omron D3V-style SPDT -// microswitch, or any snap-action switch, wired between a pin and GND. -// -// Attach it to a Stepper with setEndstop() and the axis refuses to drive -// further onto it once it is pressed; moving away stays allowed. An axis has a -// slot at each end, so a second switch on its own pin is a second Endstop with -// the other Side. Homing (Homing.h) drives the axis onto one of them to find -// its base position. -// -// Borrowed, not owned, like a StepProfile: declare it at file scope in the -// sketch so it outlives the axis. +// A limit switch between a pin and GND at one end of an axis's travel. Borrowed +// by the Stepper, so it must outlive it. See docs/homing.md. class Endstop { public: - // Prefer NORMALLY_CLOSED. Wired that way the loop is closed while the axis - // is clear, so a cut wire, a cold crimp or an unplugged connector reads - // exactly like a trigger and stops the axis. Wired NORMALLY_OPEN those - // same faults read "all clear" and the axis drives into the stop. + // Prefer NORMALLY_CLOSED: a broken wire then reads as triggered, not clear. enum Wiring : uint8_t { NORMALLY_OPEN, NORMALLY_CLOSED }; - // Which end of travel the switch sits at, as the sign of the step count - // moving towards it. + // The end of travel, as the sign of the step direction towards it. enum Side : int8_t { MIN = -1, MAX = 1 @@ -40,10 +27,8 @@ class Endstop { resync(); } - // A snap-action switch chatters for a few milliseconds around the - // transition. Waiting for a stable level would delay detection and let the - // axis overshoot the trip point, so act on the first edge seen and then - // ignore the pin until the contacts have settled. + // Acts on the first edge, then ignores the pin while the contacts chatter: + // waiting for a stable level would let the axis overshoot. void update() { if (millis() - lastChange < DEBOUNCE_MS) return; @@ -54,13 +39,8 @@ class Endstop { lastChange = millis(); } - // Ground truth, bypassing the debounce lock-out. A homing run may start - // within the debounce window of a previous one or of a knock, and opening - // the search in the wrong phase from a stale reading wastes a pass. - // - // A reading is not an edge, so it opens no lock-out of its own: the next - // update() acts on a press at once. Dated one window back to say so - the - // unsigned subtraction in update() makes that safe at any millis(). + // Reads the pin directly, bypassing the debounce window, and opens no window + // of its own. Dated one window back; unsigned subtraction keeps that safe. void resync() { triggered = readRaw(); lastChange = millis() - DEBOUNCE_MS; @@ -71,9 +51,7 @@ class Endstop { return triggered; } - // The pin right now, interpreted through the wiring. INPUT_PULLUP idles the - // pin HIGH: a NO contact closes to GND when hit (LOW means triggered), a NC - // contact opens when hit and lets the pull-up win (HIGH means triggered). + // The pin now, through the wiring: NO triggers LOW, NC triggers HIGH (pull-up). bool readRaw() const { bool high = (digitalRead(pin) == HIGH); return (wiring == NORMALLY_OPEN) ? !high : high; diff --git a/src/Homing.h b/src/Homing.h index 8058de8..4dee54f 100644 --- a/src/Homing.h +++ b/src/Homing.h @@ -4,22 +4,8 @@ #include "Endstop.h" #include "Stepper.h" -// Homing: earning a base position against an endstop instead of assuming one. -// -// Stepper::begin() can only declare "wherever the shaft is now is step 0", -// which is an assumption that silently poisons every later position if it is -// wrong. homeAxis() drives the axis onto the endstop at one end, makes the trip -// point step 0, and parks at the configured rest step. Afterwards step 0 is a -// real, repeatable place on the machine. -// -// The search is two-pass. The trip point of a snap-action switch is only -// repeatable at low speed - approach it fast and the contact closes a little -// further along each time, by an amount that varies with speed. So the seek -// pass finds it quickly, the axis backs off to release it, and a slow -// re-approach takes the reading that actually sets the base position. -// -// Works with any Stepper subtype: it only needs an Endstop attached on the -// side it homes towards. +// Two-pass homing against an endstop: fast seek, back off, slow re-approach. +// The trip point becomes step 0. See docs/homing.md. struct HomingConfig { Endstop::Side towards; // the end whose switch sets step 0 @@ -51,15 +37,8 @@ inline const __FlashStringHelper* homingResultName(HomingResult r) { namespace mc_homing { -// Runs the current move to its end. The endstop guard in Stepper::update() -// still applies, so a move towards a pressed switch ends there. The last -// update() settles the axis into HOLD, which the loop alone would leave to the -// caller's next update(). -// -// A polled engine only steps when update() is called, so it gets called as -// fast as the loop allows. An engine that steps on its own needs no help with -// the pulses: checking once a millisecond still runs the endstop guard, and -// leaves the CPU to everything else (on the ESP32, the other tasks). +// Runs the current move to its end, endstop guard included, and settles HOLD. +// Self-stepping engines are checked once a millisecond instead of spinning. inline void drive(Stepper& axis) { bool own = axis.getEngine().stepsOnItsOwn(); @@ -77,12 +56,8 @@ inline bool driveUntilTriggered(Stepper& axis, Endstop& endstop) { return endstop.isTriggered(); } -// Retreats away from the switch until it releases, in backoffSteps -// increments. A fixed retreat is not enough: the axis can power up further past -// the trip point than any single increment - shoved there by hand, or left -// there by a previous failed run - and would then never clear. Bounded by the -// seek allowance so a contact stuck closed fails instead of driving the axis -// off the far end. Returns the distance travelled, 0 on failure. +// Retreats in backoffSteps increments until the switch releases, bounded by +// maxSteps. Returns the distance travelled, 0 on failure. inline long backOffUntilClear(Stepper& axis, Endstop& endstop, const HomingConfig& cfg) { long travelled = 0; @@ -91,9 +66,7 @@ inline long backOffUntilClear(Stepper& axis, Endstop& endstop, const HomingConfi axis.moveToStep(target); drive(axis); - // Cut short means the endstop at the other end stopped the retreat: - // the axis is pinned between two "pressed" switches, which is a wiring - // fault, not something more retreating will fix. + // Stopped by the other endstop: both read pressed, a wiring fault. if (axis.getStep() != target) { MC_ERROR(F("Homing: backing off ran into the opposite endstop")); return 0; @@ -110,9 +83,7 @@ inline long backOffUntilClear(Stepper& axis, Endstop& endstop, const HomingConfi inline HomingResult search(Stepper& axis, Endstop& endstop, const HomingConfig& cfg) { endstop.resync(); - // Starting already on the switch leaves the seek pass nothing to find, so - // skip to the back-off. Either way the base position is set from a fresh - // closing edge during the slow re-approach. + // Already on the switch: skip the seek and back off first. if (!endstop.isTriggered()) { axis.moveSteps((long)cfg.towards * cfg.maxSteps); @@ -137,8 +108,7 @@ inline HomingResult search(Stepper& axis, Endstop& endstop, const HomingConfig& unsigned long fast = axis.getStepInterval(); axis.setStepInterval(cfg.slowIntervalUs); - // Allowance covers the retreat plus a margin, so a long retreat still - // leaves enough travel to reach the switch again. + // Allow the retreat distance plus a margin to reach the switch again. axis.moveSteps((long)cfg.towards * (backedOff + cfg.backoffSteps)); bool hit = driveUntilTriggered(axis, endstop); axis.setStepInterval(fast); @@ -157,15 +127,8 @@ inline HomingResult search(Stepper& axis, Endstop& endstop, const HomingConfig& } -// Runs the search, then drives to cfg.restStep. Blocking: homing is a start-up -// operation, and until it finishes there is no position worth reporting. A -// failed search leaves the axis where it stopped and says why rather than -// pretending. The driver must already be energised - StepperManager::home() -// sees to that. -// -// The speed profile is detached for the search: the trip point is only -// repeatable at a constant low speed, which is the whole reason for the slow -// pass. It is back in place for the move to the rest step. +// Runs the search, then moves to cfg.restStep. Blocking. A failure leaves the +// axis where it stopped. The driver must be energised (StepperManager::home()). inline HomingResult homeAxis(Stepper& axis, const HomingConfig& cfg) { Endstop* endstop = axis.getEndstop(cfg.towards); if (!endstop) { @@ -176,15 +139,10 @@ inline HomingResult homeAxis(Stepper& axis, const HomingConfig& cfg) { MC_INFO(F("Homing towards "), endstop->sideName(), F(" endstop on pin "), endstop->getPin()); - // Homing can be asked for at any time, not only at start-up. A move still - // in flight would otherwise be added to the seek, since relative moves - // build on the target. + // Cancel any move in flight; relative moves would otherwise add to the seek. axis.stopHere(); - // The search runs past the soft limits by design: they are only right - // once it has found where step 0 is. Back on for good once it has, and as - // they were if it failed - a failed search moved the axis, but the counter - // still knows by how much. + // The search runs past the soft limits; re-armed on success, restored on failure. bool wasArmed = axis.limitsArmed(); axis.armLimits(false); @@ -202,8 +160,7 @@ inline HomingResult homeAxis(Stepper& axis, const HomingConfig& cfg) { axis.armLimits(wasArmed); } - // The search settles the axis several times; none of that is a move the - // caller commanded, so swallow the edge rather than fire a reached callback. + // The search's own stops are not commanded moves: swallow the reached edge. axis.reachedTarget(); return result; } diff --git a/src/Log.h b/src/Log.h index 6445729..b834fda 100644 --- a/src/Log.h +++ b/src/Log.h @@ -1,13 +1,8 @@ #pragma once #include -// Compile-time log levels. Pick one per project in platformio.ini, so every -// translation unit that includes the library sees the same value: -// -// build_flags = -D MC_LOG_LEVEL=MC_LOG_DEBUG -// -// Anything above the chosen level is removed by the preprocessor - its text -// never reaches flash or RAM, which on an Uno's 2 KB matters. +// Compile-time log levels, set with build_flags = -D MC_LOG_LEVEL=...; messages +// above the level are compiled out. See docs/logging.md. #define MC_LOG_NONE 0 #define MC_LOG_ERROR 1 // something failed and the caller should know #define MC_LOG_INFO 2 // start-up and one-off events @@ -18,8 +13,7 @@ #define MC_LOG_LEVEL MC_LOG_INFO #endif -// Where the messages go. Any Print works: Serial1, a SoftwareSerial, a -// Bluetooth module's stream. It is not begun here - the sketch owns it. +// Output stream (any Print). The sketch must begin() it. #ifndef MC_LOG_STREAM #define MC_LOG_STREAM Serial #endif @@ -30,9 +24,8 @@ inline void printAll() { MC_LOG_STREAM.println(); } -// Prints each argument in turn, then a newline. Takes whatever Print::print -// takes, so pass F("...") for text and plain numbers for values - no String -// concatenation, which fragments the heap on AVR. +// Prints each argument, then a newline. Pass F("...") text, not Strings, to +// avoid heap fragmentation on AVR. template inline void printAll(const T& first, const Rest&... rest) { MC_LOG_STREAM.print(first); diff --git a/src/MotorManager.h b/src/MotorManager.h index f92ac8c..6435ceb 100644 --- a/src/MotorManager.h +++ b/src/MotorManager.h @@ -2,19 +2,9 @@ #include -// Owns and drives a fixed-size fleet of actuators, addressed by index. -// -// T is an actuator root: AngularMotor for angle-driven servos, Stepper for -// step-driven axes. Both expose the same lifecycle - begin(), a non-blocking -// update(), a State enum with MOVING and HOLD, the one-shot reachedTarget() -// edge and the debug toggle - and that lifecycle is all this class touches. Commands in the -// actuator's own unit (degrees, steps) live on the subclasses, ServoManager and -// StepperManager. -// -// Motors are registered through addMotor(), so a fleet can hold any mix of -// subtypes: steppers on different driver chips, a PCA9685Servo next to -// a ServoMotor. Swapping one out is a change at the new-expression and nowhere -// else, because the manager only ever talks through T. +// Owns a fixed-size fleet of actuators of root type T (AngularMotor or +// Stepper), addressed by index. Unit-specific commands live in the subclasses. +// See docs/architecture.md. template class MotorManager { public: @@ -36,8 +26,7 @@ class MotorManager { delete[] motors; } - // Takes ownership of motor. Returns its id, or INVALID_ID if capacity is - // full - ids are handed out in registration order, starting at 0. + // Takes ownership. Returns the id (0, 1, ... in order), or INVALID_ID when full. uint8_t addMotor(T* motor) { if (motor == nullptr || count >= capacity) return INVALID_ID; motors[count] = motor; @@ -81,10 +70,8 @@ class MotorManager { T* get(uint8_t id) const { return (id < count) ? motors[id] : nullptr; } protected: - // The fleet loop. Deliberately not called begin()/update(): a subclass that - // has work of its own to do around them (StepperManager and the enable - // line) owns those names, and naming these apart means no caller can reach - // past it through a MotorManager reference and skip that work. + // Named apart from begin()/update() so callers cannot bypass a subclass's + // own begin()/update() (e.g. StepperManager's enable line). void beginMotors() { for (uint8_t i = 0; i < count; i++) motors[i]->begin(); } diff --git a/src/PCA9685Servo.h b/src/PCA9685Servo.h index 0becc51..a492141 100644 --- a/src/PCA9685Servo.h +++ b/src/PCA9685Servo.h @@ -4,16 +4,8 @@ #include "AngularMotor.h" #include "Log.h" -// AngularMotor implementation for a servo driven through a PCA9685 16-channel PWM -// driver over I2C. One PCA9685 board is shared by every PCA9685Servo built -// on top of it (each just claims one of its 16 channels), so the driver is -// taken by reference rather than owned here. Initialize the shared driver -// once, before any PCA9685Servo::begin() runs (e.g. before ServoManager::begin()): -// -// Adafruit_PWMServoDriver pwm; -// pwm.begin(); -// pwm.setPWMFreq(50); // standard analog servo frequency -// +// AngularMotor on one channel of a shared PCA9685 board. Call pwm.begin() and +// pwm.setPWMFreq(50) before any begin() here. See docs/usage.md. class PCA9685Servo : public AngularMotor { public: PCA9685Servo( @@ -28,7 +20,7 @@ class PCA9685Servo : public AngularMotor { writeAngle(targetAngle); - // Wait for the servo to reach the default position + // Give the servo time to reach the default position. delay(1000); setState(HOLD); diff --git a/src/ServoManager.h b/src/ServoManager.h index d13b129..900dee7 100644 --- a/src/ServoManager.h +++ b/src/ServoManager.h @@ -4,10 +4,7 @@ typedef MotorManager::ReachedCallback ServoReachedCallback; -// A fleet of angle-driven motors: MotorManager's lifecycle plus commands in -// degrees. Any AngularMotor subtype can join (ServoMotor, PCA9685Servo, a -// degree wrapper around a stepper joint, ...) - the manager only talks through -// AngularMotor. +// A fleet of AngularMotors with commands in degrees. class ServoManager : public MotorManager { public: explicit ServoManager(uint8_t capacity) : MotorManager(capacity) {} @@ -25,9 +22,7 @@ class ServoManager : public MotorManager { } void setAngle(uint8_t id, int angle) { - // AngularMotor::setAngle clamps, but takes an unsigned angle: a negative one - // would wrap to a huge value and clamp to maxAngle - the far end of - // travel - rather than to the near one meant here. + // Clamp negatives here: as unsigned they would wrap and clamp to maxAngle. if (angle < 0) angle = 0; if (id < count) motors[id]->setAngle((uint16_t)angle); } diff --git a/src/ServoMotor.h b/src/ServoMotor.h index 509a799..3eacbc2 100644 --- a/src/ServoMotor.h +++ b/src/ServoMotor.h @@ -1,7 +1,6 @@ #pragma once #include -// The Arduino Servo library does not support the ESP32; ESP32Servo offers the -// same Servo class on the chip's LEDC PWM. +// ESP32Servo provides the same Servo class on the ESP32. #if defined(ESP32) #include #else @@ -11,8 +10,7 @@ #include "Log.h" -// AngularMotor backed by the Arduino Servo library, or ESP32Servo on the ESP32 -// (one PWM pin per motor). +// AngularMotor on one PWM pin via the Servo library. class ServoMotor : public AngularMotor { public: ServoMotor(uint8_t pin, uint16_t defaultAngle = DEFAULT_ANGLE, uint16_t rotationDelay = DEFAULT_ROTATION_DELAY) @@ -28,7 +26,7 @@ class ServoMotor : public AngularMotor { servo.attach(servoPin); writeAngle(targetAngle); - // Wait for the servo to reach the default position + // Give the servo time to reach the default position. delay(1000); setState(HOLD); diff --git a/src/Stepper.h b/src/Stepper.h index c0eda86..b424934 100644 --- a/src/Stepper.h +++ b/src/Stepper.h @@ -6,17 +6,8 @@ #include "StepProfile.h" -// One step-driven axis: what it should do. Where to go, which end is blocked by -// a pressed endstop, whether it is moving or holding, when it has arrived. -// How the pulses get made is the StepEngine's business, so the same axis logic -// runs on a polled loop, a hardware timer or FastAccelStepper - pick the engine -// and hand it over: -// -// PolledEngine xEngine(2, 5); -// Stepper x(xEngine, 3200); -// -// The engine is borrowed, not owned, like the endstops and the profile. For the -// common case of a polled step+dir driver, StepDirStepper bundles the two. +// One step-driven axis: target, endstop guard, soft limits and MOVING/HOLD +// state. Pulses come from a borrowed StepEngine; StepDirStepper bundles one. class Stepper { public: enum State { @@ -51,8 +42,7 @@ class Stepper { } } - // Returns immediately. Call repeatedly from loop(); movement is - // non-blocking. A polled engine makes at most one pulse per call. + // Non-blocking; call from loop(). A polled engine makes at most one pulse per call. void update() { updateEndstops(); @@ -61,9 +51,7 @@ class Stepper { return; } - // Driving further onto a pressed endstop crashes the axis. The engine - // is told which way is blocked, so one that steps on its own refuses - // the pulse too; moving away from the switch stays allowed. + // Tell the engine which way is blocked, so a self-stepping engine also refuses it. Endstop* hit = blockingEndstop(); engine.inhibit(hit ? hit->getSide() : 0); if (hit) { @@ -82,9 +70,7 @@ class Stepper { } } - // Absolute move, in steps from the base position. May be negative. - // Refused, and logged, when armed soft limits put the target out of - // range - see setLimits(). + // Absolute move in steps (may be negative). Refused if outside armed soft limits. void moveToStep(long step) { if (!withinLimits(step)) { MC_INFO(F("Move to step "), step, F(" refused: limits are "), @@ -95,23 +81,17 @@ class Stepper { setTarget(step); } - // Relative move. Anchored on the target rather than the current position - // so repeated jogs compose while a move is still in flight. + // Relative to the target, so repeated jogs add up mid-move. void moveSteps(long deltaSteps) { moveToStep(engine.target() + deltaSteps); } - // Cancels a move in flight by making the current position the target. - // Immediate: the axis stops on the next pulse it was going to emit. This is - // what an endstop hit wants, ramp or no ramp. + // Stops immediately, ramp or not. void stopHere() { setTarget(engine.position()); } - // Brings the axis to rest over the profile's stopping distance instead of - // dropping it dead, for a stop that is asked for rather than forced. Same - // thing as stopHere() when there is no profile, and it never extends a move - // that was already shorter than the stopping distance. + // Stops over the profile's stopping distance; stopHere() without a profile. void stopRamped() { if (!engine.isRunning()) return; @@ -120,18 +100,8 @@ class Stepper { if (debugEnabled) MC_DEBUG(F("Target step: "), engine.target()); } - // Soft limits: the range of steps the axis may be sent to. A move whose - // target lies outside it is refused, so the axis keeps the target it had. - // An axis already outside the range may still be sent back towards it; - // otherwise it could not be moved at all. - // - // Limits only mean something once step 0 is a known place, so they take - // effect only when armed: StepperManager arms them after a successful - // home() or a hand-set origin (setOrigin()). Before that, an axis - // without a switch could be anywhere and its limits would be wrong. - // - // A move already in flight past a new limit is cut short at the limit: - // it is running, so it can no longer be refused. + // Soft limits in steps, enforced once armed (after homing or setOrigin()). + // See docs/homing.md. void setLimits(long minStep, long maxStep) { if (minStep > maxStep) { long t = minStep; @@ -161,8 +131,7 @@ class Stepper { return maxStep; } - // Whether the limits are enforced. Homing disarms them for its search, - // which has to run past them to find the switch. + // Homing disarms the limits during its search. void armLimits(bool on = true) { armed = on; cutShortAtLimits(); @@ -172,7 +141,7 @@ class Stepper { return armed; } - // Redefines where the axis believes it is + // Redefines the current position. void setOrigin(long step = 0) { engine.setPosition(step); setState(HOLD); @@ -190,17 +159,14 @@ class Stepper { return state; } - // True exactly once, the first time this is polled after the axis settles - // into HOLD. + // True once, on the first poll after the axis settles into HOLD. bool reachedTarget() { bool r = justReached; justReached = false; return r; } - // Attach a limit switch. It goes into the slot for its own side, so an axis - // with a switch at each end takes two calls. Borrowed, not owned. Attach - // before begin(), which sets up the pin. + // Attaches a borrowed switch to the slot for its side. Call before begin(). void setEndstop(Endstop* endstop) { if (endstop) endstops[slot(endstop->getSide())] = endstop; } @@ -214,9 +180,7 @@ class Stepper { return endstops[slot(side)]; } - // Attach a speed profile, or nullptr for a flat interval (the default). - // The profile is borrowed, not owned - it is expected to outlive the axis, - // which a file-scope instance in the sketch does. + // Borrowed speed profile, or nullptr for a flat interval. void setProfile(const StepProfile* p) { engine.setProfile(p); } @@ -225,16 +189,12 @@ class Stepper { return engine.profile(); } - // The interval the next pulse will actually wait out: the profile's idea of - // the pace here, or the flat interval when there is no profile. Distinct - // from getStepInterval(), which is the top speed the axis was asked for. + // The interval right now; differs from getStepInterval() while ramping. unsigned long currentInterval() const { return engine.currentInterval(); } - // Pulse spacing, i.e. speed. With a profile attached this is the top speed - // the ramp works towards. Settable after construction because homing - // switches between a fast seek and a slow, repeatable re-approach. + // Pulse spacing; the cruise interval when a profile is attached. void setStepInterval(unsigned long us) { engine.setStepInterval(us); } @@ -243,8 +203,7 @@ class Stepper { return engine.stepInterval(); } - // The one piece of calibration a degree layer needs; unused by step-space - // motion itself. + // For converting to degrees; not used by step-space motion. uint16_t getStepsPerRevolution() const { return stepsPerRevolution; } @@ -253,8 +212,7 @@ class Stepper { return engine; } - // Per-axis switch for the DEBUG and TRACE messages. Has no effect unless - // MC_LOG_LEVEL compiles them in - see Log.h. + // Per-axis DEBUG/TRACE output, if MC_LOG_LEVEL compiles it in. void toggleDebug() { this->debugEnabled = !this->debugEnabled; } @@ -270,8 +228,7 @@ class Stepper { if (debugEnabled) MC_DEBUG(F("Target step: "), engine.target()); } - // True when step may be the axis's target: in range, or no further out of - // range than the axis already is. + // In range, or no further out of range than the axis already is. bool withinLimits(long step) const { if (!limited || !armed) return true; if (step >= minStep && step <= maxStep) return true; @@ -290,8 +247,7 @@ class Stepper { if (target < minStep && at >= minStep) setTarget(minStep); } - // Motion is reported through here rather than by writing `state`, so the - // MOVING -> HOLD edge behind reachedTarget() is detected in one place. + // The only writer of `state`, so the HOLD edge is detected in one place. void setState(State newState) { if (newState == HOLD && state != HOLD) { justReached = true; diff --git a/src/StepperManager.h b/src/StepperManager.h index a51dd10..1107c92 100644 --- a/src/StepperManager.h +++ b/src/StepperManager.h @@ -12,8 +12,7 @@ class StepperManager : public MotorManager { public: static constexpr uint8_t NO_ENABLE_PIN = 0xFF; - // Time the drivers are given to bring their outputs back up after the EN - // line is energised, before the first pulse is sent. + // Driver wake-up time after EN is energised, before the first pulse. static constexpr unsigned long DEFAULT_WAKE_US = 1000; explicit StepperManager(uint8_t capacity, uint8_t enablePin = NO_ENABLE_PIN) @@ -29,8 +28,7 @@ class StepperManager : public MotorManager { delete[] homing; } - // Energises the drivers and sets up every axis, endstop pins included. - // Homing is a separate, explicit step: home(). + // Energises the drivers and sets up every axis. Homing is separate: home(). void begin() { if (enablePin != NO_ENABLE_PIN) pinMode(enablePin, OUTPUT); @@ -45,10 +43,8 @@ class StepperManager : public MotorManager { lastActiveAt = millis(); if (!powered) enable(); - // Pulses sent while the drivers are still coming up would be lost, - // and a lost pulse is a step the counter claims but the axis never - // took. Withhold them rather than blocking, so the caller's loop - // stays responsive. + // Withhold pulses while the drivers wake (they would be counted but + // lost), without blocking the caller's loop. if (!ready()) return; } else if (releasesWhenIdle() && powered && millis() - lastActiveAt >= idleReleaseMs) { @@ -58,9 +54,8 @@ class StepperManager : public MotorManager { updateMotors(); } - // True while any axis still has steps to make. Answered from position - // rather than state, because state only turns MOVING once a pulse has gone - // out - and pulses are the thing being gated here. + // From position, not state: state turns MOVING only after a pulse, and + // pulses are what the gate holds back. bool hasWork() const { for (uint8_t i = 0; i < count; i++) { if (motors[i]->getStep() != motors[i]->getTargetStep()) return true; @@ -68,10 +63,8 @@ class StepperManager : public MotorManager { return false; } - // Idle release: drop the EN line once the whole fleet has been at rest for - // idleMs, and take it back automatically on the next move. 0 (the default) - // keeps the coils energised the whole time. Needs an enable pin; without - // one there is nothing to release. + // Releases EN after idleMs with the fleet at rest; the next move takes it + // back. 0 (default) never releases. See docs/power.md. void setIdleRelease(unsigned long idleMs, unsigned long wakeUs = DEFAULT_WAKE_US) { this->idleReleaseMs = idleMs; this->wakeUs = wakeUs; @@ -81,15 +74,12 @@ class StepperManager : public MotorManager { return idleReleaseMs; } - // The EN input is active low. Energising is what gives the axes their - // holding torque; releasing stops the coils heating, at the cost of the - // position the counters claim. + // EN is active LOW. Releasing stops the coils heating but loses holding torque. void enable() { if (enablePin != NO_ENABLE_PIN) { digitalWrite(enablePin, LOW); - // Only the transition opens a wake-up window. Re-energising a live - // line would otherwise stall the next pulse for no reason. + // Only a released -> energised transition opens a wake-up window. if (!powered) { enabledAt = micros(); waking = true; @@ -99,8 +89,6 @@ class StepperManager : public MotorManager { powered = true; - // With a wake-up window open, ready() lets the pulses through once it - // closes; without one they can go now. if (!waking) allowPulses(true); } @@ -125,8 +113,7 @@ class StepperManager : public MotorManager { if (id < count) motors[id]->moveToStep(step); } - // Relative move, anchored on the target, so repeated jogs compose while a - // move is still in flight. + // Relative to the target, so repeated jogs add up mid-move. void moveSteps(uint8_t id, long deltaSteps) { if (id < count) motors[id]->moveSteps(deltaSteps); } @@ -148,9 +135,7 @@ class StepperManager : public MotorManager { for (uint8_t i = 0; i < count; i++) motors[i]->stopHere(); } - // Redefines where an axis believes it is. Step 0 is then no longer the - // switch, so the axis stops counting as homed. It is a place someone - // chose, though, so its soft limits are armed from here on. + // Redefines the position: the axis is no longer homed, but its soft limits arm. void setOrigin(uint8_t id, long step = 0) { if (id >= count) return; motors[id]->setOrigin(step); @@ -162,9 +147,7 @@ class StepperManager : public MotorManager { for (uint8_t i = 0; i < count; i++) setOrigin(i, step); } - // Finds the axis's base position against its endstop - see Homing.h. - // Blocking. Energises the drivers first and waits out their wake-up, since - // a search that cannot move the axis just fails. + // Blocking two-pass homing (Homing.h). Energises the drivers first. HomingResult home(uint8_t id, const HomingConfig& cfg) { if (id >= count) return NOT_HOMED; @@ -192,9 +175,7 @@ class StepperManager : public MotorManager { return (id < count) ? motors[id]->getTargetStep() : 0; } - // Attach a speed profile (see LinearRamp.h), or nullptr for a flat - // interval. One instance can serve the whole fleet: profiles hold no - // per-move state. + // Borrowed speed profile, or nullptr for a flat interval. One can serve the fleet. void setProfile(uint8_t id, const StepProfile* p) { if (id < count) motors[id]->setProfile(p); } @@ -207,14 +188,12 @@ class StepperManager : public MotorManager { return (id < count) ? motors[id]->getProfile() : nullptr; } - // The pace the axis is running at right now, which under a ramp is not the - // interval it was asked for. + // The interval right now; differs from the set interval while ramping. unsigned long getCurrentInterval(uint8_t id) const { return (id < count) ? motors[id]->currentInterval() : 0; } - // Brings an axis to rest over its profile's stopping distance rather than - // dropping it dead. stopHere()/stopAll() remain immediate. + // Stops over the profile's stopping distance; stopHere()/stopAll() are immediate. void stopRamped(uint8_t id) { if (id < count) motors[id]->stopRamped(); } @@ -223,8 +202,7 @@ class StepperManager : public MotorManager { for (uint8_t i = 0; i < count; i++) motors[i]->stopRamped(); } - // Soft limits, in steps - see Stepper::setLimits(). Armed by home() and - // setOrigin(). + // Soft limits in steps, armed by home() and setOrigin(). See docs/homing.md. void setLimits(uint8_t id, long minStep, long maxStep) { if (id < count) motors[id]->setLimits(minStep, maxStep); } @@ -241,8 +219,7 @@ class StepperManager : public MotorManager { return (id < count) && motors[id]->limitsArmed(); } - // Pulse spacing, i.e. speed. Per axis, because a fleet rarely shares one - // reduction ratio. + // Pulse spacing, per axis. void setStepInterval(uint8_t id, unsigned long us) { if (id < count) motors[id]->setStepInterval(us); } @@ -264,9 +241,7 @@ class StepperManager : public MotorManager { return idleReleaseMs != 0 && enablePin != NO_ENABLE_PIN; } - // Closes the wake window as it passes, so a line that has been live for - // hours answers without consulting the clock - and cannot be caught out by - // the micros() rollover an open-ended comparison would trip over. + // Closes the wake window once passed, so micros() rollover cannot reopen it. bool ready() { if (!waking) return true; if (micros() - enabledAt < wakeUs) return false; @@ -276,9 +251,7 @@ class StepperManager : public MotorManager { return true; } - // Opens or shuts every axis's gate. Skipping update() while the drivers are - // not ready only holds back an engine that pulses from update(); one that - // steps on its own has to be told. + // Self-stepping engines must be told; skipping update() only holds polled ones. void allowPulses(bool allowed) { for (uint8_t i = 0; i < count; i++) motors[i]->getEngine().allowPulses(allowed); } From 8885f8c77855e50b58e9dd94a1aa2e20fb525ce1 Mon Sep 17 00:00:00 2001 From: AndrewwSS <76648798+AndrewwSSS@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:12:03 +0200 Subject: [PATCH 4/4] docs: shorten example and test comments; add a testing guide --- README.md | 1 + docs/testing.md | 66 +++++++++++++++++++ examples/FasEngine/FasEngine.ino | 10 ++- examples/Servos/Servos.ino | 5 +- examples/Steppers/Steppers.ino | 12 ++-- test/fake/Arduino.h | 26 ++------ test/fake/FakeEngine.h | 17 +---- test/fake/FastAccelStepper.h | 15 +---- .../test_polled_engine/test_polled_engine.cpp | 6 +- test/test_stepper/test_stepper.cpp | 11 ++-- .../test_stepper_logic/test_stepper_logic.cpp | 3 +- 11 files changed, 92 insertions(+), 80 deletions(-) create mode 100644 docs/testing.md diff --git a/README.md b/README.md index 8c9ce1d..3635f3c 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ anywhere else. Servos have their own root, `AngularMotor`, and share the same | [Releasing the coils](docs/power.md) | Idle power release on a shared enable line | | [Logging](docs/logging.md) | Compile-time log levels and the output stream | | [Architecture](docs/architecture.md) | File layout, the two roots, adding a motor type or manager | +| [Testing](docs/testing.md) | Host tests, the fake Arduino core and the fake engines | ## Tests diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..683c38b --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,66 @@ +# Testing + +The library's logic runs on the host, against fakes, so it is tested without a +board: + +```sh +pio test -e native # unit tests only +scripts/check.sh # unit tests, plus every example built for each board +``` + +CI runs `scripts/check.sh` on every push and pull request. + +## The fake Arduino core + +[`test/fake/Arduino.h`](../test/fake/Arduino.h) is just enough of the Arduino +core for the headers to compile and run on the host. Only the `native` +environment puts `test/fake/` on the include path; board builds use their real +core. Nothing touches hardware, so a test controls everything a sketch would get +from outside: + +| Area | Behaviour | +| --- | --- | +| Time | Stands still until the test moves it (`fake::advanceUs`), or moves a fixed amount on every `micros()` call (`fake::setAutoAdvance`), so busy-wait loops such as homing make progress. | +| Writes | Every `digitalWrite` is recorded with its time, so tests can count step pulses and check their spacing. | +| Reads | `digitalRead` returns what the test scripted, per pin or through a handler that can follow the axis (for example, a switch that closes at some step). | +| Serial | Collects everything printed, log output included. | + +`fake::reset()` returns to power-on: time 0, no writes, every input reading HIGH +(a pulled-up pin), Serial empty. Call it from `setUp()`. + +`F()` strings keep their own type, as on AVR, so overloads resolve exactly as +they do on a board. + +**Known gap:** `unsigned long` is 64 bits on the host and 32 bits on the boards, +so the `micros()` rollover cannot be reproduced. Tests keep time well clear of it. + +## FakeEngine + +[`test/fake/FakeEngine.h`](../test/fake/FakeEngine.h) is a `StepEngine` with no +pins and no timing, for testing the logic above the engine (`Stepper`, homing, +the fleet) on its own. It records what it was told and moves in one of two +modes: + +- **POLLED:** one step per `service()`, which returns true, like `PolledEngine`. +- **OWN:** `stepsPerService` steps per `service()`, which returns false, like an + engine that steps on its own. The steps come in a burst, the worst case for + how late the endstop guard sees a switch. + +Each step moves fake time on by the step interval, so endstop debounce runs as +it would on a real axis. `shaft()` is where the motor really is: unlike +`position()`, `setPosition()` does not reset it, so homing tests can check where +step 0 ended up. + +## Fake FastAccelStepper + +[`test/fake/FastAccelStepper.h`](../test/fake/FastAccelStepper.h) stands in for +FastAccelStepper 1.4.0, with only the calls `FasEngine` makes. It copies the +library's behaviour where that shapes `FasEngine`: + +- `moveTo()` while running is ignored when it would reverse the move, or stop it + where it is. +- `stopMove()` ramps down over `stepsToStop()` steps, never past the target. +- `forceStopAndNewPosition()` stops at once. +- Intervals faster than the hardware can step are refused. + +Nothing moves on its own: a test calls `run(n)` to let up to n steps go out. diff --git a/examples/FasEngine/FasEngine.ino b/examples/FasEngine/FasEngine.ino index 2057ee3..9c296d0 100644 --- a/examples/FasEngine/FasEngine.ino +++ b/examples/FasEngine/FasEngine.ino @@ -1,10 +1,8 @@ -// Two axes on the ESP32 whose pulses come from FastAccelStepper, in hardware: -// the swing stays smooth however long loop() is held up. Each axis owns its -// engine through EngineStepper; the ramp is the same LinearRamp a polled axis -// uses, which FasEngine hands the library as one acceleration. +// Two axes on the ESP32 with pulses from FastAccelStepper, in hardware, so the +// motion stays smooth however long loop() is held up. Each axis owns its engine +// through EngineStepper and shares one LinearRamp. // -// ESP32 only, and needs gin66/FastAccelStepper@1.4.0 in lib_deps. -// scripts/check.sh compiles it against the real library. +// ESP32 only; needs gin66/FastAccelStepper@1.4.0 in lib_deps. #include #include diff --git a/examples/Servos/Servos.ino b/examples/Servos/Servos.ino index da147a2..c3840de 100644 --- a/examples/Servos/Servos.ino +++ b/examples/Servos/Servos.ino @@ -1,8 +1,5 @@ // A hobby servo on its own pin and one on a PCA9685 channel, in one fleet, -// sweeping between two angles. -// -// Builds for the Uno and the ESP32; scripts/check.sh compiles it for both. -// On the ESP32 the PCA9685 sits on the default I2C pins, SDA 21 and SCL 22. +// sweeping between two angles. Uno or ESP32 (PCA9685 on SDA 21, SCL 22). #include #include diff --git a/examples/Steppers/Steppers.ino b/examples/Steppers/Steppers.ino index 0b79987..b214276 100644 --- a/examples/Steppers/Steppers.ino +++ b/examples/Steppers/Steppers.ino @@ -1,11 +1,7 @@ -// Two step+dir axes sharing one EN line: the first homes against a limit -// switch at start-up, then both swing back and forth under a speed ramp. X is -// a StepDirStepper, the one-line form; Y is a Stepper built on an engine of its -// own, the form that lets an axis pick its engine. -// -// Builds for the Uno (CNC Shield pins) and the ESP32 (the pin map from the -// migration plan). scripts/check.sh compiles it for both, which is how the -// library proves it still builds on each board. +// Two step+dir axes sharing one EN line: X homes against a limit switch at +// start-up, then both swing back and forth under a speed ramp. X is a +// StepDirStepper; Y is a Stepper on a separately declared engine. +// Uno (CNC Shield pins) or ESP32. #include #include diff --git a/test/fake/Arduino.h b/test/fake/Arduino.h index ebe6d56..b5e1e32 100644 --- a/test/fake/Arduino.h +++ b/test/fake/Arduino.h @@ -1,22 +1,7 @@ #pragma once -// A stand-in for the Arduino core, just big enough for the library's headers to -// compile and run on the host under `pio test -e native`. Only the native env -// puts this directory on the include path; boards build against their real core. -// -// Nothing here touches hardware, so a test controls everything a sketch would -// get from the outside world: -// -// time stands still until the test moves it (fake::advanceUs), or moves by -// a fixed amount on every micros() call (fake::setAutoAdvance) so that -// busy-wait loops such as homing's drive() make progress -// writes every digitalWrite is recorded with the time it happened -// reads digitalRead returns what the test scripted, per pin or through a -// handler that can follow the axis (a switch closing at some step) -// Serial collects whatever is printed, log output included -// -// Known gap: unsigned long is 64 bits here and 32 bits on the boards, so the -// micros() rollover cannot be reproduced. Tests keep time well clear of it. +// Minimal Arduino core for host tests: scripted time, pin reads and Serial, +// with every pin write recorded. See docs/testing.md. #include #include @@ -35,9 +20,7 @@ typedef uint8_t byte; typedef bool boolean; -// On AVR, F() keeps a string in flash and tags it with this type so Print knows -// to read it from there. On the host a plain pointer does, but the type is kept -// so overloads resolve exactly as they do on a board. +// Kept as a distinct type so overloads resolve as they do on AVR. class __FlashStringHelper; #define F(s) (reinterpret_cast(s)) @@ -128,8 +111,7 @@ struct State { inline State state; -// Back to power-on: time 0, no writes, every input reading HIGH (a pulled-up -// pin with nothing pulling it down), Serial empty. Call from setUp(). +// Power-on state: time 0, no writes, inputs HIGH, Serial empty. Call from setUp(). inline void reset() { state.nowUs = 0; state.autoAdvanceUs = 0; diff --git a/test/fake/FakeEngine.h b/test/fake/FakeEngine.h index 2fe6884..c3e634c 100644 --- a/test/fake/FakeEngine.h +++ b/test/fake/FakeEngine.h @@ -1,20 +1,7 @@ #pragma once -// A StepEngine with no pins and no timing, for testing the logic above the -// engine - Stepper, homing, the fleet - on its own. It records what it was -// told, and moves the way the test asks: -// -// POLLED one step per service(), which returns true, like PolledEngine -// OWN stepsPerService steps per service(), which returns false, like an -// engine that keeps stepping between two update() calls (FasEngine, -// TimerEngine). The steps come in a burst, which is the worst case -// for how late the endstop guard sees a switch. -// -// Each step moves fake time on by the step interval, so time passes as it would -// on a real axis and the endstops' debounce runs its course. -// -// shaft() is where the motor really is. Unlike position() it is not reset by -// setPosition(), so homing tests can check where step 0 ended up. +// A StepEngine without pins or timing, in POLLED or OWN (self-stepping) mode, +// for testing Stepper, homing and the fleet. See docs/testing.md. #include #include "StepEngine.h" diff --git a/test/fake/FastAccelStepper.h b/test/fake/FastAccelStepper.h index 862d823..c9413de 100644 --- a/test/fake/FastAccelStepper.h +++ b/test/fake/FastAccelStepper.h @@ -1,15 +1,7 @@ #pragma once -// A stand-in for FastAccelStepper 1.4.0, for testing FasEngine on the host. -// Only what FasEngine calls, with the library's behaviour where it shapes -// FasEngine's design: -// -// - moveTo() while running is silently ignored when it would reverse the -// move (or ask it to stop where it is). -// - stopMove() ramps down over stepsToStop() steps, never past the target. -// - forceStopAndNewPosition() stops at once. -// -// Nothing moves on its own: a test calls run(n) to let n steps go out. +// Fake FastAccelStepper 1.4.0 with the behaviour FasEngine depends on; a test +// calls run(n) to let steps go out. See docs/testing.md. #include @@ -133,8 +125,7 @@ class FastAccelStepperEngine { return &pool[used++]; } - // Frees every stepper, for a fresh test. The engine object itself is a - // static inside fasEngine() and outlives the tests. + // Frees every stepper; the engine itself is a static in fasEngine(). static void reset() { used = 0; } static inline FastAccelStepper pool[CAPACITY]; diff --git a/test/test_polled_engine/test_polled_engine.cpp b/test/test_polled_engine/test_polled_engine.cpp index 73d4251..ae50b8f 100644 --- a/test/test_polled_engine/test_polled_engine.cpp +++ b/test/test_polled_engine/test_polled_engine.cpp @@ -1,7 +1,5 @@ -// PolledEngine on its own, before any Stepper drives it: the same pulses, the -// same DIR handling and the same ramp pacing the Phase 0 tests pinned down for -// StepDirStepper, plus the inhibit that replaces the endstop guard inside the -// engine. +// PolledEngine on its own: STEP pulses, DIR handling, ramp pacing and the +// endstop inhibit. #include #include diff --git a/test/test_stepper/test_stepper.cpp b/test/test_stepper/test_stepper.cpp index f6085c3..a6bffae 100644 --- a/test/test_stepper/test_stepper.cpp +++ b/test/test_stepper/test_stepper.cpp @@ -1,7 +1,5 @@ -// Characterisation of today's StepDirStepper: what reaches the STEP and DIR -// pins for a move, and how the endstop guard treats a pressed switch. These -// pin down behaviour before the StepEngine split, which must not change any -// of it. +// StepDirStepper at the pins: what reaches STEP and DIR for a move, and how the +// endstop guard treats a pressed switch. #include #include @@ -232,9 +230,8 @@ void test_a_min_switch_only_guards_travel_downwards() { TEST_ASSERT_EQUAL(5, axis.getStep()); } -// A resync is a reading, not an edge: it must not blind the guard. Homing -// opens every search with one, and starting close to the switch used to -// overshoot by up to 5 ms of travel. +// A resync must not open a debounce window: homing starts with one, and a window +// would let an axis near the switch overshoot by up to 5 ms of travel. void test_a_press_straight_after_a_resync_is_seen() { Endstop maxSwitch(SWITCH_PIN, Endstop::NORMALLY_OPEN, Endstop::MAX); maxSwitch.begin(); diff --git a/test/test_stepper_logic/test_stepper_logic.cpp b/test/test_stepper_logic/test_stepper_logic.cpp index c750082..abfbdef 100644 --- a/test/test_stepper_logic/test_stepper_logic.cpp +++ b/test/test_stepper_logic/test_stepper_logic.cpp @@ -210,8 +210,7 @@ void test_a_min_switch_inhibits_downwards() { TEST_ASSERT_EQUAL(0, axis.getStep()); } -// What Phase 4's 1 ms endstop task is for: an engine that steps on its own can -// cover several steps between two looks at the switch. +// An engine that steps on its own can make several steps between two endstop checks. void test_an_engine_on_its_own_overshoots_by_at_most_one_burst() { FakeEngine engine(FakeEngine::OWN, 5); Endstop maxSwitch(SWITCH_PIN, Endstop::NORMALLY_OPEN, Endstop::MAX);