从0开始Modbus RTU通信到编程实战(4)

一、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的分享

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咸鱼真闲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值