RS485通信和Modbus协议Word文件下载.docx
《RS485通信和Modbus协议Word文件下载.docx》由会员分享,可在线阅读,更多相关《RS485通信和Modbus协议Word文件下载.docx(30页珍藏版)》请在冰豆网上搜索。
为了提高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<
reg52.h>
intrins.h>
sbitRS485_DIR=P1^7;
//RS485方向选择引脚
bitflagOnceTxd=0;
//单次发送完成标志,即发送完一个字节
bitcmdArrived=0;
//命令到达标志,即接收到上位机下发的命令
unsignedcharcntRxd=0;
unsignedcharpdatabufRxd[40];
//串口接收缓冲区
voidConfigUART(unsignedintbaud)
//串口配置函数,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
}
unsignedcharUartRead(unsignedchar*buf,unsignedcharlen)//串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度
unsignedchari;
if(len>
cntRxd)//读取长度大于接收到的数据长度时,
{
len=cntRxd;
//读取长度设置为实际接收到的数据长度
}
for(i=0;
i<
len;
i++)//拷贝接收到的数据
*buf=bufRxd[i];
buf++;
cntRxd=0;
//清零接收计数器
returnlen;
//返回实际读取长度
voidDelayX10us(unsignedchart)
//软件延时函数,延时时间(t*10)us
do{
_nop_();
}while(--t);
voidUartWrite(unsignedchar*buf,unsignedcharlen)//串口数据写入函数,即串口发送函数,待发送数据指针buf,数据长度len
RS485_DIR=1;
//RS485设置为发送
while(len--)
//发送数据
flagOnceTxd=0;
SBUF=*buf;
while(!
flagOnceTxd);
DelayX10us(5);
//等待最后的停止位完成,延时时间由波特率决定
//RS485设置为接收
voidUartDriver()//串口驱动函数,检测接收到的命令并执行相应动作
unsignedcharlen;
unsignedcharbuf[30];
if(cmdArrived)//有命令到达时,读取处理该命令
cmdArrived=0;
len=UartRead(buf,sizeof(buf)-2);
//将接收到的命令读取到缓冲区中
buf[len++]='
\r'
;
//在接收到的数据帧后添加换车换行符后发回
\n'
UartWrite(buf,len);
voidUartRxMonitor(unsignedcharms)
//串口接收监控函数
staticunsignedcharcntbkp=0;
staticunsignedcharidletmr=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;
voidInterruptUART()interrupt4
//UART中断服务函数
if(RI)
//接收到字节
RI=0;
//手动清零接收中断标志位
if(cntRxd<
sizeof(bufRxd))//接收缓冲区尚未用完时,
bufRxd[cntRxd++]=SBUF;
//保存接收字节,并递增计数器
if(TI)
//字节发送完毕
TI=0;
//手动清零发送中断标志位
flagOnceTxd=1;
//设置单次发送完成标志
/***********************main.c文件程序源代码*************************/
unsignedcharT0RH=0;
//T0重载值的高字节
unsignedcharT0RL=0;
//T0重载值的低字节
voidConfigTimer0(unsignedintms);
externvoidConfigUART(unsignedintbaud);
externvoidUartRxMonitor(unsignedcharms);
externvoidUartDriver();
voidmain()
EA=1;
//开总中断
ConfigTimer0
(1);
//配置T0定时1ms
ConfigUART(9600);
//配置波特率为9600
while
(1)
UartDriver();
voidConfigTimer0(unsignedintms)
//T0配置函数
unsignedlongtmp;
tmp=11059200/12;
//定时器计数频率
tmp=(tmp*ms)/1000;
//计算所需的计数值
tmp=65536-tmp;
//计算定时器重载值
tmp=tmp+34;
//修正中断响应延时造成的误差
T0RH=(unsignedchar)(tmp>
>
8);
//定时器重载值拆分为高低字节
T0RL=(unsignedchar)tmp;
=0xF0;
//清零T0的控制位
TMOD|=0x01;
//配置T0为模式1
TH0=T0RH;
//加载T0重载值
TL0=T0RL;
ET0=1;
//使能T0中断
TR0=1;
//启动T0
voidInterruptTimer0()interrupt1
//T0中断服务函数
//定时器重新加载重载值
UartRxMonitor
(1);
//串口接收监控
现在看这种串口程序,是不是感觉很简单了呢?
串口通信程序我们反反复复的使用,加上随着我们学习的模块越来越多,实践的越来越多,原先感觉很复杂的东西,现在就会感到简单了。
我们的下载程序模块用的是COM4,而USB转485虚拟的是COM5,通信的时候我们用的是COM5口,如图18-3所示。
图18-3RS485串行通信
18.2
Modbus通信协议介绍
我们前边学习UART、I2C、SPI这些通信协议,都是最底层的协议,是“位”级别的协议。
而我们在学习13章实用串口通信程序的时候,我们通过串口发给单片机三条指令,让单片机做了三件不同的事情,分别是"
buzzon"
、"
buzzoff"
、和"
showstr"
。
随着我们系统复杂性的增加,我们希望可以实现更多的指令。
而指令越来越多,带来的后果就是非常杂乱无章,尤其是这个人喜欢写成"
,而另外一个人喜欢写成"
onbuzz"
offbuzz"
导致不同开发人员写出来的代码指令不兼容,不同厂家的产品不能挂到一条总线上通信。
随着这种矛盾的日益严重,就会有聪明人提出更合理的解决方案,提出一些标准来,今后我们的编程必须按照这个标准来,这种标准也是一种通信协议,但是和UART、I2C、SPI通信协议不同的是,这种通信协议是字节级别的,叫做应用层通信协议。
在1979年由Modicon(现为施耐德电气公司的一个品牌)提出了全球第一个真正用于工业现场总线的协议,就是Modbus协议。
18.2.1
Modbus协议特点
Modbus协议是应用于电子控制器上的一种通用语言。
通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其他设备之间可以通信,已经成为一种工业标准。
有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。
这种协议定义了一种控制器能够认识使用的数据结构,而不管它们是经过何种网络进行通信的。
它描述了控制器请求访问其他设备的过程,如何回应来自其他设备的请求,以及怎样侦测错误记录,它制定了通信数据的格局和内容的公共格式。
在进行多机通信的时候,Modbus协议规定每个控制器必须要知道他们的设备地址,识别按照地址发送过来的数据,决定是否要产生动作,产生何种动作,如果要回应,控制器将生成的反馈信息用Modbus协议发出。
Modbus协议允许在各种网络体系结构内进行简单通信,每种设备(PLC、人机界面、控制面板、驱动程序、输入输出设备)都能使用Modbus协议来启动远程操作,一些网关允许在几种使用Modbus协议的总线或网络之间的通信,如图18-4所示。
图18-4Modbus网络体系结构实例
Modbus协议的整体架构和格式比较复杂和庞大,在我们的课程里,我们重点介绍数据帧结构和数据通信控制方式,作为一个入门级别的了解。
如果大家要详细了解,或者使用Modbus开发相关设备,可以查阅相关的国标文件再进行深入学习。
1.2.2
RTU协议帧数据
Modbus有两种通信传输方式,一种是ASCII模式,一种是RTU模式。
由于ASCII模式的数据字节是7bit数据位,51单片机无法实现,而且应用也相对较少,所以这里我们只用RTU模式。
两种模式相似,会用一种另外一种也就会了。
一条典型的RTU数据帧如图18-5所示。
图18-5RTU数据帧
和我们实用串口通信程序类似,我们一次发送的数据帧必须是作为一个连续的数据流进行传输。
我们在实用串口通信程序中采用的方法是定义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-1Modbus功能码
功能码
名称
作用
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/384484584)
可使主机模拟编程器功能修改PC从机逻辑
14
探询(184/384484584)
可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操