STM32F103模糊自整定增量式PID电机速度控制实战 简介这份资源面向STM32嵌入式初学者与电机控制方向的学习者提供一套基于STM32F103ZET6与编码器直流电机的模糊自整定增量式PID调速程序用于解决传统增量式PID在转速控制中响应与超调难以兼顾的问题。代码沿用正点原子工程格式按功能分块编写并配有注释便于对照理解模糊推理与PID参数在线整定的结合方式。压缩包共170个文件约4.74MB包含39个h头文件、38个c源文件以及编译产生的o、d、crf等中间文件另有uvprojx工程文件、hex烧录文件、map映射文件与sct分散加载文件覆盖从源码到可执行固件的完整工程结构。目前已有4281人学习下载。读者可从中获得可直接编译运行的完整工程、模糊控制表与增量式PID的实现细节以及编码器测速与定时器配置的参考写法对算法优化与课程设计、毕业设计均有实际借鉴价值。1. 从一次电机抖动说起这套模糊自整定增量式PID到底解决什么问题去年帮一个做小型移动平台的朋友调车直流编码电机在低速段跑起来一冲一冲的纯增量式PID参数调了整整两天要么响应慢半拍要么一给阶跃就超调振荡。后来换成模糊自整定增量式PID同样的硬件低速爬行稳了突加负载也能自己把参数拉回来。这套基于STM32F103的电机速度控制方案干的就是这件事用编码器测速用增量式PID算控制量再用模糊推理在线整定Kp、Ki、Kd三个参数让电机在不同转速、不同负载下都能保持稳定。它适合谁适合正在做电机闭环控制、被参数整定折磨过的嵌入式开发者也适合想入门模糊控制但不想啃纯理论的新手。整套代码跑在STM32F103上定时器编码器接口读速度PWM输出驱动电机串口打印波形硬件成本低逻辑链路完整拿来就能改。2. 系统架构与硬件资源分配定时器、编码器、PWM怎么分工2.1 整体控制链路这套方案的控制链路很清晰编码器采集电机实际转速 → 与设定值比较得到误差和误差变化率 → 模糊推理输出PID参数修正量 → 增量式PID计算新的PWM占空比 → 驱动电机。整个过程在一个定时器中断里周期执行保证控制周期固定。STM32F103的资源分配上我一般这样安排TIM1或TIM8输出PWM驱动电机TIM2或TIM3或TIM4工作在编码器模式读正交编码器再用一个基本定时器比如TIM6做控制周期中断。串口USART1负责把设定值、实际值、PID参数打到上位机看波形。这套分配在F103系列上很通用引脚不冲突中断优先级也好排。2.2 编码器测速的两种做法编码器测速常见两种方法M法和T法。M法是在固定时间内数脉冲个数适合高速T法是用脉冲间隔时间算速度适合低速。这套方案里我用的是M法因为控制周期固定实现简单低速时配合编码器四倍频也能凑合。// TIM3 编码器模式初始化四倍频 void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA6/PA7 接编码器 A/B 相 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 65535; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 编码器接口模式上升沿计数四倍频 TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICFilter 10; // 滤波防抖动 TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_Cmd(TIM3, ENABLE); }这段代码把TIM3配成编码器接口模式TI12表示A/B两相都计数实现四倍频。TIM_ICFilter设成10是硬件滤波编码器线数低或者电机振动大时这个值很关键设小了读数跳设大了丢脉冲。读速度时在固定周期里读TIM3-CNT然后清零除以周期和编码器线数就得到转速。2.3 PWM输出与电机驱动PWM用TIM1的通道1输出频率设10kHz到20kHz之间太低电机会啸叫太高驱动板可能跟不上。占空比通过修改CCR寄存器实现增量式PID输出的就是CCR的增量。// TIM1 PWM 初始化频率 10kHz void PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; // TIM1_CH1 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须开 }arr和psc决定PWM频率72MHz主频下arr7199、psc0就是10kHz。TIM_CtrlPWMOutputs这行容易被漏掉高级定时器TIM1不开这个输出使能PWM引脚没波形我在这上面翻过车。3. 模糊自整定增量式PID核心实现从误差到PWM增量的完整链路3.1 增量式PID的离散公式增量式PID和位置式最大的区别是输出的是控制量增量不是绝对量。公式是Δu(k) Kp·[e(k)-e(k-1)] Ki·e(k) Kd·[e(k)-2e(k-1)e(k-2)]这个形式天然抗积分饱和因为不需要累加历史误差而且执行器故障时不会像位置式那样一直积到满。代码里维护e(k)、e(k-1)、e(k-2)三个误差量就够了。// 增量式PID结构体 typedef struct { float Kp, Ki, Kd; float e0, e1, e2; // 当前、上次、上上次误差 float delta_u; // 控制增量 float u; // 累计控制量 float u_max, u_min; // 输出限幅 } IncPID; // 增量式PID计算 float IncPID_Calc(IncPID *pid, float target, float actual) { pid-e0 target - actual; pid-delta_u pid-Kp * (pid-e0 - pid-e1) pid-Ki * pid-e0 pid-Kd * (pid-e0 - 2*pid-e1 pid-e2); pid-u pid-delta_u; // 输出限幅防止积分饱和 if (pid-u pid-u_max) pid-u pid-u_max; if (pid-u pid-u_min) pid-u pid-u_min; pid-e2 pid-e1; pid-e1 pid-e0; return pid-u; }Kp、Ki、Kd就是模糊推理要整定的对象。u_max和u_min对应PWM占空比范围比如0到arr。注意e0、e1、e2的更新顺序写反了PID就变成玄学输出了。3.2 模糊推理误差和误差变化率怎么映射到参数修正模糊自整定的核心是建立误差e和误差变化率ec到ΔKp、ΔKi、ΔKd的模糊规则。工程上常用7个模糊子集NB、NM、NS、ZO、PS、PM、PB。e和ec的论域一般归一化到[-3,3]或[-6,6]输出修正量也类似。实现上我不建议在STM32上跑复杂的模糊推理查表法最实在。把e和ec量化到整数索引直接查二维表得到ΔKp、ΔKi、ΔKd再乘上各自的量化因子加到基础参数上。// 模糊量化把误差映射到 [-3,3] 的整数索引 int Fuzzy_Quantize(float value, float min, float max) { float norm (value - min) / (max - min) * 6.0f - 3.0f; if (norm 3.0f) norm 3.0f; if (norm -3.0f) norm -3.0f; return (int)(norm 3.5f); // 映射到 0~6 索引 } // ΔKp 模糊规则表7x7行是e列是ec const float dKp_table[7][7] { { 3, 3, 2, 2, 2, 1, 0 }, { 3, 3, 2, 2, 1, 0, 0 }, { 2, 2, 2, 1, 0, 0,-1 }, { 2, 2, 1, 0,-1,-1,-2 }, { 1, 1, 0,-1,-2,-2,-2 }, { 1, 0,-1,-2,-2,-3,-3 }, { 0,-1,-2,-2,-3,-3,-3 } }; // 模糊整定根据e和ec查表修正PID参数 void Fuzzy_Tune(IncPID *pid, float e, float ec) { int ie Fuzzy_Quantize(e, -100.0f, 100.0f); int iec Fuzzy_Quantize(ec, -50.0f, 50.0f); float dKp dKp_table[ie][iec] * 0.01f; // 量化因子 float dKi dKi_table[ie][iec] * 0.001f; float dKd dKd_table[ie][iec] * 0.005f; pid-Kp Kp_base dKp; pid-Ki Ki_base dKi; pid-Kd Kd_base dKd; // 参数限幅防止整定过头 if (pid-Kp 0) pid-Kp 0; if (pid-Ki 0) pid-Ki 0; if (pid-Kd 0) pid-Kd 0; }dKp_table的数值不是拍脑袋来的是按「误差大时加大Kp加快响应误差小时减小Kp防超调」这类经验规则填的。量化因子0.01、0.001、0.005决定了修正力度调这几个数比调规则表更直接。Kp_base、Ki_base、Kd_base是初始参数模糊整定是在这基础上做微调不是从零开始。3.3 控制周期中断里的完整执行流程把上面几块串起来控制周期中断里干这几件事读编码器算实际速度、算误差和误差变化率、模糊整定PID参数、增量式PID算输出、更新PWM占空比。// TIM6 控制周期中断1ms 一次 void TIM6_IRQHandler(void) { if (TIM_GetITStatus(TIM6, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM6, TIM_IT_Update); // 1. 读编码器算实际转速 int16_t cnt TIM3-CNT; TIM3-CNT 0; float actual_speed (float)cnt * 60.0f / (ENCODER_LINES * 4 * 0.001f); // 2. 算误差和误差变化率 static float last_error 0; float error target_speed - actual_speed; float ec error - last_error; last_error error; // 3. 模糊整定 PID 参数 Fuzzy_Tune(pid, error, ec); // 4. 增量式 PID 计算 float u IncPID_Calc(pid, target_speed, actual_speed); // 5. 更新 PWM 占空比 TIM_SetCompare1(TIM1, (uint16_t)u); } }ENCODER_LINES是编码器线数乘4是四倍频除0.001是把1ms换成秒。target_speed是设定转速单位RPM。这个流程每1ms跑一次控制周期固定模糊整定和PID计算都在里面完成。实际调试时控制周期可以放宽到5ms或10ms1ms对F103来说负担有点重尤其还开着串口打印。4. 避坑与常见问题排查编码器读数、PWM无输出、参数振荡怎么查4.1 编码器读数跳变或始终为零现象串口打印的实际速度要么一直是0要么乱跳。原因编码器A/B相接反、滤波参数不对、或者TIM3没使能。还有一种情况是编码器供电和单片机不共地信号参考电平漂了。解决先用示波器看A/B相有没有波形有波形再查TIM3-CNT在转动时变不变。不变就检查GPIO模式是不是IN_FLOATING编码器接口模式有没有配对。读数跳变就把TIM_ICFilter加大或者编码器线数设对。共地问题最隐蔽我一般先拿万用表量一下编码器GND和单片机GND通不通。4.2 PWM引脚没有波形现象电机不转示波器量PWM引脚一直是低电平。原因TIM_CtrlPWMOutputs没调用或者GPIO复用模式没配对或者CCR值一直是0。解决TIM1是高级定时器必须调TIM_CtrlPWMOutputs(TIM1, ENABLE)才有输出这行漏了是高频翻车点。GPIO要配成AF_PP复用推挽不是普通输出。CCR值可以在调试时先手动设个固定值比如TIM_SetCompare1(TIM1, 3600)确认硬件通路没问题再交给PID。4.3 电机低速抖动、高速超调现象低速时一冲一冲高速时给定阶跃后冲过头再荡回来。原因低速时编码器分辨率不够M法测速在低速段误差大高速超调是Kp太大或者模糊整定量化因子太激进。解决低速抖动可以换T法测速或者降低控制周期让每个周期内脉冲数多一点。高速超调先把Kp_base调小再把dKp的量化因子从0.01降到0.005。模糊整定的修正量是叠加在基础参数上的基础参数没调好模糊整定也救不回来。4.4 串口打印导致控制周期抖动现象一开串口打印电机声音就不对转速波动变大。原因串口发送是阻塞的在中断里打印会拉长控制周期PID计算周期不固定微分项直接算错。解决别在控制中断里直接打印。我一般用一个缓冲区存数据主循环里慢慢发或者用DMA发串口。控制中断里只做计算和PWM更新其他事都挪出去。4.5 参数整定后电机发热严重现象电机跑一会儿就烫手电流比纯PID时大。原因模糊整定让Kp、Ki频繁变化PWM占空比一直在微调电机电流纹波大。解决给模糊整定加个死区误差在±5%以内时不修正参数保持基础参数运行。另外参数限幅要设合理Ki别让它整定到太大积分作用太强电机会持续发热。5. 二次开发与参数整定技巧从能跑到跑好的最后一公里这套代码跑通之后二次开发的空间主要在三个地方模糊规则表的调整、量化因子的整定、以及控制周期的优化。先说模糊规则表。上面给的dKp_table是通用经验表实际电机特性不一样规则表要微调。我的做法是先固定Ki和Kd的修正只调Kp的规则表观察阶跃响应。如果超调大就把误差为负实际超过设定那几行的修正值往负方向调如果响应慢就把误差为正那几行往正方向调。调完Kp再调Ki最后调Kd。一次只动一个参数不然出了问题都不知道是谁的锅。量化因子是比规则表更直接的调节旋钮。dKp的量化因子从0.01调到0.02相当于模糊整定力度翻倍。我一般先用小量化因子跑确认基础参数没问题再慢慢加大量化因子看效果。量化因子太大参数整定会振荡电机声音会一高一低。控制周期这块1ms对F103来说偏紧尤其还开着编码器读和PWM更新。我一般用5ms低速测速精度也够CPU负担小很多。如果非要1ms就把模糊整定降频比如每5个控制周期整定一次参数PID还是1ms算一次。验证方法上我习惯用串口上位机看三条曲线设定速度、实际速度、PWM占空比。阶跃给定后实际速度应该在两三个控制周期内跟上超调不超过10%稳态误差接近零。如果实际速度曲线有等幅振荡说明Kp或Ki太大如果爬升太慢说明Kp太小或者模糊整定没起作用。还有一个技巧是分段整定。电机在低速和高速段的特性差别很大可以按转速分段设置不同的基础参数和量化因子。比如0到500RPM用一组500RPM以上用另一组切换时做个平滑过渡。这样比一套参数打天下效果好得多。从那以后我每次调电机控制都强制先用手动PID把基础参数调到临界振荡再开模糊整定最后用示波器看PWM和转速的相位关系确认没有异常振荡。这套流程走下来基本不会出现参数整定翻车的情况。希望帮到你。本文还有配套的精品资源点击获取