# LinuxCNC 数据系统核心原理 本文从本质上解释 LinuxCNC 的数据系统。这里的“数据系统”不是单个数据库,也不是单个消息队列,而是由多个不同实时等级、不同生命周期、不同所有权的数据通道组合而成。 LinuxCNC 的核心设计目标是:把非实时的解释、界面、配置、任务调度,与实时运动控制隔离开,同时又让它们能以受控方式交换命令、状态和信号。 ## 1. 一句话理解 LinuxCNC 的数据系统本质上分为五层: 1. **INI 配置数据**:启动时读取,决定机器拓扑、模块、限位、速度、GUI、HAL 文件。 2. **NML 命令/状态数据**:跨进程通信总线,GUI、halui、task、server 通过它交换命令和状态。 3. **Task/Interpreter 数据**:解释 G-code,维护模态状态、坐标系、刀补、运行队列,并把规范动作转成 motion/io 命令。 4. **Motion 实时共享数据**:task 和实时 motion 模块之间的 command/status/config 共享内存。 5. **HAL 信号数据**:实时和用户态组件共享的 pin/signal/parameter 网络,连接 motion、驱动、GUI、Vismach 和自定义组件。 简化图: ```text INI -> linuxcnc 启动脚本 -> NML 通道 / HAL 模块 / GUI / task / motion GUI / halui -> NML command -> milltask -> interpreter / canonical commands -> motion command shared memory -> realtime motion -> HAL pins/signals -> feedback/status -> EMC_STAT / GUI 显示 ``` ## 2. 为什么 LinuxCNC 要分成这些数据系统 CNC 控制同时有两类完全不同的需求: - **人机界面、G-code 解释、文件读取、坐标系管理**:逻辑复杂,但不要求微秒级确定性。 - **伺服周期、轨迹插补、关节输出、限位、探针采样**:必须稳定、周期性、可预测。 因此 LinuxCNC 不能把所有数据都放在一个普通进程里处理。它将系统拆成多个进程和实时模块: - GUI 可以慢一些,甚至短暂卡顿。 - task 可以做解释器、队列、状态同步。 - motion 必须按 servo period 周期运行。 - HAL 负责把实时数据以 pin/signal 方式连接起来。 本质原则: ```text 非实时层负责“决定要做什么” 实时层负责“按确定周期执行” HAL 负责“把实时变量接到机器/仿真/GUI” NML 负责“进程之间传命令和状态” ``` ## 3. INI:启动配置数据 INI 是 LinuxCNC 的静态配置入口。它不是运行时主数据通道,而是启动时的装配说明书。 典型职责: - 选择 GUI:`[DISPLAY] DISPLAY = axis` - 指定 G-code 自动打开文件:`[DISPLAY] OPEN_FILE = ...` - 指定运动学:`[KINS] KINEMATICS = ...` - 指定 joint 数:`[KINS] JOINTS = ...` - 指定坐标字母:`[TRAJ] COORDINATES = ...` - 指定实时 motion 模块参数:`[EMCMOT] SERVO_PERIOD = ...` - 指定 HAL 文件:`[HAL] HALFILE = ...` - 指定单条 HAL 命令:`[HAL] HALCMD = ...` - 指定 NML 文件:`[EMC] NML_FILE = ...`,默认常见为 `configs/common/linuxcnc.nml` `scripts/linuxcnc` 使用 `inivar` 从 INI 中读取这些配置,然后按顺序启动 server、task、halui、HAL、GUI。 INI 的特点: - 启动时影响很大。 - 运行中多数值不会自动重新读取。 - 很多 INI 项会被 task/motion 初始化代码转写到 NML 状态或 HAL pin 中。 - INI 本身不负责实时连接,实时连接由 HAL 完成。 ## 4. NML:跨进程命令/状态总线 NML 是 LinuxCNC 的进程间通信系统。它把 GUI、halui、task、server 等非实时进程连接起来。 默认 NML 配置可见: ```text configs/common/linuxcnc.nml ``` 其中定义了三个顶层 buffer: ```text emcCommand emcStatus emcError ``` 它们的本质职责: - `emcCommand`:GUI/halui 发命令给 task。 - `emcStatus`:task 汇总系统状态供 GUI/halui 读取。 - `emcError`:错误和操作信息通道。 `linuxcncsvr` 是这些 NML channel 的 master/server。启动脚本注释也说明:`linuxcncsvr` 默认第一个启动,因为它创建/持有 NML channel。 ## 5. EMC_STAT:GUI 看到的系统状态 LinuxCNC 顶层状态结构是 `EMC_STAT`,定义在: ```text src/emc/nml_intf/emc_nml.hh ``` 它聚合了: ```text EMC_STAT ├─ EMC_TASK_STAT task ├─ EMC_MOTION_STAT motion └─ EMC_IO_STAT io ``` 其中 motion 部分又包含: ```text EMC_MOTION_STAT ├─ EMC_TRAJ_STAT traj ├─ EMC_JOINT_STAT joint[] ├─ EMC_AXIS_STAT axis[] ├─ EMC_SPINDLE_STAT spindle[] ├─ synch_di[] ├─ synch_do[] ├─ analog_input[] └─ analog_output[] ``` GUI 看到的大部分状态都来自 `EMC_STAT`: - 当前模式:manual/mdi/auto - 当前任务状态:estop/off/on - 当前执行状态:done/exec/error - 当前文件、当前行、读到哪一行 - 当前 G-code/M-code 模态 - 当前坐标系偏置、G92、刀补 - 当前 commanded position - 当前 actual position - joint 状态 - spindle 状态 - motion queue 状态 AXIS 的 Python 扩展通过 `RCS_STAT_CHANNEL` 读取 `emcStatus`,poll 时将 `EMC_STAT` 复制到本地 Python 对象中。 ## 6. EMC_COMMAND:GUI/halui 发出的命令 GUI、halui、外部客户端一般不会直接写 motion 共享内存,而是向 `emcCommand` NML channel 写命令。 例如: - 上电/下电 - 解除急停 - 切换模式 - MDI 命令 - 打开程序 - cycle start - pause/resume - jog 这些命令先进入 task。task 决定命令是否合法、当前状态是否允许执行,以及是否需要调用 interpreter 或 motion。 本质上: ```text GUI/halui 不直接控制伺服周期 GUI/halui 发送意图 task 负责调度和状态一致性 motion 负责实时执行 ``` ## 7. Task/Interpreter:命令解释和调度层 `milltask` 是 LinuxCNC 的任务协调进程。它同时处理: - NML command - interpreter G-code 解释 - motion command 发送 - IO/tool/spindle/coolant 命令 - task 状态同步 - EMC_STAT 汇总更新 关键文件: ```text src/emc/task/emctaskmain.cc src/emc/task/taskintf.cc src/emc/rs274ngc/ src/emc/task/emccanon.cc ``` 典型链路: ```text GUI cycle start -> NML emcCommand -> milltask -> interpreter 读取 G-code -> canonical commands -> taskintf.cc -> usrmotWriteEmcmotCommand() -> realtime motion shared memory ``` 解释器维护的不是简单的“当前行文本”,而是一套 CNC 模态状态: - G 模态组 - M 模态组 - 坐标系 G54/G55/... - G92 偏置 - 工件平面 - 距离模式 - 进给模式 - 刀具长度补偿 - 半径补偿 - 子程序调用层级 - 参数文件变量 - remap 状态 这些数据在 task/interpreter 层处理,然后转成 motion 能理解的动作。 ## 8. Canonical command:解释器和 motion 之间的语义桥 解释器不会直接操作 joint。它输出更抽象的“规范动作”: - 直线移动 - 圆弧移动 - 设置速度 - 设置主轴 - 设置 IO - 设置 motion analog output - 等待输入 - 换刀 例如 `M68 E3 Q1` 的链路是: ```text RS274NGC 解释 M68 -> SET_AUX_OUTPUT_VALUE(3, 1) -> emccanon.cc 创建 EMC_MOTION_SET_AOUT -> taskintf.cc: emcMotionSetAout() -> usrmotWriteEmcmotCommand() -> motion command shared memory -> motion.analog-out-03 = 1 ``` 这说明 G-code 不是直接写 HAL pin,而是通过解释器、canonical 层、task、motion,再由 motion 暴露 HAL pin。 ## 9. Motion 共享内存:task 与实时 motion 的边界 实时 motion 的核心共享结构是: ```text src/emc/motion/motion_struct.h ``` 核心结构: ```c emcmot_struct_t { command_mutex; emcmot_command_t command; emcmot_status_t status; emcmot_config_t config; emcmot_error_t error; emcmot_internal_t internal; } ``` 这是一块 task 和 realtime motion 都能访问的共享内存区域。 其中: - `emcmot_command_t`:task 写入,motion 读取。 - `emcmot_status_t`:motion 周期更新,task 读取。 - `emcmot_config_t`:机器配置和 motion 参数。 - `emcmot_internal_t`:motion 内部轨迹规划/调试状态。 task 写 motion command 的路径: ```text taskintf.cc -> usrmotWriteEmcmotCommand() -> 加 command_mutex -> 复制 emcmot_command_t 到共享内存 -> 等待 motion 回显 commandNumEcho ``` motion status 读取有一个重要细节:`emcmot_status_t` 有 `head`/`tail` 字段。motion 更新状态时先改 `head`,完成后设置 `tail=head`。读取方复制后检查 `head == tail`,避免读到半更新的数据。 这是 LinuxCNC 实时数据一致性的关键手段之一。 ## 10. Motion 实时循环:周期性状态机 motion 控制循环位于: ```text src/emc/motion/control.c ``` 每个 servo cycle 处理大致顺序: ```text 读取 homing/input pins 处理 switchkins 切换 读取 HAL 输入 执行 forward kinematics 处理 probe 检查 fault/limit 确定运行模式 处理 jog/homing 轨迹规划取点 inverse kinematics 插补到 joint 输出到 HAL 更新 status heartbeat++ ``` 它的本质职责: - 在固定周期内维护运动状态。 - 从轨迹规划器获取下一个笛卡尔点。 - 调用运动学把笛卡尔位置转换为 joint 位置。 - 输出 joint 命令、spindle、IO、状态 HAL pin。 - 读取反馈、探针、限位、外部 offset 等 HAL pin。 - 更新 `emcmot_status_t` 给 task 读取。 motion 不读取 G-code 文件,也不关心 AXIS 界面。它只执行已经被 task/interpreter 转换过的命令。 ## 11. HAL:实时信号网络 HAL 是 LinuxCNC 最容易被误解的部分。它不是 NML,也不是普通配置文件。HAL 的本质是一个共享内存对象图: ```text HAL shared memory ├─ component list ├─ pin list ├─ signal list ├─ parameter list ├─ function list └─ thread list ``` 这些结构定义在: ```text src/hal/hal_priv.h ``` 关键概念: ### Component 组件是 pin/function/parameter 的拥有者。例如: - `motmod` - `pid` - `mux2` - `halui` - `xyzbc-trt-gui` - 自定义 `.comp` 组件调用 `hal_init()` 注册到 HAL。 ### Pin pin 是组件暴露的数据端口。每个 pin 有: - 名字 - 类型 - 方向 - 所属 component - 连接的 signal 方向包括: - input - output - io ### Signal signal 是多个 pin 共享的一块数据值。`net` 命令本质上是: ```text 把多个 pin 的 data pointer 指向同一个 signal value ``` 例如: ```hal net :kinstype-select <= motion.analog-out-03 => motion.switchkins-type ``` 表示: ```text motion.analog-out-03 写 signal :kinstype-select motion.switchkins-type 读 signal :kinstype-select ``` ### Parameter parameter 通常是配置量或调试量,可用 `setp` 设置。 ### Function 和 Thread 实时组件导出 function,HAL thread 周期调用这些 function。 例如: ```hal addf motion-command-handler servo-thread addf motion-controller servo-thread addf J0_pid.do-pid-calcs servo-thread ``` 这决定了每个 servo period 内函数执行顺序。 ## 12. HAL 与 NML 的本质区别 | 项目 | NML | HAL | | --- | --- | --- | | 本质 | 跨进程消息/状态通道 | 共享内存信号网络 | | 主要用途 | GUI、task、server 通信 | 实时组件连接 | | 数据形态 | command/status/error 消息 | pin/signal/parameter 值 | | 时间特性 | 非实时或软实时 | 可用于实时线程 | | 典型读写者 | AXIS、halui、milltask | motmod、驱动、pid、Vismach、halui | | 示例 | `emcCommand`, `emcStatus` | `motion.analog-out-03`, `joint.0.motor-pos-cmd` | 重要结论: ```text NML 表达“系统命令与系统状态” HAL 表达“机器信号与实时变量” ``` ## 13. 数据所有权原则 LinuxCNC 的数据系统依赖清晰的所有权。 典型规则: - GUI 不直接写 joint command。 - task 不直接修改 HAL 伺服输出。 - motion 不解释 G-code。 - HAL signal 通常只能有一个 writer。 - motion status 由 realtime motion 写,task/GUI 读。 - EMC_STAT 由 task 汇总写,GUI/halui 读。 - INI 是启动配置,不是周期数据通道。 如果违反这些边界,系统会变得不可预测。 ## 14. 数据刷新频率和一致性 不同数据有不同刷新频率: - servo-thread:通常 1 ms 或更快。 - task cycle:常见 10 ms 左右。 - GUI poll:通常几十毫秒级。 - NML status:由 task 汇总后供 GUI 读取。 - HAL pin:实时线程中按 thread 周期更新。 所以 GUI 看到的位置不是“每个伺服周期的每一个点”,而是 task/GUI poll 后的状态快照。 这也是为什么实时控制必须在 motion/HAL 内完成,不能依赖 GUI。 ## 15. xyzbc-trt 配置中的具体体现 以 `xyzbc-trt.ini` 为例: ### INI 层 ```ini [KINS] KINEMATICS = xyzbc-trt-kins sparm=identityfirst JOINTS = 5 [TRAJ] COORDINATES = XYZBC [HAL] HALFILE = LIB:basic_sim.tcl HALCMD = net :kinstype-select <= motion.analog-out-03 => motion.switchkins-type ``` INI 决定: - 加载什么运动学模块。 - 有多少 joint。 - HAL 怎样连接。 - GUI 加载什么面板。 ### HAL 层 ```hal motion.analog-out-03 -> motion.switchkins-type joint.N.pos-fb -> xyzbc-trt-gui.* motion.tooloffset.z -> xyzbc-trt-kins.tool-offset ``` HAL 决定: - M68 输出如何进入 switchkins。 - joint feedback 如何驱动 Vismach。 - 刀长补偿如何进入运动学。 ### NML/task/interpreter 层 按下 PyVCP 按钮: ```text pyvcp button -> halui MDI command -> NML command -> milltask -> interpreter 执行 M428/M429/M430 remap ``` ### motion 层 `M68 E3 Q1` 最终写入: ```text motion.analog-out-03 = 1 ``` HAL 将其连接到: ```text motion.switchkins-type = 1 ``` motion servo cycle 检测到变化: ```text handle_kinematicsSwitch() -> kinematicsSwitch(1) -> 激活 xyzbcKinematicsInverse / Forward ``` ### Vismach 层 Vismach 不参与实时控制,只读取 HAL pin: ```text joint feedback + offset pins -> 三维模型变换 ``` 它是 HAL 数据消费者,不是运动控制源。 ## 16. 本质总结 LinuxCNC 数据系统的核心不是“一个中心数据库”,而是四种不同性质的数据机制协作: 1. **INI**:装配系统。 2. **NML**:让进程交换命令、状态、错误。 3. **Task/Interpreter**:把人的意图和 G-code 转换成规范动作。 4. **Motion shared memory**:连接非实时 task 和实时 motion。 5. **HAL**:把实时变量连接成机器信号网络。 最终形成一条严格分层的数据链: ```text 人/程序意图 -> NML command -> task/interpreter -> canonical command -> motion command shared memory -> realtime motion -> HAL pins/signals -> 机器/仿真反馈 -> motion status -> EMC_STAT -> GUI/halui 显示 ``` 理解这条链,就能解释 LinuxCNC 中大多数现象: - 为什么 GUI 按钮通常不是直接控制实时变量。 - 为什么 `M68` 可以改变 HAL pin。 - 为什么 `motion.switchkins-type` 要通过 `motion.analog-out-03` 控制。 - 为什么 Vismach 只需要连 HAL pin 就能显示机床。 - 为什么 task 和 motion 之间要有 command/status 共享内存。 - 为什么 HAL signal 通常要求单 writer。 - 为什么实时动作不能依赖 GUI poll。