精华内容
下载资源
问答
  • RS485通信和Modbus协议.doc
  • RS485通信和Modbus协议

    万次阅读 多人点赞 2017-10-17 17:13:54
    在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业...而RS485则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最
    在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是
    RS232
    接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的电磁干扰,会导致信号传输错误。除此之外,
    RS232
    接口只能实现点对点通信,不具备联网功能,最大传输距离也只能达到几十米,不能满足远距离通信要求。而
    RS485
    则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最大距离可以到
    1200
    米,并且允许多个收发设备接到同一条总线上。随着工业应用通信越来越多,
    1979
    年施耐德电气制定了一个用于工业现场的总线协议
    Modbus
    协议,现在工业中使用
    RS485
    通信场合很多都采用
    Modbus
    协议,本节课我们要讲解一下
    RS485
    通信和
    Modbus
    协议。 
    
    单单使用一块KST-51开发板是不能够进行RS485实验的,应很多同学的要求,把这节课作为扩展课程讲一下,如果要做本课相关实验,需要自行购买USB485通信模块。
    18.1 RS485通信
    实际上在RS485之前RS232就已经诞生,但是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来表示,是一种典型的差分通信。
    2、RS485通信速度快,最大传输速度可以达到10Mb/s以上。
    3、RS485内部的物理结构,采用的是平衡驱动器和差分接收器的组合,抗干扰能力也大大增加。
    4、传输距离最远可以达到1200米左右,但是他的传输速率和传输距离是成反比的,只有在100Kb/s以下的传输速度,才能达到最大的通信距离,如果需要传输更远距离可以使用中继。
    5、可以在总线上进行联网实现多机通信,总线上允许挂多个收发器,从现有的RS485芯片来看,有可以挂3264128256等不同个设备的驱动器。
    RS485的接口非常简单,和RS232所使用的MAX232是类似的,只需要一个RS485转换器,就可以直接和我们单片机的UART串行接口连接起来,并且完全使用的是和UART一致的异步串行通信协议。但是由于RS485是差分通信,因此接收数据和发送数据是不能同时进行的,也就是说它是一种半双工通信。那我们如何判断什么时候发送,什么时候接收呢?
    RS485类的芯片很多,这节课我们以MAX485为例讲解RS485通信,如图18-1所示。
     
    图18-1 MAX485硬件接口
    MAX485是美信(Maxim)推出的一款常用RS485转换器。其中5脚和8脚是电源引脚,6脚和7脚就是485通信中的AB两个引脚,而1脚和4脚分别接到我们单片机的RXDTXD引脚上,直接使用单片机UART进行数据接收和发送。而2脚和3脚就是方向引脚了,其中2脚是低电平使能接收器,3脚是高电平使能输出驱动器。我们把这两个引脚连到一起,平时不发送数据的时候,保持这两个引脚是低电平,让MAX485处于接收状态,当需要发送数据的时候,把这个引脚拉高,发送数据,发送完毕后再拉低这个引脚就可以了。为了提高RS485的抗干扰性能,需要在靠近MAX485AB引脚之间并接一个电阻,这个电阻阻值从100欧到1K都可以。
    在这里我们还要介绍一下如何使用KST-51单片机开发板进行外围扩展实验。我们的开发板只能把基本的功能给同学们做出来提供实验练习,但是同学们学习的脚步不应该停留在这个实验板上。如果想进行更多的实验,就可以通过单片机开发板的扩展接口进行扩展实验。大家可以看到蓝绿色的单片机座周围有32个插针,这32个插针就是把单片机的32IO引脚全部都引出来了。在原理图上体现出来的就是我们的J4J5J6J74个器件,如图18-2所示。
     
    图18-2 单片机扩展接口
    32IO口不是所有的IO口都可以用来对外扩展,其中既作为数据输出,又可以作为数据输入的引脚是不可以用的,比如P3.2P3.4P3.6引脚,这三个引脚是不可用的。比如P3.2这个引脚,如果我们用来扩展,发送的信号如果和DS18B20的时序吻合,会导致DS18B20拉低引脚,影响通信。除这3IO口以外的其他29IO口,都可以使用杜邦线接上插针,扩展出来使用。当然了,如果把当前的IO口应用于扩展功能了,板子上的相应的功能就实现不了了,也就是说需要扩展功能和板载功能二选一。
    在进行RS485实验中,我们通信用的引脚必须是P3.0P3.1,此外还有一个方向控制引脚,我们使用杜邦线将其连接到P1.7上去。RS485的另外一端,大家可以使用一个USB485模块,用双绞线把开发板和模块上的AB分别对应连起来,USB那头插入电脑,然后就可以进行通信了。
    学习了第13章的实用串口通信的方法和程序后,做这种串口通信的方法就很简单了,基本是一致的。我们使用实用串口通信的思路,做了一个简单的程序,通过串口调试助手下发任意个字符,单片机接收到后在末尾添加“回车+换行”符后再送回,在调试助手上重新显示出来,先把程序贴出来。
    程序中需要注意的一点是:因为平常都是将485设置为接收状态,只有在发送数据的时候才将485改为发送状态,所以在UartWrite()函数开头将485方向引脚拉高,函数退出前再拉低。但是这里有一个细节,就是单片机的发送和接收中断产生的时刻都是在停止位的一半上,也就是说每当停止位传送了一半的时候,RITI就已经置位并且马上进入中断(如果中断使能的话)函数了,接收的时候自然不会存在问题,但发送的时候就不一样了:当紧接这向SBUF写入一个字节数据时,UART硬件会在完成上一个停止位的发送后,再开始新字节的发送,但如果此时不是继续发送下一个字节,而是已经发送完毕了,要停止发送并将485方向引脚拉低以使485重新处于接收状态时就有问题了,因为这时候最后的这个停止位实际只发送了一半,还没有完全完成,所以就有了UartWrite()函数内DelayX10us(5)这个操作,这是人为的增加了延时50us,这50us的时间正好让剩下的一半停止位完成,那么这个时间自然就是由通信波特率决定的了,为波特率周期的一半。
    /***********************RS485.c文件程序源代码*************************/
    #include <reg52.h>
    #include <intrins.h>
    sbit RS485_DIR = P1^7;  //RS485方向选择引脚
    bit flagOnceTxd = 0;  //单次发送完成标志,即发送完一个字节
    bit cmdArrived = 0;   //命令到达标志,即接收到上位机下发的命令
    unsigned char cntRxd = 0;
    unsigned char pdata bufRxd[40]; //串口接收缓冲区
    void ConfigUART(unsigned int baud)  //串口配置函数,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
    }
    unsigned char UartRead(unsigned char *buf, unsigned char len) //串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度
    {
        unsigned char i;
       
        if (len > cntRxd) //读取长度大于接收到的数据长度时,
        {
            len = cntRxd; //读取长度设置为实际接收到的数据长度
        }
        for (i=0; i<len; i++) //拷贝接收到的数据
        {
            *buf = bufRxd[ i];
            buf++;
        }
        cntRxd = 0;  //清零接收计数器
       
        return len;  //返回实际读取长度
    }
    void DelayX10us(unsigned char t)  //软件延时函数,延时时间(t*10)us
    {
        do {
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            } while (--t);
    }
    void UartWrite(unsigned char *buf, unsigned char len) //串口数据写入函数,即串口发送函数,待发送数据指针buf,数据长度len
    {
        RS485_DIR = 1;  //RS485设置为发送
        while (len--)   //发送数据
        {
            flagOnceTxd = 0;
            SBUF = *buf;
            buf++;
            while (!flagOnceTxd);
        }
        DelayX10us(5);  //等待最后的停止位完成,延时时间由波特率决定
        RS485_DIR = 0;  //RS485设置为接收
    }
    void UartDriver() //串口驱动函数,检测接收到的命令并执行相应动作
    {
        unsigned char len;
        unsigned char buf[30];
        if (cmdArrived) //有命令到达时,读取处理该命令
        {
            cmdArrived = 0;
            len = UartRead(buf, sizeof(buf)-2); //将接收到的命令读取到缓冲区中
            buf[len++] = '\r';   //在接收到的数据帧后添加换车换行符后发回
            buf[len++] = '\n';
            UartWrite(buf, len);
        }
    }
    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)  //接收计数器未改变,即总线空闲时,累积空闲时间
                {
                    idletmr += ms;
                    if (idletmr >= 30)  //空闲时间超过30ms即认为一帧命令接收完毕
                    {
                        cmdArrived = 1; //设置命令到达标志
                    }
                }
            }
        }
        else
        {
            cntbkp = 0;
        }
    }
    void InterruptUART() interrupt 4  //UART中断服务函数
    {
                if (RI)  //接收到字节
        {
            RI = 0;   //手动清零接收中断标志位
            if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
            {
                bufRxd[cntRxd++] = SBUF; //保存接收字节,并递增计数器
            }
                }
                if (TI)  //字节发送完毕
        {
                    TI = 0;   //手动清零发送中断标志位
            flagOnceTxd = 1;  //设置单次发送完成标志
                }
    }
    /***********************main.c文件程序源代码*************************/
    #include <reg52.h>
    unsigned char T0RH = 0;  //T0重载值的高字节
    unsigned char T0RL = 0;  //T0重载值的低字节
    void ConfigTimer0(unsigned int ms);
    extern void ConfigUART(unsigned int baud);
    extern void UartRxMonitor(unsigned char ms);
    extern void UartDriver();
    void main ()
    {
        EA = 1;           //开总中断
        ConfigTimer0(1);  //配置T0定时1ms
        ConfigUART(9600); //配置波特率为9600
       
        while(1)
        {
            UartDriver();
        }
    }
    void ConfigTimer0(unsigned int ms)  //T0配置函数
    {
        unsigned long tmp;
       
        tmp = 11059200 / 12;      //定时器计数频率
        tmp = (tmp * ms) / 1000;  //计算所需的计数值
        tmp = 65536 - tmp;        //计算定时器重载值
        tmp = tmp + 34;           //修正中断响应延时造成的误差
       
        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
    }
    void InterruptTimer0() interrupt 1  //T0中断服务函数
    {
        TH0 = T0RH;  //定时器重新加载重载值
        TL0 = T0RL;
        UartRxMonitor(1);  //串口接收监控
    }
        现在看这种串口程序,是不是感觉很简单了呢?串口通信程序我们反反复复的使用,加上随着我们学习的模块越来越多,实践的越来越多,原先感觉很复杂的东西,现在就会感到简单了。我们的下载程序模块用的是COM4,而USB485虚拟的是COM5,通信的时候我们用的是COM5口,如图18-3所示。
    图18-3 RS485串行通信
    18.2 Modbus通信协议介绍
    我们前边学习UARTI2CSPI这些通信协议,都是最底层的协议,是“位”级别的协议。而我们在学习13章实用串口通信程序的时候,我们通过串口发给单片机三条指令,让单片机做了三件不同的事情,分别是"buzz on"、"buzz off"、和"showstr"。随着我们系统复杂性的增加,我们希望可以实现更多的指令。而指令越来越多,带来的后果就是非常杂乱无章,尤其是这个人喜欢写成"buzz on"、"buzz off",而另外一个人喜欢写成"on buzz"、"off buzz"。导致不同开发人员写出来的代码指令不兼容,不同厂家的产品不能挂到一条总线上通信。
    随着这种矛盾的日益严重,就会有聪明人提出更合理的解决方案,提出一些标准来,今后我们的编程必须按照这个标准来,这种标准也是一种通信协议,但是和UARTI2CSPI通信协议不同的是,这种通信协议是字节级别的,叫做应用层通信协议。在1979年由Modicon(现为施耐德电气公司的一个品牌)提出了全球第一个真正用于工业现场总线的协议,就是Modbus协议。
    18.2.1 Modbus协议特点
    Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其他设备之间可以通信,已经成为一种工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。这种协议定义了一种控制器能够认识使用的数据结构,而不管它们是经过何种网络进行通信的。它描述了控制器请求访问其他设备的过程,如何回应来自其他设备的请求,以及怎样侦测错误记录,它制定了通信数据的格局和内容的公共格式。
    在进行多机通信的时候,Modbus协议规定每个控制器必须要知道他们的设备地址,识别按照地址发送过来的数据,决定是否要产生动作,产生何种动作,如果要回应,控制器将生成的反馈信息用Modbus协议发出。
    Modbus协议允许在各种网络体系结构内进行简单通信,每种设备(PLC、人机界面、控制面板、驱动程序、输入输出设备)都能使用Modbus协议来启动远程操作,一些网关允许在几种使用Modbus协议的总线或网络之间的通信,如图18-4所示。
     
    图18-4 Modbus网络体系结构实例
    Modbus协议的整体架构和格式比较复杂和庞大,在我们的课程里,我们重点介绍数据帧结构和数据通信控制方式,作为一个入门级别的了解。如果大家要详细了解,或者使用Modbus开发相关设备,可以查阅相关的国标文件再进行深入学习。
    1.2.2 RTU协议帧数据
    Modbus有两种通信传输方式,一种是ASCII模式,一种是RTU模式。由于ASCII模式的数据字节是7bit数据位,51单片机无法实现,而且应用也相对较少,所以这里我们只用RTU模式。两种模式相似,会用一种另外一种也就会了。一条典型的RTU数据帧如图18-5所示。
     
    18-5 RTU数据帧
    和我们实用串口通信程序类似,我们一次发送的数据帧必须是作为一个连续的数据流进行传输。我们在实用串口通信程序中采用的方法是定义30ms,如果接收到的数据超过了30ms还没有接收到下一个字节,我们就认为这次的数据结束。而ModbusRTU模式规定不同数据帧之间的间隔是3.5个字节通信时间以上。如果在一帧数据完成之前有超过3.5个字节时间的停顿,接收设备将刷新当前的消息并假定下一个字节是一个新的数据帧的开始。同样的,如果一个新消息在小于3.5个字节时间内接着前边一个数据开始的,接收的设备将会认为它是前一帧数据的延续。这将会导致一个错误,因此大家看RTU数据帧最后还有16bitCRC校验。
    起始位和结束符:图18-5上代表的是一个数据帧,前后都至少有3.5个字节的时间间隔,起始位和结束符实际上没有任何数据,T1-T2-T3-T4代表的是时间间隔3.5个字节以上的时间,而真正有意义的第一个字节是设备地址。
    设备地址:很多同学不理解,在多机通信的时候,数据那么多,我们依靠什么判断这个数据帧是哪个设备的呢?没错,就是依靠这个设备地址字节。每个设备都有一个自己的地址,当设备接收到一帧数据后,程序首先对设备地址字节进行判断比较,如果与自己的地址不同,则对这帧数据直接不予理会,如果如果与自己的地址相同,就要对这帧数据进行解析,按照之后的功能码执行相应的功能。如果地址是0x00,则认为是一个广播命令,就是所有的从机设备都要执行的指令。
    功能代码:在第二个字节功能代码字节中,Modbus规定了部分功能代码,此外也保留了一部分功能代码作为备用或者用户自定义,这些功能码大家不需要去记忆,甚至都不用去看,直到你有用到的那天再过来查这个表格即可,如表18-1所示。
    18-1 Modbus功能码
    功能码
    名称
    作用
    01
    读取线圈状态
    取得一组逻辑线圈的当前状态 (ON/OFF)
    02
    读取输入状态
    取得一组开关输入的当前状态 (ON/OFF)
    03
    读取保持寄存器
    在一个或多个保持寄存器中取得当前的二进制值
    04
    读取输入寄存器
    在一个或多个输入寄存器中取得当前的二进制值
    05
    强置单线圈
    强置一个逻辑线圈的通断状态
    06
    预置单寄存器
    把具体二进值装入一个保持寄存器
    07
    读取异常状态
    取得 个内部线圈的通断状态,这  个线圈的地址由控制器决定,用户逻辑可以将这些线圈定义,以说明从机状态,短报文适宜于迅速读取状态
    08
    回送诊断校验
    把诊断校验报文送从机,以对通信处理进行评鉴
    09
    编程(只用于 484)
    使主机模拟编程器作用,修改 PC 从机逻辑
    10
    控询(只用于 484)
    可使主机与一台正在执行长程序任务从机通信,探询该从机是否已完成其操作任务,仅在含有功能码  的报文发送后,本功能码才发送
    11
    读取事件计数
    可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时
    12
    读取通信事件记录
    可是主机检索每台从机的 ModBus 事务处理通信事件记录。如果某项事务处理完成,记录会给出有关错误
    13
    编程(184/384 484 584 )
    可使主机模拟编程器功能修改 PC 从机逻辑
    14
    探询(184/384 484 584)
    可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能 13 的报文发送后,本功能码才得发送
    15
    强置多线圈
    强置一串连续逻辑线圈的通断
    16
    预置多寄存器
    把具体的二进制值装入一串连续的保持寄存器
    17
    报告从机标识
    可使主机判断编址从机的类型及该从机运行指示灯的状态
    18
    884  MICRO 84
    可使主机模拟编程功能,修改 PC 状态逻辑
    19
    重置通信链路
    发生非可修改错误后,是从机复位于已知状态,可重置顺序字节
    20
    读取通用参数(584L)
    显示扩展存储器文件中的数据信息
        21
    写入通用参数(584L)
    把通用参数写入扩展存储文件,或修改
    22~64
    保留作扩展功能备用
    65~72
    保留以备用户功能所用
    留作用户功能的扩展编码
    73~119
    非法功能
    120~127
    保留
    留作内部作用
    128~255
    保留
    用于异常应答
        我们程序对功能码的处理,就是程序来检测这个字节的数值,然后根据其数值来做相应的功能处理。
    数据:跟在功能代码后边的是n8bit的数据。这个n值的到底是多少,是功能代码来确定的,不同的功能代码后边跟的数据数量不同。举个例子,如果功能码是0x03,也就是读保持寄存器,那么主机发送数据n的组成部分就是:2个字节的寄存器起始地址,加2个字节的寄存器数量N*。从机数据n的组成部分是:1个字节的字节数,因为我们回复的寄存器的值是2个字节,所以这个字节数也就是2N*个,再加上2N*个寄存器的值,如图18-6所示。
     
    18-6 读保持寄存器数据结构
    CRC校验:CRC校验是一种数据算法,是用来校验数据对错的。CRC校验函数把一帧数据除最后两个字节外,前边所有的字节进行特定的算法计算,计算完后生成了一个16bit的数据,作为CRC校验码,添加在一帧数据的最后。接收方接收到数据后,同样会把前边的字节进行CRC计算,计算完了再和发过来的CRC16bit的数据进行比较,如果相同则认为数据正常,没有出错,如果比较不相同,则说明数据在传输中发生了错误,这帧数据将被丢弃,就像没收到一样,而发送方会在得不到回应后做相应的处理错误处理。
    RTU模式的每个字节的位是这样分布的:1个起始位、8个数据位,最小有效位先发送、1个奇偶校验位(如果无校验则没有这一位)1位停止位(有校验位时)或者2个停止位(无校验位时)
    18.3 Modbus多机通信例程
    给从机下发不同的指令,从机去执行不同的操作,这个就是判断一下功能码即可,和我们前边学的实用串口例程是类似的。多机通信,无非就是添加了一个设备地址判断而已,难度也不是很大。我们找了一个Modbus调试精灵,通过设置设备地址,读写寄存器的地址以及数值数量等参数,可以直接替代串口调试助手,比较方便的下发多个字节的数据,如图18-7所示。我们先来就图中的设置和数据来对Modbus做进一步的分析,图中的数据来自于调试精灵与我们接下来要讲的例程之间的交互。
     
    图18-7 Modbus调试精灵
    如图:我们的USB485模块虚拟出的是COM5,波特率9600,无校验位,数据位是8位,1位停止位,设备地址假设为1
    写寄存器的时候,如果我们要把01写到一个地址是0000的寄存器地址里,点一下“写入”,就会出现发送指令:01 06 00 00 00 01 48 0A。我们来分析一下这帧数据,其中01是设备地址,06是功能码,代表写寄存器这个功能,后边跟00 00表示的是要写入的寄存器的地址,00 01就是要写入的数据,48 0A就是CRC校验码,这是软件自动算出来了。而根据Modbus协议,当写寄存器的时候,从机成功完成该指令的操作后,会把主机发送的指令直接返回,我们的调试精灵会接收到这样一帧数据:01 06 00 00 00 01 48 0A
    假如我们现在要从寄存器地址0002开始读取寄存器,并且读取的数量是2个。点一下“读出”,就会出现发送指令:01 03 00 02 00 02 65 CB。其中01是设备地址,03是功能码,代表写寄存器这个功能,00 02就是读寄存器的起始地址,后一个00 02就是要读取2个寄存器的数值,65 CB就是CRC校验。而接收到的数据是:01 03 04 00 00 00 00 FA 33。其中01是设备地址,03是功能码,04代表的是后边读到的数据字节数是4个,00 00 00 00分别是地址为00 0200 03的寄存器内部的数据,而FA 33就是CRC校验了。
    似乎越来越明朗了,所谓的Modbus这种通信协议,无非就是主机下发了不同的指令,从机根据指令的判断来执行不同的操作而已。由于我们的开发板没有Modbus功能码那么多相应的功能,我们在程序中定义了一个数组regGroup[5],相当于5个寄存器,此外又定义了第6个寄存器,控制蜂鸣器,通过下发不同的指令我们改变寄存器组的数据或者改变蜂鸣器的开关状态。在Modbus协议里寄存器的地址和数值都是16位的,即2个字节,我们默认高字节是0x00,低字节就是数组regGroup对应的值。其中地址0x00000x0004对应的就是regGroup数组中的元素,我们写入的同时把数字又显示到我们的LCD1602液晶上,而0x0005这个地址,写入0x00,蜂鸣器就不响,写入任何其他数字,蜂鸣器就报警。我们单片机的主要工作也就是解析串口接收的数据执行不同操作,也就是主要在RS485.C这个文件中了
    /***********************RS485.c文件程序源代码*************************/
    #include <reg52.h>
    #include <intrins.h>
    sbit RS485_DIR = P1^7;  //RS485方向选择引脚
    bit flagOnceTxd = 0;  //单次发送完成标志,即发送完一个字节
    bit cmdArrived = 0;   //命令到达标志,即接收到上位机下发的命令
    unsigned char cntRxd = 0;
    unsigned char pdata bufRxd[40]; //串口接收缓冲区
    unsigned char regGroup[5];  //Modbus寄存器组,地址为0x000x04
    extern bit flagBuzzOn;
    extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
    extern unsigned int GetCRC16(unsigned char *ptr,  unsigned char len);
    void ConfigUART(unsigned int baud)  //串口配置函数,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
    }
    unsigned char UartRead(unsigned char *buf, unsigned char len) //串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度
    {
        unsigned char i;
       
        if (len > cntRxd) //读取长度大于接收到的数据长度时,
        {
            len = cntRxd; //读取长度设置为实际接收到的数据长度
        }
        for (i=0; i<len; i++) //拷贝接收到的数据
        {
            *buf = bufRxd[ i];
            buf++;
        }
        cntRxd = 0;  //清零接收计数器
       
        return len;  //返回实际读取长度
    }
    void DelayX10us(unsigned char t)  //软件延时函数,延时时间(t*10)us
    {
        do {
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
            _nop_();
        } while (--t);
    }
    void UartWrite(unsigned char *buf, unsigned char len) //串口数据写入函数,即串口发送函数,待发送数据指针buf,数据长度len
    {
        RS485_DIR = 1;  //RS485设置为发送
        while (len--)   //发送数据
        {
            flagOnceTxd = 0;
            SBUF = *buf;
            buf++;
            while (!flagOnceTxd);
        }
        DelayX10us(5);  //等待最后的停止位完成,延时时间由波特率决定
        RS485_DIR = 0;  //RS485设置为接收
    }
    void UartDriver() //串口驱动函数,检测接收到的命令并执行相应动作
    {
        unsigned char i;
        unsigned char cnt;
        unsigned char len;
        unsigned char buf[30];
        unsigned char str[4];
        unsigned int  crc;
        unsigned char crch, crcl;
        if (cmdArrived) //有命令到达时,读取处理该命令
        {
            cmdArrived = 0;
            len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中
            if (buf[0] == 0x01)  //核对地址以决定是否响应命令,本例本机地址为0x01
            {
                crc = GetCRC16(buf, len-2); //计算CRC校验值
                crch = crc >> 8;
                crcl = crc & 0xFF;
                if ((buf[len-2] == crch) && (buf[len-1] == crcl)) //判断CRC校验是否正确
                {
                    switch (buf[1]) //按功能码执行操作
                    {
                        case 0x03:  //读取一个或连续的寄存器
                            if ((buf[2] == 0x00) && (buf[3] <= 0x05)) //寄存器地址支持0x00000x0005
                            {
                                if (buf[3] <= 0x04)
                                {
                                    i = buf[3];      //提取寄存器地址
                                    cnt = buf[5];    //提取待读取的寄存器数量
                                    buf[2] = cnt*2;  //读取数据的字节数,为寄存器数*2,因Modbus定义的寄存器为16
                                    len = 3;
                                    while (cnt--)
                                    {
                                        buf[len++] = 0x00;      //寄存器高字节补0
                                        buf[len++] = regGroup[ i++]; //低字节
                                    }
                                }
                                else  //地址0x05为蜂鸣器状态
                                {
                                    buf[2] = 2;  //读取数据的字节数
                                    buf[3] = 0x00;
                                    buf[4] = flagBuzzOn;
                                    len = 5;
                                }
                                break;
                            }
                            else  //寄存器地址不被支持时,返回错误码
                            {
                                buf[1] = 0x83;  //功能码最高位置1
                                buf[2] = 0x02;  //设置异常码为02-无效地址
                                len = 3;
                                break;
                            }
                            
                        case 0x06:  //写入单个寄存器
                            if ((buf[2] == 0x00) && (buf[3] <= 0x05)) //寄存器地址支持0x00000x0005
                            {
                                if (buf[3] <= 0x04)
                                {
                                    i = buf[3];             //提取寄存器地址
                                    regGroup[ i] = buf[5];   //保存寄存器数据
                                    cnt = regGroup[ i] >> 4; //显示到液晶上
                                    if (cnt >= 0xA)
                                        str[0] = cnt - 0xA + 'A';
                                    else
                                        str[0] = cnt + '0';
                                    cnt = regGroup[ i] & 0x0F;
                                    if (cnt >= 0xA)
                                        str[1] = cnt - 0xA + 'A';
                                    else
                                        str[1] = cnt + '0';
                                    str[2] = '\0';
                                    LcdShowStr(i*3, 0, str);
                                }
                                else  //地址0x05为蜂鸣器状态
                                {
                                    flagBuzzOn = (bit)buf[5]; //寄存器值转换为蜂鸣器的开关
                                }
                                len -= 2; //长度-2以重新计算CRC并返回原帧
                                break;
                            }
                            else  //寄存器地址不被支持时,返回错误码
                            {
                                buf[1] = 0x86;  //功能码最高位置1
                                buf[2] = 0x02;  //设置异常码为02-无效地址
                                len = 3;
                                break;
                            }
                            
                        default:  //其它不支持的功能码
                            buf[1] |= 0x80;  //功能码最高位置1
                            buf[2] = 0x01;   //设置异常码为01-无效功能
                            len = 3;
                            break;
                    }
                    crc = GetCRC16(buf, len); //计算CRC校验值
                    buf[len++] = crc >> 8;    //CRC高字节
                    buf[len++] = crc & 0xFF;  //CRC低字节
                    UartWrite(buf, len);      //发送响应帧
                }
            }
        }
    }
    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 < 5)  //接收计数器未改变,即总线空闲时,累积空闲时间
                {
                    idletmr += ms;
                    if (idletmr >= 5)  //空闲时间超过4个字节传输时间即认为一帧命令接收完毕
                    {
                        cmdArrived = 1; //设置命令到达标志
                    }
                }
            }
        }
        else
        {
            cntbkp = 0;
        }
    }
    void InterruptUART() interrupt 4  //UART中断服务函数
    {
                if (RI)  //接收到字节
        {
            RI = 0;   //手动清零接收中断标志位
            if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
            {
                bufRxd[cntRxd++] = SBUF; //保存接收字节,并递增计数器
            }
                }
                if (TI)  //字节发送完毕
        {
                    TI = 0;   //手动清零发送中断标志位
            flagOnceTxd = 1;  //设置单次发送完成标志
        }
    }
    /***********************lcd1602.c文件程序源代码*************************/
    #include <reg52.h>
    #define LCD1602_DB   P0
    sbit LCD1602_RS = P1^0;
    sbit LCD1602_RW = P1^1;
    sbit LCD1602_E  = P1^5;
    void LcdWaitReady()  //等待液晶准备好
    {
        unsigned char sta;
       
        LCD1602_DB = 0xFF;
        LCD1602_RS = 0;
        LCD1602_RW = 1;
        do
        {
            LCD1602_E = 1;
            sta = LCD1602_DB; //读取状态字
            LCD1602_E = 0;
        } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止
    }
    void LcdWriteCmd(unsigned char cmd)  //写入命令函数
    {
        LcdWaitReady();
        LCD1602_RS = 0;
        LCD1602_RW = 0;
        LCD1602_DB = cmd;
        LCD1602_E  = 1;
        LCD1602_E  = 0;
    }
    void LcdWriteDat(unsigned char dat)  //写入数据函数
    {
        LcdWaitReady();
        LCD1602_RS = 1;
        LCD1602_RW = 0;
        LCD1602_DB = dat;
        LCD1602_E  = 1;
        LCD1602_E  = 0;
    }
    void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str)  //显示字符串,屏幕起始坐标(x,y),字符串指针str
    {
        unsigned char addr;
       
        //由输入的显示坐标计算显示RAM的地址
        if (y == 0)
            addr = 0x00 + x; //第一行字符地址从0x00起始
        else
            addr = 0x40 + x; //第二行字符地址从0x40起始
       
        //由起始显示RAM地址连续写入字符串
        LcdWriteCmd(addr | 0x80); //写入起始地址
        while (*str != '\0')      //连续写入字符串数据,直到检测到结束符
        {
            LcdWriteDat(*str);
            str++;
        }
    }
    void LcdInit()  //液晶初始化函数
    {
        LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口
        LcdWriteCmd(0x0C);  //显示器开,光标关闭
        LcdWriteCmd(0x06);  //文字不动,地址自动+1
        LcdWriteCmd(0x01);  //清屏
    }
       关于CRC校验的算法,如果不是专门学习校验算法本身,大家可以不去研究这个程序的细节,文档直接给我们提供了函数,我们直接调用即可。
    /***********************CRC16.c文件程序源代码*************************/
    unsigned int GetCRC16(unsigned char *ptr,  unsigned char len)
    {
        unsigned int index;
        unsigned char crch = 0xFF;  //CRC字节
        unsigned char crcl = 0xFF;  //CRC字节
        unsigned char code TabH[] = {  //CRC高位字节值表
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  
            0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  
            0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  
            0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40  
        } ;  
        unsigned char code TabL[] = {  //CRC低位字节值表
            0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,  
            0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,  
            0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,  
            0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,  
            0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,  
            0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,  
            0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,  
            0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,  
            0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,  
            0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,  
            0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,  
            0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,  
            0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,  
            0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,  
            0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,  
            0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,  
            0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,  
            0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,  
            0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,  
            0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,  
            0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,  
            0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,  
            0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,  
            0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,  
            0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,  
            0x43, 0x83, 0x41, 0x81, 0x80, 0x40  
        } ;
        while (len--)  //计算指定长度的CRC
        {
            index = crch ^ *ptr++;
            crch = crcl ^ TabH[ index];
            crcl = TabL[ index];
        }
       
        return ((crch<<8) | crcl);  
    }                           
    /***********************main.c文件程序源代码*************************/
    void ConfigTimer0(unsigned int ms);
    extern void LcdInit();
    extern void ConfigUART(unsigned int baud);
    extern void UartRxMonitor(unsigned char ms);
    extern void UartDriver();
    void main ()
    {
        EA = 1;           //开总中断
        ConfigTimer0(1);  //配置T0定时1ms
        ConfigUART(9600); //配置波特率为9600
        LcdInit();        //初始化液晶
       
        while(1)
        {
            UartDriver();
        }
    }
    void ConfigTimer0(unsigned int ms)  //T0配置函数
    {
        unsigned long tmp;
       
        tmp = 11059200 / 12;      //定时器计数频率
        tmp = (tmp * ms) / 1000;  //计算所需的计数值
        tmp = 65536 - tmp;        //计算定时器重载值
        tmp = tmp + 34;           //修正中断响应延时造成的误差
       
        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
    }
    void InterruptTimer0() interrupt 1  //T0中断服务函数
    {
        TH0 = T0RH;  //定时器重新加载重载值
        TL0 = T0RL;
        if (flagBuzzOn)  //蜂鸣器鸣叫或关闭
            BUZZ = ~BUZZ;
        else
            BUZZ = 1;
        UartRxMonitor(1);  //串口接收监控
    }
    展开全文
  • 2.RS485和RS232一样都是基于串口的通讯接口,数据收发的操作是一致的,所以使用的是同样WinCE的底层驱动程序。但是它们在实际应用中通讯模式却有着很大的区别,RS485接口为半双工数据通讯模式,而RS232接口为全双工...

    一、RS485介绍
    485
    1.在工业控制场合,RS485总线因其接口简单,组网方便,传输距离远等特点而得到广泛应用。
    2.RS485和RS232一样都是基于串口的通讯接口,数据收发的操作是一致的,所以使用的是同样WinCE的底层驱动程序。但是它们在实际应用中通讯模式却有着很大的区别,RS485接口为半双工数据通讯模式,而RS232接口为全双工数据通讯模式。RS485任何时候只能有一点处于发送状态,因此,发送电路须由使能信号加以控制。
    3.RS485接口组成的半双工网络,一般是两线制(以前有四线制接法,只能实现点对点的通信方式,现很少采用),多采用屏蔽双绞线传输。这种接线方式为总线式拓扑结构在同一总线上最多可以挂接32个结点。在RS485通信网络中一般采用的是主从通信方式,即一个主机带多个从机。

    二、MODBUS介绍
    1.1979年施耐德电气制定了一个用于工业现场的总线协议Modbus协议,现在工业中使用RS485通信场合很多都采用Modbus协议。
    2.ModBus网络是一个 工业通信系统,由带智能终端的 可编程序控制器计算机 通过公用线路或局部专用线路连接而成。其系统结构既包括硬件、亦包括软件。它可应用于各种 数据采集 和过程 监控 。
    3.Modbus可以支持多种电气接口,如RS-232、 RS-485 等,还可以在各种介质上传送,如双绞线、 光纤 、无线等。

    三、RS485和MODBUS的区别
    1.RS485是一个物理接口,简单的说是硬件。
    2.MODBUS是一种国际标准的通讯协议,用于不同厂商之间的设备交换数据(一般是工业用途);所谓协议,也可以理解为上面有人说的“语言”吧,简单的说是软件。通俗点来讲,ModBus规约了起停电机,主机要分别发送什么命令给从机。ModBus规定主从机之间数据的交互,需要遵循什么样的格式,如何保证数据在传输过程中不发生冲突。只要都遵循这个协议,那么不同厂家的主从机就可以共用了。
    3.一般情况下,两台设备通过MODBUS协议传输数据:
    最早是用RS232C作为硬件接口,(也就是普通电脑上的串行通讯口(串口)); 也有用RS422的,也有常用的RS485,这种接口传输距离远,在一般工业现场用的比较多。
    4.MODBUS协议又分MODBUS RTU,MODBUS ASCII和后来发展的MODBUS TCP三种模式:
    其中前两种(MODBUS RTU,MODBUS ASCII)所用的物理硬件接口都是串行(Serial)通讯口(RS232,RS422,RS485)。而MODBUS TCP则是为了顺应当今世界发展潮流,什么都可以用 Ethernet 网或Internet来连接,传送数据。所以又MODBUS TCP模式,该模式的硬件接口就是 以太网 (Ethernet)口了,也就是我们电脑上一般用的网络口了。

    起始位和结束符:前后都至少有3.5个字节的时间间隔,起始位和结束符实际上没有任何数据,T1-T2-T3-T4代表的是时间间隔3.5个字节以上的时间,而真正有意义的第一个字节是设备地址。

    • 设备地址:在多机通信的时候,数据那么多,我们依靠什么判断这个数据帧是哪个设备的呢?没错,就是依靠这个设备地址字节。每个设备都有一个自己的地址,当设备接收到一帧数据后,程序首先对设备地址字节进行判断比较,如果与自己的地址不同,则对这帧数据直接不予理会,如果如果与自己的地址相同,就要对这帧数据进行解析,按照之后的功能码执行相应的功能。如果地址是0x00,则认为是一个广播命令,就是所有的从机设备都要执行的指令。
    • 功能代码:在第二个字节功能代码字节中,Modbus规定了部分功能代码,此外也保留了一部分功能代码作为备用或者用户自定义,这些功能码大家不需要去记忆,甚至都不用去看,直到你有用到的那天再过来查这个表格即可。
    • CRC校验:CRC校验是一种数据算法,是用来校验数据对错的。CRC校验函数把一帧数据除最后两个字节外,前边所有的字节进行特定的算法计算,计算完后生成了一个16bit的数据,作为CRC校验码,添加在一帧数据的最后。接收方接收到数据后,同样会把前边的字节进行CRC计算,计算完了再和发过来的CRC的16bit的数据进行比较,如果相同则认为数据正常,没有出错,如果比较不相同,则说明数据在传输中发生了错误,这帧数据将被丢弃,就像没收到一样,而发送方会在得不到回应后做相应的处理错误处理。
    展开全文
  • RS485通信和Modbus通信协议汇总

    万次阅读 多人点赞 2018-08-06 16:31:21
    RS-485上的软件层协议ModBus主要依赖于主从模式。主从模式是指在半双工通讯方式上,2个或者2个以上的设备组成的通讯系统中: (1) 至少且只有一个主机,其他的都是从机 (2) 不管任何时候,从机都不能主动向主机发送...

    1. 主从模式

    RS-485上的软件层协议ModBus主要依赖于主从模式。主从模式是指在半双工通讯方式上,2个或者2个以上的设备组成的通讯系统中:


    这里写图片描述
    (1) 至少且只有一个主机,其他的都是从机
    (2) 不管任何时候,从机都不能主动向主机发送数据
    (3) 主机具有访问从机的权限,从机不可以主动访问从机,任何一次数据交换,都要由主机发起
    (4)不管是主机还是从机,系统一旦上电,都要把自己置于接收状态(或者称为监听状态)
    主从机的数据交互,需要:
    a. 主机将自己转为发送状态
    b. 主机按照预先约定的格式发出寻址数据帧。
    所谓的约定,可是主机开发者和从机开发者约定好的规约,好,例如主机要通过从机控制接在从机的电机,主机要启动电机就往从机发0x1,停止电机就往从机发0x2。这就是一种预
    先约定好的格式,但是这样做,互换性、兼容性、通用性差,例如其他公司是约定发送0x03让电机转动,发0x04让电机停止。导致不同厂家的主机、从机不能相互通讯。用户需要的,就像网络操作,只要接入有网的网线那么计算机都能上网。
    所以说,我们需要一种大家都共同遵循的规则(可以是ModBus,也可以是TCP/IP等上层协议),这种大家认可,共同遵循的软件层协议。软件层协议主要是解决如何解析传输的数据,即传输的目的或者更加可靠的传输数据。
    半双工通讯中,都是主机寻找从机,主机的目的无非有: 主机要发数据给从机,或者主机要从从机中获取数据。
    c. 主机恢复自身的接收状态
    主机等待自身所寻址的从机作回应,也就是说从机接收到主机的寻址命令、数据后一定要回应主机,不然主机会认为从机通讯异常。回应数据包也是要按照ModBus协议规约(其实不局限ModBus,像TCP/IP也需要回应是吧!~)

    2. ModBus通讯协议

    通俗点来讲,ModBus规约了起停电机,主机要分别发送什么命令给从机。ModBus规定主从机之间数据的交互,需要遵循什么样的格式,如何保证数据在传输过程中不发生冲突。只要都遵循这个协议,那么不同厂家的主从机就可以共用了。
    ModBus一般是工作在一主多从的场景,还是这个图:
    这里写图片描述
    主机和从机之间的连线不一定是非要485来作为载体,也可以是IIC,SPI。因为ModBus是软件层的协议,它既可以规约485硬件接线方式,也可以规约其他硬件接线方式。很多资料会写”基于RS-485的ModBus通讯协议”,意思是底层的0、1数据是通过RS-485方式去传输的,0、1的意义则是通过ModBus去解析的。强调,硬件协议可以确保数据得以传输出去,软件协议保障数据的有序传输,数据不会发生冲突。
    ModBus规定:
    (1) 主从模式
    有的协议规定是多主模式,意思是系统中的设备都是主机,它们并没有主从之分,任何时刻,谁想发送数据都可以往总线上发送,例如网络通信、CAN总线通讯,自然它们自有一套防止数据冲突机制,485由于不具备冲突检测的硬件机制,所以它必须遵循主从模式。主从模式的原则是,整个系统只能有一个主机,每一个从机都必须有一个唯一的地址
    (2) 从机的地址是作为每个从机的唯一标识。地址取值是0-247,0号地址表示广播地址,广播地址由主机保留,当主机向0号地址发数据包的时候,每一个从机设备都会收到数据包。也就是说,当主机发出的寻址帧的地址是0的时候,所有从机都要执行主机要求的动作。按理说,从机收到主机的寻址帧之后,是要做出应答包的,但是现在是0号地址,也就是要回的话每台从机都要回,那么肯定会造成RS-485通讯线上的数据混乱,因此所有从机在主机发0号地址时候不予返回数据包应答。
    从机的地址有两个作用
    a. 主机向目标从机发寻址帧时其地址部分为从机地址,这样主机才可以检索到目标从机
    b. 对于主机的目标从机,当收到主机发来的非0地址时,要做出数据包应答,假设从机要返回数据包给主机,自然是要把数据包放到RS-485总线上,因为每台从机,其物理连线是在一起的,所以这就会造成其他从机认为数据是要发送给它的现象,所以在从机回复主机的数据包中,加上从机自身的地址,那么其他从机读取到这个地址值跟自己的地址不相同,就不会去响应了。
    (3) ModBus数据包的格式
    主机要寻找某台从机,需要发出相应格式的信息,这就需要谈到ModBus的两种传输方式:
    a. RTU传输方式
    RTU实际上也成为二进制方式。假设主机要发送0x23,那就是发送0010 0011,按照485通讯协议,先发高位,即1100 0100。前后分别加上起始、停止位: “起始位 1100 0100 停止位”共10位数据
    b. ASC传输方式
    同样要发送0x23,它是十六进制数,会将其拆成十位的’2’和个位的’3’,将它们的asc码依次发出去,’0’的asc码是0x32,’3’的asc是0x33,转为二进制为0011 0010和0011 0011,同样要加上停止、起始位,共20位数据
    很明显,asc传输方式比较低,但是由于它传输的是asc码,所以可以利用一些串口终端将其数值打印出来。

    特别提醒,RS-485硬件协议决定,对于每一个字节数据的传输是先发高位,再发地位,所以假设数组u8型数组revArr[2]存放着接收到的数据,那么接收端解析数据应该是u16型data = revArr[0] * 256 + revArr[1]。

     

    在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的电磁干扰,会导致信号传输错误。除此之外,RS232接口只能实现点对点通信,不具备联网功能,最大传输距离也只能达到几十米,不能满足远距离通信要求。

    而RS485则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最大距离可以到1200米,并且允许多个收发设备接到同一条总线上。随着工业应用通信越来越多,1979年施耐德电气制定了一个用于工业现场的总线协议Modbus协议,现在工业中使用RS485通信场合很多都采用Modbus协议,所以今天我们来了解下RS485通信和Modbus通信协议。

    【RS485通信】

    实际上在RS485之前RS232就已经诞生,但是RS232有几处不足的地方:

    1、接口的信号电平值较高,达到十几V,容易损坏接口电路的芯片,而且和TTL电平不兼容,因此和单片机电路接起来的话必须加转换电路。

    2、传输速率有局限,不可以过高,一般到几十Kb/s就到极限了。

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

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

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

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

    1.逻辑“1”以两线间的电压差为+(2—6)V表示;逻辑“0”以两线间的电压差为-(2—6)V表示。接口信号电平比RS232C降低了,就不易损坏电路的芯片,且该电平与TTL电平兼容,可方便与TTL电路连接。

    2.RS485通信速度快,数据最高传输速率为10Mbps以上

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

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

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

    RS-485采用平衡发送和差分接收,因此具有抑制共模干扰的能力。RS-485采用半双工工作方式,任何时候只能有一点处于发送状态,因此,发送电路须由使能信号加以控制。RS-485用于多点互连时非常方便,可以省掉许多信号线。应用RS-485可以联网构成分布式系统,其允许最多并联32台驱动器和32台接收器。在RS232或RS485设备联成的设备网中,如果设备数量超过2台,就必须使用RS485做通讯介质,RS485网的设备间要想互通信息只有通过“主(Master)”设备中转才能实现,这个主设备通常是PC,而这种设备网中只允许存在一个主设备,其余全部是从(Slave)设备。

    RS485有两线制和四线制两种接线,四线制只能实现点对点的通信方式,现很少采用,现在多采用的是两线制接线方式,这种接线方式为总线式拓朴结构在同一总线上最多可以挂接32个结点。在RS485通信网络中一般采用的是主从通信方式,即一个主机带多个从机。很多情况下,连接RS-485通信链路时只是简单地用一对双绞线将各个接口的“A”、“B”端连接起来。而忽略了信号地的连接,这种连接方法在许多场合是能正常工作的,但却埋下了很大的隐患,这有二个原因:

    (1)共模干扰问题:RS-485接口采用差分方式传输信号方式,并不需要相对于某个参照点来检测信号,系统只需检测两线之间的电位差就可以了。但人们往往忽视了收发器有一定的共模电压范围,RS-485收发器共模电压范围为-7~+12V,只有满足上述条件,整个网络才能正常工作。当网络线路中共模电压超出此范围时就会影响通信的稳定可靠,甚至损坏接口。

    (2)EMI问题:发送驱动器输出信号中的共模部分需要一个返回通路,如没有一个低阻的返回通道(信号地),就会以辐射的形式返回源端,整个总线就会像一个巨大的天线向外辐射电磁波。由于PC机默认的只带有RS232接口,有两种方法可以得到PC上位机的RS485电路:(1)通过RS232/RS485转换电路将PC机串口RS232信号转换成RS485信号,对于情况比较复杂的工业环境最好是选用防浪涌带隔离珊的产品。(2)通过PCI多串口卡,可以直接选用输出信号为RS485类型的扩展卡。

    【Modbus通信协议】

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

    Modbus具有以下几个特点:

    (1)标准、开放,用户可以免费、放心地使用Modbus协议,不需要交纳许可证费,也不会侵犯知识产权。目前,支持Modbus的厂家超过400家,支持Modbus的产品超过600种。

    (2)Modbus可以支持多种电气接口,如RS-232、RS-485等,还可以在各种介质上传送,如双绞线、光纤、无线等。

    (3)Modbus的帧格式简单、紧凑,通俗易懂。用户使用容易,厂商开发简单。

    R【TU协议帧数据】

    Modbus有两种通信传输方式,一种是ASCII模式,一种是RTU模式。由于ASCII模式的数据字节是7bit数据位,51单片机无法实现,而且应用也相对较少,所以这里我们只用RTU模式。两种模式相似,会用一种另外一种也就会了。一条典型的RTU数据帧如图所示。

    和我们实用串口通信程序类似,我们一次发送的数据帧必须是作为一个连续的数据流进行传输。我们在实用串口通信程序中采用的方法是定义30ms,如果接收到的数据超过了30ms还没有接收到下一个字节,我们就认为这次的数据结束。而Modbus的RTU模式规定不同数据帧之间的间隔是3.5个字节通信时间以上。如果在一帧数据完成之前有超过3.5个字节时间的停顿,接收设备将刷新当前的消息并假定下一个字节是一个新的数据帧的开始。同样的,如果一个新消息在小于3.5个字节时间内接着前边一个数据开始的,接收的设备将会认为它是前一帧数据的延续。这将会导致一个错误,因此大家看RTU数据帧最后还有16bit的CRC校验。

    起始位和结束符:前后都至少有3.5个字节的时间间隔,起始位和结束符实际上没有任何数据,T1-T2-T3-T4代表的是时间间隔3.5个字节以上的时间,而真正有意义的第一个字节是设备地址。

    设备地址:在多机通信的时候,数据那么多,我们依靠什么判断这个数据帧是哪个设备的呢?没错,就是依靠这个设备地址字节。每个设备都有一个自己的地址,当设备接收到一帧数据后,程序首先对设备地址字节进行判断比较,如果与自己的地址不同,则对这帧数据直接不予理会,如果如果与自己的地址相同,就要对这帧数据进行解析,按照之后的功能码执行相应的功能。如果地址是0x00,则认为是一个广播命令,就是所有的从机设备都要执行的指令。

    功能代码:在第二个字节功能代码字节中,Modbus规定了部分功能代码,此外也保留了一部分功能代码作为备用或者用户自定义,这些功能码大家不需要去记忆,甚至都不用去看,直到你有用到的那天再过来查这个表格即可。

    CRC校验:CRC校验是一种数据算法,是用来校验数据对错的。CRC校验函数把一帧数据除最后两个字节外,前边所有的字节进行特定的算法计算,计算完后生成了一个16bit的数据,作为CRC校验码,添加在一帧数据的最后。接收方接收到数据后,同样会把前边的字节进行CRC计算,计算完了再和发过来的CRC的16bit的数据进行比较,如果相同则认为数据正常,没有出错,如果比较不相同,则说明数据在传输中发生了错误,这帧数据将被丢弃,就像没收到一样,而发送方会在得不到回应后做相应的处理错误处理。

    鼎酷IOT部落(dkiot888)是由鼎易鸿基&万酷电子联合出品,专注于为大家提供物联网精髓的新媒体平台。我们关注物联网行业最新动态,专注做您的物联网口袋知识库,旨在搭建共同学习的知识平台,让您学得方便,聊得畅快;鼎酷IOT部落用心做大家的“良仆”。各项内容鼎酷IOT部落微信公众号同步。物联网行业顶尖产品咨询也欢迎留言。

    展开全文
  • 第18章 RS485通信和Modbus协议

    千次阅读 2017-03-10 17:32:07
    本教材现以连载的方式由网络发布,并将于2014年由清华大学出版社出版最终完整版,版权归作者清华大学出版社所有。本着开源、分享的理念,本教材可以自由传播及学习使用,但是务必请注明出处来自金沙滩工作室  在...
    本教材现以连载的方式由网络发布,并将于2014年由清华大学出版社出版最终完整版,版权归作者和清华大学出版社所有。本着开源、分享的理念,本教材可以自由传播及学习使用,但是务必请注明出处来自金沙滩工作室

      在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的电磁干扰,会导致信号传输错误。除此之外,RS232接口只能实现点对点通信,不具备联网功能,最大传输距离也只能达到几十米,不能满足远距离通信要求。而RS485则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最大距离可以到1200米,并且允许多个收发设备接到同一条总线上。随着工业应用通信越来越多,1979年施耐德电气制定了一个用于工业现场的总线协议Modbus协议,现在工业中使用RS485通信场合很多都采用Modbus协议,本节课我们要讲解一下RS485通信和Modbus协议。
      单单使用一块KST-51开发板是不能够进行RS485实验的,应很多同学的要求,把这节课作为扩展课程讲一下,如果要做本课相关实验,需要自行购买USB转485通信模块。
    18.1RS485通信
    实际上在RS485之前RS232就已经诞生,但是RS232有几处不足的地方:
    1、接口的信号电平值较高,达到十几V,容易损坏接口电路的芯片,而且和TTL电平不兼容,因此和单片机电路接起来的话必须加转换电路。
    2、传输速率有局限,不可以过高,一般到几十Kb/s就到极限了。
    3、接口使用信号线和GND与其他设备形成共地模式的通信,这种共地模式传输容易产生干扰,并且抗干扰性能也比较弱。
    4、传输距离有限,最多只能通信几十米。
    5、通信的时候只能两点之间进行通信,不能够实现多机联网通信。
      针对RS232接口的不足,就不断出现了一些新的接口标准,RS485就是其中之一,他具备以下的特点:
    1、我们在讲A/D的时候,讲过差分信号输入的概念,同时也介绍了差分输入的好处,最大的优势是可以抑制共模干扰。尤其工业现场的环境比较复杂,干扰比较多,所以通信如果采用的是差分方式,就可以有效的抑制共模干扰。而RS485就是一种差分通信方式,它的通信线路是两根,通常用A和B或者D+和D-来表示。逻辑“1”以两线之间的电压差为+(0.2~6)V表示,逻辑“0”以两线间的电压差为-(0.2~6)V来表示,是一种典型的差分通信。
    2、RS485通信速度快,最大传输速度可以达到10Mb/s以上。
    3、RS485内部的物理结构,采用的是平衡驱动器和差分接收器的组合,抗干扰能力也大大增加。
    4、传输距离最远可以达到1200米左右,但是他的传输速率和传输距离是成反比的,只有在100Kb/s以下的传输速度,才能达到最大的通信距离,如果需要传输更远距离可以使用中继。
    5、可以在总线上进行联网实现多机通信,总线上允许挂多个收发器,从现有的RS485芯片来看,有可以挂32、64、128、256等不同个设备的驱动器。
      RS485的接口非常简单,和RS232所使用的MAX232是类似的,只需要一个RS485转换器,就可以直接和我们单片机的UART串行接口连接起来,并且完全使用的是和UART一致的异步串行通信协议。但是由于RS485是差分通信,因此接收数据和发送数据是不能同时进行的,也就是说它是一种半双工通信。那我们如何判断什么时候发送,什么时候接收呢?
      RS485类的芯片很多,这节课我们以MAX485为例讲解RS485通信,如图18-1所示。

    图18-1 MAX485硬件接口

      MAX485是美信(Maxim)推出的一款常用RS485转换器。其中5脚和8脚是电源引脚,6脚和7脚就是485通信中的A和B两个引脚,而1脚和4脚分别接到我们单片机的RXD和TXD引脚上,直接使用单片机UART进行数据接收和发送。而2脚和3脚就是方向引脚了,其中2脚是低电平使能接收器,3脚是高电平使能输出驱动器。我们把这两个引脚连到一起,平时不发送数据的时候,保持这两个引脚是低电平,让MAX485处于接收状态,当需要发送数据的时候,把这个引脚拉高,发送数据,发送完毕后再拉低这个引脚就可以了。为了提高RS485的抗干扰性能,需要在靠近MAX485的A和B引脚之间并接一个电阻,这个电阻阻值从100欧到1K都可以。
      在这里我们还要介绍一下如何使用KST-51单片机开发板进行外围扩展实验。我们的开发板只能把基本的功能给同学们做出来提供实验练习,但是同学们学习的脚步不应该停留在这个实验板上。如果想进行更多的实验,就可以通过单片机开发板的扩展接口进行扩展实验。大家可以看到蓝绿色的单片机座周围有32个插针,这32个插针就是把单片机的32个IO引脚全部都引出来了。在原理图上体现出来的就是我们的J4、J5、J6、J7这4个器件,如图18-2所示。

    图18-2 单片机扩展接口

      这32个IO口不是所有的IO口都可以用来对外扩展,其中既作为数据输出,又可以作为数据输入的引脚是不可以用的,比如P3.2、P3.4、P3.6引脚,这三个引脚是不可用的。比如P3.2这个引脚,如果我们用来扩展,发送的信号如果和DS18B20的时序吻合,会导致DS18B20拉低引脚,影响通信。除这3个IO口以外的其他29个IO口,都可以使用杜邦线接上插针,扩展出来使用。当然了,如果把当前的IO口应用于扩展功能了,板子上的相应的功能就实现不了了,也就是说需要扩展功能和板载功能二选一。
      在进行RS485实验中,我们通信用的引脚必须是P3.0和P3.1,此外还有一个方向控制引脚,我们使用杜邦线将其连接到P1.7上去。RS485的另外一端,大家可以使用一个USB转485模块,用双绞线把开发板和模块上的A和B分别对应连起来,USB那头插入电脑,然后就可以进行通信了。
    学习了第13章的实用串口通信的方法和程序后,做这种串口通信的方法就很简单了,基本是一致的。我们使用实用串口通信的思路,做了一个简单的程序,通过串口调试助手下发任意个字符,单片机接收到后在末尾添加“回车+换行”符后再送回,在调试助手上重新显示出来,先把程序贴出来。
      程序中需要注意的一点是:因为平常都是将485设置为接收状态,只有在发送数据的时候才将485改为发送状态,所以在UartWrite()函数开头将485方向引脚拉高,函数退出前再拉低。但是这里有一个细节,就是单片机的发送和接收中断产生的时刻都是在停止位的一半上,也就是说每当停止位传送了一半的时候,RI或TI就已经置位并且马上进入中断(如果中断使能的话)函数了,接收的时候自然不会存在问题,但发送的时候就不一样了:当紧接这向SBUF写入一个字节数据时,UART硬件会在完成上一个停止位的发送后,再开始新字节的发送,但如果此时不是继续发送下一个字节,而是已经发送完毕了,要停止发送并将485方向引脚拉低以使485重新处于接收状态时就有问题了,因为这时候最后的这个停止位实际只发送了一半,还没有完全完成,所以就有了UartWrite()函数内DelayX10us(5)这个操作,这是人为的增加了延时50us,这50us的时间正好让剩下的一半停止位完成,那么这个时间自然就是由通信波特率决定的了,为波特率周期的一半。
     
    /***********************RS485.c文件程序源代码*************************/
    #include 
           
            
    #include 
            
             
    sbit RS485_DIR = P1^7;  //RS485方向选择引脚
    bit flagOnceTxd = 0;  //单次发送完成标志,即发送完一个字节
    bit cmdArrived = 0;   //命令到达标志,即接收到上位机下发的命令
    unsigned char cntRxd = 0;
    unsigned char pdata bufRxd[40]; //串口接收缓冲区
    void ConfigUART(unsigned int baud)  //串口配置函数,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
    }
    unsigned char UartRead(unsigned char *buf, unsigned char len) //串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度
    {
        unsigned char i;
       
        if (len > cntRxd) //读取长度大于接收到的数据长度时,
        {
            len = cntRxd; //读取长度设置为实际接收到的数据长度
        }
        for (i=0; i
             
               0)  //接收计数器大于零时,监控总线空闲时间
        {
            if (cntbkp != cntRxd)  //接收计数器改变,即刚接收到数据时,清零空闲计时
            {
                cntbkp = cntRxd;
                idletmr = 0;
            }
            else
            {
                if (idletmr < 30)  //接收计数器未改变,即总线空闲时,累积空闲时间
                {
                    idletmr += ms;
                    if (idletmr >= 30)  //空闲时间超过30ms即认为一帧命令接收完毕
                    {
                        cmdArrived = 1; //设置命令到达标志
                    }
                }
            }
        }
        else
        {
            cntbkp = 0;
        }
    }
    void InterruptUART() interrupt 4  //UART中断服务函数
    {
                if (RI)  //接收到字节
        {
            RI = 0;   //手动清零接收中断标志位
            if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
            {
                bufRxd[cntRxd++] = SBUF; //保存接收字节,并递增计数器
            }
                }
                if (TI)  //字节发送完毕
        {
                    TI = 0;   //手动清零发送中断标志位
            flagOnceTxd = 1;  //设置单次发送完成标志
                }
    }
    /***********************main.c文件程序源代码*************************/
    #include 
              
               
    unsigned char T0RH = 0;  //T0重载值的高字节
    unsigned char T0RL = 0;  //T0重载值的低字节
    void ConfigTimer0(unsigned int ms);
    extern void ConfigUART(unsigned int baud);
    extern void UartRxMonitor(unsigned char ms);
    extern void UartDriver();
    void main ()
    {
        EA = 1;           //开总中断
        ConfigTimer0(1);  //配置T0定时1ms
        ConfigUART(9600); //配置波特率为9600
       
        while(1)
        {
            UartDriver();
        }
    }
    void ConfigTimer0(unsigned int ms)  //T0配置函数
    {
        unsigned long tmp;
       
        tmp = 11059200 / 12;      //定时器计数频率
        tmp = (tmp * ms) / 1000;  //计算所需的计数值
        tmp = 65536 - tmp;        //计算定时器重载值
        tmp = tmp + 34;           //修正中断响应延时造成的误差
       
        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
    }
    void InterruptTimer0() interrupt 1  //T0中断服务函数
    {
        TH0 = T0RH;  //定时器重新加载重载值
        TL0 = T0RL;
        UartRxMonitor(1);  //串口接收监控
    }
              
             
            
           
      现在看这种串口程序,是不是感觉很简单了呢?串口通信程序我们反反复复的使用,加上随着我们学习的模块越来越多,实践的越来越多,原先感觉很复杂的东西,现在就会感到简单了。我们的下载程序模块用的是COM4,而USB转485虚拟的是COM5,通信的时候我们用的是COM5口,如图18-3所示。

    图18-3 RS485串行通信
    18.2Modbus通信协议介绍
          我们前边学习UART、I2C、SPI这些通信协议,都是最底层的协议,是“位”级别的协议。而我们在学习13章实用串口通信程序的时候,我们通过串口发给单片机三条指令,让单片机做了三件不同的事情,分别是"buzz on"、"buzz off"、和"showstr"。随着我们系统复杂性的增加,我们希望可以实现更多的指令。而指令越来越多,带来的后果就是非常杂乱无章,尤其是这个人喜欢写成"buzz on"、"buzz off",而另外一个人喜欢写成"on buzz"、"off buzz"。导致不同开发人员写出来的代码指令不兼容,不同厂家的产品不能挂到一条总线上通信。
    随着这种矛盾的日益严重,就会有聪明人提出更合理的解决方案,提出一些标准来,今后我们的编程必须按照这个标准来,这种标准也是一种通信协议,但是和UART、I2C、SPI通信协议不同的是,这种通信协议是字节级别的,叫做应用层通信协议。在1979年由Modicon(现为施耐德电气公司的一个品牌)提出了全球第一个真正用于工业现场总线的协议,就是Modbus协议。
    18.2.1Modbus协议特点
           Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其他设备之间可以通信,已经成为一种工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。这种协议定义了一种控制器能够认识使用的数据结构,而不管它们是经过何种网络进行通信的。它描述了控制器请求访问其他设备的过程,如何回应来自其他设备的请求,以及怎样侦测错误记录,它制定了通信数据的格局和内容的公共格式。
      在进行多机通信的时候,Modbus协议规定每个控制器必须要知道他们的设备地址,识别按照地址发送过来的数据,决定是否要产生动作,产生何种动作,如果要回应,控制器将生成的反馈信息用Modbus协议发出。
    Modbus协议允许在各种网络体系结构内进行简单通信,每种设备(PLC、人机界面、控制面板、驱动程序、输入输出设备)都能使用Modbus协议来启动远程操作,一些网关允许在几种使用Modbus协议的总线或网络之间的通信,如图18-4所示。

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

             Modbus协议的整体架构和格式比较复杂和庞大,在我们的课程里,我们重点介绍数据帧结构和数据通信控制方式,作为一个入门级别的了解。如果大家要详细了解,或者使用Modbus开发相关设备,可以查阅相关的国标文件再进行深入学习。
    1.2.2RTU协议帧数据
             Modbus有两种通信传输方式,一种是ASCII模式,一种是RTU模式。由于ASCII模式的数据字节是7bit数据位,51单片机无法实现,而且应用也相对较少,所以这里我们只用RTU模式。两种模式相似,会用一种另外一种也就会了。一条典型的RTU数据帧如图18-5所示。
      
    图18-5 RTU数据帧
        和我们实用串口通信程序类似,我们一次发送的数据帧必须是作为一个连续的数据流进行传输。我们在实用串口通信程序中采用的方法是定义30ms,如果接收到的数据超过了30ms还没有接收到下一个字节,我们就认为这次的数据结束。而Modbus的RTU模式规定不同数据帧之间的间隔是3.5个字节通信时间以上。如果在一帧数据完成之前有超过3.5个字节时间的停顿,接收设备将刷新当前的消息并假定下一个字节是一个新的数据帧的开始。同样的,如果一个新消息在小于3.5个字节时间内接着前边一个数据开始的,接收的设备将会认为它是前一帧数据的延续。这将会导致一个错误,因此大家看RTU数据帧最后还有16bit的CRC校验。
       起始位和结束符:图18-5上代表的是一个数据帧,前后都至少有3.5个字节的时间间隔,起始位和结束符实际上没有任何数据,T1-T2-T3-T4代表的是时间间隔3.5个字节以上的时间,而真正有意义的第一个字节是设备地址。
    设备地址:很多同学不理解,在多机通信的时候,数据那么多,我们依靠什么判断这个数据帧是哪个设备的呢?没错,就是依靠这个设备地址字节。每个设备都有一个自己的地址,当设备接收到一帧数据后,程序首先对设备地址字节进行判断比较,如果与自己的地址不同,则对这帧数据直接不予理会,如果如果与自己的地址相同,就要对这帧数据进行解析,按照之后的功能码执行相应的功能。如果地址是0x00,则认为是一个广播命令,就是所有的从机设备都要执行的指令。
           功能代码:在第二个字节功能代码字节中,Modbus规定了部分功能代码,此外也保留了一部分功能代码作为备用或者用户自定义,这些功能码大家不需要去记忆,甚至都不用去看,直到你有用到的那天再过来查这个表格即可,如表18-1所示。
      表18-1 Modbus功能码
    功能码

    名称
    作用
    01
    读取线圈状态
    取得一组逻辑线圈的当前状态(ON/OFF)
    02
    读取输入状态
    取得一组开关输入的当前状态(ON/OFF)
    03
    读取保持寄存器
    在一个或多个保持寄存器中取得当前的二进制值
    04
    读取输入寄存器
    在一个或多个输入寄存器中取得当前的二进制值
    05
    强置单线圈
    强置一个逻辑线圈的通断状态
    06
    预置单寄存器
    把具体二进值装入一个保持寄存器
    07
    读取异常状态
    取得8 个内部线圈的通断状态,这8 个线圈的地址由控制器决定,用户逻辑可以将这些线圈定义,以说明从机状态,短报文适宜于迅速读取状态
    08
    回送诊断校验
    把诊断校验报文送从机,以对通信处理进行评鉴
    09
    编程(只用于484)
    使主机模拟编程器作用,修改PC从机逻辑
    10
    控询(只用于484)
    可使主机与一台正在执行长程序任务从机通信,探询该从机是否已完成其操作任务,仅在含有功能码 9 的报文发送后,本功能码才发送
    11
    读取事件计数
    可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时
    12
    读取通信事件记录
    可是主机检索每台从机的ModBus事务处理通信事件记录。如果某项事务处理完成,记录会给出有关错误
    13
    编程(184/384 484 584 )
    可使主机模拟编程器功能修改PC从机逻辑
    14
    探询(184/384 484 584)
    可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能13的报文发送后,本功能码才得发送
    15
    强置多线圈
    强置一串连续逻辑线圈的通断
    16
    预置多寄存器
    把具体的二进制值装入一串连续的保持寄存器
    17
    报告从机标识
    可使主机判断编址从机的类型及该从机运行指示灯的状态
    18
    884 和MICRO 84
    可使主机模拟编程功能,修改PC状态逻辑
    19
    重置通信链路
    发生非可修改错误后,是从机复位于已知状态,可重置顺序字节
    20
    读取通用参数(584L)
    显示扩展存储器文件中的数据信息
        21
    写入通用参数(584L)
    把通用参数写入扩展存储文件,或修改
    22~64
    保留作扩展功能备用

    65~72
    保留以备用户功能所用
    留作用户功能的扩展编码
    73~119
    非法功能

    120~127
    保留
    留作内部作用
    128~255
    保留
    用于异常应答
           我们程序对功能码的处理,就是程序来检测这个字节的数值,然后根据其数值来做相应的功能处理。
    数据:跟在功能代码后边的是n个8bit的数据。这个n值的到底是多少,是功能代码来确定的,不同的功能代码后边跟的数据数量不同。举个例子,如果功能码是0x03,也就是读保持寄存器,那么主机发送数据n的组成部分就是:2个字节的寄存器起始地址,加2个字节的寄存器数量N*。从机数据n的组成部分是:1个字节的字节数,因为我们回复的寄存器的值是2个字节,所以这个字节数也就是2N*个,再加上2N*个寄存器的值,如图18-6所示。
      
    图18-6 读保持寄存器数据结构
           CRC校验:CRC校验是一种数据算法,是用来校验数据对错的。CRC校验函数把一帧数据除最后两个字节外,前边所有的字节进行特定的算法计算,计算完后生成了一个16bit的数据,作为CRC校验码,添加在一帧数据的最后。接收方接收到数据后,同样会把前边的字节进行CRC计算,计算完了再和发过来的CRC的16bit的数据进行比较,如果相同则认为数据正常,没有出错,如果比较不相同,则说明数据在传输中发生了错误,这帧数据将被丢弃,就像没收到一样,而发送方会在得不到回应后做相应的处理错误处理。
            RTU模式的每个字节的位是这样分布的:1个起始位、8个数据位,最小有效位先发送、1个奇偶校验位(如果无校验则没有这一位)、1位停止位(有校验位时)或者2个停止位(无校验位时)。
    18.3 Modbus多机通信例程
       给从机下发不同的指令,从机去执行不同的操作,这个就是判断一下功能码即可,和我们前边学的实用串口例程是类似的。多机通信,无非就是添加了一个设备地址判断而已,难度也不是很大。我们找了一个Modbus调试精灵,通过设置设备地址,读写寄存器的地址以及数值数量等参数,可以直接替代串口调试助手,比较方便的下发多个字节的数据,如图18-7所示。我们先来就图中的设置和数据来对Modbus做进一步的分析,图中的数据来自于调试精灵与我们接下来要讲的例程之间的交互。
      
    图18-7 Modbus调试精灵

       如图:我们的USB转485模块虚拟出的是COM5,波特率9600,无校验位,数据位是8位,1位停止位,设备地址假设为1。
       写寄存器的时候,如果我们要把01写到一个地址是0000的寄存器地址里,点一下“写入”,就会出现发送指令:01 06 00 00 00 01 48 0A。我们来分析一下这帧数据,其中01是设备地址,06是功能码,代表写寄存器这个功能,后边跟00 00表示的是要写入的寄存器的地址,00 01就是要写入的数据,48 0A就是CRC校验码,这是软件自动算出来了。而根据Modbus协议,当写寄存器的时候,从机成功完成该指令的操作后,会把主机发送的指令直接返回,我们的调试精灵会接收到这样一帧数据:01 06 00 00 00 01 48 0A。
       假如我们现在要从寄存器地址0002开始读取寄存器,并且读取的数量是2个。点一下“读出”,就会出现发送指令:01 03 00 02 00 02 65 CB。其中01是设备地址,03是功能码,代表写寄存器这个功能,00 02就是读寄存器的起始地址,后一个00 02就是要读取2个寄存器的数值,65 CB就是CRC校验。而接收到的数据是:01 03 04 00 00 00 00 FA 33。其中01是设备地址,03是功能码,04代表的是后边读到的数据字节数是4个,00 00 00 00分别是地址为00 02和00 03的寄存器内部的数据,而FA 33就是CRC校验了。
       似乎越来越明朗了,所谓的Modbus这种通信协议,无非就是主机下发了不同的指令,从机根据指令的判断来执行不同的操作而已。由于我们的开发板没有Modbus功能码那么多相应的功能,我们在程序中定义了一个数组regGroup[5],相当于5个寄存器,此外又定义了第6个寄存器,控制蜂鸣器,通过下发不同的指令我们改变寄存器组的数据或者改变蜂鸣器的开关状态。在Modbus协议里寄存器的地址和数值都是16位的,即2个字节,我们默认高字节是0x00,低字节就是数组regGroup对应的值。其中地址0x0000到0x0004对应的就是regGroup数组中的元素,我们写入的同时把数字又显示到我们的LCD1602液晶上,而0x0005这个地址,写入0x00,蜂鸣器就不响,写入任何其他数字,蜂鸣器就报警。我们单片机的主要工作也就是解析串口接收的数据执行不同操作,也就是主要在RS485.C这个文件中了。
     
    /***********************RS485.c文件程序源代码*************************/
    #include 
                      
                       
    #include 
                       
                        
    sbit RS485_DIR = P1^7;  //RS485方向选择引脚
    bit flagOnceTxd = 0;  //单次发送完成标志,即发送完一个字节
    bit cmdArrived = 0;   //命令到达标志,即接收到上位机下发的命令
    unsigned char cntRxd = 0;
    unsigned char pdata bufRxd[40]; //串口接收缓冲区
    unsigned char regGroup[5];  //Modbus寄存器组,地址为0x00~0x04
    extern bit flagBuzzOn;
    extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
    extern unsigned int GetCRC16(unsigned char *ptr,  unsigned char len);
    void ConfigUART(unsigned int baud)  //串口配置函数,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
    }
    unsigned char UartRead(unsigned char *buf, unsigned char len) //串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度
    {
        unsigned char i;
       
        if (len > cntRxd) //读取长度大于接收到的数据长度时,
        {
            len = cntRxd; //读取长度设置为实际接收到的数据长度
        }
        for (i=0; i
                        
                         > 8;
                crcl = crc & 0xFF;
                if ((buf[len-2] == crch) && (buf[len-1] == crcl)) //判断CRC校验是否正确
                {
                    switch (buf[1]) //按功能码执行操作
                    {
                        case 0x03:  //读取一个或连续的寄存器
                            if ((buf[2] == 0x00) && (buf[3] <= 0x05)) //寄存器地址支持0x0000~0x0005
                            {
                                if (buf[3] <= 0x04)
                                {
                                    i = buf[3];      //提取寄存器地址
                                    cnt = buf[5];    //提取待读取的寄存器数量
                                    buf[2] = cnt*2;  //读取数据的字节数,为寄存器数*2,因Modbus定义的寄存器为16位
                                    len = 3;
                                    while (cnt--)
                                    {
                                        buf[len++] = 0x00;      //寄存器高字节补0
                                        buf[len++] = regGroup[ i++]; //低字节
                                    }
                                }
                                else  //地址0x05为蜂鸣器状态
                                {
                                    buf[2] = 2;  //读取数据的字节数
                                    buf[3] = 0x00;
                                    buf[4] = flagBuzzOn;
                                    len = 5;
                                }
                                break;
                            }
                            else  //寄存器地址不被支持时,返回错误码
                            {
                                buf[1] = 0x83;  //功能码最高位置1
                                buf[2] = 0x02;  //设置异常码为02-无效地址
                                len = 3;
                                break;
                            }
                            
                        case 0x06:  //写入单个寄存器
                            if ((buf[2] == 0x00) && (buf[3] <= 0x05)) //寄存器地址支持0x0000~0x0005
                            {
                                if (buf[3] <= 0x04)
                                {
                                    i = buf[3];             //提取寄存器地址
                                    regGroup[ i] = buf[5];   //保存寄存器数据
                                    cnt = regGroup[ i] >> 4; //显示到液晶上
                                    if (cnt >= 0xA)
                                        str[0] = cnt - 0xA + 'A';
                                    else
                                        str[0] = cnt + '0';
                                    cnt = regGroup[ i] & 0x0F;
                                    if (cnt >= 0xA)
                                        str[1] = cnt - 0xA + 'A';
                                    else
                                        str[1] = cnt + '0';
                                    str[2] = '\0';
                                    LcdShowStr(i*3, 0, str);
                                }
                                else  //地址0x05为蜂鸣器状态
                                {
                                    flagBuzzOn = (bit)buf[5]; //寄存器值转换为蜂鸣器的开关
                                }
                                len -= 2; //长度-2以重新计算CRC并返回原帧
                                break;
                            }
                            else  //寄存器地址不被支持时,返回错误码
                            {
                                buf[1] = 0x86;  //功能码最高位置1
                                buf[2] = 0x02;  //设置异常码为02-无效地址
                                len = 3;
                                break;
                            }
                            
                        default:  //其它不支持的功能码
                            buf[1] |= 0x80;  //功能码最高位置1
                            buf[2] = 0x01;   //设置异常码为01-无效功能
                            len = 3;
                            break;
                    }
                    crc = GetCRC16(buf, len); //计算CRC校验值
                    buf[len++] = crc >> 8;    //CRC高字节
                    buf[len++] = crc & 0xFF;  //CRC低字节
                    UartWrite(buf, len);      //发送响应帧
                }
            }
        }
    }
    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 < 5)  //接收计数器未改变,即总线空闲时,累积空闲时间
                {
                    idletmr += ms;
                    if (idletmr >= 5)  //空闲时间超过4个字节传输时间即认为一帧命令接收完毕
                    {
                        cmdArrived = 1; //设置命令到达标志
                    }
                }
            }
        }
        else
        {
            cntbkp = 0;
        }
    }
    void InterruptUART() interrupt 4  //UART中断服务函数
    {
                if (RI)  //接收到字节
        {
            RI = 0;   //手动清零接收中断标志位
            if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
            {
                bufRxd[cntRxd++] = SBUF; //保存接收字节,并递增计数器
            }
                }
                if (TI)  //字节发送完毕
        {
                    TI = 0;   //手动清零发送中断标志位
            flagOnceTxd = 1;  //设置单次发送完成标志
        }
    }
    /***********************lcd1602.c文件程序源代码*************************/
    #include 
                         
                          
    #define LCD1602_DB   P0
    sbit LCD1602_RS = P1^0;
    sbit LCD1602_RW = P1^1;
    sbit LCD1602_E  = P1^5;
    void LcdWaitReady()  //等待液晶准备好
    {
        unsigned char sta;
       
        LCD1602_DB = 0xFF;
        LCD1602_RS = 0;
        LCD1602_RW = 1;
        do
        {
            LCD1602_E = 1;
            sta = LCD1602_DB; //读取状态字
            LCD1602_E = 0;
        } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止
    }
    void LcdWriteCmd(unsigned char cmd)  //写入命令函数
    {
        LcdWaitReady();
        LCD1602_RS = 0;
        LCD1602_RW = 0;
        LCD1602_DB = cmd;
        LCD1602_E  = 1;
        LCD1602_E  = 0;
    }
    void LcdWriteDat(unsigned char dat)  //写入数据函数
    {
        LcdWaitReady();
        LCD1602_RS = 1;
        LCD1602_RW = 0;
        LCD1602_DB = dat;
        LCD1602_E  = 1;
        LCD1602_E  = 0;
    }
    void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str)  //显示字符串,屏幕起始坐标(x,y),字符串指针str
    {
        unsigned char addr;
       
        //由输入的显示坐标计算显示RAM的地址
        if (y == 0)
            addr = 0x00 + x; //第一行字符地址从0x00起始
        else
            addr = 0x40 + x; //第二行字符地址从0x40起始
       
        //由起始显示RAM地址连续写入字符串
        LcdWriteCmd(addr | 0x80); //写入起始地址
        while (*str != '\0')      //连续写入字符串数据,直到检测到结束符
        {
            LcdWriteDat(*str);
            str++;
        }
    }
    void LcdInit()  //液晶初始化函数
    {
        LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口
        LcdWriteCmd(0x0C);  //显示器开,光标关闭
        LcdWriteCmd(0x06);  //文字不动,地址自动+1
        LcdWriteCmd(0x01);  //清屏
    }
       关于CRC校验的算法,如果不是专门学习校验算法本身,大家可以不去研究这个程序的细节,文档直接给我们提供了函数,我们直接调用即可。
    /***********************CRC16.c文件程序源代码*************************/
    unsigned int GetCRC16(unsigned char *ptr,  unsigned char len)
    {
        unsigned int index;
        unsigned char crch = 0xFF;  //高CRC字节
        unsigned char crcl = 0xFF;  //低CRC字节
        unsigned char code TabH[] = {  //CRC高位字节值表
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  
            0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  
            0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  
            0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  
            0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40  
        } ;  
        unsigned char code TabL[] = {  //CRC低位字节值表
            0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,  
            0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,  
            0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,  
            0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,  
            0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,  
            0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,  
            0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,  
            0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,  
            0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,  
            0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,  
            0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,  
            0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,  
            0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,  
            0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,  
            0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,  
            0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,  
            0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,  
            0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,  
            0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,  
            0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,  
            0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,  
            0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,  
            0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,  
            0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,  
            0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,  
            0x43, 0x83, 0x41, 0x81, 0x80, 0x40  
        } ;
        while (len--)  //计算指定长度的CRC
        {
            index = crch ^ *ptr++;
            crch = crcl ^ TabH[ index];
            crcl = TabL[ index];
        }
       
        return ((crch<<8) | crcl);  
    }                           
    /***********************main.c文件程序源代码*************************/
    void ConfigTimer0(unsigned int ms);
    extern void LcdInit();
    extern void ConfigUART(unsigned int baud);
    extern void UartRxMonitor(unsigned char ms);
    extern void UartDriver();
    void main ()
    {
        EA = 1;           //开总中断
        ConfigTimer0(1);  //配置T0定时1ms
        ConfigUART(9600); //配置波特率为9600
        LcdInit();        //初始化液晶
       
        while(1)
        {
            UartDriver();
        }
    }
    void ConfigTimer0(unsigned int ms)  //T0配置函数
    {
        unsigned long tmp;
       
        tmp = 11059200 / 12;      //定时器计数频率
        tmp = (tmp * ms) / 1000;  //计算所需的计数值
        tmp = 65536 - tmp;        //计算定时器重载值
        tmp = tmp + 34;           //修正中断响应延时造成的误差
       
        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
    }
    void InterruptTimer0() interrupt 1  //T0中断服务函数
    {
        TH0 = T0RH;  //定时器重新加载重载值
        TL0 = T0RL;
        if (flagBuzzOn)  //蜂鸣器鸣叫或关闭
            BUZZ = ~BUZZ;
        else
            BUZZ = 1;
        UartRxMonitor(1);  //串口接收监控
    }
                         
                        
                       
                      
     





    展开全文
  • 单片机:RS485 通信Modbus 协议

    千次阅读 2021-08-12 11:40:22
    文章目录RS485 通信Modbus 协议单片机 RS485 通信接口、控制线、原理图及程序实例 RS485 通信Modbus 协议 在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的...
  • 单片机:RS485 通信Modbus 协议.pdf
  • RS485通信Modbus协议 附源码下载

    千次阅读 2020-05-12 14:42:30
    RS485通信Modbus协议 附源码及视频教程下载 RS485通信的特点 1、采用差分信号。 2、RS485通信速率快,最大传输速率可以达到10Mb/s以上。 3、RS485内部采用平衡驱动器差分接收器的组合,抗干扰能力大大增加。...
  • 在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。...除此之外,RS232接口只能实现点对点通信,不具备联网功能,最大传输距离也只能达到几十米,不能满足远距离通信
  • C#读取串口数据封装, RS485、232 MODBUS通讯协议,亲测可用
  • 02、RS485 通信Modbus 协议   在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是 RS232 接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的...
  • RS485通信Modbus协议

    万次阅读 多人点赞 2016-11-29 16:22:38
    在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业...而RS485则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最
  •  在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的电磁干扰,会导致信号传输错误。除...
  • STM32:RS485通信和Modbus通信协议汇总

    千次阅读 2019-04-12 16:52:49
    RS485通信和Modbus通信协议汇总 1. 主从模式 RS-485上的软件层协议ModBus主要依赖于主从模式。主从模式是指在半双工通讯方式上,2个或者2个以上的设备组成的通讯系统中: (1) 至少且只有一个主机,其他的都是...
  • 针对原有阀门测试系统检测效率低、可控性差的特点,为保障多台阀门正常运行,避免不当的操作,建立了基于RS485总线和MODBUSRTU通信协议的阀门测试系统。
  • 早先学51时,宋老师的书上就有Modbus这方面的知识,可是当时实验环境不足也没去深究,现在做项目用到了RS485这类传感器和Modbus协议,特地来记录下。 一、传感器介绍 使用的传感器是一款光照度传感器,如下图所示...
  • RS485串口通信以及MODBUS协议

    千次阅读 2020-06-27 15:20:14
    一、RS485串口通信 1、首先对RS485芯片使能,打开5V电源信号 2、对RS485芯片控制引脚使能置高(处于发送模式)、失能置低(处于...二、MODBUS协议 详见:https://blog.csdn.net/brucezcg/article/details/70340361 ...
  • 该产品使用RS485接口与modbus协议的主站进行通信RS485接口采用3线进行通信,分别为A,B,PGND
  • 本文在艾默生PLC与其变频器的通讯基础上加入了基于RS485接口Modbus协议的PLC与单片机的多机通讯。介绍了艾默生PLC与其变频器的通讯特点,详细描述了通讯系统的硬件构成以及软件设计过程。所设计的通讯方案已经成功...
  • 基于STM32F103单片机开发,采用RS485总线的MODBUSRTU通讯例程,接受发送使能控制。带CRC校验计算函数。纯原创。
  • 基于stm32f407的modbus_rtu协议的代码,工程完整。工具是mdk5.4.下载直接可用。
  • 第18章 RS485通信Modbus协议  在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的...
  • RS485 MODBUS RTU通信协议

    2018-04-28 11:20:00
    RS485由RS232RS422发展而来,弥补了抗干扰能力差、通信距离短、速率低的缺点,增加了多点、双向通信能力,即允许多个发送器连接在同一条主线上,同时增加了发送器的驱动能力冲突保护特性,扩展了总线共模范围。...
  • 一、RS-485和Modbus的由来 RS-485:通常情况下,数据上传是采用串口通信的方式进行数据交换。起初采用的方式是RS232通信。由于工业现场比较复杂,各种传感器设备会在环境中产生比较多的电磁干扰,经常会出现传输...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,326
精华内容 930
关键字:

rs485通信和modbus协议