ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32电机双闭环控制:位置环+速度环串级PID的工程实现与调参实战

STM32电机双闭环控制:位置环+速度环串级PID的工程实现与调参实战 简介一套基于STM32-F4平台的位置环速度双闭环串级控制方案采用位置式PID算法配套下位机HAL库源代码面向电机控制方向的嵌入式学习者与开发者解决无刷电机位置与速度精确调节及PID参数在线整定问题。代码支持按键和上位机两种方式控制PID运行与参数调整便于观察现象或远程调试。资源包共550个文件其中h/c为HAL库与用户源码o、axf、hex等为编译中间产物与固件icf、uvprojx等为工程配置整体约20.13MB可直接导入Keil或IAR使用。已有1213人学习下载。借助上位机可快速实现位置速度双闭环控制宏定义引脚映射方式也便于移植到自定义硬件。注意目标值未做幅值限制时出现积分饱和属正常现象重新启动电机时因惯性可能造成脉冲误捕获理解这些边界条件有助于提升调试效率。 做一个电机双闭环控制项目最怕的就是代码写完了一上电电机乱震然后开始怀疑人生。这个项目的标题看起来很简单但“位置环速度环串级控制位置式PIDHAL库”这串关键词组合在一起其实就是一个非常典型的工业运动控制教学样板也是很多做智能车、云台、机器人关节的工程师绕不开的坎。我这次把这套东西完整跑通了一遍把源码、调参思路和一些踩坑的点都记录下来希望能给正在搞STM32F4直流无刷电机控制的朋友一些参考。这套方案最终实现的效果是给定一个目标位置电机能够平滑地加速、减速、精确停稳整个过程没有超调、没有振荡稳态精度控制在编码器单圈计数误差以内。如果你也在做类似的闭环控制项目这篇文章应该能帮你省下不少折腾时间。1. 控制架构解析为什么非要用双环串级1.1 位置环和速度环各自的职责边界先想明白一个核心问题只用位置环行不行理论上可以实际做出来你会发现两个问题。第一位置环直接输出PWM占空比给电机电机从静止到高速、再从高速到静止整个过程的加速度是不可控的机械冲击非常大齿轮箱和联轴器容易受损。第二位置环的带宽一般做得比较低如果系统出现负载扰动位置环的响应速度不够快会导致明显的抖动甚至失步。所以串级控制的思路就是把控制任务拆成两层速度环内环负责“跑得快且稳”位置环外环负责“走到位且准”。内环的响应速度至少要快于外环3到5倍这样才能保证外环给出的速度指令被快速跟踪。这个频率匹配关系是双闭环能不能调出来的根本前提。1.2 为什么选择位置式PID而不是增量式这个项目选的是位置式PID这是由被控对象和应用场景决定的。位置式PID的输出量是绝对值直接对应PWM占空比或者电压指令适用于执行机构本身需要绝对控制量的场景比如电机电压调速。增量式PID输出的是控制量的增量天然带积分作用适合带自锁或者积分记忆的执行机构。另外位置式PID在调试时物理意义特别清晰Kp对应“当前位置偏差”对控制量的贡献Ki对应“累计误差”的消除能力Kd对应“误差变化趋势”的抑制。调试过程中你很容易根据实际响应去调整具体某个系数这对新手友好。增量式PID的系数整定更需要经验没有位置式那么直观。1.3 采样周期的分配策略速度环1kHz定时器中断周期1ms因为内环需要快速响应电流变化和速度波动。位置环200Hz5ms外环不需要太高频率太高反而容易引入噪声且会给内环施加过大的调节压力。PWM输出频率20kHz超出人耳可听范围同时让电流纹波更小电机运行噪声显著降低。这三个时间基准一定要分开处理不要共用一个中断源否则后期调参时的时间耦合问题会让人崩溃。2. 核心代码实现HAL库下的双环控制逻辑2.1 编码器读取与速度计算的工程实现用的是STM32F407的TIM3编码器模式4倍频之后每圈产生4000个脉冲。速度计算用的是M法测频法在1ms中断里读取当前计数值与上一次的差值换算成RPM。关键点是要处理计数值的反转和溢出问题用int16_t类型的差值计算利用C语言整型回绕特性不需要额外判断方向。int16_t delta (int16_t)(encoder_count - last_count); last_count encoder_count; int16_t speed_rpm delta * 60 / 4000 * 1000; // 每1ms采样换算为rpm这个计算方式看着简单实际用下来比那种分方向加减的写法可靠得多。速度计算出来之后我做了个一阶低通滤波系数大概0.3可以滤掉编码器量化噪声引起的高频毛刺。速度环对噪声非常敏感如果不滤波调试的时候你会发现D项怎么调都不对电机高频嗡嗡响。2.2 位置式PID的C语言实现位置式PID的标准写法每个人都有自己的版本我把它写成结构体的形式方便参数整定和移植。关键点在于区分了PID计算和限幅逻辑限幅是在PID计算之前完成的避免输出饱和。typedef struct { float target; // 目标值 float feedback; // 反馈值 float kp; float ki; float kd; float integral; float last_error; float output; float max_output; float max_integral; } PID_TypeDef; void PID_Calculate(PID_TypeDef *pid) { float error pid-target - pid-feedback; pid-integral error; // 抗积分饱和限制积分项幅度 if (pid-integral pid-max_integral) pid-integral pid-max_integral; if (pid-integral -pid-max_integral) pid-integral -pid-max_integral; pid-output pid-kp * error pid-ki * pid-integral pid-kd * (error - pid-last_error); if (pid-output pid-max_output) pid-output pid-max_output; if (pid-output -pid-max_output) pid-output -pid-max_output; pid-last_error error; }2.3 双环串级的调用框架双环控制的灵魂在于嵌套调用的执行顺序。每一层中断里只做自己该做的事定时器6负责1ms时基在这个中断里执行速度环计算并更新PWM输出定时器7负责5ms时基在这个中断里执行位置环计算并更新速度环的目标值。// 定时器6中断1ms速度环 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim htim6) { read_encoder_speed(); speed_pid.feedback current_speed_rpm; PID_Calculate(speed_pid); set_motor_pwm(speed_pid.output); } } // 定时器7中断5ms位置环 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim htim7) { position_pid.feedback current_position; PID_Calculate(position_pid); speed_pid.target position_pid.output; } }这个结构确保内环始终优先执行外环的输出只作为内环的输入目标。你不需要在速度环里判断位置是否到位位置环自然会管这件事。代码逻辑清晰后期维护也方便。2.4 PWM极性匹配与刹车逻辑速度环输出有正有负但电机驱动板一般只能单向输出所以直接用一个方向的PWM是不行的。我这里用的方案是速度环输出为正时IN1给PWM、IN2拉低为负时反过来。方向切换时需要加死区延时一般5到10微秒防止上下桥臂直通。void set_motor_pwm(float output) { uint32_t pwm_value (uint32_t)(fabs(output) * PWM_PERIOD / 100.0f); if (output 0) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, pwm_value); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, 0); } else { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, pwm_value); } }还有一点很关键电机在使能前一定要保证输出为0并且要在主循环里加一个急停逻辑当位置跟踪误差超过某个阈值时直接封锁PWM输出让电机自由停车或刹车。这个保护逻辑平时用不上但一旦编码器线松了、或者位置环参数写错了它能帮你省下一块驱动板和一台电机。3. 硬件选型与HAL库工程配置实战3.1 STM32F407最小系统板的外设资源分配TIM1输出两路PWM互补通道驱动电机H桥20kHz频率。TIM3编码器接口模式读取AB相。TIM61ms中断速度环时基。TIM75ms中断位置环时基。USART2调试数据输出接VOFA上位机1kHz发送数据量约每秒1KB115200波特率足够。3.2 HAL库工程初始化常见隐藏问题HAL库上手简单但坑也不少尤其是定时器这部分。编码器接口模式必须设置为TIM_ENCODERMODE_TI12并且极性设置为上升沿和下降沿都计数否则编码器的4倍频就出不来。另外编码器模式下设置ARR要特别小心我一般把它设为65535让计数器自由跑靠int16_t截断来处理溢出。还有一个容易被忽略的坑HAL_TIM_IC_Start_IT和HAL_TIM_Encoder_Start这两个函数不能搞混。编码器模式只需要调用HAL_TIM_Encoder_Start不需要开启捕获中断否则会疯狂进入中断回调。我第一次写的时候就是把这两个搞混了导致系统卡死。HAL库的PWM初始化需要调用HAL_TIM_PWM_Start同时要注意配置死区时间。对于电机驱动用的H桥死区时间一般设置在1微秒左右STM32F407的TIM1死区寄存器配置公式需要对照参考手册去计算而不是直接填数值。// 死区时间配置以ns为单位换算 // DTG[7:0] 死区时间(us) / (1/fpclk1) - 1具体公式见参考手册 htim1.Init.DeadTime 72; // 约1usfPCLK172MHz3.3 电机和驱动板的选型经验直流无刷电机在有感模式下调试双环会容易得多推荐先用带霍尔传感器的电机或带增量式编码器的直流减速电机来跑通框架。我用的是带500线编码器的直流有刷减速电机减速比1比30这样速度和位置反馈信号非常干净适合验证算法等算法调明白了再换无刷电机也不迟。驱动板选型要看最大电流我是用一个10A的MOS驱动模块留足了余量。4. 调参实战从内环到外环的黄金顺序4.1 速度环的整定方法与关键指标双环调试的顺序是铁律先整定内环再整定外环。速度环拿到手上首先给速度目标值比如500RPM然后把I和D都设为0只加P从小到大慢慢加直到电机出现轻微的“嗡嗡”声但还没有持续振荡这个P值就是临界值最终取临界值的60%左右。然后加I让稳态误差降下来。I从0开始慢慢加你会看到速度响应曲线逐渐贴近目标值但I太大会引起低频振荡速度曲线呈S形往复。最后加DD主要是抑制超调但不能加太多不然高频噪声会被放大电机像卡带一样抖动。调试中我习惯用VOFA上位机看波形速度和目标值直接画在一个图里超调量和收敛时间一目了然。注意速度环的响应时间一般在50ms到200ms之间如果响应太慢检查是不是限幅值设太小了如果响应太快但超调大优先检查速度反馈滤波是否过度而不是一味调低P。4.2 位置环的整定难点尽量避免超调位置环整定和速度环有个本质区别位置环天然不允许超调因为没有谁会希望云台或机械臂冲过头再折返回来。所以位置环的P参数不能像速度环那样放得很激进I参数尤其要小心积分项很容易让位置冲过头然后反复修正看起来就像振荡。位置环我的经验是先把速度环调好后给位置环一个较小的目标例如转动90度从P开始试看曲线接近目标的方式是平滑逼近还是快速冲过去再回头。前者说明P合适后者说明P要减半。I参数只在有稳态误差时加但实际上用位置式PID加I之后位置稳态误差本来就不大所以我的位置环实际上I系数非常小。D参数在位置环里意义不大因为位置环的输入是位置误差的变化率而这个变化率本身已经包含了速度信息再加D容易造成系统刚度的过度增加。我最终的位置环是PD控制I设的很小实测效果比PID要好尤其是在低速停车阶段更平稳。4.3 常见调参误区与自检清单误区一上来就双环一起调。出现振荡你根本分不清是哪个环的问题。正确做法是把位置环的目标值设成常数单独给速度环阶跃信号调试。误区二速度环响应不够快就强行加大位置环P。位置环再大也没用因为内环跟踪不上外环的输出会被饱和限制住。误区三忽视限幅值的影响。位置环输出的限幅值决定了电机的最大速度这个值设太小电机运动会很慢且迟钝设太大位置环就失去了精细控制能力。5. 调试工具与上位机联调经验5.1 VOFA配置与数据上报策略我调试电机控制项目的标配是VOFA这个上位机免费、跨平台、协议简单。下位机通过串口把数据用CSV格式发出去VOFA的浮动协议直接解析。1kHz的数据量非常大窗口会刷得飞快我的策略是速度环数据通过串口DMA发送每10ms发送一次100Hz刷新率这样既保留波形细节又不会阻塞控制中断。uint8_t tx_buffer[64]; sprintf((char*)tx_buffer, %.2f,%.2f\n, target_speed, actual_speed); HAL_UART_Transmit_DMA(huart2, tx_buffer, strlen((char*)tx_buffer));用DMA发送而不是阻塞式发送这一点在控制中断里尤其重要。阻塞式发送会导致PWM更新延迟进而引发控制抖动。实测下来HAL_UART_Transmit_DMA在115200波特率下一个100字节的包大约占0.9ms的DMA时间但不会阻塞CPU控制中断完全不受影响。5.2 上位机下发指令与状态监控调试位置环时需要一个能灵活改变目标位置的通道。我用串口接收中断加简单的命令解析上位机发送P角度值\r\n下位机解析后把角度换算成编码器目标脉冲数然后写入位置环的目标变量。这样在VOFA里就可以直接通过发送指令来控制位置省去了每次都要重新编译烧录的麻烦。命令解析要放在主循环里做不要在串口中断里做数学运算和浮点计算。串口中断只负责把数据塞进环形缓冲区由主循环去解析这样可以避免长命令解析过程中其他中断被阻塞。5.3 实测效果与数据波形分析我把整个系统调好之后做了一组阶跃测试目标位置从0度到180度再回到0度。从VOFA的波形上可以明显看到三个阶段的运动过程加速段、匀速段、减速段。整个运动过程平滑没有过冲。速度环的响应曲线基本跟着位置环的输出走最大速度被限幅在800RPM加速度保持较低水平机械冲击很小。位置稳态误差在正负1个脉冲以内也就是不到0.1度。这个精度已经能够满足大多数非超高精度的应用了。如果还想进一步提高精度可以考虑上电流环把位置环和速度环串联到电流环上形成三闭环但这对于直流电机来说意义不大对无刷电机才需要。6. 工程迁移与扩展的几点心得这套HAL库源代码迁移到其他STM32系列时改动量主要在初始化部分比如时钟配置、引脚映射以及定时器外设的实例定义。只要底层接口对上PID控制逻辑和双环框架可以直接复用。如果你用的是STM32G4系列它还内置了运放和比较器做无刷电机的FOC控制会更顺手。另一个扩展方向是加电流环。对于真正的无刷电机电流环是最内层速度环给电流环下指令电流环控制力矩。位置环、速度环、电流环三环串级理论上位置环控制的是最终位置速度环控制的是运动速度电流环控制的是力矩输出。这套框架在STM32F3或者G4上跑FOC很实用只是需要额外加ADC采集和Clarke/Park变换的代码。代码工程我建议按模块拆分文件pid.c、encoder.c、motor.c、protocol.c每个模块固定接口方便以后在别的项目里复用。尤其是pid.c这个文件几乎可以原封不动地搬到任何控制项目里包括温度控制、气压控制、无人机姿态控制等只需要改一下target和feedback的赋值以及限幅范围。另外提醒一句调参时记得给所有PID参数设置一个安全的初始值写死在代码里不要一开始就用外部变量。否则一旦串口通信异常参数变成NaN或者大得离谱电机不是飞车就是过流保护折腾起来很烦。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进