Skip to content

Add experimental Teensy 4.0/4.1 support (pd_teensy backend) - #374

Merged
gin66 merged 14 commits into
gin66:masterfrom
ARDUTECH0:feature/teensy4-support
Sep 22, 2026
Merged

gin66 merged 14 commits into
gin66:masterfrom
ARDUTECH0:feature/teensy4-support

Conversation

@ARDUTECH0

@ARDUTECH0 ARDUTECH0 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Status: tested on real Teensy 4.0 hardware - full 16-stepper capacity, all 4 QuadTimer modules, and the attached driver's full rated speed

Update: this has now actually been built and run on a real Teensy 4.0, driving steppers through a real driver board (step/dir/enable) and a real motor. Three real bugs turned up during bring-up and are fixed in this branch:

  1. A defect (beyond the one in Fix static/extern linkage conflict for log2 timer-frequency variables #372) in the log2 timer-frequency fallback path: the extern declarations lived in the wrong header, after code that already needed them. This is what made the backend fail to build at all initially.
  2. A missing Cortex-M7 memory barrier (dsb) in the step-timing ISR, matching a barrier luni64/TeensyStep4's own code has for the same reason - without it, the step rate would plateau and then hang as interrupt frequency increased during acceleration.
  3. The big one, also shared with Fix static/extern linkage conflict for log2 timer-frequency variables #372: log2_timer_freq_div_sqrt_of_2 was computed with a copy-paste of the formula for a different constant right below it (squaring the tick frequency instead of dividing it by sqrt(2)). This made any move that starts or reverses from a genuine standstill compute a multi-billion-tick delay instead of a normal one - the stepper would "hang" for several minutes. It only showed up once testing moved past back-to-back moves (which never let the queue fully drain) to a sketch with a real idle gap between moves (examples/TeensyMoveAndPause, included here). setLinearAcceleration() happens to mask it completely, which is exactly why it wasn't caught by the first round of testing.

Speed ceiling, measured, not guessed: using examples/TeensyMaxSpeedFinder (sweeps setSpeedInHz(), each leg moving out and back by a distance long enough to actually reach and cruise at that speed, operator visually confirms via a marked shaft that no steps were lost) against a DM556 driver + stepper motor at 25000 pulses/rev microstepping, 30 V supply:

  • At an aggressive 200000 steps/s² acceleration, steps were lost at 125000 Hz (100000 Hz was clean) - a motor-torque/acceleration limit, not a step-generation one.
  • At a gentler 20000 steps/s² acceleration, every level up to 200000 Hz passed clean - the DM556's own datasheet-rated ceiling (500 kHz on the special-order variant), with zero steps lost anywhere in that sweep.

All 16 steppers, all 4 QuadTimer modules, measured: examples/TeensyMultiStepperTest runs the full claimed capacity - 16 steppers, 4 per module across all 4 QuadTimer modules (TMR1-TMR4) - each at a different speed/acceleration/travel distance so none are ever in lockstep. Left running continuously reversing direction: every one of the 16 axes, across all 4 modules, kept moving, reversing and reaching its target correctly the whole time - no cross-talk, no stuck state, on any channel or module. This is the first confirmation that modules 3 and 4 (not just the first two) work correctly under this backend, and that all 16 channels' combined interrupt load doesn't cause any of them to miss a deadline.

Also added examples/TeensyBringUp, examples/TeensyMoveAndPause, examples/TeensyEnableBlinkTest, examples/TeensyMaxSpeedFinder and examples/TeensyMultiStepperTest - the sketches actually used for this bring-up, diagnosis and measurement, useful for anyone else validating this backend.

Enable pin (ENA) control is independently confirmed working: a decisive test (enableOutputs()/disableOutputs() toggled directly, LED wired straight to the pin, no driver board or auto-enable timing involved) shows the pin switching states exactly in sync with the calls. Also checked against the DM556 driver's official datasheet used for this testing: its ENA polarity (LOW = enabled) matches this backend's default.

Still open / not yet checked:

  • Pulse width / edge timing on a scope (functionally verified via the speed sweep above, but never directly observed on a scope)
  • NVIC priority values (FAS_TEENSY_STEP_ISR_PRIORITY / FAS_TEENSY_RAMP_TICK_PRIORITY) are reasoned defaults, not measured
  • The 50k steps/s default ceiling in _pd_initVars() is still an untested-by-default placeholder - real applications should measure their own ceiling with examples/TeensyMaxSpeedFinder and set setAbsoluteSpeedLimit() accordingly, same as done here
  • The 16-stepper test above used light/no mechanical load per axis (verifying software correctness, not per-axis torque at speed) - the earlier single-axis speed sweep is what verified actual no-missed-steps behavior under load

Summary

  • New pd_teensy backend for the i.MX RT1062 (Teensy 4.0/4.1): up to 16 steppers via the chip's 4 QuadTimer (TMR) modules x 4 channels each.
  • Unlike the SAMD/SAM backends, the step pin isn't muxed to the timer's own output pin - the ISR toggles it directly via digitalWriteFast(), so any digital pin works (same flexibility ESP32 already has), instead of being limited to specific muxed pins.
  • Each step costs two hardware-timed compare-match interrupts (rising edge, then falling edge after the pulse width) - the same principle this library's own AVR backend already uses to time each edge exactly regardless of interrupt latency, just with a QuadTimer channel instead of AVR's output-compare pin.
  • The register-level QuadTimer sequence (CTRL/CSCTRL/COMP1/CMPLD1 setup) is adapted from luni64/TeensyStep4 (MIT licensed), a stepper library already verified on real Teensy 4.x hardware - not guessed from the reference manual. Credited in the code comments.
  • Needs the same log2 timer-frequency fixes as Fix static/extern linkage conflict for log2 timer-frequency variables #372 (this backend's 9.375 MHz tick rate doesn't match either existing fast-path TICKS_PER_S constant, so it exercises the generic runtime path). Those fixes are duplicated in this branch so it stands alone; happy to rebase onto Fix static/extern linkage conflict for log2 timer-frequency variables #372 once/if that merges, whichever is easier for you.
  • README.md/CHANGELOG.md/library.properties updated, marked experimental.

Test plan

  • Builds and uploads via Arduino IDE / Teensyduino on a real Teensy 4.0
  • Single stepper, back-and-forth move, reaches target correctly, including through direction reversals from a full stop
  • Enable pin (ENA) control verified directly against the pin (LED + manual enableOutputs()/disableOutputs() toggling)
  • Speed sweep 5000-200000 Hz, visually confirmed against a marked shaft for lost steps - clean up to the attached driver's own 200 kHz rated ceiling
  • All 16 steppers simultaneously, spanning all 4 QuadTimer modules, independent speeds/accelerations, continuous operation with no cross-talk or stuck state
  • Scope verification of pulse width / edge timing

ARDUTECH0 and others added 2 commits September 21, 2026 13:35
RampControl.cpp declared log2_timer_freq (and its two derived values)
static, while RampGenerator.h declares them extern for use from
RampGenerator.cpp, a separate translation unit. A static redeclaration
cannot follow a non-static one in the same TU, so this fails to build
for any platform whose TICKS_PER_S isn't exactly 16000000 or 21000000
(the two values RampCalculator.h has precomputed fast-path constants
for) - e.g. AVR boards not running at 16/21 MHz.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New platform backend for the i.MX RT1062 (Teensy 4.0/4.1), using the
chip's 4 QuadTimer (TMR) modules x 4 channels each for up to 16
independently timed steppers. Unlike the SAMD/SAM backends, the step
pin is not muxed to the timer's own output pin: the ISR toggles it
directly via digitalWriteFast(), so any digital pin works, the same
flexibility ESP32 already has. Each step costs two hardware-timed
compare-match interrupts (rising edge, falling edge after the pulse
width), the same principle this library's AVR backend already uses to
time each edge exactly regardless of interrupt latency.

The register-level QuadTimer sequence is adapted from luni64/TeensyStep4
(MIT licensed), a stepper library already verified on real Teensy 4.x
hardware - not guessed from the reference manual.

Includes the same log2 timer-frequency linkage fix as gin66#372, since this
backend's 9.375 MHz tick rate doesn't match either of the two existing
fast-path TICKS_PER_S constants and needs that generic code path.

STATUS: EXPERIMENTAL, NOT YET VERIFIED ON REAL HARDWARE. Written and
reviewed without access to a Teensy 4.x board, a compiler for it, or
an oscilloscope - see the warning at the top of pd_teensy/pd_config.h.
Needs someone with the actual board to build it and check step timing.
@ARDUTECH0
ARDUTECH0 force-pushed the feature/teensy4-support branch from 60d968a to 74adbf6 Compare September 21, 2026 10:44
@gin66

gin66 commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Thanks for the patch. I will keep it open until anyone has confirmed, that it works.

Verified on real Teensy 4.0 hardware for the first time (motor moves,
ramps, and reaches target correctly at 2000 Hz / 4000 steps/s^2).
Found and fixed two real bugs along the way:

- RampCalculator.h/RampGenerator.h: the log2_timer_freq extern
  declarations lived in RampGenerator.h, included *after*
  RampCalculator.h - but RampCalculator.h's own inline methods
  (ramp_config_s::update() etc.) use log2_timer_freq as soon as that
  header is parsed, before RampGenerator.h's declarations are ever
  seen. Moved the declarations into RampCalculator.h itself, in the
  same branch that defines SUPPORT_LOG2_TIMER_FREQ_VARIABLES. This is
  what made the Teensy backend fail to compile at all (also affects
  any other platform hitting this fallback branch, e.g. AVR boards not
  running at 16/21 MHz) - same root cause as the static/extern issue
  in gin66#372, a second defect in the same never-before-exercised code
  path.

- teensy_queue.cpp: the module-level ISR loop was missing a `dsb`
  memory barrier after clearing the QuadTimer CSCTRL.TCF1 flags. The
  Cortex-M7 write buffer does not guarantee that clear has reached the
  peripheral by the time the ISR returns, which showed up as the
  motor's step rate plateauing then hanging entirely as interrupt
  frequency increased during accel - exactly the failure mode
  observed on hardware. luni64/TeensyStep4's own module ISR carries
  the same barrier with the same reasoning; this backend had dropped
  it when adapting that code.

Also, now that real hardware is available to tune against:
- FAS_TEENSY_TMR_PRESCALE is now a documented, overridable macro
  (0..7) instead of a hardcoded prescaler, with TICKS_PER_S derived
  from it so the two can't drift apart.
- Step-timing QuadTimer interrupts are now given a higher NVIC
  priority than the ~4 ms ramp tick (FAS_TEENSY_STEP_ISR_PRIORITY /
  FAS_TEENSY_RAMP_TICK_PRIORITY, both overridable), so a slow queue
  refill under load can't delay an already-scheduled step edge.

Adds examples/TeensyBringUp, the bring-up sketch used for this
testing: moves one stepper back and forth and prints position/speed/
ramp-state/queue-state/enable-pin diagnostics to Serial, useful for
verifying this backend on your own board before relying on it.

Enable pin behavior was also checked during this session and works as
documented (LOW = enabled by default) - the driver board's own power
LED, which some test rigs may be watching, is unrelated to the ENA
signal and stays lit whenever the driver has power.
@ARDUTECH0

Copy link
Copy Markdown
Contributor Author

Video of the hardware test referenced above: single stepper on a DM556 driver board (step/dir/enable), Teensy 4.0, examples/TeensyBringUp sketch, 2000 Hz / 4000 steps/s^2, moving back and forth between 0 and 3200 steps. Motor accelerates, cruises, decelerates and reaches the target correctly.

IMG_4199_compressed.mp4

TeensyEnableBlinkTest: decisive test of enableOutputs()/disableOutputs()
alone (no auto-enable timing, no driver board) - toggles every 2s with
an LED wired straight to the enable pin. Used during this session's
bring-up to confirm the enable pin control itself works correctly;
earlier confusion was from watching a driver board's own power LED
(unrelated to the ENA signal) instead of the actual pin.

TeensyMaxSpeedFinder: sweeps setSpeedInHz() through increasing levels,
each leg moving a fixed step count out and back (net zero), pausing
for the operator to visually confirm (via a marked shaft) that no
steps were lost before advancing - since there's no encoder feedback,
software alone cannot detect missed steps on this backend.
ARDUTECH0 and others added 9 commits September 22, 2026 08:49
…ardware)

Found via real Teensy 4.0 bring-up: any move that starts or reverses
from a genuine standstill (no setLinearAcceleration() in use) would
"hang" for several minutes. Root cause: log2_timer_freq_div_sqrt_of_2
was computed with log2_multiply(log2_timer_freq, log2_timer_freq) - a
copy-paste of the very next line's formula for
log2_timer_freq_square_div_2 - squaring the tick frequency instead of
dividing it by sqrt(2). calculate_ticks()'s near-standstill branch then
produced multi-billion-tick results instead of a normal value.
setLinearAcceleration() masks this entirely, since it routes small
step counts through a different (cubic) branch that never touches this
constant - which is exactly why it took a real motor, run without that
option, to expose it. Shared root cause and now shared fix with gin66#372.

Adds examples/TeensyMoveAndPause, the sketch that exposed this bug
(move, sit genuinely idle for a few seconds so the driver actually
disables, move again - unlike TeensyBringUp's back-to-back moves,
which never let the queue fully drain and therefore never hit this
path).
…sult

Distance per leg was a fixed 2000 steps regardless of the speed level
being tested - nowhere near enough to actually reach (let alone cruise
at) higher speed levels, silently turning them into low triangular
blips that never tested the speed they claimed to. Distance is now
computed per level from the configured acceleration, so every level
actually reaches and briefly cruises at its target speed before
returning.

Used to measure a real ceiling on a DM556 driver + stepper motor at
25000 pulses/rev, 30V: 100000 Hz clean at 200000 steps/s^2 acceleration,
125000 Hz lost steps at that same acceleration (a motor torque/
acceleration limit) - but every level up to 200000 Hz (the DM556's own
rated ceiling) passed clean at a gentler 20000 steps/s^2. Defaults
updated to reflect the second, successful sweep.
5 steppers, each moving independently at a different speed/acceleration
(deliberately not in lockstep, to make any timing cross-talk between
channels/modules easy to notice), spanning both the first QuadTimer
module (TMR1, 4 channels - the only one any prior test here exercised)
and the second (TMR2, channel 0). Ran for several minutes: every axis's
position, speed and ramp state kept advancing independently and
correctly the whole time, none showed the frozen-position/stuck-rampState
pattern of the bugs found and fixed earlier in this branch.
Was capped at 5 (enough to cross into the 2nd QuadTimer module). Now
defaults to all 16 (4 modules x 4 channels), with a shared enable pin
across all steppers to keep the pin count within Teensy 4.0's 40 pins
(16 unique step + 16 unique dir + 1 shared enable = 33).

Ran all 16 simultaneously, each at a different speed/acceleration/
distance so none are ever in lockstep: every axis across all 4 modules
(TMR1-TMR4) kept moving, reversing and reaching its target correctly
for the full run, with no cross-talk or stuck state anywhere - the
first real confirmation that all 4 QuadTimer modules, not just the
first two, work correctly under this backend.
TeensyMaxSpeedFinder.ino called setAbsoluteSpeedLimit() unconditionally,
but that method only exists when SUPPORT_UNSAFE_ABS_SPEED_LIMIT_SETTING
is defined by the platform's pd_config.h. atmelsam (Arduino Due) does
not define it, and the CI build matrix compiles every example against
every platform, so the atmelsam job failed with "has no member named
setAbsoluteSpeedLimit". Unrelated to the RampControl/RampCalculator
fixes in this branch.
Same reasoning as the existing StepperDemo/MoveTimed skips just above:
nanoatmega168 has only 14336 bytes of usable flash after the
bootloader. TeensyMultiStepperTest drives up to 16 simultaneous
FastAccelStepper instances (16KB+ compiled), which overflowed that by
~1KB (15304 bytes). It's a Teensy-only stress test anyway and was
never meant to run on an 8-bit AVR.
@gin66

gin66 commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Great. Thanks for your (rudimentary) test on HW and github actions pass, too.

What still need to be done in readme:

  • remove remark to not being tested on HW
  • add a line to Contribution section.

If you have time, you can look at the esp32_hw_based tests and check, if some sequences can be used to test your port.. You can find the log file in that directory. I remember test of one driver/sdk combination takes approx. 20minutes. Unfortunately there is no pulse counter support as for esp32, so only visually verification is possible, if the stepper returns to zero position.

README and pd_config.h/teensy_queue.{h,cpp} said the Teensy 4.x backend
was "not yet verified on real hardware" - that was true when written,
but it has since been tested on a real Teensy 4.0 with a DM556
industrial driver (single-axis speed sweep to 200 kHz with no missed
steps, and all 16 steppers running simultaneously across all 4
QuadTimer modules with no cross-talk - see CHANGELOG). Update the
remaining stale remarks accordingly; the one caveat that still stands
is pulse width/edge timing, which hasn't been checked on a scope yet.

Also add the Teensy port to the README Contribution section, matching
the existing entries for other platform ports.
@ARDUTECH0

Copy link
Copy Markdown
Contributor Author

Thanks for the guidance! Pushed the requested README updates:

  • Removed the "not yet hardware-verified" remarks (they're now redundant given the HW testing already reported in this thread).
  • Added the Teensy port to the Contribution section.

Also took a look at extras/tests/esp32_hw_based per your suggestion. Unfortunately the seq_*.py scripts aren't directly reusable for this port: every one of them gates pass/fail on the bracketed pulse-counter value matching the API position (e.g. >> M1: @1100 [1100]), and that whole path in StepperDemo.ino is compiled only under SUPPORT_ESP32_PULSE_COUNTER - there isn't even a StepperPins_teensy.h yet for StepperDemo to target this platform. So this lines up with what you already expected: without an ESP32-style pulse counter on the Teensy side, only visual verification (mark the shaft, confirm it returns to the exact same position) is possible for now. That's the approach examples/TeensyMaxSpeedFinder.ino and examples/TeensyMultiStepperTest.ino already use.

If it'd be useful down the line, a Teensy equivalent of the automated harness could work off GPIO edge-counting (e.g. an interrupt pin wired to a spare step output) instead of PCNT, but that's a separate, bigger effort - happy to look into it if you think it's worth it.

@gin66
gin66 merged commit cec05bf into gin66:master Sep 22, 2026
117 checks passed
@ARDUTECH0

Copy link
Copy Markdown
Contributor Author

Now that this (and #372) are merged into master, would you be open to cutting a new tagged release (version bump in library.properties + a git tag) when you get a chance? That's what makes a new version show up in the Arduino Library Manager / PlatformIO registry - right now master has the Teensy support but anyone installing via Library Manager is still only getting 1.3.3. No rush, just flagging it since master already looks release-ready.

@gin66

gin66 commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Released as 1.3.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants