简介:针对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,上下沿计数 |
| TIM1 | PWM输出 | 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/1800到1800/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周期会影响电流环的实时性。这一点在调参后期会变得很关键。
本文还有配套的精品资源,点击获取