解析器设计思路:三层架构的拆解与落地
做G代码解析器,说白了就是让机器读懂人的指令。
你想想看,G代码本身只是一堆文本字符串,比如G01 X100 Y200 F500。但数控机床要执行的是精确的插补运动、主轴启停、冷却液开关。这中间差了一个翻译官——没错,就是解析器。
我个人习惯把解析器拆成三层:词法分析、语法分析、语义分析。这不是为了炫技,而是我在实际项目中踩过坑之后才明白的——不分层,后期维护就是噩梦。
第一层:词法分析——把字符串切成“单词”
词法分析,说白了就是分词。
输入是一整行G代码,比如:
N10 G01 X100.5 Y200.0 F500 M08
输出是一串Token(词法单元)。每个Token包含两个信息:类型和值。
我一般这样定义Token类型:
| Token类型 | 示例 | 说明 |
|---|---|---|
| G_CODE | G01, G02, G00 | 准备功能 |
| M_CODE | M03, M08, M30 | 辅助功能 |
| ADDRESS | X, Y, Z, F, S | 地址字 |
| NUMBER | 100.5, 200, 500 | 数值 |
| LINE_NUMBER | N10, N20 | 行号 |
| COMMENT | (钻孔循环) | 注释 |
分词后的结果大概是这样的:
[
Token(LINE_NUMBER, "N10"),
Token(G_CODE, "G01"),
Token(ADDRESS, "X"),
Token(NUMBER, 100.5),
Token(ADDRESS, "Y"),
Token(NUMBER, 200.0),
Token(ADDRESS, "F"),
Token(NUMBER, 500),
Token(M_CODE, "M08")
]
第二层:语法分析——检查“句子”通不通
词法分析完了,我们得到了一堆Token。但这一堆Token能不能组成一句合法的G代码?这就是语法分析要干的事。
语法分析的核心是定义语法规则。G代码的语法其实挺简单的,我一般用BNF(巴科斯范式)来描述:
<block> ::= <line_number>? <word>* <comment>?
<word> ::= <g_code> | <m_code> | <address> <number>
翻译成人话就是:一个程序段(block)可以有一个可选的行号,后面跟零个或多个指令字(word),最后可以跟一个可选的注释。
每个指令字要么是G代码/M代码,要么是地址字+数值的组合。
我在项目中遇到过这样的坑:有人写了G01 X Y100,X后面没跟数值。词法分析阶段不会报错,因为X本身是合法的地址字。但语法分析阶段就应该报错——因为地址字后面必须跟数值。
G01 G02 X100 Y200,语法上没问题(两个G代码可以出现在同一行),但语义上G01和G02是互斥的——这就要交给下一层了。
第三层:语义分析——理解“意思”对不对
语义分析是三层中最复杂的一层。它要回答的问题是:这段代码在逻辑上合理吗?
常见的语义检查包括:
- 模态冲突检查: 同一行不能同时出现G01和G02
- 参数范围检查: 主轴转速S不能超过机床最大值
- 运动指令完整性检查: G02/G03圆弧指令必须指定I、J、K或R
- 坐标系检查: 在G53(机床坐标系)下不能使用G92(工件坐标系偏移)
语义分析的输出,就是一棵抽象语法树(AST),或者更直接一点——一个可执行的指令对象。
我习惯这样设计指令对象:
class GCodeCommand:
def __init__(self):
self.g_code = None # G01, G02, 等
self.m_code = None # M03, M08, 等
self.words = {} # {"X": 100.5, "Y": 200.0, "F": 500}
self.line_number = None
self.comment = None
self.is_valid = True
self.error_message = ""
语义分析完成后,解析器的最终输出就是这个指令对象。后续的插补器、PLC控制器直接拿这个对象用就行,不用再碰原始字符串。
三层架构的输入与输出
把整个流程串起来,就是这样的:
输入: 原始G代码字符串
↓
[词法分析] → Token列表
↓
[语法分析] → 语法树(或扁平指令列表)
↓
[语义分析] → 可执行指令对象
↓
输出: 指令对象列表
我曾经犯过一个错误:把词法分析和语法分析混在一起写。结果代码越写越乱,改一个地方崩三个地方。后来老老实实分层,每个模块只干一件事,调试起来舒服多了。
一张图看懂三层架构
下面这张SVG图,是我自己画的三层架构流程图。你可以看到数据是怎么一步步从原始文本变成可执行指令的:
嗯,这张图我画了好一会儿。你仔细看,每一层都有明确的输入输出边界。词法分析不认识G代码的含义,语法分析不关心数值是否合理,语义分析不碰原始字符串——各司其职,这就是工程化的思维。
解析器的输出,最终就是一个GCodeCommand对象的列表。后续的插补器拿到这个列表,逐条执行就行。如果某条指令的is_valid是False,就跳过并记录错误日志。
这样设计的好处是:解析器只负责“翻译”,不负责“执行”。职责单一,测试也方便。你可以单独测试词法分析、单独测试语法分析、单独测试语义分析——每个模块的单元测试都能写得很干净。
好了,三层架构的思路就讲到这里。下一层我们开始动手写代码——从词法分析器开始。
公众号:蓝海资料掘金营,微信deep3321