ARTICLE DETAIL

资讯详情

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

STM32直流电机PID闭环调速:CubeMX+HAL库实战

STM32直流电机PID闭环调速:CubeMX+HAL库实战 1. 从一块板子到一个能转稳的电机整体方案先搭骨架直流电机的调速说白了就是一句话让电机按照我给的目标转速转负载变了、电压变了、摩擦变了它自己还能拉回来。这件事在工业现场被做烂了但轮到自己拿STM32从头撸一遍很多人第一块板子就会卡在“电机能转但转不稳”上。标题里这几个词——STM32、直流电机、PID控制、HAL库、CubeMX——其实已经把这套东西的骨架交代清楚了用CubeMX点出外设用HAL库写业务用PID闭环让PWM驱动直流电机转出想要的转速。适合谁看做过流水灯、用过GPIO和串口、但没接触过闭环控制的朋友最合适纯小白也能看只是要有点耐心。我先说结论这套方案的门槛不在算法PID本身三个参数写出来也就二十行代码。真正吃功夫的是信号链路上每一环的可信度——编码器测得准不准、PWM分辨率够不够、采样周期稳不稳、电机驱动扛不扛得住换向电流。很多人调PID调不出来最后发现问题出在编码器少接了一根线或者PWM频率设得太低导致电机啸叫跟PID一点关系没有。所以下面我会按“方案选型 → 原理拆解 → CubeMX配置 → 代码落地 → 调参实录 → 排障”这个顺序走你照着一步步来基本能在半天内让电机稳在你想要的转速上。1.1 硬件清单与各部件职责先把要准备的东西列清楚缺哪个都不行部件作用常见型号备注主控板跑算法、出PWM、读编码器STM32F103C8T6蓝板/最小系统有硬件编码器接口的定时器就行电机驱动把MCU的弱信号放大成能推电机的功率TB6612FNG / L298N / DRV8871强烈建议TB6612压降小、发热低直流电机被控对象带霍尔/光电编码器的减速电机一定要带编码器否则无法闭环电源给电机供电12V/2A 以上适配器电流余量要足启动瞬间冲得厉害调试通道看波形、改参数USB转TTLCH340用串口打印实时数据逻辑电源给MCU供电5V转3.3V或板载LDO电机电源与MCU电源共地选带编码器的减速电机是关键。减速比通常几十比一输出轴转速几百RPM编码器一圈几百到上千个脉冲经过倍频后分辨率足够低速时也不会因为脉冲太稀而测得抖。如果手头只有普通直流电机那就得自己加光电码盘或者霍尔麻烦不少不建议新手走这条路。1.2 为什么是HAL库加CubeMX而不是寄存器或标准库这个问题被问烂了。HAL库的坏处是代码臃肿、执行效率低、有些外设中断处理绕。但对PID调速这种场景控制周期是毫秒级比如10ms一次HAL库那点开销完全在预算内换来的是外设配置不用翻几百页参考手册。CubeMX把时钟树、引脚复用、定时器模式、NVIC优先级都图形化掉了配错了它会红字警告这比手写寄存器少踩一堆坑。我的建议是外设初始化交给CubeMX生成的HAL代码控制和算法全部自己在回调里写。别在CubeMX生成的主循环里塞PID那样时序不可控。PWM用HAL_TIM_PWM_Start启动后就不用管了改转速只需要改__HAL_TIM_SET_COMPARE的寄存器值编码器用HAL_TIM_Encoder_Start启动读计数直接读CNT寄存器。HAL库在这里就是个“外设管家”算法还是你自己的。至于HAL和LL的区别简单说LL更贴近寄存器、更轻但CubeMX对LL的支持不如HAL全面配置编码器接口这种复杂外设LL写起来反而累。用HAL起步等哪天你觉得中断响应不够快了再局部换LL这是比较现实的技术路径。2. 把PID这三个字母翻译成人话很多教程一上来就是那个公式新手看完全懵。我换个说法你开车维持60码看到路码表是55脚就多踩一点油门这是P比例偏差越大动作越大如果一直差3码上不去你就慢慢把油门再压深一点直到误差消失这是I积分专门消除稳态误差快到60的时候你提前松一点油免得冲过头这是D微分提前预判。PID就是这么个“看误差、按比例动作、积累历史、预判趋势”的控制器。2.1 P、I、D三项各自的脾气比例项Kp决定了响应速度。Kp太小电机没劲转速上不去Kp太大转速会在目标值附近来回震荡甚至像抽搐一样。它作用的是“当前误差”。积分项Ki是来收拾残局的。光有P的话因为摩擦、负载这些因素转速总会停在离目标差一点的地方这叫静差。积分项把历史误差累加起来慢慢把这点差距补掉。但Ki太大会“充过头”转速冲过目标值再慢慢掉回来甚至越积越大导致长时间满输出这叫积分饱和。微分项Kd是阻尼。它在误差开始减小的时候给一个反向的抑制力减少超调。但编码器测速本身有量化噪声微分对噪声极其敏感一放大就是满屏尖刺。所以实际工程里位置式PID用在电机转速上Kd经常直接给0靠Ki和输出限幅把曲线做平就够了。真要加D必须先把速度信号低通滤波一遍。2.2 位置式、增量式还是串级标题里带“级联PID控制”这个热词但新手第一版别上串级。串级需要内环电流环有快速采样能力通常要靠ADC实时采电流硬件和软件复杂度陡增。转速单闭环用位置式或者增量式就够。位置式输出直接等于三项之和物理意义清楚输出对应的是PWM占空比。缺点是每次都要算积分累加容易饱和需要加限幅。增量式输出的是“这次比上次多多少”天然带积分效果抗饱和好切换手动自动平滑。缺点是启动瞬间可能丢一小步需要额外处理。我给的建议是先用位置式配上积分限幅和输出限幅因为它的调试过程最直观改一个参数就能看到对应的现象学会之后再转增量式。2.3 离散化连续公式怎么落进MCU连续形式的PID是积分和微分MCU只能算离散点所以要做离散化。位置式的离散形式是这样的// 每 T 毫秒执行一次 error target_speed - actual_speed; // 当前偏差 integral error; // 误差累加对应积分 if (integral INTEGRAL_MAX) integral INTEGRAL_MAX; if (integral -INTEGRAL_MAX) integral -INTEGRAL_MAX; derivative error - last_error; // 误差变化对应微分 output kp * error ki * integral kd * derivative; last_error error;这里有个坑离散化之后ki 和 kd 的物理量纲变了。如果你想用“真实”的PID参数积分项要乘采样周期 Ti微分项要除采样周期 Ti。但工程上没人这么较真直接把 ki、kd 当成“在这个采样周期下好用的系数”就行调参调出来就是对的。前提是采样周期必须固定不能今天10ms明天15ms那样整定好的参数就废了。3. CubeMX里的关键配置一步错步步错打开CubeMX新建工程选芯片型号然后是时钟配置。F103C8T6外部晶振一般8MHz经过PLL倍频到72MHz作为主频这条线CubeMX里点两下就好HSE选Crystal/Ceramic ResonatorPLL Source选HSE然后调倍频系数到72。时钟树配错串口波特率会乱、PWM频率会偏所以先用示波器或者示波器替代方案串口打印验证确认一下系统节拍。3.1 定时器PWM输出配置选一个支持PWM的定时器比如TIM1的CH1PA8或者TIM3的CH1PA6。模式选PWM Generation CHx然后四个参数决定波形Prescaler预分频把72MHz降下来。设为 0 就是不分频。Counter PeriodARR决定PWM分辨率。PulseCCR初始占空比。PWM Mode一般用 Mode 1。频率计算公式是PWM频率 72MHz / (Prescaler 1) / (Counter Period 1)我想让PWM跑在10kHz那么72,000,000 / (01) / (71991) 10,000。所以预分频给0ARR给7199。这样CCR从0到7199变化对应0%到100%占空比分辨率足够细。为什么选10kHz因为低于5kHz电机线圈会有明显啸叫人耳能听到高于20kHz又对MOS管的开关损耗和驱动芯片要求变高10到16kHz是个甜点区间。如果用TIM1这种高级定时器记得在CubeMX里把“Automatic Output”或者”PWM Generation 的 Break/MOE”勾上否则PWM出不来。这个坑我踩过调了半天以为是编码器坏了结果是高级定时器的主输出没使能。3.2 编码器接口配置再选一个定时器做编码器接口比如TIM3的CH1PA6和CH2PA7。Combined Channels选 Encoder Mode然后Encoder ModeTI1 and TI2TI12表示两相都计数四倍频。Counter Period填65535。极性按编码器实际输出调一般不勾反相。启动代码里用HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_ALL);就够了。读转速的时候直接读__HAL_TIM_GET_COUNTER(htim3)然后清零。四倍频意味着编码器一圈600线的话CNT变化2400。这个数在低速下也够用采样10ms内变化个位数十几个数都能分辨。注意编码器电源一定要确认是3.3V还是5V。很多光电编码器标称5V直接接3.3V的MCU引脚轻则信号识别不到重则把引脚打坏。要么用5V供电电平转换要么选宽压3.3V的编码器。3.3 一个固定周期的定时中断PID需要一个稳定的心跳。我一般再开一个基本定时器比如TIM6或TIM7配成10ms中断一次。为什么不用主循环轮询因为主循环里还有串口打印、按键扫描执行时间不确定PID周期就不稳。放中断里优先级比串口高一点但别设成最高的免得把系统tick耽误了。TIM6配置Prescaler给7199Counter Period给99这样中断频率 72MHz / 7200 / 100 100Hz也就是10ms一次。NVIC里把TIM6中断使能优先级设成1或2。串口的优先级设低一点比如3或4这样串口打印不会打断控制环。4. 代码落地从计数到PWM输出工程生成之后先别急着写PID把三段基础代码跑通电机能正反转、编码器能读数、串口能打印。这三件事独立验证过再合起来能省掉大量“到底是谁的错”的排查时间。4.1 电机驱动的方向与调速驱动芯片一般两个方向脚加一个PWM脚TB6612是AIN1、AIN2、PWMA。方向脚用GPIO输出控制PWM脚接MCU的定时器通道。// 正转 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 反转 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);PWM占空比通过改CCR寄存器实现__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, ccr_value);如果PID输出是有正负号的正转正、反转负那么要拆成“方向 绝对值占空比”if (output 0) { set_direction(FORWARD); ccr (uint16_t)output; } else { set_direction(BACKWARD); ccr (uint16_t)(-output); } if (ccr PWM_MAX) ccr PWM_MAX; // 限幅防止爆表 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, ccr);这里PWM_MAX就是ARR值7199。output的范围也应该限制在-7199到7199之间。限幅必须在写CCR之前做因为CCR寄存器只有16位超了会回绕电机行为会变得莫名其妙。4.2 编码器测速M法和T法怎么选测速有两套思路M法固定时间数脉冲固定10ms读CNT并清零转速正比于计数值。高速时精度高低速时一个周期可能只有几个脉冲误差大。绝大多数场景用M法就够。T法固定脉冲测时间记录两个脉冲之间的时间间隔低速时精度高高速时计时器分辨率不够。工程上做低速精密控制才用。我推荐先用M法int16_t speed_raw; speed_raw (int16_t)__HAL_TIM_GET_COUNTER(htim3); __HAL_TIM_SET_COUNTER(htim3, 0); // speed_raw 就是这10ms内四倍频后的脉冲数读CNT后立即清零保证每个周期都是从零开始数累加不溢出。这里用int16_t强转很关键因为编码器反转时CNT是从0递减到65535硬转成无符号会得到一个巨大的正数转成有符号就是负的正负号方向就对了。如果单位想统一到RPM需要标定手动让电机匀速转看10ms内CNT变化量同时用转速表量实际RPM算个比例系数。或者按编码器线数和减速比理论推算RPM (speed_raw / 4 / 编码器线数) / 减速比 / 0.01 * 604.3 PID结构体与中断里的实现把PID写成结构体方便以后做双环或者多电机typedef struct { float kp, ki, kd; float target; float actual; float error, last_error; float integral; float output; float integral_max; float output_max; } PID_t; float PID_Calc(PID_t *pid, float target, float actual) { pid-target target; pid-actual actual; pid-error target - actual; pid-integral pid-error; if (pid-integral pid-integral_max) pid-integral pid-integral_max; if (pid-integral -pid-integral_max) pid-integral -pid-integral_max; float derivative pid-error - pid-last_error; pid-output pid-kp * pid-error pid-ki * pid-integral pid-kd * derivative; if (pid-output pid-output_max) pid-output pid-output_max; if (pid-output -pid-output_max) pid-output -pid-output_max; pid-last_error pid-error; return pid-output; }然后在TIM6的10ms中断回调里调用void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { int16_t raw (int16_t)__HAL_TIM_GET_COUNTER(htim3); __HAL_TIM_SET_COUNTER(htim3, 0); float actual raw * SPEED_SCALE; float out PID_Calc(pid, target_rpm, actual); Motor_SetOutput(out); } }注意HAL_TIM_PeriodElapsedCallback是个弱函数自己重写一遍就行不要动CubeMX生成的底层。中断里只做必要的计算和寄存器写操作串口打印这种耗时的事情千万别放里面会直接拖垮控制周期。5. 调参现场从抽搐到平稳的过程整定PID我按这个顺序走先Kp再Ki最后看要不要Kd。第一步把Ki和Kd都置0只留Kp。给一个目标转速比如500RPM慢慢加大Kp。你会看到转速从“爬不到目标”变成“快速接近”再变大开始出现等幅震荡。记下这个临界震荡的Kp值然后退回到它的一半这时候系统响应快但还稳。第二步加Ki消静差。从小往大加观察转速是否能停在目标值上。Ki加过头会看到转速冲过目标再慢慢回来或者在一个范围内低频摆动。出现这个就把Ki减半。第三步Kd。如果超调能接受直接用0。如果非要加先给一个很小的值同时把速度信号做一阶低通actual_filtered 0.7 * actual_filtered 0.3 * actual;再交给PID。D项对噪声极敏感不加滤波基本没法用。5.1 现象与参数对照表调参的时候对着这张表看能少走弯路现象大概率原因处理方向转速上不去离目标差一截Kp偏小或输出限幅太低加大Kp检查output_max转速在目标附近小幅快速抖动Kp偏大或PWM分辨率不足减小Kp加大ARR冲过目标再慢慢回落Ki偏大积分饱和或Kd为0减小Ki加积分限幅转速长时间满输出不回落积分饱和integral爆了引入积分分离或反计算低速时转速读数跳M法脉冲太少增大采样周期或改T法电机有明显啸叫PWM频率落在音频段提高到10kHz以上5.2 积分饱和调试里最烦人的一件事启动瞬间实际转速是0目标转速500误差是500积分项嗖嗖往上累加。等电机真的转起来积分项已经积累到很大输出被顶在最大值下不来转速冲过500一大截然后积分才慢慢退这就是典型的积分饱和。表现出来就是“启动时电机猛冲一下再回落”。解决办法有两个一是积分限幅上面代码里已经做了把integral钳在合理范围二是积分分离当误差大于某个阈值时干脆不加积分让比例项先干活误差降下来再加积分。实测下来这两个方法配合用启动曲线能顺很多。6. 排障实录那些我亲手踩过的坑6.1 电机只朝一个方向转或者干脆不转先量PWM脚有没有波形。没有波形检查高级定时器的主输出使能MOE检查HAL_TIM_PWM_Start有没有调用。有波形但电机不动量驱动芯片的使能脚STBY有没有拉高量电机两端电压。TB6612的STBY不拉高芯片是完全不工作的这个容易忘。只朝一个方向转多半是方向脚接反了或者两个方向脚都给了同一个电平。用万用表量两个方向脚的静态电平正转和反转时应该正好相反。6.2 转速抖动厉害数字跳来跳去编码器信号质量差是首因。A/B相没接反但屏蔽没做好电机换向时的电磁干扰窜进信号线计数就乱。编码器线一定要用带屏蔽的双绞线屏蔽层单端接地接MCU地。另一个原因是采样周期太短10ms内脉冲太少导致量化误差大把周期放宽到20ms试试。还有一种情况是电源共地没做好。电机大电流在地线上产生压降把MCU的参考地拉偏编码器信号电平判断就出错。电机电源地和MCU电源地必须在一点汇合不要串成一条线。6.3 串口打印把控制环拖慢了这是新手最常见的隐藏问题。在10ms中断里调用printf而printf底层是HAL_UART_Transmit阻塞发送一条几十字节的消息在115200波特率下要几毫秒。中断里等几毫秒控制周期直接变成15ms甚至更久PID参数全废。我的做法是中断里只算PID把要观察的数据存进一个全局数组主循环里慢慢往外发。或者用DMA方式发串口几乎不占CPU。调试阶段也可以降低打印频率比如每10个控制周期打印一次虽然不连续但看趋势足够了。6.4 常见问题速查问题排查顺序电机完全不转供电→STBY→PWM波形→方向脚→电机本身能转但不闭环编码器供电→A/B相接线→CNT是否有变化转速方向反了交换A/B相或取反PID输出符号调速范围窄PWM频率过高导致驱动响应跟不上降到10kHz参数调不动检查采样周期是否固定中断是否被抢占换负载后回不来Ki太小或输出限幅不足以对抗负载7. 把单环往深处做的几个方向单环转速控制跑通之后你手上就有了一套可扩展的框架。想再往上走有这么几条路。一是加电流内环做串级。内环用ADC采驱动芯片的电流采样电阻周期做到1ms以内外环还是转速。内环响应快能限制启动电流、保护电机动态性能也比单环好。代价是硬件要改采样电阻和运放得加上。二是换更精细的测速方式。高速用M法、低速用T法两者根据转速自动切换这就是所谓的M/T法全速段精度都能保证。三是位置环叠加。在转速环外面再套一层位置控制读编码器累计角度做反馈就能做定点角度控制云台、机械臂关节都是这个路子。四是参数自整定。让MCU在启动时给一次阶跃记录响应曲线用简单规则估算Kp、Ki省去手工整定。这个方法不复杂但需要你对系统模型有基本认识否则估出来的参数不一定能用。我自己的习惯是先把单环做扎实数据能稳定打印、参数能在线改通过串口命令改kp、ki再去折腾串级。上来就搞串级两个环互相耦合出了问题你根本分不清是内环还是外环的锅。最后分享个我一直用的小习惯每次改完参数把kp、ki、kd、目标转速、实际转速、输出值一起打到串口存成CSV事后用表格软件画个曲线比盯着屏幕看数字强太多。曲线一画出来是超调还是震荡、是响应慢还是饱和一眼就清楚调参效率能翻好几倍。
返回列表