第2章:G代码解析器架构设计

好,咱们直接进入正题。上一章聊了G代码解析器到底是个什么东西,这一章咱们得把它的骨架搭起来。说白了,就是回答一个问题:这个解析器,到底该怎么拆?

我个人习惯,在设计任何解析器之前,先画一张大图。把整个流程走一遍,心里才有底。你想想看,G代码从文件里读出来,到最终驱动电机转动,中间要经过多少道工序?

2.1 核心模块:三驾马车

我把整个解析器拆成了三个核心模块。这三个模块各司其职,缺一不可。我在项目中遇到过不少新手,上来就把所有逻辑揉在一起写,结果后期改一个指令格式,整个代码都得重写。嗯,那是真痛苦。

核心三模块:

  • 词法分析器(Lexer):负责把原始G代码字符串,拆成一个个有意义的“单词”(Token)。
  • 语法分析器(Parser):负责把这些“单词”按照语法规则,组装成一条条可执行的“指令”(Command)。
  • 执行器(Executor):负责真正去执行这些指令,比如更新坐标、控制IO、调用插补器。

为了让你看得更清楚,我画了一张流程图。这张图我每次讲课都会拿出来,它清晰地展示了数据在三个模块之间的流转过程。

G代码解析器核心架构流程图 原始G代码字符串 词法分析器 (Lexer) G01 X100 Y200 F500 → [G01, X100, Y200, F500] 语法分析器 (Parser) [G01, X100, Y200, F500] → 直线插补指令 执行器 (Executor) 更新坐标、调用插补器、输出脉冲 电机运动 / 状态更新

你看,数据流非常清晰。从原始字符串开始,经过词法分析变成Token流,再经过语法分析变成指令对象,最后执行器干活。每一层只做一件事,这就是模块化的精髓。

2.2 词法分析器:把字符串变成Token

词法分析,说白了就是“分词”。G代码的格式其实很规整,比如 G01 X100 Y200 F500,一眼就能看出来:一个字母后面跟一串数字。

我建议你把它设计成一个状态机。为什么?因为G代码的字符类型就那么几种:字母、数字、正负号、小数点、空格、换行。状态机处理起来干净利落。

我的经验: 词法分析器不要做得太复杂。你只需要识别出Token的类型和值就行。比如 X100,类型是 AXIS_X,值是 100.0。至于这个X轴坐标合不合法,那是语法分析器的事。

来看看核心的Token类型定义:

// 定义Token类型
typedef enum {
    TOKEN_G_CODE,      // G代码,如 G01, G02
    TOKEN_M_CODE,      // M代码,如 M03, M05
    TOKEN_AXIS_X,      // X轴坐标
    TOKEN_AXIS_Y,      // Y轴坐标
    TOKEN_AXIS_Z,      // Z轴坐标
    TOKEN_FEEDRATE,    // 进给率 F
    TOKEN_SPINDLE,     // 主轴转速 S
    TOKEN_TOOL,        // 刀具号 T
    TOKEN_NUMBER,      // 纯数字
    TOKEN_EOL,         // 行结束
    TOKEN_EOF          // 文件结束
} TokenType;

// Token结构体
typedef struct {
    TokenType type;
    double value;      // 数值
    char text[32];     // 原始文本,用于调试
} Token;

词法分析器的核心逻辑,就是一个 getNextToken() 函数。每次调用,它从输入流里读一个字符,判断类型,然后返回一个Token。我曾经见过有人用正则表达式来做词法分析,结果性能惨不忍睹。对于G代码这种格式固定的东西,手写一个状态机,又快又稳。

2.3 语法分析器:把Token变成指令

Token流出来了,接下来要干嘛?组装成指令。语法分析器要回答的问题是:这一行G代码,到底想让我干什么?

这里我强烈推荐使用策略模式。为什么?因为G代码的指令种类太多了:直线插补、圆弧插补、暂停、换刀……每种指令的处理逻辑都不一样。如果用一堆 if-else,代码会变得又臭又长。

策略模式的核心思想很简单:把每种指令的处理逻辑封装成一个独立的策略类。解析器只负责根据G代码的类型,找到对应的策略,然后执行。

策略模式在G代码解析器中的应用:

  • 上下文(Context):语法分析器本身。它持有当前解析到的Token流。
  • 策略接口(Strategy):定义一个 parse(TokenStream) 方法,返回一个指令对象。
  • 具体策略(ConcreteStrategy):比如 G01ParseStrategy、G02ParseStrategy、M03ParseStrategy。

来看代码实现:

// 策略接口
typedef struct {
    Command* (*parse)(TokenStream* stream);
} ParseStrategy;

// 具体策略:G01 直线插补
Command* parseG01(TokenStream* stream) {
    Command* cmd = createCommand(CMD_LINEAR);
    // 从Token流中读取X、Y、Z、F值
    while (hasNext(stream)) {
        Token token = peek(stream);
        switch (token.type) {
            case TOKEN_AXIS_X: cmd->x = token.value; break;
            case TOKEN_AXIS_Y: cmd->y = token.value; break;
            case TOKEN_AXIS_Z: cmd->z = token.value; break;
            case TOKEN_FEEDRATE: cmd->feedrate = token.value; break;
            default: break;
        }
        next(stream); // 消费Token
    }
    return cmd;
}

// 策略注册表
ParseStrategy* strategies[256];
strategies[1] = &parseG01;  // G01 对应策略
strategies[2] = &parseG02;  // G02 对应策略
// ... 以此类推

注意: 策略模式虽然好,但不要滥用。对于M代码这种逻辑简单的指令,直接用函数指针数组就够了。过度设计也是坑,我曾经见过有人把每个M代码都写成一个类,结果光文件就建了几十个,维护起来反而更麻烦。

2.4 执行器:让指令动起来

执行器是最后一环。它拿到语法分析器生成的指令对象,然后去更新机床的状态。这里我建议用状态机来管理。

机床的状态太多了:当前位置、当前进给率、主轴状态、冷却液状态……这些状态之间还有复杂的转换关系。比如,执行换刀指令之前,必须先执行主轴停止指令。用状态机来管理这些约束,逻辑会非常清晰。

执行器的核心状态:

状态 说明 可执行的指令
IDLE 空闲状态 G00, G01, M03, M05
MOVING 正在运动 G01, G02, G03(连续插补)
SPINDLE_ON 主轴旋转中 G01, M05(停止主轴)
TOOL_CHANGE 换刀中 M06(执行换刀)
ERROR 错误状态 M30(复位)

状态机的实现,我习惯用一个二维数组来存储状态转换表。行是当前状态,列是触发事件(指令类型),值是下一个状态。这样写出来的代码,可读性极高,而且容易扩展。

// 状态转换表
State stateTable[STATE_COUNT][EVENT_COUNT] = {
    // IDLE状态
    [IDLE] = {
        [EVENT_G01] = MOVING,
        [EVENT_M03] = SPINDLE_ON,
        [EVENT_M05] = IDLE,
        [EVENT_M06] = TOOL_CHANGE,
    },
    // MOVING状态
    [MOVING] = {
        [EVENT_G01] = MOVING,  // 连续插补
        [EVENT_M05] = IDLE,    // 停止运动
    },
    // ... 其他状态
};

避坑指南: 我曾经在状态机里犯过一个低级错误——忘记处理非法状态转换。比如在主轴旋转时,直接执行换刀指令。结果就是机床报警,工件报废。所以,状态机里一定要有一个 ERROR 状态,用来处理所有不合法的转换。

2.5 设计模式的选择:为什么是策略+状态机?

你可能会问:为什么选这两个模式?其实答案很简单——因为它们正好对应了G代码解析器的两个核心问题。

  • 策略模式解决的是“如何解析”的问题。每种G代码的解析逻辑不同,策略模式让它们各司其职,互不干扰。
  • 状态机解决的是“何时执行”的问题。机床的状态变化是有严格顺序的,状态机让这些顺序变得可预测、可管理。

这两个模式组合在一起,就像一对黄金搭档。策略模式负责“横向”扩展(增加新的G代码指令),状态机负责“纵向”约束(控制执行流程)。我在实际项目中,用这套架构处理过超过200种G代码指令,代码依然清晰可维护。

嗯,这一章的内容就到这里。记住这个三模块架构,它是你后续所有修改和测试的基础。下一章,咱们会深入词法分析器的源码,一行一行地把它拆开来看。