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