移 0:此字节为从机设备地址,同上述描述。.
偏移1:此字节为功能码,同上述描述。.
偏移2:此2字节为要写入的寄存器首地址,同上述描述。.
偏移4:此2字节为已经写入的寄存器个数,0x0004表示已经写入4个寄存器。.
最后两个字节:CRC16校验值0x33C0,低字节在前,高字节在后(算法与电脑通用,小端模式)。
如果命令解析错误,则从机返回错误帧:
数据功能:地址错误码CRC16偏移字节:
0 1 最后 2 字节。
字节数量:1 byte1 byte 2 byte
01 83 404D
梁工详细介绍了CRC校验:
//========================================================================
// 函数: u16 MODBUS_CRC16(u8 *p, u8 n)
// 描述: 计算CRC16函数.
// 参数: *p: 要计算的数据指针.
// n: 要计算的字节数.
// 返回: CRC16值.
// 版本: V1.0, 2022-3-18 梁工
//========================================================================
u16 MODBUS_CRC16(u8 *p, u8 n)
{
u8 i;
u16 crc16;
crc16 = 0xffff; //预置16位CRC寄存器为0xffff(即全为1)
do
{
crc16 ^= (u16)*p; //把8位数据与16位CRC寄存器的低位相异或,把结果放于CRC寄存器
for(i=0; i<8; i++) //8位数据
{
if(crc16 & 1) crc16 = (crc16 >> 1) ^ 0xA001; //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位,
//再异或多项式0xA001
else crc16 >>= 1; //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位
}
p++;
}while(--n != 0);
return (crc16);
}
重点还是ModBus 协议的解析函数:
/********************* modbus协议 *************************/
/***************************************************************************
写多寄存器
数据: 地址 功能码 寄存地址 寄存器个数写入字节数 写入数据 CRC16
偏移: 0 1 2 3 4 5 6 7~ 最后2字节
字节: 1 byte 1 byte 2 byte 2 byte 1byte 2*n byte 2 byte
addr 0x10 xxxx xxxx xx xx....xx xxxx
返回
数据: 地址 功能码 寄存地址 寄存器个数 CRC16
偏移: 0 1 2 3 4 5 6 7
字节: 1 byte 1 byte 2 byte 2 byte 2 byte
addr 0x10 xxxx xxxx xxxx
读多寄存器
数据:站号(地址)功能码 寄存地址 寄存器个数CRC16
偏移: 0 1 2 3 4 5 6 7
字节: 1 byte1 byte 2 byte 2 byte 2 byte
addr 0x03 xxxx xxxx xxxx
返回
数据:站号(地址)功能码 读出字节数读出数据CRC16
偏移: 0 1 2 3~ 最后2字节
字节: 1 byte 1 byte 1byte 2*n byte2 byte
addr 0x03 xx xx....xx xxxx
返回错误代码
数据:站号(地址)错误码 CRC16
偏移: 0 1 最后2字节
字节: 1 byte 1 byte 2 byte
addr 0x93 xxxx
***************************************************************************/
u8 MODBUS_RTU(void)
{
u8 i,j,k;
u16 reg_addr; //寄存器地址
u8 reg_len; //写入寄存器个数
u16 crc;
if(RX1_Buffer == 0x10) //写多寄存器
{
if(RX1_cnt < 9) return 0x91; //命令长度错误
if((RX1_Buffer != 0) || ((RX1_Buffer *2) != RX1_Buffer)) return 0x92; //写入寄存器个数与字节数错误
if((RX1_Buffer==0) || (RX1_Buffer > REG_LENGTH)) return 0x92; //写入寄存器个数错误
reg_addr = ((u16)RX1_Buffer << 8) + RX1_Buffer; //寄存器地址
reg_len = RX1_Buffer; //写入寄存器个数
if((reg_addr+(u16)RX1_Buffer) > (REG_ADDRESS+REG_LENGTH)) return 0x93; //寄存器地址错误
if(reg_addr < REG_ADDRESS) return 0x93; //寄存器地址错误
if((reg_len*2+7) != RX1_cnt) return 0x91; //命令长度错误
j = reg_addr - REG_ADDRESS; //寄存器数据下标
for(k=7, i=0; i<reg_len; i++,j++)
{
modbus_reg = ((u16)RX1_Buffer << 8) + RX1_Buffer; //写入数据, 大端模式
k += 2;
}
if(RX1_Buffer != 0) //非广播地址则应答
{
for(i=0; i<6; i++) TX1_Buffer = RX1_Buffer; //要返回的应答
crc = MODBUS_CRC16(TX1_Buffer, 6);
TX1_Buffer = (u8)crc; //CRC是小端模式, 先发低字节,后发高字节。
TX1_Buffer = (u8)(crc>>8);
B_TX1_Busy = 1; //标志发送忙
TX1_cnt = 0; //发送字节计数
TX1_number = 8; //要发送的字节数
TI = 1; //启动发送
}
}
else if(RX1_Buffer == 0x03) //读多寄存器
{
if(RX1_Buffer != 0) //非广播地址则应答
{
if(RX1_cnt != 6) return 0x91; //命令长度错误
if(RX1_Buffer != 0) return 0x92; //读出寄存器个数错误
if((RX1_Buffer==0) || (RX1_Buffer > REG_LENGTH)) return 0x92; //读出寄存器个数错误
reg_addr = ((u16)RX1_Buffer << 8) + RX1_Buffer; //寄存器地址
reg_len = RX1_Buffer; //读出寄存器个数
if((reg_addr+(u16)RX1_Buffer) > (REG_ADDRESS+REG_LENGTH)) return 0x93; //寄存器地址错误
if(reg_addr < REG_ADDRESS) return 0x93; //寄存器地址错误
j = reg_addr - REG_ADDRESS; //寄存器数据下标
TX1_Buffer = SL_ADDR; //站号地址
TX1_Buffer = 0x03; //读功能码
TX1_Buffer = reg_len*2; //返回字节数
for(k=3, i=0; i<reg_len; i++,j++)
{
TX1_Buffer = (u8)(modbus_reg >> 8); //数据为大端模式
TX1_Buffer = (u8)modbus_reg;
}
crc = MODBUS_CRC16(TX1_Buffer, k);
TX1_Buffer = (u8)crc; //CRC是小端模式, 先发低字节,后发高字节。
TX1_Buffer = (u8)(crc>>8);
B_TX1_Busy = 1; //标志发送忙
TX1_cnt = 0; //发送字节计数
TX1_number = k; //要发送的字节数
TI = 1; //启动发送
}
}
else return 0x90; //功能码错误
return 0; //解析正确
}
主要是合法性检测,对收到的数据进行解析
应答之前,先检测是否是广播地址:
if(RX1_Buffer != 0) //非广播地址则应答
{
for(i=0; i<6; i++) TX1_Buffer = RX1_Buffer; //要返回的应答
crc = MODBUS_CRC16(TX1_Buffer, 6);
TX1_Buffer = (u8)crc; //CRC是小端模式, 先发低字节,后发高字节。
TX1_Buffer = (u8)(crc>>8);
B_TX1_Busy = 1; //标志发送忙
TX1_cnt = 0; //发送字节计数
TX1_number = 8; //要发送的字节数
TI = 1; //启动发送
}
这个协议好处很明显:简单高效,容易理解:
看看梁工的说明:
本例程只支持多寄存器读和多寄存器写, 寄存器长度为64个, 别的命令用户可以根据需要按MODBUS-RTU协议自行添加.
本例子数据使用大端模式(与C51一致), CRC16使用小端模式(与PC一致).
默认参数:
串口1设置均为 1位起始位, 8位数据位, 1位停止位, 无校验.
串口1(P3.0 P3.1): 9600bps.
定时器0用于超时计时. 串口每收到一个字节都会重置超时计数, 当串口空闲超过35bit时间时(9600bps对应3.6ms)则接收完成.
用户修改波特率时注意要修改这个超时时间.
本例程只是一个应用例子, 科普MODBUS-RTU协议并不在本例子职责范围, 用户可以上网搜索相关协议文本参考.
本例定义了64个寄存器, 访问地址为0x1000~0x103f.
命令例子:
写入4个寄存器(8个字节):
10 10 1000 0004 08 1234 5678 90AB CDEF 4930
返回:
10 10 10 00 00 04 C64B
读出4个寄存器:
10 03 1000 0004 4388
返回:
10 03 08 12 34 56 78 90 AB CD EF D53D
命令错误返回信息(自定义):
0x90: 功能码错误. 收到了不支持的功能码.
0x91: 命令长度错误.
0x92: 写入或读出寄存器个数或字节数错误.
0x93: 寄存器地址错误.
注意: 收到广播地址0x00时要处理信息, 但不返回应答.
可以用DMA发送和接收
放进入自动发送接收
3.5个字符间隔的意思的,如何区分2帧数据!
好了 梁工的ModBus 协议课程到此结束!!!
下一次我们学习:串口库函数、串口DMA,USB-CDC
感谢梁工的辛苦劳动和无私奉献,讲所有源码共享给大家!感谢感谢感谢!!!