学习目标
- 熟悉协议的定义
- 掌握协议生成
- 掌握协议解析
- 熟悉消息队列处理协议
- 熟悉消息队列处理业务
学习内容
协议的定义
| | 帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | | —- | —- | —- | —- | —- | —- | —- | | 字节数 | 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 = cmd
sum += len(data)
for d in data:
sum += d
# 取高位
sum &= 0xFF00
sum >>= 8
return 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 += data
result += 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 serial
from protocol import *
from threading import Thread
def 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 512
uint8_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. error
return;
}
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 512
uint8_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. error
return;
}
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实现下位机控制,协议为自定义