拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32电机转速闭环控制:PID原理、代码与调参实战

STM32电机转速闭环控制:PID原理、代码与调参实战 做电机控制这些年最让我头疼的问题其实不是电机不转而是转速不稳定。空载的时候转到 1200RPM一加载瞬间掉到 900RPMPID 参数没调好还会持续抖动听起来就像电机在“打摆子”。后来我把控制器从 C51 换到 STM32重新梳理了控制方案把测速、PWM 驱动、PID 算法这一整套链路都走通之后才明白问题的关键不在于“用 PID”而在于“PID 用在一个闭环系统里”前面的测速准不准、控制周期稳不稳、输出限幅做没做每一项都在决定最终效果。这篇文章就从 STM32 的实际工程出发把 PID 算法控制电机转速的原理、完整代码、调参过程一次讲清楚。适合刚接触 STM32 电机控制的同学也适合那些已经在用 PID 但总是“调不出来”的朋友你可以直接照着下面这套思路和代码搭项目然后再去改自己的参数和逻辑。1. 开环控制与闭环控制的差距就是转速稳不稳的根源先看一个很多人忽略的事实如果把 PWM 占空比固定电机的转速会随着负载、电源电压、温度的变化发生明显漂移这是开环控制的天然缺陷。你用 50% 占空比驱动直流电机空载时转速是 1000RPM用手捏住轴之后转速可能掉到 600RPM但 PWM 仍然输出一样的占空比系统并不知道转速掉了。开环控制只“控制输出去”不“关心输出结果”所以不能解决转速稳定问题。闭环控制的思路则是不断测量当前转速和目标转速比较用误差值计算新的 PWM 输出。误差大了就增大输出误差小了就减小输出这样即使负载变化系统也会自动补偿。PID 算法就是这个“根据误差计算输出”的决策器它负责回答三个问题当前误差有多严重——比例项P误差积累了多少历史欠账——积分项I误差变化趋势是什么样的——微分项D很多人以为 PID 就是三个系数调一调其实先要把控制架构搭对。STM32 做电机转速控制典型闭环链路如下目标转速 - 误差计算 - PID控制器 - PWM输出 - 电机驱动 - 电机 | 实际转速 —— 编码器测速 —— 编码器 ——电机轴这也是我在这篇里要反复强调的核心框架测速、执行、控制三大块缺一不可。实际做项目时我见过不少同学把 PID 代码抄过来却发现转速反馈根本是乱的要么编码器没配置成正交模式要么 PWM 频率错了电机啸叫。所以下两节先讲硬件和测速它们和 PID 同样重要。2. 硬件选型与接口设计决定了 PID 能跑多稳2.1 最小系统、电机与驱动器的搭配我做这套实验时用的主控是 STM32F103C8T6也就是最常见的“蓝色板”价格便宜、资料多、定时器资源也比较丰富用来驱动一路直流电机完全够用。电机我选的是带霍尔编码器的直流减速电机常见型号如 MG513P30大概几十块钱自带 AB 相霍尔编码器输出脉冲用来测转速非常方便。驱动板用 TB6612 或 L298N 都可以推荐 TB6612因为它的导通压降小、发热低对电池电压利用更好。电源方面需要注意电机和单片机最好分开供电。STM32 用 USB 的 5V 或者单独 3.3V 稳压电机用 6V~12V 电源然后共地。如果不共地编码器脉冲在传输时可能因为参考地不一致产生毛刺直接导致测速抖动这个我在实际调试中踩过坑后面会展开说。2.2 引脚分配与定时器选型逻辑STM32 定时器资源非常多但也不要随意分配。我最终采用的是定时器 1 的 PWM 输出PA8 输出 PWM用来控制电机速度。定时器 2 的编码器接口模式PA0、PA1 接编码器 A、B 相用于测速。串口 1PA9、PA10用来打印目标转速、实际转速和 PID 输出方便调试。一个普通 GPIO接按键用来切换目标转速或停止电机。为什么用定时器 2 做编码器接口模式而不是外部中断因为编码器接口是硬件自动处理 A 相和 B 相的关系正反转、四倍频计数都不需要 CPU 干预你只需要定时读取计数值CPU 负担非常小。如果用外部中断手动数脉冲转速高的时候很容易丢脉冲而且代码复杂度也高。硬件连线表如下模块STM32引脚说明TB6612 PWMAPA8PWM调速信号TB6612 AIN1PB0方向控制TB6612 AIN2PB1方向控制编码器 A 相PA0定时器2通道1编码器 B 相PA1定时器2通道2编码器 VCC/GND3.3V/GND注意编码器工作电压部分电机需5V串口1 TXPA9调试信息输出串口1 RXPA10接收指令可选2.3 PWM 频率的选择细节电机驱动 PWM 频率不是一个可以随便定的值。我用的是 20kHz这个频率超出了人耳可听范围电机不会发出尖啸声。如果 PWM 频率设为几百赫兹电机会发出刺耳的“吱吱”声负载越大声音越明显。同时频率太高也不一定好比如 TB6612 这类驱动芯片的开关损耗会增加而且电机电感在高频下对电流的平滑作用已经足够不需要远超 20kHz。实际经验是 10kHz~20kHz 都合适我固定用 20kHz。对应到代码上就是配置定时器 1 的 ARR 为 899PSC 为 3在 72MHz 主频下得到 20kHz 的 PWM 频率。计算方式PWM频率 72MHz / ((ARR 1) * (PSC 1)) 72e6 / (900 * 4) 20000 Hz占空比通过 CCR 寄存器控制范围 0~899。这个数值就是 PID 输出最终要写入的变量。3. 编码器测速不仅要测准还要算得快3.1 为什么测速环节不能随便做PID 算法依赖“实际转速”这个反馈量。如果反馈值有延迟、有毛刺、单位不对后面调得再努力也白搭。我在项目里最先尝试的是定时器外部脉冲计数法也就是用一个定时器固定 1 秒统计另一个通道输入的脉冲数然后算转速。这个方法最简单但存在一个明显问题转速刷新周期是 1 秒PID 的控制周期远远小于 1 秒导致整个控制系统相当于在“睁一只眼闭一只眼”——反馈信息严重滞后电机很容易超调或振荡。后来我改用 STM32 定时器的编码器接口模式它在硬件层面处理 A/B 相信号可以实现四倍频计数而且我可以把控制周期设到 5ms~20ms每 10ms 读取一次计数并计算转速反馈新鲜度大幅提升。这个方案非常适合直流电机自带霍尔编码器的场景。3.2 编码器接口模式的初始化要点在 STM32CubeMX 里配置非常简单但我更建议你理解底层寄存器。下面是定时器 2 编码器模式的初始化代码基于标准外设库风格Keil5 工程可以直接用void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructInit(TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 65535; // 16位计数器 TIM_TimeBaseInit(TIM2, 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 0x0A; // 输入滤波抗毛刺 TIM_ICInit(TIM2, 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 0x0A; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI1, TIM_EncoderMode_TI2, TIM_ICPolarity_Rising); TIM_SetCounter(TIM2, 32768); // 设置中点方便处理负值 TIM_Cmd(TIM2, ENABLE); }这里把计数器初始值设置为 32768是因为编码器计数可能朝两个方向走设置到中间值可以方便处理反转。实际读取时把TIM_GetCounter(TIM2)减掉 32768得到的就是有符号的脉冲增量。3.3 从脉冲数换算成转速霍尔编码器每圈输出的脉冲数取决于电机型号。比如我的 MG513P30减速比是 1:30编码器装在电机轴上电机轴每转一圈霍尔输出 13 个脉冲有些是 11看具体规格那么在输出轴测到的脉冲数就是 13 × 30 390经过定时器四倍频后每转一圈输出轴对应的计数增量是 13 × 30 × 4 1560。我设定每 10ms 读取一次计数器并清零因此 10ms 内测到的脉冲数delta对应的转速可以用下面公式计算转速(RPM) delta * 100 * 60 / 1560100 是 10ms 与 1 秒的换算倍数60 是秒钟与分钟的换算1560 是每圈脉冲数。总结成代码int16_t delta TIM_GetCounter(TIM2) - 32768; // 有符号增量 TIM_SetCounter(TIM2, 32768); float speed_rpm (float)delta * 6000.0f / 1560.0f;这个方法的核心是“定时清零计数并重算”保证每次读取到的都是最近 10ms 内的增量。注意TIM_SetCounter不能在计数过程中影响硬件后续计数实际测试下来没问题因为读和写都是 16 位操作中断优先级够高就不会丢脉冲。3.4 没有编码器怎么办测频法和测周法如果电机没有编码器也可以用霍尔传感器或者光电传感器测转速思路与编码器类似。STM32 做外部脉冲计数有两种典型办法测频法1 秒内统计外部脉冲个数适合高转速。公式是转速 脉冲数 / (每转脉冲数 * 时间)。测周法用定时器捕获两个相邻上升沿之间经过的微秒数适合低转速。转速越低脉冲周期越长测周法越准。我的建议是做闭环控制尽量用编码器没有编码器可以先估算但控制效果会差很多。测频法数据更新慢测周法在转速突变时容易产生尖峰都比不上编码器接口模式好用。4. PID 控制器原理与离散化实现逻辑比公式更重要4.1 先理解三个项的“性格”PID 控制器的连续表达式大家都熟u(t) Kp * e(t) Ki * ∫e(t)dt Kd * de(t)/dt但真正写进单片机的必须是离散化版本。理解三作用时我习惯这样类比P比例项看到现在。误差大输出就大。但它只会“跟随”不会“记住”所以单独用会产生稳态误差。I积分项记住过去。把历史上所有误差累加起来用来消除稳态误差。问题在于容易积分过头产生超调所以需要限幅。D微分项预测未来。误差变化越快D 输出越大用来抑制超调但也会放大噪声。对于电机转速控制标准的位置式 PID 够用但实际工程里我更推荐增量式 PID它输出的是“PWM 增量”不是最终的 PWM 值天然不会出现积分饱和失控的问题而且从代码角度看更容易理解。4.2 增量式 PID 的离散化推导增量式 PID 公式并不复杂。先记住位置式u(k) Kp * e(k) Ki * sum(e(k)) Kd * (e(k) - e(k-1))再写上一拍u(k-1) Kp * e(k-1) Ki * sum(e(k-1)) Kd * (e(k-1) - e(k-2))两个式子相减得到增量式du(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) du(k)注意这里的u(k)对应 PWM 的比较值CCR它必须限幅在 0~ARR 范围内否则 PWM 占空比超过 100% 或变成负值系统就会出问题。增量式的好处是du通常较小我们可以在u(k-1)累加后再整体限幅实现抗积分饱和。4.3 代码结构从结构体到控制函数我习惯把 PID 相关状态封装成结构体这样项目大了以后可以同时控制多个电机而不需要复制代码。定义如下typedef struct { float target; // 目标转速单位RPM float feedback; // 实际转速单位RPM float err_now; // 当前误差 float err_last; // 上一次误差 float err_prev; // 上上次误差 float Kp; float Ki; float Kd; float du; // 本次增量 float output; // PWM比较值输出 float out_min; float out_max; } PID_TypeDef;初始化函数void PID_Init(PID_TypeDef *pid, float Kp, float Ki, float Kd, float out_min, float out_max) { pid-target 0.0f; pid-feedback 0.0f; pid-err_now 0.0f; pid-err_last 0.0f; pid-err_prev 0.0f; pid-Kp Kp; pid-Ki Ki; pid-Kd Kd; pid-du 0.0f; pid-output 0.0f; pid-out_min out_min; pid-out_max out_max; }每一次控制周期都调用PID_Calcfloat PID_Calc(PID_TypeDef *pid, float target, float feedback) { pid-target target; pid-feedback feedback; pid-err_prev pid-err_last; pid-err_last pid-err_now; pid-err_now pid-target - pid-feedback; pid-du pid-Kp * (pid-err_now - pid-err_last) pid-Ki * pid-err_now pid-Kd * (pid-err_now - 2.0f * pid-err_last pid-err_prev); pid-output pid-du; if (pid-output pid-out_max) pid-output pid-out_max; else if (pid-output pid-out_min) pid-output pid-out_min; return pid-output; }需要留意的是我这里的err_prev保存的是上上拍的误差。变量名有点绕但含义清楚。如果你让别人维护代码建议注释写清楚否则后面调参时容易看晕。4.4 为什么我不用位置式 PID位置式 PID 需要累加所有历史误差这就导致一个问题如果电机长时间堵转误差一直很大积分项会无限增大等堵转解除后控制器还保持一个很大的输出电机猛地冲出去非常危险。增量式 PID 的输出增量不受过去总量限制只要误差减小du就会改变方向系统恢复更快。对于转速控制这种“执行机构可以快速变化”的场景增量式更可靠。当然位置式 PID 也不是一无是处某些需要精确位置控制的场景下位置式更容易做目标规划。但如果你是入门光电编码器直流电机控制增量式是我首推的方案。5. 完整实战代码主循环、定时中断与串口调试三件套5.1 工程结构规划实际工程里不要把所有代码塞进 main.c。我按功能模块拆开文件功能main.c初始化与主循环encoder.c/h编码器初始化与速度读取pwm.c/hPWM输出初始化与占空比设置pid.c/hPID结构体与运算uart.c/h串口打印与调试下面直接给出一份可以编译运行的核心代码框架适配 Keil5 STM32F103C8。5.2 PWM 初始化代码void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler 3; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 899; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, 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(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_CtrlPWMOutputs(TIM1, ENABLE); // TIM1为高级定时器必须使能主输出 TIM_Cmd(TIM1, ENABLE); }注意 TIM1 是高级定时器比通用定时器多一步TIM_CtrlPWMOutputs(TIM1, ENABLE)这一步漏掉的话 PWM 死活没输出很多新手在这里卡一整天。5.3 10ms 控制周期的定时器中断PID 控制周期不能放在主循环里靠延时凑那样会有 jitter导致控制效果不稳定。我用 TIM3 做 10ms 定时中断void Control_TIM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler 719; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); }计算方式72MHz / (720 * 1000) 100Hz也就是 10ms 一次。5.4 定时中断里执行控制逻辑核心控制逻辑写在TIM3_IRQHandler中每次进入先清中断标志然后读编码器、计算实际转速、调用 PID、设置 PWMPID_TypeDef speed_pid; extern float current_speed_rpm; extern float target_speed_rpm; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 1. 读取10ms内脉冲增量 int16_t delta (int16_t)(TIM_GetCounter(TIM2) - 32768); TIM_SetCounter(TIM2, 32768); // 2. 换算转速 current_speed_rpm (float)delta * 6000.0f / 1560.0f; // 3. 调用PID得到PWM占空比比较值 float pwm_out PID_Calc(speed_pid, target_speed_rpm, current_speed_rpm); // 4. 写入PWM寄存器 TIM_SetCompare1(TIM1, (uint16_t)pwm_out); // 5. 串口打印 printf(target:%.1f rpm, speed:%.1f rpm, pwm:%.0f\r\n, target_speed_rpm, current_speed_rpm, pwm_out); } }printf走串口需要重定向fputc这部分属于 Keil 标准做法我在文末的完整代码里一并给出。5.5 主循环与目标转速设置主程序里初始化各模块后可以在一个死循环里轮询按键调整目标转速。下面这个例子简单直接int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); UART_Init(115200); PWM_Init(); Encoder_Init(); Control_TIM_Init(); PID_Init(speed_pid, 2.0f, 0.3f, 0.1f, 0.0f, 899.0f); target_speed_rpm 1000.0f; current_speed_rpm 0.0f; printf(System start, target1000rpm\r\n); while (1) { // 这里可扩展按键逻辑、蓝牙/串口指令等 } }代码的主体到这里已经完整。之所以串口打印放在中断里是因为控制周期 10ms 时打印频率足够高能清晰观察转速变化曲线如果你觉得刷屏太快也可以改成 100ms 打印一次但要另做一个计数标志。6. 调参实战从振荡到稳定的完整链路6.1 先定控制周期再谈 PID 参数不少人的调参顺序是“先调 PID 参数再看效果”其实第一步应该先确认控制周期。控制周期太长反馈滞后严重不管你怎么调 P 都会振控制周期太短测速增量可能经常为 0导致 PID 输出抖动。对中等惯性的直流电机转速系统我建议从 10ms 开始然后根据响应表现微调。我用 10ms 控制周期、20kHz PWM 频率的组合整体响应和稳定性都不错。6.2 经验整定法先纯 Kp再加 Ki最后碰 Kd下面是我常用的一套“三阶段整定法”分享出来供你参考先把 Ki 和 Kd 设成 0Kp 从一个比较小的值开始。逐渐加大 Kp观察转速曲线。当我看到电机在目标转速附近来回震荡且振幅不再收敛时把当前 Kp 记下来取它的一半作为最终 Kp 参考值。加入 Ki。从小值开始比如当前 Kp 的 10% 左右观察稳态误差是否消失。如果转速最终稳定在目标值附近 no error就说明 Ki 有效如果出现持续低频振荡说明 Ki 太大减半再试。最后加 Kd。小负载系统其实不加 Kd 也可以但加入一定的 Kd 可以抑制超调。Kd 约取 Kp 的 5%~10%如果发现电机噪声变大、PWM 输出跳动频繁说明 Kd 偏大立即减小。我在这套硬件上最终用的参数是 Kp2.0、Ki0.3、Kd0.1在这个参数下空载转速从 800RPM 直接给定到 1000RPM大约 150ms 内稳定超调量不到 5%稳态误差在 ±3RPM 以内表现足够满足大多数教学和轻负载场景。6.3 从串口波形判断系统状态调参时眼睛盯着串口数字不如看曲线直观。STM32 串口打印出来的数据可以先通过串口助手或虚拟示波器软件显示。我用的是 VOFA 之类的免费工具把 target 和 speed 两列数据画成实时曲线这样能够明显看出转速曲线是否发散是否收敛但慢是否超调后持续振荡是否有高频抖动。根据曲线形态来调参比盲目改参数高效得多。下面我整理了一个常见问题排查表现象可能原因解决方案转速始终达不到目标Ki 太小或输出限幅太小加大 Ki检查 PWM 最大值是否被限死转速来回震荡Kp 太大或控制周期太长减小 Kp缩短控制周期启动瞬间猛冲积分饱和或 Kd 偏小使用增量式并限幅必要时加 Kd稳态是锯齿状抖动测速量化误差太大提高编码器线数或增加滤波负载变化后恢复慢Ki 偏小适当加大 Ki6.4 我踩过的几个硬坑这里想重点说几个实操中非常容易踩的坑它们跟 PID 本身关系不大但会直接毁掉整个控制效果。第一个是编码器输入滤波没有配置。我的编码器线比较长又没有加 RC 滤波转速快速变化时 PA0/PA1 上会出现毛刺定时器计数值偶尔跳变导致转速反馈突然从 1000RPM 变成 1200RPM 又跳回来。PID 看到这种虚假误差会不断施加修正最终表现为电机咔咔响。解决方法是把 GPIO 配置为浮空输入的同时在定时器输入捕获里配置TIM_ICFilter 0x0A这个值相当于给输入加了数字滤波器能有效滤除窄毛刺。第二个是 PID 输出和 PWM 寄存器类型不匹配。我用float pwm_out但TIM_SetCompare1形参是uint16_t如果你忘了做类型转换负值会变成很大的无符号数电机直接全速反转。必须保证 PID 输出被限幅在0~899再传给 PWM 寄存器。第三个是电机和单片机不共地。刚开始我把电机电源、单片机的 USB 电源分开供电结果编码器信号一直不稳定。后来把两个电源的 GND 连在一起毛刺问题立刻缓解。测速系统一定要保证稳定的参考地电势。第四个是关于串口打印的影响。如果调试信息打印频率太高会影响控制中断的实时性。我把串口波特率提到 115200并且避免在打印函数里做浮点格式化以外的耗时操作基本没事。但如果你的控制周期是 1ms打印 50 个字符在 115200 波特率大约耗时 4ms那就会挤占控制时间这时候要么降低打印频率要么用 DMA 发送。6.5 参数整定过程实录最后分享一次我在这套平台上的完整调参记录帮你建立“调参手感”。初始设 Kp1.0、Ki0.1、Kd0目标转速 1000RPM。上电后转速从 0 爬升到 1100RPM然后缓慢降到 990RPM来回滚了 3 个周期才稳定说明 P 偏大、I 略大。把 Kp 降到 0.6 之后超调减少但仍然有稳态误差 15RPM。把 Ki 升到 0.2稳态误差逐渐消除但负载突变后恢复慢。加上 Kd0.05 后启动超调被明显压制。最后微调成 Kp2.0、Ki0.3、Kd0.1 时系统响应和抗负载扰动都做到比较满意的状态。整个过程大概花了 40 分钟主要时间花在等待转速稳定上所以调整后要观察足够长的时间不要只看一两秒就急着改参数。7. 从速度环到更复杂的系统后续还能怎么扩展速度环调好之后如果你想继续深入最自然的扩展方向是位置环与速度环的级联控制。外环是位置环内环是速度环位置环输出的是目标转速速度环再输出 PWM。这种两级结构在云台、小车、机械臂里都很常见代码上只需要再加一个 PID 结构体控制周期通常外环 50ms内环 10ms形成级联。另一个值得改进的是前馈控制。对于阶跃目标变化PID 总是要等误差出现后才作出反应这会导致响应偏慢。如果已知电机大致的“PWM-转速”关系可以把理论前馈量直接加到 PID 输出上即u u_pid feedforward这样目标突变时能快速接近目标值再由 PID 修正剩余误差。前馈项不是必须的但加到系统里之后我的启动响应时间又缩短了约 30%。当然PID 并不是万能的如果你的电机带有严重的非线性摩擦或者控制对象延时很大可以考虑使用更高级的算法比如模糊 PID、自适应控制。但这些算法的前提仍然是有一个可靠的测速链路和稳定的控制周期所以我建议先把最基础的 PID 做得足够扎实再往上层扩展。我自己做完这个项目之后最大的体会是PID 参数不是调出来的而是“看”出来的。把反馈数据用串口可视化之后你每一次改动都会有一个明确的方向。对于想从零开始做 STM32 电机控制的朋友照这套思路硬件搭一个最小系统软件按测速、PWM、PID 三个模块独立实现然后慢慢把不同模块接起来你会发现表面上复杂的电机控制其实每一步都有清晰的逻辑。
返回列表