直流无刷电机双闭环串级控制:位置环与速度环的PID实现与调试
简介:这是一份面向STM32F4开发者与无刷电机控制学习者的HAL库工程源码,实现位置环与速度环串联的双闭环控制,并采用位置式PID算法。资源既支持按键现场调试,也可通过上位机串口在线修改PID参数与启停控制,便于观察电机响应并完成闭环调参。代码中引脚连接均以宏定义形式给出,可灵活适配个人硬件接线,工程结构清晰,适合有一定单片机基础、正在研究电机控制算法的读者直接移植或二次开发。
压缩包共550个文件,约20.13MB,以.h与.c源码为主,同时包含编译生成的.o、.d、.axf、.hex等文件,以及工程配置文件,可帮助理解MDK下HAL库工程的完整构建过程。当前已有1213人学习下载。
值得注意的是,资料中提到上位机设置目标值未做幅值限制时可能出现积分饱和;电机未停止时重新启动可能引起捕获脉冲异常,这些细节能帮助使用者避开常见的调试陷阱。整体而言,这是一份兼具代码实例与调参经验的双闭环电机控制参考工程。 最近做的一个设备改造项目遇到一个挺典型的控制需求:用直流无刷电机实现高精度定位,要求停止位置波动小、响应还不能太肉。以前我总觉得位置控制就是把位置误差塞进PID直接给PWM,真到现场调试才发现这套思路根本撑不住——空载能转,一带负载就开始抖或者爬行。后来把控制架构改成位置环和速度环双闭环串级,PID全部采用位置式,下位机基于STM32-F4的HAL库实现,问题才算彻底解决。这篇就把这套双闭环串级控制的完整思路、HAL库工程实现细节和调试过程中踩过的坑整理出来,给正在折腾直流无刷电机的朋友做个参考。如果你手头也是带编码器的无刷电机,想从“能转”走到“定位准”,这篇文章应该能帮到你;当然,如果只是想让电机快速转起来看个效果,那直接跑官方例程就好,没必要上双闭环。
1. 双闭环串级控制到底在串什么
1.1 为什么位置环不能直接给PWM
很多刚接触电机控制的开发者,包括我一开始,惯性思维都是:读编码器得到位置误差,误差直接换算成PWM占空比,不就是闭环定位吗?实践下来这套思路在空载、低速、摩擦小的场合勉强能动,但一旦负载稍变或速度要求上来,就会出现一个很尴尬的现象:位置环P给大了,电机嗡嗡震;P给小了,又慢吞吞还带爬行。原因是PWM占空比控制的是电压,电压和最终位置之间隔了好几层:电压产生电流,电流产生力矩,力矩克服负载改变速度,速度积分才是位置。中间任何一环的非线性——摩擦、齿槽转矩、负载扰动——都会让高增益的位置环失稳。
所以位置环直接输出的合适量不是占空比,而是期望速度。把“位置误差”翻译成“该跑多快”,再由速度环去处理“电压给多少”,这才是双闭环串级的核心逻辑。整个控制链可以写成一串:位置误差 → 位置环PID → 速度指令 → 速度环PID → PWM占空比 → 电机 → 编码器 → 位置。这和你开车入库很像:位置环管的是“离停车位还有多远、该以多快的车速接近”,而真正的油门和刹车操作,永远是在这个速度目标下细调出来的。你不会因为离车位差了半米就直接把油门踩到底。
1.2 内外环响应速度必须拉开差距
为什么串起来能稳?因为内外环的工作带宽是分开的。速度环负责快速响应,把外环给的指令尽快跟踪住,所以它的采样周期短、动作快;位置环相对慢,只负责消除最终的位置误差,采样周期可以放长。我这里外环周期5ms,内环周期1ms,就是一个很典型的比例。这样做最直接的收益是:内环已经收敛了,外环才更新一次指令,两个环不会互相追逐、越追越乱。
这个“带宽分离”原则很多人会忽略。我见过有同事图省事,把位置环和速度环都放在同一个1ms中断里跑,结果位置环的微分项把编码器量化噪声放大了,速度环一直接到抖动的位置指令,两个环耦合在一起振荡。定位这种感觉,就像让两个人在同一频道同时说话,谁也不听谁的。位置环周期取速度环的2到5倍是实践经验里比较稳妥的范围,我后来沿用了5ms/1ms这个搭配,动态和稳定性都很满意。
2. 位置式PID在双闭环里的取舍与实现
2.1 位置式与增量式的关键差异
这个项目名里点名了位置式PID,我在设计时其实是做过对比的。位置式PID输出的是绝对值,公式是:u(k) = Kp·e(k) + Ki·Σe(j) + Kd·[e(k) - e(k-1)]。增量式PID输出的则是本次相比上次的变化量,公式是:Δu(k) = Kp·[e(k)-e(k-1)] + Ki·e(k) + Kd·[e(k)-2e(k-1)+e(k-2)]。它们没有绝对的好坏,关键看用在哪。
这个项目里位置环的输出是速度指令,而速度指令天然是一个绝对目标值,不是“在当前速度基础上再加一点”。如果位置环用增量式,速度设定值在系统里就没有一个明确的绝对值,限幅、断电重启、平滑启停都会变得很别扭。速度环输出的是占空比,本质上也可以是0到100%的绝对值,所以同样可以用位置式。两个环统一用位置式,代码逻辑一致,调试时心智负担小很多。增量式的优势主要在抗积分饱和和掉电恢复,但这些都不是这个场景的硬需求。
| 对比维度 | 位置式PID | 增量式PID |
|---|---|---|
| 输出语义 | 绝对指令值,含义清晰 | 增量变化值,需外部累加 |
| 积分饱和 | 需要额外做积分限幅 | 天然抗饱和 |
| 断电重启 | 状态量需重新初始化 | 输出从0开始缓慢恢复 |
| 适合环节 | 输出就是给定量,如位置环输出速度 | 执行器本身需要增量调节的场合 |
2.2 位置式PID实现中两个必须处理的细节
位置式PID最容易翻车的地方是积分项。积分项会一直累加,当输出已经被限幅挡住时,积分还在继续涨,一旦误差方向反转,控制器要花很长时间“退饱和”,实际表现就是电机过了目标位置还在继续冲。解决方法是积分限幅和输出限幅分开处理:输出限幅在PID计算完成之后做,积分限幅在积分累加时就做。只做输出限幅,积分累计值可能已经涨到好几千,输出却被一直压在100,误差方向一变,要退很久才能真正改变输出。
第二个细节是微分项的取值方式。经典位置式用误差变化率作为微分项,但位置环的目标值经常是阶跃给定的,误差突然跳一下,微分项会产生一个很大的尖峰脉冲,PWM占空比就被冲一下。我的做法是D项只作用于反馈值变化,也就是用-kd·(current_actual - old_actual)来代替kd·(当前误差-上次误差),这样目标值跳变时不会产生微分冲击,而实际的阻尼效果还在。
typedef struct { float kp, ki, kd; float integral; float integral_min, integral_max; float out_min, out_max; float old_actual; } PosPID_t; float PosPID_Calc(PosPID_t *pid, float target, float actual) { float err = target - actual; pid->integral += err; if (pid->integral > pid->integral_max) pid->integral = pid->integral_max; if (pid->integral < pid->integral_min) pid->integral = pid->integral_min; /* 微分项只对反馈值作用,避免目标值阶跃引起尖峰 */ float derivative = -(actual - pid->old_actual); pid->old_actual = actual; float out = pid->kp * err + pid->ki * pid->integral + pid->kd * derivative; if (out > pid->out_max) out = pid->out_max; if (out < pid->out_min) out = pid->out_min; return out; }3. 基于HAL库的定时器、编码器与PWM配置
3.1 定时器资源分配与PWM频率选择
先说明硬件背景。我工程里用的是STM32F407VET6,电机是带增量式编码器的直流无刷电机,驱动方式为三相全桥。如果驱动器已经内置换相,MCU只需要输出一路调速PWM,那定时器资源会更宽裕;我这里按三相PWM的情况讲,这是最完整的形态。
定时器分配我建议是这样:
| 定时器 | 功能 | 说明 |
|---|---|---|
| TIM1 | 三相PWM输出 | 高级定时器,CH1/CH2/CH3配互补通道,带死区 |
| TIM2 | 编码器接口 | 32位计数器,杜绝16位溢出问题 |
| TIM6 | 1ms控制节拍 | 基本定时器,产生更新中断 |
PWM频率我选的是20kHz。这个值有两个好处:一是超过了人耳听觉的敏感频段,电机噪声明显低;二是对大多数无刷电机的相电感来说,20kHz的开关纹波在可控范围,不会让电流纹波大得离谱。死区时间设在1微秒左右,对IR2103这类常见栅极驱动芯片足够稳妥,太大浪费调制范围,太小则有上下桥直通炸管的风险。
编码器接口是另一件必须说清楚的事。STM32F4的TIM2/TIM5是32位计数器,TIM1/TIM3/TIM4是16位计数器。编码器模式需要占用定时器的CH1和CH2两个引脚,分别接编码器的A相和B相,配置为TI1和TI2双沿计数后,编码器线数会被四倍频。我用的编码器是500线,四倍频后每圈2000个脉冲,也就是每个脉冲对应0.18度,位置分辨率完全够用。
3.2 HAL库配置里最容易踩的三个坑
用HAL库调这套外设,表面上看CubeMX点点点就行了,实际有三个坑值得单独说。
第一,高级定时器的PWM启动不是调一个函数就完事。HAL_TIM_PWM_Start启用的只是主输出,互补通道和刹车输出需要额外处理,而且高级定时器的主输出使能位MOE必须打开,否则PWM是发不出去的。我最初只调了HAL_TIM_PWM_Start,上电后电机纹丝不动,查了半天发现是主输出没使能。
第二,编码器方向接反是个高频问题。电机正转时CNT计数应该递增,如果实际是递减,不要急着改程序逻辑,最简单的办法是在CubeMX里把编码器模式改成反方向计数,或者直接对调A/B两路输入。改硬件比在软件里加一堆负数判断要干净得多。
第三,编码器计数器溢出问题。我用32位定时器还有处理溢出的原因是,位置量是长时间累积的,不能把一个int32直接当位置用。我习惯通过两次读数的差值来计算速度,同时把CNT转成int32后再参与运算,这样即使发生回绕,只要单次周期内的位移不超过半个计数范围,数据就是正确的。这种做法在电机正反转切换时尤其重要,否则位置会突然跳变一大截。
4. 串级控制的核心链路:从初始化到控制周期
4.1 控制周期与中断设计
控制系统的时基设计比PID公式本身更影响稳定性。我的方案是:TIM6产生1ms的更新中断,速度环在每个中断里执行一次;位置环用计数器累计,每5次中断执行一次,也就是5ms。这样两个环的节拍非常清晰,不会互相干扰。
中断里做控制计算的顺序也很讲究。很多初学者习惯先算PID再更新采样值,或者把采样放在计算的后面,这等于让控制器总在用旧数据做决策,响应会慢一拍。我的中断处理函数里固定顺序是:先读编码器CNT、计算速度和位置,再执行位置环判断和速度环计算,最后把新的占空比写入PWM比较寄存器。同时,所有耗时操作比如串口打印、OLED刷新、蓝牙通信,一律不进中断,只通过标志位交给主循环处理。否则一旦printf把中断拖住,整个控制节拍就乱套了。
4.2 代码骨架与保护逻辑
串级控制的核心代码骨架大致如下,我已经把采样、计算、输出三步分开:
void Motor_Control_Tick(void) { /* 1. 采样:读编码器,计算速度和位置 */ int32_t cnt = (int32_t)TIM2->CNT; int32_t delta = cnt - last_cnt; last_cnt = cnt; float speed_rpm = Speed_Calc(delta); /* 由脉冲数换算转速 */ float position_deg = Position_Calc(cnt); /* 由累计脉冲换算角度 */ /* 2. 位置环:每5ms执行一次,输出目标速度 */ if (++pos_loop_cnt >= 5) { pos_loop_cnt = 0; target_speed = PosPID_Calc(&pos_pid, set_position, position_deg); if (target_speed > max_speed) target_speed = max_speed; if (target_speed < -max_speed) target_speed = -max_speed; } /* 3. 速度环:每1ms执行一次,输出占空比 */ float duty = SpeedPID_Calc(&spd_pid, target_speed, speed_rpm); if (duty > duty_limit) duty = duty_limit; if (duty < -duty_limit) duty = -duty_limit; /* 4. 输出到PWM:根据正负决定方向 */ if (duty >= 0) { DIR_FWD; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint32_t)(duty / 100.0f * pwm_period)); } else { DIR_REV; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint32_t)(-duty / 100.0f * pwm_period)); } }代码本身不复杂,但有两点必须强调。第一,位置环输出限幅和速度环输出限幅是两份独立的限幅,位置环限幅是“最大允许速度”,速度环限幅是“最大占空比”,不能混用。第二,一定要在骨架里预留保护逻辑。我在这个基础之上加了堵转保护:位置环输出接近限幅而实际速度持续低于阈值,持续一段时间直接停机;还加了软限位,在目标位置附近设一个缓冲区,防止机械碰撞。
5. 实测调试中的震荡、抖动与爬行排查
5.1 速度环高频震荡
第一版速度环参数调试时,我把P直接给到了3.0,积分几乎没加,结果电机高速转动时整个机构都在嗡嗡响,从串口打印的速度波形看有明显的20Hz左右振荡。我一开始怀疑编码器安装偏心,特意换了个编码器,问题依旧。后来用控制变量法逐步排查,才发现根子在速度反馈的噪声被P项放大了。编码器测速本质上是脉冲计数,低速时一个脉冲的跳变就会引起速度计算值的明显波动,P一大,这个量化噪声就变成了真正的占空比波动。
解决方法是给速度反馈加一阶低通滤波,截止频率取200Hz左右,同时把速度环的Kd保持为零。不要一上来就加D,速度测量的噪声比你想象的大,D在这种场合经常是添乱。加了滤波之后,同一个P值下,速度波形明显平滑了,噪声问题消失。这个案例给我的教训是:速度环出现高频抖动,先查反馈信号质量,再排查增益问题,顺序不能反。
5.2 位置环输出抖动与极限位置爬行
位置环叠加上去后出现了两个很典型的现场问题。第一个是停止状态下电机还在不断往一个方向顶,像虫子一样一拱一拱往前爬,这叫低速爬行。原因是位置环的积分项让速度指令始终是非零的小值,而速度环在极低速时增益不足,摩擦非线性又让电机一停一动。解决办法是加静摩擦补偿:当速度指令绝对值小于某个阈值时,直接给一个固定的启动占空比,让电机克服静摩擦平滑运动。这个阈值和补偿值要根据实际负载实测,我工程里阈值取50rpm,补偿占空比取3%左右。
第二个问题是目标位置附近来回抖动。位置是离散量,编码器误差跳一个码,位置环的微分项就会产生一个尖峰,PWM占空比被猛地冲一下,电机就在目标位置附近来回“呼吸”。解决思路是微分项只对位置反馈做滤波,或者干脆位置环不放D,把阻尼作用交给速度环。我最终把位置环的Kd设为0,配合速度环的P,整个系统的稳定性和定位精度都有明显改善。
5.3 排查顺序的建议
这一节其实是给所有调双闭环的人的一条忠告:不要两个环一起调。我见过太多人直接在内环外环同时开启的状态下改参数,出问题后根本分不清是哪个环在捣乱。正确的排查链路应该是:
- 把位置环输出强制设为固定值,也就是断开外环,先单独给速度环一个目标速度,看它能不能稳定跟踪。
- 从低速到高速逐步测速度环,记录稳态误差和动态响应,确认速度环没问题再往下走。
- 叠上位置环,位置环的P从零开始缓慢加大,每次加一点就观察系统是否出现超调或震荡。
- 出现振荡时先判断频率:高频振荡(几十赫兹以上)多半出在速度环、采样周期或PWM环节;低频振荡(几赫兹)多半是位置环增益过大或内环响应太慢。
| 故障现象 | 主要嫌疑 | 验证手段 |
|---|---|---|
| 高频嗡鸣、电流发烫 | 速度环P过大 / 反馈噪声 | 降低P或加低通滤波观察 |
| 目标附近来回抖动 | 位置环D过大 / 编码器量化 | 去掉位置环D重新测试 |
| 稳态爬行、一拱一拱 | 低速摩擦补偿缺失 | 增加静摩擦补偿 |
| 低速速度跳变 | 编码器分辨率不足 | 换高线数编码器或改用M/T测速 |
6. 参数整定顺序与个人经验
6.1 先内环后外环的具体操作
参数整定顺序没有悬念:先内环,后外环。速度环用阶跃给指令,比如从0直接给到200rpm,通过串口或虚拟示波器观察转速波形。P从小开始加,加到系统刚出现振荡就记录这个临界增益,然后把P退到临界值的一半左右,这样能留出足够的稳定余量。I从小到大逐步加,用来消除稳态误差,同样留出余量。速度环的D我一般不加,最多给一个非常小的值,原因前面已经说过,测速噪声会把D项放大成剧烈抖动。
速度环稳定后叠加位置环。位置环的P决定了达到目标位置时的速度倍数,P过大会让速度环接到超出能力的指令,表现为位置过冲。我实测某负载下位置环P=3.0时,位置超调接近20%;降到1.2之后,超调控制在5%以内。位置环的I在我这个项目里最终没有启用,因为速度环已经能消除大部分稳态误差,位置环再加I反而容易在目标点附近制造缓慢振荡。如果你的机械结构有明显的静差,再考虑加位置环I,而且一定要配合积分限幅。
6.2 几个值得注意的边界情况
最后聊几个容易被忽略的边界情况,这些在普通教程里很少讲到。
第一,编码器线数直接决定低速性能。如果编码器只有几十线每圈,四倍频后依然很稀疏,低速时速度环的分辨率会严重不足,导致低速段出现差值振荡。遇到这种问题,要么换高线数编码器,要么把测速方式从M法改成M/T法,用高频时钟填充脉冲间隔来提高低速分辨率。
第二,负载惯量如果会随工况变化,同一套PID参数可能从“稳定”变成“震荡”。我一般会在程序里预留两套参数,一套对应空载,一套对应带载,根据实际工况动态切换。如果要更智能,可以做简单的惯量辨识或增益调度,但对多数设备来说,两套参数切换已经够用。
第三,用串口把PID的三个分量都打印出来,这个习惯能帮你省一半调试时间。很多“调不稳”的情况,一打印就会看到积分项其实一直在饱和状态,或者微分项在疯狂输出尖峰。看清每个分量在输出中的占比,比盲目调三个系数有效得多。我现在的调试流程里,第一步永远是先接好串口,把目标值、实际值、Kp·e、Ki·∫e、Kd·de分通道打出来,再动手改参数。
最后再分享一个小技巧:位置环的目标值最好是平滑给定的,不要把阶跃目标直接丢给PID。我习惯在程序里加一个梯形速度规划器,让位置环的目标位置按最大加速度和最大速度曲线逼近最终值。这样PID的压力会小非常多,位置环的P可以放心加大,定位速度反而更快,超调也更小。这个细节是我调试这套串级控制时最满意的一个改动,强烈建议你也试试。
本文还有配套的精品资源,点击获取
