4. AT指令解析引擎:AT指令格式、响应解析、URC数据处理

AT指令这东西,说白了就是嵌入式世界里最古老的“对话协议”。我最早接触AT指令是在2008年做GPRS数传终端那会儿,一个简单的“AT+CSQ”查信号强度,愣是被各种模组的奇葩响应折腾了好几天。后来我悟出一个道理:AT指令解析引擎的核心不是发指令,而是收响应。

今天咱们就把这个引擎拆开看看。你想想看,一个模组在你面前,你发一条指令,它可能回OK,可能回ERROR,可能回一堆数据,还可能在你毫无防备的时候突然吐出一行URC——这玩意儿怎么处理?

4.1 AT指令的“标准”与“不标准”

先说说指令格式。理论上,AT指令遵循一套标准:

指令类型 格式示例 说明
测试指令 AT+<参数>=? 查询参数取值范围
查询指令 AT+<参数>? 查询当前参数值
设置指令 AT+<参数>=<值> 设置参数
执行指令 AT+<命令> 执行特定操作

但现实很骨感。我在项目中遇到过一款NB-IoT模组,它的AT+CGATT?查询指令返回的不是标准的+CGATT: 1,而是+CGATT:1——少了个空格。还有的模组在OK前面加了个换行,有的在ERROR后面跟了一串调试信息。

注意:千万不要假设模组会100%遵循标准。我见过最离谱的案例,某模组在返回长数据时,中间会随机插入一个\r\n把数据包拆成两段。如果你的解析器是按“收到完整一行再处理”的逻辑写的,那就会死得很惨。

4.2 响应解析:状态机才是王道

解析AT响应,我个人习惯用状态机。为什么?因为串口数据是流式的,你不知道下一字节什么时候来,也不知道一次中断会收到多少字节。用状态机可以做到来一字节处理一字节,不依赖“完整行”的假设。

我常用的状态机设计是这样的:

// 状态定义
typedef enum {
    AT_STATE_IDLE,          // 空闲,等待指令开始
    AT_STATE_HEADER,        // 正在接收响应头(如 +CSQ:)
    AT_STATE_DATA,          // 正在接收数据体
    AT_STATE_TERMINATOR,    // 等待结束符(\r\n)
    AT_STATE_COMPLETE,      // 响应接收完成
    AT_STATE_TIMEOUT        // 超时
} at_parse_state_t;

// 核心解析函数(每收到一个字节调用一次)
at_parse_state_t at_parse_byte(uint8_t byte) {
    switch (g_state) {
        case AT_STATE_IDLE:
            if (byte == '\r' || byte == '\n') {
                // 跳过空行
                break;
            }
            g_state = AT_STATE_HEADER;
            // 开始缓存数据
            break;
            
        case AT_STATE_HEADER:
            if (byte == '\r') {
                // 头部结束,准备检查
                g_state = AT_STATE_TERMINATOR;
            } else {
                // 继续缓存
            }
            break;
            
        case AT_STATE_TERMINATOR:
            if (byte == '\n') {
                // 收到完整一行
                g_state = AT_STATE_COMPLETE;
            } else {
                // 异常情况,回退
                g_state = AT_STATE_IDLE;
            }
            break;
    }
    return g_state;
}
经验之谈:状态机里一定要加超时处理。我习惯在发送指令后启动一个500ms的定时器,如果超时还没收到OK/ERROR,就强制重置状态机并报超时错误。有些模组在信号差的时候,响应可能会延迟到1-2秒。

4.3 URC数据处理:那个不请自来的“客人”

URC(Unsolicited Result Code)是AT指令解析里最头疼的部分。你正美滋滋地发指令收响应呢,模组突然冒出一行+CMTI: "SM",3——来短信了。如果你的解析器还在傻等OK,那这行URC就会被当成响应的一部分吃掉,导致解析失败。

怎么处理?我的方案是双缓冲 + 优先级判断:

  1. 串口中断收到的数据先进入一个环形缓冲区
  2. 解析线程从缓冲区取数据,逐字节喂给状态机
  3. 每收到完整一行,先判断是不是URC(通过前缀匹配)
  4. 如果是URC,直接扔到URC队列,由专门的回调函数处理
  5. 如果不是URC,才交给当前指令的响应解析器

这里有个关键点:URC可能出现在任何位置。比如你发AT+CMGS发短信,指令还没发完呢,模组可能先回了个+CME ERROR: 3。所以URC检测必须在状态机的每一行都做,不能只在空闲态做。

常见URC类型及处理建议:

  • +CMTI / +CDSI:新短信/新状态报告 → 触发短信读取流程
  • +CREG / +CGREG:网络注册状态变化 → 更新网络状态标志
  • +CSQ:信号强度上报(某些模组会周期性上报)→ 更新信号强度缓存
  • RING:来电指示 → 触发接听或挂断逻辑
  • +IPD:TCP数据到达(常见于GPRS模组)→ 触发数据接收回调

4.4 实战:一个完整的AT指令交互流程

咱们画个图,把整个流程串起来。这是我在一个4G Cat.1模组项目里实际用过的架构:

AT指令解析引擎架构图 应用层任务 AT指令队列 指令发送器 串口驱动层 无线模组 环形缓冲区 字节状态机 URC? URC回调队列 响应缓存器 关键设计原则 • 每字节独立处理,不依赖行缓冲 • URC检测在每一行都执行 • 发送与接收异步解耦 • 超时机制保护状态机

这个架构的核心思想是发送和接收完全异步。应用层只管往指令队列里扔指令,发送器按顺序一条条发。接收端不管发没发指令,一直在收数据、解析、分流。URC来了直接回调,响应来了缓存起来等应用层来取。

4.5 避坑指南

最后分享几个我踩过的坑:

我曾经遇到过一个坑:某款4G模组在TCP连接断开时,会连续发送3行URC:+IPCLOSE: 0、空行、CLOSED。如果解析器只处理了第一行就认为处理完了,后面两行就会污染下一个指令的响应。解决方案是:URC回调里设置一个“消化期”,收到URC后继续读取直到遇到非URC行或超时。

另一个经验:AT指令的响应超时时间不能一刀切。AT+CSQ这种简单查询,200ms就够了。但AT+CGATT(附着网络)可能耗时5-10秒。我习惯在指令结构体里加一个expected_timeout_ms字段,不同指令用不同的超时值。

嗯,AT指令解析引擎的核心就这些。说白了就是:别信标准,用状态机,防URC,加超时。把这四点做到位了,市面上99%的模组你都能搞定。