VOFA+ 串口调试与调参系统集成教程
一、 核心原理
这个系统的核心逻辑是利用串口(UART)作为 PC 和单片机之间的桥梁:
-
下行(PC -> MCU): 使用自定义的字符串协议(如
pkp:10.5)。单片机通过串口中断接收字符,存入缓冲区,检测到换行符\n后在低优先级任务中解析,更新全局参数结构体。 -
上行(MCU -> PC): 使用 VOFA+ 的 JustFloat 协议。单片机利用重定向的
printf,将关键数据(目标值、实际值、PID参数等)以逗号分隔的格式发送,VOFA+ 软件自动解析并绘图。
二、 如何扩展系统(增加电机ID、增加参数)
场景 1:增加电机数量 (例如从 3 个增加到 4 个)
步骤 1:修改宏定义
在 Src/usr_vofa.c 中:
C
#define MOTOR_COUNT 4 // <--- 修改这里
步骤 2:修改数据结构初始值
在 Src/usr_vofa.c 的 vofa_data 初始化部分,为新增的电机添加默认值。注意数组长度必须匹配 MOTOR_COUNT。
C
static VofaControlData_t vofa_data = {
// ...
.enabled = {false, false, false, false}, // 增加一个
.pos_kp = {100.0f, 100.0f, 60.0f, 100.0f}, // 增加第4个电机的默认参数
// ... 其他数组同理都要增加一个元素
};
步骤 3:修改发送格式 (VOFA_SendData)
这是最容易遗漏的一步。如果你想在 VOFA 软件里看到第 4 个电机的波形,必须修改 printf。
在 Src/usr_vofa.c 的 VOFA_SendData 函数中:
-
目前的逻辑是:只发送
vofa_data.motor_id指定的那一个电机的数据。 -
如果你想同时看所有电机的数据,你需要极大地扩展
printf的内容。 -
如果你只想看当前选中的电机(当前逻辑),则不需要修改
VOFA_SendData,因为代码里用的是vofa_data.actual_pos[id],id改变时输出的数据源会自动改变。
场景 2:增加GM6020(电压控制)
代码id与GM6020电机id映射
场景 3:增加新的控制参数 (例如增加“最大速度限制” max_vel)
步骤 1:修改结构体定义
在 Src/usr_vofa.c 的 VofaControlData_t 结构体中添加数组:
C
typedef struct {
// ... 原有参数
float max_vel[MOTOR_COUNT]; // <--- 新增:每个电机独立的最大速度
} VofaControlData_t;
步骤 2:初始化结构体
在 vofa_data 实例中添加默认值:
C
static VofaControlData_t vofa_data = {
// ...
.max_vel = {500.0f, 500.0f, 500.0f}, // <--- 初始化
};
步骤 3:添加指令解析
在 VOFA_ProcessCommands 函数中,添加 sscanf 解析逻辑:
C
// 这里的 cmd 字符串匹配协议完全由你自己定,例如 "mvel:500"
else if (sscanf(cmd, "mvel:%f", &v) == 1) {
vofa_data.max_vel[id] = v; // id 是当前通过 Motor_id:x 选中的电机
}
步骤 4:添加获取接口
在 Src/usr_vofa.c 添加 Getter 函数,并在 Inc/usr_vofa.h 声明:
/* usr_vofa.c */
float VOFA_GetMaxVel(uint8_t id) {
if (id >= MOTOR_COUNT) return 0.0f;
return vofa_data.max_vel[id];
}
/* usr_vofa.h */
float VOFA_GetMaxVel(uint8_t id);
步骤6:添加发送接口
在 Src/usr_vofa.c
VOFA_SendData
步骤 7:在 MotorTask 中使用
在 app_freertos.c 中调用:
float limit = VOFA_GetMaxVel(k);
三、 VOFA+ 上位机配置指南
要让这一套系统工作,VOFA+ 软件端需要配合设置。
1. 控件设置 (发送指令)
你需要向单片机发送参数。在 VOFA+ 中添加 “命令组 (Command Group)” 控件。
原理:
VOFA+ 发送字符串 -> 单片机 sscanf 解析。代码使用了“先选ID,再发参数”的逻辑。
注意:必须勾选 VOFA+ 发送设置中的 \n (换行符),或者手动在字符串末尾加 \n,因为你的代码 if (received == '\n') 依赖它来判断指令结束。
四、 进阶:参数数组化控制原理
你现在的代码已经实现了**“参数数组化”**。
原理:
-
VofaControlData_t结构体中,所有参数都是数组float pos_kp[MOTOR_COUNT]。 -
有一个变量
uint8_t motor_id记录当前“正在通过串口修改哪个电机”。 -
写参数时: 当你发送
pkp:20时,代码执行vofa_data.pos_kp[vofa_data.motor_id] = 20;。 -
读参数时:
MotorTask中的循环for (int k = 0; k < MOTOR_COUNT; k++)会分别读取vofa_data.pos_kp[k]。
优点:
-
节省指令:不需要为每个电机定义
pkp1,pkp2指令,只需切换Motor_id上下文。 -
扩展性强:增加电机只需增加
MOTOR_COUNT宏,不用改指令解析逻辑。
五、 现有系统的集成步骤
如果你要在一个新工程中复刻这个功能,请按以下步骤操作:
1. CubeMX 配置
-
UART: 开启一个串口(例如 USART1),波特率建议
115200或更高(代码中是115200)。 -
NVIC: 开启该串口的全局中断(
USART1 global interrupt)。 -
DMA (可选): 目前你的代码使用的是中断接收 (
HAL_UART_Receive_IT),暂不需要 DMA。
2. 添加驱动文件
将 Src/usr_vofa.c 和 Inc/usr_vofa.h 复制到工程对应目录。
3. 代码挂载点
A. Src/main.c (串口重定向与初始化)
你需要重写 __io_putchar 以支持 printf,并在初始化部分启动 VOFA。
C
/* 在 main.c 头部包含头文件 */
#include "usr_vofa.h"
#include "stdio.h"
/* --- 1. 串口重定向 (放在 USER CODE BEGIN 4 区域) --- */
int __io_putchar(int ch) {
// 这里的 huart1 要对应你实际连接 VOFA 的串口
HAL_UART_Transmit(&huart1, (uint8_t *) &ch, 1, 1000);
return ch;
}
/* --- 2. 串口中断回调 (放在 USER CODE BEGIN 4 区域) --- */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 将接收到的字节交给 VOFA 处理
VOFA_UART_RxCallback(huart);
}
/* --- 3. 初始化 (放在 main 函数 USER CODE BEGIN 2) --- */
VOFA_Init(&huart1); // 开启接收中断
B. Src/stm32g4xx_it.c (确保中断被调用)
CubeMX 生成的代码通常会自动调用 HAL_UART_IRQHandler,只需确认该函数存在即可。你代码中已有。
C. Src/app_freertos.c (任务集成)
你需要两个任务来配合:
-
打印/解析任务 (
PrintTask):低优先级,负责处理收到的指令字符串,并定时发送波形数据。 -
电机控制任务 (
MotorTask):高优先级,负责从 VOFA 模块读取参数控制电机,并将反馈值写入 VOFA 模块。
C
/* --- 1. PrintTask (低优先级) --- */
void StartPrintTask(void *argument)
{
for(;;)
{
// 解析上位机发来的指令 (如 pkp:10.0)
VOFA_ProcessCommands();
// 发送波形数据给上位机
VOFA_SendData();
// 发送频率不宜过高,以免占用太多 CPU 或串口带宽
osDelay(20);
}
}
/* --- 2. MotorTask (高优先级) --- */
void StartMotorTask(void *argument)
{
for(;;)
{
// ... 控制循环开始 ...
// [读取参数]:从 VOFA 模块获取最新的 PID 参数
// 你的代码使用了数组 p_kp[k] 来缓存,只有变化时才更新 PID 对象
for (int k = 0; k < MOTOR_COUNT; k++) {
VOFA_GetPosParams(k, &new_pkp, ...);
// 更新逻辑...
}
// ... 执行 PID 计算 ...
// [写入反馈]:将电机实际状态写入 VOFA 模块,供 PrintTask 发送
for (int k = 0; k < MOTOR_COUNT; k++) {
VOFA_SetActualData(k, theta[k], rpm[k]);
VOFA_SetTicks(k, ticks[k]);
}
vTaskDelayUntil(...);
}
}