☰
STM32模糊自整定增量式PID编码电机速度闭环实战
2026/10/21 15:40:59 网站建设 项目流程

简介:基于STM32F103ZET6与编码器直流电机的模糊自整定增量式PID速度控制工程,面向嵌入式初学者和电机控制开发者,主要解决普通增量式PID参数整定困难、超调量较大、动态响应不易兼顾等问题。程序按照正点原子工程规范分模块组织,注释完整,代码块清晰,涵盖模糊规则表、模糊推理、增量式PID计算、编码器测速、PWM输出及串口调试等关键环节,可直接在Keil中打开工程进行编译、烧录和实验验证。压缩包共170个文件,以h头文件、c源文件为主,同时包含o目标文件、hex可执行文件、uvprojx工程文件以及map、axf、sct等构建链接与调试辅助文件,整体4.74MB,目录结构规整,便于按模块检索代码和理解编译过程。目前已有4282人学习下载,作者实测运行可靠,对初学者理解模糊自整定原理、对比普通PID控制效果以及规范嵌入式编程习惯都有较好的参考价值。

1. 模糊自整定增量式PID:为什么调速要它

去年在调一台两轮自平衡原型车时,我遇到一个很典型的现象:空载时速度很稳,一加负载、推一下车身,电机声音立刻变成高频尖叫,转速掉下去又冲上来,来回只有两三个周期就转飞了。换了几组固定Kp、Ki、Kd都压不住,后来我把控制器换成模糊自整定增量式PID,才把这种“负载变了参数就得重调”的毛病根治掉。这就是你手里这份资源的核心:在STM32F103上用模糊控制在线修正增量式PID的三个参数,配编码电机做速度闭环,载入变化时不需要手动重调PID,板子自己去匹配工况。它适合正在做小车、云台、传送带类项目,又受够了固定PID在不同负载下反复翻车的开发者。

2. 系统架构与关键器件:STM32F103 + 编码电机的闭环回路

2.1 整体控制链路:从编码器回到PWM输出

这个项目本质上是一个单闭环调速系统。被控对象是带减速箱的直流电机,执行器是H桥驱动,反馈来自电机轴上的霍尔/光电编码器。控制器的物理载体是STM32F103,它要做的事情只有三件:读编码器、算模糊PID增量、输出PWM。

数据流是这样的:电机转动,编码器输出两路正交方波,定时器工作在编码器模式,计数方向随电机转向变化。单片机每个控制周期(比如10ms)读取一次计数增量,换算成当前实际转速。把目标转速和当前转速相减得到误差e,把本次误差和上次误差的差值作为误差变化率ec,然后同时喂给模糊控制器和增量式PID。模糊控制器先根据e和ec查规则表,解模糊后输出Kp、Ki、Kd的调整量,增量式PID再算出本次需要加到PWM基值上的控制增量。这个增量累加到占空比上,通过定时器PWM输出给电机驱动芯片,驱动芯片改变电机端电压,完成一次闭环。

这套链路里最需要注意的一点是:模糊PID并没有替代PID,它只是PID的“参数在线调节器”。固定PID是一个黑匣子,参数定死;模糊PID是给这个黑匣子加了一套“看到误差大就加大Kp、误差小就减小Kp”的自动规则。F103的主频只有72MHz,模糊控制的计算量很小,反而增量式PID的累加和限幅更值得关注。

2.2 硬件选型与接线:编码电机、驱动板、电源防坑

这份资源对应的硬件方案比较标准:主控是STM32F103C8T6或RCT6,电机是带霍尔编码器的减速直流电机(常见的是30:1或75:1那种),驱动板用TB6612FNG或DRV8833都可以。编码器一般是13线或20线的霍尔码盘,配合1/30减速比,电机轴转一圈编码器输出大约390个或600个脉冲。这个分辨率做速度环完全够用。

接线建议分成三组:编码器组、驱动组、电源组。编码器的A、B相分别接STM32的PA0/PA1,如果定时器1不能用,就换TIM2的CH1/CH2,正交解码必须要同一定时器的两个通道。驱动板IN1/IN2接普通GPIO,PWM接定时器通道,比如TIM3的CH1。电源部分最容易翻车,电机电源和逻辑电源必须分开,电机用6V或者7.4V电池,单片机和驱动板逻辑侧用3.3V稳压,共地也要注意,最好在电源入口加一个几百微法的电解电容,否则逆变时电压波动会直接让编码器读数瞎跳。

初始化代码里,编码器定时器要配成Encoder Mode,而不是普通计数模式。常见写法是配置TIM2的CCER、SMCR和CR1,确保计数方向跟随AB相序。下面是一段可用的初始化片段:

// TIM2编码器模式初始化 void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0/PA1 复用为TIM2通道1/2 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); TIM_InitStruct.TIM_Prescaler = 0; TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 编码器模式下自动处理方向 TIM_InitStruct.TIM_Period = 0xFFFF; TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_InitStruct); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, // 两路正交计数 TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM2, 0x8000); // 计数初值放中间,留出正负余量 TIM_Cmd(TIM2, ENABLE); }

定时器Period设置为0xFFFF,计数初值放在0x8000,是为了能同时表示正向和反向的计数变化。编码器模式下的方向由A相和B相的相位差决定,单片机内部自动加减计数,所以不需要在软件里额外判断转向。这里有一点要特别说明:如果初始化顺序乱了,比如先使能定时器再配置编码器接口,会出现首周期计数不回零的玄学问题。我一般会把GPIO配置放在定时器配置之前,并且配置完成后把计数寄存器手动清零一次。

驱动板部分,PWM频率选20kHz左右比较合适。频率太低会听到明显噪声,太高MOS管开关损耗变大。定时器PWM初始化时,把ARR设为999,PSC设为72,就能得到20kHz。注意PWM占空比增量每一步不要给太多,我的经验是单次PID输出限制在±50以内,否则电机电流冲击会让驱动板进入保护状态。

2.3 软件工程结构:目录与任务分工

整个工程是按“模块”组织的,不是把所有代码堆在main.c里。这份资源解压后大概能看到下面的结构:

. ├── Core/ │ ├── inc/ # 头文件 │ └── src/ ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ ├── App/ │ ├── main.c │ ├── pid.c # 增量式PID本体 │ ├── pid.h │ ├── fuzzy.c # 模糊控制器 │ ├── fuzzy.h │ ├── speed_ctrl.c # 速度环控制周期调度 │ ├── encoder.c # 编码器读写 │ ├── motor.c # PWM输出和驱动方向 │ └── user_debug.c # 串口打印和参数可视化 └── MDK-ARM/

main.c只负责初始化硬件和进入一个while(1)死循环,具体的控制逻辑由各模块接口提供。speed_ctrl.c里放一个ms级的控制周期入口,比如SpeedCtrl_Update(),由定时器中断或Systick调度。fuzzy.c是被动调用,没有调度权,它只负责根据输入查规则表并输出参数修正量。这样拆的好处是,以后想换位置环或者加上位机通信,只动speed_ctrl这一层,不用把PID和模糊代码推翻重写。

这种分层也是我认为这个项目最值得学的地方:算法部分保持“纯函数”属性,不依赖具体芯片外设;硬件驱动部分只管寄存器读写,不带业务逻辑。分离之后,你在PC上用同样的算法仿真,和单片机上的行为基本一致。

3. 核心模块拆解:模糊自整定增量式PID的实现

3.1 增量式PID:为什么不用位置式

速度环控制的执行机构是PWM占空比,它天然适合增量式PID。位置式PID的输出本身就是占空比的绝对值,每次都要重新计算,一旦积分项饱和,电机就会长时间停不回来;而增量式PID只输出上一次和这一次之间的差值,单片机只需要做一次“旧占空比加增量、再限幅”的操作。

增量式PID的公式是常规的:

float dOut = Kp * (e - e_pre) + Ki * e + Kd * (e - 2 * e_pre + e_pre2);

这次输出dOut不是新的占空比,而是占空比修正量。最终要执行的是duty += dOut。这里Kp对应比例增量的权重,Ki对应本次误差的积分增量,Kd对应误差二次差分,也就是误差加速度。因为只有三项差值,所以即使误差恒定为0,积分增量也为0,不会出现位置式那种误差一直存在但是输出已经饱和的问题。

在实现上,我一般会把增量式PID封装成一个结构体,把历史误差放在结构体内部,避免外部到处传参。代码片段如下:

typedef struct { float Kp; float Ki; float Kd; float e_pre; float e_pre2; float out_max; // 单次增量限幅 float duty_min; float duty_max; } IncPID_T; float IncPID_Calc(IncPID_T *pid, float target, float current) { float e = target - current; float dOut; dOut = pid->Kp * (e - pid->e_pre) + pid->Ki * e + pid->Kd * (e - 2.0f * pid->e_pre + pid->e_pre2); // 增量限幅,防止大误差时一步跳太多 if (dOut > pid->out_max) dOut = pid->out_max; if (dOut < -pid->out_max) dOut = -pid->out_max; pid->e_pre2 = pid->e_pre; pid->e_pre = e; return dOut; }

这里有个容易忽略的点:e_pre2的更新顺序。我写过一次把e_pre更新放在e_pre2前面的代码,结果二阶差分项用的永远是同一个历史值,Kd完全失效。所以每次计算完成后,先让e_pre2继承e_pre,再让e_pre继承当前e。顺序反了,控制品质会差一个量级。

3.2 模糊控制器:误差和误差变化率如何生成Kp、Ki、Kd

模糊控制器输入是误差e和误差变化率ec。先做模糊化:把当前实际值映射到[-6, +6]区间,然后判断它落在哪些模糊集合上。这里模糊集合取7个:NB(负大)、NM(负中)、NS(负小)、ZO(零)、PS(正小)、PM(正中)、PB(正大)。每个输入在每个集合里都有一个隶属度,常用的是三角形隶属度函数,计算简单,适合F103这种小资源单片。

每个输出变量(Kp增量、Ki增量、Kd增量)也定义同样的7个集合,但规则表和去模糊方式不同。Kp的整定思路是:误差大时Kp大,误差小时Kp小,误差变化率大时Kp适当减小防止超调。Ki的整定思路是:误差大时Ki小甚至为负,避免积分饱和;误差中等时Ki中等;误差小时Ki加大来消除静差。Kd的整定思路是:误差变化率大时Kd大,误差小且变化率也小时Kd小。

规则表是这份资源里最核心的数据结构。拿Kp的调整表举例,行是误差e的等级,列是误差变化率ec的等级:

e\ecNBNMNSZOPSPMPB
NBPBPBPMPMPSZOZO
NMPBPBPMPSPSZONS
NSPMPMPMPSZONSNS
ZOPMPMPSZONSNMNM
PSPSPSZONSNSNMNM
PMPSZONSNMNMNMNB
PBZOZONMNMNMNBNB

这张表看着复杂,实际上就是一句人话:“误差大时给足比例,误差小就别让比例太大”。Ki和Kd的规则表结构一样,只是数值方向相反或偏移。模糊化时,每个输入可能同时命中两个相邻的集合,比如e=1.2在ZO和PS之间,ZO的隶属度可能是0.4,PS是0.6。这时两个集合都得参与模糊推理,不能用“只取最大隶属度的集合”这种简化方式,否则那就不叫模糊PID,叫查表开关PID了。

3.3 核心代码逐段讲解:模糊推理与解模糊

下面这段是fuzzy.c里最核心的函数,完成一次Kp、Ki、Kd修正量的计算。代码里特意把输入量化、规则查表、重心法解模糊分成了三个独立函数,方便你在上位机里复现同样的算法。

#define NB 0 #define NM 1 #define NS 2 #define ZO 3 #define PS 4 #define PM 5 #define PB 6 // 三角形隶属度函数 float Trimf(float x, float a, float b, float c) { if (x <= a || x >= c) return 0.0f; if (x == b) return 1.0f; if (x > a && x < b) return (x - a) / (b - a); return (c - x) / (c - b); } // 输入e和ec映射为7个集合的隶属度 void Fuzzy_Membership(float e, float* me, float ec, float* mec) { float pos[7] = {-6.0f, -4.0f, -2.0f, 0.0f, 2.0f, 4.0f, 6.0f}; int i; for (i = 0; i < 7; i++) { float left = (i > 0) ? pos[i-1] : pos[i] - 2.0f; float right = (i < 6) ? pos[i+1] : pos[i] + 2.0f; me[i] = Trimf(e, left, pos[i], right); mec[i] = Trimf(ec, left, pos[i], right); } } // 解模糊:加权平均,输出系数修正量 float Fuzzy_Defuzz(float* member, int *rule, int rule_rows, int rule_cols) { float num = 0.0f; float den = 0.0f; float center[7] = {-6.0f, -4.0f, -2.0f, 0.0f, 2.0f, 4.0f, 6.0f}; int i, j; for (i = 0; i < rule_rows; i++) { for (j = 0; j < rule_cols; j++) { float w = (member[i] < member[j]) ? member[i] : member[j]; int out_idx = rule[i * rule_cols + j]; num += w * center[out_idx]; den += w; } } if (den < 0.0001f) return 0.0f; return num / den; }

说明一下这段代码的几个关键点。第一,member[i] < member[j] ? member[i] : member[j]是取小操作,代表同时满足两个前提条件的可信度。这里是7×7的规则表,每个e的集合和每个ec的集合组合出一条规则,所以会有49条规则同时参与评估。第二,解模糊用重心法,输出是一个在[-6,6]区间的量,再乘上各自的量化因子,变成真正的Kp、Ki、Kd修正量。量化因子在调用的时候由外部传入,比如deltaKp = Fuzzy_Defuzz(me, rule_kp, 7, 7) * Kp_scale;。第三,规则表的存储格式是一维数组,比如int rule_kp[49] = {...},顺序是按e从NB到PB,ec从NB到PB展开。初始化模糊参数时,Kp_scale我一般取0.2~0.5,也就是模糊修正量最多改变原Kp的50%。如果这个值给得太大,系统容易在稳态附近反复震荡。

3.4 参数说明与调参流程

这套模糊PID一共需要人工设置三类参数。第一类是PID基准值,也就是模糊控制器没介入时的初始Kp、Ki、Kd。第二类是量化因子,包括e和ec的输入量化因子、三个输出的缩放因子。第三类是规则表,一般来说不需要全改,只要动少数几个格子就能改变系统性格。

调参顺序我建议这样走:先关掉模糊控制器,把固定PID调到“能转但不一定稳”的程度。这个阶段只调基准值,让系统有50%以上的响应能力,但不追求无超调。然后再启用模糊控制器,从小到大逐步加输入量化因子,观察阶跃响应。如果超调大,就加大ec的量化因子,让模糊控制器对误差变化更敏感;如果响应太慢,就增加误差量化因子和Kp缩放因子。每一步改完都要记录一次阶跃曲线,不要同时改两个参数,否则事后你根本不知道是哪个改动起了作用。

4. 速度闭环实现:编码器测速与PWM输出

4.1 编码器测速:TIM编码器模式与脉冲计数读取

速度反馈的精度直接决定模糊PID能不能稳住。编码器模式读取计数不要用外部中断,STM32的定时器编码器模式是硬件正交解码,CPU不参与计数过程,不会漏脉冲。读取计数值的接口也很简单:

int16_t Encoder_GetCount(void) { int16_t raw = (int16_t)TIM2->CNT - 0x8000; TIM2->CNT = 0x8000; return raw; }

这段代码先把CNT减去0x8000得到带符号的增量,然后把CNT重置回0x8000。注意数据类型一定要是int16_t,因为0x8000在寄存器里是0x0000,减0x8000后是一个负数,赋值给uint16_t就变成65535了。我在这上面踩过一次坑,编码器读数从-1变成65535,PID直接当正误差处理,电机猛加速。

测速周期要和控制周期保持一致。我控制周期取10ms,那么当前转速等于count / (10ms * 编码器每圈脉冲数 * 减速比) * 60,单位是转/分钟。实际代码里我不会每次算浮点除法,而是预先算出一个脉冲到转速的换算系数,直接乘一下。比如编码器每转390个脉冲,10ms周期,转速系数就是1.0f / (0.01f * 390) * 60.0f,算出来约等于153.8。这样每条指令就一次乘法,速度很快。

4.2 PWM输出与电机驱动控制

PWM输出使用通用定时器TIM3。在速度闭环中,PWM占空比和电机转向分别由两路信号控制。驱动板的IN1、IN2决定转向,PWM引脚决定速度大小。要么IN1为高、IN2为低,电机正转;反之为反转。PWM占空比始终是正数,范围0到ARR。

电机控制函数的接口设计很关键,我一般是这样:

void Motor_SetDuty(int16_t duty) { if (duty >= 0) { IN1_HIGH(); IN2_LOW(); } else { IN1_LOW(); IN2_HIGH(); duty = -duty; } if (duty > 999) duty = 999; TIM_SetCompare1(TIM3, (uint16_t)duty); }

duty为负时互换转向并将绝对值作为占空比。这里有个细节:切换转向时,如果占空比线性变化,驱动板输出的电压方向会瞬间翻转,电机电流会非常大。所以速度环代码里一般要加一个duty_change_max限制,比如每10ms最多允许占空比变化50,这样换向时电流是缓慢爬的,不会触发驱动芯片过流保护。

PWM定时器初始化里还有两个隐藏参数。一是ARR和PSC的关系,PSC=72-1,ARR=1000-1,频率就是72MHz / 72 / 1000 = 1kHz,太低了会听到噪声,我把PSC改成72,ARR改成999,频率是20kHz,听不到声音但驱动芯片的开关损耗会略大一点。二是互补输出和死区时间,在这个项目里用不到,别开。

4.3 主循环与控制周期调度

控制节奏必须固定,不能在while(1)里随手调用PID,否则不同负载下控制周期会漂移。这个项目用的是定时器中断触发控制,Systick只负责维护系统时钟和串口调试。控制中断里执行的是最小集合:读编码器、算速度、调模糊PID、输出PWM。所有耗时操作,比如串口打印、OLED显示,都挪到主循环里。

// TIM4中断服务函数 void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) { int16_t cnt = Encoder_GetCount(); float speed = (float)cnt * speed_factor; float dOut = SpeedLoop_Update(target_speed, speed); Motor_SetDuty(last_duty + (int16_t)dOut); TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } }

SpeedLoop_Update内部会调用模糊控制器更新Kp、Ki、Kd,然后调用增量式PID计算增量。注意target_speed是全局变量,可以被上位机指令修改,在中断里读取时不需要特殊保护,因为它只是一个16位或32位的简单值。但如果你在上位机线程里不停写、中断里不停读,就可能读到撕裂的中间状态。简单的处理是把target_speed声明成volatile,或者用临时变量缓冲一下。这个项目资源里用的就是volatile,够用。

主循环里主要做状态机和调试输出。状态机一般分为“停止”“软启动”“运行”“刹车”四种状态。软启动阶段,目标速度不是一步给满,而是按斜坡从0增加到设定值,这样模糊PID不会在启动瞬间收到一个巨大的阶跃误差,运行更平顺。

5. 避坑记录:模糊PID调速的5个常见问题

5.1 电机只朝一个方向转,反转失效

现象是给定负数目标速度,电机还是正转,或者反转时速度只有正转的一半。原因多半是编码器计数方向判断错误。增量式PID输出的dOut为负时,占空比要加到PWM上,此时PWM基值减小,电机速度应该下降。如果编码器计数的符号和实际转向反了,PID看到的误差方向是反的,它会认为正转速度越高越好,于是一直正向加速。

解决方法是把编码器A、B相接线互换,或者在编码器初始化时把TI1和TI2的极性问题换个方向。我用的是后者,不改线,只改TIM_EncoderInterfaceConfig里的两个TIM_ICPolarity参数。如果A相接反,把两个都从Rising改成Falling,计数方向就会翻转。还有一种更快的验证方式:手动转动电机轴,看串口打印的计数正负,和你设定的转动方向是否一致。不一致就先极性,再动软件。

5.2 编码器读数跳变甚至出现突刺

现象是速度曲线里每隔几十毫秒冒出一个尖峰,PID误以为电机瞬间转得飞快。原因大概率是编码器线和电机线绑在一起,电机电流变化时电磁干扰串进编码器信号,导致正交解码器捕获到虚假脉冲。初期我以为是代码bug,查了很久,最后发现是接线问题。

解决方法是把编码器信号线改成绞合线,远离电机电源线;在编码器电源引脚加一个0.1uF电容,如果信号比较长,再加RC滤波。也有人用定时器输入捕获的滤波功能,在GPIO配置里加上GPIO_InitStruct.GPIO_Filter,但这在F103的编码器模式里支持有限,不能完全依赖。硬件上把干扰源分开,比任何软件滤波都有效。

5.3 模糊规则表无效,输出像普通PID

现象是启用了模糊控制器之后,系统响应和没启用时完全一样,或者只在极个别工况下有一点点变化。原因很可能是量化因子没配置对,输入量化因子太大或输出缩放因子太小。比如误差范围是±500 RPM,输入量化因子你配了0.001,那映射到[-6,6]区间后,正常误差下的值只有-0.6到0.6,永远落在ZO附近,模糊控制器基本不产出修正量。

解决方法是先打印量化前后的e和ec值。我调试时会加一个调试接口,把raw_e和fuzzy_input_e同时打印出来,确认在最大阶跃误差下,fuzzy_input_e能到±4左右。如果到不了,就把量化因子往上调。输出缩放因子我们之前说的0.2~0.5,这里不要给太小,否则Kp修正量只有0.01,PID基准值完全主导,模糊看起来就是摆设。

5.4 高速时速度抖动,低速时爬行

现象是中速很稳,转速一高就细碎抖动,转速一低就一顿一顿。高速抖动主要原因是PWM占空比分辨率不够,ARR设成999时,每格占空比变化是0.1%,输出力矩阶梯感明显。解决方法是把ARR提高到1999,PSC保持72不变,PWM频率变成约18kHz,占空比分辨率翻倍,抖动会明显下降。但要注意,ARR变了,限幅上限也要跟着改成1999。

低速爬行是增量式PID的一个天生弱点:误差很小时dOut也很小,可能一直小于占空比一格的量,电机就停在那里不动。解决办法是给增量式PID加一个“死区补偿”:|dOut|小于某个阈值时,把它提升到最小可驱动占空比。这个阈值要根据你的电机空载启动占空比实测,比如那台电机需要35%占空比才开始转,那么低速段补偿到30%左右,再用PID微调。

5.5 在线整定后系统发散

现象是模糊PID运行一段时间后,突然开始震荡,然后速度失控。常见原因有两个:一是输出限幅没做好,PWM占空比顶到100%后,增量式PID的积分项还在往上累加,但实际执行已经到顶,等到误差反向时,积出的那一大坨又吐出来,系统自然发散。解决方法是强制对实际占空比做限幅,并且让积分增量在接近限幅时逐渐减少,这就是简单的抗积分饱和。

另一个原因是模糊规则表里Kd修正量和Ki修正量在区间重叠处冲突。e为正、ec为负时,表格里Kp还在增大,同时Kd也在增大,两者叠加会让系统在某几个点出现突变。解决方法是把规则表画成曲线看一眼,如果发现相邻两格之间的修正量差值超过两个等级,就做一下平滑。模糊PID不是规则越多越好,表格里每一条突变都可能是一个不稳点。

6. 二次开发:从速度环到位置环,往上再走一步

6.1 把速度环输出变成位置环的内环

如果你做完了速度闭环,最自然的扩展是把速度环当作位置环的内环。位置环控制周期可以放慢到20ms或50ms,因为位置环不要求太快的响应,相反太快会和速度环打架。位置环用简单的比例控制就能工作:位置误差 = 目标位置 - 当前累计位置,然后输出目标速度给速度环。目标速度限幅要设成速度环允许的最大值的70%,否则内环跟不上外环,位置会过冲。

int32_t position = 0; // 由速度积分而来 float pos_ctrl_kp = 2.5f; void PositionLoop_Update(float target_pos) { float pos_err = target_pos - (float)position; float speed_ref = pos_err * pos_ctrl_kp; // 限幅到速度环允许范围 speed_ref = CLAMP(speed_ref, -300.0f, 300.0f); target_speed = (int16_t)speed_ref; }

位置环P控制不要给太大,Kp大了系统会震荡;Kp小了会慢慢爬但也能到位。这个项目里我还会把位置环输出串一个低通滤波,目的不是平滑,而是让速度环的输入不会突变。位置环改好后,你可以把小车的轮子转圈数变成精确的V型停靠,或者做一个小型云台的回中动作。

6.2 把模糊规则表改成可视化调整

资源里的规则表是二维数组,直接在代码里改数组不直观。我的习惯是导出一份CSV,用表格软件打开,行是e,列是ec,表格里是输出值的序号。改好后再转成数组替换。每次调整完,我还会把模糊查询时命中的两个集合编号打印出来,确认期望调整的那条规则确实被激活了。比如想让误差大时Kp更强,就把(PM, ZO)和(PB, ZO)那两个格子的值从PM改成PB,而不是全表改。

如果嫌灰度重心法太占Flash,可以试试把整张模糊映射表预先算好,存成一个一维数组,输入e和ec量化后直接查索引取输出。这样做运算量减少到几乎为零,代价是规则变化时不能实时生效。对固定工况的项目,这个预计算方案更实用;对需要在线调参的,保留原方案。

6.3 验证方法:用阶跃记录代替肉眼感受

改完任何参数,都要给系统做一次可量化的验证。最简单的是串口打印目标速度和实际速度,间隔100ms输出一次,然后在Excel里画t-v曲线。看三个指标:超调量、调节时间、稳态误差。模糊自整定PID的验收标准不是“看起来不抖”,而是同一套参数下,空载和带载两组曲线的超调量都能压在20%以内,稳态误差在10RPM以内。

做完这些,我再把模糊规则表导出来对着表格审视一遍。每次改完代码,我习惯强制自己记录一次“改了哪条规则、量化因子改到多少、PWM限幅是多少”,再让系统跑一轮完整的负载扰动测试。从那次因为没记录,第二天忘了调了哪个参数导致系统重新发散之后,这个习惯就固定下来了。希望这套拆解思路也能帮你少走一段弯路,跑的时候不翻车。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询