How TinyG Keeps Real Time - synthetos/TinyG GitHub Wiki

How TinyG keeps real time on an 8-bit processor

TinyG runs on an Atmel ATxmega192A3: an 8-bit processor at 32 MHz with no floating-point unit (FPU). It still parses JSON and G-code text, converts numbers to floating point, plans jerk-limited motion, and produces step pulses with precise timing. It can do this because the work is split by how urgent it is, and the slow work never runs in the time-critical path.

The pipeline

Work flows down four stages. Each one runs earlier, and with more slack, than the one below it.

Stage Where it runs Arithmetic Deadline
Receive Serial receive interrupt Copy a byte into a ring buffer One character time
Parse Main loop Text, JSON, string to float None: lines wait in the buffer
Plan Main loop Software floating point The time the queued moves take to run
Prepare segments Low-priority interrupt Floating point, once per segment About 5 ms (a segment)
Step High-priority timer interrupt, 50 kHz Integer additions only 20 µs (one tick)

Receive

Characters arrive in an interrupt that only copies each byte into a ring buffer. Nothing is interpreted here.

Parse: once per line, with no deadline

The main loop (the controller, a cooperative "superloop") reads a complete line from the buffer and parses it: JSON or text commands, or G-code. This is where strings are converted to floating-point numbers, one conversion per number on the line. It is relatively slow on an 8-bit processor, but nothing is waiting on it. Motion that is already planned keeps running while a line is parsed, and the next line simply waits in the buffer.

Plan: well ahead of the motion

Each parsed move goes into the planner queue (up to 32 moves). The planner computes the jerk-limited speed profile for the queued moves in software floating point. Because it works on moves that will run later, its deadline is the time the whole queue takes to execute, usually tenths of a second to seconds, not microseconds. A move is replanned as new moves arrive behind it.

Prepare segments: the first time-sensitive step

The running move is cut into short segments, nominally 5 ms each (2.5 ms minimum). For each segment, a low-priority interrupt converts the planned speed into what the step generator needs: integer step counts and a rate for every motor. This is the last place floating point is used, and it happens a few hundred times a second, not tens of thousands.

Step: integers only

The step interrupt runs at a constant 50 kHz. On each tick it adds a precomputed integer to each motor's accumulator and pulses the motor when it overflows (a DDA, or Bresenham, step generator). It never touches floating point, strings or JSON. Its cost per tick is small and fixed, which is what keeps the pulse timing exact. Details: Stepper Module.

Why this works

  • The expensive work is done early and once. A number in a G-code line is converted from text once, when the line is parsed. A move's profile is computed once (and refined as later moves arrive), not on every step.
  • Queues absorb the slack. The serial ring buffer and the planner queue let the slow stages run at their own pace. As long as the host keeps sending and the planner stays a few moves ahead, the step interrupt never waits.
  • The hard real-time code is tiny and integer-only. Its worst case is short and predictable, so it can preempt everything else without disturbing it.

Limits

The design has limits, and they show up as speed, not as errors:

  • Very dense G-code (thousands of tiny moves) can keep the planner and parser fully busy. The machine then runs slower than the programmed feed rather than skipping steps. The feed-limit advisor reports this, see Runtime session (rt).
  • The host must keep the queue fed. If lines arrive too slowly, the planner runs out of moves and the machine stops between them. Line mode ($rxm=1) is the recommended way to stream; see Sending Data to TinyG using RX Packet Mode.
  • The step rate has a ceiling. The step generator's 50 kHz clock limits how fast any motor can step; the planner keeps moves below it.

For the code itself, see Introduction to the TinyG Code Base.