
简介面向STM32F407及STM32F4系列单片机开发者这份压缩包是一套直流有刷电机电流环PID控制的完整C语言工程实现覆盖从底层驱动到上层应用的关键环节重点解决电机调速过程中电流闭环调节与运行稳定性问题。包内含242个文件核心为124个头文件和110个源文件覆盖寄存器定义、外设驱动、PID运算与主控逻辑另附Keil工程文件、hex/bin固件和调试配置信息便于直接编译烧写及逐步调试整个压缩包体积仅1.66MB。代码采用模块化组织包含ADC电流采样、PWM输出生成、PID参数整定以及串口交互等模块既能在STM32F407开发板上直接运行也可作为STM32F4系列其他芯片的移植参考适合嵌入式爱好者深入理解有刷电机闭环控制。已有405人学习下载对于希望从零上手电机驱动与PID算法的开发者来说是兼具学习价值与工程参考意义的资料。1. 电流环PID直流有刷电机控制里第一层也是最后一道坎做电机控制的人都知道一个反直觉的事实电压给对了电流不一定对电流对了力矩才真正可控。直流有刷电机在电气上就是一个R-L串联回路真正决定力矩输出的不是PWM占空比而是流过电枢的电流。STM32F407这类Cortex-M4单片机做电流环PID硬件底子刚好够用——12位ADC采集相电流高级定时器TIM1/TIM8输出带死区的互补PWM168MHz主频配合FPU跑浮点PID毫不吃力。相比F103F407最大的优势是ADC和PWM的同步触发链路更完整电流环可以做进20kHz的控制周期而不丢采样点。本篇文章以ST官方生态为主线从采样链路、PWM驱动到离散PI实现和参数整定全部过一遍适合正在调电机发热、力矩抖动或响应迟钝的嵌入式工程师。2. 电流采样链路先把“测准”这件事做对再谈PID电流环PI调不好六成问题不在PID而在采样链路。STM32F407内部的12位ADC在168MHz主频下最短采样时间能做到约0.42us但采样时刻不对带宽再高也是白搭。电流采样的核心矛盾在于PWM开关会在母线电压跳变处注入巨大的共模干扰采样点如果落在开关沿附近读到的电流值既不稳定也不代表平均值。所以第一步不是写PID而是把采样点钉在PWM开通区间的中点。2.1 采样拓扑选型与增益计算常见的电流采样拓扑有三种低端采样、高端采样和相电流直接采样。低端采样的采样电阻接在MOSFET源极和地之间共模电压低运放选型容易但问题是采样窗口受下管导通时间的限制——占空比越小窗口越窄。高端采样则在电源正端取样需要高共模抑制比的差分放大器成本高但不受占空比限制。相电流采样把采样电阻串在电机端和H桥输出之间能同时看到续流二极管路径上的电流电机控制里用得最多F407的ADC配合运放输出端做RC低通后直接接在ADC引脚上就可以。选型时先用欧姆定律把量程定下来。假设电机堵转电流峰值是6A留出1.5倍裕量后设计满量程为9AADC参考电压3.3V那么采样电阻加上运放增益后的等效传递关系应该满足R_shunt * G_amp 3.3V / 9A ≈ 0.367 ohm实际中R_shunt不能选太大0.02欧姆是常见起点太大功耗高且容易烧电阻。如果R_shunt 0.02那么需要的运放增益G_amp 0.367 / 0.02 ≈ 18.3倍可以用两级放大或一级仪表放大器来实现。放大倍数不要精算到小数实际调试时用万用表量运放输出电压和真实电流的比例标定一次就够了。电流采样的“准”最终靠标定不靠计算因为电阻的温漂和运放的失调电压都会让理论值和实测值出现系统性偏差。2.1.1 采样电阻的布线与滤波电容取值采样电阻的走线要遵守开尔文接法运放差分输入的两根线直接从采样电阻两端引出不要和功率走线共用一段铜皮。哪怕只是多了几毫米的铜皮走线寄生电阻也会叠加到采样信号里造成增益误差。RC滤波的截止频率要结合PWM频率来设。若PWM为20kHzRC截止频率设在约100kHzR取100欧姆、C取15nF既能把开关毛刺削掉又不至于把电流环带宽压到10kHz以下。ADC引脚的等效输入电容通常约8pF外加15nF并联后RC充电时间常数为1.5us和F407的ADC采样时间叠加总采样窗口要留够。2.2 用TIM1/TRGO触发ADC注入组把采样点和PWM中心对齐F407的ADC有规则组和注入组两套通道注入组可以抢占规则组的转换而且在硬件触发源的配置上更灵活。这里的关键链路是TIM1的TRGO事件在计数器到达某个设定值时触发ADC注入组转换。通过合理配置让TRGO事件恰好发生在PWM高电平的中点此时MOSFET完全导通续流二极管关断采样电阻上的电压和电枢电流基本一致这就是中心对齐采样。代码里用HAL库配置的要点是TIM1的TRGO2事件映射到ADC注入组触发源且ADC配置为只转换注入通道。每次PWM周期产生一次触发一次周期内采样一次瞬态电流值。要注意的是F407的ADC不能同时在规则组和注入组跑不同的触发源必须把所有采样任务放在同一个组里。// 使能TIM1的TRGO事件作为ADC触发源 TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; sBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_ENABLE; sBreakDeadTimeConfig.DeadTime 36; // 死区时间单位tDTS sBreakDeadTimeConfig.BreakState TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; HAL_TIMEx_ConfigBreakDeadTime(htim1, sBreakDeadTimeConfig); // ADC注入通道配置触发源选择TIM1_TRGO ADC_InjectionConfTypeDef sConfigInjected {0}; sConfigInjected.InjectedChannel ADC_CHANNEL_8; // PB0电流采样运放输出 sConfigInjected.InjectedRank 1; sConfigInjected.InjectedNbrOfConversion 1; sConfigInjected.InjectedSamplingTime ADC_SAMPLETIME_84CYCLES; sConfigInjected.ExternalTrigInjecConv ADC_EXTERNALTRIGINJECCONV_T1_TRGO2; sConfigInjected.AutoInjectedConv DISABLE; sConfigInjected.InjectedDiscontinuousConvMode DISABLE; HAL_ADCEx_InjectedConfigChannel(hadc1, sConfigInjected, 0);InjectedSamplingTime选择84周期在168MHz主频下约等于0.5us。采样时间既不能太长也不能太短太短会受RC充电时间常数影响导致精度下降太长则和PWM周期相位误差变大。ADC_EXTERNALTRIGINJECCONV_T1_TRGO2要核对具体芯片的参考手册F407系列部分型号TRGO和TRGO2的映射不一样如果发现触发不稳定优先查这个枚举是否匹配。在PWM配置中要让TIM1的计数器工作在中心对齐模式CMS TIM_CENTERALIGNED_MODE_1并把重复计数寄存器RCR设为1。此时计数器先向上计数、再向下计数TRGO事件可以配置在向上计数达到比较值时触发也可以配置在向下计数达到比较值时触发两种方式分别对应不同的采样点。工程里常用的是设置TIM1的CCR2为PWM周期的一半再指定TRGO事件为“比较2”事件这样采到的电流正好是PWM周期中段的平均电流趋势值。2.2.1 为什么不用定时器中断里读ADC很多初学者直接在定时器更新中断里调用HAL_ADC_Start再读转换结果这在电流环里是错误做法。F407的ADC启动需要软硬件触发时间中断响应本身也有抖动定时器更新中断的优先级稍微被其他中断延迟几个微秒采样点就从电流稳定区滑到了开关边沿。硬件触发链路完全不需要CPU干预ADC转换完成后由DMA或注入组结束中断把结果搬进内存CPU只负责在每个周期固定时刻读取最新值。用IO翻转加示波器验证采样点相位确保电平翻转沿没有和PWM跳变沿重叠。2.3 采样值换算与滤波策略ADC读到的12位原始值需要换算成安培。F407的ADC是12位逐次逼近型参考电压3.3V因此电流值(A) (ADC原始值 / 4095.0 - 0.5) * (3.3 / (R_shunt * G_amp))这里捎带一个零点概念运放把电流信号偏置到1.65V中点这样双向电流在ADC里都有得表示。当电枢电流为0时ADC原始值约为2048不同运放的失调电压会让这个值有正负几十的偏差所以程序启动时要先做零漂采样——电机不转、占空比为0时连续采样50次取平均作为零点保存下来。零漂校准代码uint32_t zero_sum 0; // 电流环启动前占空比输出0等待1秒让电枢电流归零 for (int i 0; i 50; i) { HAL_ADCEx_InjectedStart(hadc1); if (HAL_ADCEx_InjectedPollForConversion(hadc1, 10) HAL_OK) { zero_sum HAL_ADCEx_InjectedGetValue(hadc1, 1); } HAL_Delay(1); } int16_t zero_offset zero_sum / 50;滤波策略上有个容易犯的错误电流环内部不要用移动平均或一阶低通滤波。电流环带宽本来就高在环路内部加滤波等于人为降低相位裕度容易引起振荡。正确做法是ADC原始值直接进入PIDPID输出限幅后再处理如果需要滤波只对用于速度环或力矩监视的电流值做低通不要把滤波后的值喂给电流PID。3. PWM输出与H桥驱动死区、方向和使能逻辑电流环算出了目标PWM值但这个PWM值要变成真实的电机端子电压中间隔着一个H桥。STM32F407的TIM1和TIM8是高级定时器支持互补输出和可编程死区插入和H桥的上下桥臂驱动芯片是天生一对。这一章把PWM频率选择、死区时间计算和方向切换逻辑说透。3.1 互补PWM加死区TIM1/8的配置关键参数直流有刷电机不需要像BLDC那样换相一块H桥的四个管子Q1/Q4一组、Q2/Q3一组两组交替导通就能实现正反转。关键是上下桥臂不能同时导通否则电源直接短路。死区时间就是在关闭一组管子之后、开启另一组管子之前插入的一个等待窗口由TIM1的DTR寄存器控制。死区时间的计算单位是tDTS由TIM1的时钟分频决定。假设APB2定时器时钟为168MHz内部不分频那么tDTS 1/168MHz ≈ 5.95ns。若死区时间要求0.5us则DTR值 0.5us / 5.95ns ≈ 84。实际工程里死区要结合驱动芯片的延迟特性设置MOSFET的关断延迟和开通延迟之差越大需要的死区越长但死区太长会引入输出电压畸变电流谐波增加。常见做法是先按驱动芯片数据手册的典型延迟值算出理论死区再乘以2倍作为初始值用示波器确认无直通后再逐步下调。PWM频率的选择要平衡三个约束频率越高电流纹波越小电机噪声也越小但MOSFET开关损耗线性上升。直流有刷电机的电枢电感一般在几十微亨到几百微亨之间频率太低时电流纹波过大电机换向器火花严重。常见做法是选10kHz到20kHz。20kHz还有一个好处是正好超过人耳听觉上限没有吱吱声。F407跑20kHz的PWM没有任何压力TIM1时基配置如下// TIM1输出比较配置PWM模式1中心对齐 static void MX_TIM1_Init(uint16_t period) { TIM_OC_InitTypeDef sConfigOC {0}; TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; htim1.Instance TIM1; htim1.Init.Prescaler 0; // 不分频168MHz htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period period - 1; // 20kHz对应4200-1 htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 1; // 中心对齐模式必须设1 htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim1); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); }RepetitionCounter必须设为1因为只有RCR为1时才能在中心对齐模式下每个完整PWM周期产生一次更新事件。如果不设更新事件频率和PWM频率不一致电流环的周期计算就会出乱子。3.2 方向切换与逻辑保护先断PWM再换向直流有刷电机的方向切换比想象中危险。如果电机正在正向高速旋转突然把PWM占空比改为反方向最大值电枢电流会瞬间冲到几十安培MOSFET大概率烧掉。正确做法是加入“顺序换向”逻辑先输出0占空比等待电枢电流衰减到0再改变方向再恢复PWM。用F407的状态机来管理这个过程比在中断里直接改CCR值可靠得多。另一个保护逻辑是过流检测当电流环计算的电流反馈值超过硬件设定的最大阈值时立刻封锁PWM输出。这里可以利用TIM1的Break功能把比较器的输出接到TIM1_BKIN引脚。一旦触发PWM输出引脚被硬件自动拉到安全电平不需要CPU参与。这套保护在F407上是免费的不用白不用。// 使能Break功能BKIN引脚出现上升沿即锁定PWM输出为低 sBreakDeadTimeConfig.BreakState TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.BreakFilter 0; // 滤波器0表示不滤波 sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_DISABLE;AutomaticOutput如果设为DISABLEBreak事件触发后需要软件调用HAL_TIMEx_ConfigBreakDeadTime重新初始化并显式解除锁定才能恢复输出这是一层很实用的安全联锁。启用Break后占空比输出必须通过HAL_TIMEx_PWMN_Start启动主输出和互补输出不要用普通的HAL_TIM_PWM_Start。3.3 H桥芯片与F407的逻辑电平适配市面上常见的半桥驱动芯片有IR2104、EG2134等逻辑输入电平多在2.5V以上即可识别F407的3.3V GPIO直接驱动没有问题。但全桥芯片如BTN7971、DRV8701等需要注意输入接口类型有的是独立方向使能接口有的是两路PWM直通接口。两路PWM直通接口意味着IN1和IN2各接一路PWM一路高则另一路必须低死区要在外部由软件配置。F407的高级定时器互补输出正好对应这种接口把TIM1_CCx接到IN1TIM1_CCxN接到IN2死区由TIM1自动插入。如果选的是带独立使能脚的驱动芯片用普通GPIO控制方向、用TIM1的通道输出PWM控制使能脚死区完全由芯片内部控制F407这边只需要保证使能信号稳定。4. 电流环PID实现离散化、限幅和参数整定有了可靠的电流采样和PWM输出下一步就是真正的电流环PID算法。电流环带宽高控制周期短传统的PID三个参数在电流环场景下通常只用PI——D项对噪声太敏感微分放大采样噪声后会让系统更差。本章从连续域传递函数切入推导出适合F407定点或浮点运行的离散PI形式并给出实际整定路径。4.1 从电机电气方程到数字PI的离散化直流有刷电机的电枢回路可以写成电压方程U R*i L*di/dt E其中E是反电动势在电流环响应速度足够快时可以把反电动势视为一个缓慢变化的外部扰动。对电流环来说被控对象是R-L串联回路其传递函数为I(s) / U(s) 1 / (L*s R)这是一个一阶惯性环节时间常数T L/R。电流环控制的目标就是让这个一阶系统的输出快速跟踪给定电流。工程上把电流环整定成典型的I型系统PI控制器的零点与被控对象极点对消得到期望的闭环带宽。PI控制器的连续形式是u(t) Kp*e(t) Ki*∫e(t)dt离散化用后向差分或双线性变换。把积分项离散化后得到位置式PItypedef struct { float Kp; // 比例系数 float Ki; // 积分系数 float output; // 当前输出 float integral; // 积分累积值 float out_max; // 输出上限 float out_min; // 输出下限 } CurrentPI_t; float CurrentPI_Update(CurrentPI_t *pid, float ref, float feedback) { float error ref - feedback; // 积分分离误差过大时不做积分累积避免超调 if (error pid-out_max * 0.8f) { pid-integral pid-Ki * error; } // 积分限幅防止windup if (pid-integral pid-out_max) pid-integral pid-out_max; if (pid-integral pid-out_min) pid-integral pid-out_min; pid-output pid-Kp * error pid-integral; // 输出限幅 if (pid-output pid-out_max) pid-output pid-out_max; if (pid-output pid-out_min) pid-output pid-out_min; return pid-output; }integral累积的是Ki*error等效于位置式PI中的积分项Kp*error和integral相加得到输出。这里有一个容易混淆的点Ki实际是连续域积分系数除以控制周期得到的离散系数如果参考实现里用的是增量式代码形式完全不同但物理含义相同。每个控制周期结束时调用一次CurrentPI_Update输出值直接作为TIM1的CCR脉宽值。4.1.1 增量式PI还是位置式PI电流环里两种形式都有人用。增量式PI只输出本次占空比的增量累加由PWM硬件完成优点是输出不会突变、手动切换时冲击小缺点是积分累积到哪去了不直观。位置式PI的output直接对应PWM占空比物理含义清楚配合积分限幅更不容易出现积分饱和。我自己习惯在电流环用位置式PI因为电流给定突变时位置式能更快响应误差清零后输出立即归位。增量式PI在给定连续变化的小功率跟随场景表现更平滑但直流有刷电机的电流环通常要求快所以优先位置式。4.2 PI参数整定从阶跃响应看带宽和阻尼参数整定不能靠猜。第一步是测量系统的电气时间常数用示波器看电机端电压阶跃时的电流上升曲线找到电流从10%升到90%的时间。这个时间约等于2.2倍的L/R时间常数据此估算L/R的数值范围。Kp的单位是“占空比/安培”物理含义是每安培误差对应的PWM输出变化量而Ki的单位是“占空比/安培·秒”。先只加Kp把系统调成欠阻尼但不振荡再加Ki消除稳态误差。实操中我会按下面的顺序调Kp从0开始以0.1的步长增加观察电流阶跃响应。当响应开始出现等幅振荡记录此时的Kp作为临界值回退到临界值的60%到70%。加入Ki从很小的值比如Kp的1%量级开始增加Ki直到稳态误差在一个控制周期内收敛。最后做堵转试验给定一个小的电流指令看实际电流是否快速到达且不超调。电流环整定的目标不是“快”而是在给定带宽内稳定。带宽太高会把采样噪声放大成音频噪声带宽太低则力矩跟随迟钝。F407的16KB CC RAM可以存放电流环关键变量利用CCM RAM访问不经过总线矩阵的特性把PID结构体放在那里可以降低一点点访问延迟实质提升有限很多老工程师不用这个优化也能跑20kHz电流环。真正决定性能的是整定质量而不是RAM位置。4.3 串级控制里电流环的周期与优先级分配如果这个项目最终要做速度环或位置环电流环是内环速度环是外环那么周期的分配原则是电流环周期是速度环周期的1/5到1/10。F407跑电流环10kHz、速度环1kHz是常见配置两个环都用定时器中断驱动电流环中断优先级高于速度环。用TIM1更新事件触发电流环PI计算用TIM2或基本定时器触发速度环计算。电流环中断里只做三件事读ADC转换值、调用PI更新函数、写TIM1的CCR寄存器。任何打印、延时、浮点库调用都不要出现在这个中断里。5. 电流环调试技巧抗饱和、前馈补偿和示波器验证前面把电流环跑通了这一章收在工程上最实用的几个调试手段上。很多人PID参数整定没问题但一加上负载就丢步或振荡问题出在积分饱和、反电动势扰动和采样毛刺上。下面三个技巧按调试优先级排序配合示波器可以快速定位问题。5.1 积分饱和处理给定突变时不超调位置式PI最大的坑是积分饱和。当给定电流突然从1A跳到5A误差很大积分项快速累积到输出上限此时实际电流还在上升但PI输出已经饱和在最大占空比。当电流到达5A后误差虽然归零积分项仍然处于饱和状态需要好几个周期才能退出来这段时间里电流会冲到6A、7A才回落。处理办法在标准积分限幅之外还可以使用“条件积分”——只有输出未达到饱和限幅时才允许积分累积输出饱和时冻结积分项// 抗积分饱和输出饱和时暂停积分累加 float output_unlimited pid-Kp * error pid-integral; if (output_unlimited pid-out_max || output_unlimited pid-out_min) { // 输出饱和不做积分等误差反向后再恢复 } else { pid-integral pid-Ki * error; }对比前一个代码版本的区别前者的积分分离阈值是误差绝对大小这个版本关注的是输出是否饱和。误差大但输出未饱和时正常积分输出饱和了才锁住积分效果更直接。5.2 电流采样毛刺修正与最小脉宽保护PWM占空比极小时电流采样会出问题。占空比低于某个阈值比如5%高电平区间太短采样点可能落在开关沿附近读到的是上一周期的续流电流或者毛刺。对策是在软件里加最小脉宽保护当PI输出绝对值小于最大占空比的5%时直接输出0把这部分控制交给下个周期的积分项处理。这会带来一点死区特性但避免了大电流失控的风险。另一个毛刺来源是电机换向器火花好在F407的ADC可以配置数字滤波器ADC_CR2的DFC位虽然只有有限档位但对几纳秒的窄毛刺有效。5.3 示波器验证的三个关键波形调完参数之后别急着上负载先把以下三个波形在示波器上看清楚。第一是PWM输出引脚波形确认死区时间是否合适——上下管驱动波形的下降沿到另一个上升沿之间有一个缺口缺口宽度就是死区时间。第二是电流采样点波形把示波器的探头接到运放输出观察采样时刻附近是否平坦没有尖刺。第三是电流阶跃响应用控制程序在给定电流上叠加一个1A的方波跳变看实际电流的上升时间和超调量。这个测试建议在电机轴堵转时做堵转状态下反电动势为零测出来的是最纯粹的电流环闭环特性。正常情况下上升到目标电流的63%所需时间应等于1/闭环带宽如果实测时间明显偏长说明PI的响应带宽没达到预期回头检查控制周期是否被其他中断打乱。堵转测试时电机会快速发热单次测试时间控制在2秒以内测试间隔休息30秒。配合红外测温枪记录电机外壳温度温度超过80度就应该停手检查电流环是否已经失控。F407在这个场景里还有一个额外的调试工具——用DAC输出电流环内部变量把PI输出、电流反馈值转成模拟信号接到示波器第二通道和电流波形对照看能一眼定位是采样问题还是控制算法问题。本文还有配套的精品资源点击获取