STM32电机PID控制实战:从时钟配置到自整定调参全解析
2026/9/16 20:29:50 网站建设 项目流程

简介:针对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()之后系统时钟是72MHz,APB1总线默认二分频得到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 应为 72MHz,PCLK1 应为 36MHz */ RCC_ClocksTypeDef clocks; RCC_GetClocksFreq(&clocks);

注意APB2和APB1是两条不同的总线:GPIO和ADC挂在APB2上,最高72MHz;TIM2、TIM3挂在APB1上,定时器时钟虽然是72MHz,但ADC的输入时钟却要从APB2分频得到。很多人习惯把所有外设时钟一起使能,这没问题,但ADC时钟不能直接给72MHz,ADC时钟上限约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型号最后一页是0x0800FC00,128KB型号则是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-2e0+e-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; /* (99+1)*(7199+1)/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参数整定方法里,临界比例法最直观,操作成本也低,在数字控制器上完全可复现。步骤如下:

  1. 把PID三个参数设置为Kp初始值1、Ki=0、Kd=0。
  2. 给定一个中等目标转速,取额定转速的50%左右,让电机空载运行。
  3. 逐步增大Kp,每次增幅约30%,观察速度反馈波形。
  4. 当Kp增大到输出出现等幅振荡时,记下这个增益为临界增益Ku,同时记下振荡周期Tu。
  5. 按表4-1计算实际参数,填入代码,重新观察响应。

表4-1 临界比例法参数公式

控制器类型KpKiKd
P0.50 × Ku00
PI0.45 × Ku0.54 × Ku / Tu0
PID0.60 × Ku1.20 × Ku / Tu0.075 × Ku × Tu

同一个电机的Ku和Tu不是固定不变的:负载变重、电源电压下降、驱动电路死区差异,都会让这两个值漂移。表格里的系数是Ziegler-Nichols公式的经验值,用它算出来的参数能保证系统稳定,但超调量通常会偏大,一般在填入后还要做一次手调微调。等幅振荡的判断标准是反馈波形幅度稳定不衰减,如果波形发散,说明Kp已经过大,迅速调小。注意:临界比例法激活的振荡是持续振动的,让电机在这种状态跑太久对机械结构不好,记录完Tu后要立刻把输出降下来。

4.2 先P后I,微分在速度环里能不加就不加

手调顺序要遵循“先P、后I、最后才考虑D”的原则。先把Ki和Kd设成0,Kp从很小开始加,观察目标转速下的阶跃响应:如果响应慢、达不到目标,继续加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值与整定结束时是否完全相同——一致才表示闭环从识别到存储再到加载的路径是通的。

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

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

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

立即咨询