在一些工业应用的场合,我们经常需要用到串口通信,既然是要通信肯定是需要相关协议的支持,业内比较标准的协议当然要数MODBUS协议了,但是MODBUS协议要完全弄懂,也并非易事,很多时候,可能我们只需要简单控制一些输出同时读取输入输出状态,以及设置一些参数等。如果用标准的MODBUS肯定是没有问题的,但是并不是所有人都能在短时间内摸透MODBUS协议,那么,或许有人会说,自己随便写个简单的协议不就好了!没错,这样也是可以,只要通信设备双方都按照约定好的协议去执行相关动作即可,这一帖中,笔者就要着重介绍这种自定义协议的通信了。
说到自定义协议,笔者第一次接触的时候,还是在用迪文DGUS屏的时候,在接触了迪文DGUS屏的指令后,笔者才学会的使用自定义协议来做一些通信。那么,笔者就以迪文DGUS屏的指令为例,跟大家详细一下自定义协议的相关知识吧。
迪文DGUS串口数据帧的架构是由以下几个部分组成:
帧头(2字节)+ 数据长度(1字节)+ 指令(1字节)+ 数据(N字节)+ CRC校验(2字节)
所有指令都是以十六进制数发送,以写控制寄存器指令(80)为例:
例如从当前页面切换到第五幅图片,向屏发送如下指令即可:5A A5 04 80 03 00 05,那么这些十六进制数都代表什么意思呢?其含义如下:
5A A5:帧头由两个字节组成,可以自定义
04:发送数据的长度(指从指令开始到最后的数据长度,此处从80指令开始共发送4个字节)
80:写控制寄存器指令
03:控制寄存器地址
00 05:图片地址
在这条命令中,省去了CRC校验,由CRC校验相对比较复杂,很多场合也可以用和校验的方式替代CRC校验,比如,我们可以将上述指令改成如下方式:5A A5 04 80 03 00 05 92,最后一个数92即是一个校验和,从数据长度位开始到最后一个数据累加求余,即得到了校验和。当然,这个校验和我们可以用1个字节,也可以用2个字节,看大家使用习惯了,笔者经常都是用1个字节来做和校验位。看到这里,相信大家对于自定义协议应该有一定了解了。
其实,自定义协议很简单,可以自己任意定义一串数字,只要双方都按照定义好的格式收发数据即可。
接下来,笔者就一个实际的案例,在这款工控板上演示一下自定义协议通信。该工控板上有6个输出,分别是Y0-Y5;8个输入,分别是X00-X07,用串口助手模拟上位机来发送指令,分别控制每个输出口的输出状态,在工控板接收到串口助手发来的指令后,根据不同指令执行相关动作,并返回此时输入输出口的状态。
首先,笔者定义串口助手发送的通信帧格式如下:
帧头(2字节)+ 长度(1字节)+ 命令(1字节)+ 地址(1字节)+ 指令(1字节)
例如:
Y0输出ON 的指令为:5A A5 03 06 00 01
Y0输出OFF的指令为:5A A5 03 06 00 00
Y1输出ON 的指令为:5A A5 03 06 01 01
Y1输出OFF的指令为:5A A5 03 06 01 00
其中:
5A A5为帧头
03指数据长度
06为命令字
00为Y0的地址/01为Y1的地址
01为输出ON指令/00为输出OFF指令
本例中笔者偷懒了,也省去了和校验,不过笔者相信,看到这里了,大家对应该是有能力自己增加上和校验的,要是实在不知道,可以私聊笔者。
那么,发送问题是解决了,但是,要怎么接收这一串指令呢?还是参考迪文DGUS接收数据帧的方式,首先校验帧头:5A A5,然后再判断长度位,根据长度位来确定需要接收数据的长度,比如,此处长度位为03,那么,我们只需要在接收到长度位之后,继续再接收完3个数据即可认为数据接收完成,具体代码实现如下:
/********************* UART1中断函数************************/
void UART1_int (void) interrupt UART1_VECTOR
{
u8 UART1_DataTemp;
//命令格式:帧头+长度+命令字+地址+指令
//5A A5 03 06 00 01--Y00-ON 5A A5 03 06 00 00--Y00-OFF
if(RI)
{
RI = 0;
UART1_DataTemp = SBUF;
RX_Busy = 1; //接收忙
if(RX_5A_OK)
{
if(RX_A5_OK)
{
RX1_Buffer = UART1_DataTemp; //将接收到的数组暂存到RX1_Buffer数组
COM1.RX_write ++;
if(COM1.RX_write == RX1_Buffer + 1) //接收完成
{
Uart1RXFinish = 1; //数据接收完成,将标志位置1
RX_5A_OK = 0;
RX_A5_OK = 0;
COM1.RX_write = 0;
RX_Busy = 0;
}
}
else
{
if(UART1_DataTemp == 0xA5)
{
RX_A5_OK = 1;
COM1.RX_write = 0;
}
}
}
else
{
if(UART1_DataTemp == 0x5A)
{
RX_5A_OK = 1;
}
}
if(COM1.RX_write >= RX_Length) COM1.RX_write = 0;
}
if(TI)
{
TI = 0;
COM1.TX_Busy = 0;
}
}
当然,这里其实我们也可以使用结束符来实现,比如回车换行符,也很简单,只要找到回车换行对应的ASCII码的十六进制数就好了,要是笔者没记错的话,应该是:0x0D、0x0A,那么我们可以在串口接收到0x0D、0x0A两个数据之后认为是接收完成,当然,代码部分笔者就不再贴出来了,留给读者去完成了。
最后,讲一下返回数据帧的格式,笔者在本例中用了如下格式:
帧头(2字节)+ 长度(1字节)+ 命令(1字节)+ X00状态(1字节)+ X01状态(1字节)+ X02状态(1字节)+ X03状态(1字节)+ X04状态(1字节)+ X05状态(1字节)+ X06状态(1字节)+ X07状态(1字节)+ Y0状态(1字节)+ Y1状态(1字节)+ Y2状态(1字节)+ Y3状态(1字节)+ Y4状态(1字节)+ Y5状态(1字节)
当然,这里笔者写的有点繁琐,其实8个输入状态完全可以用一个字节来实现,6个输出状态也可以完全用一个字节来实现,这里笔者只是为了让大家更好的理解这个协议,所以写的繁琐了一点。数据返回的关键代码如下:
/********************* 串口1发送命令************************/
void Uart1_Send(void)
{
//定时自动发送数据
if(Uart1RXFinish) //
{
if((RX1_Buffer==0x03)&&(RX1_Buffer==0x06))
{
if(RX1_Buffer==0x00)//Y0
{
if(RX1_Buffer==0x01)
{
OUT00 = 1;
Y00_State = 1;
}
if(RX1_Buffer==0x00)
{
OUT00 = 0;
Y00_State = 0;
}
}
if(RX1_Buffer==0x01)//Y1
{
if(RX1_Buffer==0x01)
{
OUT01 = 1;
Y01_State = 1;
}
if(RX1_Buffer==0x00)
{
OUT01 = 0;
Y01_State = 0;
}
}
if(RX1_Buffer==0x02)//Y2
{
if(RX1_Buffer==0x01)
{
OUT02 = 1;
Y02_State = 1;
}
if(RX1_Buffer==0x00)
{
OUT02 = 0;
Y02_State = 0;
}
}
if(RX1_Buffer==0x03)//Y3
{
if(RX1_Buffer==0x01)
{
OUT03 = 1;
Y03_State = 1;
}
if(RX1_Buffer==0x00)
{
OUT03 = 0;
Y03_State = 0;
}
}
if(RX1_Buffer==0x04)//Y4
{
if(RX1_Buffer==0x01)
{
OUT04 = 1;
Y04_State = 1;
}
if(RX1_Buffer==0x00)
{
OUT04 = 0;
Y04_State = 0;
}
}
if(RX1_Buffer==0x05)//Y5
{
if(RX1_Buffer==0x01)
{
OUT05 = 1;
Y05_State = 1;
}
if(RX1_Buffer==0x00)
{
OUT05 = 0;
Y05_State = 0;
}
}
//输入状态刷新
if(IN00)X00_State = 0;else X00_State = 1;
if(IN01)X01_State = 0;else X01_State = 1;
if(IN02)X02_State = 0;else X02_State = 1;
if(IN03)X03_State = 0;else X03_State = 1;
if(IN04)X04_State = 0;else X04_State = 1;
if(IN05)X05_State = 0;else X05_State = 1;
if(IN06)X06_State = 0;else X06_State = 1;
if(IN07)X07_State = 0;else X07_State = 1;
Uart1RXFinish = 0;
Uart1SendEN = 1;
SendDataInit = 0;
}
}
if(Uart1SendEN)
{
if(!RX_Busy)
{
//发送命令
if(COM1.TX_Busy == 0)
{
COM1.TX_Busy = 1; //标志发送忙
if(!SendDataInit)
{
SendData = 0x5A;//帧头
SendData = 0xA5;//帧头
SendData = 0x0F;//长度
SendData = 0x06;//命令0x06
SendData = X00_State;//X00输入状态L
SendData = X01_State;//X01输入状态L
SendData = X02_State;//X02输入状态L
SendData = X03_State;//X03输入状态L
SendData = X04_State;//X04输入状态L
SendData = X05_State;//X05输入状态L
SendData = X06_State;//X06输入状态L
SendData = X07_State;//X07输入状态L
SendData = Y00_State;//Y00输出状态L
SendData = Y01_State;//Y01输出状态L
SendData = Y02_State;//Y02输出状态L
SendData = Y03_State;//Y03输出状态L
SendData = Y04_State;//Y04输出状态L
SendData = Y05_State;//Y05输出状态L
SendDataInit = 1;
COM1.TX_read = 0;
}
SBUF = SendData; //发一个字节
COM1.TX_read++;
if(COM1.TX_read>=18)//命令长度4位
{
COM1.TX_read = 0;
Uart1SendEN = 0;
}
}
}
}
}
接下来,我们只需要将程序下载到控制板中,再用串口助手来发送指令,即可验证效果了,笔者在此就用串口监控精灵来验证数据的收发,具体控制板上的输入输出状态就不贴图了:
从串口精灵监控到的数据我们可以看到:
当串口助手发送指令:5A A5 03 06 00 01的时候,控制板返回数据:5A A5 0F 06 00 00 00 00 00 00 00 00 01 00 00 00 00 00 ,即Y0输出ON;
当串口助手发送指令:5A A5 03 06 00 00的时候,控制板返回数据:5A A5 0F 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ,即Y0输出OFF;
当串口助手发送指令:5A A5 03 06 01 01的时候,控制板返回数据:5A A5 0F 06 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ,即Y1输出ON;
当串口助手发送指令:5A A5 03 06 01 00的时候,控制板返回数据:5A A5 0F 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ,即Y1输出OFF;
同时,在控制板上我们也可以分别看到Y0先输出ON,然后再输出OFF;Y1先输出ON,然后再输出OFF(此处省去动图),至此,一个完整的通信就完成了,感兴趣的读者,可以在此基础上进行改善,比如,数据发送帧和数据返回帧都加上校验和。
好了,有关自定义协议的知识就简单介绍到这里了,有疑问的读者可以跟帖回复。