资讯动态

STM32电机PID控制实战:从时钟配置到自整定调参全解析

发布时间:2026/9/16 20:29:56 来源:尧图企业网站定制
简介针对STM32平台的PID电机自控制需求这套工程包提供了从算法实现到硬件驱动的完整参考面向嵌入式学习者与电机控制开发者可帮助理解PID比例、积分、微分项在实际闭环控制中的调节方法以及速度/位置控制系统的搭建思路。压缩包共180个文件约4.97MB包含33个C源文件、35个头文件、Keil工程配置uvproj/uvopt、编译生成的hex/axf/map文件以及o/d/crf等中间文件和备份、清理脚本。工程内stm32f10x_tim.c、stm32f10x_adc.c等外设驱动清晰可见方便对照学习定时器PWM输出、ADC反馈采集等关键环节结合提供的hex等产物也能直接烧录验证控制效果。目前已有83人学习/下载适合课程设计、毕业设计或入门调参时参考。1. 把PID挪到STM32上真正的分水岭不在算法而在时间基准在Simulink里调得漂漂亮亮的PID速度环搬进STM32F103之后开始振荡这种事我见过不止一次。多数人第一反应是参数没调好实际上参数只是替罪羊真正的问题往往出在三个底层环节闭环执行的周期是否稳定、编码器采样的时刻是否正确、PWM频率与电机驱动器的死区是否匹配。以一份典型的STM32F10x PID电机自控制工程来看文件清单里的stm32f10x_tim.c同时承担PWM生成和编码器计数stm32f10x_adc.c负责电流采样stm32f10x_flash.c用来掉电保存PID参数stm32f10x_rcc.c分配整棵时钟树这四类外设文件其实就是一条完整闭环链路的骨架。适合人群有C语言基础、能在Keil里跑示例但想把速度环、位置环真正整定明白的嵌入式开发者。这篇按“底层搭建、算法实现、参数整定、自整定落地”的顺序拆开讲每步都给出可复现的代码和参数依据。2. 底层四件套用stm32f10x_rcc.c和stm32f10x_tim.c搭出闭环骨架2.1 时钟分配为什么APB1上的定时器时钟是PCLK1的两倍拿到工程先别急着看PID先把stm32f10x_rcc.c里的时钟树捋清楚。STM32F103在SystemInit()之后系统时钟是72MHzAPB1总线默认二分频得到36MHz但挂在APB1上的TIM2、TIM3等定时器时钟是36MHz的两倍也就是72MHz。这个“两倍关系”最容易被忽略你按36MHz去算PWM频率得到的结果会比实际值小一倍编码器的计数时间基准也会整体偏移整定出的PID参数自然对不上。我一般会在外设初始化完成后调用RCC_GetClocksFreq()把SYSCLK、HCLK、PCLK1、PCLK2四个值打印出来确认一遍尤其在改过芯片型号或时钟树之后。标准外设库的RCC配置大致是下面这种写法/* stm32f10x_rcc.c 中的常见片内外设时钟使能 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE); /* 校验实际时钟树SYSCLK 应为 72MHzPCLK1 应为 36MHz */ RCC_ClocksTypeDef clocks; RCC_GetClocksFreq(clocks);注意APB2和APB1是两条不同的总线GPIO和ADC挂在APB2上最高72MHzTIM2、TIM3挂在APB1上定时器时钟虽然是72MHz但ADC的输入时钟却要从APB2分频得到。很多人习惯把所有外设时钟一起使能这没问题但ADC时钟不能直接给72MHzADC时钟上限约14MHz通常配置成72MHz除6等于12MHz。如果ADC超频采样值会明显跳动反映到速度环上就是电流纹波被当成真实信号。工程里如果有keilkilll.bat这个批处理一般就是清理.uvgui这类用户界面临时文件用的说明这个项目本身可以用Keil直接打开源码和配置文件是完整的。拿到这类项目我建议第一步做的事是检查RCC配置和实际晶振是否一致外部8MHz晶振配错了整个时间基准全都会偏PID整定做得再细也没有意义。2.2 PWM与编码器一个定时器生成驱动波形一个定时器数脉冲速度闭环的输出端是PWM输入端是编码器或霍尔信号两端都落在TIM外设上所以stm32f10x_tim.c是这份工程里被改写最多的文件。先看PWM侧频率选择要和电机驱动器的死区时间匹配。IR2104这类半桥驱动芯片的死区一般在530ns到2.6us死区时间内上下桥臂都不能导通PWM频率选太高会压缩有效占空比区间。直流电机控制里20kHz是常见的折中值高于人耳听觉上限又能避开大部分机械谐振频段。按前面确认的72MHz定时器时钟设置ARR为3599、PSC为0得到20kHz PWM。/* TIM2_CH1 PA0输出20kHz PWM */ TIM_TimeBaseInitTypeDef tb; tb.TIM_Period 3599; /* 重装值3599 1 3600分频 */ tb.TIM_Prescaler 0; tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tb); TIM_OCInitTypeDef oc; oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_Pulse 0; /* 初始占空比0%后续PID输出直接改这个值 */ oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE);PWM模式1下CNT小于比较寄存器值时输出有效电平改TIM_SetCompare1(TIM2, value)即可调节占空比。ARR预装载和OC预装载必须同时开启两者都关闭的话在运行中修改比较值时可能先冒出一两个周期的异常脉宽速度环会因此看到毛刺。占空比分辨率等于重装值加1也就是3600级对应0.028%的调节精度对一个速度环来说足够用了。编码器侧通常用另一个独立的定时器。CH1和CH2分别接编码器的A相和B相配置成编码器接口模式后定时器会自动根据两相相位差判断旋转方向/* TIM3编码器接口CH1/CH2接A/B相 */ TIM_TimeBaseInitTypeDef tb_enc; tb_enc.TIM_Period 0xFFFF; /* 自由计数靠方向决定溢出方向 */ tb_enc.TIM_Prescaler 0; tb_enc.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, tb_enc); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM3, ENABLE);在PID中断里这样读取增量int16_t delta (int16_t)TIM3-CNT; /* 反转时计数下溢符号位自动处理 */ TIM3-CNT 0;TIM3-CNT是16位寄存器直接强转成int16_t后反转产生的下溢会变成负数不用额外判断方向。把CNT清零时要注意必须在读完的一瞬间清否则会丢掉半个周期的脉冲。编码器模式四倍频后如果电机带500线编码器每圈就是2000个计数这个值在后面算转速时要用到。2.3 ADC电流采样与Flash参数落盘闭环之外的硬件功夫stm32f10x_adc.c在速度环里的任务有两件过流保护和电流限幅。PID计算本身不需要电流信号但如果没有电流监控堵转或线缆短路可能直接烧毁驱动管所以ADC采样通道一般接在低端采样电阻或电流检测放大器输出上。标准外设库的ADC配置写法如下ADC_InitTypeDef adc; adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode ENABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE);连续转换模式开启后读到的值是不断刷新的直接读就行。PWM开关会产生共模噪声采样时间选239.5周期虽然慢但稳更适合速度环这种低频场景。为了减少噪声我一般会在中断里连续读8次再右移3位取平均代价是每次采样多了十几个指令周期换来的是速度环不会被单个毛刺干扰。参数掉电保存则落到stm32f10x_flash.c上。PID的Kp、Ki、Kd整定完之后如果每次上电都从头调工程没法用。把参数写进Flash用户区的做法typedef union { float value; uint32_t word; } float_u32; float_u32 kp_u; kp_u.value pid.Kp; FLASH_Unlock(); FLASH_ErasePage(0x0800FC00); /* 64KB Flash的最后一页1KB/页 */ FLASH_ProgramWord(0x0800FC00, kp_u.word); FLASH_Lock();这里要注意STM32F103不同容量的Flash页大小和地址不同64KB型号最后一页是0x0800FC00128KB型号则是0x0801FC00移植到别的芯片时必须对照参考手册查表。写入前必须先擦除整页不能单独擦一个字。如果Flash里还存着代码地址别选在最前面的页避免擦掉程序区。上电时用同样地址读回数据先跟一个预置的校验字比对校验通过才加载进PID结构体。这样一个简单的持久化机制就能让整定好的参数跨掉电保存。3. PID实现位置式与增量式的真实差距不在公式3.1 为什么速度环普遍用增量式PID位置式PID和增量式PID从数学上看只是移项关系但在嵌入式实现里两者对异常状态的容忍度完全不同。位置式的输出直接是占空比绝对值u(k) Kp·e(k) Ki·Σe·T Kd·Δe/T积分项累加的是全部历史误差一旦电机在启动阶段被卡住几秒积分项会累到很大的值等障碍移除输出瞬间冲破限幅电机猛冲。增量式只输出占空比的增量float PID_Incremental(PID_TypeDef *pid, float sp, float fb, float dt) { float err sp - fb; /* Δu Kp(e1-e0) Ki·e1·dt Kd(e1-2e0e-1)/dt */ float delta pid-Kp * (err - pid-e_pre) pid-Ki * err * dt pid-Kd * (err - 2.0f * pid-e_pre pid-e_pre2) / dt; pid-e_pre2 pid-e_pre; pid-e_pre err; pid-out delta; /* 输出在外部累加掉电不会丢 */ return pid-out; }增量式对执行机构来说更友好但有个前提输出累加变量pid-out本身必须做限幅。否则控制器从异常状态恢复时累加值会越过PWM比较值的上限写入TIM比较寄存器后实际上是乱序输出。速度环的增量式代码里几乎不需要Kd因为编码器差分本身已经是一阶噪声放大再微分会把量化噪声直接变成输出抖动。后面整定部分会细说。位置式不是不能用它在需要“输出与执行机构位置严格对应”的场合更直观比如舵机或阀门控制。但对电机转速环增量式加上输出限幅和积分分离是工程上更稳的组合。3.2 抗积分饱和的实现顺序积分饱和的本质是误差持续存在积分项一直增长但执行机构已经到极限增长的部分全部浪费。等到误差反向时积分项还得慢慢泄放系统表现为明显的延迟和超调。处理手段很多最常用的是条件积分只在输出没有进入饱和区时做积分。float PID_Update(PID_TypeDef *pid, float sp, float fb, float dt) { float err sp - fb; /* 先算总输出再判断是否饱和 */ pid-out pid-Kp * err pid-integral pid-Kd * (err - pid-e_pre) / dt; if (pid-out PWM_MAX) { pid-out PWM_MAX; } else if (pid-out PWM_MIN) { pid-out PWM_MIN; } else { /* 只有输出未饱和时才让积分累加 */ pid-integral pid-Ki * err * dt; } pid-e_pre err; return pid-out; }关键在顺序必须先计算包含积分的总输出再判断饱和饱和时停掉积分累加。如果先积分再限幅积分项在饱和区间内仍然疯狂增长和没处理没区别。PWM_MIN不要设成0电机驱动里0占空比意味着刹车或续流反向通过另一路PWM控制所以速度环输出允许负值也很正常PWM输出的最小值通常对应反方向的最大占空比。这里PWM_MAX和PWM_MIN是经过归一化后的输出范围比如±100映射到实际占空比时要继续换算。条件积分是工程里最常用的做法但不是唯一。积分分离只在误差小于某阈值时积分适合阶跃给定很大的场合变速积分按误差大小动态调整Ki效果更好但调参成本高。速度环建议先用条件积分简单直接参数含义不会因为引入权重系数而变得模糊。3.3 控制周期固定化把闭环放进定时器中断PID公式里的dt不是随便填的它必须和控制器的执行周期严格一致。STM32里常见的错误是把PID放在主循环里读到什么算什么一次循环0.5ms下一次循环3ms微分项会把这个抖动完全当作真实变化放大出去。正确的做法是用一个独立定时器产生固定周期中断在中断服务函数里完成“读编码器、算PID、更新PWM”三件事。10ms控制周期对一般直流减速电机速度环来说是常见的起点太快会让编码器在每个周期内计数过少速度分辨率变差太慢则动态响应迟钝。用TIM4做10ms节拍TIM_TimeBaseInitTypeDef tb_tick; tb_tick.TIM_Period 7199; /* (991)*(71991)/72MHz 10ms */ tb_tick.TIM_Prescaler 99; tb_tick.TIM_ClockDivision TIM_CKD_DIV1; tb_tick.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, tb_tick); TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); TIM_Cmd(TIM4, ENABLE);计算方式72MHz除以100再除以7200得到100Hz即10ms一次。中断里读取TIM3编器计数换算成转速调用PID_Update得到输出再调用TIM_SetCompare1写PWM。中断优先级要保证编码器读取和PID计算不被其他外设打断太久。串口打印、LED刷新这类任务放主循环即使主循环卡住几毫秒速度环仍然稳定在10ms执行一次。判断一个工程实时性做得好不好最快的方法是看PID中断里每周期耗时是否稳定如果在中断里调用了Flash擦写这种毫秒级操作速度环当场就会崩溃。4. 参数整定临界比例法表格与实际手调节奏4.1 临界比例法的完整操作步骤PID参数整定方法里临界比例法最直观操作成本也低在数字控制器上完全可复现。步骤如下把PID三个参数设置为Kp初始值1、Ki0、Kd0。给定一个中等目标转速取额定转速的50%左右让电机空载运行。逐步增大Kp每次增幅约30%观察速度反馈波形。当Kp增大到输出出现等幅振荡时记下这个增益为临界增益Ku同时记下振荡周期Tu。按表4-1计算实际参数填入代码重新观察响应。表4-1 临界比例法参数公式控制器类型KpKiKdP0.50 × Ku00PI0.45 × Ku0.54 × Ku / Tu0PID0.60 × Ku1.20 × Ku / Tu0.075 × Ku × Tu同一个电机的Ku和Tu不是固定不变的负载变重、电源电压下降、驱动电路死区差异都会让这两个值漂移。表格里的系数是Ziegler-Nichols公式的经验值用它算出来的参数能保证系统稳定但超调量通常会偏大一般在填入后还要做一次手调微调。等幅振荡的判断标准是反馈波形幅度稳定不衰减如果波形发散说明Kp已经过大迅速调小。注意临界比例法激活的振荡是持续振动的让电机在这种状态跑太久对机械结构不好记录完Tu后要立刻把输出降下来。4.2 先P后I微分在速度环里能不加就不加手调顺序要遵循“先P、后I、最后才考虑D”的原则。先把Ki和Kd设成0Kp从很小开始加观察目标转速下的阶跃响应如果响应慢、达不到目标继续加Kp如果出现持续小幅振荡说明Kp过大退回来一些。找到Kp让系统有轻微超调但能稳定后开始加Ki。Ki的作用是消除稳态误差一点一点加加到转速误差收敛到编码器分辨率以内即可。Ki过大表现是低频周期振荡周期往往是控制周期的几十倍这个特征非常明显。速度环的微分项多数情况下直接省略。编码器计数是差分信号本身带一阶滞后再对速度做微分等于二次差分量化噪声会被明显放大。如果确实需要阻尼效果Kd先从0.05×Ku×Tu起步观察电机是否有滋滋的电流噪声有就降回去。位置环才是Kd发挥价值的地方位置环的D项提供阻尼让定位不来回过冲。4.3 从空载到带载负载漂移与级联PID的适用性电机从空载变到带载机械时间常数变大同一个Kp、Ki的参数组合表现会变差空载调好的Kp放到负载上可能振荡带负载调好的Kp空载时又太迟钝。这不是算法的问题而是被控对象特性变了。让参数能在一定负载范围内可用常用做法是取两个极端负载空载和最大负载下的Ku、Tu各算一组参数取中间值并留出20%左右的稳定裕度。如果负载变化范围大而且要求响应快速度环外面还要叠加一个位置环构成级联PID。这里有一个原则内环电流环或速度环的响应速度必须明显快于外环速度环或位置环。在STM32F103这种主频72MHz的芯片上做级联合理的控制周期分配是电流环1ms、速度环10ms、位置环50ms三个环用三个独立的定时器中断节拍优先级从内到外递减。级联调参时先固定内环参数不变再单独整定外环不要同时动两环的参数否则出问题都没法定位。5. 自整定落地继电反馈加串口交互把调参收敛到几分钟5.1 继电反馈自动识别临界参数的原理手动逐次增大Kp找到Ku太慢自动化的办法是继电反馈整定这是Astrom和Hagglund在1984年提出的方法非常适合嵌入式环境因为它不依赖被控对象的传递函数。原理是让控制器临时充当一个继电器输出在高低两个电平之间来回切换系统被强制激励出稳定的极限环振荡。继电器的输出幅值和振荡幅度可以算出临界增益Ku振荡周期直接作为Tu。/* 继电反馈替代PID输出两电平切换 */ float auto_tune_out(float sp, float fb) { float err sp - fb; if (err 30.0f) /* 误差正方向阈值按目标转速的5%设 */ return 65.0f; /* 高电平输出65%占空比 */ if (err -30.0f) return 35.0f; /* 低电平35%保留电流续流能力 */ return 50.0f; /* 误差在滞回区内保持中位 */ }输出为什么不用0%和100%因为0%占空比在多数H桥驱动中意味着制动或续流反向切入时会产生很大的电流冲击100%占空比也会让驱动管在切换瞬间承受过大di/dt。65%、35%是经验值给输出留出缓冲。误差阈值30转对应一个1000转左右额定转速的电机具体值按目标速度5%取即可。继电反馈启动后记录输出跳变沿之间的时间差就得到极限环振荡周期Tu。实际执行时只记录上升沿之间的时间能滤掉半个周期内的毛刺干扰。代码层面用一个状态变量即可static uint8_t tune_state 0; /* 0表示第一次沿1表示第二次沿 */ static uint32_t tune_time_ms 0; static float tune_Tu 0.0f; void auto_tune_tick(float sp, float fb, float dt) { float err sp - fb; uint8_t now (err 30.0f) ? 1 : 0; /* 只取上升沿 */ if (now !tune_last) { if (tune_state 1) tune_Tu (float)tune_time_ms; /* 本次沿到上次沿为周期 */ tune_time_ms 0; tune_state; } if (tune_state 10) /* 避免整定无限跑下去 */ tune_time_ms (uint32_t)(dt * 1000.0f); tune_last now; }临界增益Ku通过4h/(πa)估算其中h是继电器输出幅值本文中是15%占空比对应的电压幅值a是实测误差振荡的峰峰幅度。a可以从串口打印的速度反馈波形里读出。得到Ku和Tu后套用第4章的表4-1直接算出Kp、Ki、Kd。这个流程把整定时间从人工试凑的小时级压缩到大约一两分钟而且结果可复现。5.2 整定参数写回Flash与验证技巧继电反馈整定结束得到一组新参数之后要写回Flash才能算真正落地。用第2.3节介绍的union方式把PID结构体里三个float逐字写入用户页并在参数末尾追加一个固定的校验字。上电时读取Flash中的Kp、Ki、Kd和校验字校验字匹配才加载不匹配就用默认参数运行这样可以防止Flash里是乱数据导致电机上电乱转。需要补充的是自整定得到的是被控对象在某一工作点上的线性近似参数它对惯量变化剧烈的负载仍然有局限。工程上做完继电整定后还必须做一次阶跃测试验证目标速度从0阶跃到50%额定转速记录超调量如果超调超过10%把Kp按70%比例收缩Ki按50%比例收缩再测试一次。一个验证参数是否正常写好的直接方法整定结束后断电再上电让控制器进入正常PID模式给固定目标转速看稳态速度误差是否和整定前一致并对比Flash地址里的Kp值与整定结束时是否完全相同——一致才表示闭环从识别到存储再到加载的路径是通的。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价