Skip to content

Pulse feeder contract

/dev/glowforge is the pulse-stream ring device of glowforge.ko. This page is the contract a feeder must obey: the device semantics, the run-control attributes, live appends and backpressure, end-of-data and underrun, the dead man's switch, and backtrack. The byte layout and the hardware ring (SDMA + EPIT into a GPIO register) are on Step engine. The other attributes of the module are on Kernel module. This page is the authoritative text of the feeder contract.

Everything here is enforced by the SDMA script and the driver. It is not negotiable at run time.

The ring, and two ways to fill it

The ring itself, and the two ways a controller uses it (GRBL mode's live stream and cloud mode's preload), are on Step engine. The GRBL-side feeder is on grblHAL driver; the cloud preloader is on Cloud mode. What follows is what a feeder must obey either way.

Ring size and the reserved pool

The ring size is the ring_mb module parameter: the size in MiB, a power of two from 1 to 1024, default 32. An invalid value is logged and the default is used. The ring must fit the cnc reserved-memory pool (cnc-pulsebuf in the device tree), a size-aligned no-map pool of 32 MiB that the device tree reserves for it; the pool is described on Image and BSP. For longer cloud-mode jobs, raise both the pool and ring_mb.

A writer must leave 32 KiB of ring unwritten behind the play head. The gap is retained history: 32 KiB is 3.2 s at the 10 kHz print tick, and it survives any fill, live-fed or preloaded, so a pause always has at least this much to back into.

The device: /dev/glowforge

Write/Seek/Lock, Binary, ring (size = the ring_mb module parameter, default 32 MiB; power of two, must fit the cnc reserved-memory pool).

Interface to the pulse-stream ring buffer. Exclusive-open: a second open fails with EBUSY, so one process holds one fd and routes every write and seek through it.

Seek to Effect
0 Clear program data, byte counters, and position counters
1 Clear program data and byte counters
2 Clear position counters

Locking the file (flock LOCK_EX) arms the dead man's switch: if the fd is closed while locked and a program is running, the device performs an emergency stop. The switch is per-holder state: every fresh open starts with it disarmed, shared locks (LOCK_SH) are rejected with EINVAL, and LOCK_UN disarms.

Every close of the device locks the laser latch (Kernel module). The open itself has no rail side effect; the 40 V rail moves only on cnc/enable and cnc/disable writes.

Under ForgeFIRM, forgectrl opens the device once and holds it for its lifetime, and the controllers inherit the fd (GF_PULSE_FD). A controller crash is therefore not a final close, and the supervisor is the dead-man for its writers. That arrangement is on forgectrl.

Run control attributes

These attributes live under /sys/glowforge/cnc.

run

Write, ASCII, 1

Writing "1" switches the device to the "run" state from "idle" and starts executing the loaded program. Every run started this way resets the laser duty to about 100 % (see Power bytes).

stop

Write, ASCII, 1

Writing "1" performs a controlled stop of a running program: the step frequency ramps down to the minimum at ramp_rate and the device switches to the "idle" state, with no steps lost, so the reported position stays accurate (Step engine). For an immediate stop with no deceleration, at the cost of possibly losing steps, see halt.

In the "underrun" state, writing "1" acknowledges the underrun and returns the device to "idle" (position should be re-homed before it is trusted). Otherwise it has no effect unless the device is in the "running" state.

halt

Write, ASCII, 1

Writing "1" performs an immediate stop of a running program: pulse-data processing ends at once with no deceleration, and the device switches to the "idle" state. As with stop, the laser-enable line is released (laser off) and the step lines are driven low, while the stepper motors remain powered. Unlike stop, there is no controlled ramp-down, so if the machine is moving at speed the motors may lose steps (overshoot) and the reported position may no longer be accurate. Use halt for an immediate or emergency stop where stopping promptly matters more than preserving position; use stop for a clean, position-preserving stop. Has no effect unless the device is in the "running" state.

resume

Write, ASCII, -268435455 to 268435455

  • Negative values: laser disabled. Accelerate backward, run the number of specified steps, then decelerate and stop. Refused (EPERM) if the request is longer than max_backtrack: the run is never quietly shortened, because a caller sizes the laser-on lead of its resume to the distance it asked for, and a silent shortfall would put the beam back on ahead of the pause point. A live feed is backtrackable on the same terms as a preloaded program: what bounds the walk is the ring's retained history, not how the ring is filled.
  • Positive values: accelerate forward, run the number of requested steps with the laser held off, re-enable the laser at that byte, and continue the program normally. The hold and the release are the SDMA script's own (a mask it applies to every word it writes and clears at the waypoint byte), so the lead is exact to the byte.
  • Zero: accelerate forward, continue the program without re-enabling the laser.

The step count is a 28-bit waypoint counter; magnitudes at or above 228 are rejected with EINVAL rather than silently truncated.

max_backtrack

Read, ASCII, steps

The longest backward run (see resume) the ring can still play: bytes it has already played, that belong to this program, that are not overwritten, less the steps the controlled deceleration plays out past the waypoint. A pause reads this and sizes both its backtrack and the laser-on lead of the resume that follows from it, so a pause early in a program (or on a ring a live feed has just refilled) shortens the retrace instead of failing it.

The floor is the retained gap: a writer must leave 32 KiB of ring unwritten behind the play head, so 32 KiB of history (3.2 s at the 10 kHz print tick) survives any fill, live-fed or preloaded. Each read performs SDMA channel-0 transactions.

streaming

Read/Write, ASCII, 0-1

Declares how end-of-data is interpreted. A live feeder (one that streams pulse data while the program runs, rather than preloading it) writes "1" before starting a run: running out of data mid-run then transitions the device to the "underrun" state instead of "idle", making buffer starvation distinguishable from normal completion. Write "0" after enqueueing the final bytes of a job so the terminal end-of-data counts as completion. Default is 0 (factory/preload behavior: any end-of-data is a normal stop).

At end-of-data the SDMA script itself forces the laser and step lines low before signaling the host, regardless of this setting.

underruns

Read, ASCII, count

Number of streaming underruns (see streaming) since module load.

free

Read, ASCII, bytes

Free space in the pulse ring (already net of the reserved 32 KiB backtrack gap): the largest write that succeeds at the moment of the read. This is a diagnostic, advisory readback, not the backpressure primitive: a feeder paces by wall clock and takes the write's -ENOMEM return as the back-off signal (see Pacing and backpressure). Each read performs SDMA channel-0 transactions. Do not poll it for pacing.

The feeder contract

Everything a streaming feeder (for example the grblHAL step backend) must obey.

Byte layout

One byte per EPIT tick. If bit 7 is clear, the byte is a step/laser command; if bit 7 is set, the low 7 bits are a laser power level. The bit table and the X, Y, and Z direction conventions are on Step engine.

Fixed byte density

There is no per-byte timing. The engine consumes exactly one byte per timer tick at step_freq. All velocity is expressed as step density across bytes (software DDS/Bresenham resampling of variable-rate segments into a fixed-tick stream). step_freq is immutable while running (-EBUSY); a mid-run speed change is density, not clock.

Effective rate ceiling

The script's per-byte execution time is about 6 µs: above about 165 kHz the EPIT outruns the script and the effective consumption rate saturates (measured on hardware: 164.6 kHz sustained at step_freq=200000). Plan machine ticks at or below 100 kHz; 20 to 50 kHz covers realistic kinematics with a large margin.

Power bytes

A power byte sets the laser PWM duty (7-bit, written raw into PWMSAR; full range at the ~40 kHz carrier). Two rules:

  • Consecutive power bytes are dropped. Only the first of a run of power bytes applies; the rest are consumed without effect (one power change per non-power byte). Interleave a step byte between power changes.
  • Every run started without preserve_power (the plain run attribute) resets the duty to about 100 %. A stream must send its first power byte before its first laser-on byte, or the first pulses fire at full power.

Termination

End every stream with laser-off bytes (bit 4 clear). The script forces the laser and step lines low at end-of-data as a hardware backstop, but the stream must not rely on it: it is the underrun safety net.

Streaming protocol

Write streaming=1 before a live-fed run. End-of-data mid-run then lands in the underrun state (position no longer trusted; re-home) instead of "idle", and new runs are refused until acknowledged via stop. Write streaming=0 after enqueueing the final bytes of a job so its terminal end-of-data counts as completion.

Pacing and backpressure

Writes either commit fully or fail -ENOMEM (no partials; 32 KiB of ring is reserved as a backtrack gap). Do not poll free for pacing: every read costs two channel-0 SDMA transactions. Pace by wall clock (enqueued_target = elapsed * step_freq + queue_depth) and treat -ENOMEM as "back off". Keep the queue depth bounded (50 to 200 ms) so feed and power overrides take effect promptly. The ring (default 32 MiB, the ring_mb module parameter) holds many minutes of stream, so depth is a latency choice, not a capacity one.

A whole-file preloader (cloud mode) buffers as much of the job as the ring holds, about 1 MiB per 100 s of 10 kHz stream, so about 56 min at the 32 MiB default, and appends the rest as the ring drains.

Measured reference (i.MX6 Solo, CONFIG_PREEMPT, SCHED_FIFO feeder, full CPU and I/O load): a 150 ms depth at 100 kHz ran 2 minutes with 0.2 ms worst write latency and zero underruns.

Do not write while running backward

A write during a backtrack would clobber the backtrack dead-stop. Such writes are rejected with -EBUSY.

Pausing a live feed

The 32 KiB the writer keeps clear is retained history, so a live-fed program can back up and resume exactly like a preloaded one. Read max_backtrack for the distance available, ask for no more than that (a longer request is refused, not shortened), and lead the laser back on over the ground the backward run retraced. Topping the ring up between the read and the resume write can shorten the answer, so treat an -EPERM there as "read it again or hold where you are", not as a fault.

Hold the fd open and flock'd for the whole job (dead-man armed). Prefill one queue depth. Write run. Top up on a 10 to 20 ms cadence by wall clock. On underrun, raise a controller alarm, re-home, acknowledge via stop, seek-clear, and regenerate. For feed hold or jog cancel use stop (controlled deceleration) or halt plus seek-clear and regenerate.

Progress counters under a live feed

The position attribute's two byte counters diverge past 4 GiB under a long live stream, because one wraps and the other saturates. A feeder that compares them for progress must track the wrap itself, or use its own count of bytes written. The layout and the reason are on Kernel module.

Fast beam stop on a feeder stall

The fast beam-stop path on a feeder stall is the ring-drain chain: the ring runs dry, the SDMA script forces the FIRE and step lines low in the same tick, the driver leaves the running state, the charge pump self-terminates on its next 200 ms tick, and the HV watchdog disarms the chain. Cloud mode preloads a job that fits the ring, so that ring does not drain on a feeder stall (a job longer than the ring is live-fed past it, and can); that residual is covered by the cooling engine's hung-controller dead-man (forgectrl).