前言
本篇博客主要学习和了解一些单片机协议的格式,在对传输大数据或者要求准确性的时候,都需要通过协议来发送接收,下面通过了解协议的基本构成和代码来分析和实现协议的发送和接收。本篇博客大部分是自己收集和整理,如有侵权请联系我删除。
本次博客开发板使用的是正点原子精英版,芯片是STM32F103ZET6,需要资料可以@我拿取。
交流群:717237739
如果觉得有用点赞关注收藏三连,多谢支持
本博客内容原创,创作不易,转载请注明
————————————————
一 .什么是协议?
协议,是网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。
如怎么建立连接,怎么样互相识别等,只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:语法,语义,时序。为了使数据在网络上从源到达目的,网络通信的参与方必须遵循相同的规则,这套规则为协议,最终体现为在网络上传输的数据包的格式。
例如,串口的波特率,也是协议的一种表现格式。
参考知乎的资料了解七层协议:OSI 七层模型和TCP/IP模型及对应协议(详解)
OSI 七层模型和TCP/IP模型及对应协议(详解)
以上图片是为了让我们对协议有个充足的概念,具体的自己可以先行了解协议的含义和作用。
二 .协议的组成
1.概念
这里说的数据协议是建立在物理层之上的通信数据包格式。所谓通信的物理层就是指我们通常所常用的RS232,RS485,红外,光纤,无线等通信方式
2.组成部分
比较可靠的通信协议包括:帧头,地址信息,数据类型,数据长度,数据块,校验位,帧尾。
在日常自己定义的格式里,我们一般也是要遵循这些格式来定义一个数据包的发送和接收,以保证数据的完整性和准确性。
- 帧头:一帧数据的开始,可以使用多个字节,具体内容自己定义。例如:0X55
- 地址信息:主要用于多机通信中,通过地址信息的不同来识别不同的通信终端,确定和哪个设备进行通信(类似于IIC的芯片地址ID)。例如:0X01
- 数据类型:可以标识后面紧接的是命令还是数据,例如0X01标识二进制,0x02标识十六进制等。
- 数据长度:标识后面的要发送/接收的数据长度的个数。例如:0X02,表示后面跟两个数据。
- 数据块:真正发送的数据内容,发送数据内容的长度和上面的数据长度对应。
- 校验码:用来检验数据的完整性和准确性,一般常见的校验方式有(MODBUS_CRC16/CRC32,ADD8/16求和等等)
- 帧尾:判断数据包的结束,可以为一个数据或者多个数据,自己定义,例如0XAA
根据以上的协议格式介绍,一般正常的数据包为:
0X55 0X01 0X01 0X02 0X13 0X88 CRC16H CRC16L 0XAA
(帧头 地址 数据类型 长度 数据 数据 CRC检验H CRC校验L 帧尾)
三. 类似的芯片手册协议格式讲解
这里参考一个芯片手册:SYN6658中文语音合成芯片 用户手册
从这里分析可以得到:
帧头为: 0XFD 数据长度:2字节 0xXX 0xXX 命令字:参考手册一字节
命令参数:一字节 数据文本:最大4K字节
下面是手册的一些命令字和命令参数的参考:
完整的数据包格式:
注意:在一些使用其他芯片的场景下,我们一般都需要遵守各个芯片的协议格式,然后根据手册和要求发送对应的数据包,这样协议对应才能驱动芯片。
四 . STM32代码实现自定义格式协议
我们在网上找到有对应的嵌入式协议测试题目,我们根据这个题目写出对应的协议发送和接收代码,代码内部做了注释,整体就不再讲解了,不懂的只能去补补C语言了,代码仅做参考。
测试题:
题目分析:
- 1.数据包格式,建议直接做成结构体的模式,然后传入地址,一个个调用就很方便了
- 2.已知的信息,直接宏定义调用,例如帧头帧尾
- 3.提前写好串口数据包的发送和接收函数,类似数组遍历那种,然后了解清楚占用的字节大小
- 4.题目要求最大65536,但是我们在STM32F103上实现,就最大256算了,不然超范围了
- 串口发送和接收函数讲解和应用:基于STM32 + UART串口通信新手详解
代码.C.H部分和main部分
#include "rs485.h"
#include <stdio.h>
#include <string.h>
u8 sendbuf[Send_Buf_Size]; //发送数据缓冲区
u8 recbuf[Rec_Buf_Size]; //接收数据缓冲区
RS485 rs485def={sendbuf,recbuf,0,0}; //rs485相关信息结构体
//中断服务函数接收数据
void USART3_IRQHandler(void)
{
u8 data;
u32 head,tail;
u8 len;
if(USART3->SR & 1<<5)
{
data= USART3->DR;
// printf("%x\t",data);
rs485def.recbuf[rs485def.reclen]=data ;//存放收到的数据
rs485def.reclen++;
if(rs485def.reclen >=8) //收到了包头和数据域长度
{
head = *(u32*)rs485def.recbuf;//获取包头
// printf("head:0x%x\r\n",head);
if(head == PACK_START)//收到包头
{
len = *(u32*)&rs485def.recbuf[4];//获取数据域长度
// printf("reclen:%d,len:%d\r\n",rs485def.reclen,len);
if(rs485def.reclen >= len+12) //一帧数据接收完成
{
tail = *(u32*)&rs485def.recbuf[len+4+4];//获取包尾 4包头所占4字节,4数据域长度所占4个字节
// printf("tail:0x%x\r\n",tail);
if(tail == PACK_TAIL )//包尾正确
{
rs485def.recflag = 1; //接收完成标志
}
else
{
rs485def.reclen = 0 ;
}
}
}
else
{
rs485def.reclen = 0 ;
}
}
}
}
//数据包接收函数
//函数功能:得到数据包的相关内容
//出口参数 :
//返回值 : 0 接收到数据 1 ,没收到数据
u8 RecPacket(Packet* pdata)
{
u8* ptemp = rs485def.recbuf;
u8 len;
u8 buf[Rec_Buf_Size-20];
pdata->pInform = (char *)buf;
if(rs485def.recflag == 1)
{
rs485def.recflag = 0 ;
//跳过包头
ptemp += 4;
//获取数据域长度
len = *(u32*)ptemp;
//获取验证码
ptemp += 4;
pdata->identify = *(u16*)ptemp;
//获取源地址
ptemp += 2;
pdata->SrcAddr = *(u16*)ptemp;
//获取目的地址
ptemp += 2;
pdata->DesAddr = *(u16*)ptemp;
//获取命令码
ptemp += 2;
pdata->CmdNum = *(u16*)ptemp;
ptemp += 2; //指向信息内容
//获取信息长度
pdata->InformLlen = len - 8;
//获取信息内容
memcpy(pdata->pInform,ptemp,pdata->InformLlen);
//清除
// memset(&rs485def,0,sizeof(RS485));
rs485def.reclen = 0 ;
return 0 ;
}
return 1;
}
//数据包发送函数
//入口参数:发送的数据包
void SendPacket(Packet* pdata)
{
u8* ptemp = rs485def.sendbuf;
u8 sendlen;
// u8 i;
//把需要发送的数据赋给sendbuf
//包头
*(u32*)ptemp=PACK_START;
// printf("0x%x\r\n",*ptemp);
//数据域长度
ptemp += 4;
*(u32*)ptemp = pdata->InformLlen+8;
//验证码
ptemp += 4;
*(u16*)ptemp = pdata->identify;
//源地址
ptemp += 2;
*(u16*)ptemp = pdata->SrcAddr;
//目的地址
ptemp += 2;
*(u16*)ptemp = pdata->DesAddr;
//命令码
ptemp += 2;
*(u16*)ptemp = pdata->CmdNum;
//信息内容
ptemp += 2;
//strcpy strncpy memcpy memset-->这几个函数的区别
memcpy(ptemp,pdata->pInform,pdata->InformLlen);
//RS485_Send(ptemp,pdata->InformLlen);
//包尾
ptemp+=pdata->InformLlen;
*(u32*)ptemp=0x88CC55AA;
//发送数据
ptemp += 4;
sendlen = ptemp - rs485def.sendbuf;//发送数据长度
// printf("send:");
// for(i=0;i<sendlen;i++)
// printf("%x\t",rs485def.sendbuf[i]);
RS485_Send(rs485def.sendbuf,sendlen); //发送数据
}
#ifndef _RS485_H_
#define _RS485_H_
#include "stm32f10x.h"
#include "io_bit.h"
#define Send_Buf_Size 256
#define Rec_Buf_Size Send_Buf_Size
typedef struct
{
u8 *sendbuf; //发送数据缓冲区
u8 *recbuf; //接收数据缓冲区
u8 reclen; //接收数据总长度
u8 recflag; //接收完成事件标志位
}RS485;
extern RS485 rs485def;
//数据包
#define PACK_START 0xAA55CC88 //帧头
#define PACK_TAIL 0x88CC55AA //帧尾
typedef struct
{
u16 identify; //验证码 --字节
u16 SrcAddr; //源地址
u16 DesAddr; //目的地址
u16 CmdNum; //命令码
char* pInform; //信息内容
u8 InformLlen; //信息长度
}Packet;
u8 RecPacket(Packet* pdata);
void SendPacket(Packet* pdata);
#endif
main
int main()
{
Packet SendPack;
Packet RecPack;
char sendbuf[6]= "12345";
LED_Init();
KEY_Init();
SCB->AIRCR= 0X05FA0500 ; //设置为分组2
USART1_Init(115200);
delay_init(72);
OLED_Init();
W25QXX_Init();
DHT11_Init();
RS485_Init();
while(1)
{
if(Key_Scanf(0)) //发送
{
// printf("key\r\n");
//填充数据包 --- 学习 433模块 CC1101
SendPack.identify = 0x1234; //验证码
SendPack.SrcAddr = 0x5678; //源地址
SendPack.DesAddr = 0x90ab; //目的地址
SendPack.CmdNum = 0xcdef; //命令码
SendPack.pInform = sendbuf; //信息内容
SendPack.InformLlen=strlen(sendbuf);//信息长度
SendPacket(&SendPack);
}
if(!RecPacket(&RecPack))//收到数据
{
//打印收到的数据
printf("identify:0x%x\r\n",RecPack.identify); //验证码
printf("SrcAddr:0x%x\r\n",RecPack.SrcAddr); //源地址
printf("DesAddr:0x%x\r\n",RecPack.DesAddr); //目的地址
printf("CmdNum:0x%x\r\n",RecPack.CmdNum); //命令码
printf("pInform:%s\r\n",RecPack.pInform); //信息内容
printf("InformLlen:%d\r\n",RecPack.InformLlen);//信息长度
}
}
}
总结:
协议在一些公司的项目一般都会用到,最常见的就是485-modbus,不过基本的格式都差不多,这部分内容在项目了算是比较重要的,IAP升级的常用YModem协议也是异曲同工,类似的芯片DHT11和模块也会有协议格式要求。大家如果对我的博客有疑问或者错误,可以@我修改,大家相互交流。
交流群:717237739
如果觉得有用点赞关注收藏三连,多谢支持
本博客内容原创,创作不易,转载请注明
点赞收藏关注博主,不定期分享单片机知识,互相学习交流。
————————————————