尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32 HAL库直流无刷电机位置与速度双闭环PID控制实现

STM32 HAL库直流无刷电机位置与速度双闭环PID控制实现 简介面向STM32F4开发者的直流无刷电机控制例程包完整实现位置环与速度环双闭环串级控制位置式PID算法由下位机HAL库代码承载。通过按键或配套上位机即可启停电机并实时调整PID参数便于对比不同参数下的响应曲线非常适合无刷电机驱动、运动控制类毕业设计或工程预研。资源包为zip压缩格式大小20.13MB共550个文件其中包含156个.h头文件、101个.c源文件、Keil工程文件、链接脚本、编译中间文件、调试配置以及可直接烧录的hex文件。整个工程结构紧凑导入KEIL即可编译运行。该资源目前已有1213人浏览学习在无刷电机PID控制类代码中具有较高参考价值。代码将引脚连接集中到对应头文件的宏定义中硬件改动时只需调整宏即可同时例程中特别注明了未限制目标值可能导致的积分饱和、电机未停止时重新启动引起的脉冲误捕获等问题可帮助开发者避开常见调试隐患更快掌握位置环与速度环串级PID的工程设计方法。 我接手这个项目的时候手里只有一个明确要求用 STM32-F4 系列芯片配合 HAL 库把一套直流无刷电机的位置环、速度环双闭环串级控制跑起来而且下位机代码要具备直接参考和复用的价值。位置式 PID不是增量式这是立项时就定好的技术路线。当时团队里还有人在犹豫要不要直接上 FOC但考虑到项目周期、算力分配以及最终只需要控制电机转角位置这一核心目标我们最终选定了带霍尔传感器的方波控制方案配合双闭环串级 PID。整套系统跑通之后效果比预期稳定得多。这篇内容我就把这套方案从设计思路到 HAL 库源码实现再到调参踩坑完整拆开来讲。项目里用到的芯片型号是 STM32F407VET6主频拉到 168MHz控制周期 1ms位置环和速度环的运算均在这个周期内完成。这个方案适合谁参考如果你正在做机器人关节、云台、自动阀门、小车转向这类需要“精确停在某个角度”的场景那这篇文章应该能帮你少走不少弯路。如果你还不太理解什么叫“位置环速度双闭环”或者不清楚为什么串级控制比单环更好调那从这里开始也完全来得及。我尽量把原理讲得通俗把代码写得直白把那些藏在文档和教程背后的经验教训也一并交代清楚。1. 项目整体设计与双闭环思路拆解1.1 为什么一定用“位置环速度双闭环”而不是单环先看一个最直接的场景你想要电机从当前角度 A 转到目标角度 B。如果只做一个位置环——也就是把“目标角度和实际角度的差值”直接换算成 PWM 占空比输出会发生什么电机会以最大占空比冲出去快到目标位置时才突然减速但因为惯性和力矩的对抗几乎一定会过冲然后来回震荡好几次才能稳定。位置精度勉强能看但动态过程一塌糊涂而且如果你改变负载整个系统的特性都会跟着变参数又要重调。双闭环的核心逻辑是位置环不直接输出 PWM而是输出一个“目标速度”速度环再去追踪这个目标速度最终输出的才是 PWM 占空比。这样做的好处非常明显。位置环只负责决定“该转多快”具体到电压和力矩的控制权全部下放给速度环。速度环天生响应快能对负载扰动、摩擦力变化做快速补偿位置环在它的外圈相当于一个“调度员”不需要关心电机内部的复杂特性。打个比方说单位置环就像是你开车时不看仪表只盯着目的地一脚油门踩到底再踩刹车双闭环则是一个驾驶员位置环在说“我们还需要再快一点/慢一点”而真正踩油门的是副驾驶速度环他根据当前车速精确控制踏板深度。这种分层控制的好处就是每一层只关心自己该做的事整体调参难度反而降低了。1.2 “串级控制”究竟串在哪里串级这个词很多初学者会觉得玄乎。其实拆开看就是两个 PID 控制器一个套一个最外层是位置 PID它的输入是“目标位置 - 实际位置”输出是“目标速度”这个目标速度直接作为内层速度 PID 的给定值速度 PID 的输入是“目标速度 - 实际速度”输出才是 PWM 占空比。信号传递路径就是这样一路串下来的。整个控制链路里位置环的输出限幅非常关键。比如我的项目里电机最大转速设定为 3000rpm那么位置环输出的目标速度必须钳位在 -3000 到 3000 之间。如果不加这个限幅位置误差一大位置环给出的目标速度会超过电机物理极限速度环拼命追也追不上积分项疯狂累积最终表现就是系统开始超调和震荡。这个限幅值本质上是系统的物理约束相当于给调度员划了一条“不能超过这个速度”的红线。另一个关键点是控制周期的选择。我最终采用的是 1ms 控制周期也就是电流环这里指 PWM 输出调节和速度环的处理都在 1ms 定时中断里完成。STM32F407 的主频 168MHz 跑这点浮点运算绰绰有余其实 500 微秒到 2 毫秒范围内都是可行的但 1ms 是一个性能和稳定性的折中。周期太短霍尔采样和调速的噪声会变大周期太长位置环的响应会显得迟钝动态性能跟不上。1.3 位置反馈的获取方式霍尔传感器与定时器编码器模式位置反馈是整个闭环的地基。这个项目用的是带霍尔传感器的直流无刷电机。霍尔传感器提供了 6 个换相扇区信息同时也能计算转速。但想获得更高的位置精度单靠霍尔换相点是不够的。我的做法是同时使用定时器的编码器模式来读取电机后端安装的光电编码器信号形成完整的位置反馈链路。STM32 的定时器编码器模式是一个非常方便的外设功能。它可以直接把 A、B 两相正交编码信号解码成计数脉冲硬件自动处理倍频逻辑不需要 CPU 干预也基本不占用额外开销。我在项目里使用的是 TIM4 编码器模式工作于 4 倍频状态。电机轴每转一圈编码器输出 1000 线4 倍频之后就是 4000 个脉冲位置分辨率大约是 0.09 度。实际调试下来这个精度对于云台控制来说完全够用。2. 位置式 PID 与增量式 PID 的选型分析2.1 位置式 PID 的公式与离散化实现PID 控制器的连续形式是 u(t) Kp * e(t) Ki * ∫e(t)dt Kd * de(t)/dt。在单片机里我们只能处理离散量所以需要把积分和微分离散化。位置式 PID 的离散公式长这样u(k) Kp * e(k) Ki * Σe(j) * T Kd * (e(k) - e(k-1)) / T其中 e(k) 是第 k 个采样周期的误差T 是控制周期Σe(j) 从启动到当前的误差累加和。注意这里的输出 u(k) 是绝对的控制量直接对应 PWM 的占空比或目标速度值。位置式 PID 实现起来非常直观但对于执行器存在饱和约束的系统必须额外处理积分饱和问题。我的项目里PWM 占空比最大只能到 100%如果 PID 输出已经顶到上限而误差还存在积分项会继续累积等误差反向时积分项已经“淤积”了一大堆系统会明显过冲。这也是很多新手调 PID 发现“怎么调都震荡”的隐藏原因之一。2.2 什么场景下不用增量式而选位置式增量式 PID 输出的是控制量的增量 Δu(k)也就是这次和上次的差值。它的优点是输出中天然带积分记忆且不会出现积分饱和——但代价是它更适合执行机构自带积分特性的场景比如步进电机驱动或者需要通过累积小步长来逼近目标值的系统。直流无刷电机的 PWM 控制本身没有积分特性如果我想要一个“直接等于多少占空比”的明确控制量位置式更适合。而且位置式 PID 的每一项都有明确的物理意义比例项对应“当前误差有多大”积分项对应“历史误差累积了多少”微分项对应“误差变化有多快”。调试时你可以直接看波形判断是哪一项出了问题这对后续调参非常有帮助。当然位置式也有它的麻烦事主要就是前面说的积分饱和以及启动瞬间可能出现的超调。我个人的处理习惯是积分分离当误差绝对值大于某个阈值时积分项不参与运算只有误差小于阈值才启用积分。这样启动阶段的超调就被削掉了。输出限幅位置环输出限幅是给速度环的目标速度限幅速度环输出限幅是给 PWM 占空比限幅两个限幅缺一不可。3. HAL 库配置与核心代码实现3.1 定时器 PWM 输出与编码器接口初始化HAL 库开发的核心是用 STM32CubeMX 生成初始化代码然后在上层添加控制逻辑。PWM 输出我用的是高级定时器 TIM1 的通道 1。直流无刷电机的方波控制需要按霍尔状态依次导通上下桥臂所以每 60 度电角度就要更新一次换相表。这个换相逻辑放在霍尔传感器触发的外部中断里完成优先级必须设置得比控制周期中断更高否则换相延迟会带来明显的电流噪声。编码器接口这边TIM4 配置为编码器模式。CubeMX 里操作很简单选好 TIM4把 Slave Mode 设为 Encoder Mode然后选择 TI1 和 TI2 同时计数。计数器范围设置要根据编码器线数来4000 个脉冲对应一圈那计数器溢出值就设为 999使用中心对齐计数模式。另外编码器计数器的值需要做零位校准我通常在电机上电后先让电机执行一次归零动作然后把计数器清零。PWM 的频率我设的是 20kHz。这个频率是经过权衡的20kHz 以上基本听不到电机的高频噪声避免项目现场被人问“这声音怎么这么尖”同时开关频率足够高电流纹波小电机运行更平顺。它正好高于人耳听力范围大约 15kHz又没高到让 MOS 管开关损耗爆炸的程度。3.2 位置环和速度环的 PID 控制代码框架下面这是我在项目里实际使用的速度环 PID 代码框架基于位置式 PID 并做了积分分离处理。整套控制 1ms 执行一次先算位置环再算速度环。位置环和速度环共用同一个 PID 结构体只是参数不同。typedef struct { float Kp; float Ki; float Kd; float target; // 目标值 float feedback; // 反馈值 float error; // 当前误差 float last_error; // 上一次误差 float integral; // 积分累积值 float output; // 输出值 float integral_limit; // 积分限幅 float output_limit; // 输出限幅 float deadband; // 死区 } PID_TypeDef; float PID_Calculate(PID_TypeDef *pid, float target, float feedback) { pid-target target; pid-feedback feedback; pid-error target - feedback; // 积分分离误差过大时禁掉积分防止饱和 if (fabs(pid-error) pid-deadband) { pid-integral 0.0f; } else { pid-integral pid-error; // 积分限幅 if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; else if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; } // 微分项直接按误差变化率计算可以加一阶低通滤波进行平滑 float derivative pid-error - pid-last_error; pid-last_error pid-error; // 位置式 PID 输出 pid-output pid-Kp * pid-error pid-Ki * pid-integral pid-Kd * derivative; // 输出限幅 if (pid-output pid-output_limit) pid-output pid-output_limit; else if (pid-output -pid-output_limit) pid-output -pid-output_limit; return pid-output; }速度环初始化时我设置的参数是 Kp 0.25Ki 0.18Kd 0.005积分限幅 1000输出限幅 1000对应 100% 占空比。这个参数是在空载情况下调出来的基线值带负载后根据系统响应做了小幅修正。主控制周期中断里的调用逻辑大概是这样的void Control_Task_1ms(void) { // 读取编码器当前计数值即当前位置 int32_t enc_now __HAL_TIM_GET_COUNTER(htim4); float current_pos (float)enc_now / ENCODER_PULSES_PER_REV * 360.0f; // 位置环输入目标位置输出目标速度 float target_speed PID_Calculate(pid_position, target_position, current_pos); // 速度环输入目标速度和当前速度输出 PWM 占空比 float current_speed Get_Speed_RPM(); // 通过编码器差分计算 float duty PID_Calculate(pid_speed, target_speed, current_speed); // 限幅到 PWM 范围并保证换相方向正确 if (duty 0) { Motor_Set_Duty(0, duty); // 正转 } else { Motor_Set_Duty(1, -duty); // 反转 } }3.3 霍尔换相与 PWM 施加逻辑直流无刷电机的方波控制核心是根据三路霍尔的状态组合决定哪两相导通、哪一相悬空。六个扇区对应六组换相表。我的做法是在 Hall 外部中断服务函数里查表更新 PWM 输出通道组合。这里有一个细节值得提醒霍尔信号在电机刚切换瞬间会产生抖动最好在代码里做一个简单的消抖——连续读到相同的霍尔状态两次再切换或者使用软件滤波延时。否则会出现“换相抖动”听上去像是电机在发颤。在 HAL 库中换相的输出我直接操作 TIM1 的通道比较寄存器。CubeMX 里把 TIM1 的三个通道配成 PWM 输出启用互补通道和刹车功能安全考虑换相时只需要组合修改 CCR 和 CCER 即可。注意无论是否使用霍尔换相PWM 占空比都来自速度环的输出换相只是改变三相的导通顺序不改变占空比。4. 调参流程与方法陷阱4.1 先内环后外环从速度环开始调串级控制调参有一条铁律先调内环再调外环。内环就是速度环外环是位置环。如果不按这个顺序来两个 PID 同时乱调最后你根本分不清问题是出在速度环没跟上还是位置环在震荡。速度环调参的具体做法是把位置环暂时去掉直接给速度环一个固定的目标转速比如 500rpm观察实际转速的阶跃响应。先只给 Kp从小到大慢慢加直到转速出现小幅震荡后退回 60% 左右作为 Kp 基线。然后加 Ki消除稳态误差一边加一边观察低速时的表现。Kp 和 Ki 都调好后最后加一点点 Kd用来压制速度波动。此时速度环的响应带宽和阻尼特性已经基本合格了。位置环调参就要简单很多在速度环已经稳定的前提下给一个固定目标位置让电机从 0 度转到 90 度观察位置响应曲线。同样从 Kp 起步调出临界震荡后再回退然后加 Ki 消除定位残差。位置环的 Kd 一般不需要太大如果你的速度环响应够快位置环的微分项甚至可以设为 0。4.2 常见问题排查实录问题一电机启动时抖动厉害甚至反转一下才正转。这个一般不是 PID 的问题是霍尔换相表错误或者霍尔信号线序接反。检查方法很简单手动缓慢转动电机轴霍尔状态的变化顺序应该沿着 1-3-2-6-4-5或者反向的规律走。如果跳号或者乱序请检查三相线和霍尔线的对应关系。问题二位置环稳态时有低频振荡电机在目标位置附近来回小幅摆动。八成是速度环的 Ki 太大了速度超调导致位置反复修正。我会把速度环的 Ki 减小 30% 再试如果还震就再检查编码器的安装固定机械松动也会引入这种随机扰动。问题三大阶跃响应时位置超调明显但缩小 PID 参数又会变慢。这是典型的积分饱和症状。你需要在位置环输出处和速度环输出处都加限幅并且启用我代码里的积分分离。在实际测试中这个处理能把超调量从 12% 压到 3% 以下响应时间几乎不受影响。问题四PWM 输出最大值只能到 100%但速度环误差一直存在最终系统稳不住。检查速度环输出限幅是不是设得太低。我最初把速度环输出限幅设成了 800导致占空比被上限卡死电机电压不足。发现后改成 1000问题立刻消失。这种“隐性限幅”问题在串级控制里特别容易忽略排查时不妨先确认所有限幅值是否真的给了系统足够的活动空间。4.3 一个值得尝试的调试工具组合调 PID 如果没有实时波形全靠看现象效率会低很多。我在项目里用 VOFA 作为上位机调试工具。下位机通过串口把目标位置、实际位置、目标速度、实际速度、PID 输出这五个变量以 float 形式打包发送上位机直接显示波形。这样每次修改参数后你都能清楚看到是哪一环节在拖后腿。串口发送的代码很简单把关键变量通过 DMA 以固定帧格式发出去就行。我的波特率用的是 1152001ms 周期发送一帧数据每帧大约 24 字节串口带宽占用很低完全不会影响控制实时性。调试完成后你再决定要不要关掉这路输出或者继续保留作为系统运行状态的观测窗口。5. 源码工程结构与后续扩展建议5.1 下位机源码的工程组织整个下位机工程我按功能划分了模块HAL 库的底层初始化放在 Core 目录业务代码拆成 Motor、PID、Encoder、Communication 几个模块。这样分工有一个直接好处当你换一个型号的电机时只需要修改 Motor 模块里的换相表和 Encoder 模块里的线数常量PID 和通信层完全不需要动。源码的入口逻辑也简单明了main 函数完成时钟、外设、全局变量初始化后进入 while(1) 主循环。主循环里处理的是相对慢速的任务比如串口指令解析、状态机管理。所有实时控制逻辑全部放在定时器中断里执行确保 1ms 的控制周期严格无抖动。5.2 后续可以怎么扩展如果你想让这个方案更“现代”一些可以从三个方向延伸。第一个方向是把速度环的 PID 换成 ADRC自抗扰控制或者滑模控制。对于负载变化剧烈的场景ADRC 的抗扰能力比 PID 强很多代价是算法复杂度上升和对算力的更高要求。不过 STM32F407 的 FPU 跑 ADRC 也不吃力只是参数整定门槛更高。第二个方向是加入电流环做成位置-速度-电流三闭环。加了电流环之后系统对堵转和过流保护的反应会快很多。直流无刷电机的电流环需要采样母线电流或相电流通常配一个电流传感器或采样电阻。三闭环的调参顺序是电流环、速度环、位置环仍然遵循先内后外的原则。第三个方向是通信升级。现在下位机只通过串口汇报状态如果你要做成可组网的多轴系统可以换成 CAN 总线或者 EtherCAT。HAL 库对 FDCAN 的支持很完善改造成本并不高。我的建议是先把现有这套双闭环吃透。你在调参过程中积累的对采样延迟、限幅、积分饱和这些问题的直觉会是后面做更复杂控制算法时最宝贵的底子。等你能一眼从示波器波形判断出问题在于速度环阻尼不够还是位置环误差偏大时四轴、机械臂、舵轮底盘这些项目你都有底气接得下来。本文还有配套的精品资源点击获取
返回列表