在工业控制领域,MODBUS-RTU堪称最通用、最久经考验的通讯协议之一。它就像设备之间的“普通话”,让不同厂商的仪表、PLC和控制器能够顺畅交流。
今天,我将抛开复杂的理论,直接通过最常用的四个功能码:01、03、05、06,结合我在PIC单片机上的实战代码,带你彻底掌握MODBUS-RTU的精髓。
一、 MODBUS-RTU协议极简导读
你可以把MODBUS通讯想象成一场有序的问答:
主机(Master) 问:“1号从站,你的线圈状态什么情况?”
从站(Slave) 答:“1号从站回复,我的线圈状态是XXXX。”
协议帧格式(重中之重):
无论问与答,都遵循以下结构,这是分析和调试所有问题的根本。
|
从站地址 |
功能码 |
数据 |
CRC校验 |
|
1 Byte |
1 Byte |
N Bytes |
2 Bytes |
从站地址:0为广播地址,1-255为设备地址。
功能码:告诉从站要做什么事。
数据:请求或回复的具体内容。
CRC校验:确保数据在传输过程中没有出错(低8位在前,高8位在后)。
二、 四大核心功能码实战详解
1. 功能码 01(Read Coils)- 读取线圈状态
功能:读取一组开关量输出(DO)的状态,比如继电器的吸合/释放。
主机请求帧(例如:读取1号从站,线圈地址0x0000开始的4个线圈状态)
|
地址 |
功能码 |
起始地址高8位 |
起始地址低8位 |
数量高8位 |
数量低8位 |
CRC16 |
|
0x01 |
0x01 |
0x00 |
0x00 |
0x00 |
0x04 |
0x31CA |
从站正常回复(假设4个线圈状态为:ON, OFF, ON, OFF)
|
地址 |
功能码 |
字节数 |
数据(线圈状态) |
CRC16 |
|
0x01 |
0x01 |
0x01 |
0x05 (二进制 0000 0101) |
0xE1C8 |
注意:数据0x05的二进制为0000 0101,从最低位开始看,分别对应线圈1ON、2OFF、3ON、4OFF。若返回的线圈数不是 8 的倍数,则在最后的数据字节中的剩余位至字节的最高位全部填零,字节数区说明全部数据的字节数。
2. 功能码 05(Write Single Coil)- 强制单个线圈
功能:强制改变一个开关量输出点的状态,非常常用,比如操作某个继电器吸合或释放。
主机请求帧(强制1号从站地址为0x0002的线圈为ON)
|
地址 |
功能码 |
输出地址高8位 |
输出地址低8位 |
输出值高8位 |
输出值低8位 |
CRC16 |
|
0x01 |
0x05 |
0x00 |
0x02 |
0xFF |
0x00 |
0x8C3A |
注意:强制ON固定为0xFF00,强制OFF固定为0x0000。
从站正常回复:原样回复主机发送的指令,表示执行成功。
|
地址 |
功能码 |
输出地址高8位 |
输出地址低8位 |
输出值高8位 |
输出值低8位 |
CRC16 |
|
0x01 |
0x05 |
0x00 |
0x02 |
0xFF |
0x00 |
0x8C3A |
3. 功能码 03(Read Holding Registers)- 读取保持寄存器
功能:读取设备参数、采集到的数据等,是使用最频繁的功能码之一。例如读取温度、压力、流量等。
主机请求帧(读取1号从站,起始地址0x0000的2个寄存器)
|
地址 |
功能码 |
起始地址高8位 |
起始地址低8位 |
数量高8位 |
数量低8位 |
CRC16 |
|
0x01 |
0x03 |
0x00 |
0x00 |
0x00 |
0x02 |
0xC40B |
从站正常回复(假设两个寄存器的值分别为0x1234, 0x5678)
|
地址 |
功能码 |
字节数 |
数据1高8位 |
数据1低8位 |
数据2高8位 |
数据2低8位 |
CRC16 |
|
0x01 |
0x03 |
0x04 |
0x12 |
0x34 |
0x56 |
0x78 |
0x1FE1 |
4. 功能码 06(Write Single Register)- 预置单个寄存器
功能:修改设备的一个参数,如设定目标温度、压力上限等。
主机请求帧(将1号从站地址为0x0001的寄存器值改为0x55AA)
|
地址 |
功能码 |
寄存器地址高8位 |
寄存器地址低8位 |
数据高8位 |
数据低8位 |
CRC16 |
|
0x01 |
0x06 |
0x00 |
0x01 |
0x55 |
0xAA |
0x12E4 |
从站正常回复:同样原样回复主机指令,表示修改成功。
三、 PIC单片机MODBUS-RTU代码
下面是部分主程序:
#include<pic16f1947.h> //以PIC16F1947为例
unsigned char address_485=1;//默认从机地址为1
unsigned char recive[100]; //从机接收数据缓冲区100字节
unsigned int crc_hl; //16位两个字节校验码
void calccrc(uchar crcbuf) //校验码算法子程序
{
uchar i;
crc_hl=crc_hl^crcbuf;
for(i=0;i<8;i++)
{
uchar TT;
TT=crc_hl&1;
crc_hl=crc_hl>>1;
crc_hl=crc_hl&0x7fff;
if(TT==1)
crc_hl=crc_hl^0xa001;
crc_hl=crc_hl&0xffff;
}
}
void main()
{
unsigned char i,j;
//初始化
while(1)
{
if(x) // x为判断uart成功通讯接收一包数据的条件
{
crc_hl=0xffff;
for(i=0; i<6; i++)
{
calccrc(recive[i]);
}
if((recive[0]== address_485)&&(recive[1]==0x03)&&(recive[6]==(crc_hl&0xff))&&(recive[7]==(crc_hl>>8))) //判断地址、功能码及校验码
{
//1、判断寄存器地址及填写要回复的数据
//2、校验码计算
//3、把要回复的数据及校验码发送给主机
//如果是功能码05、06直接原样把数据发送给主机,不需要2这两步
}
}
}
}
MODBUS-RTU物理层通常采用RS-485。一个稳定可靠的RS-485电路是通讯的保障。
下图是我一直使用的RS485接口电路图
具体介绍请参考我的另一篇文章:深入解析RS-232、RS-485、RS422总线与实战防护电路

掌握01、03、05、06这四个核心功能码,你就能解决80%以上的MODBUS-RTU应用场景。协议本身不难,关键在于严谨的实现和对现场干扰的防护。
如果你对这篇文章感兴趣或有什么问题,欢迎在评论区留言
后续干货不断,咱们一起在单片机的世界里,共同进步。
675

被折叠的 条评论
为什么被折叠?



