大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。 Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
转自:http://www.51hei.com/bbs/dpj-23230-1.html
在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是 RS232 接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的电磁干扰,会导致信号传输错误。除此之外, RS232 接口只能实现点对点通信,不具备联网功能,最大传输距离也只能达到几十米,不能满足远距离通信要求。而 RS485 则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最大距离可以到 1200 米,并且允许多个收发设备接到同一条总线上。随着工业应用通信越来越多, 1979 年施耐德电气制定了一个用于工业现场的总线协议 Modbus 协议,现在工业中使用 RS485 通信场合很多都采用 Modbus 协议,本节课我们要讲解一下 RS485 通信和 Modbus 协议。
单单使用一块KST-51 开发板是不能够进行 RS485 实验的,应很多同学的要求,把这节课作为扩展课程讲一下,如果要做本课相关实验,需要自行购买 USB 转 485 通信模块。
18.1 RS485通信
实际上在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 所示。
<ignore_js_op> 图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 所示。
<ignore_js_op> 图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>
#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 ,而 USB 转 485 虚拟的是 COM5 ,通信的时候我们用的是 COM5 口,如图 18-3 所示。
图18-3 RS485串行通信
18.2 Modbus通信协议介绍
我们前边学习UART 、 I 2C、 SPI 这些通信协议,都是最底层的协议,是“位”级别的协议。而我们在学习 13 章实用串口通信程序的时候,我们通过串口发给单片机三条指令,让单片机做了三件不同的事情,分别是 “buzz on”、”buzz off”、和”showstr”。随着我们系统复杂性的增加,我们希望可以实现更多的指令。而指令越来越多,带来的后果就是非常杂乱无章,尤其是这个人喜欢写成”buzz on”、”buzz off”,而另外一个人喜欢写成”on buzz”、”off buzz”。导致不同开发人员写出来的代码指令不兼容,不同厂家的产品不能挂到一条总线上通信。
随着这种矛盾的日益严重,就会有聪明人提出更合理的解决方案,提出一些标准来,今后我们的编程必须按照这个标准来,这种标准也是一种通信协议,但是和UART、 I 2C、 SPI 通信协议不同的是,这种通信协议是字节级别的,叫做应用层通信协议。在1979 年由 Modicon( 现为施耐德电气公司的一个品牌 ) 提出了全球第一个真正用于工业现场总线的协议,就是 Modbus 协议。
18.2.1 Modbus协议特点
Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络 ( 例如以太网 ) 和其他设备之间可以通信,已经成为一种工业标准。有了它,不同厂商生产的控制设备可以连成工业网络,进行集中监控。这种协议定义了一种控制器能够认识使用的数据结构,而不管它们是经过何种网络进行通信的。它描述了控制器请求访问其他设备的过程,如何回应来自其他设备的请求,以及怎样侦测错误记录,它制定了通信数据的格局和内容的公共格式。
在进行多机通信的时候,Modbus 协议规定每个控制器 必须要知道他们的设备地址,识别按照地址发送过来的数据,决定是否要产生动作,产生何种动作,如果要回应,控制器将生成的反馈信息用 Modbus 协议发出。 Modbus协议允许在各种网络体系结构内进行简单通信,每种设备 (PLC 、人机界面、控制面板、驱动程序、输入输出设备 ) 都能使用 Modbus 协议来启动远程操作,一些网关允许在几种使用 Modbus 协议的总线或网络之间的通信,如图 18-4 所示。
<ignore_js_op> 图18-4 Modbus网络体系结构实例
Modbus协议的整体架构和格式比较复杂和庞大,在我们的课程里,我们重点介绍数据帧结构和数据通信控制方式,作为一个入门级别的了解。如果大家要详细了解,或者使用 Modbus 开发相关设备,可以查阅相关的国标文件再进行深入学习。
1.2.2 RTU协议帧数据
Modbus有两种通信传输方式,一种是 ASCII 模式,一种是 RTU 模式。由于 ASCII 模式的数据字节是 7bit 数据位, 51 单片机无法实现,而且应用也相对较少,所以这里我们只用 RTU 模式。两种模式相似,会用一种另外一种也就会了。一条典型的 RTU 数据帧如图 18-5 所示。
<ignore_js_op> 图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 功能码
取得
8 个内部线圈的通断状态,这 8 个线圈 的地址由控制器 决定,用户逻辑可以将这些线圈定义,以说明从机状态,短报文适宜于迅速读取状态
可使主机与一台正在执行长程序任务从机通信,探询该从机是否已完成其操作任务,仅在含有功能码 9 的报文发送后,本功能码才发送
可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时
可是主机检索每台从机的 ModBus 事务处理通信事件记录。如果某项事务处理完成,记录会给出有关错误
可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能 13 的报文发送后,本功能码才得发送
可使主机判断编址从机的类型及该从机运行指示灯的状态
发生非可修改错误后,是从机复位于已知状态,可重置顺序字节
我们程序对功能码的处理,就是程序来检测这个字节的数值,然后根据其数值来做相应的功能处理。
数据:跟在功能代码后边的是n 个 8bit 的数据。这个 n 值的到底是多少,是功能代码来确定的,不同的功能代码后边跟的数据数量不同。举个例子,如果功能码是 0x03 ,也就是读保持寄存器,那么主机发送数据 n 的组成部分就是: 2 个字节的寄存器起始地址,加 2 个字节的寄存器数量 N *。从机数据n 的组成部分是: 1 个字节的字节数,因为我们回复的寄存器 的值是 2 个字节,所以这个字节数也就是 2N *个,再加上2N *个寄存器的值,如图18-6 所示。 <ignore_js_op> 图18-6 读保持寄存器数据结构
CRC校验: CRC 校验是一种数据算法,是用来校验数据对错的。 CRC 校验 函数把一帧数据除最后两个字节外,前边所有的字节进行特定的算法计算,计算完后生成了一个16bit 的数据,作为 CRC 校验码,添加在一帧数据的最后。接收方接收到数据后,同样会把前边的字节进行 CRC 计算,计算完了再和发过来的 CRC 的 16bit 的数据进行比较,如果相同则认为数据正常,没有出错,如果比较不相同,则说明数据在传输中发生了错误,这帧数据将被丢弃,就像没收到一样,而发送方会在得不到回应后做相应的处理错误处理。 RTU模式的每个字节的位是这样分布的: 1 个起始位、 8 个数据位,最小有效位先发送、 1 个奇偶校验位 ( 如果无校验则没有这一位 ) 、 1 位停止位 ( 有校验位时 ) 或者 2 个停止位 ( 无校验位时 ) 。
18.3 Modbus多机通信例程
给从机下发不同的指令,从机去执行不同的操作,这个就是判断一下功能码即可,和我们前边学的实用串口例程是类似的。多机通信,无非就是添加了一个设备地址判断而已,难度也不是很大。我们找了一个Modbus 调试精灵,通过设置设备地址,读写寄存器 的地址以及数值数量等参数,可以直接替代串口调试助手,比较方便的下发多个字节的数据,如图 18-7 所示。我们先来就图中的设置和数据来对 Modbus 做进一步的分析,图中的数据来自于调试精灵与我们接下来要讲的例程之间的交互。 <ignore_js_op> 图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 <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寄存器 组,地址为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<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] == crc h) && (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;
}
{
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 crc l = 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 ((crc h<<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); //串口接收监控
}
18.4 作业
1、了解 RS485 通信以及和 RS232 的不同用法。
2、了解 Modbus 协议以及 RTU 数据帧的规则。
3、写一个电子钟程序,并且可以通过 485 调试器校时。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/167301.html 原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...