简介:面向嵌入式开发者与电机控制工程师的STM32F4直流有刷电机三闭环控制源码包,聚焦位置环、速度环、电流环的级联设计与位置式PID实现,采用C语言和HAL库编写,可直接在STM32F4平台运行或移植,适合正在学习电机控制或需要工程参考的进阶用户。压缩包内共有555个文件,以h头文件、c源文件为主,辅以o目标文件、s启动文件、icf链接配置文件等,整体约20.51MB,目录结构清晰、工程链完整。已有401人学习下载。源码涵盖外设初始化、PID控制模块、三闭环控制逻辑、编码器中断处理等关键代码,并涉及GPIO、定时器、ADC等配置思路,对理解电流采样、速度反馈与位置跟踪的级联控制很有帮助。通过阅读和调试该工程,可掌握PID参数整定方法,并在此基础上进一步扩展过流保护、过热保护等实用功能。
1. 三闭环控制?先拆开看位置环、速度环、电流环谁在管什么事
直流有刷电机,空载转得好好的,一加负载转速就掉、位置就偏,调一个单环PID怎么调都别扭——这是很多人从单环迈向三闭环的第一个痛点。标题里的「STM32-F4 直流有刷电机-位置环速度环电流环 三闭环控制」本质上就是把三个独立的PID级联起来:内环管电流(也就是电磁转矩),中间环管速度,外环管最终位置,每一级都在给下一级当执行器。这套架构跑起来,电机才算摸到伺服控制的门槛。下面用C语言和HAL库把它拆开讲,适合刚把F4跑起来想做高性能位置控制的开发者,也适合想系统理解级联PID控制闭环关系的读者。
2. 级联PID闭环结构:为什么内环要快、外环要慢,位置式公式怎么落
2.1 三个环路的控制对象、反馈元件与执行周期
电流环的控制对象是电枢回路,反馈来自电流采样:采样电阻或电流传感器,经运放放大后进ADC。它要抵消反电动势和负载扰动对电磁转矩的冲击,让实际电流快速跟随目标电流。速度环的控制对象是电机加负载的机械系统,反馈来自编码器,STM32的定时器编码器模式直接读A/B相脉冲,换算成转速。位置环反馈同样来自编码器,但用的是累计脉冲数而不是频率。
三环是串联结构:位置环输出目标速度,速度环输出目标电流,电流环输出PWM占空比。这种结构就是常说的级联PID控制,它对时间尺度有一个硬约束:内环必须比外环快。电流环执行周期一般在1kHz到20kHz之间,速度环取1kHz以内的时基足够,位置环几十到几百Hz就够。原因很简单:位置环给一个速度指令,速度环要在自己的周期内尽量追上,电流环又要在更短周期内把电磁转矩顶上,这个时间差是级联稳定性的基础。如果在同一个频率跑三个环,外环每拍都在改目标,内环永远追不上,系统表现出来的就是慢性的自激振荡。
2.2 位置式PID离散公式与增量式的取舍
位置式PID的离散公式是:
u(k) = Kpe(k) + Ki∑e(n) + Kd*[e(k) - e(k-1)]
u(k)是当前拍的控制器输出,在电流环里对应PWM比较寄存器值,在速度环里对应目标电流,在位置环里对应目标速度。三个环路语义不同,但PID计算函数完全可以复用,只需要把输出限幅和积分限幅改成各自的量纲。
位置式PID与增量式PID算法的本质差别在于:增量式只输出变化量,天然没有积分累积项,主控复位或切换模式时冲击小,但被限幅卡住时误差会一直累积,出现积分饱和;位置式直接累加误差,积分饱和问题必须靠限幅和抗积分饱和逻辑处理。写代码时我习惯在结构体里同时预留积分累积量、上次误差、输出上限、积分上限,把限幅当成PID控制器的一部分,而不是留给调用方做。这也是标题点名位置式的直接原因。
表格 2-1:三环路的反馈信号与PID输出语义
| 环路 | 反馈来源 | PID输入 | PID输出 | 典型执行周期 |
|---|---|---|---|---|
| 电流环 | ADC采样电枢电流 | 目标电流与实测电流偏差 | PWM占空比 | 100µs(10kHz) |
| 速度环 | 编码器脉冲频率 | 目标速度与实测速度偏差 | 目标电流 | 1ms(1kHz) |
| 位置环 | 编码器累计脉冲 | 目标位置与实际位置偏差 | 目标速度 | 5~20ms(50~200Hz) |
周期取值不是拍脑袋定的。电流环频率受ADC采样时间和PWM定时器影响,F4主频168MHz时开到10kHz没有问题;速度环更新周期要能覆盖机械谐振频率,电流环带宽则必须比速度环高一个数量级,否则外环命令电流、内环还在振荡,三个环就互相打架。
2.3 HAL库写PID的性能边界
有5年以上工程经验的人会问:HAL库函数调用有开销,放在10kHz电流环里行不行?我的结论是,PID运算放在中断回调里、查询式API换成中断和DMA驱动的接口,10kHz完全撑得住。F4内核做整数乘加只要几个周期,真正的开销在ADC转换和PWM寄存器更新,这两处HAL库都有对应的事件回调,比如HAL_ADC_ConvCpltCallback、HAL_TIM_PWM_PulseFinishedCallback,把PID塞进回调,主循环只跑状态机,性能不吃亏。想压到50kHz以上才需要寄存器直操和专用定时器,那是另一个话题。
3. 用STM32CubeMX生成最小工程:PWM通道、编码器模式、ADC采样的配置要点
3.1 外设选择与CubeMX里的关键设置
F4做有刷电机三闭环,外设需求是固定的:一路高级定时器TIM1或TIM8输出PWM,驱动H桥;一路普通定时器开编码器模式,接电机编码器的A/B相;一路ADC采电流,采样电阻加运放或电流传感器;再加一个串口打印调试。TIM1的PWM用CH1和CH1N互补输出,由死区电路保证上下管不同时导通;CH2、CH3留作普通IO或空闲。
CubeMX里最容易配错的是编码器模式。TIM4的Encoder Mode选「TI1 and TI2」,也就是四倍频,位置分辨率最高;还要把计数器设为循环模式(overflow自动回绕),这样正反转越过零点时不会把计数方向搞错。PWM的Pulse初值必须设成0,否则上电瞬间电机就会全速转。电流采样如果想让ADC在PWM周期的中点触发,可以在TIM1的TRGO上配置ADC触发源,这样采到的电流能避开开关边沿毛刺,这是H桥电流采样的通用做法。
3.2 外设启动顺序与HAL库核心API
CubeMX生成代码后,在main函数里补充外设启动。下面这段是基于HAL库的启动骨架,我一般把采样和编码器先启动,最后才放开PWM输出:
uint16_t adc_buf[8]; /* DMA循环缓冲区 */ volatile float i_phase = 0.0f; /* 实测电流,在ADC回调更新 */ int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); /* PWM输出 */ MX_TIM4_Init(); /* 编码器 */ MX_ADC1_Init(); /* 电流采样 */ MX_USART2_Init(); /* 调试串口 */ /* 启动顺序:先反馈,后驱动 */ HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 8); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); while (1) { /* 速度环和位置环在这里跑 */ } }HAL_TIM_Encoder_Start只管让编码器定时器开始计数,方向由A/B相相位关系由硬件决定,软件不需要判断。要得到速度和位置,需要周期性读取TIM4->CNT,与上次读到的值做差就是速度,把差值累加就是位置。HAL_TIMEx_PWMN_Start启动的是互补通道,H桥下管靠它驱动,漏掉这一步电机只会上管导通,绕组根本不吃电。
死区参数在CubeMX的Break and Dead Time配置页里设置。死区设太大占空比有效范围变小,设太小开关瞬间上下管直通烧管。先在功率管数据手册找建议值,上电后用示波器看Vgs波形,确认两路PWM之间有明确间隔后再往上加电流。
3.3 ADC采样:零偏标定与滤波相位
电流反馈是电流环的输入,进PID前先做两件事。标零偏:电机静止时连续采100次求平均,记为offset_zero,实测电流等于(adc_value - offset_zero)乘以系数scale,scale由采样电阻阻值和运放增益决定。第二件是检查反馈方向:给一个固定占空比,电流随占空比增加而增加才是正确的。方向反了,PID会直接朝饱和方向冲,表现是Kp越大越振荡,而且换向永远修不回来。
滤波上按需选,ADC配合DMA连续采集多个点做滑动平均即可,但要注意滤波器相位滞后。负载惯量越大滞后越危险,速度环容易因此高频抖动。我一般只做一阶低通,截止频率至少比电流环带宽高5倍,优先保电流环的实时性。
4. 位置式PID的C语言实现与三环调用顺序:结构体、限幅、抗积分饱和
4.1 可复用的PID结构体与核心计算函数
位置式PID控制器不关心被控对象,一套结构体能同时跑三个环。下面是工程里常用的结构体定义,每个字段都有明确量纲含义:
typedef struct { float Kp; /* 比例系数 */ float Ki; /* 积分系数 */ float Kd; /* 微分系数 */ float ref; /* 目标值 */ float fbk; /* 反馈值 */ float err; /* 当前误差 */ float err_last; /* 上一拍误差 */ float integral; /* 积分累积项 */ float out; /* 控制器输出 */ float out_max; /* 输出限幅 */ float int_max; /* 积分限幅 */ float d_filter; /* 微分低通系数 0~1 */ float d_term_last; /* 上次微分项 */ } PID_TypeDef;核心函数就是标准位置式公式套上积分限幅和输出限幅:
float PID_Calc(PID_TypeDef *pid, float ref, float fbk) { pid->ref = ref; pid->fbk = fbk; pid->err = ref - fbk; /* 积分累积,单独限幅防止积分饱和 */ pid->integral += pid->Ki * pid->err; if (pid->integral > pid->int_max) pid->integral = pid->int_max; if (pid->integral < -pid->int_max) pid->integral = -pid->int_max; /* 微分项带一阶低通,抑制编码器和ADC的高频噪声 */ float d_raw = pid->Kd * (pid->err - pid->err_last); float d_term = d_raw * pid->d_filter + pid->d_term_last * (1.0f - pid->d_filter); pid->out = pid->Kp * pid->err + pid->integral + d_term; if (pid->out > pid->out_max) pid->out = pid->out_max; if (pid->out < -pid->out_max) pid->out = -pid->out_max; pid->err_last = pid->err; pid->d_term_last = d_term; return pid->out; }有三处和教科书公式不一样。第一,积分限幅int_max单独存在,避免误差大时积分项先冲到饱和,导致反向调整时还要等积分慢慢退回来。位置环误差100个脉冲,积分按Ki每拍加0.1,不加int_max它会一直涨到输出被out_max卡住,最终表现就是电机停在半路,要好几秒才慢慢回正。第二,微分项带低通,d_filter取0.8左右能明显减小低速时的电流高频抖动,代价是一点点相位滞后。第三,输出限幅做成对称值,电机要正反转,单向限幅会让换向动态变差。
4.2 三环的调用节奏:中断、定时器、主循环怎么分工
级联PID控制的关键不在PID公式,在三环的调用频率。我的做法是:电流环放在ADC转换完成中断里,由PWM定时器触发ADC,10kHz左右更新;速度环放在1ms定时器中断里,读编码器增量、换算转速、算速度PID;位置环放到主循环或10ms定时器,读累计脉冲、算位置PID。
volatile float current_ref = 0.0f; /* 速度环输出的电流目标 */ volatile float speed_ref = 0.0f; /* 位置环输出的速度目标 */ volatile int32_t enc_cnt = 0; /* 编码器累计位置 */ /* ADC转换完成回调:电流环,约10kHz */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { uint16_t raw = adc_buf[last_index]; /* 当前拍采样值,last_index由DMA半满/全满中断维护 */ float i_fbk = (float)raw * current_scale; /* 标定后的电流 */ uint32_t pwm = (uint32_t)PID_Calc(&pid_current, current_ref, i_fbk); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pwm); } /* 1ms定时器中断:速度环 */ void Speed_ISR(void) { int32_t cnt = (int32_t)TIM4->CNT; int32_t delta = cnt - last_cnt; last_cnt = cnt; enc_cnt += delta; /* 位置就是脉冲累加 */ /* 四倍频后每转PPR*4个脉冲,1ms采样一次: rpm = 脉冲增量 * 1000(ms转s) * 60(s转min) / (PPR*4) */ float rpm = (float)delta * 60000.0f / (float)(PPR * 4); current_ref = PID_Calc(&pid_speed, speed_ref, rpm); if (current_ref > MAX_CURRENT) current_ref = MAX_CURRENT; if (current_ref < -MAX_CURRENT) current_ref = -MAX_CURRENT; } /* 主循环或10ms任务:位置环 */ void Position_Update(void) { speed_ref = PID_Calc(&pid_position, pos_ref, enc_cnt); if (speed_ref > MAX_SPEED_RPM) speed_ref = MAX_SPEED_RPM; if (speed_ref < -MAX_SPEED_RPM) speed_ref = -MAX_SPEED_RPM; }三个关键点。第一,last_index要和DMA循环采样的缓冲区下标对齐,工程上常用DMA半满/全满中断做ping-pong切换,不要在回调里读正在采样的那个字。第二,速度环公式里的60000.0f来自三处换算:编码器模式四倍频后每转计数PPR*4,中断周期1ms要乘1000变成秒,再乘60变成每分钟转速,方向则由delta的正负自动确定。第三,位置环输出必须限幅在电机额定速度以内,否则位置误差大时,速度环会得到一个大到电流环跟不上的目标速度,系统在极限状态来回冲。
环间带宽的经验比例是:速度环带宽约为电流环的1/5到1/10,位置环比速度环再低3到5倍。这个比例不是硬约束,但调试顺序是死的:先把电流环比稳,再闭合速度环,最后才碰位置环,跳步会让故障的归属变得模糊。
4.3 位置式的常见误用:积分饱和、微分冲击与符号翻转
三闭环项目最常翻车在热身阶段。电机静止时位置误差很大,积分项先冲满,输出一直是out_max,电机全电流往前冲,过冲后又要等积分排空才能反向,这就是积分饱和的经典表现,也是压力调节PID参数经验值里大家反复提的一点。抗饱和除了int_max,还有个容易遗漏的边界:输出被out_max限幅时,积分项不该继续累积。严格的实现要在限幅分支里冻结积分,我工程里直接把int_max设得比out_max对应的小,保证输出到顶时积分还有余量,简单且现场好调。
微分冲击和启动方式相关。Ramp斜坡没做时,位置环目标从0直接跳到1000,第一拍误差突变,微分项输出一个巨大的负值,电机会猛打方向。d_filter低通能压住一部分,配合目标值斜坡发生器才能根治。符号翻转是最隐蔽的一类:编码器方向接反或电流反馈极性反了,PID无论怎么调都自激振荡,判据是把反馈线反接,振荡消失就说明是方向问题而不是参数问题。
5. 三闭环参数整定顺序与波形判读:先内环后外环,临界比例法落到代码
5.1 电流环整定:只调Kp找临界振荡,再加Ki消静差
电流环被控对象是电枢回路,电气时间常数远小于机械时间常数,响应很快。整定电流环时先把速度环和位置环旁路,速度PID输出直接给固定值;电流环的Ki和Kd置0,只留下Kp从0开始慢慢加大。用调试器或串口持续观察实测电流波形,直到电流出现等幅振荡,记下这一拍的Kp,再乘0.6作为实际Kp,这就是临界比例法,也是Ziegler-Nichols法的工程简化版。之后加Ki消除静差,静差的表现是给定100mA稳定在95mA,Ki从小到大加,加到稳态误差消失且不出现低频慢漂为止。
电流环Ki过大时PWM占空比会周期性摆动,电磁转矩在反复横扫,机械系统跟着抖。如果电流波形不是贴紧目标值的平滑线,而是毛刺很多的锯齿,优先查ADC采样窗口和滤波截止频率,不急着加Kp。
5.2 速度环整定:位置环开路,观察阶跃响应的过冲与收敛
速度环整定要先把位置环的输出断开,直接给速度环一个恒定的目标转速。速度环Kp、Ki从零开始调整,方法同样是临界比例法。速度环反馈来自编码器,低速时脉冲间隔长,速度计算有固有滞后,Kp开太大会在低速段出现「咔哒咔哒」的抖动。经验参数的起步值可以这样估:Kp的量纲是电流每rpm,从允许最大电流除以额定转速的1/10起跳,比如允许10A、额定3000rpm,Kp就从0.033往上调。
加Ki观察恒速下的稳态误差,带载后转速静差最明显。合格的阶跃响应是:转速快速爬升,出现10%~30%过冲,1秒内收敛,没有持续振荡。转速在目标附近缓慢蠕动,说明积分项和负载扰动在拉锯,把Ki减半再观察。
表格 5-1:速度环阶跃响应波形判读
| 现象 | 原因 | 调整方向 |
|---|---|---|
| 过冲大、回摆两次以上 | Kp偏大或积分相位滞后 | Kp减半,Ki适当降低 |
| 稳态偏差长期不消失 | Ki偏小 | Ki增加50%,观察5秒 |
| 低速区间抖动 | 速度环周期过长或Kp过大 | 缩短速度环周期,Kp减到抖动消失 |
| 带载转速跌落明显 | 电流环带宽不够 | 回到电流环加大Kp,确认内环先快起来 |
5.3 位置环整定:配合Ramp斜坡调过冲,最后处理静止保持
位置环反馈是编码器累计脉冲,没有速度环那种量化噪声,Kp和Ki可以开得激进,但目标值常是大阶跃,必须配合Ramp斜坡发生器一起调。先给一个小阶跃,比如转5圈,观察运动曲线过冲情况和是否在目标点反复穿越。位置环Ki不能大,积分累积会让电机到达目标后继续顶着,表现是目标位置附近持续小幅震荡,电机发出「嗡嗡」声。
三环全闭合后,完整运动过程应该是:加速段电流环快速响应、速度环平稳爬升,接近目标时速度环平滑减速,位置环用很小的积分项把位置钉在目标点。静止时电流环会持续给一个很小的占空比抵消摩擦力和重力,这是正常的;如果看到电流恒定在反向饱和值附近弹不回来,说明位置环积分项或误差死区设置有问题,要去查这两处。
6. 三闭环的启动时序、堵转保护与Ramp斜坡:最后三道保险
6.1 使能顺序反了会烧H桥
三闭环的使能顺序不能随便写。ADC还没完成第一次转换就启动PWM,电流环会用残留值输出一个随机占空比,此时H桥上下管一旦直通,烧的是功率管。我的固定顺序是:先启动编码器和ADC,等50ms确认位置和电流反馈有效,再启动PWM主通道和互补通道,最后置速度环和位置环的使能标记。反过来的顺序在调试器里看不出来,只有上电瞬间的电流尖峰能说明问题。
6.2 堵转检测的两个条件缺一不可
堵转是位置控制里最容易被忽略的保护场景:位置环还在让电机往目标走,机械结构被卡住,速度环持续输出满电流命令,绕组发热冒烟。检测逻辑用两个条件同时成立:实测电流持续超过额定电流60%,且编码器速度持续低于阈值的时长超过500ms。此时清零所有环路输出并锁存故障标志,只有手动复位才能重新使能。不要只用速度为零一个条件判断堵转,启动瞬间速度本来就为零,误触发放飞了整个机构的复位流程。
#define I_OVERLOAD_LEVEL 0.6f /* 额定电流的60% */ #define SPEED_STALL_RPM 50.0f /* 堵转速度阈值 */ void Fault_Check(void) /* 在1ms速度环中断里调用 */ { static uint32_t stall_time = 0; if (fabsf(i_phase) > I_OVERLOAD_LEVEL && fabsf(rpm) < SPEED_STALL_RPM) { if (++stall_time >= 500) /* 500ms 去抖 */ { __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0); fault_flag = 1; } } else { stall_time = 0; } }这块逻辑放在速度环ISR里调用。最容易漏的是fabsf:反向堵转时电流和速度都是负的,丢了绝对值函数,反向堵转永远检测不到。
6.3 Ramp斜坡:给速度环一个梯形的目标
位置环输出直接当速度环目标,从0突变到3000rpm会让速度环瞬间饱和,电流环跟着打满,机械冲击很大。我习惯在位置环输出和速度环输入之间插一段斜坡限幅逻辑,把目标速度的变化率限制在可配置范围,比如500rpm/100ms。实现就是一个增量限幅函数,每拍往目标方向最多移动max_step,放在位置环更新之后、速度环读取目标之前。这个斜坡同时解决了位置环大阶跃时的微分冲击,和堵转保护、启动顺序放在一起,正好覆盖了三闭环项目最容易出问题的三个时间点:上电时、卡死时、换向时。把这一整套配置和代码放进F4工程,再回头看增量式PID算法、寄存器直操和FOC,就有了对照的坐标系。
本文还有配套的精品资源,点击获取