您的位置 首页 厂商

手把手教你学51单片机之十八 RS485通讯与Modbus协议

在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业现场比…

工业操控、电力通讯、智能仪表等范畴,通常情况下是选用串口通讯的办法进行数据交换。开端选用的办法是RS232接口,由于工业现场比较杂乱,各种电气设备会在环境中发生比较多的电磁搅扰,会导致信号传输过错。除此之外,RS232接口只能完结点对点通讯,不具有联网功用,最大传输距离也只能抵达十几米,不能满意远距离通讯要求。而RS485则处理了这些问题,数据信号选用差分传输办法,能够有用的处理共模搅扰问题,最大距离可达1200米,而且答应多个收发设备接到同一条总线上。跟着工业运用通讯越来越多,1979年施耐德电气拟定了一个用于工业现场的总线协议Modbus协议,现在工业中运用RS485通讯场合许多都选用Modbus协议,本节课咱们就来解说一下RS485通讯和Modbus协议。

单单运用一块KST-51开发板是不能够进行RS485实验的,应许多同学的要求,把这节课作为扩展课程讲一下,假设要做本课相关实验,需求自行购买USB转RS485通讯模块,或衔接其它的RS485主控设备进行。

1.1RS485通讯

RS232规范是诞生于RS485之前的,可是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等不同个设备的驱动器。

6、RS485的接口十分简略,与RS232所运用的MAX232是相似的,只需求一个RS485转化器,就能够直接与单片机的UART串口衔接起来,而且运用彻底相同的异步串行通讯协议。可是由于RS485是差分通讯,因而接纳数据和发送数据是不能一同进行的,也便是说它是一种半双工通讯。那咱们怎么判别什么时分发送,什么时分接纳呢?

RS485转化芯片许多,这节课咱们以典型的MAX485为例解说RS485通讯,如图18-1所示。

图18-1MAX485硬件接口

MAX485是美信(Maxim)推出的一款常用RS485转化器。其间5脚和8脚是电源引脚;6脚和7脚便是RS485通讯中的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口中并不是一切的都能够用来对外扩展,其间既作为数据输出,又能够作为数据输入的引脚是不能够用的,比方P3.2、P3.4、P3.6引脚,这三个引脚是不可用的。比方P3.2这个引脚,假设咱们用来扩展,发送的信号假设和DS18B20的时序符合,会导致DS18B20拉低引脚,影响通讯。除这3个IO口以外的其它29个,都能够运用杜邦线接上插针,扩展出来运用。当然了,假设把当时的IO口运用于扩展功用了,板子上的相应功用就完结不了了,也便是说需求扩展功用和板载功用之间二选一。

在进行RS485实验中,咱们通讯用的引脚有必要是P3.0和P3.1,此外还有一个方向操控引脚,咱们运用杜邦线将其衔接到P1.7上去。RS485的其他一端,咱们能够运用一个USB转RS485模块,用双绞线把开发板和模块上的A和B别离对应连起来,USB那头刺进电脑,然后就能够进行通讯了。

学习了第13章有用的串口通讯办法和程序后,做这种串口通讯的办法就很简略了,根本是共同的。咱们运用有用串口通讯例程的思路,做了一个简略的程序,经过串口调试帮手下发恣意个字符,单片机接纳到后在结尾添加“回车+换行”符后再送回,在调试帮手上从头显现出来,先把程序贴出来。

程序中需求留意的一点是:由于往常都是将MAX485设置为接纳状况,只要在发送数据的时分才将MAX485改为发送状况,所以在UartWrite()函数最初将MAX485方向引脚拉高,函数退出前再拉低。可是这儿有一个细节,便是单片机的发送和接纳中止发生的时刻都是在中止位的一半上,也便是说每逢中止位传送了一半的时分,RI或TI就现已置位而且立刻进入中止(假设中止使能的话)函数了,接纳的时分天然不会存在问题,但发送的时分就不相同了:当紧接着向SBUF写入一个字节数据时,UART硬件会在完结上一个中止位的发送后,再开端新字节的发送,但假设此刻不是继续发送下一个字节,而是现已发送完毕了,要中止发送并将MAX485方向引脚拉低以使MAX485从头处于接纳状况时就有问题了,由于这时分最终的这个中止位实践只发送了一半,还没有彻底完结,所以就有了UartWrite()函数内DelayX10us(5)这个操作,这是人为的添加了50us的延时,这50us的时刻正好让剩余的一半中止位完结,那么这个时刻天然便是由通讯波特率决议的了,为波特率周期的一半。

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

#include

#include

sbitRS485_DIR=P1^7;//RS485方向挑选引脚

bitflagFrame=0;//帧接纳完结标志,即接纳到一帧新数据

bitflagTxd=0;//单字节发送完结标志,用来代替TXD中止标志位

unsignedcharcntRxd=0;//接纳字节计数器

unsignedcharpdatabufRxd[64];//接纳字节缓冲区

externvoidUartAction(unsignedchar*buf,unsignedcharlen);

/*串口装备函数,baud-通讯波特率*/

voidConfigUART(unsignedintbaud)

{

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*/

voidDelayX10us(unsignedchart)

{

do{

_nop_();

_nop_();

_nop_();

_nop_();

_nop_();

_nop_();

_nop_();

_nop_();

}while(–t);

}

/*串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度*/

voidUartWrite(unsignedchar*buf,unsignedcharlen)

{

RS485_DIR=1;//RS485设置为发送

while(len–)//循环发送一切字节

{

flagTxd=0;//清零发送标志

SBUF=*buf++;//发送一个字节数据

while(!flagTxd);//等候该字节发送完结

}

DelayX10us(5);//等候最终的中止位完结,延时时刻由波特率决议

RS485_DIR=0;//RS485设置为接纳

}

/*串口数据读取函数,buf-接纳指针,len-指定的读取长度,回来值-实践读到的长度*/

unsignedcharUartRead(unsignedchar*buf,unsignedcharlen)

{

unsignedchari;

if(len>cntRxd)//指定读取长度大于实践接纳到的数据长度时,

{//读取长度设置为实践接纳到的数据长度

len=cntRxd;

}

for(i=0;i

{

*buf++=bufRxd[i];

}

cntRxd=0;//接纳计数器清零

returnlen;//回来实践读取长度

}

/*串口接纳监控,由闲暇时刻断定帧完毕,需在守时中止中调用,ms-守时刻隔*/

voidUartRxMonitor(unsignedcharms)

{

staticunsignedcharcntbkp=0;

staticunsignedcharidletmr=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;

}

}

/*串口驱动函数,监测数据帧的接纳,调度功用函数,需在主循环中调用*/

voidUartDriver()

{

unsignedcharlen;

unsignedcharpdatabuf[40];

if(flagFrame)//有指令抵达时,读取处理该指令

{

flagFrame=0;

len=UartRead(buf,sizeof(buf)-2);//将接纳到的指令读取到缓冲区中

UartAction(buf,len);//传递数据帧,调用动作履行函数

}

}

/*串口中止服务函数*/

voidInterruptUART()interrupt4

{

if(RI)//接纳到新字节

{

RI=0;//清零接纳中止标志位

if(cntRxd

{//保存接纳字节,并递加计数器

bufRxd[cntRxd++]=SBUF;

}

}

if(TI)//字节发送完毕

{

TI=0;//清零发送中止标志位

flagTxd=1;//设置字节发送完结标志

}

}

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

#include

unsignedcharT0RH=0;//T0重载值的高字节

unsignedcharT0RL=0;//T0重载值的低字节

voidConfigTimer0(unsignedintms);

externvoidUartDriver();

externvoidConfigUART(unsignedintbaud);

externvoidUartRxMonitor(unsignedcharms);

externvoidUartWrite(unsignedchar*buf,unsignedcharlen);

voidmain()

{

EA=1;//开总中止

ConfigTimer0(1);//装备T0守时1ms

ConfigUART(9600);//装备波特率为9600

while(1)

{

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

}

}

/*串口动作函数,依据接纳到的指令帧履行呼应的动作

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

voidUartAction(unsignedchar*buf,unsignedcharlen)

{

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

buf[len++]=r;

buf[len++]=n;

UartWrite(buf,len);

}

/*装备并发动T0,ms-T0守时时刻*/

voidConfigTimer0(unsignedintms)

{

unsignedlongtmp;//暂时变量

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

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

tmp=65536-tmp;//核算守时器重载值

tmp=tmp+33;//补偿中止呼应延时形成的差错

T0RH=(unsignedchar)(tmp>>8);//守时器重载值拆分为凹凸字节

T0RL=(unsignedchar)tmp;

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

TMOD|=0x01;//装备T0为形式1

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

TL0=T0RL;

ET0=1;//使能T0中止

TR0=1;//发动T0

}

/*T0中止服务函数,履行串口接纳监控*/

voidInterruptTimer0()interrupt1

{

TH0=T0RH;//从头加载重载值

TL0=T0RL;

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

}

现在看这种串口程序,是不是感觉很简略了呢?串口通讯程序咱们反反复复的运用,加上跟着学习的模块越来越多,实践的越来越多,原先感觉很杂乱的东西,现在就会感到简略了。从设备管理器里能够检查一切的COM标语,咱们下载程序用的是COM4,而USB转RS485虚拟的是COM5,通讯的时分咱们用的是COM5口,如图18-3所示。

图18-3RS485通讯实验设置和成果

1.2Modbus通讯协议介绍

咱们前边学习UART、I2C、SPI这些通讯协议,都是最底层的协议,是“位”等级的协议。而咱们在学习13章做有用串口通讯程序的时分,咱们经过串口发给单片机三条指令,让单片机做了三件不同的作业,别离是“buzzon”、“buzzoff”和“showstr”。跟着体系杂乱性的添加,咱们期望能够完结更多的指令。而指令越来越多,带来的结果便是十分乱七八糟,特别是这个人喜爱写成“buzzon”、“buzzoff”,而其他一个人喜爱写成“onbuzz”、“offbuzz”。导致不同开发人员写出来的程序代码不兼容,不同厂家的产品不能挂到一条总线上通讯。

跟着这种对立的日益严重,就会有聪明人提出更合理的处理方案,提出一些规范来,往后咱们的编程有必要依照这个规范来,这种规范也是一种通讯协议,可是和UART、I2C、SPI通讯协议不同的是,这种通讯协议是字节等级的,叫做运用层通讯协议。在1979年由Modicon(现为施耐德电气公司的一个品牌)提出了全球第一个真有用于工业现场总线的协议,便是Modbus协议。

1.2.1Modbus协议特色

Modbus协议是运用于电子操控器上的一种通用言语。经过此协议,操控器相互之间、操控器经由网络(例如以太网)和其他设备之间能够通讯,现已成为一种工业规范。有了它,不同厂商出产的操控设备能够连成工业网络,进行会集监控。这种协议界说了一种操控器能够知道运用的数据结构,而不论它们是经过何种网络进行通讯的。它描绘了操控器恳求拜访其它设备的进程,怎么回应来自其它设备的恳求,以及怎样侦测过错记载,它拟定了通讯数据的格局和内容的公共格局。

在进行多机通讯的时分,Modbus协议规矩每个操控器有必要要知道它们的设备地址,辨认依照地址发送过来的数据,决议是否要发生动作,发生何种动作,假设要回应,操控器将生成的反应信息用Modbus协议宣布。

Modbus协议答应在各种网络体系结构内进行简略通讯,每种设备(PLC、人机界面、操控面板、驱动程序、输入输出设备等)都能运用Modbus协议来发动长途操作,一些网关答应在几种运用Modbus协议的总线或网络之间的通讯,如图18-4所示。

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

Modbus协议的全体架构和格局比较杂乱和巨大,在咱们的课程里,咱们要点介绍数据帧结构和数据通讯操控办法,作为一个入门等级的了解。假设咱们要具体了解,或许运用Modbus开发相关设备,能够查阅相关的国标文件再进行深化学习。

1.1.1RTU协议帧数据

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功用码

(ON/OFF)

(ON/OFF)

8个内部线圈的通断状况,这8个线圈的地址由操控器决议,用户逻辑能够将这些线圈界说,以阐明从机状况,短报文适宜于敏捷读取状况

484

PC从机逻辑

484

9的报文发送后,本功用码才发送

ModBus事务处理通讯工作记载。假设某项事务处理完结,记载会给出有关过错

PC从机逻辑

13的报文发送后,本功用码才得发送

和M%&&&&&%RO84

PC状况逻辑

程序对功用码的处理,便是来检测这个字节的数值,然后依据其数值来做相应的功用处理。

数据:跟在功用代码后边的是n个8bit的数据。这个n值的究竟是多少,是功用代码来确认的,不同的功用代码后边跟的数据数量不同。举个比如,假设功用码是0x03,也便是读坚持寄存器,那么主机发送数据n的组成部分便是:2个字节的寄存器开端地址,加2个字节的寄存器数量N。从机数据n的组成部分是:1个字节的字节数,由于咱们的寄存器的值是2个字节,所以这个字节数也便是2N个,再加上2N个寄存器的值,如图18-6所示。

图18-6读坚持寄存器数据结构

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

RTU形式的每个字节的位是这样散布的:1个开端位、8个数据位,最小有用位先发送、1个奇偶校验位(假设无校验则没有这一位)、1位中止位(有校验位时)或许2个中止位(无校验位时)。

1.1Modbus多机通讯例程

给从机下发不同的指令,从机去履行不同的操作,这个便是判别一下功用码即可,和咱们前边学的有用串口例程是相似的。多机通讯,无非便是添加了一个设备地址判别罢了,难度也不大。咱们找了一个Modbus调试精灵,经过设置设备地址,读写寄存器的地址以及数值数量等参数,能够直接代替串口调试帮手,比较便利的下发多个字节的数据,如图18-7所示。咱们先来就图中的设置和数据来对Modbus做进一步的剖析,图中的数据来自于调试精灵与咱们接下来要讲的例程之间的交互。

图18-7Modbus调试精灵

如图,咱们的USB转RS485模块虚拟出的是COM5,波特率9600,无校验位,数据位是8位,1位中止位,设备地址假设为1。

写寄存器的时分,假设咱们要把01写到一个地址是0000的寄存器地址里,点一下“写入”,就会呈现发送指令:010600000001480A。咱们来剖析一下这帧数据,其间01是设备地址,06是功用码,代表写寄存器这个功用,后边跟0000表明的是要写入的寄存器的地址,0001便是要写入的数据,480A便是CRC校验码,这是软件主动算出来的。而依据Modbus协议,当写寄存器的时分,从机成功完结该指令的操作后,会把主机发送的指令直接回来,咱们的调试精灵会接纳到这样一帧数据:010600000001480A。

假设咱们现在要从寄存器地址0002开端读取寄存器,而且读取的数量是2个。点一下“读出”,就会呈现发送指令:01030002000265CB。其间01是设备地址,03是功用码,代表读寄存器这个功用,0002便是读寄存器的开端地址,后一个0002便是要读取2个寄存器的数值,65CB便是CRC校验。而接纳到的数据是:01030400000000FA33。其间01是设备地址,03是功用码,04代表的是后边读到的数据字节数是4个,00000000别离是地址为0002和0003的寄存器内部的数据,而FA33便是CRC校验了。

好像越来越明亮了,所谓的Modbus通讯协议,无非便是主机下发了不同的指令,从机依据指令的判别来履行不同的操作罢了。由于咱们的开发板没有Modbus功用码那么多相应的功用,咱们在程序中界说了一个数组regGroup[5],相当于5个寄存器,此外又界说了第6个寄存器,操控蜂鸣器,经过下发不同的指令咱们改动寄存器组的数据或许改动蜂鸣器的开关状况。在Modbus协议里寄存器的地址和数值都是16位的,即2个字节,咱们默许高字节是0x00,低字节便是数组regGroup对应的值。其间地址0x0000到0x0004对应的便是regGroup数组中的元素,咱们写入的一同把数字又显现到1602液晶上,而0x0005这个地址,写入0x00,蜂鸣器就不响,写入任何其它数值,蜂鸣器就报警。咱们单片机的首要作业也便是解析串口接纳的数据履行不同操作。

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

(此处省掉,可参阅之前章节的代码)

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

(此处省掉,可参阅之前章节的代码)

/CRC16.c文件程序源代码/

/*CRC16核算函数,ptr-数据指针,len-数据长度,回来值-核算出的CRC16数值*/

unsignedintGetCRC16(unsignedchar*ptr,unsignedcharlen)

{

unsignedintindex;

unsignedcharcrch=0xFF;//高CRC字节

unsignedcharcrcl=0xFF;//低CRC字节

unsignedcharcodeTabH[]={//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

};

unsignedcharcodeTabL[]={//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);

}

关于CRC校验的算法,假设不是专门学习校验算法自身,咱们能够不去研讨这个程序的细节,直接运用现成的函数即可。

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

#include

sbitBUZZ=P1^6;

bitflagBuzzOn=0;//蜂鸣器发动标志

unsignedcharT0RH=0;//T0重载值的高字节

unsignedcharT0RL=0;//T0重载值的低字节

unsignedcharregGroup[5];//Modbus寄存器组,地址为0x00~0x04

voidConfigTimer0(unsignedintms);

externvoidUartDriver();

externvoidConfigUART(unsignedintbaud);

externvoidUartRxMonitor(unsignedcharms);

externvoidUartWrite(unsignedchar*buf,unsignedcharlen);

externunsignedintGetCRC16(unsignedchar*ptr,unsignedcharlen);

externvoidInitLcd1602();

externvoidLcdShowStr(unsignedcharx,unsignedchary,unsignedchar*str);

voidmain()

{

EA=1;//开总中止

ConfigTimer0(1);//装备T0守时1ms

ConfigUART(9600);//装备波特率为9600

InitLcd1602();//初始化液晶

while(1)

{

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

}

}

/*串口动作函数,依据接纳到的指令帧履行呼应的动作

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

voidUartAction(unsignedchar*buf,unsignedcharlen)

{

unsignedchari;

unsignedcharcnt;

unsignedcharstr[4];

unsignedintcrc;

unsignedcharcrch,crcl;

if(buf[0]!=0x01)//本例中的本机地址设定为0x01,

{//如数据帧中的地址字节与本机地址不符,

return;//则直接退出,即丢掉本帧数据不做任何处理

}

//地址相符时,再对本帧数据进行校验

crc=GetCRC16(buf,len-2);//核算CRC校验值

crch=crc>>8;

crcl=crc&0xFF;

if((buf[len-2]!=crch)||(buf[len-1]!=crcl))

{

return;//如CRC校验不符时直接退出

}

//地址和校验字均相符后,解析功用码,履行相关操作

switch(buf[1])

{

case0x03://读取一个或接连的寄存器

if((buf[2]==0x00)&&(buf[3]<=0x05))//只支撑0x0000~0x0005

{

if(buf[3]<=0x04)

{

i=buf[3];//提取寄存器地址

cnt=buf[5];//提取待读取的寄存器数量

buf[2]=cnt*2;//读取数据的字节数,为寄存器数*2

len=3;//帧前部已有地址、功用码、字节数共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;

}

case0x06://写入单个寄存器

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]=;

LcdShowStr(i*3,0,str);

}

else//地址0x05为蜂鸣器状况

{

flagBuzzOn=(bit)buf[5];//寄存器值转为蜂鸣器的开关

}

len-=2;//长度-2以从头核算CRC并回来原帧

&nb

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);//发送回来帧

}

/*装备并发动T0,ms-T0守时时刻*/

voidConfigTimer0(unsignedintms)

{

unsignedlongtmp;//暂时变量

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

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

tmp=65536-tmp;//核算守时器重载值

tmp=tmp+33;//补偿中止呼应延时形成的差错

T0RH=(unsignedchar)(tmp>>8);//守时器重载值拆分为凹凸字节

T0RL=(unsignedchar)tmp;

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

TMOD|=0x01;//装备T0为形式1

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

TL0=T0RL;

ET0=1;//使能T0中止

TR0=1;//发动T0

}

/*T0中止服务函数,履行串口接纳监控和蜂鸣器驱动*/

voidInterruptTimer0()interrupt1

{

TH0=T0RH;//从头加载重载值

TL0=T0RL;

if(flagBuzzOn)//履行蜂鸣器鸣叫或封闭

BUZZ=~BUZZ;

else

BUZZ=1;

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

}

咱们能够看到担任解析协议的UartAction函数很长,由于协议解析原本便是一件很繁琐的作业。咱们的例程仅解析履行了两个功用指令,就现已有近百行程序了,假设你需求解析更多的功用指令的话,那么主张把每个功用都做一个函数,然后在相应的case分支里调用即可,这样就不会使单个函数过于巨大而难以保护。

1.1操练题

1、了解RS485通讯以及和RS232的不同用法。

2、了解Modbus协议以及RTU数据帧的规矩。

3、写一个电子钟程序,而且能够经过485调试器校时。

声明:本文内容来自网络转载或用户投稿,文章版权归原作者和原出处所有。文中观点,不代表本站立场。若有侵权请联系本站删除(kf@86ic.com)https://www.86ic.net/changshang/266024.html

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: kf@86ic.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部