ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

直流无刷电机双闭环串级控制:位置环与速度环的PID实现与调试

直流无刷电机双闭环串级控制:位置环与速度环的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位溢出问题TIM61ms控制节拍基本定时器产生更新中断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过大会让速度环接到超出能力的指令表现为位置过冲。我实测某负载下位置环P3.0时位置超调接近20%降到1.2之后超调控制在5%以内。位置环的I在我这个项目里最终没有启用因为速度环已经能消除大部分稳态误差位置环再加I反而容易在目标点附近制造缓慢振荡。如果你的机械结构有明显的静差再考虑加位置环I而且一定要配合积分限幅。6.2 几个值得注意的边界情况最后聊几个容易被忽略的边界情况这些在普通教程里很少讲到。第一编码器线数直接决定低速性能。如果编码器只有几十线每圈四倍频后依然很稀疏低速时速度环的分辨率会严重不足导致低速段出现差值振荡。遇到这种问题要么换高线数编码器要么把测速方式从M法改成M/T法用高频时钟填充脉冲间隔来提高低速分辨率。第二负载惯量如果会随工况变化同一套PID参数可能从“稳定”变成“震荡”。我一般会在程序里预留两套参数一套对应空载一套对应带载根据实际工况动态切换。如果要更智能可以做简单的惯量辨识或增益调度但对多数设备来说两套参数切换已经够用。第三用串口把PID的三个分量都打印出来这个习惯能帮你省一半调试时间。很多“调不稳”的情况一打印就会看到积分项其实一直在饱和状态或者微分项在疯狂输出尖峰。看清每个分量在输出中的占比比盲目调三个系数有效得多。我现在的调试流程里第一步永远是先接好串口把目标值、实际值、Kp·e、Ki·∫e、Kd·de分通道打出来再动手改参数。最后再分享一个小技巧位置环的目标值最好是平滑给定的不要把阶跃目标直接丢给PID。我习惯在程序里加一个梯形速度规划器让位置环的目标位置按最大加速度和最大速度曲线逼近最终值。这样PID的压力会小非常多位置环的P可以放心加大定位速度反而更快超调也更小。这个细节是我调试这套串级控制时最满意的一个改动强烈建议你也试试。本文还有配套的精品资源点击获取
返回列表