“Much of this information is now outdated and has never been reviewed for accuracy.”
docs/src/code/code-notes.adoc — “Intended audience”, opening section
The document warns you itself. This audit takes the warning at its word and puts the
Code Notes against the source of the master branch — both block diagrams, the
motion command list, the libnml and CMS chapters, the tool table, the documented
semantics of every surviving command, handler by handler in command.c — and,
beyond the Code Notes, a sampled audit of the G-code reference against the interpreter.
Twenty-seven corrections. Two commands declared “always accepted” can in fact be rejected.
A defect described as “currently broken” has been fixed without the text moving — twice. Three
documented commands still have no handler at all, and nothing even emits them. The G-code
reference documents two errors that are implemented nowhere. What holds up is listed as
such — sixteen points, including one whole chapter. Fixes for the actionable errata are
prepared as three patches (see upstream/ beside this file).
Every finding here is against master, which is unreleased. The newest tag in the
repository is v2.9.10. Most of the errata hold for 2.9 as well — HAL, the
emcmot segment, the queues, the servo cycle, joints versus axes — but
erratum 3 does not.
The commit that moved all I/O handling out of the iocontrol process and into
milltask is 764655eb4d (2023-05-16). git tag --contains
returns nothing for it and git branch -r --contains returns only
origin/master. If you run 2.9.x — the current stable — iocontrol is
still a separate process on your machine. Ask which version you are on before acting on that one.
A second date worth keeping straight: iotaskintf.cc was dropped from the task
Submakefile back on 2011-08-03 (d56fdbfcbb) — twelve years before
the I/O handling itself moved. Two separate events, easily conflated.
Topology and captions follow the original. Blocks struck through or ringed in red carry a numbered marker keyed to the errata tables below.
| # | What the diagram claims | What master says | Evidence |
|---|---|---|---|
| 1 | “NML?” between EMCTASK and shared memoryThe task→motion link would be an NML channel with its CMD/STAT/ERR triplet. | It is not NML. The task writes straight into an RTAPI shmem segment opened by motmod with key 100, through usrmotWriteEmcmotCommand(). And there is exactly one NML triplet in the whole system — GUI to task — not three. |
motion.c:44 usrmotintf.h:67 linuxcnc.nml |
| 2 | “FIFOS?” between shared memory and EMCMOTThe crossing into real time would be done with queues. | No. It is a single slot: emcmot_command_t command, guarded by command_mutex, with a commandNum counter whose echo the task waits for in commandNumEcho. One command at a time, 1 s timeout. The only ring in that segment is emcmot_error_t — 32 × 1024 B, lock-free MPSC. |
motion_struct.h:20-21 motion.h:211, 584 motion.h:746 |
| 3 | EMCIO, a fourth processA separate discrete I/O controller with its own NML channels to EMCTASK. | The process is gone. The task Submakefile carries # disabled: emc/task/iotaskintf.cc, and the 14 iocontrol.0.* pins are created by milltask itself. The iocontrol name is kept so existing .hal files keep working. |
task/Submakefile:13 taskclass.cc:41,133 |
| 4 | HAL appears nowhereThe motion controller would talk to hardware directly. | Structural omission, and the heaviest one: the diagram predates HAL. Today all coupling goes through a 2 MiB shared block (key 0x48414C32) holding components, pins, signals, functions and threads. The motion controller does nothing but write to pins. |
hal_priv.h:120-122 motion.c:518+ |
| 5 | PID SERVO, D/A CONVERTER, ENCODER COUNTER, LIMIT SWITCHES inside EMCMOTServo loop and I/O would be internal to the motion controller. | All moved out. They are independent HAL components, wired in a .hal file by the integrator. The comment in control.c is unambiguous: the final motor position goes “to the HAL (which routes it to the PID”. Nothing in src/emc/motion/ computes a PID. |
control.c:180 hal/components/pid.c |
| 6 | “AXIS 1 … AXIS N”One controller per axis, axis and motor conflated. | These are joints, not axes. Since the joints/axes rework a joint is a motor and an axis is a Cartesian coordinate: EMCMOT_MAX_JOINTS 16 against EMCMOT_MAX_AXIS 9. On a trivkins machine they coincide; on a delta or a hexapod they do not. |
emcmotcfg.h:25,31 |
| 7 | SPINDLE CONTROLLER inside EMCIOThe spindle would be driven outside real time. | The spindle moved into real time. motmod exports spindle.N.on, speed-out, at-speed, index-enable, orient… for 8 spindles. It had to: threading and rigid tapping need servo-rate synchronisation. |
motion.c:709-734 emcmotcfg.h:33 |
| 8 | Planner, kinematics and homing as fixed blocksThree functions welded into EMCMOT. | They are separately loaded modules: tpmod, homemod, and one kinematics module out of 19. Each can be replaced by a custom implementation without recompiling the rest. In master, cruckig — a 34-file pure-C port of Ruckig — brings a finite-jerk planner. |
tp/tpmod.c motion/homemod.c tp/cruckig/ |
| 9 | The “NON-REALTIME / REALTIME” lineImplies a kernel boundary. | By default it is not one any more. With the uspace flavour on a PREEMPT_RT kernel, “real-time” components are SCHED_FIFO user-space threads hosted by rtapi_app. The boundary has become a scheduling boundary. Kernel-module mode survives only for RTAI. |
rtapi/uspace_posix.cc rtapi/rtai_rtapi.c |
| 10 | No queue shown anywhereThe diagram writes “FIFOS?” where there are none, and nothing where there are. | Both real queues are elsewhere and absent from the drawing: interp_list, the std::deque that absorbs how far the interpreter runs ahead of the machine; and TC_QUEUE, the ring of 2000 segments (≈ 1 MB) that gives the planner the lookahead it needs to blend corners. |
nml_intf/interpl.hh:46 tp/tcq.h emcmotcfg.h:70 |
Deliberately laid out like Sheet A so the comparison can be made block by block. Amber marks what was corrected or added; blue marks what the original already had right.
The document carries a second diagram, of a single joint controller. It ages far better than the first: all ten pins it shows still exist, at the same points in the chain. Two corrections only.
| # | What the document claims | What master says | Evidence |
|---|---|---|---|
| 11 | Pin “index-pulse-in”A plain pulse input on the joint controller. | It is called joint.N.index-enable and it is not an input: it is a bidirectional HAL_IO pin, hence a handshake — the requester sets it, the encoder driver clears it when the index is seen. It is moreover created by homemod, not motmod — a direct consequence of erratum 8. |
homing.c:254-255 homing.c:113,537 |
| 12 | The joint controller ignores axesOnly joint quantities flow. | The whole axis.L.* family is missing: axis.x.pos-cmd, axis.x.teleop-vel-cmd and the external offsets. It carries Cartesian jogging and plasma torch height control, which act in parallel with the chain drawn. |
motion.c · axis.c emcmotcfg.h:31 |
| 13 | “This section simply lists all of the commands”27 documented commands, presented as the inventory. | The cmd_code_t enumeration holds 76. Seven documented names no longer exist, and 57 commands were never documented — including whole features: reverse run, rigid tapping, finite jerk, external offsets, spindle orient, multi-spindle. |
motion.h · cmd_code_t code-notes.adoc:227-740 |
| 14 | “LinuxCNC manages tool information in a tool table file”A single text file. | Storage was rebuilt: three back-ends coexist in src/emc/tooldata/ — mmap (readers map the table read-only), an external database driven by [EMCIO]DB_PROGRAM, and NML. A third-party program can therefore own the tool data. None of this appears in the chapter. |
tooldata_mmap.cc tooldata_db.cc taskclass.cc:147 |
| 15 | “type — SHMEM, LOCMEM, FILEMEM, PHANTOM, or GLOBMEM”Five buffer types announced as valid in the NML config file. | Two of the five do not exist. FILEMEM and GLOBMEM are recognised nowhere — GLOBMEM survives only in a comment on the buffer_type field declaration. Only three types actually construct an object: PHANTOM (:729), SHMEM (:819), LOCMEM (:849). A fourth string, RTLMEM (:844), is recognised solely in order to be refused — rcs_print_error("RTLMEM not supported."). |
cms_cfg.cc libnml/buffer/ |
The widest gap in the document. The “Commands” chapter runs five hundred lines and presents itself as the inventory of the motion controller's commands. It covers a quarter of them.
Seven documented names match no symbol in the code:
The first two are gone outright: amp enabling became the joint.N.amp-enable-out pin. The
others were renamed — EMCMOT_JOINT_ACTIVATE, EMCMOT_JOINT_DEACTIVATE,
EMCMOT_JOINT_HOME, EMCMOT_CLEAR_PROBE_FLAGS in the plural — and
SCALE was split into FEED_SCALE, RAPID_SCALE and
SPINDLE_SCALE.
Among the 57 missing, these are not details — they are whole machine features.
EMCMOT_REVERSE, EMCMOT_FORWARD — replaying a program backwards.EMCMOT_SET_JERK, SET_JOINT_JERK_LIMIT, SET_AXIS_JERK_LIMIT.EMCMOT_SET_PLANNER_TYPE, SETUP_ARC_BLENDS.EMCMOT_RIGID_TAP, SET_SPINDLESYNC.EMCMOT_SET_AXIS_* family — limits, velocity, acceleration, locking joint.SPINDLE_ORIENT, SET_NUM_SPINDLES, SET_SPINDLE_PARAMS, brakes.EMCMOT_SET_OFFSET — plasma torch height control rests on it.JOG_ABORT, JOINT_UNHOME, SET_PROBE_ERR_INHIBIT, SET_DOUT, SET_AOUT.The libnml and CMS chapters. Every class described still exists at the stated path:
linklist.hh, shmem.hh, memsem.hh, timer.hh,
cms.hh, nml.hh. The description of the config file format, the
B and P lines, the update() mechanism and the NML base class
is accurate. This is the best-aged part of the document — unsurprisingly: libnml is inherited
NIST code nobody rewrites.
Two of the five buffer types do not exist — FILEMEM and GLOBMEM
(erratum 15). Only PHANTOM, SHMEM and LOCMEM construct
anything; RTLMEM is recognised only to be refused. In fairness the document says a few
lines later that “SHMEM [is] the only buffer type to use” — the error is signalled without
being corrected.
The toolchanger model. The random / nonrandom distinction, pocket 0 as the spindle, the special meaning of T0 under nonrandom, copy versus swap: all still accurate. It is the storage that changed, not the model (erratum 14).
Three times in this audit a name survived what it designated. Pins are still called
iocontrol.0.* though the iocontrol process is gone. The INI section holding
RANDOM_TOOLCHANGER, TOOL_TABLE and DB_PROGRAM is still
[EMCIO], named after a component that no longer exists.
This is not negligence, it is compatibility: thousands of user-written .hal and
.ini files keep working. But it explains why documentation ages so badly here:
the names stay right while the structure they describe has been dismantled. A reader trusting
the names will conclude iocontrol is a process. It has not been one for a long time.
Knowing that a name exists says nothing about behaviour. Here are the nineteen surviving commands, each against its handler. Four are contradicted, three have no handler, four are not described at all — and eight hold up.
| Command | Verdict | What the audit finds | Evidence |
|---|---|---|---|
| ABORT | incomplete | The document writes “I don't know exactly what kind of stop results” for teleop mode. The code answers: axis_jog_abort_all(0). It also adds a spindle at-speed diagnostic the text ignores. | command.c:492 |
| FREE | accurate | The deferred flag switch is described correctly: the handler only sets coordinating = 0 and teleoperating = 0. | command.c:583 |
| TELEOP | moved | The handler is now one call: switch_to_teleop_mode(). The described conditions no longer live in the handler; they migrated into that function. | command.c:618 |
| COORD | accurate | Same deferred mechanism as FREE. Description conforms. | command.c |
| ENABLE | contradicted | “Requirements: None” is false. Rejected if the motion.enable pin is low. See erratum 16. | command.c:1366 |
| DISABLE | accurate | Always accepted, effect deferred to the controller cycle. Conforms. | command.c |
| ENABLE_WATCHDOG | dead | No case in command.c. The document gives it a full section. Erratum 20. | command.c |
| DISABLE_WATCHDOG | dead | Same. The document admits “Currently nothing”, which stays true — but it is a phantom command. | command.c |
| PAUSE | incomplete | The handler matches (tpPause() then paused = 1), but following the flag into the planner answers the document's own open question — and finds an omission that matters. Erratum 25. | command.c:1230 tp.c:243-252 |
| RESUME | accurate | stepping = 0, tpResume(), paused = 0. Conforms. | command.c:1252 |
| STEP | contradicted | “Requirements: None” is false. Rejected with an error unless the planner is already paused. Erratum 17. | command.c:1261 |
| OVERRIDE_LIMITS | contradicted | The defect documented as “currently broken” has been fixed. Erratum 19. | command.c:702 |
| JOG_CONT | contradicted | No longer free-mode only, and five new rejection conditions. Erratum 18. | command.c:796 |
| JOG_INCR | contradicted | Same handler, same gaps. | command.c |
| JOG_ABS | contradicted | Same handler, same gaps. | command.c |
| SET_LINE | undocumented | The document says “(More later)”. Nothing to audit — there is no description. | — |
| SET_CIRCLE | undocumented | “(More later)”. | — |
| PROBE | undocumented | “(More later)” — for an entire probing feature. | — |
| SET_TELEOP_VECTOR | dead | “(More later)”, and no handler either. Erratum 20. | command.c |
“None. The command can be issued at any time, and will always be accepted.”
The command is refused when the hardware input is low. An operator who has not armed the enable chain gets a rejection, not an acceptance.
“None. The command can be issued at any time, and will always be accepted.” and “The trajectory planner resumes, and later pauses”.
STEP only acts on an already paused planner. Otherwise, an explicit error. That is the opposite of a command that is always accepted.
| # | What the document claims | What master says | Evidence |
|---|---|---|---|
| 16 | ENABLE — “Requirements: None”“The command can be issued at any time, and will always be accepted.” | There is a condition. ENABLE is rejected with an error message if the motion.enable HAL pin is low — a hardware condition: the enable chain must be armed. The clause “if forward kinematics are not available, switch to free mode” is still accurate — that is the KINEMATICS_INVERSE_ONLY test. |
command.c:1366 |
| 17 | STEP — “Requirements: None”“Always accepted. The planner resumes, and later pauses.” | The command only acts when emcmotStatus->paused is true. Otherwise it is refused with “can't STEP while already executing”. The document describes exactly half the behaviour. |
command.c:1261 |
| 18 | JOG_CONT / JOG_INCR / JOG_ABS“Rejected if the machine is not in free mode, or if any joint is in motion. The joint is identified by emcmotCommand->axis.” |
Three gaps. Jogging is no longer free-mode only: the handler tests !GET_MOTION_TELEOP_FLAG() and also serves the teleop case, where jogging is Cartesian. The field is joint, not axis — axis now means a Cartesian coordinate (erratum 6). And five rejection conditions unknown to the document were added: the jog-inhibit pin, homing in progress, a jogwheel already active on that joint, a locking joint needing unlock, and jogging further onto a limit — the last no longer silent but raising SET_JOINT_ERROR_FLAG. |
command.c:796-840 |
| 19 | OVERRIDE_LIMITS — “This is currently broken”“Once an OVERRIDE_LIMITS command is received, limits are ignored until another OVERRIDE_LIMITS re-enables them.” And: “Limits on all joints are over-ridden.” | The defect was fixed; the document still describes the bug. The code comment now says the opposite: “they are automatically re-enabled at the end of the next jog” — precisely the behaviour the document presented as unrealised intent. Second gap: only limits actually tripped are overridden, through a mask built from GET_JOINT_NHL_FLAG and GET_JOINT_PHL_FLAG — not “all joints”. |
command.c:702-730 |
| 20 | Three commands described as operationalSET_TELEOP_VECTOR “instructs the motion controller to move along a specific vector”, plus ENABLE_WATCHDOG and DISABLE_WATCHDOG, each with its Requirements/Results section. |
All three have no case in command.c. Issuing them does nothing. For the watchdogs the document half-admits it (“Currently nothing”); for SET_TELEOP_VECTOR it does not — that one is presented as a motion command. |
motion.h vs command.c |
| 21 | Internal note dated 6/5/2020“The cmd_code_t enumeration contains 73 commands, but the switch statement in command.c contemplates only 70.” | The document therefore already contains a self-audit — itself now stale. The figures today are 76 and 73. The gap of three is unchanged and covers exactly the same three commands as in 2020: nothing was fixed in six years, neither the code nor the note. Adversarial re-check: a whole-tree search shows the three appear only in motion.h and motion-logger.c — nothing even emits them. |
code-notes.adoc:242-246 motion.h · command.c |
| 22 | “For an example of such a bug, see the treatment of axes in src/emc/ini/initraj.cc:loadTraj()”Offered as a live example of a joints/axes bug. | Fixed. The code now reads: “originally, this code would only set axes X, Y and Z … Now all axes are set”. The second instance of the document preserving a bug report past its repair (the first was OVERRIDE_LIMITS, erratum 19) — at two, it is a characteristic, not an accident: the document records defects and never their repair. | initraj.cc:203-205 |
| 23 | The EMCIO chapter versus its own diagramThe block diagram shows EMCIO as a fourth process; the prose opens “The I/O Controller is part of TASK” — then describes an “iocontrol main loop process” anyway. | The document contradicts itself, independently of which version matches the code. The first line was updated for the merge into task; the rest of the chapter still describes the dead separate process. | code-notes.adoc:754-767 |
| 24 | TELEOP requires all joints homedStated unconditionally. | True only when kinType != KINEMATICS_IDENTITY. On a trivkins machine, teleop needs no homing at all. Found by following the handler's delegate, switch_to_teleop_mode(). |
motion.c:173-178 |
| 25 | PAUSE — “I don't know if it pauses all motion immediately, or if it completes the current move”The document's own open question, plus “Requirements: None”. | Neither. The machine decelerates to a stop mid-segment at that segment's accel/jerk limits. And the omission that matters: pause is silently ignored while the segment is position-synchronized to the spindle — threading (G33) and rigid tapping — because tpGetFeedScale() forces 1.0 there, bypassing pause and feed override. Pressing pause during a G33 does nothing until the synchronized move ends. Nothing in the chapter says so. |
tp.c:243-252 tp.c:2782-2787 |
command.c holds 76 case EMCMOT_* labels for 73 distinct values: the
three jog commands appear twice, in two different switch statements — one for handling,
the other only for an error message. Against the 76 enumeration values, three commands are left with
no handler.
The same method pointed at the documentation users actually read: docs/src/gcode/g-code.adoc
(2 786 lines) against src/emc/rs274ngc/. A sampled audit — five families tied to spindle
synchronization and path control — and a finding of a different kind: the reference documents
errors that are implemented nowhere.
| # | What the reference claims | What the interpreter says | Evidence |
|---|---|---|---|
| 26 | G33 / G33.1 — “It is an error if … The requested linear motion exceeds machine velocity limits due to the spindle speed” | No such check exists — not in the interpreter, not in task, not in motion. Triple-confirmed through independent oracles: the NCE_* error catalog, every _() string in the interpreter, and all 26 .po translation catalogs — the message has never existed in any translated release. Meanwhile two real errors were missing from the documented list: K word required (NCE_K_WORD_MISSING_WITH_G33) and F word forbidden (NCE_F_WORD_USED_WITH_G33). Also stated now: “spindle not turning” tests the commanded M3/M4 state, not measured rotation. |
interp_check.cc:375-378 interp_convert.cc:5496-5529 |
| 27 | G96 — “It is an error if … A feed move is specified in G96 mode while the spindle is not turning” | No such check exists anywhere. The spindle-not-turning checks apply only to G33/G33.1/G76 and the tapping cycles. A plain G1 in CSS mode with a stopped spindle is accepted without a word. Documented in passing: without D, CSS mode applies no RPM limit — SET_SPINDLE_MODE(s, 1e30). |
interp_cycles.cc:270 interp_convert.cc:5087 |
Both phantom errors describe physically sensible constraints — things a reader would believe a CNC ought to check. That is exactly why they survived: they are plausible. This documentation does not only rot by aging; it also contains invented safety checks that were perhaps once planned and never implemented.
interp_convert.cc:5534 — G76's $-validity check reports
“Invalid D-number in G76 cycle”, the wrong word name. command.c:1475 and
:1553 — PROBE and RIGID_TAP carry the copy-pasted comment “requires … enable
off” while the test three lines below requires enable on. command.c:1966 —
SET_AXIS_LOCKING_JOINT's debug message prints a name matching no command.
Errata 15–17, 19, 21–23 and 25–27 are fixed in three patches on the audit-fixes
branch of the local clone, exported to upstream/ with a README mapping each patch to its
errata — all three verified to apply cleanly on pristine master. The complete 76-command inventory the
Code Notes never had is written out in motion-commands-reference.md. Submission upstream
is the repository owner's call.
An audit that only found faults would be dishonest. Sixteen points hold, including one whole chapter and one I expected to be obsolete.
joint->cubic, cubicDrain(). It bridges the trajectory rate and the servo rate.pos-lim-sw-in, neg-lim-sw-in, home-sw-in, amp-enable-out, amp-fault-in, motor-pos-cmd, motor-pos-fb, pos-fb, motor-offset, backlash and screw comp. All present.simple_tp.c is still there, and joint jogging still goes through it.tpPause() / tpResume() and the paused flag: conform to the description, with no hidden condition.KINEMATICS_INVERSE_ONLY test is still there.The per-joint cubic interpolator looked like the obvious candidate for removal — many modern
planners emit the setpoint directly at the servo rate. On checking, cubic.c is still
compiled and control.c still calls cubicDrain() on every joint. The original
diagram was right about this.
Two earlier limits have since been lifted: the delegate functions were followed
(switch_to_teleop_mode() yielded erratum 24, the pausing flag yielded
erratum 25), and the 57 undocumented commands are now written up in
motion-commands-reference.md. The chapters “Backlash and Screw Error Compensation” and
“User Interfaces” turned out to be single-line FIXMEs — empty; “Task controller —
State” and “Reckoning of joints and axes” check out (the latter with one stale bug pointer,
erratum 22).
Still unexamined: “Terms and definitions”, “Adding custom NML commands”, the tool G-codes
(T, M6, G43, G10 L1, M61, G41/G42) and the internal state variables. The G-code reference audit is a
sample — five families out of a 2 786-line document. Other files under
docs/src/code/ — style guide, building, writing tests — are out of scope.