学习目标
- 熟悉协议的定义
- 掌握协议生成
- 掌握协议解析
- 熟悉消息队列处理协议
- 熟悉消息队列处理业务
学习内容
协议的定义
| | 帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | | —- | —- | —- | —- | —- | —- | —- | | 字节数 | 1 | 1 | 1 | n | 1 | 1 | | 默认值 | 0x7a | 待定 | 待定 | 待定 | 待定 | 0x7b |
命令位: 表示命令类型数据位n值,由数据长度位的值决定校验位:(命令位 + 数据长度位 + 数据位)的结果,取高位
以控制开发板上PID配置为例,定义出控制协议如下
| 帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | ||||
|---|---|---|---|---|---|---|---|---|---|
| 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值
协议生成
通过上位机生成协议,然后通过串口发送给下位机。
类型转换
python中,构建的协议最终是bytes,但是有意义的数据类型是int、float等等类型,因此存在转换。
def int2byte(v: int) -> bytes:return struct.pack('B', v)def float2bytes(v: float) -> bytes:return struct.pack('f', v)def bytes2float(v: bytes) -> float:return struct.unpack('f', v)[0]
协议构建
协议构建就是拼装出规定数据:
| 帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | |
|---|---|---|---|---|---|---|
| 字节数 | 1 | 1 | 1 | n | 1 | 1 |
| 默认值 | 0x7a | 待定 | 待定 | 待定 | 待定 | 0x7b |
def checksum(cmd: int, data: bytes) -> bytes:sum = cmdsum += len(data)for d in data:sum += d# 取高位sum &= 0xFF00sum >>= 8return int2byte(sum)def make_request(cmd: int, data:bytes) -> bytes:# 帧头 | 命令 | 数据长度 | 数据 | 校验码 | 帧尾# 1字节 | 1字节 | 1字节 | n字节 | 1字节 | 1字节# 命令: 请求类型的标识# 数据长度: 表示后面 数据 的字节个数# 校验码: 命令 + 数据长度 + 数据, 取高位result = b''result += int2byte(FRAME_HEAD)result += int2byte(cmd)result += int2byte(len(data))result += dataresult += checksum(cmd, data)result += int2byte(FRAME_END)return result
具体指令构建
具体指令构建是基于标准协议基础,处理具体逻辑的协议,为自定义:
| 帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | ||||
|---|---|---|---|---|---|---|---|---|---|
| idx | P | I | D | ||||||
| 字节数 | 1 | 1 | 1 | 1 | 4 | 4 | 4 | 1 | 1 |
| 默认值 | 0x7a | 0x01 | 待定 | 待定 | 0x7b |
def make_action_request(idx: int, p: float, i: float, d: float):data = b''data += int2byte(idx)data += float2bytes(p)data += float2bytes(i)data += float2bytes(d)return make_request(0x01, data)def print_request(request: bytes):print()for r in request:print("{:02x} ".format(r), end='')print()
串口调试
通过串口发送数据到设备,查看协议的接收
import serialfrom protocol import *from threading import Threaddef do_recv():print('do recv')while True:buffer = ser.read(1)print(buffer)if __name__ == '__main__':ser = serial.Serial(baudrate=115200, port="COM15")Thread(target=do_recv).start()request = make_action_request(0, 10, 0.1, 0.3)print_request(request)ser.write(request)# ser.close()
协议解析
通过下位机解析数据
环形缓冲

环形缓冲,也被称为环形队列,是一种常用的数据结构,用于实现在固定大小的缓冲区中高效地插入和删除数据。
环形缓冲区的特点是它具有固定的容量,并且在达到最大容量时,新的数据可以覆盖最旧的数据。这使得环形缓冲区适用于需要保持最新数据并丢弃旧数据的场景,如实时数据流处理、音频/视频流缓冲等。
环形缓冲区通常使用一个固定大小的数组来实现。该数组有两个指针,一个指向缓冲区的读取位置,另一个指向缓冲区的写入位置。读取位置用于获取缓冲区中的数据,写入位置用于添加新的数据。
主要操作为往环形数组中添加和移除数据。
添加如图所示:
添加数据其实,就是拨动结束的指针,然后长度加一
移除如图所示:
添加数据其实,就是拨动开始的指针,然后长度减一
环形缓冲实现:
#define RING_SIZE 512uint8_t ring[RING_SIZE];uint32_t ring_head = 0;uint32_t ring_end = 0;uint32_t ring_len = 0;void ring_put(uint8_t data) {if(ring_len > RING_SIZE) {// ring is full. errorreturn;}ring[ring_end++] = data;ring_end %= RING_SIZE;ring_len++;}void ring_take(uint8_t *data) {if(ring_len == 0) {// ring is empty.return;}*data = ring[ring_head++];ring_head %= RING_SIZE;ring_len--;}
解析逻辑
基于协议进行读取校验协议正确性
void do_parse() {// 帧头 | 命令 | 数据长度 | 数据 | 校验码 | 帧尾// 1字节 | 1字节 | 1字节 | n字节 | 1字节 | 1字节// 命令: 请求类型的标识// 数据长度: 表示后面 数据 的字节个数// 校验码: 命令 + 数据长度 + 数据, 取高位if(ring_len < 3) return;uint8_t tmp;uint8_t cmd;uint8_t len;// 判断帧头if(ring[ring_head] != FRAME_RX_HEAD) {// 不是,丢弃ring_take(&tmp);do_parse();return;}// 解析命令cmd = ring[(ring_head + 1) % RING_SIZE];// 数据长度len = ring[(ring_head + 2) % RING_SIZE];// 判断数据长度if(ring_len < len + 5) return;// 帧尾if(ring[(ring_head + 4 + len) % RING_SIZE] != FRAME_RX_END) {// 丢弃第一个// 不是,丢弃ring_take(&tmp);do_parse();return;}uint16_t sum = 0;sum += len;uint32_t i;for(i = 0;i < len; i++) {sum += ring[(ring_head + 3 + i) % RING_SIZE];}sum >>= 8;// 校验码if(sum != ring[(ring_head + 3 + len) % RING_SIZE]) {// 丢弃第一个// 不是,丢弃ring_take(&tmp);do_parse();return;}// 校验通过uint8_t* action = malloc((len + 5) * sizeof(uint8_t));for(i = 0;i < len + 5; i++) {ring_take(&tmp);action[i] = tmp;}Protocol_on_action(action, len + 5);free(action);}
对于具体功能性协议,需要解析出具体功能,例如控制舵机角度的协议:
static void Protocol_on_action(uint8_t *data, uint32_t len) {uint8_t cmd = data[1];if(cmd == CMD_PID && len == 18) {uint8_t idx = data[3];uint8_t tmp[4];tmp[0] = data[4];tmp[1] = data[5];tmp[2] = data[6];tmp[3] = data[7];float p = bytesToFloat(tmp);tmp[0] = data[8];tmp[1] = data[9];tmp[2] = data[10];tmp[3] = data[11];float i = bytesToFloat(tmp);tmp[0] = data[12];tmp[1] = data[13];tmp[2] = data[14];tmp[3] = data[15];float d = bytesToFloat(tmp);Protocol_on_pid(idx, p, i, d);}}
处理数据转换
typedef union {float f;unsigned char b[4];} FloatBytes;static void floatToBytes(float value, unsigned char *bytes) {FloatBytes fb;fb.f = value;bytes[0] = fb.b[0];bytes[1] = fb.b[1];bytes[2] = fb.b[2];bytes[3] = fb.b[3];}static float bytesToFloat(unsigned char *bytes) {FloatBytes fb;fb.b[0] = bytes[0];fb.b[1] = bytes[1];fb.b[2] = bytes[2];fb.b[3] = bytes[3];return fb.f;}
完整逻辑
#include "protocol.h"#include <stdio.h>#define FRAME_RX_HEAD 0xFA#define FRAME_RX_END 0xFB#define CMD_LEG 0x01#define RING_SIZE 512uint8_t ring[RING_SIZE];uint32_t ring_head = 0;uint32_t ring_end = 0;uint32_t ring_len = 0;//////////////////////////// 数据转换:不同平台可能存在大小端问题 /////////////////////////////// 当前是大端,修改顺序即可改为小端 ///////////////////typedef union {float f;unsigned char b[4];} FloatBytes;static void floatToBytes(float value, unsigned char *bytes) {FloatBytes fb;fb.f = value;bytes[0] = fb.b[0];bytes[1] = fb.b[1];bytes[2] = fb.b[2];bytes[3] = fb.b[3];}static float bytesToFloat(unsigned char *bytes) {FloatBytes fb;fb.b[0] = bytes[0];fb.b[1] = bytes[1];fb.b[2] = bytes[2];fb.b[3] = bytes[3];return fb.f;}///////////////////////////////////////////////////////////////void ring_put(uint8_t data) {if(ring_len > RING_SIZE) {// ring is full. errorreturn;}ring[ring_end++] = data;ring_end %= RING_SIZE;ring_len++;}void ring_take(uint8_t *data) {if(ring_len == 0) {// ring is empty.return;}*data = ring[ring_head++];ring_head %= RING_SIZE;ring_len--;}void Protocol_init() {}static void Protocol_on_action(uint8_t *data, uint32_t len) {uint8_t cmd = data[1];if(cmd == CMD_PID && len == 18) {uint8_t idx = data[3];uint8_t tmp[4];tmp[0] = data[4];tmp[1] = data[5];tmp[2] = data[6];tmp[3] = data[7];float p = bytesToFloat(tmp);tmp[0] = data[8];tmp[1] = data[9];tmp[2] = data[10];tmp[3] = data[11];float i = bytesToFloat(tmp);tmp[0] = data[12];tmp[1] = data[13];tmp[2] = data[14];tmp[3] = data[15];float d = bytesToFloat(tmp);Protocol_on_pid(idx, p, i, d);}}static void do_parse() {// 帧头 | 命令 | 数据长度 | 数据 | 校验码 | 帧尾// 1字节 | 1字节 | 1字节 | n字节 | 1字节 | 1字节// 命令: 请求类型的标识// 数据长度: 表示后面 数据 的字节个数// 校验码: 命令 + 数据长度 + 数据, 取高位if(ring_len < 3) return;uint8_t tmp;uint8_t cmd;uint8_t len;// 判断帧头if(ring[ring_head] != FRAME_RX_HEAD) {// 不是,丢弃ring_take(&tmp);do_parse();return;}// 解析命令cmd = ring[(ring_head + 1) % RING_SIZE];// 数据长度len = ring[(ring_head + 2) % RING_SIZE];// 判断数据长度if(ring_len < len + 5) return;// 帧尾if(ring[(ring_head + 4 + len) % RING_SIZE] != FRAME_RX_END) {// 丢弃第一个// 不是,丢弃ring_take(&tmp);do_parse();return;}uint16_t sum = 0;sum += len;uint32_t i;for(i = 0;i < len; i++) {sum += ring[(ring_head + 3 + i) % RING_SIZE];}sum >>= 8;// 校验码if(sum != ring[(ring_head + 3 + len) % RING_SIZE]) {// 丢弃第一个// 不是,丢弃ring_take(&tmp);do_parse();return;}// 校验通过uint8_t* action = malloc((len + 5) * sizeof(uint8_t));for(i = 0;i < len + 5; i++) {ring_take(&tmp);action[i] = tmp;}Protocol_on_action(action, len + 5);free(action);}void Protocol_parse(uint8_t *data, uint32_t len) {uint32_t i;for(i = 0;i < len;i++) {ring_put(data[i]);}do_parse();}
#ifndef __PROTOCOL_H__#define __PROTOCOL_H__#include "gd32f4xx.h"#include "systick.h"void Protocol_init();void Protocol_parse(uint8_t *data, uint32_t len);extern void Protocol_on_pid(uint8_t idx, float p, float i, float d);#endif
消息调度
串口只负责接收数据,不做重活。
void Usart0_recv(uint8_t *data, uint32_t len) {uint32_t i = 0, cnt = 32, size = len / cnt;bt_rx_data_t buffer;printf("BT recv \r\n");//portENTER_CRITICAL();for(i = 0; i < size; i++) {printf("BT A recv %d \r\n", i);memcpy(buffer.data, &data[i*cnt], cnt);buffer.len = cnt;xQueueSendFromISR(bt_rx_queue, &buffer, 0);}if(len % cnt != 0) {printf("BT B recv %d \r\n", i);memcpy(buffer.data, &data[i*cnt], len % cnt);buffer.len = len % cnt;xQueueSendFromISR(bt_rx_queue, &buffer, 0);//xQueueSend(bt_rx_queue, &buffer, 0);}//portEXIT_CRITICAL();printf("BT recv end\r\n");}
开启任务,负责专门的解析
static void bt_rx_task() {bt_rx_queue = xQueueCreate(64, sizeof(bt_rx_data_t));bt_rx_data_t data;uint32_t i;while(1) {if(xQueueReceive(bt_rx_queue, &data, portMAX_DELAY)) {printf("BT parse \r\n");for(i = 0; i < data.len; i++) {Protocol_pull(data.data[i]);printf("%02X ", data.data[i]);}printf("\r\n");Protocol_parse();}}}
针对具体逻辑,开启消息队列处理具体事项。以四条腿同时动作为例:
void PID_on_pid(unsigned char chn, float kp, float ki, float kd) {if(chn == 0) {Balance_Kp = kp;Balance_Ki = ki;Balance_Kd = kd;printf("Update Bp: %4.2f Bi: %4.2f Bd: %4.2f\n", kp, ki, kd);left = 0;right = 0;Motors_setSpeeds(0, 0);} else if (chn == 1) {Velocity_Kp = kp;Velocity_Ki = kp / 200.0f;printf("Update Vp: %4.2f Vi: %4.2f \n", Velocity_Kp, Velocity_Ki);Motors_setSpeeds(0, 0);}}
练习
- 上位机GUI实现下位机控制,协议为自定义
