2. G代码解析引擎架构:解析器设计模式、词法分析、语法分析、语义分析
好,咱们直接切入正题。上一章聊了G代码的“前世今生”,这一章,咱们得亲手拆解一台解析引擎。说白了,就是让机器读懂G代码的那套“脑子”是怎么长的。
我个人习惯把解析引擎比作一个翻译团队。你想想看,你给老外(机器)说一句中文(G代码),他听不懂。你得先有个人把句子拆成单词(词法分析),再有人按语法规则把单词串成句子结构(语法分析),最后还得有人理解这句话到底要干嘛(语义分析)。缺一个环节,机器就得“死机”。
2.1 解析器设计模式:别重复造轮子
做解析器,最容易犯的错就是“一把梭”。拿到一行G代码,从头读到尾,边读边执行。嗯,我刚开始做CNC系统时就这么干的,结果被坑得很惨——遇到一个语法错误,机器已经跑了一半行程了。
所以,成熟的解析引擎都采用分层设计模式。我个人推荐用管道过滤器模式(Pipeline & Filter)。每一层只干一件事,干完了传给下一层。
核心设计原则:
- 单一职责:词法分析器只负责切Token,别管语法对不对
- 开闭原则:要支持新的G代码指令,加个过滤器就行,别改核心代码
- 依赖倒置:高层模块不依赖低层细节,都依赖抽象接口
举个例子,我做过一个项目,客户要求支持自定义的M代码。如果当初设计成“一把梭”模式,我得把整个解析器翻一遍。但用了管道模式,我只需要在语义分析层加一个自定义指令处理器,其他层完全不动。省心多了。
2.2 词法分析:把字符串变成Token
词法分析,说白了就是“分词”。把G01 X100 Y50 F200这串字符,拆成一个个有意义的单词(Token)。
为什么会需要这一步?你想想看,机器不认识“G01”是个整体,它只认识字符‘G’、‘0’、‘1’。词法分析器的作用,就是把这些字符组合成有意义的单元。
我在项目中遇到过一个问题:某台老机床的G代码里,注释居然用括号(...),但标准应该是分号;。词法分析器如果不处理这个,后面全乱套。
实战技巧:词法分析器最好支持状态机模式。比如遇到(就进入注释状态,遇到)才退出。这样处理各种边界情况特别稳。
典型的Token类型包括:
| Token类型 | 示例 | 说明 |
|---|---|---|
| ADDRESS | G, M, X, Y, Z, F, S | 地址字,表示指令类型或轴 |
| NUMBER | 01, 100, 50, 200 | 数值,跟在地址字后面 |
| EOL | \n, \r\n | 行结束符,表示一条指令结束 |
| COMMENT | (钻孔循环), ; 注释 | 注释内容,解析时跳过 |
| UNKNOWN | @, # 等 | 非法字符,需要报错 |
// 词法分析器核心逻辑(伪代码)
function tokenize(line: string): Token[] {
let tokens = [];
let i = 0;
while (i < line.length) {
let ch = line[i];
if (ch === 'G' || ch === 'M' || ch === 'X' || ch === 'Y' || ch === 'Z' || ch === 'F' || ch === 'S') {
// 读取地址字
tokens.push({ type: 'ADDRESS', value: ch });
i++;
} else if (isDigit(ch)) {
// 读取数字
let num = '';
while (i < line.length && isDigit(line[i])) {
num += line[i];
i++;
}
tokens.push({ type: 'NUMBER', value: parseInt(num) });
} else if (ch === '(') {
// 跳过括号注释
while (i < line.length && line[i] !== ')') i++;
i++; // 跳过 ')'
} else if (ch === ';') {
// 分号注释,跳过整行
break;
} else if (ch === '\n') {
tokens.push({ type: 'EOL' });
i++;
} else {
// 空格或非法字符
i++;
}
}
return tokens;
}
2.3 语法分析:检查句子通不通
词法分析完了,你得到了一堆Token。但G01 X100和X100 G01哪个对?语法分析就是干这个的——检查Token的顺序是否符合G代码的语法规则。
我曾经接手过一个项目,之前的工程师写的语法分析器,居然用了一大堆if-else嵌套。代码长得像意大利面条,加一个新指令得改十几个地方。后来我全推倒重来,改用递归下降解析器。
注意:G代码的语法其实比大多数编程语言简单,但有个坑——模态指令。比如G01是直线插补,如果你下一行只写了X200,它默认还是G01模式。语法分析器必须维护这个状态。
语法分析的结果,通常是一棵抽象语法树(AST)。每个节点代表一个指令或参数。
// 语法分析器示例(递归下降)
class Parser {
constructor(tokens) {
this.tokens = tokens;
this.pos = 0;
this.currentModal = 'G00'; // 默认快速定位
}
parseLine() {
let commands = [];
while (this.pos < this.tokens.length) {
let token = this.tokens[this.pos];
if (token.type === 'ADDRESS') {
if (token.value === 'G') {
commands.push(this.parseGCommand());
} else if (token.value === 'M') {
commands.push(this.parseMCommand());
} else if (['X','Y','Z'].includes(token.value)) {
// 轴移动指令,继承当前模态
commands.push(this.parseAxisMove());
} else {
throw new Error(`未知地址字: ${token.value}`);
}
} else if (token.type === 'EOL') {
this.pos++;
break;
} else {
this.pos++;
}
}
return commands;
}
parseGCommand() {
this.pos++; // 跳过 'G'
let numToken = this.tokens[this.pos];
if (numToken.type !== 'NUMBER') throw new Error('G指令后需要数字');
this.pos++;
let gCode = numToken.value;
// 更新模态状态
if (gCode === 0) this.currentModal = 'G00';
else if (gCode === 1) this.currentModal = 'G01';
// ... 其他G代码
return { type: 'G', code: gCode };
}
}
2.4 语义分析:理解这句话的意思
语法分析只检查“句子通不通”,语义分析才真正理解“这句话什么意思”。
举个例子,G01 X100 Y50 F200语法上完全正确。但如果你当前刀具在Z轴-10的位置,而工件表面在Z0,直接执行X100 Y50,刀具会直接撞上去。语义分析就是要发现这种“逻辑上不合理”的情况。
我记得有一次调试五轴机床,客户反馈说程序跑着跑着就撞刀了。查了半天,发现是语义分析层没有检查G43(刀具长度补偿)是否在G49(取消补偿)之前被正确配对。从那以后,我就在语义分析里加了一个状态机,专门跟踪各种模态指令的配对情况。
语义分析的核心职责:
- 类型检查:比如
F后面必须是正数,不能是负数 - 范围检查:比如主轴转速
S不能超过机床最大值 - 状态一致性:比如不能在攻丝循环中切换坐标系
- 资源冲突:比如不能同时用两个刀具
// 语义分析器示例
class SemanticAnalyzer {
constructor() {
this.state = {
currentTool: null,
currentWorkOffset: 'G54',
coolantOn: false,
spindleSpeed: 0
};
}
analyze(ast) {
for (let node of ast) {
if (node.type === 'M' && node.code === 6) {
// M6 换刀指令
if (this.state.spindleSpeed > 0) {
throw new Error('换刀前必须先停止主轴!');
}
// ... 更新刀具状态
}
if (node.type === 'G' && node.code === 43) {
// G43 刀具长度补偿
if (this.state.currentTool === null) {
throw new Error('执行G43前必须先选刀!');
}
}
// ... 其他语义规则
}
}
}
我的经验:语义分析是解析引擎里最容易被忽视,但也是最能体现“工匠精神”的地方。很多CNC系统跑着跑着出问题,不是词法语法错了,而是语义分析没做到位。我建议你在语义分析层至少留出30%的代码量,专门处理各种边界情况和异常状态。
好了,这一章的内容就到这儿。解析引擎的三层架构——词法、语法、语义,每一层都有它的脾气。下一章咱们聊聊更具体的东西:如何用代码实现一个轻量级的G代码解析器,以及怎么处理那些“不守规矩”的自定义指令。