解析器设计思路:三层架构的拆解与落地

做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")
]
我的小技巧: 词法分析阶段不要做任何语义判断。比如遇到X后面跟了个字母,你只需要报“词法错误:X后面期望数值”,而不是去猜用户想干嘛。保持简单,保持纯粹。

第二层:语法分析——检查“句子”通不通

词法分析完了,我们得到了一堆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列表
  ↓
[语法分析] → 语法树(或扁平指令列表)
  ↓
[语义分析] → 可执行指令对象
  ↓
输出: 指令对象列表

我曾经犯过一个错误:把词法分析和语法分析混在一起写。结果代码越写越乱,改一个地方崩三个地方。后来老老实实分层,每个模块只干一件事,调试起来舒服多了。

核心原则: 每一层只依赖前一层的输出,不越级访问原始数据。词法分析只看字符串,语法分析只看Token,语义分析只看语法结构。这样任何一层出问题,都能快速定位。

一张图看懂三层架构

下面这张SVG图,是我自己画的三层架构流程图。你可以看到数据是怎么一步步从原始文本变成可执行指令的:

G代码解析器三层架构 输入:原始G代码字符串 "N10 G01 X100.5 Y200.0 F500 M08" 第一层:词法分析 将字符串拆分为Token列表 输出:[Token(LINE_NUMBER), Token(G_CODE), ...] 第二层:语法分析 检查Token序列是否符合语法规则 输出:语法树 / 扁平指令结构 第三层:语义分析 检查逻辑合理性,生成可执行指令 输出:GCodeCommand 对象列表 各层职责 ● 词法:分词 ● 词法:识别Token类型 ● 语法:结构检查 ● 语法:错误定位 ● 语义:逻辑检查 ● 语义:参数校验 ● 语义:生成指令 每一层只依赖前一层的输出,不越级访问原始数据

嗯,这张图我画了好一会儿。你仔细看,每一层都有明确的输入输出边界。词法分析不认识G代码的含义,语法分析不关心数值是否合理,语义分析不碰原始字符串——各司其职,这就是工程化的思维。

避坑指南: 我曾经在项目里把语义检查塞进语法分析里,结果代码耦合度极高。后来重构花了整整两天。记住:每一层只做自己该做的事,别越界。

解析器的输出,最终就是一个GCodeCommand对象的列表。后续的插补器拿到这个列表,逐条执行就行。如果某条指令的is_valid是False,就跳过并记录错误日志。

这样设计的好处是:解析器只负责“翻译”,不负责“执行”。职责单一,测试也方便。你可以单独测试词法分析、单独测试语法分析、单独测试语义分析——每个模块的单元测试都能写得很干净。

好了,三层架构的思路就讲到这里。下一层我们开始动手写代码——从词法分析器开始。


公众号:蓝海资料掘金营,微信deep3321