STM32/GD32——自己制定协议和解析协议数据

温馨提醒:

由于我最害怕的就是接触各种新协议,尤其是对各种协议和解析协议数据简直就是职业生涯的噩梦,但工作中不免和不同的协议打交道。本着要啃就啃最难的,大不了放弃的心态。所以我学习了如何自定义制定自己的协议,如果你学会自己自定义协议后,那以后无论什么新协议对你来说都不再困难,也不再恐惧。但本文纯属个人学习经验分享,如介意,请千万不要食用。

芯片选型

Ciga Device — GD32F470系列

协议定义

什么是协议?不要把协议想的那么困难,协议有简单的,也有复杂的。最易懂的解释就是,我们约定好数据是什么格式,大家都按照这个规则来收发数据,这个规则就是协议。

举个例子,最简单的协议数据我们甚至可以用一个字符串来表示,类似于“led:1”,咱们约定好,发送端发送对应的LED数字,用冒号隔开,冒号后面是数字几,就点亮第几盏灯;那接收端收到数据后也用这个格式来解析出数据,再点亮数字对应的第几盏灯。

但在集成电路开发中,我们的寄存器资源是非常珍贵的,它不像手机内存动不动就几个G,有的芯片的内存可能只有几KB。再加上在电路与计算机底层中,0和1能让它们计算得更快。所以我们通常用位来存储数据,我们也用位和字节来制定协议。千万不要小看一个字节,一个字节有8个BIT,可以表示256种数据。试想一下,如果我们按照字符串“led:1”来传输数据,这需要5个字节,但如果我们约定用BIT来传输,1个BIT就够了,甚至不到1字节,是不是传输效率大大滴增加?计算效率大大滴提升?

大多数硬件协议定义通常都是有套路的,下面我们来详细康康。

硬件协议通用套路

帧头+命令+数据长度位+数据位+校验位+帧尾。(通常用字节进行传输)

协议定义
帧头命令位数据长度数据位校验位帧尾
字节数111n11
默认值0x7a待定待定待定待定0x7b
  • 帧头:可以是任意数据,代表着从这里我要开始传输数据啦;
  • 命令位:表示命令的类型;(比如1是调节PWM的;0是调节PID)
  • 数据长度:通常用来说明传输的数据位有多少字节,经常用于校验数据传输中是否有数据丢失或其他异常情况;
  • 校验位:也是用来确定我们的数据传输是否正确而不是其他方伪造或存在丢失情况;
  • 帧尾:可以是任意数据,代表着我的数据传完啦、结束啦;

此时我们有个需求:上位机要传输PID的调试数据,数据位中有4个数据,通道ID(1个字节),P、I、D都是浮点类型数据(每个4字节),我们可以像下面这样定义:

帧头

命令位

数据长度

数据位

校验位

帧尾

idx

P

I

D

字节数

1

1

1

1

4

4

4

1

1

默认值

0x7a

0x01

待定

待定

0x7b

  • idx:1个字节,int类型, 表示配置哪一组PID
  • P: 4个字节,float类型。P值
  • I: 4个字节,float类型。I值
  • D: 4个字节,float类型。D值

协议生成

协议生成其实就是按照我们上面制定的那些规则拼装出规则数据再发出去。是的,你没看错,就是把数据按规则拼装出来,再发出去,就这么简单。

这里不得不插入一下校验位是咋肥事,否则没法发送校验码。(当然我们也可以不发送校验码,或者把校验码位固定发送成0x00也不是不行哈!只要解析的时候也按照这个套路来那就么得问题)

校验位

数据在传输过程中,可能会存在数据出错的情况。为了保证数据传输的正确性,因此会采取一些方法来判断数据是否正确,或者在数据出错的时候及时发现进行改正。常用的几种数据校验方式有奇偶校验、CRC校验、LRC校验、格雷码校验、和校验、异或校验等。

我们只说说异或校验、和校验、奇偶校验,其他的我也不会哈,请自行问AI吧!

  • 奇校验(ODD):校验位被设置为确保数据位中1的总数为奇数。例如,数据位中的“1”总数为奇数,校验位被设置为低电平(拉低为0),否则设置为高电平。故而,如果接收方统计发现“1”总数为偶数,且校验是低电平,则校验失败,否则成功。
  • 偶校验(Even): 校验位被设置为确保数据位中1的总数为偶数。例如,数据位中的“1”总数为偶数,校验位被设置为低电平(拉低为0),否则设置为高电平。故而,如果接收方统计发现“1”总数为奇数,且校验是低电平,则校验失败,否则成功。
  • 异或校验:帧头、命令位、数据长度、所有的数据位全部异或后=校验位的值;(常见的XOR8校验算法就是全部异或后,取低8位的值)
  • 和校验:帧头+命令位+数据长度+所有的数据位=校验位的值;(常见的ADD8校验算法就是全部进行与运算后,取低8位的值。像CRC16就是保留后2个字节)

协议构建与发送

// 发送数据结构体
typedef struct {float channels[8];uint8_t cmd; // 区分这是什么类型的数据uint8_t len;
} bt_tx_data_t;/*
使用Hex格式通讯协议包格式: 帧头	命令码		数据长度						自定义数据								 	    校验码	   帧尾0x7A	 0xFF			0x0D	 0x00	0x1F854541 0x1F854541 0x1F854541   0xE1   0x7B 校验码:XOR8, ADD8, CRC167A FF 0D 00 CF F7 71 41 6F 12 83 3A 8F C2 F5 3C C0 7B -----------------------------------
7A 01 0D FF 00 00 70 41 6F 12 83 3A 8F C2 F5 3C F8 7B*/
// 定义帧头字节
#define TX_FRAME_HEAD 0x7A//定义帧尾字节
#define TX_FRAME_TAIL 0x7B
static void send_bt_protocol(bt_tx_data_t *data){// 计算包内总长度uint8_t frame_len = 5+(4*data->len);//动态申请内存uint8_t * frame_pack = malloc(frame_len);//帧头uint8_t index =0;frame_pack[index++] = TX_FRAME_HEAD//命令码frame_pack[index++] =data->cmd;//数据长度frame_pack[index++] =data->len;//填充自定义数据uint8_t bytes[4];//遍历数据,for	(int i=0;i<data->len;i++){//将每个float转化成bytesfloatToBytes(data->channels[i],bytes);frame_pack[index++]=bytes[0];frame_pack[index++]=bytes[1];frame_pack[index++]=bytes[2];frame_pack[index++]=bytes[3];}//填充校验码uint8_t checksum = xor8(frame_pack,index);//填充帧尾frame_pack[index++]=checksum;//把数据发出去USART1_send_data(frame_pack,frame_len);//释放动态申请内存free(frame_pack);
}

校验与转换工具

/* Protocol.c */#include "Protocol.h"// ADD校验计算函数
uint8_t add8(uint8_t *data, uint32_t size) {uint8_t checksum = 0;for (uint32_t i = 0; i < size; i++) {checksum += data[i];}return checksum & 0xFF;
}
// 异或校验计算函数
uint8_t xor8(uint8_t *data, uint32_t size) {uint8_t checksum = 0;for (uint32_t i = 0; i < size; i++) {checksum ^= data[i];}return checksum;
}
// 将float转换为字节数组
void floatToBytes(float f, uint8_t bytes[]) {FloatBytes fb;fb.floatValue = f;for (int i = 0; i < 4; i++) {bytes[i] = fb.bytesValue[i];}
}// 将字节数组转换为float
float bytesToFloat(uint8_t bytes[]) {FloatBytes fb;for (int i = 0; i < 4; i++) {fb.bytesValue[i] = bytes[i];}return fb.floatValue;
}
/* Protocol.h */#ifndef __PROTOCOL_H__
#define __PROTOCOL_H__#include "gd32f4xx.h"// 联合体
typedef union {float floatValue;uint8_t bytesValue[4];
} FloatBytes;uint8_t add8(uint8_t *data, uint32_t size);uint8_t xor8(uint8_t *data, uint32_t size);// 将float转换为字节数组
void floatToBytes(float f, uint8_t bytes[]);
// 将字节数组转换为float
float bytesToFloat(uint8_t bytes[]);
#endif

协议解析

理想数据正常解析

这是理想状态,假设每次接收的数据都是完整的从帧头到帧尾。

void handle_with_protocol(bt_rx_data_t * buffer){if(buffer->len<16) return;uint8_t cmd_code = buffer->data[1];uint8_t chn_code = buffer->data[3];float kp = bytesToFloat(&buffer->data[4]);float ki = bytesToFloat(&buffer->data[4+4]);float kd = bytesToFloat(&buffer->data[4+4+4]);
}

真实情况数据解析处理

真实的数据的传输经常会有连包、粘包、断包情况。此时需要先将接收到的数据存入环形队列中,再进行解析

环形队列原理

循环队列 (Circular Queue) 是一种数据结构(或称环形队列、圆形队列)。它类似于普通队列,但是在循环队列中,当队列尾部到达数组的末尾时,它会从数组的开头重新开始。这种数据结构通常用于需要固定大小的队列,例如计算机内存中的缓冲区。循环队列可以通过数组或链表实现,它具有高效的入队和出队操作。

队列的几种状态

区分循环队列空和满

环形队列代码实现

头文件:circular_queue.h
#ifndef __CIRCULAR_QUEUE_H__
#define __CIRCULAR_QUEUE_H__#include <stdint.h>typedef struct {uint32_t head;      // 队头指针uint32_t tail;      // 队尾指针uint32_t size;      // 队列大小uint8_t *buffer;    // 队列缓冲区
} QueueType_t;typedef enum {QUEUE_OK = 0,  // 成功QUEUE_FULL,   // 队列满QUEUE_EMPTY,  // 队列空QUEUE_ERROR   // 错误} QueueStatus_t;// 初始化
void queue_init(QueueType_t *queue, uint8_t *buffer, uint32_t buffer_size);// 入队
QueueStatus_t queue_push(QueueType_t *queue, uint8_t dat);// 出队
QueueStatus_t queue_pop(QueueType_t *queue, uint8_t *p_dat);// 压入一组数据
uint32_t queue_push_array(QueueType_t *queue, uint8_t *p_arr, uint32_t len);// 出队一组数据
uint32_t queue_pop_array(QueueType_t *queue, uint8_t *p_arr, uint32_t len);// 获取队列数据个数
uint32_t queue_data_count(QueueType_t *queue);#endif
C实现:circular_queue.c
#include "circular_queue.h"/*** @brief 初始化环形队列    * \param queue  队列结构体变量指针* \param buffer 队列缓存区地址* \param buffer_size   队列最大大小*/
void queue_init(QueueType_t *queue, uint8_t *buffer, uint32_t buffer_size)
{queue->head = 0;queue->tail = 0;queue->size = buffer_size;queue->buffer = buffer;
}/*** @brief 数据入队(向队列尾部插入数据)* * \param queue 队列结构体变量指针* \param dat  一个字节数据* \return QueueStatus_t  入队结果 QUEUE_OK 成功*/
QueueStatus_t queue_push(QueueType_t *queue, uint8_t dat)
{// 计算下一个元素的索引uint32_t next_index = (queue->tail + 1)  % queue->size;// 队列满(保留一个空位)if (next_index == queue->head) {return QUEUE_FULL;} // 写入数据queue->buffer[queue->tail] = dat;// 更新队尾指针queue->tail = next_index;return QUEUE_OK;
}/*** @brief 数据出队(从队首弹出数据)* * \param queue 队列结构体变量指针* \param pdat  出队数据指针* \return QueueStatus_t */
QueueStatus_t queue_pop(QueueType_t *queue, uint8_t *p_dat){// 如果head与tail相等,说明队列为空if (queue->head == queue->tail) {return QUEUE_EMPTY;}// 取head的数据*p_dat = queue->buffer[queue->head];// 更新队头指针queue->head = (queue->head + 1) % queue->size;return QUEUE_OK;
}/*** @brief 获取队列数据个数* * \param queue  队列指针* \return uint32_t  队列有效数据个数*/
uint32_t queue_data_count(QueueType_t *queue){if (queue->tail >= queue->head){// 队尾指针在队头指针后边return queue->tail - queue->head;}// 队尾指针在队头指针前边(转了一圈到了队头指针之前)return queue->size + queue->tail - queue->head;
}/*** @brief 压入一组数据* * \param queue  队列结构体变量指针* \param p_arr  待入队数组指针* \param len    待入队数组长度 * \return uint32_t 实际写入的数据个数 */
uint32_t queue_push_array(QueueType_t *queue, uint8_t *p_arr, uint32_t len){uint32_t i;for(i = 0; i < len; i++){if (queue_push(queue, p_arr[i]) == QUEUE_FULL){break;}}return i;
}/*** @brief 出队一组数据* * \param queue 队列指针* \param p_arr 待出队数组指针* \param len   待出队数组长度* \return QueueStatus_t */
uint32_t queue_pop_array(QueueType_t *queue, uint8_t *p_arr, uint32_t len){uint32_t i;for(i = 0; i < len; i++){if (queue_pop(queue, &p_arr[i]) == QUEUE_EMPTY){break;}}return i;
}

HEX数据解析

代码逻辑步骤
  1. 创建一个环形队列,将接收到的HEX数据缓存到环形队列中 (这一步在串口接收到数据后存到环形队列中)
  2. 创建一个临时数组,用来存放符合协议约定的一组正常数据
  3. 如果环形队列中接收到数据,则开始解析
  4. 循环判断环形队列中的数据是否为0X7A,如不是则找下一个
  5. 如有数据为0X7A,则读出长度数据,判断长度是否符合协议规定
  6. 判断帧尾是否与期待的一致
  7. 判断临时数组中的校验码是否与期望的一致
  8. 把有效数据(去掉帧头、校验位、校验位)发送给调用者
  9. 重置临时数组索引与数据个数(以便于找下一组数据)
代码实现
//重置索引
void reset_recv_info(){g_recv_index=0;g_recv_data_cnt=0;
}#define DATA_PACKAGE_LEN_MAX 30
int g_recv_index=0; // 环形队列中取数据的索引
int g_recv_data_cnt=0; //这个有效数据包的长度
uint8_t read_buf[DATA_PACKAGE_LEN_MAX]={0};
void Protoclo_task(){// 只要队列里面有数据,就解析数据while(queue_data_count(&g_recv_queue)>0){if(queue_pop(&g_recv_queue,&read_buf[g_recv_index])!=QUEUE_OK){continue;}g_recv_index++;//判断0位是否是0x7aif(read_buf[0]!=)x7a){reset_recv_info();continue;}//记录数据长度,计算数据包长度if(g_recv_index==3){g_recv_data_cnt=read_buf[2];//如果数据包超过最大长度,说明是无效数据if(g_recv_data_cnt+5>DATA_PACKAGE_LEN_MAX){   reset_recv_info();continue;}}//判断数据个数是否符合目标,如数据不够,继续等int pack_len = g_recv_data_cnt+5;if(g_recv_data_cnt<pack_len ){continue;}//如果缓存区长度符合目标,检查校验码和帧尾if(read_buf[g_recv_data_cnt-1]!=0x7b){reset_recv_info();continue;}uint8_t expect_xor8 = xor8(read_buf,pack_len-2);if(read_buf[pack_len -2]!=expect_xor8 ){reset_recv_info();continue;}//发送给调用者send_data_to_called(read_buf+1,pack_len -3);//重新解析reset_recv_info();    }}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/776695.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小程序的编译上传

小程序如果是hbuilderx开发的&#xff0c;当开发完成后运行到小程序模拟器里面的微信开发者工具&#xff0c;或者&#xff08;发行里面的小程序微信&#xff08;仅适用于uniapp&#xff09;&#xff09;&#xff0c;然后打开微信开发者工具的 项目→里面的导入项目 &#x…

Unity中使用AssetPostprocessor对模型动画处理

在游戏开发的过程中&#xff0c;会遇到模型动作同事频繁的修改模型动画&#xff0c;比如可能某个动作不对了等等。而程序使用的动画clip是从模型中拷贝一份出来的&#xff0c;而在AssetPostprocessor中就可以做一些处理这样模型同事频繁修改动画的话&#xff0c;只需要重新导入…

JVM虚拟机-实战篇

专属小彩蛋:前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站(前言 - 床长人工智能教程) 目录 一、内存溢出和内存泄漏 什么是内存泄漏? 二、解决内存泄漏 解决内存泄漏的思路 top命令 发现问题 VisualVM 发现问…

Java File类(文件操作类)

背景&#xff1a; 在Java编程语言中&#xff0c;操作文件和目录是一项常见的任务。而File类&#xff0c;则是java.io包中的重要类&#xff0c;它是唯一代表磁盘文件本身的对象。通过File类提供的方法&#xff0c;我们可以轻松地创建、删除、重命名文件和目录等操作。 构造方法&…

记一个越权+逻辑漏洞产生的高危组合漏洞

懒得打码不放图 1、有两个测试账号&#xff0c;a&#xff08;攻击账号&#xff09;&#xff0c; 测试账号b&#xff08;被修改信息的账号&#xff09; 2、图片拉到最下有修改信息按钮&#xff0c;点击修改并抓包&#xff0c;得到两份数据包 ​经过观察得知&#xff0c;两个账…

ffmpeg重点之时间戳,PTS、DTS、time_base

PTS和DTS和时间基time_base 首先我们知道PTS是一帧音频或视频显示的时间&#xff0c;DTS是解码时间戳 既然是时间&#xff0c;PST和DTS的单位是什么呢?秒还是毫秒&#xff0c;抑或是纳秒&#xff1f; 先说结论—都不是 先引入FFmpeg中时间基的概念&#xff0c;也就是time_bas…

C++在非面向对象方面的扩充

1.注释行 在C中&#xff0c;可以用"/*"和"*/"作为注释分界符号。如&#xff1a; /* This is a test.*/ 还可以用//来进行注释&#xff0c;如&#xff1a; //This is a test. "/*~~~~~~*/"表示方式不能嵌套&#xff0c;却可以在里面嵌套&quo…

利用FLEX BISON 快速实现简单的C 语言编译器前端

Flex Flex是一个生成词法分析器的工具&#xff0c;它可以利用正则表达式来生成匹配相应字符串的C语言代码&#xff0c;其语法格式基本同Lex相同。 格式 LEX的源文件由三个部份组成&#xff0c;每个部分之间用顶行的 %%’ 分割&#xff0c;其格式如下&#xff1a; 定义部份 %…

k8s 如何获取加入节点命名

当k8s集群初始化成功的时候&#xff0c;就会出现 加入节点 的命令如下&#xff1a; 但是如果忘记了就需要找回这条命令了。 kubeadm join 的命令格式如下&#xff1a;kubeadm join --token <token> --discovery-token-ca-cert-hash sha256:<hash>--token 令牌--…

虚拟机-从头配置Ubuntu18.04(包括anaconda,cuda,cudnn,pycharm,ros,vscode)

最好先安装anaconda后cuda和cudnn&#xff0c;因为配置环境的时候可能conda会覆盖cuda的路径&#xff08;不确定这种说法对不对&#xff0c;这里只是给大家的建议&#xff09; 准备工作&#xff1a; 1.Ubuntu18.04&#xff0c;x86_64&#xff0c;amd64 虚拟机下载和虚拟机Ubu…

[密码学] 密码学基础

目录 一 为什么要加密? 二 常见的密码算法 三 密钥 四 密码学常识 五 密码信息威胁 六 凯撒密码 一 为什么要加密? 在互联网的通信中&#xff0c;数据是通过很多计算机或者通信设备相互转发&#xff0c;才能够到达目的地,所以在这个转发的过程中&#xff0c;如果通信包…

康耐视visionpro-CogToolBlock工具详细说明

CogToolBlock功能: 将多个工具组合在一起完成某个功能&#xff0c;接口简单且可以重用 CogToolBlock操作说明&#xff1a; 1.打开工具栏&#xff0c;双击或点击鼠标拖拽添加CogToolBlock CogToolBlock操作说明 ②.添加输入图像&#xff0c;右键“链接到”或以连线拖拽的方式选…

Games104 听后笔记

1、为什么UE5要自己写一套STL库 因为传统的STL库中&#xff0c;例如&#xff1a;vector&#xff0c;它一般采用的是双倍扩容法&#xff0c;加入1000个数据装满了&#xff0c;现在需要又加一个&#xff0c;那么就开辟了2000个数据的空间&#xff0c;那么当前就又999的数据空间暂…

Python爬虫:爬虫常用伪装手段

目录 前言 一、设置User-Agent 二、设置Referer 三、使用代理IP 四、限制请求频率 总结 前言 随着互联网的快速发展&#xff0c;爬虫技术在网络数据采集方面发挥着重要的作用。然而&#xff0c;由于爬虫的使用可能会对被爬取的网站造成一定的压力&#xff0c;因此&#…

UE RPC 外网联机(1)

技术&#xff1a;RPC TCP通信 设计&#xff1a;大厅服务<---TCP--->房间服务<---RPC--->客户端&#xff08;Creator / Participator&#xff09; 1. PlayerController 用于RPC通信控制 2.GameMode 用于数据同步 3.类图 4. 注意 &#xff08;1&#xff09;RPC&a…

记录一次数组越界导致的线程死锁问题

1. 问题描述 在一次代码调试的过程中&#xff0c;遇到过一个问题&#xff0c;线程在调用pthread_cancel时&#xff0c;提示未找到目标线程&#xff0c;然后程序阻塞在了与目标线程相关的条件变量的释放上&#xff0c;造成了死锁的现象。 2. 问题复现 #include <pthread.h…

微机原理与接口技术-精选复习题

1、ADC0809的START和EOC引脚的功能是什么&#xff1f;在查询方式和中断方式中&#xff0c;EOC引脚分别如何处理&#xff1f; START是转换启动信号&#xff0c;EOC信号是转换结束信号。在查询方式中&#xff0c;EOC可作为状态信息输入至CPU以供查询&#xff1b;在中断方式中EOC…

python--初学函数

函数&#xff08;function&#xff09;&#xff1a; 什么是函数&#xff1f; 具有名称的&#xff0c;是为了解决某一问题&#xff0c;功能代码的集合&#xff0c;叫做函数 python中函数如何定义&#xff1a;def>define function定义函数 def function_name([args临时变量…

JavaScript 常用方法(1):JS日期格式化函数、JS实现并列排序、JS实现数字每三位添加逗号、JS 实现根据日期和时间进行排序

1、JS日期格式化函数 JS日期格式化转换方法 /*** description 格式化时间* param fmt 格式 如&#xff1a;yyyy-MM-dd、yyyy-MM-dd HH:mm:ss、yyyy年MM月dd日 W HH:mm:ss等* param {String} date 时间戳* returns {string|null}* 对 Date 的扩展&#xff0c;将 Date 转化为指…

如何在CentOS使用Docker搭建MinIO容器并实现无公网ip远程访问本地服务

文章目录 前言1. Docker 部署MinIO2. 本地访问MinIO3. Linux安装Cpolar4. 配置MinIO公网地址5. 远程访问MinIO管理界面6. 固定MinIO公网地址 前言 MinIO是一个开源的对象存储服务器&#xff0c;可以在各种环境中运行&#xff0c;例如本地、Docker容器、Kubernetes集群等。它兼…