电机驱动开发学习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 启动与停止)
- [3.6.1 单次速度环:`speed_pid_control` + `speed_pid_apply_output`](#3.6.1 单次速度环:
- [四、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_TriggerCallback 里 bldcm_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 0 → speed_pid_stop() |
清 loop_en + 停电机 |
| SP 停转 | 写 g_fm_target_rpm=0 |
speed_pid_apply_target_stop() 自动关 PWM |
| 堵转/过流 | speed_pid_fault_stop() |
只停电机,不清 loop_en,便于 FreeMASTER 观察 |
| 开环摸底 | s 0 后 v 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_rpm、g_fm_pid.kp/ki/kd 时,程序会自动 pid_reset()(带浮点容差,避免 FreeMASTER 抖动误触发)。
Scope 建议同时看:g_fm_target_rpm、g_fm_actual_rpm、g_fm_pwm_out、g_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 0 或 g_fm_speed_loop_en=0):按键 K1/K2 可调开环 PWM;改 SP 不驱动电机。
闭环开:PID 自动控制 PWM,按键无效。
五、主程序流程
5.1 初始化
- 时钟、LED、串口
bldcm_init():TIM8 PWM、TIM5 霍尔- ADC / 过流(lesson6)
pid_init():设置速度环 Kp/Ki/Kd、dt、输出限幅- 启动 TIM6 中断 或 记录
HAL_GetTick基准 - 默认 不使能电机 ,等待
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 4、ki 1、kd 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 300 → s 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% 满量程 → r → kp 8 → ki 2 → t 300 → s 1。
6.5 分步调参
只加 P: r → ki 0 → kd 0 → kp 5(逐步加大)→ t 300 → s 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_out 、g_fm_pid.p_term,勿绑错 FreeMASTER 符号。
6.7 负载扰动(可选)
空载稳定后轻触加阻力,观察 ACT 跌落再恢复。

实测只能上700RPM左右,离电机额定转速相差比较大,如果用FOC转速能再高一些,以后再测。
七、本章小结
- 速度环 = 以 RPM 为反馈 、PWM 为输出 的单环 PID。
- 启闭环 须
g_fm_speed_loop_en=1.0或s 1;只改 SP 不会自动启电机。 - RPM 环 Kp/Ki 须重调 ;建议先开环摸底,再
kp 8ki 2量级起步。 - FreeMASTER (SWD)+ VOFA+ FireWater(串口)+ 串口命令可并用。
- 大改 SP 前
r清积分;区分「积分饱和」与「增益过小」两种波形。
下一章 lesson12 位置环控制 将把目标从 RPM 换成 累计位置(霍尔圈数/计数) ,控制结构类似,但需注意 位置误差 wrap 与 速度限制。
*参考:野火《电机应用开发实战指南》「无刷电机速度环控制(BLDC)」;