4、命令解析引擎:Query/ACK/Read命令处理、CRC校验实现、状态机设计

好,咱们接着聊。上一章我们把物理层和数据链路层打通了,现在数据包能在线上跑了。但有个关键问题——芯片收到一串字节,它怎么知道对方想干嘛?是问我状态,还是让我读数据,还是确认我发的东西收到了?

这就是命令解析引擎要做的事。说白了,它就是个翻译官,把收到的二进制流翻译成具体的操作指令。我个人习惯把这块叫做「协议的大脑」,因为所有业务逻辑都从这里分发出去。

4.1 命令集设计:Query、ACK、Read

先看看我们常用的三种命令。在电子标签协议里,命令类型通常用一个字节的CMD字段来标识。我见过不少新手把命令码定义得乱七八糟,结果后期维护想死的心都有。

命令码 命令名称 方向 说明
0x01 Query 读写器 → 标签 查询标签状态/信息
0x02 ACK 标签 → 读写器 确认收到,携带状态
0x03 Read 读写器 → 标签 读取指定存储区数据

你可能会问:「就三个命令?够用吗?」嗯,实际产品里当然不止这些,但核心逻辑就这三板斧。Query负责握手,ACK负责应答,Read负责数据传输。其他命令比如Write、Lock,都是在Read的基础上扩展的。

核心思路:命令解析引擎只关心「是什么命令」和「参数对不对」,不关心「命令怎么执行」。这样设计,引擎和业务逻辑就解耦了。

4.2 CRC校验实现:别让数据「带病上岗」

数据在无线传输中,受干扰是家常便饭。我遇到过最坑的一次,标签返回的ACK包因为一个比特翻转,读写器误判成Read命令,结果去读了一个不存在的地址,整个系统卡死了。

从那以后,我对CRC校验格外上心。咱们用CRC-16/CCITT,多项式是 0x1021。为什么选这个?因为它在嵌入式平台上查表法实现快,而且检错能力够用。

直接上代码,这是我项目里一直在用的查表法实现:

// CRC-16/CCITT 查表法
static const uint16_t crc16_table[256] = {
    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
    // ... 中间省略,实际项目要完整256项
};

uint16_t calc_crc16(const uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;  // 初始值
    for (uint16_t i = 0; i < len; i++) {
        crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF];
    }
    return crc;
}

小技巧:CRC初始值用0xFFFF而不是0x0000,可以检测出前导零的丢失。这是我在一次现场调试中发现的坑——空口环境差的时候,前导字节经常被吞掉。

校验流程很简单:发送方计算CRC并附加到数据包尾部,接收方对整包(包括CRC字段)重新计算,如果结果为0,说明数据完好。嗯,这里要注意,有些协议把CRC放在包头,有些放在包尾,我个人习惯放包尾,因为解析时可以边收边算,不用等收完再回头算。

4.3 状态机设计:让芯片知道「自己在哪」

命令解析引擎的核心,其实是一个有限状态机。标签芯片不可能同时处理多个命令,它必须清楚自己当前处于什么状态,能响应什么命令。

我设计的状态机一般分四个状态:

  • IDLE(空闲态):芯片上电或复位后的默认状态。只响应Query命令。
  • QUERIED(已查询态):收到Query后进入,等待读写器进一步操作。可以响应Read命令。
  • ACKED(已确认态):Read命令执行完毕,发送ACK确认。之后回到IDLE。
  • ERROR(错误态):CRC校验失败或命令格式错误时进入。需要复位才能恢复。

状态转移图用文字描述就是:

IDLE --(收到Query)--> QUERIED
QUERIED --(收到Read)--> ACKED
ACKED --(发送ACK完成)--> IDLE
任何状态 --(CRC错误)--> ERROR
ERROR --(复位)--> IDLE

注意:千万不要在状态机里做耗时操作!比如Read命令要访问Flash,如果直接在状态机里等Flash读完,整个协议栈就卡死了。正确做法是:状态机只负责「发起操作」,操作完成后由中断或回调通知状态机跳转。

我曾经在一个项目里犯过这个错。标签读Flash需要5ms,我直接在状态机里用while循环等,结果读写器那边超时重发了三次,标签还在那傻等。后来改成「发起读操作→切回IDLE→读完成中断触发→切到ACKED」,问题就解决了。

4.4 命令解析的完整流程

把上面三块串起来,一个完整的命令解析流程是这样的:

  1. 物理层收完一包数据,触发回调。
  2. 引擎先做CRC校验。如果失败,直接丢弃,状态机切到ERROR。
  3. 校验通过,解析命令码(CMD字段)。
  4. 根据当前状态机状态,判断该命令是否合法。比如IDLE状态下收到Read,直接返回错误。
  5. 命令合法,执行对应的处理函数(QueryHandler、ReadHandler等)。
  6. 处理完成后,状态机按规则跳转。

你看,逻辑其实不复杂。但实际工程里,坑往往出在细节上。比如CRC校验的边界条件、状态机在异常情况下的恢复、多命令并发时的互斥……这些都需要在代码里一点点打磨。

我个人习惯在状态机里加一个「超时看门狗」——如果某个状态停留超过规定时间(比如10ms),自动复位到IDLE。这招救过我很多次,尤其是在无线环境恶劣的时候。

好了,命令解析引擎的核心就这些。下一章我们聊聊数据缓冲区管理和内存优化,那又是另一片战场了。