简介基于STM32的PID电机自控制工程主要面向嵌入式学习者和自动化方向学生用一套完整实例说明PID算法如何在真实MCU上实现电机速度与位置控制。压缩包共180个文件、4.97MB包含35个H头文件与33个C源文件以及Keil工程文件、hex烧写文件、axf/map等编译产物下载后可直接查看修改或烧录运行。工程代码覆盖比例、积分、微分三个核心环节包含电机反馈采集、ADC配置、PWM定时器初始化等外设操作清晰呈现“采样-比较-计算-输出”的闭环控制过程设计中还考虑了调参方法和过流、过温保护等可靠性细节。目前已有83人学习下载适合正在准备课程设计、毕业设计或希望快速上手STM32电机控制的开发者参考其工程结构与调试思路。1. 电机转速“自己稳住”STM32上的PID闭环究竟在干什么用 STM32 把电机转速锁在目标值附近绕不开 PID 闭环。很多人的第一版代码是照着一个“标准公式”抄下来的结果一上电就抖转速在目标值附近来回冲或者负载加上去以后爬坡爬不动甚至直接啸叫。问题通常不在 P、I、D 三个系数本身而在采样周期不固定、输出没有限幅、编码器测速噪声灌进了微分项。PID 在单片机上的落地从来不是“调三个数”而是一整条信号链路测速、做差、算控制量、输出 PWM、再测速。下面从增量式 PID 的离散化讲起到 M 法测速、电机驱动信号配合、串口调参再到抗积分饱和和级联 PID适合做毕设、云台、小车、泵阀类原型的工程师也适合从开环调速转向闭环控制的开发者。2. 增量式PID在STM32上的离散化PB、Ti、Td和C代码怎么对齐2.1 为什么直流电机对象天然吃PID直流有刷电机的电枢回路加上机械惯量工程上常简化成一阶惯性环节或者二阶振荡环节输入 PWM 电压输出转速。一阶模型的最大特点是控制量变化后输出要过一段时间才跟上这就是纯比例控制会留下稳态误差的根源。比例项只对“当前误差”反应积分项把历史误差累计进来补足静差微分项则对误差变化率提前动作。对这个一阶惯性对象PI 已经能胜任D 主要用来压超调。真正的麻烦在于对象模型是连续的单片机是离散定时采样代码里跑的必须是差分方程。实际工程里还有一类对象是双鼠笼转子异步电机和永磁同步电机它们的暂态模型比有刷直流复杂得多转子磁场定向和反电动势观测都不能靠单环 PID 解决。PMSM 就常用转速环 PI 叠加电流环 PI 的级联结构。所以“这个标题能不能成立”取决于你有没有把对象近似到 PID 能处理的阶数。2.2 位置式还是增量式选型的核心是“输出限幅”连续域公式写成工业习惯就是 u(t) Kp[e(t) (1/Ti)∫e dt Td de/dt]。位置式离散化直接输出绝对控制量积分项需要单独累加还要和输出限幅联动输出死在 100% 时误差还一直在累加退出饱和就会冲过头这就是教科书说的积分饱和。增量式算的是 Δu也就是这次和上次控制量之差最后输出 u 由 u Δu 得到。增量式的优势在于它没有显式的积分累加器误差的累积作用藏在对输出增量的求和里。只要把 u 限制在 PWM 最大最小占空比范围内超出的增量自然被“截断”积分项的无限增长就无从发生。代价是它对测量噪声更敏感因为 Δu u(k) - u(k-1) 相当于对误差做差分量化噪声会被放大。转速环的反馈来自编码器1 ms 周期下 M 法测速的量化误差可能达到 ±1 rpm这一点直接决定了微分项不能开太大。2.3 PB、Ti、Td 和 Kp、Ki、Kd 的换算很多人吃亏在这里工业仪表和 PLC 上常见的 PID 参数是 PB比例带%、Ti积分时间秒、Td微分时间秒而 STM32 的例程里往往直接写 Kp、Ki、Kd。PB 100 / KpPB 越大比例作用越弱Ti 越大积分收得越慢Td 越大微分刹车越强。在离散代码里设控制周期为 T那么Ki Kp * T / TiKd Kp * Td / T这个换算直接决定了你的参数是否可移植。同样的物理系统用 1 kHz 和用 200 Hz 采样Kp 可以不变Ki 和 Kd 却必须按 T 重新折算。很多人把 1 kHz 下整定好的 Ki 直接带到 200 Hz 工程里结果积分项效率骤降一加负载转速就掉。2.4 增量式PID的C实现定点还是浮点以及抗积分饱和代码走 C99 即可控制周期 1 ms占空比用千分率0~1000表示转速用 rpm 的整数。用浮点实现最直观但强实时控制里我倾向于定点把 Kp、Ki、Kd 放大 100 倍存成 int32_t避免每个周期都做浮点运算也方便在串口里直观打印参数。typedef struct { int32_t target; /* 目标转速 rpm */ int32_t feedback; /* 当前转速反馈 rpm */ int32_t err_1, err_2; /* e(k-1) e(k-2) */ int32_t output; /* 累加输出0~1000 */ int32_t out_max, out_min; int32_t kp_x100; /* Kp * 100 */ int32_t ki_x100; /* Kp * T / Ti * 100 */ int32_t kd_x100; /* Kp * Td / T * 100 */ } PidInc_t; int32_t PID_IncStep(PidInc_t *p, int32_t ref, int32_t fdb) { int32_t e ref - fdb; int32_t de e - p-err_1; int32_t d2 e - 2 * p-err_1 p-err_2; int32_t du (p-kp_x100 * de p-ki_x100 * e p-kd_x100 * d2) / 100; int32_t out p-output du; if (out p-out_max) out p-out_max; if (out p-out_min) out p-out_min; p-output out; p-err_2 p-err_1; p-err_1 e; return out; }这个实现里误差差分 de、二阶差分 d2 都按当前采样周期计算du 是控制量的增量。关键在输出钳位先算 out output du再把它限制在 out_min 和 out_max 之间等效于对 du 本身限幅误差在饱和期间不会被积分器无限累积退出饱和的响应就比传统位置式快一个数量级。err_1、err_2 仍然照常更新所以误差方向一旦反转下一拍立刻产出反向 du把电机从饱和里拉回来。kx_x100 的换算由外部函数在改参数时更新避免控制中断里反复做浮点除法。参数初始化时以 1 kHz 控制周期、Ti0.4 s、Td8 ms 为例PidInc_t pid { .out_max 1000, .out_min 0, .kp_x100 (int32_t)(10.0f * 100.0f), .ki_x100 (int32_t)(10.0f * 0.001f / 0.4f * 100.0f), .kd_x100 (int32_t)(10.0f * 0.008f / 0.001f * 100.0f) };ki_x100 在 1 kHz 采样下只有 2.5数值很小但它乘的是误差本身rpm 量级再叠加到 Δu 上积分速度并不慢。务必先确认控制周期 T 真的是 0.001 s用定时器产生 1 ms 中断而不是在 while 循环里用 delay 凑合否则 T 抖动会直接变成转速抖动。3. 编码器M法测速与PWM驱动把STM32接到电机轴上的信号链3.1 M法、T法、M/T法怎么选低速是分水岭测速是 PID 的反馈来源测不准调参就是在叠 buff。常见的三种测速方法对比如下测速法原理典型误差适用转速范围M法固定时间窗口内统计脉冲数低速下量化误差大中高速T法测量单个脉冲周期再换算高速下周期太短精度差低速M/T法同时测脉冲数和时间全范围较均匀宽范围对 13 线的霍尔码盘4 倍频后每圈也只有 52 个脉冲。如果电机只转 60 rpm1 rps1 ms 窗口几乎只能数到 0 或 1 个脉冲M法测出来就是跳变。转速环的最低可控转速由分辨率决定只在大几百 rpm 到几千 rpm 范围运行M 法简单可靠。3.2 STM32定时器编码器模式的M法测速一行代码拿转速用硬件编码器接口是最好的做法TIM3 的 CH1 和 CH2 接编码器 A、B 相配置编码器模式后计数器的增减由硬件自动完成不用在中断里判断方向。CubeMX 里把 TIM3 设成 Encoder Mode选 TI1 and TI2计数器周期 0xFFFF。寄存器初始化里需要指定计数边沿组合TIM_Encoder_InitTypeDef sConfig {0}; sConfig.EncoderMode TIM_ENCODERMODE_TI12; /* 双沿计数4倍频 */ sConfig.IC1Polarity TIM_ICPOLARITY_RISING; sConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler TIM_ICPSC_DIV1; sConfig.IC1Filter 0; sConfig.IC2Polarity TIM_ICPOLARITY_RISING; sConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler TIM_ICPSC_DIV1; sConfig.IC2Filter 0; htim3.Instance TIM3; htim3.Init.Prescaler 0; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 0xFFFF; if (HAL_TIM_Encoder_Init(htim3, sConfig) ! HAL_OK) Error_Handler(); HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL);EncoderMode 选 TI12CH1 和 CH2 的四个边沿都会计数等于把码盘线数翻了 4 倍。计数器保持 16 位不加预分频是因为 M 法关心的是相邻采样之间计数器的差值不是绝对值计数溢出时用 int16_t 做差会自动回绕这是故意利用补码溢出来简化程序的写法。速度计算放在 1 ms 定时中断里#define ENC_LINES 13 #define ENC_4X (ENC_LINES * 4) extern TIM_HandleTypeDef htim3; static int16_t last_count 0; void Speed_ISR_1ms(void) { int16_t now_count (int16_t)__HAL_TIM_GET_COUNTER(htim3); int16_t delta_cnt now_count - last_count; last_count now_count; speed_rpm (int32_t)delta_cnt * 60000 / (ENC_4X * 1); /* T1ms */ }delta_cnt 是 1 ms 内的脉冲增量乘以 60000 得到每分钟的计数增量再除以每圈的 4 倍频脉冲数结果就是整数 rpm。注释里的 T1ms 对应分母中的 1控制周期改成 2 ms 时分母要改成 2。now_count 和 last_count 声明成 int16_t 而不是 uint16_t是为了让差异的高位符号位自然处理计数器回绕。3.3 PWM输出与电机驱动模块的配合占空比、方向、死区PWM 频率选在 10~20 kHz一方面避开人耳敏感区另一方面让电枢电流纹波小一点。以 APB1 时钟 84 MHz 为例TIM2 输出 PWM预分频 83 时计数器时钟 1 MHz自动重装载值 4199 得到 10 kHz占空比由 CCR 决定写 0 就是 0%写 4199 就是 100%。驱动模块用 TB6612 或者 AT8236 这类全桥驱动时要注意方向信号和 PWM 的配合顺序。TB6612 的 AIN1/AIN2 与 PWMA 之间如果同时切换H 桥上下桥臂可能短暂直通而过流。常见做法是先更新方向引脚等待 1~2 个 PWM 周期的软延时再更新占空比方向翻转时强制 PWM 先归零一个周期。AT8236 的手册还强调逻辑电源和电机电源要共地驱动板的 GND 必须和 STM32 的 GND 连在一起否则脉冲电流会从信号引脚回流直接打坏芯片。3.4 启动最小占空比与低速死区PID输出小不等于电机在转摩擦力矩和换向死区决定了 PWM 占空比存在一个“死区”从小占空比往上加电机纹丝不动升到某个阈值突然转起来转速还跳很高。这个非线性会让纯 PID 在低速段振荡误差小的时候输出落在死区里电机不动误差一直存在积分项缓慢增长攒够以后一个大输出把转速推过头然后又进入死区。处理办法有两个一是开环标定时测出死区下限闭环输出小于下限值时按比例补偿到死区上沿二是对目标转速做调速范围限制低于额定 2% 的指令一律按 0 处理接受低速不可控这个物理事实。我的习惯是两件事都做占空比 0~260 之间按线性补偿到 260低于额定 2% 的目标不计算闭环。4. 开环标定与串口调参从临界振荡到能用的P、Ti、Td4.1 先做开环标定知道1%占空比能换来多少转速不要一上来就接闭环。把 PID 的输出直接旁路成手动占空比给电机加一个固定值等 1~2 秒转速稳定后记录转速。从 10% 到 90% 占空比按 10% 步进测 9 个点能画出 PWM 到转速的静态曲线。理想情况下这是条过原点的直线斜率的倒数就是 Kp 的量级参考目标转速 3000 rpm 时10% 占空比对应约 300 rpm那么 1 rpm 误差对应的控制量增量是 0.033% 占空比。Kp 取这台系统系数的 1~3 倍起步后续只在这个范围内微调。这一步还能顺手确认极性和编码器方向占空比加大转速是否真的在加反馈值和设定值方向是否一致。4.2 串口PID调试协议改参数不用重新编译调参的基建是运行中的板子能随时改参数。串口用 115200每行一条指令推荐这种紧凑格式void UART_HandleLine(char *line) { float kp, ti, td; int speed; if (sscanf(line, K %f %f %f, kp, ti, td) 3) { pid.kp_x100 (int32_t)(kp * 100.0f); pid.ki_x100 (int32_t)(kp * LOOP_T / ti * 100.0f); pid.kd_x100 (int32_t)(kp * td / LOOP_T * 100.0f); printf(PID OK Kp%.2f Ti%.3f Td%.4f\r\n, kp, ti, td); } else if (sscanf(line, SPD %d, speed) 1) { pid.target speed; printf(SPD OK %d rpm\r\n, speed); } else { printf(CMD? K kp ti td | SPD speed\r\n); } }K 命令的三个参数分别是 Kp、Ti、TdSPD 命令设置目标转速。sscanf 解析成功才更新参数省去了一大串 switch-case。这里暴露的是 Ti 和 Td 而不是 Ki、Kd原因是 Ti 的数值和人脑直觉一致0.4 秒就是比 4 秒积分快 10 倍而 Ki 在 1 ms 采样率下全是零点几到几的小数容易把人带偏。反馈数据用逗号分隔每 10 ms 打印一行“时间戳,目标,反馈,输出”方便在 PC 上画波形。4.3 临界振荡法三步整定先量Kc和Tc再套表工程上最快的粗整定是 Ziegler-Nichols 调参法把所有积分和微分关掉Ti 设成很大、Td 设成 0只留比例项逐渐加大 Kp 直到系统出现等幅振荡。此时记录临界增益 Kc 和振荡周期 Tc然后按下面表格换算控制器类型KpTiTdP0.5 Kc∞0PI0.45 Kc0.83 Tc0PID0.6 Kc0.5 Tc0.125 Tc实际操作时“等幅振荡”很难精确判定我会把超调幅度限制在 20% 以内就算到临界点然后把 Kp 设成 0.6Kc、Ti 设成 0.5Tc、Td 设成 0.125Tc直接发一条 K 命令观察阶跃响应。Z-N 公式是起点不是终点它整出的比例增益偏大适合抗扰动系统的粗调通常随后要把 Kp 降三分之一、Ti 提三分之一换取无超调。注意第一步必须在轻载空转下测负载变化会同时改变 Kc 和 Tc这张表就失效了。4.4 转速环参数范围与压力调节经验值的差别不少人拿温控或压力 PID 的整定经验套电机结果 Ti 设成几十秒电机爬坡爬到怀疑人生。压力调节 PID 参数的典型经验是 P30%~70%Ti10~300 sTd0~30 s对象是秒级到分钟级的慢过程积分时间差一个数量级没多大影响。电机的机电时间常数是几十毫秒到几百毫秒Ti 取 0.1~1 s 就已经很慢。直流减速电机转速环的一组可复现起始参数10 kHz PWM、1 ms 控制周期、占空比 0~1000、目标 3000 rpm如下Kp 起步 5Ti 0.5 sTd 0。转速稳定后如果超调超过 30%把 Kp 降到 3再看稳态误差有超过 5% 的稳态误差再把 Ti 降到 0.3。Td 加入的场景是负载变化频繁且希望减少超调从 0.005 s 开始加每次翻倍看到反馈毛刺明显放大就退回上一个值。记住一个原则微分项的放大倍数是 Kp*Td/TT 只有 1 ms 时Td0.004 s 也相当于给 Kp 乘了 4 倍编码器量化噪声会被这 4 倍直接变成 PWM 抖动。5. 参数调好的PID还会掉链子积分分离、级联和前馈三个技巧5.1 积分分离起跑时不超调的关键转速阶跃时最典型的表现是满速追赶、冲过头误差反向时积分项还在回味之前的大误差。处理办法是误差超过阈值时禁用积分项if (abs(e) 300) ki_used 0; /* 大误差阶段只走PD避免积分蓄能 */ else ki_used 1;误差收进 300 rpm 以内再开放积分补静差。这个阈值一般取额定转速的 10%不要设得太小否则积分项刚启动就被甩出去稳态误差很难消除。5.2 位置-速度双环级联让位置环去指挥速度环单环位置 PID 在小车、云台上很容易抖。级联 PID 的做法是位置环在外、速度环在内位置环以 5~10 ms 周期运行输出的是“目标速度”速度环以 1 ms 周期运行输出 PWM。内环先抗住负载扰动外环只管位置跟随。位置环的比例增益要小因为它的输出是速度给定单位错配会让内环目标速度来回过冲。5.3 前馈让PID只处理“意外”如果已经知道开环静态增益完全可以把目标转速直接折算成占空比作为前馈叠加到 PID 输出上。目标 3000 rpm 对应 30% 占空比那么 PID 只需要贡献目标被扰动后偏离的那一小部分。这就是标题里“自控制”的进阶含义反馈负责压制扰动前馈负责完成大部分跟随职责分离后 Kp 可以调得更小系统也更稳。速度前馈只在目标变化频繁的场景才划算恒速运行加不加差别不大。5.4 上台架前的离线验证用Python仿真代替盲调上电机之前先把开环测得的增益和机电时间常数放进一阶模型用同样的 PID 参数跑一遍阶跃响应Ts 0.001 K_ol, Tm 2200.0, 0.05 # 开环增益 rpm/占空比, 机电时间常数 s def plant(duty, last): y_ref K_ol * duty alpha Ts / (Tm Ts) # 一阶低通模拟电机惯性 return last alpha * (y_ref - last) y 0.0 u 0.0 err_1 err_2 0.0 ref 3000.0 for k in range(5000): y plant(u / 1000.0, y) e ref - y du kp * (e - err_1) ki * e kd * (e - 2 * err_1 err_2) u du u max(0, min(1000, u)) err_2, err_1 err_1, e if k % 100 0: print(k, y, u)脚本使用同样的增量式结构复现第 2 章的代码逻辑能先看到超调、上升时间和稳态误差。仿真里超调小于 20%、1 s 内稳定再把参数发到串口。那几秒脚本时间换掉的是电机反复过冲带来的惯性冲击和减速箱磨损。本文还有配套的精品资源点击获取