先说解决方案,细节和实现代码都放在正文
下位机:把结构体拆分成8位的整型数据,加上数据包头和包尾,然后按顺序单个单个地发出;
上位机:把串口里的数据读取出来,找到包头,按顺序装填到结构体中,然后使用结构体引用数据;
一、串口通信
串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式
相信浏览本文的朋友都已经使用过串口通信协议在各机器之间传递信息。这种通讯方式只需要四只引脚就能在短距离内实现全双工的通信,非常方便。目前许多通用芯片(如STM32)还提供了硬件支持,并给定了数据收发的接口函数。
以stm32为例,我们可以通过一些特定的函数使用芯片上的USART外设,不需要操心协议的电平规定就能够进行数据的收发,相关的函数在STM32的参考手册中列出:
在固件库中也可以找到:
大致看上去十分方便,但细看就会发现一个十分重要的问题,譬如发送数据的这个函数USART_SendData()
:
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
assert_param(IS_USART_ALL_PERIPH(USARTx));
assert_param(IS_USART_DATA(Data));
USARTx->DR = (Data & (uint16_t)0x01FF);
}
他只给你提供了单个数据的发送方式,而且数据类型限制位16位无符号整型(uint16_t)
,这就引出了我们在文章开头提到的拆分并封包发送的必要性。下面我们先介绍一下发送的结构体的样子,然后再说拆分封包的问题。
二、定义要发送的结构体
首先明确发送的结构体是什么样子的
typedef struct CSModuleInfo_ACC{
float _acc_X;
float _acc_Y;
float _acc_Z;
}CSInfo_Acc;
typedef struct CSmouduleInfo_LL{
float _latitude;
float _longitude;
}CSInfo_LL;
typedef struct CSInfoStrcutre CSInfoS;
typedef struct CSInfoStrcutre{
float _temp_O_MCU;
float _temp_env;
float _gp;
CSInfo_Acc _acc;
CSInfo_LL _ll;
}* ptrCSInfo;
为了保证本文能符合大伙的需求,咱搞一个结构体嵌套,并且把数据类型都定义浮点数。意在说明我们这种传输结构体的方式不受结构体类型的限制,也不受浮点数的存储方式的限制,请放心学习使用。
注:代码中部分的
ptrCSInfo
是这个大结构体的指针类型,CSInfoS
是这个结构体的别名,这种写法是C语言的语法规则所允许的,不用感到奇怪。
三、下位机封包发送
封包发送的过程可以用下面的代码实现:
void CSInfo_PackAndSend(ptrCSInfo ptrInfoStructure)
{
uint8_t infoArray[32]={ 0};
uint8_t infoPackage[38]={ 0};
CSInfo_2Array_uint8(ptrInfoStructure,infoArray);
CSInfo_Pack(infoPackage,infoArray,sizeof(infoArray));
CSInfo_SendPack(infoPackage,sizeof(infoPackage));
}
向这个函数传入一个装有数据的结构体的指针ptrInfoStructure
,依次调用CSInfo_2Array_uint8
、CSInfo_Pack
、CSInfo_SendPack
这三个自定义函数,即可通过串口将结构体发送出去。
这三个函数分别对应着擦拆分结构体、按照协议/规则封包和发送数据三个过程,具体说明和代码如下:
1、拆分
文章开头我们已经说了,先要把结构体拆分成8位无符号整型(uint8_t
)的数据:
void CSInfo_2Array_uint8(ptrCSInfo infoSeg,uint8_t* infoArray)
{
int ptr=0;uint8_t
*infoElem=(uint8_t*)infoSeg;
for(ptr=0;ptr<sizeof(CSInfoS);ptr++){
infoArray[ptr] = (*(infoElem+ptr));
}
}
传入一个结构体的指针,并传入一个对应大小(uint8_t
)类型的数组,用来装结构体拆分出来的元素。
那么数组需要多大呢?我们知道8位(bit)就是一个字节(Byte),所以这个数组理论上只需要和这个结构体的字节数一样大就可以了!也就是:
sizeof(CSInfoS)
的返回值。这里我们也可以口算一下,结构体中总共有8个float
类型的数据,也就是8×32bit=8×4Byte=32Byte。结构体的大小也就是32字节,所以可以拆分成32个unit8_t
类型的元素,数组大小也就需要32。
注意:传入的数组需要足够的大小,不要整个空指针或者不够大的数组进去。当然,你也可以返回一个数组,但我喜欢这种隐式返回的风格。
2、封包
选定一组特定的数据作为数据包的头部,选定另一组特定的数据作为数据包的尾部,方便我们在上位机接收数据后找到每一组数据的开始和结尾。
这里我们选定:
0x80 0x81 0x82
作为数据包的头部,同时选定:
0x82 0x81 0x80
作为数据包的尾部。所以我们向上位机发送的单个数据包都是如下形式的:
上面|CSInfoStrcutre|
的位置就是我们在上一步获得的uint8_t
类型的数组infoArray
,内容是CSInfoStrcutre中的数据。
封包的过程如下面的代码所示:
void CSInfo_Pack(uint8_t* infopackage,uint8_t* infoArray,uint8_t infoSize)
{
uint8_t ptr=0;
infopackage[0] = HEAD1;
infopackage[1] = HEAD2;
infopackage[2] = HEAD3;
for(;ptr<infoSize;ptr++){
infopackage[ptr+3] = infoArray[ptr];
}
infopackage[ptr+3] = TAIL1;
infopackage[ptr+4] = TAIL2;
infopackage[ptr+5] = TAIL3;
}
3、发送
接着,我们将把这个玩意儿(infopackage
)通过串口发送出去:
void CSInfo_SendPack(uint8_t* infoPackage,uint8_t packSize)
{
int ptr=0;
for(ptr=0;ptr<packSize;ptr++){
USART_SendByte(infoPackage[ptr]);
}
}
注意,为了方便使用,这里我们用到了一个名为USART_SendByte
的自定义函数,其定义如下:
void USART_SendByte(uint8_t byte)
{
USART_SendData(DEBUG_USARTx,byte);
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
}
到这里,我们就了解完了下位机打包发送的部分,接下来我们转到上位机视角,看一下咋个接收数据,咋个解析数据,也就是咋个把数据又装回一个结构体里,方便我们引用。
四、上位机接收数据并解包
回顾一下文章开头,我们说上位机的这部分工作的流程是这样的:
1、把串口里的数据读取出来
2、找到包头,
3、把数据包中对应数据的部分按顺序装填到结构体中
大致流程如下面的代码所示:
uint8_t packages[INFOSIZE*3]={ 0};
int numHasRead = readInfoFromSerialport(packages);
uint8_t infoArray[INFOSIZE];
bool readable = CSInfo_GetInfoArrayInpackages(infoArray,packages,numHasRead);
if(readable)
CSInfo_InfoArray2CSInfoS(infoArray,this->_ptrCSInfo);
也即是依次调用readInfoFromSerialport
、CSInfo_GetInfoArrayInpackages
、CSInfo_InfoArray2CSInfoS
在这三个函数,从串口缓冲区的一堆数据里找到一个完整的数据包并把它装填到结构体里。
下面详细介绍这三个自定义函数:
1、读取数据
你可以用你所知的任何方法从串口的缓冲区读取出来,只要你能把它们放到一个方便后续的解包操作访问的地方。
这里我使用Qt开发的上位机界面,故而也顺带使用Qt提供的serialport类中的方法来读取,具体可以参考Qt的帮助文档,这里只做简要说明:
int readInfoFromSerialport(uint8_t* packages)
{
int numHasRead(0);
if(QSerialPortInfo::availablePorts().isEmpty())
return 0;
_port = new QSerialPort(QSerialPortInfo::availablePorts()[0]);
_port->setPort(QSerialPortInfo::availablePorts()[0]);
if(!_port->open(QIODevice::ReadWrite)){
goto next;
}else{
_port->setParity(QSerialPort::NoParity);
_port->setBaudRate(QSerialPort::Baud115200);
_port->setDataBits(QSerialPort::Data8);
_port->setStopBits(QSerialPort::OneStop);
_port->setFlowControl(QSerialPort::NoFlowControl);
_port->waitForReadyRead();
QByteArray dataArray = _port->read(200);
numHasRead = dataArray.size();
if(INFOSIZE*3<numHasRead){
for(int i=0;i<INFOSIZE*3;i++){
*(packages+i) = dataArray[i];
}
}
}
next:;
delete _port;
return numHasRead;
}
上述代码首先获取了一个serialport
类的对象_port
,然后通过一系列的setxxx
函数配置了必要的参数。接着调用readAll把串口中所有的数据读取到dataArray
(readAll()
的返回值就是一个QByteArray
类型的容器),然后把大小等同于三个infoStructure
的数据放到packages
中,预备进行下一步的解包操作。
注意,之所以要读取三个基数,是为了保证至少包含一个完整的数据包。
2、找到一个完整的数据包
前面提到了,我们设定每个数据包的头部是0x80|0x81|0x82 ,而数据包的尾部则反过来。根据这个特征: |
---|
我们可以先在上一步获得的packages中找到一个数据包的头部,以确定一个数据包的开始位置:
bool CSInfo_GetInfoArrayInpackages(uint8_t* infoArray,uint8_t* packages,int sizepackages)
{
int ptr;bool readable(true);
if(sizepackages<INFOSIZE*3){
readable = false;
return readable;
}
for(ptr=0;ptr<INFOSIZE*3;ptr++){
// or: for(ptr=0;ptr<sizepackages-3;ptr++){ */
if((packages[ptr]==HEAD1)&&(packages[ptr+1]==HEAD2)&&(packages[ptr+2]==HEAD3))
break;
}
ptr += 3;
for(int i=0;i<INFOSIZE;i++)
infoArray[i] = packages[ptr+i];
return readable;
}
通过调用这个函数,我们把packages
中的一个完整的数据包的InfoStructure
部分放到了infoArray
中。接下看第三步,我们将把这个结构体的数据写入一个结构体中,真正还原它在下位机中的样子:
3、解析数据
直接把结构体当成一个数组,把数据依次填写进去就ok了
void CSInfo_InfoArray2CSInfoS(uint8_t* const infoArray,ptrCSInfo infoStrc)
{
uint8_t* u8PtrOStrc = (uint8_t*)infoStrc;
for(int i=0;i<INFOSIZE;i++)
*(u8PtrOStrc+i) = infoArray[i];
}
到这里,我们就完成了使用串口发送结构体的任务,而且了解了封包和解包的基本思路。
我把上位机的源代码链接放到这里,需要的可以单击自取。读取和解析的代码分别在Sources/CSInfoReader.c
和Sources/CSInfoParser.c
文件中。
下位机的代码暂未上传,需要的朋友可以留言索取。