ELS register map (protocolVersion 10)¶
elsStop_t occupies 142 registers at 104..245 of rampsSharedData_t.
hot — 56 registers (elsStop 0..55, absolute 104..159)¶
Read as ONE FC3 request every board tick (30 Hz) by Board._refresh_els_stop_snapshot. The quantity being minimised is REQUESTS, not bytes -- each request is an independent chance for the firmware to miss its answering window while the motion ISR is saturated (2026-08-23: six of six cuts lost comms, every drop a timeout). That measurement was taken at the old 100 kHz tick; the ISR has run at 50 kHz since 2026-08-28, which relieves the pressure without changing the argument.
| reg | abs | type | field | access | notes |
|---|---|---|---|---|---|
| 0 | 104 | uint16 |
enable |
sw_write | SW write: 1 = enable ELS stop feature |
| 1 | 105 | uint16 |
scaleIndex |
sw_write | SW write: which scale (0–3) is the position reference (Z axis) |
| 2 | 106 | int32 |
stopPosition |
sw_write | SW write: threshold in encoder counts |
| 4 | 108 | int16 |
stopDirection |
sw_write | SW write: 1 = stop when pos >= threshold, -1 = stop when pos <= threshold |
| 5 | 109 | uint16 |
active |
bidirectional | bidirectional: firmware sets to 1 when triggered; SW writes 0 to resume |
| 6 | 110 | float |
threadPitchSteps |
sw_write | SW write: leadscrew steps per thread pitch (float); 0.0f = turning (no correction) |
| 8 | 112 | int32 |
hysteresis |
sw_write | SW write: encoder counts carriage must retract before re-enabling; 0 = no hysteresis |
| 10 | 114 | float |
zCountsPerPitch |
sw_write | SW write: Z scale encoder counts per thread pitch; 0.0f = correction disabled |
| 12 | 116 | uint32 |
backlashSteps |
sw_write | SW write: leadscrew backlash takeup magnitude in servo steps; direction derived from sign(syncRatioNum) × sign(threadPitchSteps × zCountsPerPitch); 0 = takeup disabled |
| 14 | 118 | int32 |
latchedZ |
ro_firmware | READ-ONLY (firmware-owned): scales[scaleIndex].position at first trigger of the job |
| 16 | 120 | int32 |
latchedSpindle |
ro_firmware | READ-ONLY (firmware-owned): scales[0].position at first trigger of the job |
| 18 | 122 | uint16 |
referenceLatched |
ro_firmware | READ-ONLY (firmware-owned): 0 until first trigger captures the reference, 1 thereafter; reset on enable 0→1 |
| 19 | 123 | uint16 |
takeupPending |
ro_firmware | READ-ONLY (firmware-owned): 1 while the backlash take-up that starts EVERY pass (first pass and turning included since 2026-08-21) is executing or awaiting Z confirmation; gates sync off meanwhile |
| 20 | 124 | float |
lastIdealAdvance |
ro_firmware | READ-ONLY (firmware-owned): last resume's deltaSpindle × syncRatioNum / syncRatioDen |
| 22 | 126 | float |
lastActualAdvance |
ro_firmware | READ-ONLY (firmware-owned): last resume's deltaZ × threadPitchSteps / zCountsPerPitch |
| 24 | 128 | float |
lastPhaseError |
ro_firmware | READ-ONLY (firmware-owned): last resume's idealAdvance − actualAdvance (pre-modulo) |
| 26 | 130 | float |
lastCorrection |
ro_firmware | READ-ONLY (firmware-owned): last resume's correction added to stepsToGo (post-modulo) |
| 28 | 132 | uint16 |
protocolVersion |
ro_firmware | READ-ONLY (firmware-owned): register-layout version, starts at 1. Bump whenever this struct changes; reflex-ui checks it at connect so a map mismatch names itself instead of surfacing as garbled reads |
| 29 | 133 | uint16 |
takeupSeq |
ro_firmware | seq — monotonic ack READ-ONLY (firmware-owned): increments once per take-up outcome; lets SW tell completed-normally from host-cleared, which takeupPending alone cannot |
| 30 | 134 | uint16 |
takeupResult |
ro_firmware | READ-ONLY (firmware-owned): outcome of the last take-up. ELS_CAL_/ELS_TAKEUP_ in els_backlash_cal.h; 0 = OK. Replaces a binary fault flag so "carriage never moved" and "never reached target" stay distinguishable |
| 31 | 135 | — | (alignment pad) | — | before lastTakeupZDelta |
| 32 | 136 | int32 |
lastTakeupZDelta |
ro_firmware | READ-ONLY (firmware-owned): signed Z counts moved across the last take-up, projected onto the take-up direction. NEGATIVE means the carriage moved the WRONG way — a distinct fault signature from "didn't move" |
| 34 | 138 | int32 |
takeupThreshCounts |
sw_write | READ-ONLY (firmware-DERIVED, not operator-set): Z counts the last take-up had to move to be confirmed. Derived from (backlashSteps - mean(calMeasured)) via elsTakeupConfirmThreshold(); falls back to calMotionThreshCounts with no calibration on file or in turning mode. Published so the UI can say "moved 3, needed 4" instead of just refusing |
| 36 | 140 | uint32 |
stepPulseMinCycles |
ro_firmware | READ-ONLY except for reset: narrowest STEP pulse since host wrote 0. 0 = nothing measured yet |
| 38 | 142 | uint32 |
stepPulseRuntCount |
ro_firmware | READ-ONLY except for reset: pulses narrower than ELS_STEP_RUNT_CYCLES since host wrote 0 |
| 40 | 144 | uint16 |
diagSeq |
ro_firmware | seq — monotonic ack READ-ONLY (firmware-owned): increments once per COMPLETED capture. Edge-detect this; there is deliberately no "capture in progress" register to poll. ORDERING INVARIANT: must stay at a LOWER address than the capture payload it counts -- see the calSeq comment above for why a reorder reintroduces the torn-read bug |
| 41 | 145 | uint16 |
machineMode |
ro_firmware | READ-ONLY (firmware-owned): ELS_MMODE_* (els_machine_mode.h), republished every servoEnableTask tick in every build |
| 42 | 146 | uint16 |
latchSeq |
ro_firmware | seq — monotonic ack READ-ONLY (firmware-owned): increments once per ACCEPTED manual latch. Monotonic; the ack for latchCommand |
| 43 | 147 | uint16 |
phaseOffsetSeq |
ro_firmware | seq — monotonic ack READ-ONLY (firmware-owned): increments once per ACCEPTED apply. Monotonic; the ack for phaseOffsetCommand |
| 44 | 148 | int32 |
phaseOffsetSteps |
ro_firmware | READ-ONLY (firmware-owned): the live cumulative total in leadscrew steps, applied at every phase correction. Cleared on the enable 0->1 edge that clears referenceLatched -- an offset is meaningless without the datum it offsets -- and survives per-pass stop/resume within a job |
| 46 | 150 | uint16 |
stopTriggerSeq |
ro_firmware | seq — monotonic ack READ-ONLY (firmware-owned): increments once per stop trigger, immediately BEFORE the payload below. Monotonic; edge-detect it, and re-read on no edge |
| 47 | 151 | — | (alignment pad) | — | before stopTriggerZ |
| 48 | 152 | int32 |
stopTriggerZ |
ro_firmware | READ-ONLY (firmware-owned): scales[scaleIndex].position at the trigger, and it is the SAME value the threshold comparison was made on, not a re-read. (settled Z) - this = the coast. NOTE it is the reference scale as of the PREVIOUS tick when scaleIndex is above the sync-enabled scale's index -- the trigger test runs inside that scale's loop iteration, before this one's position is updated. 10 us of lag, inherent to the DECISION rather than to this register (latchedZ has always had it), and the right endpoint precisely because overshoot is measured from where the firmware decided to stop |
| 50 | 154 | int32 |
stopTriggerZSpeed |
ro_firmware | READ-ONLY (firmware-owned): scales[scaleIndex].speed at the trigger, encoder counts/s -- the same register and units the DRO shows. The table's x-axis. Computed by updateSpeedTask over the 50 ms window ENDING BEFORE the trigger, so unlike a host estimate it cannot straddle the coast; it is up to 50 ms old, which on a constant-feed threading pass is the steady approach speed and is exactly what is wanted |
| 52 | 156 | int32 |
stopTriggerStepsToGo |
ro_firmware | READ-ONLY (firmware-owned): servo.stepsToGo at the trigger. NONZERO means the firmware was still commanding motion, so the overshoot is not purely mechanical coast -- the 2026-08-28 stop-overshoot captures found zero emitted steps after the trigger in 12 of 14 passes, and this makes that check automatic per pass instead of a one-off probe build |
| 54 | 158 | int32 |
stopTriggerSpindleSpeed |
ro_firmware | READ-ONLY (firmware-owned): scales[0].speed at the trigger, counts/s. Context, and enough for a host to reconstruct the commanded feed of a threading pass from the sync ratio |
cold — 86 registers (elsStop 56..141, absolute 160..245)¶
Never read on the tick path. Fetched on demand -- at connect, after a calibration run, or when a diagnostic build is being read out.
| reg | abs | type | field | access | notes |
|---|---|---|---|---|---|
| 56 | 160 | uint16 |
calCommand |
bidirectional | command — cleared on consume, poll calSeq bidirectional: SW writes 1 to request a calibration run; FIRMWARE CLEARS IT on consume. This is the atomic hand-off. SW must NOT poll it for completion — it clears the instant the ISR picks it up, long before the run finishes. Edge-detect calSeq instead |
| 57 | 161 | uint16 |
calSeq |
ro_firmware | seq — monotonic ack READ-ONLY (firmware-owned): increments once per finished run, success OR failure. Monotonic, so a host polling at Modbus rates cannot alias a fast run |
| 58 | 162 | uint16 |
calResult |
ro_firmware | READ-ONLY (firmware-owned): outcome of the run counted by calSeq. ELS_CAL_* in els_backlash_cal.h; 0 = OK |
| 59 | 163 | uint16 |
latchCommand |
bidirectional | command — cleared on consume, poll latchSeq bidirectional: SW writes 1 to request a manual reference latch; FIRMWARE CLEARS IT on consume. Consumed ONLY while enable == 1 (a reference is meaningless outside a job and would be wiped by the next enable 0->1 anyway); when enable == 0 it is cleared with NO latchSeq increment, so an absent ack IS the refusal. SW must edge-detect latchSeq, never poll this |
| 60 | 164 | uint16 |
phaseOffsetCommand |
bidirectional | command — cleared on consume, poll phaseOffsetSeq bidirectional: SW writes 1 to apply phaseOffsetPending as the new total; FIRMWARE CLEARS IT on consume. Not a completion flag -- edge-detect phaseOffsetSeq |
| 61 | 165 | — | (alignment pad) | — | before phaseOffsetPending |
| 62 | 166 | int32 |
phaseOffsetPending |
sw_write | host-written candidate total, leadscrew steps. Read by the ISR ONLY under a nonzero phaseOffsetCommand; write it BEFORE the command, never after |
| 64 | 168 | uint16 |
bootCommand |
bidirectional | command — cleared on consume, poll bootSeq bidirectional: SW writes ELS_BOOT_CMD_* (els_identity.h); FIRMWARE CLEARS IT on consume. 1 = reboot into the bootloader and stay resident, 2 = plain reboot. Refused (cleared, no ack) while enable != 0 |
| 65 | 169 | uint16 |
bootSeq |
ro_firmware | seq — monotonic ack READ-ONLY (firmware-owned): increments once per ACCEPTED boot command, immediately before the reset it triggers |
| 66 | 170 | int32[3] |
calMeasured |
ro_firmware | ⚠ group assignment UNVERIFIED READ-ONLY (firmware-owned): lash measured at each of the 3 reversals, in servo steps. The HOST judges whether the spread is acceptable — measurement lives here, policy lives in the UI |
| 72 | 176 | int32 |
calCeilingSteps |
sw_write | SW write: per-leg hard ceiling in servo steps. Driving this far without Z moving IS the open-half-nut / uncoupled failure. MACHINE-SPECIFIC; size it comfortably past the largest credible lash |
| 74 | 178 | int32 |
calMotionThreshCounts |
sw_write | SW write: Z scale counts that count as real motion. MACHINE-SPECIFIC — ~2 counts on elspi (200 counts/mm, so 1 count ≈ 2.5 servo steps); emulator is 400 counts/mm. 0 disables detection and FAILS CLOSED (never confirms) — deliberate: an unconfigured threshold must refuse, not wave everything through |
| 76 | 180 | uint32 |
executionCyclesPeak |
ro_firmware | ⚠ group assignment UNVERIFIED READ-ONLY except for reset: highest executionCycles since the host last wrote 0 here. Compare against 1000 (the per-tick budget at 100 MHz / 10 us) |
| 78 | 182 | uint16 |
diagSchema |
ro_firmware | READ-ONLY (firmware-owned): identifies the probe compiled into the block. 0 = none; do NOT interpret anything below it. Never assume a schema you did not read |
| 79 | 183 | uint16 |
diagBucketTicks |
ro_firmware | READ-ONLY (firmware-owned): ISR ticks summed into each diagTrace bucket. PUBLISHED so the host never has to assume the ISR rate — the repo has disagreed with itself about that rate by 10x |
| 80 | 184 | uint16 |
diagBucketCount |
ro_firmware | READ-ONLY (firmware-owned): populated diagTrace entries, for the same reason |
| 81 | 185 | — | (alignment pad) | — | before diagSettleTicks |
| 82 | 186 | int32 |
diagSettleTicks |
ro_firmware | READ-ONLY (firmware-owned): ticks from capture start to the LAST tick that saw nonzero dZ. THE measurement ELS_SLIP_SETTLE_TICKS is a guess at — meaningful in v2, where the capture stops before the pass starts |
| 84 | 188 | int32 |
diagNetCounts |
ro_firmware | READ-ONLY (firmware-owned): signed Z counts summed across the capture |
| 86 | 190 | int16[50] |
diagTrace |
ro_firmware | READ-ONLY (firmware-owned): per-bucket SIGNED sum of dZ. Signed rather than magnitude on purpose — encoder dither cancels, real motion does not, which is exactly the distinction a quiescence test needs and the reason to prefer net displacement over summed |
| 136 | 240 | uint16 |
diagCaptureTicks |
ro_firmware | READ-ONLY (firmware-owned): ticks the capture actually ran, i.e. how long the servo stayed silent after the take-up. Distinct from diagSettleTicks, which is when Z last MOVED |
| 137 | 241 | uint16 |
diagEndReason |
ro_firmware | READ-ONLY (firmware-owned): ELS_DIAG_END_*. A window-full capture did not finish measuring; treat its tail as a floor, not a result |
| 138 | 242 | uint16[4] |
diagReserved |
ro_firmware | reserved pads the block to a fixed 128 bytes so its size never depends on which probe is in it |