VOFA+ 串口调试系统集成与扩展完整教程

138 阅读6分钟

VOFA+ 串口调试与调参系统集成教程

一、 核心原理

这个系统的核心逻辑是利用串口(UART)作为 PC 和单片机之间的桥梁:

  1. 下行(PC -> MCU): 使用自定义的字符串协议(如 pkp:10.5)。单片机通过串口中断接收字符,存入缓冲区,检测到换行符 \n 后在低优先级任务中解析,更新全局参数结构体。

  2. 上行(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.cvofa_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.cVOFA_SendData 函数中:

  • 目前的逻辑是:只发送 vofa_data.motor_id 指定的那一个电机的数据。

  • 如果你想同时看所有电机的数据,你需要极大地扩展 printf 的内容。

  • 如果你只想看当前选中的电机(当前逻辑),则不需要修改 VOFA_SendData,因为代码里用的是 vofa_data.actual_pos[id]id 改变时输出的数据源会自动改变。

场景 2:增加GM6020(电压控制)

代码id与GM6020电机id映射 image.png

image.png

场景 3:增加新的控制参数 (例如增加“最大速度限制” max_vel)

步骤 1:修改结构体定义

Src/usr_vofa.cVofaControlData_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

image.png

步骤 7:在 MotorTask 中使用

app_freertos.c 中调用:

image.png

float limit = VOFA_GetMaxVel(k);

三、 VOFA+ 上位机配置指南

要让这一套系统工作,VOFA+ 软件端需要配合设置。

1. 控件设置 (发送指令)

你需要向单片机发送参数。在 VOFA+ 中添加 “命令组 (Command Group)” 控件。

原理:

VOFA+ 发送字符串 -> 单片机 sscanf 解析。代码使用了“先选ID,再发参数”的逻辑。

注意:必须勾选 VOFA+ 发送设置中的 \n (换行符),或者手动在字符串末尾加 \n,因为你的代码 if (received == '\n') 依赖它来判断指令结束。


四、 进阶:参数数组化控制原理

你现在的代码已经实现了**“参数数组化”**。

原理:

  1. VofaControlData_t 结构体中,所有参数都是数组 float pos_kp[MOTOR_COUNT]

  2. 有一个变量 uint8_t motor_id 记录当前“正在通过串口修改哪个电机”。

  3. 写参数时: 当你发送 pkp:20 时,代码执行 vofa_data.pos_kp[vofa_data.motor_id] = 20;

  4. 读参数时: 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.cInc/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 (任务集成)

你需要两个任务来配合:

  1. 打印/解析任务 (PrintTask):低优先级,负责处理收到的指令字符串,并定时发送波形数据。

  2. 电机控制任务 (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(...);
    }
}