简介:基于STM32F407ZGT6与HAL库的直流电机PID控制完整工程,配套正点原子4.3寸TFT LCD实时显示转速,面向嵌入式初学者与需要电机调速/闭环控制的开发者。资源包含MG310电机(带GMR编码器)配合L298N驱动器的完整配置,CubeMX初始化工程及PID控制逻辑。包内共127个文件,以h和c源码为主,辅以ioc工程配置、hex固件、uvprojx工程文件、txt说明及bat批处理,整体约938KB,结构紧凑适合直接参考或二次开发。已有985人学习下载。读者可获得HAL库定时器编码器接口、PWM输出、PID闭环调节与LCD显示的完整代码框架,能沿现有工程快速验证转速闭环效果,并据注释与配置迁移到其他STM32型号。
1. “免费”的STM32+HAL直流电机PID控制,到底给你省了什么
搜“STM32+HAL 直流电机 PID 控制”时,很多人会额外加一个“免费”的前缀。说实话,搜到几十个下载包很容易,但难的是把下回来的代码跑出稳定速度环——你缺的不是源码,而是把闭环每一步吃透的路径。真正的免费,是 CubeMX、HAL 库、调试工具这些硬件生态全是零成本,不存在的那个“付费版”就是你自己写出来的两百行 C 代码。这套方案适合手里有带编码器的直流减速电机、任意一块 STM32 开发板的人,做毕设、比赛、小型自动化项目都够用。下文按“理论 → 配置 → 代码 → 调参避坑 → 验证”的顺序展开,新手能直接照着复现,熟手可以对着边界参数和踩坑记录核对自己的系统。
2. 直流电机PID控制的理论底子:控制回路、速度测量与两套PID公式
2.1 闭环回路里每个环节在物理上是什么
直流电机速度闭环的本质是让电机转速跟随目标值,并且在负载变化时自己能纠正偏差。这个负反馈回路的物理链路并不复杂:目标转速是个数值,编码器把当前转速变成另一个数值,两者相减得到误差,PID 控制器拿误差算出一个控制量,控制量再映射成 PWM 占空比和方向引脚电平,H 桥驱动按这个占空比给电机两端供电,电机转速变化,编码器脉冲频率随之改变,反馈回控制器形成闭环。整套逻辑在 STM32 上以固定周期循环执行,这个周期就是速度环采样周期。
做这套系统前要先确认硬件条件:带霍尔编码器的直流减速电机、一块 H 桥驱动板、任意 STM32 开发板、一个限流保护合适的电源。很多人卡在第一步,手里只有两根电源线的普通直流电机,没有编码器,那只能做开环调速,PID 无从谈起。编码器是闭环的眼睛,没有它,后面所有内容都不成立。
这里要澄清一个常见误解:PID 控制器输出给电机的到底是什么?不是单纯的 PWM 占空比,而是“方向 + 占空比”的组合量。PWM 占空比决定平均电压大小,方向引脚决定 H 桥电流方向,电机才能正反转。这个组合量在代码里怎么表示,直接决定你后面方向控制好不好写。我一般让 PID 输出一个带符号浮点数,正负代表方向,绝对值代表占空比,这样做换向逻辑非常直接,也方便上位机观察。
2.2 位置式PID和增量式PID:公式差异与选型判断
位置式 PID 的输出是控制量绝对值,由三项组成:比例项用当前误差乘 Kp,积分项把所有历史误差累加后乘 Ki,微分项用当前误差与上一次误差的差乘 Kd。它物理意义很清晰——输出多少占空比一目了然,调试时打印出来容易对照。但它有个老毛病:积分饱和。一旦执行机构饱和或系统长时间消不掉误差,积分项会越积越深,等到误差反转时,积分拖着输出半天不肯回来,表现为大超调甚至震荡。
增量式 PID 输出的是控制量增量,只和当前误差、上一次误差、上上次误差有关,不需要维护累加积分项。好处是天然没有积分饱和,因为历史误差的累计效果由外部变量累加完成,你只需在输出累加时做一次限幅。坏处是增量本身没有物理意义,你只看到“这次加多少”,看不到当前占空比是多少,还得在外部维护累加变量。另外增量式对编码器噪声更敏感,差分运算天然放大高频抖动,低速时几个脉冲的跳变都会被放大成可见的转速波动。
面对这两套公式,直流电机速度环怎么选?我的习惯是:STM32+HAL 速度环用带积分限幅和积分分离的位置式,因为速度环采样周期 10ms 左右,误差收敛也快,积分状态可控;如果控制对象换成云台角度环、舵机位置环,或者系统里存在明显的执行机构死区,增量式用起来更顺手。新手不必两个都实现,先把位置式吃透,后面要用增量式时,改公式也只是一下午的事。
| 对比项 | 位置式PID | 增量式PID |
|---|---|---|
| 输出含义 | 当前占空比绝对值 | 占空比变化量 |
| 积分作用 | 显式积分,需防饱和 | 隐式积分,靠输出累加 |
| 饱和处理 | 需单独限幅积分项 | 输出累加时限幅即可 |
| 噪声敏感度 | 一般 | 较敏感 |
| 适用范围 | 速度环、模拟量输出 | 位置环、步进伺服跟随 |
位置式配合积分分离和积分限幅,在直流电机场景下是我验证过最稳的组合。选型定了,后面代码实现就有明确方向。
2.3 三个系数Kp/Ki/Kd在电机轴上的表现
Kp 是系统对误差的响应速度。Kp 太小,电机响应像慢动作,目标变了半天转速才爬上去;Kp 太大,转速在目标附近来回穿越,严重时变成等幅震荡,甚至听到电机轴“咔咔”撞限位的声音——这就是飞车的前兆。飞车不是电机失控空转,而是控制系统在目标两侧反复驱动,功率全部耗在来回换向上。
Ki 管静差。纯比例控制下,电机维持转速需要一个持续的电压,这个电压和稳态误差成正比,所以稳定后转速和目标之间总是差一小块。Ki 把过去所有误差攒起来,补上这块电压。Ki 太小,静差怎么调都调不掉;Ki 太大,启动阶段误差大,积分迅速堆积,转速冲过头再回调,超调量肉眼可见。
Kd 理论上能提前抑制误差变化趋势,让系统更稳,但在编码器反馈噪声明显的普通减速电机上,Kd 会把量化噪声差分放大成剧烈控制量,速度曲线像心电图。所以速度环里我基本让 Kd 先给 0,等系统稳定且编码器波形干净时,再考虑用 Kd 压超调。
| 参数 | 作用 | 太小表现 | 太大表现 | 初始值方向 |
|---|---|---|---|---|
| Kp | 响应速度 | 响应慢、跟随差 | 震荡、飞车 | 按满量程对标估算 |
| Ki | 消除静差 | 稳态误差消不掉 | 超调大、启动冲击 | 从Kp的1/10开始 |
| Kd | 抑制误差变化趋势 | 几乎无感 | 放大编码器噪声 | 速度环先置0 |
调参思路会一直用这张表:先 Kp,再 Ki,Kd 按需。后面第五章的排查记录就是围绕这张表展开的。
3. 用STM32CubeMX把外设配好:PWM输出、编码器输入、10ms调度中断
3.1 定时器PWM通道的参数设置:PSC/ARR与载波频率
CubeMX 里新建工程选芯片型号,这一步人人会,关键是外设参数。我的固定套路是:PWM 用一个定时器,编码器用另一个定时器,再加一个基本定时器做 10ms 周期中断来运行速度环。三者分工明确,互不干扰,后续排查问题也方便定位。常见分配方式:TIM1 的通道 1 输出 PWM 到 H 桥的 PWMIN 引脚;TIM3 的通道 1 和通道 2 接编码器 AB 相;TIM6 做 10ms 定时中断。
PWM 频率由两个寄存器决定:PSC 预分频器和 ARR 自动重载值。公式是 Fpwm = Fclk / (PSC+1) / (ARR+1),其中 Fclk 是定时器时钟。以常见的 72MHz 主频为例,PSC=71、ARR=99 时得到 72M/72/100 = 10kHz。10kHz 是一个很实用的起点:频率低于人耳听阈,几乎没有高频啸叫;高于 20kHz 虽然更安静,但 H 桥开关损耗明显增大,普通驱动板发热更严重。
| 定时器 | 功能 | PSC | ARR | 计算结果 |
|---|---|---|---|---|
| TIM1 | PWM输出 | 71 | 99 | 10kHz载波 |
| TIM3 | 编码器输入 | 0 | 0xFFFF | 四倍频直接计数 |
| TIM6 | 10ms周期中断 | 7199 | 99 | 100Hz采样频率 |
TIM6 的分频计算同理:72M / 7200 / 100 = 100Hz,正好 10ms 中断一次。速度环采样周期选择 10ms 是基于经验折中:太快则低速时编码器脉冲太少,测速跳动大;太慢则系统响应迟钝,负载突变时恢复时间过长。5ms 也可以,但初始调参先用 10ms,简单可靠。
3.2 编码器模式的配置:四倍频与计数方向
编码器在 CubeMX 里的配置方式是:选中定时器后把其中一个通道设成 Encoder Mode,另一个通道也设成 Encoder Mode,两个通道都绑定定时器的编码器功能。以 TIM3 为例,TI1 和 TI2 同时接入编码器 AB 相信号。计数模式保持默认,这里有个关键点:编码器模式下,计数方向会自动跟随电机正反转,CNT 值递增或递减都由硬件完成,不需要软件干预。
TI1+TI2 组合是四倍频模式,每个编码器完整脉冲产生 4 个计数。以常见的霍尔编码器减速电机为例,PPR=13,减速比 1:30,电机输出轴转一圈,编码器本体转 30 圈,产生 13×30=390 个原始脉冲,四倍频后单圈计数 1560。低速时这些计数的统计噪声不可忽略,但速度环在高频采样下可以通过 Kp/Ki 吸收掉一部分波动。
注意:编码器模式下不要手动向定时器的 CNT 寄存器写数值,硬件计数与控制逻辑会互相打架;需要清零计数值时,先停用编码器,再使用 __HAL_TIM_SET_COUNTER,最后重新启动。
3.3 生成代码后的启动顺序:PWM、编码器、周期中断
生成代码后,main() 里外设初始化顺序由 CubeMX 自动排好,但启动顺序要自己写对。启动外设的顺序是:先 PWM,再编码器,最后周期中断。顺序反了会导致第一次中断里读到异常计数值,尤其是刚上电编码器可能没有稳定输出时。代码非常简单,但漏掉任何一行都是板子不动。
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); MX_TIM3_Init(); MX_TIM6_Init(); MX_USART2_Init(); /* 启动顺序:先PWM,再编码器,最后开中断 */ HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); HAL_TIM_Base_Start_IT(&htim6); while (1) { /* 主循环负责接收目标转速指令 */ } }HAL_TIM_PWM_Start 把 TIM1 的 PWM 输出使能,这一步之后 H 桥 PWM 引脚才有波形;HAL_TIM_Encoder_Start 让 TIM3 开始跟随编码器脉冲计数,此时 CNT 会随电机转动变化;HAL_TIM_Base_Start_IT 开启 TIM6 更新中断,速度环的调度核心在这里启动。三个启动调用缺一不可,少了任意一个,整个系统都会表现为“代码能烧进去但电机不动”或者“电机动一下然后死掉”。
调试阶段先不启动中断,直接用调试器观察 TIM3 的 CNT 值是否能随手动转动电机而变化。如果 CNT 不动或一直跳,优先检查编码器 AB 相是否接反了,或者两个通道的编码器模式是否同时使能。
4. 动手写PID控制器:结构体、速度换算与HAL库输出代码
4.1 PID结构体与初始化函数的C语言实现
PID 控制器在 C 语言里就是一个结构体加三个函数。结构体把参数、中间状态放一起,目的是以后给多个电机实例化多个控制器,互不干扰。下面这段是工程里最常用的写法,不依赖任何库,只用了标准 C 类型。
/* pid.h */ typedef struct { float Kp; /* 比例系数 */ float Ki; /* 积分系数 */ float Kd; /* 微分系数 */ float target; /* 目标转速, 单位rpm */ float last_error; /* 上一次误差, 用于微分项 */ float integral; /* 误差积分值, 带限幅 */ float out_max; /* 输出上限, 例如1000 */ float out_min; /* 输出下限, 例如-1000 */ } PID_TypeDef; void PID_Init(PID_TypeDef *pid, float kp, float ki, float kd, float out_max, float out_min); float PID_Calc(PID_TypeDef *pid, float current);初始化函数做两件事:赋系数、清状态。注意 target 一定要初始化为 0,否则上电瞬间目标值是内存残留随机数,PID 会立刻输出一个很大的控制量,电机可能猛地冲一下。
/* pid.c */ void PID_Init(PID_TypeDef *pid, float kp, float ki, float kd, float out_max, float out_min) { pid->Kp = kp; pid->Ki = ki; pid->Kd = kd; pid->target = 0.0f; pid->last_error = 0.0f; pid->integral = 0.0f; pid->out_max = out_max; pid->out_min = out_min; }out_max 和 out_min 设成参数而不是宏,是为了适配不同 PWM 分辨率:有些人定时器 ARR=99,CCR 范围 0~99;有些人 ARR=65535,满量程上千。把输出范围放到结构体里,换驱动板时只改初始化参数,不用动核心算法。这里的限幅范围选定后,后面第 5 章的积分饱和问题很大程度上就靠它兜底。
4.2 从编码器脉冲到转速:M法测速的换算
速度反馈不能直接拿编码器 CNT 值当转速,因为 CNT 只是个不断累加或递减的计数值。正确的做法是 M 法测速:固定时间窗口内统计脉冲数,再换算成转速。换算公式需要四个参数:编码器线数、减速比、四倍频系数、采样周期。
/* 速度换算参数 */ #define ENC_PPR 13 /* 编码器线数 */ #define GEAR_RATIO 30 /* 减速比 30:1 */ #define SAMPLE_TIME 0.01f /* 10ms采样周期, 秒 */ #define FOUR_MULT 4 /* 编码器四倍频计数 */ /* 每个计数值对应的转速: rpm */ #define RPM_PER_CNT (60.0f / (ENC_PPR * GEAR_RATIO * FOUR_MULT * SAMPLE_TIME)) float rpm_estimate(int16_t delta_cnt) { return (float)delta_cnt * RPM_PER_CNT; }公式推导逻辑:电机输出轴转一圈,编码器本体要转 30 圈,产生 13×30=390 个原始脉冲,四倍频后是 1560 个计数。若 10ms 采样周期里测到 1560 个计数,说明输出轴正好转了一整圈,对应转速是 1 圈/0.01秒 = 6000 圈/分钟。因此每个计数对应的转速是 60/1560 ≈ 0.0385 rpm。RPM_PER_CNT 就是那个系数。
需要特别注意 delta_cnt 的数据类型。F1 系列定时器 CNT 是 16 位,计数值超过 0xFFFF 会回绕,直接做无符号减法会得到巨大差值;必须先转成 int16_t 再做差,才能正确得到带符号计数增量。如果芯片是 F4 系列,CNT 是 32 位,可以直接用 int32_t,但代码里统一转 int16_t 也不会出错,因为 10ms 内电机竖着转也不可能溢出 16 位计数范围。
4.3 速度环中断处理:采样、计算、方向与占空比输出
10ms 中断是速度环的心脏,整个过程分三步:读编码器差值换算转速、调用 PID 计算输出、按输出符号设置方向和占空比。代码放在 HAL_TIM_PeriodElapsedCallback 回调里,这个回调是所有定时器中断的公共入口,所以必须用 htim->Instance 判断来源。
/* main.c 中的全局变量 */ volatile float target_rpm = 0.0f; volatile float current_rpm = 0.0f; volatile float pid_output = 0.0f; PID_TypeDef speed_pid; int16_t last_cnt = 0; #define PWM_MAX 99 /* 与ARR值一致 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { int16_t delta_cnt; float out_abs; if (htim->Instance == TIM6) { /* 1. 读编码器差值, 注意先转int16_t再相减 */ delta_cnt = (int16_t)(__HAL_TIM_GET_COUNTER(&htim3) - last_cnt); last_cnt = __HAL_TIM_GET_COUNTER(&htim3); current_rpm = rpm_estimate(delta_cnt); /* 2. 更新目标并计算PID输出 */ speed_pid.target = target_rpm; pid_output = PID_Calc(&speed_pid, current_rpm); /* 3. 方向与占空比输出 */ if (pid_output >= 0) { HAL_GPIO_WritePin(DIR1_GPIO_Port, DIR1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(DIR2_GPIO_Port, DIR2_Pin, GPIO_PIN_RESET); out_abs = pid_output; if (out_abs > PWM_MAX) out_abs = PWM_MAX; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint16_t)out_abs); } else { HAL_GPIO_WritePin(DIR1_GPIO_Port, DIR1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(DIR2_GPIO_Port, DIR2_Pin, GPIO_PIN_SET); out_abs = -pid_output; if (out_abs > PWM_MAX) out_abs = PWM_MAX; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint16_t)out_abs); } } }这段代码有三个关键点。第一,编码器差值计算里,__HAL_TIM_GET_COUNTER 返回的是无符号 16 位值,先存回变量再减,最后强转 int16_t,正反转都能得到正确的带符号差值,这一步错了低速反转时会测出“巨大正速度”。第二,PID 输出是带符号浮点,正负号天然对应电机方向,绝对值对应占空比,这让 H 桥换向逻辑只有三个分支,逻辑简单不容易出 bug。第三,输出做了两次限幅:一次在 PID_Calc 内部针对控制器输出,一次在这里针对 PWM 硬件范围,双重保护避免异常值直接写进 CCR。
DIR1_GPIO_Port、DIR1_Pin 这些宏由 CubeMX 根据你在图形界面配置的引脚自动生成到 main.h 里,实际使用时不要手动改 GPIO,而是回到 CubeMX 重新生成。
4.4 Kp/Ki/Kd初始值怎么估:满量程对标法
代码写完,下一步就是填参数。填 0.1 电机可能根本不转,填 100 立刻飞车,问题出在没有估算量级。这里给一个可落地的粗估方法:把系统近似看成线性比例环节,Kp 的初始值 = PWM 满量程 / 转速满量程。
假设占空比满量程是 1000(对应 CCR 范围 0~1000),实际会用的目标转速最大是 500 rpm,那么 Kp 就从 1000 / 500 = 2 开始。注意这个“转速满量程”不是电机空载最大转速,而是你项目里真实会用到的目标范围。如果你只需要 0~100 rpm,Kp 就取 10,差异很大。
Ki 初始取 Kp 的 1/10,比如 Kp=2 时 Ki=0.2。Kd 直接给 0,后续按需加。这样估出来的参数不会一步到位,但它给了你一个正确量级的起点,后面微调只需在 2 附近来回试探,而不是从毫无头绪的 0.001 开始爬。
5. PID调参与避坑记录:震荡、飞车、静差和噪声的现场排查
5.1 一套能落地的调参顺序:先纯P,再I,最后考虑D
调参顺序比参数本身重要。我固定按三步走:先让 Ki=0、Kd=0,只留 Kp,给定一个目标转速,比如 200 rpm,观察转速响应。如果稳定后离目标有一段静差,这是正常现象,不要急着加 Ki;如果出现等幅震荡,把 Kp 减半再试。反复三四轮,直到系统呈现阻尼衰减或稳定但小静差,这时的 Kp 就是“比例基线”。
第二步加 Ki,从 Kp/10 起步,比如 Kp=2 就取 Ki=0.2。加完观察两个指标:静差是否清零,启动超调是否过大。如果超调超过 20%,Ki 减半;如果静差迟迟消不掉,Ki 往上加,幅度控制在 1.5 倍以内,不要一次翻倍。Kd 最后考虑,在速度环里通常用不上,除非你发现系统在 Kp/Ki 调好后仍有可见的震荡尾巴,且编码器波形干净,才试着从 Kd=Ki/10 开始加。
5.2 电机只往一个方向转,反向没反应:方向逻辑与输出限幅的坑
现象:给定负目标转速,电机还是正转,或者反向指令下达后电机停在原地不转。排查顺序从硬件到软件:先用万用表量 H 桥 IN1 和 IN2 电平,看反向时两个引脚是否交换;再在调试器里观察 pid_output 是否真的变成了负数;最后检查方向分支代码有没有被优化掉,或者限幅把负值砍成了 0。
原因通常有三个:方向引脚初始化时被设成了同一个电平,H 桥无法换向;驱动板本身是单向调速电路,只有一路 PWM 输入;软件上把 pid_output 做了绝对值处理,符号信息丢失。解决:确认驱动板是 H 桥双极性驱动,至少具备 IN1/IN2 两个方向引脚;换向逻辑保持正向 IN1=1、IN2=0,反向 IN1=0、IN2=1;两次换向之间插入 5ms 左右延时,避免 H 桥上下管直通瞬间电流过大。这个坑最容易发生在刚把代码从开环调成闭环的时候,因为你习惯了只输出占空比,忘了方向也是闭环的一部分。
5.3 电机“嗡嗡”叫且转速不稳:PWM载波频率与采样周期不匹配
现象:低速时电机发出明显“嗡嗡”声,转速表打出来有周期性波动,加速时偶尔出现“咔咔”的机械撞击感。这个现象出现后先不要怀疑 PID 参数,先查两个基础配置:PWM 频率和采样周期。
原因很直接:PWM 频率太低(比如 1kHz~2kHz),电机线圈电流纹波很大,转矩脉动叠加在机械结构上,振动噪声明显;另一个原因是速度环采样周期太长,比如 200ms 才采一次转速,中间经历二十个 PWM 周期,转速平均值被严重滞后,PID 看到的永远是“过去的速度”。解决:把 PWM 频率提到 10kHz 以上,速度环采样固定 10ms 或 5ms,两者保持三个数量级以上的差距。编码器信号如果过长且靠近电机电源线,还要在信号线上加 RC 滤波,电容取 10nF 量级,否则差分测速会出现伪脉冲,转速值跳变幅度比 PID 噪声大得多。
5.4 目标500转实测495转:积分饱和与静差问题
现象:速度挺稳定,但稳态误差差 5 转,加负载后差距拉大到 30 转,怎么调 Kp 都没用。这时候先确认 Ki 是不是 0,再检查积分项有没有被限幅卡死。很多人在 PID_Calc 里只对输出限幅,不给积分项单独限幅,结果误差大时积分无节制增长,之后误差缩小,输出仍被大积分撑着,这就是积分饱和。
解决方式是积分分离加积分限幅。积分分离的原理:误差超过阈值时认为系统还在加速或减速阶段,不让积分累积,只有误差进入较小范围时才开始积,这样启动阶段不超调,稳态又有积分补静差。同时给 integral 单独设上下限,避免极端情况下积分跑飞。
#define INTEGRAL_LIMIT 50.0f /* 误差在此范围内才积分 */ #define INTEGRAL_MAX 500.0f /* 积分限幅 */ float PID_Calc(PID_TypeDef *pid, float current) { float error = pid->target - current; float p_out = pid->Kp * error; /* 积分分离: 大误差时停止积分 */ if (error > -INTEGRAL_LIMIT && error < INTEGRAL_LIMIT) { pid->integral += error; } else { pid->integral = 0.0f; } /* 积分限幅 */ if (pid->integral > INTEGRAL_MAX) pid->integral = INTEGRAL_MAX; if (pid->integral < -INTEGRAL_MAX) pid->integral = -INTEGRAL_MAX; float i_out = pid->Ki * pid->integral; float d_out = pid->Kd * (error - pid->last_error); pid->last_error = error; float out = p_out + i_out + d_out; /* 输出限幅 */ if (out > pid->out_max) out = pid->out_max; if (out < pid->out_min) out = pid->out_min; return out; }5.5 速度曲线像心电图:微分放大编码器噪声
现象:转速能在目标附近稳住,但连续打点记录速度,曲线每个采样周期都在跳,跳幅几十转,而且听电机声音又没感觉转得那么乱。这种“数据在跳、机构平稳”的情况,几乎可以锁定微分项或测速噪声问题。
原因:Kd 不为 0,编码器每次读回的计数值存在量化噪声,尤其是低速下每个采样周期就几个计数,一个计数的跳变换算成转速可能是十几转,微分项把这个跳变放大成剧烈的控制波动。解决:速度环先把 Kd 置 0,让系统重新回到“纯 Kp + Ki”的稳定状态;如果确实需要微分来压超调,必须先在转速上做一阶低通滤波,再进 PID。滤波系数取 0.2~0.3,即 new_speed = 0.7new_speed + 0.3raw_speed,具体系数根据采样周期微调。这个坑的特征非常明显,踩过一次之后你会对“先滤波再微分”这句话有极其深刻的体会。
6. 调完PID之后:用打点曲线验证速度环的阶跃响应
6.1 把速度曲线导出来,别靠耳朵调参
系统转起来之后,最忌讳的是拍脑袋说一句“看着挺稳”。耳朵和手感知不到超调量是多少,也没有办法量化稳态误差,更难对比调整前后差异。正确的做法是把每次采样的 current_rpm 存成一个数组,通过串口导出成 CSV,再用串口助手或 Excel 画速度曲线,观察阶跃响应。这套验证方式不需要任何外部上位机,几行代码就能搞定,却是把调参从“玄学”变成“能回溯”的关键一步。
具体实现:全局声明float speed_log[256]; uint16_t log_idx = 0;,在 TIM6 的 10ms 中断里把 current_rpm 写入 speed_log[log_idx++],超过 256 就回绕覆盖;主循环里收到串口字符 ‘E’ 时,把数组从 0 到 log_idx 逐行输出,格式为idx,rpm\n。然后在串口里给目标转速一个阶跃,比如从 0 直接跳到 300 rpm,等几十秒后发 ‘E’ 导出数据。
画出来的曲线看三个指标:上升时间、超调量、稳态误差。超调超过 20%,按“Ki 减半、Kp 微调”的顺序处理;上升时间过慢,优先加 Kp;末端静差明显,加 Ki。每改一次参数前先导出一次曲线存好,改完再导一次并对比,参数对系统的影响就完全暴露在眼前,而不是靠手感去猜。
我第一次调这个系统时,把 Kp 从 2 直接调到 20,电机瞬间飞车,连轴器都甩飞了。后来养成习惯:每次只改一个参数,改完必打点看曲线,十次以内就能拿到一条漂亮的阶跃曲线。这套方法把 PID 调参从“碰运气”变成了“看曲线改参数”的工程流程,希望你也能把它带到自己的项目里,用免费的工具链做出一套心里有底的速度环,希望帮到你。
本文还有配套的精品资源,点击获取