ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103模糊自整定增量式PID电机调速实战解析

STM32F103模糊自整定增量式PID电机调速实战解析

简介:面向STM32电机控制学习者的模糊自整定增量式PID速度控制工程,基于STM32F103ZET6与编码器直流电机搭建,采用正点原子代码风格,分模块组织并附有详细注释,适合作为课设、毕设或入门进阶的参考。相比普通增量式PID,该方案利用模糊控制在线整定比例、积分、微分参数,以改善电机调速的动态响应和抗干扰能力,为理解增量式PID与模糊控制的结合提供了较完整的代码实现。压缩包共170个文件、约4.74MB,包含39个h头文件、38个c源码文件,以及uvprojx工程、hex/axf烧录文件、map映射文件、bat清理脚本等编译与调试产物,工程结构清晰,可在Keil中直接编译运行。已有4282人浏览学习,代码经作者实测稳定可靠,对掌握定时器编码器测速、增量式PID计算流程和模糊整定方法具有直接的参考价值。

1. STM32F103 模糊自整定增量式 PID:先把转速调稳,再谈省事

做坏过三块板子后我的结论是:固定参数 PID 能跑,但它只能服务一套工况,负载一变就要重新手摸参数。STM32F103 上做模糊自整定增量式 PID 控制电机速度,思路是让控制器根据当前转速误差和误差变化率,实时修正 Kp、Ki、Kd,不是替你省掉 PID,而是把调参这件事从反复试凑变成查表加推理。它解决的是起步大误差时快速逼近、稳态小误差时消除静差、突加负载时不明显超调这三个典型问题。适合刚接触速度环的小车和桌面机器人的开发者,也适合想了解模糊控制如何落到单片机上的人。下面按我自己拆这套工程的过程,从原理到代码到翻车点全过一遍。

2. 增量式 PID 与编码器测速:控制节奏和反馈精度谁先谁后

先明确一件事:模糊自整定只是把三个系数改活,底层速度环仍然是增量式 PID,反馈仍然来自编码器。这章先把这层地基立住,后面查表才有意义。

2.1 为什么电机速度环用增量式而不是位置式

位置式 PID 输出的是占空比绝对量:

u(k) = Kp * e(k) + Ki * Σe(i) + Kd * (e(k) - e(k-1))

它把历史误差全部累加在积分项里,启动时偏差大会让积分项堆得很高,一旦误差反向,积分项要从很高的绝对值往回退,现象就是大超调。增量式 PID 输出的是占空比的变化量:

Δu(k) = Kp * (e(k) - e(k-1)) + Ki * e(k) + Kd * (e(k) - 2*e(k-1) + e(k-2)) u(k) = u(k-1) + Δu(k)

注意输出量的历史通过 u(k-1) 累积在控制器内部,增量式公式里没有显式积分累加项,所以不会出现位置式那种积分深度饱和的情况。电机速度环被控对象自带惯性,每周期只做小幅调整,比每次重置绝对占空比更平顺。

增量式不是没有代价。如果输出限幅不合理,或者编码器方向反了,u(k) 会朝一个方向一直累加,表现出来就是电机越跑越快直到满转。代码里必须有输出限幅,这不是可选项。

常见做法是在速度环里做对称限幅:

#define PWM_PERIOD 7199 // ARR 值,对应 10kHz #define PWM_MAX_DUTY 7199 // 上限,对应 100% #define PWM_MIN_DUTY 0 // 下限,对应 0% float u_out = 0.0f; // 增量式输出累加 u_out += du; // 输出限幅 if (u_out > PWM_MAX_DUTY) { u_out = PWM_MAX_DUTY; } else if (u_out < PWM_MIN_DUTY) { u_out = PWM_MIN_DUTY; } TIM1->CCR1 = (uint16_t)u_out;

逻辑说明:du 是增量式 PID 算出的占空比变化量,u_out 是积分后的绝对占空比,限幅在赋值给 CCR1 前完成。这里最容易被忽略的是 PWM_MIN_DUTY 设成了 0,导致电机启动需要克服静摩擦力时总从零爬升,低速响应会慢半拍。我一般会在启动阶段叠加一个最小占空比偏置,跑起来后再交给 PID 接管。

参数说明:PWM_PERIOD 由定时器重装载值决定,7200 个计数单位对应 10kHz PWM。PWM_MAX_DUTY 和 ARR 相等时占空比 100%,实际使用建议留 5% 裕量,防止占空比饱和后增量式输出继续累加,退出饱和时延迟增大。

2.2 编码器测速的 M 法与 T 法:低速直接决定模糊表能不能用

STM32F103 的定时器编码器模式把方向判别和计数都做进硬件里,读 CNT 寄存器就能拿到位置增量,这个模块本身不复杂,复杂的是测速方法选错后速度反馈全是毛刺,模糊自整定会把毛刺当成真实误差变化率去调 Kd,结果越调越抖。

M 法,固定时间窗口内数脉冲个数。适用于每个控制周期内脉冲数较多的场合,一般要求不低于 20 个,否则分辨率不够。

T 法,测量相邻脉冲的时间间隔,低速下脉冲少反而有效。适用于低速场合,但每个周期都要捕捉捕获事件,逻辑复杂一些。

M 法换算公式如下:

speed_rpm = (pulse_count / pulses_per_rev) * (60 / control_period_s)

假设电机是 11 线霍尔编码器,减速比 1:30,则每转总脉冲数 11 * 30 = 330。目标是 60rpm 时每秒转一圈,也就是每秒 330 个脉冲;如果控制周期 10ms,每周期只有 3.3 个脉冲。10ms 窗口 M 法测出来不是 60rpm,而是 3 个或 4 个脉冲对应的 54rpm 或 72rpm,这就是低速抖动和模糊表乱跳的根源。

所以工程上常见做法是先算一下自己系统的"每周期脉冲数"再决定测速方式:

// 每转脉冲数:编码器线数 * 减速比 #define ENCODER_LINES 11 #define GEAR_RATIO 30 #define PULSES_PER_REV (ENCODER_LINES * GEAR_RATIO) // 330 // 目标最低转速下的每周期脉冲数预算 #define MIN_SPEED_RPM 60 #define CONTROL_PERIOD_MS 10 #define MIN_PULSES_PER_LOOP \ (MIN_SPEED_RPM * PULSES_PER_REV / 60 / (1000 / CONTROL_PERIOD_MS))

逻辑说明:先按项目里最低工作转速算出一个周期能拿到多少脉冲,如果算出来小于 10 个,就要么提高编码器线数,要么换 T 法,要么把控制周期拉长。脉冲数太少时,速度反馈量化噪声会传导到 ec 计算,模糊控制规则对噪声又敏感,后面 Kd 修正项会周期性跳变。

参数说明:PULSES_PER_REV 必须和实际减速比一一对应。编码器是轴端编码器还是输出轴编码器会差很多,不少拿到手的电机标称减速比和实测值有偏差,校准方法在最后一章给。

2.3 定时器编码器模式初始化:不是所有定时器都能数正交脉冲

STM32F103 的 TIM1 到 TIM4 才带编码器接口,不是所有定时器都能当编码器用。常用分配是把 TIM3 或者 TIM4 让给编码器,TIM1 或者 TIM2 做 PWM 输出。我这里用 TIM3 做编码器,TIM1 做 PWM,标准外设库初始化如下:

void Encoder_Init_TIM3(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_ICInitTypeDef ic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA6 = TIM3_CH1,PA7 = TIM3_CH2,编码器 A/B 相接这里 gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); // 16 位计数器,向上计数,最大计数范围 0xFFFF tim.TIM_Period = 0xFFFF; tim.TIM_Prescaler = 0; tim.TIM_ClockDivision = TIM_CKD_DIV1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &tim); // 两个通道都做输入捕获,TI1 与 TI2 配合成 4 倍频 ic.TIM_Channel = TIM_Channel_1; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x0F; TIM_ICInit(TIM3, &ic); ic.TIM_Channel = TIM_Channel_2; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; ic.TIM_ICSelection = TIM_ICSelection_IndirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x0F; TIM_ICInit(TIM3, &ic); // TI1 和 TI2 都计数,4 倍频模式 TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); }

逻辑说明:TIM_EncoderMode_TI12 模式下 A/B 两路正交信号都被纳入计数,计数器在四个边沿都翻转,结果相当于硬件的 4 倍频。ICFilter 设到 0x0F 可以滤掉编码器在换向瞬间产生的毛刺。每次读取后用 CNT 清零,得到的就是控制周期内的脉冲增量。

参数说明:TIM_Period 设成 0xFFFF 意味着计数器在 65535 处回绕。对 16 位增量不做符号扩展的话,超过半个周期的正方向增量会被读成负数。低速下建议减小 TIM_Period,比如设成 2000,再用 int16_t 强转读回来,这样回绕窗口和实际增量范围匹配,不容易出符号错乱。

读取速度的核心代码:

int16_t enc_count = (int16_t)(TIM3->CNT); TIM3->CNT = 0; float speed_rpm = (float)enc_count * 60.0f / PULSES_PER_REV / (CONTROL_PERIOD_MS / 1000.0f);

逻辑说明:CNT 先强转成 int16_t,是为了把 65535 回绕转成有符号增量,前提是单个控制周期内的增量不会超过 32767。速度公式就是把脉冲数先换成圈数,再除以周期换算成转每分钟。CONTROL_PERIOD_MS 必须和控制循环实际执行周期一致,否则转速整体按比例偏大或偏小。

3. 模糊自整定怎么落地:量化因子、49 条规则表和 Sugeno 推理

模糊自整定这层要解决的工程问题是:给定一组 PID 基准值,根据实时误差 e 和误差变化率 ec,输出三个修正量 ΔKp、ΔKi、ΔKd。不要把它想成多高深的东西,落在 F103 就是三个二维表加上一点加权运算。这章直接给出一套能抄的表和推理代码。

3.1 输入量化与论域选择:先定边界再谈隶属度

物理量不能直接进规则表。误差 e 的单位是 rpm,误差变化率 ec 的单位是 rpm/s,必须各自缩放到标准论域 [-3, 3]:

typedef struct { float Ke; // 误差量化因子 float Kec; // 误差变化率量化因子 float limit; // 论域边界,默认 3.0 } FuzzyInput; // 将物理量量化到 [-limit, limit] float FuzzyQuantize(float x, float scale, float limit) { float q = x * scale; if (q > limit) q = limit; if (q < -limit) q = -limit; return q; }

逻辑说明:量化因子的物理含义是论域半宽除以实际物理量最大幅值。比如速度误差经常在 ±300rpm 以内,Ke 取 3.0 / 300 = 0.01;误差变化率正常不超过 ±1000rpm/s,Kec 取 3.0 / 1000 = 0.003。把 e 和 ec 先限幅再缩放,可以避免偶发毛刺把输入推到论域之外。

参数说明:limit 不一定要 3,取 2 也行,但是量化档位变密后规则表灵敏度会增加,建议先用 3,跑完阶跃再调。Ke 偏大会让系统等效 Kp 放大,偏小会让小误差根本到不了中心档位,稳态精度下降。

3.2 规则表设计逻辑:大误差快追,小误差细调

规则表是模糊自整定里的"黑匣子",很多人照着抄但不知道为什么这样写。核心原则是三条:

误差大的时候,Kp 要给大,让系统快速靠近目标;Kd 要放小,防止起步就被微分项压住;Ki 也要适当放小,避免积分累积过头。误差中等的时候,Kp 收一点,Kd 回到适中,让系统平滑过渡。误差小的时候,Kp 继续减小,Kd 略微加大用来抑制稳态抖动,Ki 加大用来消除静差。

下面是常用的 Kp 修正规则表,行是 ec,列是 e,取值是输出论域上的离散档位,NB 对应 -3,NM -2,NS -1,ZO 0,PS 1,PM 2,PB 3:

ec \ eNBNMNSZOPSPMPB
NBPBPBPMPMPSPSZO
NMPBPBPMPSPSZONS
NSPMPMPMPSZONSNS
ZOPMPMPSZONSNMNM
PSPSPSZONSNSNMNM
PMPSZONSNSNMNMNB
PBZONSNSNMNMNBNB

注意这张表沿主对角线呈反对称结构,这个结构保证了误差为正和误差为负时修正方向相反,不会出现某个象限里参数越调越紧导致系统自发振荡。

Ki 和 Kd 表类似。Ki 表在中间 ZO 列处给正向增益,因为稳态附近需要 Ki 拉大来消静差;Kd 表在设计上要注意,ec 大的时候输出档位不要给太高,不然速度反馈的量化噪声会被放大成高频抖动。

3.3 模糊推理与重心法去模糊的 C 实现

规则表存的是输出档位,需要转成实际修正量。完整 Mamdani 推理在 F103 上也就是三重循环加浮点乘除,控制周期 5ms 到 10ms 完全跑得动。F103 没有 M4 的 FPU,float 运算是软件算的,一个推理周期实测在几十到一百多微秒量级,对速度环不是瓶颈。

核心推理函数:

// 三角形隶属度函数,center 为中心位置,half_width 为半宽 float Trimf(float x, float center, float half_width) { float d = fabsf(x - center); if (d >= half_width) return 0.0f; return 1.0f - d / half_width; } // 计算输入 x 在 [-3,3] 上七个模糊子集的隶属度 void FuzzyMembership(float x, float m[7]) { for (int i = 0; i < 7; i++) { m[i] = Trimf(x, (float)(i - 3), 1.0f); } } // Sugeno 式重心推理,rule 是 7x7 规则表 float FuzzyInfer(float e, float ec, const int8_t rule[7][7]) { float me[7], mec[7]; float num = 0.0f, den = 0.0f; FuzzyMembership(e, me); FuzzyMembership(ec, mec); for (int i = 0; i < 7; i++) { for (int j = 0; j < 7; j++) { // 取小运算作为规则强度 float w = me[i] < mec[j] ? me[i] : mec[j]; if (w <= 0.0001f) continue; // 输出模糊子集在这里用单值重心近似,即规则表档位 float zc = (float)rule[i][j]; num += w * zc; den += w; } } if (den < 0.0001f) return 0.0f; return num / den; }

逻辑说明:每个输入在七个模糊子集上都有隶属度,一条规则同时对 e 的某个档位和 ec 的某个档位生效,规则强度取两者较小的那个,这就是取小运算。输出不是只取激活度最高的一条规则,而是把 49 条规则的加权平均值算出来,这样输出是连续的,不会出现档位跳变。

参数说明:Trimf 的 half_width 取 1.0,意味着相邻模糊子集在中心点交界处恰好相交,任何输入最多激活相邻两个子集,其他隶属度为 0。这个设置保证了推理输出平滑。如果你想要系统更灵敏,把 half_width 调小到 0.75;更迟钝则调大到 1.25。

4. STM32F103 工程落地:引脚分配、控制循环和 PWM 输出映射

原理讲完进入实际工程。这套东西不复杂,但引脚分配和定时器资源规划一旦出错,硬件上很难查,先说清楚布局再贴代码。

4.1 定时器资源规划与引脚映射

STM32F103C8T6 只有 TIM1-TIM4 支持编码器接口。把编码器放在 TIM3,PWM 放在 TIM1,这个分配最省心:

功能定时器/外设引脚说明
编码器 A 相TIM3_CH1PA6外部输入
编码器 B 相TIM3_CH2PA7外部输入
PWM 输出TIM1_CH1PA810kHz,接驱动模块 IN
PWM 方向普通 GPIOPB0控制电机正反转
串口调试USART1PA9/PA10转速打印、参数观察

TIM1 是高级定时器,输出 PWM 前需要配置刹车和死区相关寄存器,否则 CCR1 写入不生效,这是新手最容易卡住的地方。如果不想碰高级定时器的输出控制位,把 PWM 挪到 TIM2_CH1 的 PA0 也行,普通定时器没有刹车机制,省事很多。

4.2 10ms 速度环主循环:读编码器、算模糊、跑增量式

控制周期我一般按 5ms 到 10ms 选。速度环比电流环慢一个数量级,10ms 足够。核心循环写成周期调度形式,不要在 while 里直接延时,否则串口打印和编码器读取会互相拖延:

volatile uint16_t g_tick_1ms = 0; void SysTick_Handler(void) { g_tick_1ms++; } // 主循环,10ms 执行一次速度环 int main(void) { // ... 外设初始化略 uint16_t last = 0; float target_rpm = 120.0f; float e_prev = 0.0f, e_prev2 = 0.0f; float speed = 0.0f; float u_out = 0.0f; // 基准 PID 值和输出步进 const float kp_base = 0.08f; const float ki_base = 0.02f; const float kd_base = 0.01f; const float kp_step = 0.02f; const float ki_step = 0.005f; const float kd_step = 0.003f; while (1) { if ((uint16_t)(g_tick_1ms - last) >= 10) { last = g_tick_1ms; // 读取编码器增量并换算为转速 int16_t cnt = (int16_t)TIM3->CNT; TIM3->CNT = 0; speed = (float)cnt * 60.0f / PULSES_PER_REV / 0.01f; // 计算误差与误差变化率 float e = target_rpm - speed; float ec = (e - e_prev) / 0.01f; // 量化后查模糊表,得到修正档位 float eq = FuzzyQuantize(e, 0.01f, 3.0f); float ecq = FuzzyQuantize(ec, 0.003f, 3.0f); float dKp = FuzzyInfer(eq, ecq, KpRuleTable); float dKi = FuzzyInfer(eq, ecq, KiRuleTable); float dKd = FuzzyInfer(eq, ecq, KdRuleTable); // 基准值加修正值 float kp = kp_base + dKp * kp_step; float ki = ki_base + dKi * ki_step; float kd = kd_base + dKd * kd_step; // 增量式 PID float du = kp * (e - e_prev) + ki * e + kd * (e - 2.0f * e_prev + e_prev2); u_out += du; if (u_out > PWM_MAX_DUTY) u_out = PWM_MAX_DUTY; else if (u_out < PWM_MIN_DUTY) u_out = PWM_MIN_DUTY; e_prev2 = e_prev; e_prev = e; // 占空比映射到比较寄存器 TIM1->CCR1 = (uint16_t)u_out; } } }

逻辑说明:控制周期 10ms,每次读取编码器增量后立即清零,保证下次窗口从零开始计数。ec 的计算是把两次误差差除以周期,得到每秒误差变化量。模糊推理直接操作量化后的 [-3,3] 数值,输出档位通过 step 系数还原成 PID 参数修正量。

参数说明:kp_base 是固定参数 PID 调试出的基准值,模糊表在它附近摆动。kp_step 决定自整定的修正幅度,0.02 表示最大修正 ±0.06,也就是基准值在实际运行中最多偏移 75%。如果发现负载突变时响应不够,加大 kp_step;如果出现抖动,减小它。注意每个控制周期都是从基准值重新计算,不是累加修正,这样模糊表输出偶发跳变时不会把 PID 参数一直带偏。

4.3 PWM 频率与占空比映射:CCR1 写错就是半速运行

F103 主频 72MHz,ARR 取 7200 得到 10kHz 的 PWM 频率。这个频率下电机电磁噪声小,驱动芯片开关损耗也可以接受。PWM 初始化:

void PWM_Init_TIM1(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Pin = GPIO_Pin_8; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); tim.TIM_Period = arr; // 7200 tim.TIM_Prescaler = psc; // 0 tim.TIM_ClockDivision = TIM_CKD_DIV1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &tim); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 0; oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &oc); // 高级定时器必须开启主输出,否则 PA8 没有波形 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }

逻辑说明:TIM_CtrlPWMOutputs 在高级定时器上是必须的,它打开 MOE 主输出,少了这一行 PA8 永远是低电平,电机一动不动还查不出为什么。PWM 模式 1 表示计数器小于 CCR1 时输出高电平,占空比 = CCR1 / ARR。

参数说明:占空比输出是 u_out,单位是计数单位而不是百分比。有些人习惯把 PID 输出限幅在 0~100 再赋值给 CCR1,导致电机永远在很低的占空比下运行,表现为转速整体偏小且加载后掉速明显。正确的做法是速度环输出直接以 CCR 计数单位为基准,限幅到 0 和 ARR 之间。

5. 避坑记录:五个让电机失控或抖动的实际问题

这章是我自己调试时踩过的坑,每一条都跟着现象走。如果你照着上面代码改完还是不稳定,先过一遍这里。

5.1 现象:电机上电直接朝一个方向加速到满转,速度环完全没起作用

原因典型有两个方向。一是编码器 A/B 相接反了,电机实际正转时计数器在减数,速度反馈是负值,误差 e 始终大于零,模糊表不断输出正向 Kp 修正,PWM 越给越大。二是 PWM 和电机驱动方向控制不匹配,占空比使能方向和编码器计数方向相反。

解决先把编码器 A/B 对调,或者软件里把增量取反:cnt = -cnt。然后在没接负载的情况下手动开环给一个固定占空比,串口看编码器读数,确认占空比增加时计数方向符合预期。

5.2 现象:电机转起来了,但实际转速只有目标的一半,PID 输出却已经接近 100%

这个坑隐蔽在映射关系上。跑回环之前占空比低于 50% 时电机其实能全速转,如果 PWM 占空比不是从 0% 到 100% 线性对应到电机电压,而是驱动模块 IN 引脚还叠加了使能信号,会吃掉一部分有效占空比。

解决量一下电机端电压和占空比的关系,确认驱动模块的输入是独立的 PWM 还是需要使能引脚一直拉高。常见的驱动模块使能脚如果悬空或接错引脚,PWM 输入只会让输出端出现"半开"状态。

5.3 现象:目标 60rpm 时电机哒哒响,波形上转速在 50rpm 和 70rpm 之间跳

M 法测速在低速下的波长问题。每转 330 个脉冲,60rpm 时 10ms 窗口内只有 3.3 个脉冲,计数要么 3 要么 4,换算出来相差 18rpm,反馈本身就带大台阶。

解决这个场景建议把编码器 4 倍频利用起来。TIM_EncoderMode_TI12 已经把每转脉冲放大到 1320,10ms 窗口内 13 个脉冲,分辨率提升了四倍。如果还是不够,把控制周期拉到 50ms,但这会让速度环响应变慢,模糊表对负载突变的抵抗能力会变差。另一个方案是换 T 法测速,不过 T 法在 F103 上要占用输入捕获中断资源,代码复杂度上一个台阶。

5.4 现象:空载波形挺好,一带负载就出现周期性振荡,Kd 修正值剧烈摆动

负载变化导致速度瞬时跌落,ec 突变到论域边界,模糊表给出很大的 Kd 修正,微分项又被编码器量化噪声进一步放大。越调越抖之后模糊表使用率非常高,但实际系统性能比固定 PID 还差。

解决先对 ec 做一阶低通滤波再送模糊表。滤波系数取 0.2 附近:

ec_filt = ec_filt + 0.2f * (ec - ec_filt);

同时把 Kd 修正步进 kd_step 减小一半,或者在规则表上把 Kd 表四角的值压低一档。记住模糊自整定调的是"趋势",不是让 Kd 完全跟随瞬时值。

5.5 现象:串口打印转速波形时,控制循环间歇性卡顿,波形整体被拉慢

printf 往串口发数据时阻塞等待发送完成,发送一帧几十个字节在较低波特率下耗时好几毫秒,直接把 10ms 控制周期拖成长短不一的周期。控制周期不稳,速度环相位裕量下降,模糊表也跟着遭殃。

解决串口输出用 DMA 或者只在特定标志位时才打印一帧。常见做法是维护一个环形缓冲,主循环把格式化好的字符串丢进缓冲,DMA 自己慢慢发,速度环不等待发送完成。或者把打印周期拉长到 100ms 一次,波形虽然略稀但控制周期保持稳定。

6. 三档阶跃验证法:十分钟内确认模糊表有没有调对

调完频率和占空比,最后做一次系统验证。我不建议直接看波形就下结论,每次调完都按三档阶跃走一遍,记录下来的数据比感觉可靠得多。

先把目标转速设成 100rpm,稳态运行 30 秒,记录稳态误差波动范围。再把目标突然切到 200rpm,观察超调量和调节时间。最后切到接近额定转速的 300rpm,检查是否还能达到目标。三档数据填进下表的对应项:

校验项判定标准失败时的排查方向
100rpm 稳态误差波动不超过 ±5rpmKp 基础值偏小,Ki 修正幅度不足
100→200rpm 超调量不超过 20rpmKd 过快介入,模糊表在中等误差区修正过大
200→300rpm 调节时间小于 1.5 秒Kp 基准值偏小,量化因子 Ke 偏小
300rpm 满载保持掉速不超过 30rpm编码器每转脉冲数标称值不准

顺带把编码器每转脉冲数校准一下:电机轴空转,手动拨一圈轮子,从串口读出计数增量,和标称值对比。很多标称 1:30 的减速器实际减速比在 29 到 31 之间波动,按标称值换算出来的转速会有几个百分点的系统偏差。之前一个项目一直抱怨速度控制精度不够,查到最后是减速比偏差 4%,速度环和模糊表本身没问题。

进阶一点的技巧是给模糊输出加一个衰减系数。模糊表给出的修正量先乘一个 0.6 到 0.9 的系数再用,系统会更稳定。这相当于给自整定加阻尼,代价是负载突变时参数响应稍微慢半拍。我习惯在调试初期系数给 0.5,等波形稳定后逐步加大到 0.9。

最后说一个习惯:从那以后,我每次拿到新板子都会先跑一遍三档阶跃,转速稳不下来就强制把 PWM 频率、编码器分频和量化因子全查一遍,不搞玄学调参。固定 PID 参数对照组的波形也要留一版,模糊自整定有没有真的改善性能,对比数据说话。这套流程你照着走一遍,模糊表能不能干活基本十分钟内就能判断出来。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进