
简介这份资源面向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同样的硬件负载突变时转速波动从 ±80rpm 压到 ±15rpm 以内恢复时间缩短到 200ms 左右。这套方案的核心思路是用模糊推理根据当前误差和误差变化率实时修正 PID 三个参数同时用增量式输出避免积分饱和和累加溢出。它适合谁适合手上有 STM32F103 最小系统板、带 AB 相编码器的直流电机、想从“能转”进阶到“转得稳”的嵌入式开发者。下面我把这套东西从原理到代码到踩坑完整拆一遍。2. 模糊自整定增量式PID的原理拆解与参数映射2.1 为什么选增量式而不是位置式位置式 PID 的输出直接对应执行器的绝对量公式是 u(k) Kp·e(k) Ki·Σe(j) Kd·[e(k)-e(k-1)]。问题在于那个累加项一旦电机堵转或者编码器丢脉冲积分项会一路累加到饱和等故障消失后输出要很久才能退回来这就是常说的积分饱和。增量式 PID 输出的是控制量的增量 Δu(k)公式变成 Δu(k) Kp·[e(k)-e(k-1)] Ki·e(k) Kd·[e(k)-2e(k-1)e(k-2)]。它天然没有累加器积分饱和问题从结构上就弱化了而且输出增量直接对应 PWM 占空比的调整量和定时器寄存器操作天然契合。我一般会跟新手说只要你的执行器是“在当前基础上加一点减一点”这种模式增量式就是更顺手的选择。2.2 模糊推理怎么介入PID参数固定参数 PID 的死穴是一组 Kp、Ki、Kd 只能在一段工况里表现好。模糊自整定的做法是把误差 e 和误差变化率 ec 作为输入各自划分成若干模糊集比如 NB、NM、NS、ZO、PS、PM、PB 七个然后根据一张模糊规则表去查当前应该给 Kp、Ki、Kd 各加多少修正量。规则表的逻辑很朴素误差大时加大 Kp 加快响应误差小时减小 Kp 防止超调误差变化率大时加大 Kd 抑制振荡。这张表不需要精确的数学模型靠的是“大误差猛拉、小误差细调”这种工程直觉。实际写代码时模糊推理可以简化成查表加插值STM32F103 的 72MHz 主频跑这个绰绰有余单次推理耗时通常在几十微秒量级。2.3 从模糊输出到PID参数的映射关系模糊推理输出的是 Kp、Ki、Kd 的修正量 ΔKp、ΔKi、ΔKd最终参数是 Kp Kp0 ΔKp以此类推。这里有个关键点Kp0、Ki0、Kd0 是基础值需要你先用常规方法整定出一组“还凑合”的参数模糊推理只负责在这个基础上做微调。如果你基础值差得离谱模糊推理也救不回来。我一般会先用临界比例度法或者试凑法把基础参数调到“能稳但不够快”的状态再开模糊自整定。另外ΔKp、ΔKi、ΔKd 的论域范围要根据你的电机和负载来定不能照抄别人的数值这个后面避坑章节会细说。3. 在STM32F103上落地编码器采集、模糊推理与PWM输出3.1 硬件连接与定时器配置先确认硬件STM32F103C8T6 最小系统板一块带 AB 相输出的增量式编码器电机一套TB6612 或 L298N 驱动模块一个。编码器 A、B 相接定时器编码器模式引脚比如 TIM3 的 PA6、PA7PWM 输出用 TIM1 的 PA8 或 TIM4 的 PB6串口用于调试打印USART1 的 PA9、PA10。定时器配置上编码器模式用 TIM_EncoderInterfaceConfig 把 TIM3 设成编码器模式计数范围 0~65535四倍频后分辨率是 4 倍线数。PWM 频率我一般设 10kHz~20kHz太低电机会啸叫太高驱动模块发热。下面是一段初始化代码// TIM3 编码器模式初始化PA6/PA7 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); 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; // 16位计数满量程 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 表示同时用通道 1 和通道 2 计数四倍频。TIM_ICFilter 设成 10 是硬件滤波能滤掉编码器抖动带来的毛刺这个值根据你的编码器质量调整质量差就加大。计数周期设 65535 是为了避免频繁溢出但读取时要注意处理溢出翻转。3.2 速度计算与增量式PID实现速度计算用固定周期采样编码器计数值比如每 10ms 读一次 TIM3-CNT算出增量再换算成 rpm。增量式 PID 的实现要注意历史误差的保存和输出限幅typedef struct { float Kp, Ki, Kd; // 当前PID参数 float Kp0, Ki0, Kd0; // 基础参数 float e0, e1, e2; // e(k), e(k-1), e(k-2) float delta_Kp, delta_Ki, delta_Kd; // 模糊输出修正量 float out; // 累计输出 float out_max, out_min; // 输出限幅 } IncPID; float IncPID_Calc(IncPID *pid, float target, float actual) { float e target - actual; float ec e - pid-e1; // 误差变化率 // 模糊推理更新 delta_Kp, delta_Ki, delta_Kd Fuzzy_Update(e, ec, pid-delta_Kp, pid-delta_Ki, pid-delta_Kd); pid-Kp pid-Kp0 pid-delta_Kp; pid-Ki pid-Ki0 pid-delta_Ki; pid-Kd pid-Kd0 pid-delta_Kd; // 增量式PID公式 float du pid-Kp * (e - pid-e1) pid-Ki * e pid-Kd * (e - 2*pid-e1 pid-e2); pid-out du; if (pid-out pid-out_max) pid-out pid-out_max; if (pid-out pid-out_min) pid-out pid-out_min; pid-e2 pid-e1; pid-e1 e; return pid-out; }逻辑说明e 是目标转速减实际转速ec 是本次误差减上次误差。Fuzzy_Update 负责查模糊规则表算出三个修正量。增量式公式里 Kp 乘的是误差差分Ki 乘的是当前误差Kd 乘的是二阶差分。输出累加后做限幅防止 PWM 占空比超出 0~100% 范围。参数上out_max 和 out_min 对应定时器 ARR 值的上下限比如 ARR 设 7200那 out_max 就是 7200。3.3 模糊推理的查表实现模糊推理在 STM32 上没必要跑复杂的隶属度函数计算直接做查表加线性插值就够了。把 e 和 ec 的论域各分成 7 档规则表存成二维数组// 模糊规则表行是e的档位列是ec的档位值是Kp修正量档位 const int8_t Kp_rule[7][7] { { 3, 3, 2, 2, 2, 1, 0}, { 3, 3, 2, 1, 1, 0, -1}, { 2, 2, 1, 1, 0, -1, -1}, { 2, 1, 1, 0, -1, -1, -2}, { 1, 1, 0, -1, -1, -2, -2}, { 1, 0, -1, -1, -2, -2, -3}, { 0, -1, -2, -2, -2, -3, -3} }; float Fuzzy_Update(float e, float ec, float *dKp, float *dKi, float *dKd) { // 把e和ec映射到0~6的档位索引 int idx_e Map_To_Index(e, E_MIN, E_MAX, 7); int idx_ec Map_To_Index(ec, EC_MIN, EC_MAX, 7); // 查表得到档位值再乘以缩放系数 *dKp Kp_rule[idx_e][idx_ec] * KP_SCALE; *dKi Ki_rule[idx_e][idx_ec] * KI_SCALE; *dKd Kd_rule[idx_e][idx_ec] * KD_SCALE; }Map_To_Index 把实际误差值线性映射到 0~6 的整数索引超出范围就钳位。KP_SCALE、KI_SCALE、KD_SCALE 是缩放系数决定模糊修正量的实际大小。规则表的数值不是死的但有个基本规律误差为正且变化率为正时说明转速偏低且在继续恶化Kp 修正量给大正值误差接近零时修正量趋近零甚至给负值防止超调。Ki 和 Kd 的规则表逻辑类似但侧重点不同。4. 避坑与排查调这套控制器最容易翻车的五个地方4.1 编码器计数溢出导致速度突变现象电机匀速转着速度反馈突然跳变一下PID 输出跟着抖。原因TIM3 是 16 位计数器正转到 65535 再转就翻到 0反转从 0 翻到 65535如果读取时没处理溢出算出来的增量就是错的。解决读取 CNT 时用 int16_t 强制转换或者记录上次值做差值时判断溢出方向。我一般直接用(int16_t)(TIM3-CNT - last_cnt)这种方式让编译器处理有符号差值。4.2 模糊规则表论域范围设错现象模糊自整定开了跟没开一样或者一开就振荡。原因E_MIN、E_MAX、EC_MIN、EC_MAX 设得和实际误差范围不匹配。比如实际误差最大 500rpm你把 E_MAX 设成 50那所有误差都钳位到最高档模糊推理失去分辨力。解决先跑一遍固定参数 PID用串口把误差范围打出来按实际最大误差的 1.2~1.5 倍设论域边界。4.3 增量式输出没有限幅导致积分风卷现象电机启动时猛冲一下或者堵转后恢复时输出爆表。原因增量式虽然没有显式积分项但 out 累加器没有限幅的话连续同向增量会让 out 无限增长。解决在 IncPID_Calc 里对 out 做上下限钳位上限对应 PWM 满占空比下限对应 0 或负限幅。另外可以在误差符号反转时对 out 做衰减加快退出饱和。4.4 PWM频率与电机电感不匹配现象电机发热严重、噪音大、低速时抖动。原因PWM 频率太低电流纹波大频率太高驱动模块开关损耗大。解决有刷直流电机一般 10kHz~20kHz 比较合适无刷电机要看驱动器手册。我一般先用 15kHz 试摸驱动模块温度烫手就降到 10kHz电机啸叫就升到 20kHz。4.5 速度采样周期与PID周期不匹配现象PID 响应迟钝或者高频振荡。原因速度采样周期太长反馈滞后太短编码器分辨率不够速度量化误差大。解决采样周期一般取 5ms~20ms编码器线数低就取长一点线数高可以取短一点。PID 计算周期和速度采样周期保持一致不要在一个周期里多次调用 PID。5. 进阶技巧用串口波形和阶跃响应验证整定效果调完之后怎么验证我习惯用串口把目标转速、实际转速、PID 输出三路数据打到上位机用波形工具看阶跃响应。具体做法是在定时器中断里每 10ms 调一次 PID同时把数据打包成帧发出去帧格式用简单的帧头数据校验。上位机用任意串口示波器工具接收看三条曲线的形态。判断标准很直接给一个从 0 到额定转速的阶跃实际转速应该在 200ms 内到达目标值的 90%超调不超过 10%稳态波动在 ±1% 以内。如果超调大减小 Kp 修正量的正向档位或者加大 Kd 修正量如果响应慢加大 Kp 修正量或者减小 Ki 修正量。下面是一个简单的串口发送代码// 每10ms调用一次发送三路数据 void Send_WaveData(float target, float actual, float out) { uint8_t buf[16]; int16_t t (int16_t)(target * 10); // 放大10倍发整数 int16_t a (int16_t)(actual * 10); int16_t o (int16_t)(out); buf[0] 0xAA; buf[1] 0x55; // 帧头 buf[2] t 8; buf[3] t 0xFF; buf[4] a 8; buf[5] a 0xFF; buf[6] o 8; buf[7] o 0xFF; buf[8] buf[2]^buf[3]^buf[4]^buf[5]^buf[6]^buf[7]; // 校验 for (int i 0; i 9; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, buf[i]); } }这段代码把三路数据放大 10 倍转成整数发送避免浮点传输的麻烦。帧头 0xAA55 用于上位机同步校验字节用简单异或。上位机解析时先找帧头再读 6 个数据字节最后校验。我一般会在上位机脚本里加一个滑动平均滤波把原始波形和滤波后波形叠在一起看这样既能看细节又能看趋势。还有一个技巧在整定初期先把模糊自整定关掉用固定参数跑把 Kp0、Ki0、Kd0 调到“临界稳定”状态——也就是给阶跃后等幅振荡记下此时的 Kp 和振荡周期然后按经验公式折算出基础参数。开模糊自整定后观察修正量的变化范围如果 ΔKp 一直在正负满量程之间跳说明 KP_SCALE 设大了要往小调。从那以后我每次调新电机都强制走一遍“先固定、后模糊、再看修正量范围”的流程能省下大量反复试凑的时间。希望帮到你。本文还有配套的精品资源点击获取