STM32直流有刷电机PID速度环:从接线到调参完整指南
2026/9/15 6:47:37 网站建设 项目流程

简介:面向STM32直流电机调速开发者的完整工程包,以L298N驱动直流有刷电机为基础,实现了PID速度环闭环控制,可直接导入Keil等IDE进行编译与烧录。包内共657个文件,涵盖C源文件、头文件、工程配置文件、编译中间文件以及hex烧录文件等,整体大小约18.83MB,目录结构清晰,便于学习与二次开发。工程代码包含PWM调速信号生成、电机速度反馈检测、PID参数整定、L298N正反转控制及调试接口,底层使用标准库与HAL库混合编程,适合有一定STM32基础的入门者或需要快速搭建电机控制平台的开发者,也适用于嵌入式课程设计与毕业设计参考。已有1024人学习下载,对于深入理解电机控制原理、掌握PID算法实际调参过程具有直接参考价值。

1. 为什么这些项目总在“调PID”这一步卡住

做 STM32 控制直流有刷电机的毕业设计或比赛小车,硬件半天就能搭起来:一个 L298N、一个编码器电机、几根杜邦线。真正让人熬夜的永远是速度环——电机要么嗡嗡叫、要么转起来忽快忽慢、要么一上电就疯转。问题通常不出在“PID 没学懂”,而是出在 PWM 频率、编码器采样节拍、积分限幅这些代码里看不见的细节上。这一篇把 STM32 + L298N + 直流有刷电机 + PID 速度环从接线到调参的完整链路写清楚:先讲速度环为什么非做不可,再给一套可直接改用的工程代码骨架,最后用串口和曲线把参数一个个调出来。适合正准备做 STM32 项目、手上有 L298N 和带霍尔编码器的直流减速电机的人,也适合给只知道“PID 三个数”的队友一份能照着操作的检查清单。

2. 直流有刷电机的速度环:PWM 只是开环,PID 才闭环

2.1 有刷电机模型和“速度环”到底是什么

直流有刷电机的等效模型是一阶惯性加积分:电枢电压变化时,电流建立转矩,转矩让转子加速,反电动势又抵消输入电压,最终转速稳定在一个平衡点。开环给 50% 占空比,电机转速不等于空载转速的一半——负载一变、电源电压一跌、温度一升高,转速就跟着掉。速度环做的事情很简单:把编码器采到的实际转速和目标转速做差,用 PID 算出一个 PWM 占空比增量,形成一个负反馈回路。

速度环和电流环的区别值得先说明白:电流环响应快,带宽做到几百赫兹,管的是转矩的瞬态;速度环带宽通常只有几十赫兹,管的是转速平均值。L298N 没有电流采样引脚,所以这个项目里只能做单速度环,把电流环欠掉的账记在 PID 参数头上——这才是后面调参时 Kp 不敢给太大的根本原因。

2.2 位置式 PID 与增量式 PID:裸机工程里我选增量式

位置式 PID 的表达式是:

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

输出 u(k) 直接作为 PWM 占空比。它的问题在于积分项 Σe(i) 需要长期保存,一旦电机堵转、编码器丢脉冲,积分饱和后要好几秒才能退出来,表现就是“电机转疯了停下来还要转一会儿”。增量式 PID 不做积分累加,而是输出每一次控制动作的增量 Δu(k):

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

实际给到 PWM 的是:

u(k) = u(k-1) + Δu(k)

增量式天然带记忆,u(k-1) 就是上一拍的占空比,不显式保存积分项;想要手动清积分或切换目标转速时,直接把 u(k-1) 清零即可。另外增量式天然适合 STM32 的定时器 PWM 输出——只需对 CCR 寄存器做加减,不必担心一个巨大的绝对占空比把系统顶到饱和区。这是纯裸机环境下我优先选增量式的理由;位置式更多配合上位机做离线仿真用。

2.3 采样节拍:PID 不是跑得越快越好

速度环的 PID 计算和 PWM 输出是两个不同的节拍。常规做法是 PWM 用定时器硬件生成(比如 10kHz 到 20kHz),而 PID 用固定频率的定时器中断驱动,通常取 100Hz 到 1kHz。采样频率和电机机电时间常数要匹配:直流减速电机的截止频率一般在 10-50Hz 区间,PID 跑到 1kHz 以上并不是浪费,但前提是编码器测速要跟得上。

编码器测速有两种常见方法,M 法测速和 T 法测速。M 法是在固定时间窗口内数编码器脉冲个数,高速时分辨率高;T 法测两个脉冲之间的时间间隔,低速时分辨率高。做速度环时我常用 M 法,定时器每 5ms 或 10ms 主动读一次编码器计数,然后清零重数。测速周期要放在 PID 运算里考虑——把测速和 PID 都放进同一个定时器中断的同一拍,否则采样时序乱掉,调出来的参数永远不对。

3. STM32 与 L298N 的接线要点:电源、PWM 频率、共地和编码器反馈

3.1 L298N 引脚逻辑与最小接线图

L298N 的使能端(ENA/ENB)接 PWM,输入引脚(IN1-IN4)接普通 GPIO。逻辑关系固定为:IN1=1、IN2=0 时电机正转,反过来反转;ENA 为 0 时无论 IN 状态如何,电机都停止。用定时器通道输出 PWM 时,把这个关系做成一张表会安全很多:

IN1IN2ENA(PWM)电机状态
100停止
1050%正转 50% 占空比
0150%反转 50% 占空比
11x刹车(不建议长期使用)

L298N 的 IN1/IN2 接普通 GPIO,ENA 必须接 STM32 的定时器 PWM 输出引脚,这个引脚需要有 TIMx_CHx 复用功能。注意 IN 引脚的电平翻转要发生在 PWM 占空比的边界上,不要在 PWM 为高电平期间切换方向,否则 H 桥直通短路,轻则发热重则烧片。代码实现上,至少先停 PWM、再切 GPIO、最后重新开 PWM。

3.2 双电源策略:为什么 12V 电机电源不能碰 3.3V 逻辑电

L298N 模块上通常有两个电源端子,VMS(电机电源)和 VSS(逻辑电源)。常见错误是只接一个电源,用板载 5V 稳压输出给 STM32 供电——这在小电流空载测试时能跑,但电机堵转时母线电压跌落,STM32 直接复位或 flash 写入错乱。

我一般的做法是双电源隔离:电机侧使用 9-12V 电源给 L298N 的 VMS,逻辑侧使用 STM32 板载 3.3V,L298N 的 VSS 也接 3.3V(模块逻辑电路经过电平转换后可以接受 3.3V 输入,实测稳定;若你的模块明确要求 5V 逻辑,则用 STM32 板载 5V 引脚供电但必须在 VMS 端并联大电容)。双电源的核心问题是共地,GND 必须全部接在一起,否则 IN 引脚的电平参考地和 L298N 的逻辑地不一致,电机偶发抖动但无法形成稳定 PWM 控制。另外在 VMS 两端并联一个 470uF 电解电容和一个 104 陶瓷电容,这是成本最低的防抖手段。

3.3 编码器接口:定时器编码器模式还是外部中断计数

带霍尔编码器的直流减速电机,通常输出 A、B 两相互差 90° 的方波。判断方向和测速有两类做法:

做法一是把 A、B 相接到 STM32 定时器的 CH1/CH2,启用编码器模式(比如 TIM4_Encoder_Mode),硬件自动根据相位关系加减计数。这是最推荐的方式,不占用 CPU,计数分辨率高,方向自动判。做法二是把单相(只用 A 相)接到 GPIO 外部中断,上升沿计数。这能测速,但无法判断方向,且中断频率高时 CPU 开销大。

从标题的“调式 PID 速度环”目标出发,必须用编码器模式。中断里做太多次数统计会让 PID 中断节拍被干扰——这是新手最常见的隐性坑。下面是编码器模式的定时器初始化和读取代码,注意编码器模式不算 PWM 输出,需要单独用一个定时器:

/* 使用 TIM4 的编码器模式读取电机 AB 相 */ void Encoder_TIM4_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM4, &TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; TIM_ICInit(TIM4, &TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); } /* 读取编码器计数并清零 */ int16_t Encoder_Get_Count(void) { int16_t count = (int16_t)TIM_GetCounter(TIM4); TIM_SetCounter(TIM4, 0); return count; }

配置要点有几个:TIM_Period 设成 0xFFFF 是为了让 16 位计数器不用频繁处理溢出;TIM_EncoderMode_TI12 表示双沿采样,四倍频计数,对减速比 30:1、单圈 13 线的电机来说,每圈计数是 13430 = 1560;TIM_ICFilter 加到 0x0F 是给编码器信号做数字滤波,防止抖动造成误计数。返回的 count 是 int16_t,因为编码器计数可能为负(反转),用无符号类型会导致方向判断失效。

编码器计数溢出时可能出现跨正负边界的问题,这里用 int16_t 强制转换,如果采样周期内计数不超过 32767 就是安全的,一般 5ms 采样周期内不会出现。

4. 速度环代码骨架:定时器测速、PID 运算与 PWM 输出的完整实现

4.1 定时器分配与 PWM 初始化

在 STM32F1 系列中,常见做法是 TIM1 或 TIM2 输出 PWM 给 L298N 的 ENA,TIM4 做编码器计数。注意定时器的通道引脚不能冲突,以 stm32f103c8t6 为例:TIM1 的 CH1 在 PA8,CH2 在 PA9;TIM2 的 CH1 在 PA0,CH2 在 PA1。L298N 还需要两个普通 GPIO 做 IN1、IN2 方向控制,TIM2 的 PA0/PA1 上方刚好可以做事,方便布局。

PWM 频率选 10kHz 比较合适。F103 的定时器时钟是 72MHz,PSC 设为 71,ARR 设为 99,频率就是 72000000 / (72 * 100) = 10kHz。这个频率下有刷电机的电流纹波较小,L298N 的开关损耗也不会太大。频率低于 5kHz 时电机会产生明显噪声,高于 20kHz 后 L298N 的驱动芯片开关损耗上升,容易发热。

/* TIM2 输出 PWM 控制 L298N ENA,PA0 为 CH1 */ void PWM_TIM2_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; 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 - 1; TIM_TimeBaseStructure.TIM_Prescaler = psc - 1; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &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(TIM2, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }

调用时 arr=100、psc=72,这样定时器计数 0~99,共 100 个节拍,每个节拍 1us,PWM 周期 100us,即 10kHz。需要调整 PWM 占空比时,调用TIM_SetCompare1(TIM2, duty),duty 范围 0~99。这里使用 PWM1 模式,当 CNT < CCR 时输出高电平,CCR 从 0 逐渐增大到 99,占空比从 0% 逐渐到接近 100%。

4.2 增量式 PID 的裸机 C 实现

PID 运算要在固定时间节拍里执行,通常用一个基础定时器(比如 TIM6)每 5ms 触发一次更新中断。在这个中断里完成编码器读数、速度计算、PID 运算、限幅和 PWM 更新。写成函数便于在不同工程里复用,参数用结构体封装:

/* 增量式 PID 控制器结构体 */ typedef struct { float Kp; /* 比例系数 */ float Ki; /* 积分系数 */ float Kd; /* 微分系数 */ float target_speed; /* 目标转速,单位 rpm */ float current_speed; /* 当前实际转速 */ float error_last; /* 上一次误差 */ float error_before; /* 上上次误差 */ float pid_out; /* PID 输出,占空比增量 */ float pid_out_last; /* 上一次 PID 总输出 */ float out_max; /* 输出限幅,0~100 */ float out_min; /* 输出下限 */ } IncrementalPID; /* 增量式 PID 核心运算 */ float IncrementalPID_Calc(IncrementalPID *pid, float target, float current) { float error, delta_out, out; error = target - current; delta_out = pid->Kp * (error - pid->error_last) + pid->Ki * error + pid->Kd * (error - 2.0f * pid->error_last + pid->error_before); pid->error_before = pid->error_last; pid->error_last = error; out = pid->pid_out_last + delta_out; /* 输出限幅,防止 PWM 占空比越界 */ if (out > pid->out_max) out = pid->out_max; else if (out < pid->out_min) out = pid->out_min; pid->pid_out_last = out; pid->pid_out = out; return out; }

这段代码里几个参数要注意:

  • Kp 和 Ki 的量纲不一致。Kp 是“每 rpm 误差对应的占空比百分比”,Ki 是“每个采样周期对误差累积的占空比百分比”。如果目标转速从 0 突加到 1000rpm,Kp=0.1 时首拍输出就是 10% 占空比,感觉温和;Kp=1 时首拍就是 100%,电机会瞬间打满。
  • Kd 项用的是二阶差分,作用是抑制误差变化率。编码器测速波动大时,Kd 会给 PWM 叠加高频抖动,所以裸机速度环里 Kd 通常给得极小甚至为 0,先把 PI 调好再加 D。
  • 输出限幅不是只限制最终占空比,还隐含限制了积分深度。由于增量式不做显式积分累加,所以这里的 out 限幅就够了,不需要单独的积分限幅。
  • 代码中的 pid_out_last 保存了上一拍的输出,使系统在目标突变或负载突变时能平滑过渡。

这个函数本身没有方向信息。当需要反转时,把目标转速设为负值即可。电机方向由 IN1、IN2 决定,与 PID 输出的正负无关。PWM 输出的是绝对值,方向由 GPIO 控制,所以还要在调用处做一次符号分离:

void Motor_Set_Speed(float speed) { if (speed > 0) { GPIO_SetBits(GPIOB, GPIO_Pin_12); /* IN1 = 1 */ GPIO_ResetBits(GPIOB, GPIO_Pin_13); /* IN2 = 0 */ TIM_SetCompare1(TIM2, (u16)(speed > 99 ? 99 : speed)); } else if (speed < 0) { GPIO_ResetBits(GPIOB, GPIO_Pin_12); /* IN1 = 0 */ GPIO_SetBits(GPIOB, GPIO_Pin_13); /* IN2 = 1 */ TIM_SetCompare1(TIM2, (u16)(-speed > 99 ? 99 : -speed)); } else { /* 目标速度为 0 时,直接停止 PWM,不要用 IN 引脚刹停 */ TIM_SetCompare1(TIM2, 0); } }

很多工程在转速接近 0 时会让 IN1=IN2=1 做刹停,L298N 的 datasheet 确实有这种模式,但会带来两个问题:一是电感和反电动势形成的制动电流直接灌到 H 桥上,L298N 发热严重;二是刹车解除瞬间的电流冲击会让电源电压产生尖峰。所以速度环输出 0 附近时,不要刹停,直接让 PWM 为 0,让电机自由滑行。如果需要快速停车,应在负方向上给一个短时的反向占空比脉冲,比如 -20% 持续 50ms。

4.3 定时器中断里的完整测速与 PID 执行流程

PID 中断函数要做的事情按顺序排列:读编码器计数、根据线数和减速比换算转速、调用 PID 计算、设置 PWM占空比。这个顺序不能颠倒,因为“先更新测量再计算输出”和“先计算输出再更新测量”都试过,后者会让控制系统多一个采样周期的滞后。

/* 全局变量:目标转速由主循环或串口指令更新 */ volatile int16_t encoder_count; volatile float target_rpm = 600.0f; volatile float current_rpm = 0.0f; volatile float pid_output = 0.0f; IncrementalPID speed_pid; #define PULSE_PER_REV 1560.0f /* 13线 * 4倍频 * 30减速比 */ #define SPEED_SAMPLE_TIME 0.005f /* 5ms 采样周期 */ void TIM6_IRQHandler(void) { if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM6, TIM_IT_Update); /* 1. 读取 5ms 内的编码器脉冲数 */ encoder_count = Encoder_Get_Count(); /* 2. 换算成转速,单位 rpm */ current_rpm = (float)encoder_count * 60.0f / (PULSE_PER_REV * SPEED_SAMPLE_TIME); /* 3. PID 计算,得到 PWM 占空比百分比 */ pid_output = IncrementalPID_Calc(&speed_pid, target_rpm, current_rpm); /* 4. 输出到电机 */ Motor_Set_Speed(pid_output); } }

转速换算公式的来源不复杂:编码器 5ms 内数到 N 个脉冲,一秒钟就是 N200 个脉冲,一分钟是 N200*60,再除以每转脉冲数 PULSE_PER_REV 就是 rpm。这里的 1560 是 13 线编码器、4 倍频、30:1 减速后的数值。如果电机参数不同,这个常量要改,否则转速显示和 PID 参数全部失真。

需要特别提醒一步:中断里调用了 Motor_Set_Speed,这个函数里有 GPIO 操作和 TIM_SetCompare1,如果在 PID 计算前把编码器清零和 PWM 设置放在一个临界区,还要考虑中断嵌套问题。不建议在这个中断里加 printf 做调试——串口打印的耗时可能在毫秒级,严重拉长中断时间。串口调试应放在主循环里,用标志位通知主循环去发送。

5. 串口在线调 PID:开环自测、临界比例度法与参数限幅

5.1 开环自测:先确认 PWM 和转速的关系是线性的

接上电直接调 PID 是浪费时间的开始,必须先做开环测试:手动给定一个 PWM 占空比,测出稳定转速。分别试 10%、20%、30%、50% 的占空比,记录对应的转速值。这里有个严重警告:L298N 在占空比低于 10% 左右时电机可能不转,因为 H 桥驱动芯片的压降和电机启动转矩问题导致死区。这个死区必须测出来并记下,后面 PID 输出限幅时不要让输出长期停留在死区里。

常见的做法是写一个临时测试代码:主循环里读取串口发来的占空比数字(0~100),调用 Motor_Set_Speed,同时把编码器测得转速通过串口发回上位机。这个测试程序可以直接沿用前面的模块,只是 PID 参数全设为 0。开环测试的重点是确认以下几点:

测试项判定标准
方向是否正确给定正占空比,编码器计数为正
PWM 死区值实测 XX%(通常 5-15%),转速才稳定起步
线性度占空比 20%-80% 区间内转速是否近似线性
电源纹波电机高速时 STM32 是否复位或串口乱码

方向判断错误是新手第一坑:PID 输出为正,电机正转,但编码器计数为负,这样算出的误差就是目标减负值,越调越大,最终 PWM 打满。如果测试发现方向反了,两种解法,一种是把编码器 A/B 相对调——硬改硬件最快;另一种是软件里给 Encoder_Get_Count 的返回值取负。

电源纹波问题的判定标准:串口数据乱码、STM32 频繁复位、L298N 上的红色 LED 亮度随转速波动。解决方案已经在第三章说过了:VMS 并联大电容、逻辑电平独立供电、共地。如果复位频繁,还可以在 STM32 的 VDD 引脚附近加 100nF 去耦电容,或者干脆让电机电源远离单片机排线。

5.2 用临界比例度法找到 Kp 和 Ki 的起点

先给 Kd 和 Ki 设 0,只留 Kp。从一个很小的值开始,比如 0.05,然后在运行状态下逐步增大,观察转速响应。临界比例度法的操作是:增大 Kp 直到系统产生等幅振荡——转速围绕目标值上下波动且幅度不衰减。

这个等幅振荡的 Kp 记为 Kr,振荡周期记为 Tr。然后用经验公式算出起始参数:Kp = 0.45 * Kr,Ki = 0.54 * Kr / Tr。这个方法来自 PID 整定的经典经验,但要注意的是:速度环里 Tr 通常是几百毫秒,如果 Ki 算出来太大,就减半再试。

实际操作时序很重要:把目标转速设为电机额定转速的 60% 左右,比如 600rpm。先让电机开环转动到接近目标,再把 PID 切入闭环。不要从静止直接切闭环——PID 输出会先打满,电机猛然起飞,容易把目标转速冲过头。切换瞬间的处理是让 pid_out_last 初始化为当前开环 PWM 值,这样 PID 从当前工作点开始调整,而不是从 0 开始。

串口在线调试的格式建议用逗号分列的文本:目标转速,实际转速,PID输出,PWM占空比,一行一帧。直接用串口助手看数字也能调,但配合简单的串口波形工具(比如匿名上位机或者 SerialPlot)效率会高很多。看到的数据曲线应该长这样:转速从目标值下方爬升,轻微超调后回落,最终稳定在目标转速附近 ±2% 以内。

5.3 参数限幅和防积分饱和的裸机处理

增量式 PID 虽然没有显式的积分项,但 pid_out_last 等效于一个慢速积分器——它记住的是控制量的历史累积。当电机堵转时,误差恒定为正,PID 输出逐渐爬到限幅值,如果此时让电机恢复转动,输出需要从限幅值慢慢退回来,这个过程本身就等价于积分饱和。处理方式很简单,在输出限幅的同时,检查是否达到限幅边界:

if (out >= pid->out_max || out <= pid->out_min) { /* 输出饱和,冻结 pid_out_last,不再让控制量继续累积 */ pid->pid_out_last = pid->out_max_target; /* 用一个接近边界的值替代 */ }

但上面的写法会让系统在退出饱和时失去“记忆”,实际工程中更常见的做法是反算限幅。做法是:当计算出的 out 超过限幅时,把 delta_out 截断,而不是直接截断 out。换句话说,如果 out > out_max,则 out = out_max,然后反推 pid_out_last = out_max——这等价于把超出的增量直接丢弃。由于 pid_out_last 控制下一拍的起点,这一步能让系统在脱离饱和时响应更快。

限幅值的设置:PWM 占空比最大值设为 90% 而不是 100%,因为 L298N 在接近全占空比时发热严重,而且有刷电机的换向火花也会更剧烈。最小值设为 PWM 死区的实测值,通常在 8-15% 之间。这样 PID 输出被限制在工作区间内,不会落入非线性死区。

6. 三个必调的细节:启动防反转、梯形加减速与堵转保护

6.1 启动无反转:目标值的斜坡发生器

直接给一个阶跃目标转速,PID 会先输出一个很大的控制量,电机转速冲过目标值后,PID 又要反向拉回来,表现为启动瞬间“咯噔”一下反转或抖动。这个现象的根源是微分环节对阶跃的响应——误差从 0 跳到 600,Δe 巨大,微分项瞬间爆表。即使 Kd=0,Ki 项也会在误差大的时候给较大的输出增量,导致超调。

解决办法不是调参数而是加斜坡发生器:目标转速不是直接给 600,而是每 10ms 增加 20rpm,300ms 后到达目标值。把斜坡发生器放在主循环里,或者放在 PID 中断前一级:

static float ramped_target = 0.0f; void Speed_Ramp_Task(void) { static uint32_t last_time = 0; uint32_t now = HAL_GetTick(); if (now - last_time >= 10) { if (ramped_target < target_rpm) { ramped_target += 20.0f; if (ramped_target > target_rpm) ramped_target = target_rpm; } else if (ramped_target > target_rpm) { ramped_target -= 20.0f; if (ramped_target < target_rpm) ramped_target = target_rpm; } pid.target_speed = ramped_target; last_time = now; } }

斜坡的斜率取值比 PID 参数更影响启动质感。斜率太慢显得迟钝,太快又回到阶跃的问题。实际调试中从每 10ms 增加 10rpm 起调,观察启动时的电流声——没有明显顿挫感就说明斜率合适。如果目标是频繁加减速的应用(比如小车走直线),斜坡斜率就是加减速的“加速度”,这个值要记录到设计文档里。

6.2 堵转检测:编码器长时间为零时的保护策略

PID 速度环有一个天然的危险场景:电机堵转。目标转速 600rpm,实际转速 0,PID 输出限幅在 90%,占空比持续最大,电机绕组持续通过大电流,L298N 的驱动管温度在十几秒内就能升到烫手。堵转检测的逻辑可以直接放在 5ms 中断里,也可以放在主循环里,逻辑是:

static uint16_t stall_count = 0; /* 每 5ms 执行一次 */ if (encoder_count == 0 && pwm_output > 50) { stall_count++; if (stall_count > 200) { /* 堵转超过 1 秒,停 PWM,记录错误码 */ Motor_Set_Speed(0); pid.pid_out_last = 0; error_code = ERROR_STALL; stall_count = 0; } } else { stall_count = 0; }

这里的判断条件“encoder_count == 0 && pwm_output > 50”能区分堵转和“目标为 0 时正常停止”。阈值 50 表示 PWM 输出已经过半,电机仍无脉冲,基本可以判定为堵转。堵转后的处理策略取决于应用场景:简单做法是直接停车并报错;复杂做法是反向转一下再正转尝试。

注意堵转后必须清 pid_out_last,否则恢复供电时电机会猛地冲一下,相当于 PID 从饱和区直接起步,冲击电流极大。

6.3 速度环调完后的验证清单

参数整定完成后,验证是否合格可以按下面的清单逐项测试:

  1. 空载启动到 600rpm,超调量不应超过 5%(即 630rpm),调节时间不超过 500ms。
  2. 在稳定运行中用手轻捏电机轴模拟负载扰动,转速跌落不超过 10%,恢复时间不超过 300ms。
  3. 1000rpm 切换到 500rpm,下落过程中不应有明显反转或过冲。
  4. 连续运行 30 分钟,L298N 散热片温度不应超过 80℃(手能持续触摸约 5 秒)。
  5. 编码器读数在静止时应为 0,不应有抖动计数。

最后还要复查一个常被忽视的地方:共用电源的 ADC 采样。如果 L298N 的电机电源和 STM32 的供电之间有走线耦合,PWM 开关噪声会通过地线串扰,导致 ADC 采样值波动,进而让 PID 的输入噪声变大。验证方法是把 PID 设为 0,观察编码器测速曲线——曲线毛刺大,优先检查接地,而不是继续调 Kd。D 参数能压住表面噪声,但掩盖不了硬件的问题。

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

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

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

立即咨询