news 2026/9/16 1:53:13

STM32-F4双闭环电机控制:位置环与电流环PID整定实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32-F4双闭环电机控制:位置环与电流环PID整定实战

简介:针对STM32-F4平台实现直流有刷电机位置环与电流环双闭环控制,采用位置式PID算法,基于C语言和HAL库编写,适合嵌入式开发者和电机控制方向学习者参考。资源为完整Keil工程,共554个文件、约20.46MB,核心源码包含158个h头文件和103个c源文件,涵盖TIM定时器、ADC电流采样、编码器位置读取、PWM输出等外设驱动,另含uvprojx工程配置、ICF链接脚本及编译中间文件,可直接编译下载调试。目前已有1104人学习使用。通过阅读代码可掌握双闭环控制的层级关系——电流环作为内环快速响应,位置环作为外环精确到位,并理解位置式PID的参数整定方法,以及HAL库在电机控制中的典型调用方式。工程目录结构清晰,适合在现有代码基础上进行电机控制功能二次开发,也适合作为毕业设计或竞赛项目的底层驱动参考。

1. 双闭环电机控制,先想清楚环为什么会抖

做位置控制时,单纯用一个位置环去推PWM,系统在到位后总会表现出两个让人头疼的问题:一是到位瞬间电流冲击大,电机“撞”过去再弹回来;二是带负载时位置静态误差压不下去,调大Kp又开始震荡。这背后的原因在于,单环系统把电机绕组电感、反电动势和负载扰动全部丢给了位置控制器去抵抗,而位置环的带宽又远跟不上电流的变化速度。用STM32-F4做双闭环,本质上是把“要多少力”和“给多少力”拆成两个环,位置环输出的是电流指令,电流环来保证实际电流快速跟随。这个项目面向的是用过HAL库但没做过闭环控制,以及做过速度环但没把位置环做稳的开发者,核心价值在于搞懂内环该多快、外环该怎么输出,而不是简单抄一份PID代码。

2. 位置式PID与双闭环结构:为什么这里不能用增量式

2.1 位置式PID的离散化写法

位置式PID直接对误差信号做比例、积分、微分运算,输出的是控制量的绝对值。离散化表达为:

/* * 位置式PID核心运算 * error : 当前周期误差 * integral : 误差积分累计值 * last_error : 上一周期误差 * dt : 控制周期,单位秒 * Kp, Ki, Kd : 三个增益系数 * 返回控制量绝对值 */ float pid_positional_calc(float error, float *integral, float *last_error, float dt) { *integral += error * dt; /* 积分项累积 */ float derivative = (error - *last_error) / dt; /* 微分项计算 */ *last_error = error; return Kp * error + Ki * (*integral) + Kd * derivative; }

这是一个教科书式的差分方程实现,实际工程中要在调用前对返回的控制量做限幅,防止积分项在电机堵转时无限累积。位置式PID的输出和当前执行器的绝对位置直接对应,代码里能清楚看到每一项对最终输出的贡献,排查问题时可以分别打印三个分量的数值。

2.2 增量式PID为什么不适用于位置环

增量式PID输出的是控制量的增量,公式为 Δu[k]=u[k]−u[k−1],它天然适合步进电机这类需要“每次走多少”的执行器。但对于直流有刷电机,PWM占空比是一个绝对值,增量式PID如果不对输出做累积和限幅,一旦通讯中断或者程序跑飞,执行器会停在原值而不是归零。位置环的控制目标是明确的绝对位置,位置式PID的输出限幅可以直接约束电流指令范围,这种可预测性在调试阶段非常重要。

2.3 双闭环的串级结构

双闭环的本质是串级控制,内环电流环负责快速消除电流误差,外环位置环负责给出电流指令。结构如下:

位置指令 -> 位置环PID -> 电流指令 -> 电流环PID -> PWM占空比 -> 电机 -> 编码器 ^ | |-------------------------

电流环的带宽一般为1-2kHz,位置环带宽通常为20-50Hz。这个带宽差不是随意选的,电流环必须比位置环快一个数量级以上,外环的输出变化才有意义。如果位置环输出剧烈波动而电流环跟不上,整个系统会表现为“指令发出去但力没上来”的延迟感。

2.4 为什么电流环能抗扰动

直流有刷电机的电枢回路方程是电压平衡方程,电流变化率受绕组电感和反电动势影响。当负载突变时,反电动势会随之改变,如果不加电流环,位置环必须额外承担这部分扰动。电流环的存在让实际电流紧密跟随指令值,相当于把电机变成了一个“电流-力矩”转换器,位置环只需要考虑运动学关系,大大简化了调参复杂度。

3. STM32-F4 HAL库初始化:编码器、电流采样、PWM输出

3.1 定时器复用:编码器模式与PWM输出

STM32-F4的高级定时器TIM1和TIM8支持互补PWM输出,带死区插入,适合驱动全桥电路。普通定时器TIM2-TIM5支持编码器接口模式。我一般把TIM3配成编码器模式,TIM1的CH1/CH2配成互补PWM输出,这样配置的核心是让编码器反馈和PWM驱动在物理上互不干扰。

外设功能关键配置
TIM3正交编码器Encoder Mode TI1+TI2,上下沿计数
TIM1PWM输出CH1/CH2互补输出,PWM频率建议20kHz
ADC1电流采样定时器触发,三通道扫描,DMA循环传输

编码器模式中,TIM3的计数方向由A/B相信号相位决定,Counter Mode要设置为Encoder Mode TI1 and TI2,这样4倍频计数才能拿到编码器的全部分辨率。

3.2 电流采样的关键细节

直流有刷电机的电流采样常用低边采样电阻加运放放大,STM32-F4的ADC用定时器触发是为了让采样时刻对齐PWM的开关中点,避免开关噪声干扰。配置为PWM中心对齐模式时,计数器的上溢和下溢时刻正好对应PWM的中间位置。

/* ADC1 初始化:由TIM1触发,DMA循环模式 */ hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T1_CC2; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ContinuousConvMode = DISABLE; /* 由硬件触发启停 */ hadc1.Init.DMAContinuousRequests = ENABLE; /* DMA循环,持续搬运 */ HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 3);

这几行配置表示ADC每次转换由TIM1的捕获比较事件触发,触发沿为上升沿。不使用连续转换模式,因为硬件触发已经能保证采样节奏,连续转换反而会让ADC始终在跑,白白增加功耗。DMAContinuousRequests置为ENABLE后,DMA会循环地把三个通道的转换结果搬到数组里,CPU不需要进中断去读。

3.3 PWM频率选择

直流有刷电机PWM频率的选取要同时照顾电流纹波、驱动器和噪音三方面。频率太低电流纹波大,电机会有啸叫;频率太高驱动器开关损耗上升。20kHz是业界常用值,既超出人耳听觉范围,又不会让MOSFET发热到需要额外散热。配置TIM1的Pulse值要配合PID输出限幅使用,我一般把占空比限幅设为900/18001800/1800,留出裕量让电流环有调节空间。

/* * PWM频率和周期配置 * 主频168MHz,预分频0,自动重装值8400 * 输出频率 = 168MHz / (8400 + 1) = 20kHz * 对应PWM分辨率1200级 */ htim1.Init.Period = 8400; htim1.Init.Prescaler = 0; htim1.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED1; HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);

中心对齐模式下,计数器先加后减,PWM脉冲在周期中间对齐。这种模式的好处是ADC触发点稳定在PWM中点,电流采样时刻不受占空比变化影响,采样结果能真实反映平均电流。

4. 双闭环控制实现:中断优先级与控制周期

4.1 控制时序设计

双闭环核心是确定两个环的执行时序。我的做法是把电流环放在PWM周期中断里执行(2kHz),位置环通过计数器分频到100Hz执行。这样电流环始终以最高优先级抢占CPU,位置环在两次电流环之间穿插执行,不会阻塞电流环的实时性。

/* * TIM1更新中断回调:电流环核心执行函数 * 每200us执行一次,即控制频率5kHz */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { /* 读取编码器位置 */ int32_t encoder_raw = __HAL_TIM_GET_COUNTER(&htim3); /* 读取ADC电流采样值,计算实际电流 */ float current_actual = (adc_buf[0] - CURRENT_OFFSET) * CURRENT_SCALE; /* 电流误差 = 电流指令 - 实际电流 */ float current_error = current_command - current_actual; /* 电流环PI计算,输出PWM占空比 */ float duty = pi_current_calc(current_error); /* 限制输出范围,更新PWM比较寄存器 */ duty = clamp(duty, DUTY_MIN, DUTY_MAX); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint32_t)duty); /* 每50次电流环执行一次位置环,即100Hz */ if (++position_divider >= 50) { position_divider = 0; float position_actual = (float)encoder_raw * ENCODER_TO_RAD; float position_error = target_position - position_actual; current_command = pi_position_calc(position_error); current_command = clamp(current_command, I_CMD_MIN, I_CMD_MAX); } } }

注意这里没有把位置环单独拆成函数,而是放在同一个中断里分频执行,避免两个中断互相打断时出现电流环延迟。电流环调用顺序必须在前,位置环计算结果在下一周期才生效,这本身就是一拍的延迟,如果反过来会多出一整拍。

4.2 电流PI控制器实现

电流环通常只用PI控制,不加微分项。电流采样信号本身包含开关纹波,微分项会把噪声放大成高频抖动。如果电机在低速时出现电流震荡,先查PI参数和采样滤波,不要急着加Kd。

/* * 电流环PI控制器 * error : 电流指令与反馈之差,单位安培 * 返回 : 占空比,范围0-8400 */ float pi_current_calc(float error) { static float integral; /* 积分分离:误差超过限幅值时停止积分 */ if (error < I_SATURATION && error > -I_SATURATION) { integral += error * CURRENT_LOOP_DT; } float duty = KP_CURRENT * error + KI_CURRENT * integral; /* 输出限幅,同时做积分清零保护 */ if (duty > DUTY_MAX) { duty = DUTY_MAX; integral = 0; /* 防止积分饱和 */ } if (duty < DUTY_MIN) { duty = DUTY_MIN; integral = 0; } return duty; }

I_SATURATION是积分分离阈值,单位是安培。当位置环给出的电流指令超出电流环的调节能力时,比如电机堵转或者加速度极大,误差会超过这个阈值,积分项立刻停止累积,避免PID在恢复后出现大超调。这比单纯对积分输出限幅更主动。

4.3 位置环PI控制器与限幅

/* * 位置环PI控制器 * error : 位置误差,单位弧度 * 返回 : 电流指令,单位安培 */ float pi_position_calc(float error) { static float integral; /* 误差大于0.5弧度时不积分,让P项先快速收敛 */ if (error < 0.5f && error > -0.5f) { integral += error * POSITION_LOOP_DT; } float current_cmd = KP_POSITION * error + KI_POSITION * integral; /* 电流指令限幅,避免位置环瞬间要太大电流 */ if (current_cmd > I_CMD_MAX) current_cmd = I_CMD_MAX; if (current_cmd < I_CMD_MIN) current_cmd = I_CMD_MIN; return current_cmd; }

位置环的限幅范围决定了电机的最大加速度,改这个值比改PID参数更直观地影响系统快速性。我一般把I_CMD_MAX设为额定电流的1.5倍,让位置环能短时过载加速,但不允许长时间超电流运行。

4.4 中断优先级配置

/* 定时器配置:占优先级0,子优先级0,最高优先级 */ __HAL_RCC_TIM1_CLK_ENABLE(); HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 0, 0); HAL_NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn); /* ADC DMA中断:占优先级2,位置环运行期间不被电流环打断 */ HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 2, 0); HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);

中断优先级分配有一条原则:控制频率越高的优先级越高。电流环在最高优先级保证不丢周期,DMA中断在低一级但也要能抢占主循环,位置环放在电流环回调里分频执行自然就得到了合适的优先级。主循环里只处理参数下发、状态上报这类非实时任务。

5. 参数整定顺序与波形验证技巧

5.1 先内环后外环的整定流程

双闭环调试有一条铁律:先整定内环,再整定外环。内环参数不准,外环调的再好也是白费。

电流环整定用阶跃响应法:屏蔽位置环,直接给电流环一个固定指令,观察实际电流的响应波形。Kp从小往大加,电流波形会经历“缓缓爬升→快速上升带小超调→震荡发散”三个阶段。取超调量10%左右那个Kp,然后加Ki消除稳态误差。这里有一个关键判断点:电流指令1A,实际电流如果超调20%以内且调节时间小于5ms,电流环就合格了。

位置环整定先给一个预设好的电流环参数,然后从P项开始调。位置响应如果出现震荡,优先检查是不是电流指令被限幅了,因为限幅会改变系统的等效增益,前一步稳定在限幅边缘的参数在下一次可能就不成立。

5.2 常见坑与排查表

现象可能原因排查步骤
电机静止时有嗡鸣声PWM频率低或死区不足先查PWM频率是否为20kHz,再用示波器看PWM波形死区
到位后来回抖动位置环Kp过大导致极限环震荡先减小Kp,确认电流指令输出波形是否规律脉冲
电流环响应慢ADC采样点不对齐PWM中点检查采样触发配置是否来自TIM1,采样时刻是否在开关切换瞬间
编码器计数反向A/B相接反了换相或者在初始化里交换TIM的CH1/CH2输入映射
低速时爬行电流采样有偏置万用表量运放输出是否在中点,检查HAL_ADC校准是否执行

5.3 用示波器验证双闭环的带宽匹配

最终验证双闭环效果有一个简单方法:把示波器两个探头接在不同物理量上,CH1接电流采样输出,CH2接编码器Z相脉冲。让位置环跟随一个正弦指令变化,频率从1Hz慢慢往上加,观察电流响应是否始终领先于位置变化,两者相位差接近90度说明系统带宽匹配良好。如果电流环滞后于位置环,整个系统会出现“位置在等电流”的无力感,此时需要返回去加快电流环响应。

/* 调试参数打印,使用ITM协议 */ void debug_print_position(float pos_actual, float current_actual) { /* ITM_PORT0 为周期端口,配合SWO输出 */ ITM_SendChar((uint8_t)(pos_actual * 100.0f)); ITM_SendChar((uint8_t)(current_actual * 100.0f)); }

位置和电流的换算比例在打印时放大10-100倍,避免整型截断丢失细节。用SWO输出避免了调试过程中额外占用串口,因为控制逻辑都在中断里跑,调试通道再占用CPU周期会影响电流环的实时性。这一点在调参后期会变得很关键。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:52:44

M1 Mac上为CLion配置GCC编译器:从Homebrew到CMake实战指南

新买的M1 Mac&#xff0c;装了CLion&#xff0c;随手建了个C项目&#xff0c;默认工具链跑得飞快。但等你想把Linux上的老工程拿过来编译&#xff0c;或者想交叉编译个嵌入式固件&#xff0c;问题就来了&#xff1a;CLion里那个叫gcc的东西&#xff0c;其实不是GCC&#xff0c;…

作者头像 李华
网站建设 2026/9/16 1:50:54

8G显存跑AI视频生成:LTX-2.3 int8量化实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:49:42

GitOps在Kubernetes配置管理中的实践与优化

1. GitOps与Kubernetes的配置管理困局去年我们团队在迁移微服务架构到Kubernetes时&#xff0c;曾遭遇过典型的配置版本混乱问题。某个深夜的紧急回滚中&#xff0c;运维人员误用了两周前的旧版ConfigMap&#xff0c;导致生产环境服务大面积异常。这种配置与代码版本脱节的情况…

作者头像 李华
网站建设 2026/9/16 1:49:01

非LVM分区扩容实战:growpart与xfs_growfs使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:48:54

CAD局部放大图制作全攻略:模型空间与布局视口详解

做工程的人&#xff0c;大概都经历过这种场面&#xff1a;图纸上密密麻麻全是尺寸&#xff0c;你拿着卷尺对着屏幕眯着眼核了半天&#xff0c;到了现场还是发现钢筋排布和图纸差了那么一截。问题不一定出在施工队&#xff0c;很多时候根源就在图纸本身——比例压得太小、信息挤…

作者头像 李华
网站建设 2026/9/16 1:48:51

长江存储PC550:首款PCIe 5.0低功耗消费级SSD

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华