Robohouse ’26 Library
Contents

Chapter 3

Timers, PWM, and the art of making step pulses

5 sections · about 8 minutes

3.1 What a stepper driver actually wants from you

Before the Teensy's timers, it helps to be precise about what the controller has to produce.

A step/direction driver like the TMC2209 has two logic inputs that matter for motion. DIR is a level: high means one direction, low means the other. STEP is an edge: on each rising edge, the driver advances its internal microstep counter by one position, which shifts the current in the two motor coils by one increment around the sine/cosine table, which rotates the motor by one microstep.

That is the whole interface. There is no position, no velocity, no acknowledgement. The driver has no idea where the motor is and no idea whether it got there. Everything about position and speed is encoded purely in when you send the edges.

The requirements are: DIR must be stable for a short setup time before the STEP edge (the TMC2209 needs on the order of 20 ns, which is nothing — but if you toggle DIR in one interrupt and STEP in another you can violate it, so don't); the STEP pulse must be at least a minimum width high and a minimum width low (again, tens of nanoseconds for the TMC2209 — trivially satisfied); and the interval between edges determines speed.

The interval is where the difficulty lives. Motor speed in revolutions per second is simply the step frequency divided by the number of microsteps per revolution. To accelerate smoothly, you must smoothly vary the interval between consecutive edges. To coordinate six axes so that the tool moves in a straight line, you must produce six independent, precisely-related pulse trains simultaneously. And any error in the timing of those edges — any jitter — is a small velocity error, which at best makes noise and at worst causes a motor to lose synchronisation with its field and skip.

3.2 The step-rate budget

It is worth working out how hard this is for the VCP6.

A standard NEMA-17 stepper is 1.8° per full step, which is 200 full steps per revolution. At 1/32 microstepping — which is what the PAROL6 uses on all axes — that is 6400 microsteps per motor revolution. Now apply the reduction ratios: joint 2 has a 20:1 planetary gearbox, so a full revolution of joint 2 takes 128,000 microsteps.

Suppose you want joint 2 to move at 60° per second, which is a fairly brisk but not extreme speed for a desktop arm. That is one sixth of a revolution per second, so about 21,300 steps per second. Joint 1, with its 6.4:1 belt, needs about 6,800 steps per second for the same angular rate. Sum across six axes moving together and you are looking at a worst case somewhere in the region of 60,000 to 100,000 step events per second.

Now the key architectural question: can you afford an interrupt per step? At 100,000 interrupts per second on a 600 MHz core, you have 6,000 clock cycles between interrupts. Interrupt entry and exit on a Cortex-M7 costs on the order of 20 to 30 cycles. Even a fairly heavy handler doing per-axis Bresenham arithmetic will fit in a few hundred cycles. So yes, comfortably. The Teensy's speed buys you the simple architecture here, which is worth taking.

Compare that with an 8-bit AVR at 16 MHz, where the same interrupt rate would leave you 160 cycles and the answer would be a firm no. Much of the received wisdom in the 3D printer and CNC world about clever step-generation tricks was developed under that constraint, which you do not have.

3.3 The timing resources available

Periodic Interrupt Timers (PIT) are the simplest option, and on the Teensy they are exposed through the IntervalTimer class. You get four of them. You give one a period in microseconds and a function to call, and it calls that function at that rate, forever. IntervalTimer accepts floating-point microseconds and internally computes the closest achievable divisor from the peripheral clock, so the resolution is far better than one microsecond. This is the workhorse for a fixed-rate control loop.

FlexPWM modules are the sophisticated option. There are four of them, each with three submodules, each submodule having two outputs. FlexPWM can generate pulses entirely in hardware with no CPU involvement, can be reloaded from a buffer, can be triggered by and can trigger other peripherals, and can be driven by DMA. If you want perfectly jitter-free step pulses at very high rates, FlexPWM is how you get them. The cost is complexity: you are programming a fairly intricate peripheral more or less directly.

QuadTimer modules — four of them, four channels each — sit between the two in capability. They can generate periodic outputs, capture input edges, and cascade. The well-known TeensyStep library uses these to generate step pulses in hardware, which is why it can hit step rates in the hundreds of thousands per second without the CPU melting.

General Purpose Timers (GPT), of which there are two, are similar in spirit to the PIT but with more features.

And finally the ARM cycle counter, ARM_DWT_CYCCNT, is a free-running 32-bit counter incrementing once per core clock. At 600 MHz it wraps about every seven seconds. It is not a scheduling mechanism, but it is the easiest way to measure how long a piece of code takes. Instrument your interrupt handler with it early.

3.4 Three architectures, and which to choose

Architecture one: the fixed-rate DDA. You run a single IntervalTimer at a constant high rate — say 20 kHz or 40 kHz. On every tick, for each of the six axes, you run a Bresenham-style accumulator: add that axis's "steps remaining" increment to an accumulator, and when the accumulator overflows, emit a step pulse on that axis. Acceleration is handled by recomputing the increments periodically from a velocity profile.

This is the architecture used by GRBL, by Marlin, and by most of the CNC world. It produces exactly coordinated multi-axis motion, because all axes are driven from the same tick and therefore cannot drift relative to one another. It is straightforward to reason about, and it has a bounded, predictable CPU cost — one interrupt at a fixed rate, regardless of how fast the motors are going. And the step timing quantisation it introduces (each step lands on a tick boundary) is small if your tick rate is comfortably above your maximum step rate.

Its limitation is exactly that quantisation. If your tick rate is 20 kHz, your maximum step rate is 20 kHz per axis, and step intervals are quantised to 50 µs. At high speeds that quantisation becomes a meaningful fraction of the interval and shows up as velocity ripple. The fix is simply to raise the tick rate, which on a 600 MHz M7 you can afford to do.

Architecture two: per-axis hardware timers. Give each axis its own timer channel, programmed to generate a pulse train at that axis's current required rate, and reprogram the rate periodically as the velocity profile evolves. TeensyStep works essentially this way. The advantage is very high achievable step rates with essentially zero jitter and almost no CPU load. The disadvantage is that coordinating six axes so that they arrive together requires care, because each timer runs independently and there is no shared tick keeping them in lockstep.

Architecture three: DMA-fed hardware PWM. You precompute a buffer of pulse timings and let DMA feed them to a FlexPWM module. This is the highest-performance and highest-complexity option — appropriate for very fast machines, and overkill for a desktop arm.

My recommendation for the VCP6 is architecture one, a fixed-rate DDA at something like 25 to 50 kHz, driven by an IntervalTimer. It gives you exact multi-axis coordination, which is what a robot arm needs — a Cartesian straight line depends on all six joints tracking a common time base. Measure the ISR duration with the cycle counter, confirm you are using well under half the available time, and move on.

3.5 Structuring the step ISR

A fixed-rate step ISR has a specific shape: minimum possible work, and no blocking. Concretely:

The ISR reads a "current segment" structure that some lower-priority code has prepared: the step increments per axis, the direction bits, and how many ticks this segment lasts. It updates the six accumulators, sets any direction pins that need to change, emits step pulses where accumulators overflowed, decrements the segment's remaining tick count, and — if the segment is finished — pops the next segment from a ring buffer. That is all.

Everything else happens outside the ISR. Velocity profiling, look-ahead, inverse kinematics, communication with the host: all of it runs in the main loop or in a lower-priority interrupt, producing segments into the ring buffer that the ISR consumes. The ring buffer is the boundary between the hard real-time world and the soft real-time world, and keeping that boundary clean is the structural decision the rest of the firmware rests on.

Two implementation details are worth stating explicitly. Variables shared between the ISR and the main loop must be declared volatile, or the compiler will happily cache them in a register and your main loop will never see the ISR's updates. And on a Cortex-M7 with a write buffer, if the last thing your ISR does is clear an interrupt flag, add a memory barrier or a dummy read afterwards, or the write may not have landed before the ISR returns and you will get a spurious re-entry.

For the step pulse itself, there is no need for a delay. Set all the step pins high at the start of the ISR body, do all your accumulator arithmetic, and set them low at the end. The arithmetic itself provides the pulse width, it is comfortably longer than the driver's minimum, and you have spent no time waiting.