电机驱动开发学习11. 速度环控制

电机驱动开发学习11. 速度环控制

  • 一、速度环是什么
    • [1.1 开环 PWM 与闭环速度的区别](#1.1 开环 PWM 与闭环速度的区别)
    • [1.2 控制周期](#1.2 控制周期)
  • 二、实验简介
    • [2.1 本章目标](#2.1 本章目标)
    • [2.2 硬件说明](#2.2 硬件说明)
  • 三、程序设计
    • [3.1 目录与模块划分(规划)](#3.1 目录与模块划分(规划))
    • [3.2 速度反馈:霍尔 RPM](#3.2 速度反馈:霍尔 RPM)
    • [3.3 速度环 PID 数据流](#3.3 速度环 PID 数据流)
    • [3.4 PID 参数与限幅建议](#3.4 PID 参数与限幅建议)
    • [3.5 基本定时器 TIM6(速度环节拍)](#3.5 基本定时器 TIM6(速度环节拍))
    • [3.6 核心代码示例](#3.6 核心代码示例)
      • [3.6.1 单次速度环:`speed_pid_control` + `speed_pid_apply_output`](#3.6.1 单次速度环:speed_pid_control + speed_pid_apply_output)
      • [3.6.2 主循环入口:`speed_pid_poll`](#3.6.2 主循环入口:speed_pid_poll)
      • [3.6.3 占空比与换相](#3.6.3 占空比与换相)
      • [3.6.4 启动与停止](#3.6.4 启动与停止)
  • [四、FreeMASTER 与串口调参](#四、FreeMASTER 与串口调参)
    • [4.1 FreeMASTER 监视变量(推荐)](#4.1 FreeMASTER 监视变量(推荐))
    • [4.2 VOFA+ FireWater 波形(串口)](#4.2 VOFA+ FireWater 波形(串口))
    • [4.3 串口命令](#4.3 串口命令)
  • 五、主程序流程
    • [5.1 初始化](#5.1 初始化)
    • [5.2 主循环](#5.2 主循环)
  • [六、建议操作与 PID 整定](#六、建议操作与 PID 整定)
    • [6.1 推荐操作流程(总览)](#6.1 推荐操作流程(总览))
    • [6.2 开环摸底(必做)](#6.2 开环摸底(必做))
    • [6.3 启停与改目标](#6.3 启停与改目标)
    • [6.4 推荐 PID 起步参数(空载)](#6.4 推荐 PID 起步参数(空载))
    • [6.5 分步调参](#6.5 分步调参)
    • [6.6 积分饱和与两种「怪现象」](#6.6 积分饱和与两种「怪现象」)
    • [6.7 负载扰动(可选)](#6.7 负载扰动(可选))
  • 七、本章小结

一、速度环是什么

本章把前面章节的六步换相、位置式PID模块 接到电机上 :不再手动调 v 3000 这种占空比,而是设定 目标转速(RPM),由 PID 自动算出该输出多少 PWM,让实际转速跟随目标,即闭环控制:

这就是 单环速度闭环:被控量是转速,执行量是 PWM,反馈来自霍尔。

1.1 开环 PWM 与闭环速度的区别

方式 做法 问题
开环 固定占空比,负载变大转速掉、电压变转速漂 同一占空比在不同工况下 RPM 不一致
闭环 给定目标 RPM,PID 自动加减占空比 负载、电压变化时仍能稳住转速(在 PID 能力范围内)

如前章所述,本系列 BLDC 主线统一采用 位置式 PID (lesson8 / lesson9 已说明)。速度环里 PID 的输出就是 PWM 占空比的绝对值 ,直接 set_bldcm_speed(duty),与 lesson4 开环接口一致。

增量式 PID 更适合「在现有输出上微调」的场景(如步进脉冲频率),速度环用位置式更直观。

1.2 控制周期

PID 必须 固定周期 调用(lesson9 强调 dt 与定时器一致)。野火官方例程常用 TIM6 基本定时器,默认 50 ms 一次速度环;本仓库 lesson9 仿真用 10 ms 。速度环周期越短响应越快,但对 CPU 和测速噪声更敏感,一般 10~50 ms 均可,改周期后 Kp/Ki/Kd 需重新整定


二、实验简介

2.1 本章目标

  • 在 lesson7 霍尔测速基础上,增加 速度环 PID ,实现 目标 RPM → 自动 PWM
  • 复用 lesson9 的 bsp_pid.c/h(输出限幅、积分限幅、抗饱和)
  • 用 lesson10 的 FreeMASTER 监视 SP / ACT(RPM) / OUT(PWM) / P/I/D,在线改目标与 PID 参数
  • 串口输出 VOFA+ FireWater 7 通道波形(与 lesson9 相同协议)
  • 保留 lesson6/7 的 过流保护、堵转检测,闭环运行时仍生效
  • 串口保留基本启停与目标 RPM 命令(与 FreeMASTER 互补)

2.2 硬件说明

实验平台:野火骄阳 F407 + 无刷驱动板 + 配套 BLDC 电机(带霍尔)。

接线与 lesson4 / lesson7 相同:三相 U/V/W、霍尔 HU/HV/HW、驱动板 SD、5V/GND 等。推荐使用牛角排线接在开发板 「无刷电机驱动接口 2」


三、程序设计

3.1 目录与模块划分(规划)

在 lesson10 工程基础上扩展,核心新增/改动如下:

复制代码
User/
├── pid/
│   ├── bsp_pid.c/h          # 复用 lesson9,不改公式
│   └── bsp_pid_app.c/h      # 速度环应用:读 RPM、调 PWM、串口/FreeMASTER
├── vofa/
│   └── bsp_vofa.c/h         # VOFA+ FireWater 7 通道波形
├── tim/
│   └── bsp_motor_tim.c/h    # TIM8 PWM + TIM5 霍尔测速
├── bldcm_control/           # 启停、方向、set_bldcm_speed
├── freemaster/
│   └── bsp_fm_vars.h        # g_fm_target_rpm、g_fm_speed_loop_en 等
└── main.c                   # 初始化电机 + 速度环 + 过流/堵转保护

3.2 速度反馈:霍尔 RPM

lesson7 已在 TIM5 霍尔捕获中断 里更新转速,应用层只需读取:

c 复制代码
uint16_t rpm = motor_hall_get_rpm();   /* 无符号转速,单位 RPM */

内部换算(本仓库 bsp_motor_tim.h)要点:

  • TIM5 计数时钟 84 MHz ,预分频 128
  • 电机 2 对极 ,机械转一圈 12 次霍尔状态变化(6 步 × 2)
  • 两次跳变间隔 capture 个计数,则
    RPM = 60 × 84000000 / (6 × 2 × 128 × capture)

主循环中应周期性调用 motor_hall_speed_poll()超过约 200 ms 无霍尔跳变则 RPM 置 0(电机停转或堵转)。

3.3 速度环 PID 数据流

复制代码
定时中断(如每 50 ms)或 HAL_GetTick 节拍:
  1. actual_rpm = motor_hall_get_rpm()
  2. out = pid_update(&pid, target_rpm, (float)actual_rpm)
  3. 若 out < 0:取绝对值,direction = 反转;否则 direction = 正转
  4. duty = clamp(out, 0, MOTOR_PWM_MAX_PERIOD_COUNT)
  5. set_bldcm_direction(...); set_bldcm_speed(duty); 使能电机

与野火官方 bldcm_pid_control() 思路一致:PID 输出带符号表示「转矩方向 + 大小」,负值时反转并取绝对值作为占空比。

3.4 PID 参数与限幅建议

项目 建议初值 说明
目标 SP 500~1500 RPM 空载先试,勿一步给过高
out_min 0 PWM 不能为负
out_max MOTOR_PWM_MAX_PERIOD_COUNT(约 5500) 与 lesson4 一致
integral_max 视 Ki 调整,如 5000~15000 防积分过大
dt 0.05f(50 ms)或 0.01f(10 ms) 与速度环周期严格一致
Kp / Ki / Kd 需实机整定 可参考下文第六节

注意 :lesson9 仿真里 SP/ACT 是 0~100 的无量纲值;本章 SP 与 ACT 都是 RPM ,数量级变大,不能直接照搬 lesson9 的 Kp=1.5、Ki=0.35,必须重新整定。

3.5 基本定时器 TIM6(速度环节拍)

野火例程用 TIM6 产生固定周期中断,在中断里调用速度环,保证 dt 恒定。配置要点(与官方一致):

  • 时钟源 TIM6CLK = 84 MHz(APB1 定时器倍频)
  • 预分频 1680 → 计数频率 50 kHz
  • 自动重装载 ARR = 50×N − 1 → 周期 N ms(N=1~1000)

也可像 lesson10 一样在 pid_app_poll() 里用 HAL_GetTick()10 ms 节拍;两种方式二选一,不要中断和主循环各算一遍 PID

3.6 核心代码示例

位于 User/pid/bsp_pid_app.c

3.6.1 单次速度环:speed_pid_control + speed_pid_apply_output

c 复制代码
/**
 * speed_pid_control --- 读霍尔 RPM → 算 PID → 输出 PWM
 * 由 speed_pid_poll() 每 50ms 调用,或在 g_fm_speed_loop_en 0→1 时立即调用一次
 */
static void speed_pid_control(void)
{
    float cont_val;
    uint16_t actual_rpm;

    /* ① 反馈:TIM5 霍尔捕获中断里更新的 motor_rpm,经 EMA 滤波 */
    actual_rpm = motor_hall_get_rpm();
    g_fm_actual_rpm = (float)actual_rpm;
    g_fm_error = g_fm_target_rpm - g_fm_actual_rpm;   /* 供 FreeMASTER / VOFA+ 监视 */

    /* ② SP 低于 SPEED_PID_STOP_RPM(10):视为停转,不跑 PID,清积分 */
    if (g_fm_target_rpm < SPEED_PID_STOP_RPM)
    {
        set_bldcm_speed(0U);
        bldcm_apply_state();
        g_fm_pwm_out = 0.0f;
        pid_reset(&g_fm_pid);
        return;
    }

    /* ③ 位置式 PID:out 为带符号控制量,量纲 ≈ PWM 计数 (0~5500) */
    cont_val = pid_update(&g_fm_pid, g_fm_target_rpm, g_fm_actual_rpm);

    /* ④ 符号→方向、限幅、斜坡、写硬件 */
    speed_pid_apply_output(cont_val);
}

/**
 * speed_pid_apply_output --- 把 PID 浮点输出变成「方向 + 占空比」
 * @param cont_val  PID 输出;负值表示反转,绝对值为 PWM 大小
 */
static void speed_pid_apply_output(float cont_val)
{
    uint16_t duty;
    motor_dir_t dir;

    /* 负输出:反转;正输出:正转(与野火 bldcm_pid_control 思路一致) */
    if (cont_val < 0.0f)
    {
        cont_val = -cont_val;
        dir = MOTOR_REV;
    }
    else
    {
        dir = MOTOR_FWD;
    }

    duty = (uint16_t)cont_val;
    if (duty > MOTOR_PWM_MAX_PERIOD_COUNT)   /* 约 5500,与 lesson4 一致 */
    {
        duty = MOTOR_PWM_MAX_PERIOD_COUNT;
    }

    /* 斜坡限幅:每 50ms PWM 最多 ±250,抑制霍尔噪声导致 OUT 锯齿 */
    if (duty > s_pwm_slew_duty + SPEED_PID_PWM_MAX_STEP)
        duty = (uint16_t)(s_pwm_slew_duty + SPEED_PID_PWM_MAX_STEP);
    else if (duty + SPEED_PID_PWM_MAX_STEP < s_pwm_slew_duty)
        duty = (uint16_t)(s_pwm_slew_duty - SPEED_PID_PWM_MAX_STEP);
    s_pwm_slew_duty = duty;

    set_bldcm_direction(dir);
    set_bldcm_speed(duty);      /* 内部 set_pwm_pulse(),换相中断里真正输出 */
    bldcm_apply_state();        /* 使能 SD、hall_enable,电机转起来 */

    g_fm_pwm_out = (float)duty; /* FreeMASTER 必看:实际占空比 */
}

3.6.2 主循环入口:speed_pid_poll

本章用 HAL_GetTick() + 50ms 节拍,未用 TIM6 中断(与 3.5 二选一即可,勿重复算 PID):

c 复制代码
/**
 * main() while(1) 每圈调用一次
 * 顺序:deal_speed_pid_serial_data() → speed_pid_poll() → speed_pid_vofa_poll()
 */
void speed_pid_poll(void)
{
    uint32_t now = HAL_GetTick();
    static float s_prev_loop_en = 0.0f;
    int loop_on = speed_loop_is_on();   /* g_fm_speed_loop_en >= 0.5f */
    int prev_on = (s_prev_loop_en >= 0.5f) ? 1 : 0;

    /* --- 闭环开关边沿 --- */
    if (loop_on && !prev_on)            /* 0→1:用户串口 s 1 或 FreeMASTER 写 loop_en=1.0 */
    {
        pid_reset(&g_fm_pid);
        set_bldcm_direction(MOTOR_FWD);
        motor_stall_reset_timer();
        s_pwm_slew_duty = get_bldcm_speed();  /* 从当前 PWM 起坡,避免启环突跳 */
        speed_pid_control();                  /* 立刻算一次,不必等 50ms */
    }
    else if (!loop_on && prev_on)       /* 1→0:关环停电机 */
    {
        speed_pid_stop_motor_only();
    }
    s_prev_loop_en = g_fm_speed_loop_en;

    /* --- 监视量(开/闭环都刷新)--- */
    g_fm_actual_rpm = (float)motor_hall_get_rpm();
    g_fm_error = g_fm_target_rpm - g_fm_actual_rpm;
    g_fm_pwm_out = (float)get_bldcm_speed();  /* 开环 v/按键时也能看到真实 PWM */
    g_fm_vbus_v = get_vbus_val();

    speed_pid_apply_target_stop();      /* SP≈0 强制停,与 loop_en 无关 */

    if (!loop_on)
        return;                         /* 闭环关:只刷新监视量,不算 PID */

    if ((now - s_last_pid_ms) < SPEED_PID_LOOP_MS)  /* 50ms 未到 */
        return;

    s_last_pid_ms = now;
    fm_reset_on_param_change();         /* FreeMASTER 改 SP/Kp/Ki/Kd 时清积分 */
    speed_pid_control();
}

要点

条件 行为
只改 g_fm_target_rpm 不会自动启电机
g_fm_speed_loop_en = 1.0 才进入上面 0→1 分支,开始 PID
g_fm_speed_loop_en = 0 可串口 v [pwm] 开环摸底

3.6.3 占空比与换相

set_pwm_pulse() / set_bldcm_speed() 只保存 motor_bldcm_pulse;霍尔 TIM5 触发中断 HAL_TIM_TriggerCallbackbldcm_commutate() 才按六步表输出 PWM。

速度环每次改占空比后调用 bldcm_apply_state(),保证换相逻辑读到最新 pulse(与开环相同)。

3.6.4 启动与停止

操作 代码路径 说明
上电 speed_pid_init() 默认 SP=500、闭环关、PID Kp=2 Ki=0.5
启闭环 串口 s 1 或写 g_fm_speed_loop_en=1.0 建议先 t 300 低目标;大改 SP 前 r 清积分
停闭环 串口 s 0speed_pid_stop() 清 loop_en + 停电机
SP 停转 g_fm_target_rpm=0 speed_pid_apply_target_stop() 自动关 PWM
堵转/过流 speed_pid_fault_stop() 只停电机,不清 loop_en,便于 FreeMASTER 观察
开环摸底 s 0v 2500 PWM 0~5500,记录 ACT 与 PWM 关系

四、FreeMASTER 与串口调参

4.1 FreeMASTER 监视变量(推荐)

编译后将 .axf 加载到 FreeMASTER,Watch / Scope 使用 符号名(不要自起名 SP/OUT 以免绑错地址):

符号 类型 含义
g_fm_target_rpm float 目标转速 SP
g_fm_actual_rpm float 霍尔反馈 ACT
g_fm_error float SP − ACT
g_fm_pid.output float PID 总输出
g_fm_pid.p_term / i_term / d_term float P / I / D 分项
g_fm_pwm_out float 实际 PWM 占空比(最重要)
g_fm_vbus_v float 母线电压 (V),PF9 ADC,与 lesson5 一致
g_fm_speed_loop_en float 1.0=闭环开,0.0=关(必须 float 4 字节)
g_fm_fault_code float 0=正常,1=堵转,2=过流

启停闭环 :只改 g_fm_target_rpm 不会 自动启电机,须另写 g_fm_speed_loop_en = 1.0 。保护触发后 g_fm_fault_code 非 0,电机停转但 loop_en 可仍为 1(便于观察)。

在线修改 g_fm_target_rpmg_fm_pid.kp/ki/kd 时,程序会自动 pid_reset()(带浮点容差,避免 FreeMASTER 抖动误触发)。

Scope 建议同时看:g_fm_target_rpmg_fm_actual_rpmg_fm_pwm_outg_fm_vbus_v

4.2 VOFA+ FireWater 波形(串口)

本章串口波形为 VOFA+ FireWater

通道 含义
ch0 SP g_fm_target_rpm
ch1 ACT g_fm_actual_rpm
ch2 ERR SP − ACT
ch3 OUT PID 总输出
ch4/5/6 P/I/D 分项

每 50ms 一行 CSV,例如:400.00,363.00,37.00,2692.13,28.00,2664.13,0.00

VOFA+ 设置 :115200、协议 FireWater 、通道数 7 。命令回显带 [MOT] 前缀,波形为纯 CSV。

4.3 串口命令

波特率 115200,以回车结束;与 FireWater 共用 USART1。

命令 作用 示例
t [rpm] 设置目标转速 SP t 300
kp / ki / kd 修改 PID 系数 kp 8
s 1 / s 0 启动 / 停止速度环 s 1
pid 打印 SP、ACT、PWM、loop pid
r 复位 PID(清积分) r
? 帮助 ?
v [pwm] 闭环关时开环 PWM(0~5500) v 2500
`d [0 1]` 闭环关时改方向

闭环关s 0g_fm_speed_loop_en=0):按键 K1/K2 可调开环 PWM;改 SP 不驱动电机。

闭环开:PID 自动控制 PWM,按键无效。


五、主程序流程

5.1 初始化

  1. 时钟、LED、串口
  2. bldcm_init():TIM8 PWM、TIM5 霍尔
  3. ADC / 过流(lesson6)
  4. pid_init():设置速度环 Kp/Ki/Kd、dt、输出限幅
  5. 启动 TIM6 中断 记录 HAL_GetTick 基准
  6. 默认 不使能电机 ,等待 s 1 或 FreeMASTER 写启动标志

5.2 主循环

text 复制代码
while (1)
{
    deal_speed_pid_serial_data();
    deal_key_input();
    speed_pid_poll();          /* 50ms 速度环 PID */
    speed_pid_vofa_poll();     /* 50ms FireWater 波形 */
    motor_stall_poll();
    motor_hall_speed_poll();
    motor_overcurrent_poll();
}

速度环在 speed_pid_poll()(50 ms) 中执行;VOFA+ 波形在 speed_pid_vofa_poll() 中发送。


六、建议操作与 PID 整定

安全提示 :首次闭环请 空载、低目标 RPM(200~500) ,手不要靠近转子;振动大、过流或堵转时立即 s 0 或按复位。

6.1 推荐操作流程(总览)

text 复制代码
① 烧录 → ② 开环摸底(v) → ③ 清积分(r) → ④ 设参数(kp/ki) → ⑤ 设目标(t) → ⑥ 启闭环(s 1) → ⑦ 看波形调参
步骤 操作 目的
1 烧录 lesson11,FreeMASTER 加载 .axf 确认符号、变量类型
2 s 0 ,串口 v 1500 / v 2500 / v 3500 记录「PWM → RPM」关系
3 r 或写 g_fm_pid.integral = 0 清积分,避免旧状态
4 kp 4ki 1kd 0(起步值;过大易 OUT 锯齿振荡) 已加转速 EMA + PWM 斜坡限幅
5 t 300 或写 g_fm_target_rpm = 300 先低目标
6 s 1 或写 g_fm_speed_loop_en = 1.0 必须单独启闭环
7 FreeMASTER / VOFA+ 看 SP、ACT、g_fm_pwm_out 调 Kp/Ki

6.2 开环摸底(必做)

闭环前先用开环弄清本机 PWM--RPM 关系:

text 复制代码
s 0
v 1000
v 2000
v 3000

记下:

  • PWM=1500 → 约 180 RPM;
  • PMM=2000 → 约 240 RPM;
  • PWM=3000 → 约 350 RPM;

PID 稳态时 g_fm_pwm_out 应接近该开环 PWM

6.3 启停与改目标

FreeMASTER:

g_fm_target_rpm = 300

g_fm_speed_loop_en = 1.0

观察 g_fm_pwm_out

串口:

t 300s 1

停止: s 0

大改 SP 前先 r

6.4 推荐 PID 起步参数(空载)

参数 起步值 说明
Kp 5~15 lesson9 的 1.5 不能直接搬
Ki 1~5 不要用 0.1
Kd 0 有噪声再试 0.01~0.05
目标 SP 200~500 确认后再升高

转速上不去 (SP=300,ACT≈93,PWM≈800):Ki 太小、PWM 仅 ~15% 满量程 → rkp 8ki 2t 300s 1

6.5 分步调参

只加 P: rki 0kd 0kp 5(逐步加大)→ t 300s 1

加 I: ki 1 起,逐步加大。加 D(可选): kd 0.02

阶跃: 稳定后 t 500,观察 ACT 上升与超调。

6.6 积分饱和与两种「怪现象」

现象 SP vs ACT OUT / I 处理
积分饱和 ACT 已接近 SP I≈2000+ r 清积分;减小 Ki
增益过小 ACT 远低于 SP PWM 偏小且缓升 加大 Kp/Ki;开环摸底

重点看 g_fm_pwm_outg_fm_pid.p_term,勿绑错 FreeMASTER 符号。

6.7 负载扰动(可选)

空载稳定后轻触加阻力,观察 ACT 跌落再恢复。


实测只能上700RPM左右,离电机额定转速相差比较大,如果用FOC转速能再高一些,以后再测。

七、本章小结

  • 速度环 = 以 RPM 为反馈PWM 为输出 的单环 PID。
  • 启闭环g_fm_speed_loop_en=1.0s 1;只改 SP 不会自动启电机。
  • RPM 环 Kp/Ki 须重调 ;建议先开环摸底,再 kp 8 ki 2 量级起步。
  • FreeMASTER (SWD)+ VOFA+ FireWater(串口)+ 串口命令可并用。
  • 大改 SP 前 r 清积分;区分「积分饱和」与「增益过小」两种波形。

下一章 lesson12 位置环控制 将把目标从 RPM 换成 累计位置(霍尔圈数/计数) ,控制结构类似,但需注意 位置误差 wrap速度限制


*参考:野火《电机应用开发实战指南》「无刷电机速度环控制(BLDC)」;

代码地址:https://gitee.com/xundh/learn-motor-stm32