一、Modbus RTU通信流程
如果在一个正确接收的 MODBUS ADU 中,不出现与请求 MODBUS 功能有关的差错,那么服务器至客户机的响应数据域包括请求数据。如果出现与请求 MODBUS 功能有关的差错,那么域包括一个异常码,服务器应用能够使用这个域确定下一个执行的操作。
例如,客户机能够读一组离散量输出或输入的开/关状态,或者客户机能够读/写一组寄存器的数据内容。
当服务器对客户机响应时,它使用功能码域来指示正常(无差错)响应或者出现某种差错(称为异常响应)。对于一个正常响应来说,服务器仅对原始功能码响应。
对于异常响应,服务器返回一个与原始功能码等同的码,设置该原始功能码的最高有效位为逻辑 1。
主机上MODBUS 事务处理的一般处理过程,程序大致的框架也是如此。
一旦服务器处理请求,使用合适的 MODBUS 服务器事务建立 MODBUS 响应。根据处理结果,可以建立两种类型响应:
(1)一个正 MODBUS 响应:响应功能码=请求功能码
(2)一个MODBUS 异常响应:用来为客户机提供处理过程中与被发现的差错相关的信息;响应功能码 = 请求功能码 +0x80;提供一个异常码来指示差错原因。
传输模式状态图:
主节点状态图:
二、校验和异常处理
1、校验码
modbus rtu中一般使用循环冗余校验(CRC),循环冗余校验(CRC)字段为两个字节,包含一个二进制16位值,发送设备计算CRC值,将CRC值附加到报文中,在接收报文过程中,接收设备重新计算CRC值,并将计算值与接收到的CRC字段中实际值相比较,如果两个值不相等,则说明报文有错误。
通过对一个16位寄存器预装载全“1"来启动CRC计算,然后开始将报文中的后续8位字节与当前寄存器中的内容进行计算,只有每个字符中的8个数据位参与生成CRC的计算,起始位,停止位和校验
位不参与CRC 计算。
在生成CRC过程中,每个8位字符与寄存器中的值异或,然后,向最低有效位(1LSB)方向移动这个结果,而用零填充最高有效位(MSB),提取并检查LSB,如果LSB为1,则寄存器中的值与一个固定的预置值异或:如果LSB为0,则不进行异或操作。
这个过程将重复直到执行完8次移位、完成最后一次(第8次)移位之后,下一个8位字节与寄存器的当前值异或,然后像上述描述的那样重复8次这个过程,在已经计算报文中所有字节之后,寄存器的最终值就是CRC。
更详细的解释参考:Modbus-RTU通讯协议中CRC校验_modbus crc-CSDN博客
关于CRC校验,可以不用完全弄懂,只需要会用就行可以。
CRC校验码的生成有两种方法:查表法和计算法。
程序源码参考:MODBUS RTU 通信协议 CRC16校验算法-CSDN博客
《GBT 19582.1-2008 基于Modbus协议的工业自动化网络规范》的最后几页也有CRC说明。
2、异常处理
Modbus通信有正常响应和异常响应两种,异常响应从机会返回对应的错误码,告诉主机问题所在,而主机也要根据异常码做出相应的处理,最简单的便是报告错误。
各个异常码的含义:
从Modbus事务处理的状态图中可以看到,比较常见的是01——功能码错误,02——寄存器地址错误,03——数据错误,04——校验错误。
我们以Modbus RTU协议为例,地址码为0x01,写操作0x10,寄存器地址为0x018E,CRC校验。如寄存器可读写的话,返回正常,如寄存器只读,返回异常。
下发指令:01 10 01 8E 00 01 02 00 00 69 BE(向寄存器0x018E写入一个数值为0的数据)
正确回应指令:01 10 01 8E 00 01 60 1E(向寄存器地址0x018E写操作一个寄存器)
错误回应指令:01 90 01 8D C0(写操作非法功能,可能是向输入寄存器写数据)
三、51单片机上的主机程序
51单片机上的程序来源于:
《手把手教你学51单片机:第18章 RS485 通信与 Modbus 协议 》——宋雪松
从之前的文章中我们了解到,modbus RTU通信是一种串行通信,一般建立在RS485基础上。因此,需要先弄懂RS485通信程序。
#include <reg52.h>
#include <intrins.h>
sbit RS485_DIR = P1^7; //RS485方向选择引脚
bit flagFrame = 0; //帧接收完成标志,即接收到一帧新数据
bit flagTxd = 0; //单字节发送完成标志,用来替代TXD中断标志位
unsigned char cntRxd = 0; //接收字节计数器
unsigned char pdata bufRxd[64]; //接收字节缓冲区
extern void UartAction(unsigned char *buf, unsigned char len);
/* 串口配置函数,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
RS485_DIR = 0; //RS485设置为接收方向
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud; //计算T1重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动T1
}
/* 软件延时函数,延时时间(t*10)us */
void DelayX10us(unsigned char t)
{
do {
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
} while (--t);
}
/* 串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度 */
void UartWrite(unsigned char *buf, unsigned char len)
{
RS485_DIR = 1; //RS485设置为发送
while (len--) //循环发送所有字节
{
flagTxd = 0; //清零发送标志
SBUF = *buf++; //发送一个字节数据
while (!flagTxd); //等待该字节发送完成
}
DelayX10us(5); //等待最后的停止位完成,延时时间由波特率决定
RS485_DIR = 0; //RS485设置为接收
}
/* 串口数据读取函数,buf-接收指针,len-指定的读取长度,返回值-实际读到的长度 */
unsigned char UartRead(unsigned char *buf, unsigned char len)
{
unsigned char i;
if (len > cntRxd) //指定读取长度大于实际接收到的数据长度时,
{ //读取长度设置为实际接收到的数据长度
len = cntRxd;
}
for (i=0; i<len; i++) //拷贝接收到的数据到接收指针上
{
*buf++ = bufRxd[i];
}
cntRxd = 0; //接收计数器清零
return len; //返回实际读取长度
}
/* 串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔 */
void UartRxMonitor(unsigned char ms)
{
static unsigned char cntbkp = 0;
static unsigned char idletmr = 0;
if (cntRxd > 0) //接收计数器大于零时,监控总线空闲时间
{
if (cntbkp != cntRxd) //接收计数器改变,即刚接收到数据时,清零空闲计时
{
cntbkp = cntRxd;
idletmr = 0;
}
else //接收计数器未改变,即总线空闲时,累积空闲时间
{
if (idletmr < 30) //空闲计时小于30ms时,持续累加
{
idletmr += ms;
if (idletmr >= 30) //空闲时间达到30ms时,即判定为一帧接收完毕
{
flagFrame = 1; //设置帧接收完成标志
}
}
}
}
else
{
cntbkp = 0;
}
}
/* 串口驱动函数,监测数据帧的接收,调度功能函数,需在主循环中调用 */
void UartDriver()
{
unsigned char len;
unsigned char pdata buf[40];
if (flagFrame) //有命令到达时,读取处理该命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf)-2); //将接收到的命令读取到缓冲区中
UartAction(buf, len); //传递数据帧,调用动作执行函数
}
}
/* 串口中断服务函数 */
void InterruptUART() interrupt 4
{
if (RI) //接收到新字节
{
RI = 0; //清零接收中断标志位
if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
{ //保存接收字节,并递增计数器
bufRxd[cntRxd++] = SBUF;
}
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
flagTxd = 1; //设置字节发送完成标志
}
}
下图是笔者的理解。
该程序包含 lcd1602.c、CRC16.c、RS485.c、main.c,程序比较多,在这里就不贴出来了,最后会给出源码链接。
四、32单片机上的主机程序
该程序主要参考:STM32F103 modbus-RTU RS485源码-OpenEdv-开源电子网
笔者对上述程序进行了完善和改进,删掉了不必要的代码,能够支持01、02、03、04、15、16功能码,下图是笔者的理解。
测试:
在程序中设置好参数,编译下载至单片机,这里是STM32F103ZET6。
打开串口调试助手,波特率9600.
打开Modbus Slave,这里是16进制的2!!!
看清端口,不要和串口调试助手的弄混了,还是9600波特率,这个可以在程序中改
连接从机软件后,通信正常
通信异常的时候,改变从机功能码
改变从机地址
改变寄存器地址
另外,程序中CRC也是随机的,不会报CRC校验错误
五、温湿度传感器实例
笔者使用兆泰盛温湿度传感器和程序进行了测试,主要变动的地方是刚开始的参数设置、CRC校验部分以及增加了03功能码接收的数据处理程序。
在兆泰盛温湿度传感器手册中,可以找到:
按照要求更改程序:
void Master_Service(u8 SlaverAddr,u16 Fuction,u16 StartAddr,u16 ValueOrLenth)
{
// u16 calCRC;
u8 i=0;
RS485_TX_BUFF[0] = SlaverAddr;
RS485_TX_BUFF[1] = Fuction; //modbus 指令码
RS485_TX_BUFF[2] = HI(StartAddr);
RS485_TX_BUFF[3] = LOW(StartAddr);
RS485_TX_BUFF[4] = HI(ValueOrLenth);
RS485_TX_BUFF[5] = LOW(ValueOrLenth);
// calCRC=CRC_Compute(RS485_TX_BUFF,6); //crc校验码生成
// RS485_TX_BUFF[6]=(calCRC>>8)&0xFF;
// RS485_TX_BUFF[7]=(calCRC)&0xFF;
RS485_TX_BUFF[6]=0xC4;
RS485_TX_BUFF[7]=0x0B;
RS485_SendData(RS485_TX_BUFF,8);
printf("TX: ");
for(i=0;i<8;i++)
{
printf("%.2X ", RS485_TX_BUFF[i]);
}
}
void Modbus_03_Solve(void)
{
if(1)//寄存器地址+数量在范围内
{
int i;
int count=(int)RS485_RX_BUFF[2];//这是数据个数
//printf("从机返回 %d 个寄存器数据:\r\n",count/2);
for(i=0;i<count;i=i+2)
{
humidity=(int)RS485_RX_BUFF[4+i]+((int)RS485_RX_BUFF[3+i])*256;
temperature=(int)RS485_RX_BUFF[6+i]+((int)RS485_RX_BUFF[5+i])*256;
printf("湿度 = %.2f 温度 = %.2f \n",humidity/10,temperature/10);
printf("\n");
}
}
else
{
ComErr=3;
}
TX_RX_SET=0; //命令完成
}
测试:
下载:
链接:https://pan.baidu.com/s/1eHrXhqBvI8p2MpTnXOPh9g?pwd=6666
提取码:6666
--来自百度网盘超级会员V6的分享