前沿
还是和之前一样的方式:给流程图(我的图是用draw画的,用语言描述然后发给ai,让ai生成draw文件,然后导入draw,所以图可能看起来有ai味,请见谅),对流程图里的术语或者知识点进行讲解,再给核心代码,最后我会展示一下如何让ai输出期望的代码(我对工程语言的描述还很拉,我还在一点一点学习,希望大佬们多指导)
希望对同为小白的我们有用,也希望各位大佬多多指点
流程图

术语或知识点
__packed(字节对齐/打包)
含义:取消字节对齐填充
只要涉及到“直接把结构体通过串口发出去”或者“把接收到的字节流直接映射到结构体”,就必须加 __packed
数据发送
为什么数据发送要用DMA?
- DMA数据的搬运工作完全由硬件完成,无需占用CPU的参与,这样CPU 就可以直接去跑 PID 算法
为什么数据发送要检查串口是否忙?
- 这是非阻塞重入检查,如果在串口忙时直接调用数据发送函数HAL 库内部会直接返回一个忙状态,虽然不会打断旧数据,但是会直接拒绝发送新数据
数据接收
对于数据接收为什么要使用DMA + 空闲中断 这种组合
- DMA:数据由硬件搬运,无需 CPU 参与
- 空闲中断:只有在 串口总线在一个字节的时间内没有收到新数据时才会触发中断,也就是当完整数据发完,会触发中断,这样可以实现自动分包,也就是帧结束检测
为什么数据接收的中断服务会有把数据先从接收区搬运到解析区的操作?
- 因为DMA 是一个“自顾自”搬运的机器,如果你直接在 Rx_buffer 里解析,万一你解析到一半,对方又发来一条新指令,DMA 会毫不留情地把你正在读的数据冲掉
- 所以这一步计算在数据给冲掉之前给数据拍一个快照
救命稻草
- 外设(蓝牙)发送数据的速度快于单片机初始化接收代码的速度,会导致上电瞬间数据溢出,发生ORE 错误
- HAL 库在默认情况下,遇到 ORE 错误会调用 HAL_UART_ErrorCallback。如果用户没定义这个回调(Weak定义为空),HAL 库通常选择**“躺平”**(不清除错误,也不重启接收),导致串口假死
- 所以要在HAL_UART_ErrorCallback回调中中重写HAL_UART_ErrorCallback,一旦检测到串口错误,立即清除错误并重启 DMA 接收
JustFloat协议
这是我再VOFA+中截图出来的,这个协议虽然复杂(FireWater协议只需pringf),不过比起printf来说消耗很小,所以选择用这个

核心代码
#define RX_BUFFER_SIZE 128 // 接收缓冲区的长度
typedef struct __packed // __packed 告诉编译器:别乱动,字节给我挤紧点
{
// — 数据段 —
float Reality_angle; // 现实角度
float expect_angle; // 期望角度
float PWM_output_value; // PWM输出值
float Encoder_Velocity; // 当前速度
float Filtered_Velocity; // 经过滤波后的速度
// — 帧尾段 —
uint32_t tail; // 必须放在最后,用于存储 0x7F800000
}VofaData;
VofaData g_vofa_frame; // 公告板
uint8_t Rx_buffer[RX_BUFFER_SIZE]; // 接收缓冲区
uint8_t Process_buffer[RX_BUFFER_SIZE + 1]; // 解析缓冲区
volatile uint8_t IsParse = 0; // 解析标识位
// 初始化函数
void VOFA_Init(void)
{
/*=================== 上行发送初始化 ===================*/
// 数据段清零
g_vofa_frame.expect_angle = 0;
g_vofa_frame.Reality_angle = 0;
g_vofa_frame.PWM_output_value = 0;
// 帧尾段赋予 0x7F800000
g_vofa_frame.tail = 0x7F800000;
/*=================== 下行接收初始化 ===================*/
HAL_UARTEx_ReceiveToIdle_DMA(&huart3, Rx_buffer, RX_BUFFER_SIZE); // 因为作为STM32方,我并不知道数据长度
}
/**
* @brief 数据上发
*/
void VOFA_SendDta(void)
{
// 0.保证后缀
g_vofa_frame.tail = 0x7F800000;
// 1. 安全检查
if (huart3.gState != HAL_UART_STATE_READY)
return;
// 2.数据搬运
HAL_UART_Transmit_DMA(&huart3, (uint8_t *)&g_vofa_frame, sizeof(g_vofa_frame));
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART3)
{
// 记忆上一次的位置
static uint16_t old_pos = 0;
// 确定当前位置
uint16_t current_pos = Size;
// 获取DMA搬运的数据长度
uint16_t len = 0;
// 获取数据
// 数据没溢出
if (old_pos < current_pos)
{
len = current_pos – old_pos; // 获取DMA搬运长度
memcpy(Process_buffer, Rx_buffer + old_pos, len); // 复制到解析区
}
else
{
uint16_t tail_len = RX_BUFFER_SIZE – old_pos;
memcpy(Process_buffer, &Rx_buffer[old_pos], tail_len);
uint16_t head_len = current_pos;
memcpy(&Process_buffer[tail_len], &Rx_buffer[0], head_len);
len = tail_len + head_len;
}
// 解析
if (len > 0)
{
Process_buffer[len] = '\\0';
IsParse = 1;
}
// 更新上一次的位置
old_pos = current_pos;
}
}
/*
U:直立环
S:速度环
T:转向环
比如:UKp15\\n,说明直立环的p项改成15,\\n表示数据完毕
*/
// 数据解析
void VOFA_Parse_Command(char *data)
{
// 遍历字符串,只要遇到换行符 (\\r 或 \\n),就强制截断
for (int i = 0; data[i] != '\\0'; i++)
{
if (data[i] == '\\r' || data[i] == '\\n')
{
data[i] = '\\0'; // 替换为结束符
break; // 只要处理了第一个换行,后面就不用管了
}
}
float val = 0.0f;
// 1. 判断是哪个环 (根据首字母)
switch (data[0])
{
/* ================= 直立环 (Upright) ================= */
case 'U':
// 跳过 'U',检查接下来的两个字符
if (strncmp(data + 1, "Kp", 2) == 0) // 指令: UKp…
{
val = atof(data + 3); // 跳过 "UKp" (3个字符),转换后面的数字
UprightRing.Kp = val;
}
else if (strncmp(data + 1, "Kd", 2) == 0) // 指令: UKd…
{
val = atof(data + 3);
UprightRing.Kd = val;
}
break;
/* ================= 速度环 (Speed) ================= */
case 'S':
if (strncmp(data + 1, "Kp", 2) == 0) // 指令: SKp…
{
val = atof(data + 3);
SpeedRing.Kp = val;
}
else if (strncmp(data + 1, "Ki", 2) == 0) // 指令: SKi…
{
val = atof(data + 3);
SpeedRing.Ki = val;
}
break;
/* ================= 转向环 (Turn) ================= */
case 'T':
if (strncmp(data + 1, "Kp", 2) == 0) // 指令: TKp…
{
val = atof(data + 3);
TurnRing.Kp = val;
}
break;
/* ================= 目标速度 (target_speed) ================= */
case 'W':
if (strncmp(data + 1, "speed", 5) == 0)
{
val = atof(data + 6);
target_speed = (int)val;
}
break;
/* ================= 转向指令 (turn_cmd) ================= */
case 'V':
if (strncmp(data + 1, "turn", 4) == 0)
{
val = atof(data + 5);
turn_cmd = (int)val;
}
break;
}
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
// 判断是否是我们用的串口3
if (huart->Instance == USART3)
{
// 这里的逻辑非常重要:
// 1. 发生错误时,HAL库通常会关掉DMA
// 2. 我们需要清除错误标志(HAL库底层已处理),并重新启动DMA接收
// 重新开启接收(这行代码是救命稻草)
HAL_UARTEx_ReceiveToIdle_DMA(&huart3, Rx_buffer, RX_BUFFER_SIZE);
}
}
ok,到重轴戏了
ai输出期望代码

#include "vofa.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
static UART_HandleTypeDef *vofa_uart;
// ===== 发送帧 =====
static VOFA_TxFrame_t tx_frame;
// ===== 接收缓存 =====
static uint8_t rx_dma_buf[VOFA_RX_BUF_SIZE];
static uint8_t rx_parse_buf[VOFA_PARSE_BUF_SIZE];
static volatile uint8_t parse_flag = 0;
static volatile uint16_t rx_len = 0;
// ================= 初始化 =================
void VOFA_Init(UART_HandleTypeDef *huart)
{
vofa_uart = huart;
// 初始化发送帧尾
tx_frame.tail[0] = JUSTFLOAT_TAIL_0;
tx_frame.tail[1] = JUSTFLOAT_TAIL_1;
tx_frame.tail[2] = JUSTFLOAT_TAIL_2;
tx_frame.tail[3] = JUSTFLOAT_TAIL_3;
// 启动 DMA 循环接收
HAL_UART_Receive_DMA(vofa_uart, rx_dma_buf, VOFA_RX_BUF_SIZE);
// 打开空闲中断
__HAL_UART_ENABLE_IT(vofa_uart, UART_IT_IDLE);
}
// ================= DMA 单次发送 =================
void VOFA_SendFrame(float real_angle, float target_angle, float pwm)
{
tx_frame.real_angle = real_angle;
tx_frame.target_angle = target_angle;
tx_frame.pwm_out = pwm;
// 启动 DMA 单次发送
HAL_UART_Transmit_DMA(vofa_uart, (uint8_t*)&tx_frame, sizeof(tx_frame));
// ===== 发送时序安全约束 =====
// 等待 DMA 发送完成
while (HAL_UART_GetState(vofa_uart) != HAL_UART_STATE_READY)
{
// 防止死锁:检测 UART 错误
if (__HAL_UART_GET_FLAG(vofa_uart, UART_FLAG_ORE))
{
VOFA_UART_ErrorCallback(vofa_uart);
break;
}
}
}
// ================= 空闲中断处理 =================
void VOFA_UART_IDLE_IRQHandler(UART_HandleTypeDef *huart)
{
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE))
{
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 计算本次接收长度
uint16_t dma_cnt = __HAL_DMA_GET_COUNTER(huart->hdmarx);
rx_len = VOFA_RX_BUF_SIZE – dma_cnt;
// 停止 DMA
HAL_UART_DMAStop(huart);
// ===== 缓存隔离 =====
if (rx_len < VOFA_PARSE_BUF_SIZE)
{
memcpy(rx_parse_buf, rx_dma_buf, rx_len);
rx_parse_buf[rx_len] = 0; // 字符串结束符
parse_flag = 1;
}
// 重启 DMA 接收
HAL_UART_Receive_DMA(huart, rx_dma_buf, VOFA_RX_BUF_SIZE);
}
}
// ================= 指令解析 =================
void VOFA_ParseTask(void)
{
if (!parse_flag) return;
parse_flag = 0;
char *ptr = (char*)rx_parse_buf;
float val;
// 手动截断解析(逐条匹配)
if (sscanf(ptr, "UKp%f", &val) == 1) UprightRing.Kp = val;
else if (sscanf(ptr, "UKd%f", &val) == 1) UprightRing.Kd = val;
else if (sscanf(ptr, "SKp%f", &val) == 1) SpeedRing.Kp = val;
else if (sscanf(ptr, "SKi%f", &val) == 1) SpeedRing.Ki = val;
else if (sscanf(ptr, "TKp%f", &val) == 1) TurnRing.Kp = val;
else if (sscanf(ptr, "Wspeed%f", &val) == 1) target_speed = (int)val;
else if (sscanf(ptr, "Vtyen%f", &val) == 1) turn_cmd = (int)val;
// 清空解析缓存
memset(rx_parse_buf, 0, VOFA_PARSE_BUF_SIZE);
}
// ================= UART 错误回调 =================
void VOFA_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_ORE))
{
__HAL_UART_CLEAR_OREFLAG(huart);
}
// 重启 DMA 接收
HAL_UART_Receive_DMA(huart, rx_dma_buf, VOFA_RX_BUF_SIZE);
}
会发现,md,ai写得比我还好





