3、核心通信协议(一):Modbus RTU 协议原理、寄存器映射、CRC校验

各位工程师朋友,咱们今天聊聊充电桩的“神经系统”——Modbus RTU协议。说实话,我入行那会儿,第一次调试充电桩,就是被这个协议给“教育”了一顿。当时桩子死活连不上后台,我折腾了一整天,最后发现是CRC校验码算错了。嗯,从那以后,我对这个协议就格外上心。

Modbus RTU在充电桩领域,可以说是“老黄牛”级别的存在。它简单、可靠、成本低,很多充电桩的主控板与充电模块之间,用的就是它。你想想看,一个充电桩里,可能有四五个充电模块,每个模块都要实时上报电压、电流、温度,主控板还要下发指令控制输出。这么多数据,全靠Modbus RTU在背后默默传递。

3.1 协议原理:主从问答,简单粗暴

Modbus RTU的核心思想,说白了就是“一问一答”。主设备(比如充电桩主控板)发出请求,从设备(比如充电模块)收到后,必须回复。没有主设备问,从设备绝对不能主动说话。这个规矩,我在项目中遇到过好几次有人想打破,结果就是总线冲突,数据全乱套。

一个完整的Modbus RTU报文,长这样:

| 地址码 (1字节) | 功能码 (1字节) | 数据区 (N字节) | CRC校验 (2字节) |

举个例子,主控板想读取1号充电模块的当前输出电压(寄存器地址0x0001),它会发:

01 03 00 01 00 01 CRC_H CRC_L

这里:

  • 01:从设备地址,就是1号模块
  • 03:功能码,表示“读取保持寄存器”
  • 00 01:起始寄存器地址,从0x0001开始读
  • 00 01:读取1个寄存器
  • CRC_H CRC_L:校验码,后面细说

如果模块正常,它会回复:

01 03 02 0B B8 CRC_H CRC_L

其中02表示数据长度是2字节,0B B8就是电压值(十进制3000,代表300.0V)。

个人经验:我建议你在设计报文时,把地址码和功能码单独定义成宏。比如#define MODBUS_ADDR_CHARGER_MODULE_1 0x01。这样后期维护代码,一眼就能看懂,不用去翻文档。

3.2 寄存器映射:充电桩的“内存地图”

寄存器映射,就是给每个数据分配一个固定的“门牌号”。主控板要知道,想读电压就去0x0001,想读电流就去0x0002。这个映射表,是充电桩厂家必须提供的核心文档。

我见过最坑的一次,是某厂家给的映射表里,把电压和电流的地址写反了。结果现场调试,主控板读到的“电压”其实是电流值,差点把充电模块烧了。所以,拿到映射表后,第一件事就是拿万用表实测验证。

一个典型的充电模块寄存器映射表,大概长这样:

寄存器地址 数据类型 描述 单位/说明
0x0000 U16 模块状态 0=待机, 1=运行, 2=故障
0x0001 U16 输出电压 0.1V/bit, 如3000=300.0V
0x0002 U16 输出电流 0.1A/bit, 如500=50.0A
0x0003 U16 模块温度 0.1℃/bit, 如450=45.0℃
0x0010 U16 输出电压设定值 写操作, 0.1V/bit
0x0011 U16 输出电流设定值 写操作, 0.1A/bit
避坑指南:我曾经遇到过,某个模块的寄存器地址是“0-based”还是“1-based”没写清楚。比如文档说“电压寄存器地址是1”,但实际代码里要从0x0000开始算。这种歧义,一定要在协议设计阶段就明确。我现在的习惯是,所有寄存器地址都从0x0000开始,并且在文档里用表格明确标出。

3.3 CRC校验:数据完整性的“守门员”

CRC校验,是Modbus RTU协议里最容易被忽视,但又最关键的部分。为什么?因为充电桩工作环境恶劣,电磁干扰大。没有CRC,你收到的数据可能早就被“篡改”了。

CRC-16 Modbus的算法,其实不复杂。我直接给你看代码,这是我用了好几年的实现,没出过问题:

// 计算Modbus RTU CRC16
// 输入:数据指针,数据长度
// 输出:16位CRC值
uint16_t Modbus_CRC16(uint8_t *data, uint16_t len)
{
    uint16_t crc = 0xFFFF;  // 初始值
    uint16_t i, j;
    
    for (i = 0; i < len; i++)
    {
        crc ^= data[i];  // 异或数据字节
        for (j = 0; j < 8; j++)
        {
            if (crc & 0x0001)  // 检查最低位
            {
                crc >>= 1;
                crc ^= 0xA001;  // 多项式
            }
            else
            {
                crc >>= 1;
            }
        }
    }
    
    // 注意:Modbus RTU发送时,低字节在前,高字节在后
    return crc;
}

这段代码的核心逻辑,就是逐字节异或,然后逐位处理。多项式0xA001是Modbus标准规定的,不能改。

关键点:发送CRC时,一定要先发低字节,再发高字节。比如CRC算出来是0x1234,那么报文里应该是0x34 0x12。这个顺序搞反了,接收端校验就会失败。我刚开始做的时候,就栽在这个细节上。

接收端怎么校验?很简单:把收到的整个报文(包括CRC本身)重新算一遍CRC。如果结果是0,说明数据没问题。否则,直接丢弃,请求重发。

我的习惯:在调试阶段,我会在代码里加一个CRC校验失败的计数器。如果这个计数器增长很快,说明通信链路有问题,要么是干扰大,要么是波特率不匹配。有一次,我就是靠这个计数器,发现某个充电模块的晶振偏差太大,导致波特率漂移。

好了,关于Modbus RTU的核心三要素——协议原理、寄存器映射、CRC校验,我就讲到这里。下一节,咱们聊聊Modbus TCP,看看它和RTU有什么不同,以及怎么在充电桩远程运维中选型。