学习目标

  1. 熟悉协议的定义
  2. 掌握协议生成
  3. 掌握协议解析
  4. 熟悉消息队列处理协议
  5. 熟悉消息队列处理业务

    学习内容

    协议的定义

    | | 帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | | —- | —- | —- | —- | —- | —- | —- | | 字节数 | 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等等类型,因此存在转换。

  1. def int2byte(v: int) -> bytes:
  2. return struct.pack('B', v)
  3. def float2bytes(v: float) -> bytes:
  4. return struct.pack('f', v)
  5. def bytes2float(v: bytes) -> float:
  6. return struct.unpack('f', v)[0]

协议构建

协议构建就是拼装出规定数据:

帧头 命令位 数据长度 数据位 校验位 帧尾
字节数 1 1 1 n 1 1
默认值 0x7a 待定 待定 待定 待定 0x7b
  1. def checksum(cmd: int, data: bytes) -> bytes:
  2. sum = cmd
  3. sum += len(data)
  4. for d in data:
  5. sum += d
  6. # 取高位
  7. sum &= 0xFF00
  8. sum >>= 8
  9. return int2byte(sum)
  10. def make_request(cmd: int, data:bytes) -> bytes:
  11. # 帧头 | 命令 | 数据长度 | 数据 | 校验码 | 帧尾
  12. # 1字节 | 1字节 | 1字节 | n字节 | 1字节 | 1字节
  13. # 命令: 请求类型的标识
  14. # 数据长度: 表示后面 数据 的字节个数
  15. # 校验码: 命令 + 数据长度 + 数据, 取高位
  16. result = b''
  17. result += int2byte(FRAME_HEAD)
  18. result += int2byte(cmd)
  19. result += int2byte(len(data))
  20. result += data
  21. result += checksum(cmd, data)
  22. result += int2byte(FRAME_END)
  23. return result

具体指令构建

具体指令构建是基于标准协议基础,处理具体逻辑的协议,为自定义:

帧头 命令位 数据长度 数据位 校验位 帧尾
idx P I D
字节数 1 1 1 1 4 4 4 1 1
默认值 0x7a 0x01 待定 待定 0x7b
  1. def make_action_request(idx: int, p: float, i: float, d: float):
  2. data = b''
  3. data += int2byte(idx)
  4. data += float2bytes(p)
  5. data += float2bytes(i)
  6. data += float2bytes(d)
  7. return make_request(0x01, data)
  8. def print_request(request: bytes):
  9. print()
  10. for r in request:
  11. print("{:02x} ".format(r), end='')
  12. print()

串口调试

通过串口发送数据到设备,查看协议的接收

  1. import serial
  2. from protocol import *
  3. from threading import Thread
  4. def do_recv():
  5. print('do recv')
  6. while True:
  7. buffer = ser.read(1)
  8. print(buffer)
  9. if __name__ == '__main__':
  10. ser = serial.Serial(baudrate=115200, port="COM15")
  11. Thread(target=do_recv).start()
  12. request = make_action_request(0, 10, 0.1, 0.3)
  13. print_request(request)
  14. ser.write(request)
  15. # ser.close()

协议解析

通过下位机解析数据

环形缓冲

074.png
环形缓冲,也被称为环形队列,是一种常用的数据结构,用于实现在固定大小的缓冲区中高效地插入和删除数据。
环形缓冲区的特点是它具有固定的容量,并且在达到最大容量时,新的数据可以覆盖最旧的数据。这使得环形缓冲区适用于需要保持最新数据并丢弃旧数据的场景,如实时数据流处理、音频/视频流缓冲等。
环形缓冲区通常使用一个固定大小的数组来实现。该数组有两个指针,一个指向缓冲区的读取位置,另一个指向缓冲区的写入位置。读取位置用于获取缓冲区中的数据,写入位置用于添加新的数据。
主要操作为往环形数组中添加和移除数据。
添加如图所示:
075.png
添加数据其实,就是拨动结束的指针,然后长度加一

移除如图所示:
76.png
添加数据其实,就是拨动开始的指针,然后长度减一

环形缓冲实现:

  1. #define RING_SIZE 512
  2. uint8_t ring[RING_SIZE];
  3. uint32_t ring_head = 0;
  4. uint32_t ring_end = 0;
  5. uint32_t ring_len = 0;
  6. void ring_put(uint8_t data) {
  7. if(ring_len > RING_SIZE) {
  8. // ring is full. error
  9. return;
  10. }
  11. ring[ring_end++] = data;
  12. ring_end %= RING_SIZE;
  13. ring_len++;
  14. }
  15. void ring_take(uint8_t *data) {
  16. if(ring_len == 0) {
  17. // ring is empty.
  18. return;
  19. }
  20. *data = ring[ring_head++];
  21. ring_head %= RING_SIZE;
  22. ring_len--;
  23. }

解析逻辑

基于协议进行读取校验协议正确性

  1. void do_parse() {
  2. // 帧头 | 命令 | 数据长度 | 数据 | 校验码 | 帧尾
  3. // 1字节 | 1字节 | 1字节 | n字节 | 1字节 | 1字节
  4. // 命令: 请求类型的标识
  5. // 数据长度: 表示后面 数据 的字节个数
  6. // 校验码: 命令 + 数据长度 + 数据, 取高位
  7. if(ring_len < 3) return;
  8. uint8_t tmp;
  9. uint8_t cmd;
  10. uint8_t len;
  11. // 判断帧头
  12. if(ring[ring_head] != FRAME_RX_HEAD) {
  13. // 不是,丢弃
  14. ring_take(&tmp);
  15. do_parse();
  16. return;
  17. }
  18. // 解析命令
  19. cmd = ring[(ring_head + 1) % RING_SIZE];
  20. // 数据长度
  21. len = ring[(ring_head + 2) % RING_SIZE];
  22. // 判断数据长度
  23. if(ring_len < len + 5) return;
  24. // 帧尾
  25. if(ring[(ring_head + 4 + len) % RING_SIZE] != FRAME_RX_END) {
  26. // 丢弃第一个
  27. // 不是,丢弃
  28. ring_take(&tmp);
  29. do_parse();
  30. return;
  31. }
  32. uint16_t sum = 0;
  33. sum += len;
  34. uint32_t i;
  35. for(i = 0;i < len; i++) {
  36. sum += ring[(ring_head + 3 + i) % RING_SIZE];
  37. }
  38. sum >>= 8;
  39. // 校验码
  40. if(sum != ring[(ring_head + 3 + len) % RING_SIZE]) {
  41. // 丢弃第一个
  42. // 不是,丢弃
  43. ring_take(&tmp);
  44. do_parse();
  45. return;
  46. }
  47. // 校验通过
  48. uint8_t* action = malloc((len + 5) * sizeof(uint8_t));
  49. for(i = 0;i < len + 5; i++) {
  50. ring_take(&tmp);
  51. action[i] = tmp;
  52. }
  53. Protocol_on_action(action, len + 5);
  54. free(action);
  55. }

对于具体功能性协议,需要解析出具体功能,例如控制舵机角度的协议:

  1. static void Protocol_on_action(uint8_t *data, uint32_t len) {
  2. uint8_t cmd = data[1];
  3. if(cmd == CMD_PID && len == 18) {
  4. uint8_t idx = data[3];
  5. uint8_t tmp[4];
  6. tmp[0] = data[4];
  7. tmp[1] = data[5];
  8. tmp[2] = data[6];
  9. tmp[3] = data[7];
  10. float p = bytesToFloat(tmp);
  11. tmp[0] = data[8];
  12. tmp[1] = data[9];
  13. tmp[2] = data[10];
  14. tmp[3] = data[11];
  15. float i = bytesToFloat(tmp);
  16. tmp[0] = data[12];
  17. tmp[1] = data[13];
  18. tmp[2] = data[14];
  19. tmp[3] = data[15];
  20. float d = bytesToFloat(tmp);
  21. Protocol_on_pid(idx, p, i, d);
  22. }
  23. }

处理数据转换

  1. typedef union {
  2. float f;
  3. unsigned char b[4];
  4. } FloatBytes;
  5. static void floatToBytes(float value, unsigned char *bytes) {
  6. FloatBytes fb;
  7. fb.f = value;
  8. bytes[0] = fb.b[0];
  9. bytes[1] = fb.b[1];
  10. bytes[2] = fb.b[2];
  11. bytes[3] = fb.b[3];
  12. }
  13. static float bytesToFloat(unsigned char *bytes) {
  14. FloatBytes fb;
  15. fb.b[0] = bytes[0];
  16. fb.b[1] = bytes[1];
  17. fb.b[2] = bytes[2];
  18. fb.b[3] = bytes[3];
  19. return fb.f;
  20. }

完整逻辑

  1. #include "protocol.h"
  2. #include <stdio.h>
  3. #define FRAME_RX_HEAD 0xFA
  4. #define FRAME_RX_END 0xFB
  5. #define CMD_LEG 0x01
  6. #define RING_SIZE 512
  7. uint8_t ring[RING_SIZE];
  8. uint32_t ring_head = 0;
  9. uint32_t ring_end = 0;
  10. uint32_t ring_len = 0;
  11. //////////////////////////// 数据转换:不同平台可能存在大小端问题 /////////
  12. ////////////////////// 当前是大端,修改顺序即可改为小端 ///////////////////
  13. typedef union {
  14. float f;
  15. unsigned char b[4];
  16. } FloatBytes;
  17. static void floatToBytes(float value, unsigned char *bytes) {
  18. FloatBytes fb;
  19. fb.f = value;
  20. bytes[0] = fb.b[0];
  21. bytes[1] = fb.b[1];
  22. bytes[2] = fb.b[2];
  23. bytes[3] = fb.b[3];
  24. }
  25. static float bytesToFloat(unsigned char *bytes) {
  26. FloatBytes fb;
  27. fb.b[0] = bytes[0];
  28. fb.b[1] = bytes[1];
  29. fb.b[2] = bytes[2];
  30. fb.b[3] = bytes[3];
  31. return fb.f;
  32. }
  33. ///////////////////////////////////////////////////////////////
  34. void ring_put(uint8_t data) {
  35. if(ring_len > RING_SIZE) {
  36. // ring is full. error
  37. return;
  38. }
  39. ring[ring_end++] = data;
  40. ring_end %= RING_SIZE;
  41. ring_len++;
  42. }
  43. void ring_take(uint8_t *data) {
  44. if(ring_len == 0) {
  45. // ring is empty.
  46. return;
  47. }
  48. *data = ring[ring_head++];
  49. ring_head %= RING_SIZE;
  50. ring_len--;
  51. }
  52. void Protocol_init() {
  53. }
  54. static void Protocol_on_action(uint8_t *data, uint32_t len) {
  55. uint8_t cmd = data[1];
  56. if(cmd == CMD_PID && len == 18) {
  57. uint8_t idx = data[3];
  58. uint8_t tmp[4];
  59. tmp[0] = data[4];
  60. tmp[1] = data[5];
  61. tmp[2] = data[6];
  62. tmp[3] = data[7];
  63. float p = bytesToFloat(tmp);
  64. tmp[0] = data[8];
  65. tmp[1] = data[9];
  66. tmp[2] = data[10];
  67. tmp[3] = data[11];
  68. float i = bytesToFloat(tmp);
  69. tmp[0] = data[12];
  70. tmp[1] = data[13];
  71. tmp[2] = data[14];
  72. tmp[3] = data[15];
  73. float d = bytesToFloat(tmp);
  74. Protocol_on_pid(idx, p, i, d);
  75. }
  76. }
  77. static void do_parse() {
  78. // 帧头 | 命令 | 数据长度 | 数据 | 校验码 | 帧尾
  79. // 1字节 | 1字节 | 1字节 | n字节 | 1字节 | 1字节
  80. // 命令: 请求类型的标识
  81. // 数据长度: 表示后面 数据 的字节个数
  82. // 校验码: 命令 + 数据长度 + 数据, 取高位
  83. if(ring_len < 3) return;
  84. uint8_t tmp;
  85. uint8_t cmd;
  86. uint8_t len;
  87. // 判断帧头
  88. if(ring[ring_head] != FRAME_RX_HEAD) {
  89. // 不是,丢弃
  90. ring_take(&tmp);
  91. do_parse();
  92. return;
  93. }
  94. // 解析命令
  95. cmd = ring[(ring_head + 1) % RING_SIZE];
  96. // 数据长度
  97. len = ring[(ring_head + 2) % RING_SIZE];
  98. // 判断数据长度
  99. if(ring_len < len + 5) return;
  100. // 帧尾
  101. if(ring[(ring_head + 4 + len) % RING_SIZE] != FRAME_RX_END) {
  102. // 丢弃第一个
  103. // 不是,丢弃
  104. ring_take(&tmp);
  105. do_parse();
  106. return;
  107. }
  108. uint16_t sum = 0;
  109. sum += len;
  110. uint32_t i;
  111. for(i = 0;i < len; i++) {
  112. sum += ring[(ring_head + 3 + i) % RING_SIZE];
  113. }
  114. sum >>= 8;
  115. // 校验码
  116. if(sum != ring[(ring_head + 3 + len) % RING_SIZE]) {
  117. // 丢弃第一个
  118. // 不是,丢弃
  119. ring_take(&tmp);
  120. do_parse();
  121. return;
  122. }
  123. // 校验通过
  124. uint8_t* action = malloc((len + 5) * sizeof(uint8_t));
  125. for(i = 0;i < len + 5; i++) {
  126. ring_take(&tmp);
  127. action[i] = tmp;
  128. }
  129. Protocol_on_action(action, len + 5);
  130. free(action);
  131. }
  132. void Protocol_parse(uint8_t *data, uint32_t len) {
  133. uint32_t i;
  134. for(i = 0;i < len;i++) {
  135. ring_put(data[i]);
  136. }
  137. do_parse();
  138. }
  1. #ifndef __PROTOCOL_H__
  2. #define __PROTOCOL_H__
  3. #include "gd32f4xx.h"
  4. #include "systick.h"
  5. void Protocol_init();
  6. void Protocol_parse(uint8_t *data, uint32_t len);
  7. extern void Protocol_on_pid(uint8_t idx, float p, float i, float d);
  8. #endif

消息调度

串口只负责接收数据,不做重活。

  1. void Usart0_recv(uint8_t *data, uint32_t len) {
  2. uint32_t i = 0, cnt = 32, size = len / cnt;
  3. bt_rx_data_t buffer;
  4. printf("BT recv \r\n");
  5. //portENTER_CRITICAL();
  6. for(i = 0; i < size; i++) {
  7. printf("BT A recv %d \r\n", i);
  8. memcpy(buffer.data, &data[i*cnt], cnt);
  9. buffer.len = cnt;
  10. xQueueSendFromISR(bt_rx_queue, &buffer, 0);
  11. }
  12. if(len % cnt != 0) {
  13. printf("BT B recv %d \r\n", i);
  14. memcpy(buffer.data, &data[i*cnt], len % cnt);
  15. buffer.len = len % cnt;
  16. xQueueSendFromISR(bt_rx_queue, &buffer, 0);
  17. //xQueueSend(bt_rx_queue, &buffer, 0);
  18. }
  19. //portEXIT_CRITICAL();
  20. printf("BT recv end\r\n");
  21. }

开启任务,负责专门的解析

  1. static void bt_rx_task() {
  2. bt_rx_queue = xQueueCreate(64, sizeof(bt_rx_data_t));
  3. bt_rx_data_t data;
  4. uint32_t i;
  5. while(1) {
  6. if(xQueueReceive(bt_rx_queue, &data, portMAX_DELAY)) {
  7. printf("BT parse \r\n");
  8. for(i = 0; i < data.len; i++) {
  9. Protocol_pull(data.data[i]);
  10. printf("%02X ", data.data[i]);
  11. }
  12. printf("\r\n");
  13. Protocol_parse();
  14. }
  15. }
  16. }

针对具体逻辑,开启消息队列处理具体事项。以四条腿同时动作为例:

  1. void PID_on_pid(unsigned char chn, float kp, float ki, float kd) {
  2. if(chn == 0) {
  3. Balance_Kp = kp;
  4. Balance_Ki = ki;
  5. Balance_Kd = kd;
  6. printf("Update Bp: %4.2f Bi: %4.2f Bd: %4.2f\n", kp, ki, kd);
  7. left = 0;
  8. right = 0;
  9. Motors_setSpeeds(0, 0);
  10. } else if (chn == 1) {
  11. Velocity_Kp = kp;
  12. Velocity_Ki = kp / 200.0f;
  13. printf("Update Vp: %4.2f Vi: %4.2f \n", Velocity_Kp, Velocity_Ki);
  14. Motors_setSpeeds(0, 0);
  15. }
  16. }

练习

  1. 上位机GUI实现下位机控制,协议为自定义