3、解析器架构设计:解析器整体模块划分(词法分析、语法分析、语义解释)、数据流与接口设计
手写G代码解析器的核心挑战,不在于逐行读取文本,而在于如何将非结构化的字符串流,转化为可供运动控制内核直接消费的结构化指令序列。一个健壮的解析器架构,必须清晰划分职责边界,定义稳定的数据契约,并设计可测试的流水线。本章将围绕“词法分析(Lexer)→ 语法分析(Parser)→ 语义解释(Interpreter)”三段式架构展开,重点阐述各模块的输入输出接口、内部状态管理以及它们之间的数据流关系。我们不会直接进入具体代码实现,而是先建立一张“工程蓝图”,确保后续编码时有据可依。
上图展示了本课程推荐的解析器整体架构。它并非简单的线性管道,而是一个带有明确错误反馈回路的闭环系统。词法分析器负责“分词”,语法分析器负责“组句”,语义解释器负责“执行意图”。三者之间通过不可变的数据对象(、AST节点、指令结构体)进行通信,从而降低耦合度,便于单元测试与后续扩展(如增加宏处理器或刀具补偿模块)。
3.1 词法分析模块:从字符流到流
核心原理
词法分析(Lexical Analysis)是解析器的“眼睛”。它逐字符读取G代码文件,忽略空白、换行、注释(以;或(...)开头的内容),并将有意义的字符序列组合成。一个是带有类型标签的最小语义单元。对于G代码而言,类型通常包括:地址字(如G、M、X、Y、Z、F、S、T)、数字字面量(整数或浮点数,如100、3.14)、字符串(用于文件名或消息)、以及特殊符号(如%、#用于变量)。
操作流程
- 初始化状态机:定义起始状态、数字状态、字母状态、注释状态。
- 逐字符扫描:对每个字符判断其类别(字母、数字、符号、空白)。
- 累积值:当遇到分隔符(空格、换行)或字符类型切换时,将累积的字符序列封装为。
- 附加位置信息:记录所在的行号(line)和列号(col),这是后续错误报告的关键。
- 输出流:将按顺序放入一个队列或列表,供语法分析器消费。
案例拆解:处理“N10 G01 X100.5 Y-20.3 F300”
| 输入片段 | 类型 | 值 | 备注 |
|---|---|---|---|
| N10 | ADDRESS_N | 10 | 行号,可忽略但需保留 |
| G01 | ADDRESS_G | 1 | 模态指令,01表示直线插补 |
| X100.5 | ADDRESS_X | 100.5 | 目标X坐标 |
| Y-20.3 | ADDRESS_Y | -20.3 | 目标Y坐标,注意负号处理 |
| F300 | ADDRESS_F | 300 | 进给速率 |
在实现时,需特别注意数字的解析:100.5应被识别为浮点数,而-20.3的负号应作为数字的一部分,而非单独的运算符。若遇到无法识别的字符(如@),应立即抛出带行号的词法错误。
3.2 语法分析模块:从流到AST
核心原理
语法分析(Syntax Analysis)是解析器的“大脑”。它读取流,根据G代码的语法规则(即“文法”),构建一棵抽象语法树(AST)。AST是流的结构化表示,它显式地表达了指令的层次关系。例如,一行G代码可能包含一个GCodeLine节点,其子节点包括AddressWord(地址字)和Parameter(参数)。对于模态指令(如G01),语法分析器还需维护一个模态状态表,记录当前默认的G指令、M指令、坐标系、刀具号等,以便在后续指令省略某些字段时自动补全。
判断方法与操作流程
- 递归下降解析:为每种语法规则(如
line、block、parameter)编写一个解析函数,函数间相互调用。 - 模态状态管理:在解析过程中,每遇到一个模态指令(如G01),就更新当前状态表。当后续行未指定G指令时,沿用上一行的值。
- 错误恢复策略:当遇到语法错误(如
G01 X100 Y缺少Y值),不应立即终止整个解析,而应跳过当前行,记录错误,并尝试解析下一行,以收集更多错误。
实战场景:处理模态指令的省略
G01 X10 Y10 F500
X20 Y20 ; 此处省略了G01和F,应沿用上一行的G01和F500
语法分析器在解析第二行时,发现没有G地址字,于是从模态状态表中取出当前值G01,并生成一个完整的GCodeLine节点。这种设计大大减少了G代码文件的冗余,但也要求解析器必须严格维护状态。
3.3 语义解释模块:从AST到运动指令
核心原理
语义解释(Semantic Interpretation)是解析器的“双手”。它遍历AST,执行具体的动作。对于运动指令(G0/G1/G2/G3),它计算目标坐标、进给速率,并生成一个运动指令对象(MotionCommand),该对象包含起点、终点、速度、插补类型等字段,可直接被运动控制内核消费。对于辅助指令(M指令,如M03主轴正转),它调用相应的系统函数。此外,语义解释器还负责处理变量赋值(#1=100)、算术表达式(#2=#1*2)和宏调用(G65 P1000)。
操作流程
- 遍历AST:采用深度优先或访问者模式(Visitor Pattern)遍历所有节点。
- 上下文管理:维护一个运行时上下文(Context),包含当前坐标、刀具长度补偿值、坐标系偏移等。
- 执行动作:根据节点类型,执行相应的处理函数。例如,遇到
GCodeLine节点,提取其G地址和参数,调用运动规划器。 - 生成输出:将处理结果封装为统一的
Instruction结构体,放入输出队列。
案例拆解:坐标偏移与刀具补偿
假设当前工件坐标系G54偏移为(X=10, Y=20),刀具长度补偿H01为5mm。当解释器处理G01 Z-10 F100时,它需要:
- 读取当前模态G01,确认是直线插补。
- 将Z坐标-10与当前Z坐标比较,计算运动距离。
- 应用刀具补偿:实际Z目标 = -10 + 5 = -5mm。
- 生成
MotionCommand{axis: Z, target: -5, feed: 100}。
这一过程涉及多个上下文变量的读取与计算,是语义解释器最复杂的部分。
3.4 模块间接口设计与数据流优化
接口定义
清晰的接口是模块化设计的基石。建议使用结构体或类定义数据契约:
// 结构体
typedef struct {
TokenType type; // 枚举:ADDRESS_G, ADDRESS_X, NUMBER, ...
char* lexeme; // 原始字符串
double value; // 数值(若适用)
int line;
int col;
} ;
// AST节点
typedef struct ASTNode {
NodeType type; // 枚举:GCODE_LINE, PARAMETER, COMMENT
char* address; // 地址字,如"G"
double value; // 参数值
struct ASTNode* next; // 指向同一行的下一个参数
} ASTNode;
// 运动指令
typedef struct {
int motion_type; // 0: 快速定位, 1: 直线, 2: 圆弧CW, 3: 圆弧CCW
double target_x, target_y, target_z;
double feed_rate;
int is_absolute; // /增量模式
} MotionCommand;
数据流优化策略
- 流水线并行:词法分析器可以独立于语法分析器运行,通过线程安全的队列传递流,提高多文件批处理效率。
- 延迟计算:语义解释器中的坐标转换和刀具补偿,可以延迟到真正需要执行指令时再进行,以减少不必要的计算。
- 错误隔离:每个模块只处理自己层面的错误。词法错误不进入语法层,语法错误不进入语义层。最终由顶层控制器统一汇总错误列表。