
简介这套基于STM32-F1与HAL库的直流有刷电机三闭环控制工程源代码面向嵌入式开发者与电机控制初学者完整实现了位置环、速度环、电流环的位置式PID控制。项目从底层外设初始化入手包含编码器位置反馈、电机测速、电流采样以及三环级联调节、PID参数整定和中断服务程序设计等关键环节可直接编译运行也可按模块移植到自有硬件平台。资源包共371个文件压缩后约21.16MB以h头文件和c源文件为主覆盖HAL库外设驱动、应用层控制逻辑、工程配置与链接脚本同时附有o、crf等编译中间文件和axf、hex、map、lst等构建调试输出方便对照分析编译流程与结果。已有348人学习下载适合正在学习STM32电机控制或需要参考成熟三闭环PID框架的开发者使用可快速理解从霍尔/编码器反馈到电流、速度、位置逐层闭环的实现思路。1. 三闭环PID控制为什么电机控制要套三层环做电机控制的工程师都知道一个反直觉的结论单环PID永远做不到“位置又准、速度又稳、电流又不冲”。如果你只是用编码器读位置、然后一个PID直接输出PWM那电机在带载和启停时一定会出现要么响应迟钝、要么一抖一冲的问题。这套基于STM32F1的直流有刷电机三闭环源码就是按工业上最标准的做法把电流环、速度环、位置环从内到外叠起来用HAL库、位置式PID算法、C语言实现适合正在做巡线小车、机械臂关节、云台或者实验室伺服平台的开发者直接抄作业。它能解决的核心问题有两个一是让电机在任何负载下都按你期望的位置停下来二是让你能看到每个环节的PID参数独立生效而不是互相打架。下文我会拆解它的控制架构、HAL库工程接线、位置式PID代码结构再把我实际调试中踩过的坑全部列出来。2. 三闭环的基本架构从电流环到位置环为什么是“由内向外”叠2.1 三个环各自管什么电流环是地基位置环是天花板理解这套源码的第一步是搞清楚三个环分别控制什么。电流环是最内层直接控制电枢电流也就是转矩速度环在中间它的输出是电流环的给定值位置环在最外层它的输出是速度环的给定值。信号流向是位置给定 → 位置环PID → 速度给定 → 速度环PID → 电流给定 → 电流环PID → PWM输出 → 电机。这里有一个关键的带宽原则内环一定要比外环快。电流环的响应速度最快一般在几百赫兹到一千赫兹级别速度环次之几十到一百赫兹位置环最慢几赫兹到十几赫兹就够了。为什么因为外环的输出是内环的给定如果外环波动太快内环根本追不上系统就会震荡。源码里每个环使用独立的PID参数和独立计算周期就是为了满足这个原则。位置式PID在这个架构里还有一个优势它直接输出的是绝对量每个环的给定值和反馈值都有明确的物理单位调试的时候你能直接从波形上看懂某个环到底追没追上。2.2 反馈来源与采样编码器管位置/速度采样电阻管电流这套系统需要两个反馈源。位置和速度反馈来自电机尾轴的光电编码器电流反馈来自电机供电回路中的采样电阻或电流传感器。编码器的处理要区分“位置”和“速度”两种数据位置环反馈直接读取编码器累积计数速度环反馈则是单位时间内的脉冲增量也就是M法测速。电流采样要注意跟PWM的开关时序对齐——最好在PWM中心时刻采样避开MOS管开关瞬间的尖峰。源码中电流反馈用的ADC是连续采样加DMA搬运这样CPU不需要在每次电流环中断里等待ADC转换完成。电流环的计算频率通常用定时器PWM周期中断来驱动比如PWM频率设为20kHz那电流环也是20kHz速度环在此基础上分频比如每10次电流中断执行一次位置环再分频每50次执行一次。这种基于中断次数的分频方式比用多个定时器错开更简单也不会出现多个中断互相抢占的抖动。3. 基于HAL库的工程实现CubeMX配置、定时器、编码器与ADC3.1 定时器资源分配PWM用TIM1编码器用TIM2/TIM3拿到这份源码后你不需要改板子但需要把定时器资源对上号。主流做法是TIM1输出两路PWM控制H桥一路带互补输出另一路做死区插入TIM2或TIM3工作在编码器模式读取AB相。STM32F1的定时器编码器模式属于硬件正交解码不需要外部中断去数脉冲CPU开销为零脉冲计数由硬件完成这也是这套源码用HAL库时最值得复用的一部分。CubeMX里的关键配置项包括编码器模式选Encoder Mode TI1 and TI2极性都设成Rising计数器设成32位或16位溢出前清零。速度环读取周期内计数器增量后要立即把计数器清零或记录上一次计数值否则增量会丢。PWM输出那边的核心参数是PWM频率选20kHz左右死区时间设成1微秒以内——有刷电机的MOS管开关速度没那么快死区太大反而会让电流波形变得不连续。3.2 电流采样链路ADCDMA的HAL配置电流采样在F1系列上只有一种推荐接法ADC1的某个通道接采样电阻放大后的电压信号用定时器触发ADC转换再通过DMA把结果搬到内存循环缓冲区。HAL库配置里要把ADC设置为扫描模式关闭、单通道、连续转换关闭使用外部触发。触发源选TIM1的TRGO事件——也就是PWM周期信号这样每个PWM周期采样一次电流采样点正好落在PWM波形的已知相位上。这里有个细节容易被忽略TIM1的TRGO触发ADC时CubeMX的ADC触发源列表里选的不叫“TIM1”而是“TIM1_TRIG”要跟PWM的通道输出区分开。DMA那边用Normal模式还是Circular模式我建议用Circular配合双缓冲电流环中断里直接读当前最新的转换结果。ADC采样值用右对齐、分辨率为12位电流值的物理换算依靠采样电阻值和运放放大倍数来定。3.3 代码初始化顺序先时钟再编码器最后PWM输出初始化顺序直接影响上电瞬间电机是否乱动。推荐的顺序是先初始化系统时钟和所有GPIO然后初始化编码器定时器让位置反馈先有数据接着初始化ADC与DMA最后才开PWM输出。原因很简单PWM一开启电机就可能转如果此时速度环和位置环还没有可靠的反馈数据PID计算会输出一个不合理的占空比电机就会猛冲一段。另外一个常见问题在HAL库使能PWM时有些开发者直接在CubeMX里把PWM设置为“初始即输出”上电就有占空比这在小车上可能无所谓但如果在机械臂或云台上上电猛转一下就可能撞坏结构。正确做法是把PWM通道的初始占空比设为0等所有反馈就绪再手动调用HAL_TIM_PWM_Start。源码里已经做了这个逻辑你移植时注意别把它改成上电自动启动。4. 位置式PID三环代码实现结构体定义、计算函数与限幅逻辑4.1 三环PID结构体设计与初始化参数源码的PID实现是典型的位置式算法不是增量式。位置式和增量式最大的区别在于位置式输出的是“控制量的绝对值”比如占空比50%增量式输出的是“控制量的变化量”比如这周期比上周期多3%。位置式的好处是每个环节的物理意义直观速度环的输出直接是电流环的给定值调试时看得懂缺点是必须做积分限幅和输出限幅否则积分饱和会拖垮动态响应。typedef struct { float Kp; // 比例增益 float Ki; // 积分增益 float Kd; // 微分增益 float target; // 目标值外环给的给定 float feedback; // 反馈值编码器/ADC采样 float integral; // 积分累加值 float last_error; // 上一次误差用于微分 float out_limit; // 输出限幅 float integral_limit;// 积分限幅防止饱和 } PID_TypeDef;这套结构体三环各持一份。out_limit用于限制本环节输出范围比如电流环输出限幅在PWM占空比的±100%而速度环输出限幅在电流给定范围的±80%位置环输出限幅在速度给定范围的±90%。integral_limit是关键字段它单独限制积分项的积累量——如果积分项达到饱和叠加到输出上会让PID在该停止的时候迟迟收不住。4.2 位置式PID计算函数核心代码与参数落地float PID_Calculate(PID_TypeDef *pid, float target, float feedback) { float error target - feedback; // 比例项 float p_out pid-Kp * error; // 积分项先累加再做积分限幅 pid-integral error; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; float i_out pid-Ki * pid-integral; // 微分项差分形式 float d_error error - pid-last_error; pid-last_error error; float d_out pid-Kd * d_error; // 计算总输出并做输出限幅 float output p_out i_out d_out; if (output pid-out_limit) output pid-out_limit; if (output -pid-out_limit) output -pid-out_limit; return output; }这个函数的执行顺序有讲究先算比例项再算积分限幅最后算微分项。微分项用的是“误差差分”而不是“反馈值差分”这是位置式PID的标准写法区别在于误差差分对目标值变化更敏感目标突变时会有微分冲击。代码里last_error在每次计算结束时更新切不可放在函数开头更新否则微分项永远算的是当前误差减当前误差等于零这是移植时最容易翻车的地方。电流环的数据类型需要特别说一下如果直接用floatF1系列的单精度浮点运算没问题但ADC采样值进来时先转float再计算可避免整数除法的精度损失。而输出给PWM的占空比最后一步要把float换算成定时器比较寄存器的整数值再用__HAL_TIM_SET_COMPARE写入。4.3 三环串联的代码组织中断里怎么嵌套调用三个环在代码层面是嵌套调用的不是各写各的循环。核心结构是在PWM周期中断里执行电流环电流环输出直接写PWM比较寄存器每N次中断后执行速度环速度环输出作为电流环的target每M次中断后执行位置环位置环输出作为速度环的target。void TIM1_UP_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim1, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim1, TIM_FLAG_UPDATE); // 电流环每次中断都执行 current_feedback Get_Current_From_ADC(); current_output PID_Calculate(pid_current, speed_output, current_feedback); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, (int)current_output); // 速度环每10次中断执行一次 speed_counter; if (speed_counter 10) { speed_counter 0; speed_feedback Get_Speed_From_Encoder(); speed_output PID_Calculate(pid_speed, position_output, speed_feedback); } // 位置环每50次中断执行一次 position_counter; if (position_counter 50) { position_counter 0; position_feedback Get_Position_From_Encoder(); position_output PID_Calculate(pid_position, position_target, position_feedback); } } }分频常数10和50不是固定值它们的比值应该等于电流环带宽和速度环带宽的比值。如果电流环周期是0.05ms20kHz速度环每10次执行一次就是0.5ms位置环每50次就是2.5ms。这个时序安排直接决定三环的响应速度是否匹配比我见过的一些直接把三个环都放在同一频率下跑的工程要合理得多。有一点需要提醒中断里不要调用HAL_Delay也不要调用任何会阻塞的函数。ADC的DMA传输是后台完成的中断里拿到的只是最新一次DMA搬运结果不要主动触发转换再用轮询等待结果。这套代码的时序设计就是靠DMA的自动更新来实现的改为轮询反而会打乱PWM采样对齐。5. 避坑与调试三闭环最常见的五个坎5.1 电机一上电就猛转到底按停都没用现象代码烧进去上电瞬间电机直接满占空比旋转直到撞到机械限位才停。原因编码器初始化失败或方向接反导致位置环反馈值永远为0或符号反了。比如编码器A/B相接反了电机正转时反馈却在反向计数位置误差越来越大PID输出饱和电机全速追踪一个永远追不上的目标。解决上电后先打印编码器的原始计数值用手拨动电机轴看计数值是否随转动方向单调增减。确认方向一致后再让电机开环输出一个10%占空比同时打印编码器反馈确认编码器方向和PWM方向一致。这个过程必须在闭环之前做跳过这一步直接调PID你会怀疑每个参数都有问题。另外如果用的是H桥驱动检查PWM通道1和通道2的逻辑是否真的让电机往目标方向转有时候IN1和IN2的逻辑接反了闭环也会表现为“追不上”。5.2 空载和带载时电机声音完全不同带载甚至出现抖动现象电机空载运行时电流波形平稳一旦带载机身开始“嗡嗡”响转速波动严重时出现低频抖动。原因多半是电流环的Ki太大或电流环的PID计算频率低于PWM频率导致电流环在每个PWM周期的控制步进不均匀。电流环的本质是“周期内平均电流控制”如果计算频率跟不上PWM频率电流波形会出现拍频。解决先确认电流环计算频率等于PWM频率——TIM1更新中断的频率就是你的PWM频率20kHz的PWM就必须有20kHz的电流环计算。然后把电流环的Ki调小一半观察电流波形是否恢复平滑。如果仍然抖动再检查电流采样是否对齐了PWM中心F1的ADC触发可以配置在PWM中心或边沿建议选中心对齐这样采到的电流更接近周期平均值。5.3 位置环到位后总有一点“来回哆嗦”现象电机到达目标位置后编码器读数在其附近来回跳动电机轴肉眼可见地轻微抖动位置误差在±10个脉冲左右。原因这是位置环的Kp太大加上位置环输出未做死区处理导致的极限环振荡。位置环输出的是速度给定当位置误差只有几个脉冲时速度给定还在不停换向电机在零速附近反复启停。解决在位置环计算里加一个死区判断——当|error| deadzone时直接把位置环输出置0同时把速度环的给定也置0。这个死区要设成比编码器噪声略大比允许的定位精度略小。编码器噪声通常来自机械振动你可以先看静止时编码器读数的波动范围再把这个值乘以1.5作为死区。注意死区不是越大越好死区过大位置误差会超出用户允许的精度那就本末倒置了。5.4 速度环响应太慢位置环一给速度给定电机要“反应”很久现象位置环输出变化明显了但电机转速要隔几十毫秒才跟上整个系统“发肉”。原因速度环的PID参数太保守——通常Kp太小、Ki几乎没有。速度环在系统里是中间环节它的作用不是“慢慢逼近”而是“快速跟踪位置环给的给定值”所以它的带宽虽然比电流环低但也不能慢到让位置环等它。解决先把速度环的积分项清零只调Kp从小到大逐步加大直到电机转速能在一两个速度环周期内跟上给定值的80%。然后再加Ki用于消除稳态误差。如果调大Kp后出现超调不要急着加Kd先看是不是电流环的out_limit太小限制了电流输出——电流环限幅不到达饱和速度环再努力也只是把电流给定推到限幅值实际转矩却提不上去。5.5 HAL库的HAL_TIM_PWM_Start放在初始化末尾电机还是抖了一下现象即使在CubeMX里把PWM初始占空比设为0上电瞬间电机还是会抖一下。原因PWM比较值寄存器更新存在延迟。F1系列在PWM计数自由运行时修改比较值不会立即生效要等下一个PWM周期边界才加载。上电初始化时HAL_TIM_PWM_Start使能了输出但可能GPIO引脚和AFIO时序还没完全配置好此时即使比较值寄存器是0PWM输出也有一个极短的不确定电平。解决上电时把PWM通道的引脚先配置为普通推挽输出引脚电平拉低占空比寄存器设为0等所有初始化完成再调用HAL_TIM_PWM_Start同时切换引脚为AF模式。这一步属于“时序安全”问题源码里没有明确写就需要自己补。另外一个习惯是调试期间在main函数里加一个延时标志位通过串口或按键确认可以启动电机后再启动PWM避免每次上电都撞限位。6. 调参实用手法三环参数从内到外一轮一轮试调参的顺序没有商量余地——必须先电流环、再速度环、最后位置环。任何跳过步骤的做法最终都会因为“不知道是哪个环的问题”而浪费大量时间。我分享一个实战流程每一步的量化指标都有参考价值你可以直接照搬。电流环调试只给一个固定速度给定屏蔽速度环和位置环输出让电流环独立运行。电流环的给定电流给一个阶跃观察电流波形上升时间。先Kp后KiKp从0.1开始往上加每次调整后观察电流波形是否出现振荡一旦出现高频振铃就退回上一档。Ki的作用是消除稳态误差但如果Ki大于Kp的1/3电流波形基本就会开始“呼吸”。调好标准给定阶跃电流实际电流无超调、无振荡、上升时间在2-3个PWM周期内。速度环调试速度环的给定值用手动给一个固定转速观察编码器测速波形。如果电流环已经调好了速度环的输出可以直接作为电流给定而电流环会像“理想执行器”一样把它转成实际转矩。速度环的表现主要体现在加减速过程中给一个速度阶跃观察转速的上升过程和是否超调。这里有个实用技巧速度环的Kd是最不需要调的项因为电机自身的惯性就是一个天然的阻尼Kd加多了反而会把编码器测速的噪声放大。位置环调试最后再碰位置环。位置环的输出是速度给定所以调试位置环之前必须先确认速度环跟踪性能足够好——速度环无法跟上给定值位置环就会认为“电机没动”而持续累加输出。位置环的P参数从小往大加观察定位响应的临界阻尼状态。位置环的I参数一般不急着加因为三环系统有足够的内环误差抑制能力很多工程的位置环只用PD就够了。调试过程中我用到的验证方法是把三个环的实际输出和给定值打包通过串口或SWD发送到上位机画在同一张曲线图上。三闭环的调试核心就是“看每一步的输出是否跟得上输入”曲线比手感可靠得多。位置环到位后如果存在稳态误差且误差大于死区那就说明速度环的Ki没加够回速度环去调而不是在位置环继续堆P。这里提醒一点参数修改后优先在低速、小行程状态下验证不要一上来就做大行程阶跃。大行程时位置环输出很容易进入饱和整段行程里位置环都在限幅状态下运行这时候看到的响应曲线反映的其实是速度环或电流环的性能而不是位置环的——这一点我踩过多次以前经常在位置环饱和区浪费时间调P后来养成了习惯任何参数修改后都必须先跑一组往返10°的小行程动作再跑大行程两组的曲线都稳定才说明参数在所有状态下都成立。从那以后我每次调三闭环都强制走一遍这个流程电流环阶跃波形是干净的速度环跟踪不超调位置环小行程无振荡然后才开始放大行程和工作负载的测试。希望这套思路和源码里的工程实现能帮你在F1平台上少走弯路一次调通三闭环。本文还有配套的精品资源点击获取