RS485通信与Modbus协议


RS485通信与Modbus协议

      在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的电磁干扰,会导致信号传输错误。除此之外,RS232接口只能实现点对点通信,不具备联网功能,最大传输距离也只能达到十几米,不能满足远距离通信要求。而RS485则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最大距离可达1200米,并且允许多个收发设备接到同一条总线上。随着工业应用通信越来越多,1979年施耐德电气制定了一个用于工业现场的总线协议Modbus协议,现在工业中使用RS485通信场合很多都采用Modbus协议,本节课我们就来讲解一下RS485通信和Modbus协议。


      单单使用一块KST-51开发板是不能够进行RS485实验的,应很多同学的要求,把这节课作为扩展课程讲一下,如果要做本课相关实验,需要自行购买USBRS485通信模块,或连接其它的RS485主控设备进行


1.1 RS485通信

RS232标准是诞生于RS485之前的,但是RS232有几处不足的地方:

1接口的信号电平值较高,达到十几V使用不当容易损坏接口芯片,电平标准也与TTL电平不兼容。

2传输速率有局限,不可以过高,一般到一两百千比特每秒(Kb/s)就到极限了。

3接口使用信号线和GND与其它设备形成共地模式的通信,这种共地模式传输容易产生干扰,并且抗干扰性能也比较弱。

4传输距离有限,最多只能通信几十米。

5通信的时候只能两点之间进行通信,不能够实现多机联网通信。


      针对RS232接口的不足,就不断出现了一些新的接口标准,RS485就是其中之一,它具备以下的特点:

1、采用差分信号。我们在讲A/D的时候,讲过差分信号输入的概念,同时也介绍了差分输入的好处,最大的优势是可以抑制共模干扰。尤其当工业现场环境比较复杂,干扰比较多时,采用差分方式可以有效的提高通信可靠性。RS485采用两根通信线,通常用AB或者D+D-来表示。逻辑“1”以两线之间的电压差为+(0.2~6)V表示,逻辑“0”以两线间的电压差为-(0.2~6)V来表示,是一种典型的差分通信。

2RS485通信速率快,最大传输速度可以达到10Mb/s以上。

3RS485内部的物理结构,采用的是平衡驱动器和差分接收器的组合,抗干扰能力也大大增加。

4传输距离最远可以达到1200米左右,但是它的传输速率和传输距离是成反比的,只有在100Kb/s以下的传输速度,才能达到最大的通信距离,如果需要传输更远距离可以使用中继。

5可以在总线上进行联网实现多机通信,总线上允许挂多个收发器,从现有的RS485芯片来看,有可以挂3264128256等不同个设备的驱动器。

6RS485的接口非常简单,与RS232所使用的MAX232是类似的,只需要一个RS485转换器,就可以直接与单片机的UART串口连接起来,并且使用完全相同的异步串行通信协议。但是由于RS485是差分通信,因此接收数据和发送数据是不能同时进行的,也就是说它是一种半双工通信。那我们如何判断什么时候发送,什么时候接收呢?


      RS485转换芯片很多,这节课我们以典型的MAX485为例讲解RS485通信,如图18-1所示。

《手把手教你学51单片机》之十八----RS485通信与Modbus协议 

18-1  MAX485硬件接口


      MAX485是美信(Maxim)推出的一款常用RS485转换器。其中5脚和8脚是电源引脚;6脚和7脚就是RS485通信中的AB两个引脚;1脚和4脚分别接到单片机的RXDTXD引脚上,直接使用单片机UART进行数据接收和发送;2脚和3脚是方向引脚,其中2脚是低电平使能接收器,3脚是高电平使能输出驱动器,我们把这两个引脚连到一起,平时不发送数据的时候,保持这两个引脚是低电平,让MAX485处于接收状态,当需要发送数据的时候,把这个引脚拉高,发送数据,发送完毕后再拉低这个引脚就可以了。为了提高RS485的抗干扰能力,需要在靠近MAX485AB引脚之间并接一个电阻,这个电阻阻值从100欧到1K是可以。


      在这里我们还要介绍一下如何使用KST-51单片机开发板进行外围扩展实验。我们的开发板只能把基本的功能给同学们做出来提供实验练习,但是同学们学习的脚步不应该停留在这个实验板上。如果想进行更多的实验,就可以通过单片机开发板的扩展接口进行扩展实验。大家可以看到蓝绿色的单片机座周围有32个插针,这32个插针就是把单片机的32IO引脚全部都引出来了。在原理图上体现出来的就是J4J5J6J74个器件,如图18-2所示。

《手把手教你学51单片机》之十八----RS485通信与Modbus协议 

18-2  单片机扩展接口


      这32IO中并不是所有的都可以用来对外扩展,其中既作为数据输出,又可以作为数据输入的引脚是不可以用的,比如P3.2P3.4P3.6引脚,这三个引脚是不可用的。比如P3.2这个引脚,如果我们用来扩展,发送的信号如果和DS18B20的时序吻合,会导致DS18B20拉低引脚,影响通信。除这3IO口以外的其它29个,都可以使用杜邦线接上插针,扩展出来使用。当然了,如果把当前的IO口应用于扩展功能了,板子上的相应功能就实现不了了,也就是说需要扩展功能和板载功能之间二选一。


      在进行RS485实验中,我们通信用的引脚必须是P3.0P3.1,此外还有一个方向控制引脚,我们使用杜邦线将其连接到P1.7上去。RS485的另外一端,大家可以使用一个USBRS485模块,用双绞线把开发板和模块上的AB分别对应连起来,USB那头插入电脑,然后就可以进行通信了。


      学习了第13章实用的串口通信方法和程序后,做这种串口通信的方法就很简单了,基本是一致的。我们使用实用串口通信例程的思路,做了一个简单的程序,通过串口调试助手下发任意个字符,单片机接收到后在末尾添加“回车+换行”符后再送回,在调试助手上重新显示出来,先把程序贴出来。


      程序中需要注意的一点是:因为平常都是将MAX485设置为接收状态,只有在发送数据的时候才将MAX485改为发送状态,所以在UartWrite()函数开头将MAX485方向引脚拉高,函数退出前再拉低。但是这里有一个细节,就是单片机的发送和接收中断产生的时刻都是在停止位的一半上,也就是说每当停止位传送了一半的时候,RITI就已经置位并且马上进入中断(如果中断使能的话)函数了,接收的时候自然不会存在问题,但发送的时候就不一样了:当紧接着向SBUF写入一个字节数据时,UART硬件会在完成上一个停止位的发送后,再开始新字节的发送,但如果此时不是继续发送下一个字节,而是已经发送完毕了,要停止发送并将MAX485方向引脚拉低以使MAX485重新处于接收状态时就有问题了,因为这时候最后的这个停止位实际只发送了一半,还没有完全完成,所以就有了UartWrite()函数内DelayX10us(5)这个操作,这是人为的增加了50us的延时,这50us的时间正好让剩下的一半停止位完成,那么这个时间自然就是由通信波特率决定的了,为波特率周期的一半。


/****************************RS485.c文件程序源代码*****************************/

#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;  //设置字节发送完成标志

    }

}

/*****************************main.c文件程序源代码******************************/

#include <reg52.h>

 

unsigned char T0RH = 0;  //T0重载值的高字节

unsigned char T0RL = 0;  //T0重载值的低字节

 

void ConfigTimer0(unsigned int ms);

extern void UartDriver();

extern void ConfigUART(unsigned int baud);

extern void UartRxMonitor(unsigned char ms);

extern void UartWrite(unsigned char *buf, unsigned char len);

 

void main()

{

    EA = 1;              //开总中断

    ConfigTimer0(1);   //配置T0定时1ms

    ConfigUART(9600);  //配置波特率为9600

    

    while (1)

    {

        UartDriver();  //调用串口驱动

    }

}

/* 串口动作函数,根据接收到的命令帧执行响应的动作

   buf-接收到的命令帧指针,len-命令帧长度 */

void UartAction(unsigned char *buf, unsigned char len)

{

    //在接收到的数据帧后添加换车换行符后发回

    buf[len++] = 'r';

    buf[len++] = 'n';

    UartWrite(buf, len);

}

/* 配置并启动T0ms-T0定时时间 */

void ConfigTimer0(unsigned int ms)

{

    unsigned long tmp;  //临时变量

    

    tmp = 11059200 / 12;       //定时器计数频率

    tmp = (tmp * ms) / 1000;  //计算所需的计数值

    tmp = 65536 - tmp;         //计算定时器重载值

    tmp = tmp + 33;            //补偿中断响应延时造成的误差

    T0RH = (unsigned char)(tmp>>8);  //定时器重载值拆分为高低字节

    T0RL = (unsigned char)tmp;

    TMOD &= 0xF0;   //清零T0的控制位

    TMOD |= 0x01;   //配置T0为模式1

    TH0 = T0RH;     //加载T0重载值

    TL0 = T0RL;

    ET0 = 1;        //使能T0中断

    TR0 = 1;        //启动T0

}

/* T0中断服务函数,执行串口接收监控 */

void InterruptTimer0() interrupt 1

{

    TH0 = T0RH;  //重新加载重载值

    TL0 = T0RL;

    UartRxMonitor(1);  //串口接收监控

}


      现在看这种串口程序,是不是感觉很简单了呢?串口通信程序我们反反复复的使用,加上随着学习的模块越来越多,实践的越来越多,原先感觉很复杂的东西,现在就会感到简单了。从设备管理器里可以查看所有的COM口号,我们下载程序用的是COM4,而USBRS485虚拟的是COM5,通信的时候我们用的是COM5口,如图18-3所示。

《手把手教你学51单片机》之十八----RS485通信与Modbus协议 

18-3  RS485通信试验设置和结果

1.2 Modbus通信协议介绍

 我们前边学习UARTI2CSPI这些通信协议,都是最底层的协议,是“位”级别的协议。而我们在学习13做实用串口通信程序的时候,我们通过串口发给单片机三条指令,让单片机做了三件不同的事情,分别是“buzz on”、“buzz off”和“showstr”。随着系统复杂性的增加,我们希望可以实现更多的指令。而指令越来越多,带来的后果就是非常杂乱无章,尤其是这个人喜欢写成“buzz on”、“buzz off”,而另外一个人喜欢写成“on buzz”、“off buzz”。导致不同开发人员写出来的程序代码不兼容,不同厂家的产品不能挂到一条总线上通信。

随着这种矛盾的日益严重,就会有聪明人提出更合理的解决方案,提出一些标准来,今后我们的编程必须按照这个标准来,这种标准也是一种通信协议,但是和UARTI2CSPI通信协议不同的是,这种通信协议是字节级别的,叫做应用层通信协议。在1979年由Modicon(现为施耐德电气公司的一个品牌)提出了全球第一个真正用于工业现场总线的协议,就是Modbus协议。

1.2.1 Modbus协议特点

      Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其他设备之间可以通信,已经成为一种工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。这种协议定义了一种控制器能够认识使用的数据结构,而不管它们是经过何种网络进行通信的。它描述了控制器请求访问其它设备的过程,如何回应来自其它设备的请求,以及怎样侦测错误记录,它制定了通信数据的格局和内容的公共格式。


       在进行多机通信的时候,Modbus协议规定每个控制器必须要知道它们的设备地址,识别按照地址发送过来的数据,决定是否要产生动作,产生何种动作,如果要回应,控制器将生成的反馈信息用Modbus协议发出。


      Modbus协议允许在各种网络体系结构内进行简单通信,每种设备(PLC、人机界面、控制面板、驱动程序、输入输出设备等)都能使用Modbus协议来启动远程操作,一些网关允许在几种使用Modbus协议的总线或网络之间的通信,如图18-4所示。

《手把手教你学51单片机》之十八----RS485通信与Modbus协议 

18-4  Modbus网络体系结构实例


      Modbus协议的整体架构和格式比较复杂和庞大,在我们的课程里,我们重点介绍数据帧结构和数据通信控制方式,作为一个入门级别的了解。如果大家要详细了解,或者使用Modbus开发相关设备,可以查阅相关的国标文件再进行深入学习。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值