简介:这份资源面向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主频下arr=7199、psc=0就是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或10ms,1ms对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和转速的相位关系确认没有异常振荡。这套流程走下来,基本不会出现参数整定翻车的情况。希望帮到你。
本文还有配套的精品资源,点击获取