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

资讯详情

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

STM32直流减速电机PID闭环调速实战:原理、代码与调参

STM32直流减速电机PID闭环调速实战:原理、代码与调参 做电机控制这块只要碰过直流减速电机的人迟早都得正面硬刚一次PID。不管是做小车底盘、机械臂关节还是搞个变频风机电机转速控制始终是两个思路开环给PWM或者闭环上PID。开环简单但负载一变化转速就飘闭环PID则能让电机顶着负载波动也能稳稳咬住目标转速。这篇博客我就用STM32F103C8T6这颗最常用的芯片手把手把PID控制电机转速这套东西拆开讲透从原理、硬件接线、编码器测速到位置式PID和增量式PID的完整C代码、调参方法和踩坑记录一并写清楚。适合刚接触电控的在校学生也适合准备做机器人、小车、云台项目的DIY玩家代码可以直接抄过去改参数用。我先说下最终效果硬件上用一个带霍尔编码器的直流减速电机STM32通过TIM输出PWM控制电机驱动模块TIM编码器模式实时读取转速PID调节器每10ms运行一次最终能让电机在空载和带载情况下都稳定在设定转速附近误差控制在几十转以内。文章的代码我都实测过可以直接用。1. 项目整体设计与思路拆解1.1 为什么开环PWM不够用很多人第一个电机实验都是直接给PWM波占空比越大转速越快。这个做演示没问题但一旦认真起来就露馅。我举个实际例子一辆小车在平整地面PWM给到50%轮子转速假设是300RPM推到地毯上阻力增加同样50%占空比转速可能掉到200RPM。你没有转速反馈就不知道电机已经“偷懒”了这在闭环系统里是绝对不允许的。开环控制做不到的事情本质上是缺少一个“比较基准”。你只知道你给了多少输入却不知道系统实际输出了多少。闭环控制的思路是在输出端加一个传感器把实际转速读回来和目标值做差再用这个误差去修正输入。这就是自动控制原理里最基本的反馈控制。PID是工程中使用最广泛的一种反馈控制算法。P是比例I是积分D是微分三者各司其职比例项对当前误差做出反应误差大就输出大积分项负责消除稳态误差把历史误差积累起来补上微分项预测误差变化趋势提前抑制超调。1.2 PID闭环系统由哪几部分组成一套完整的电机转速PID闭环系统你必须搞清楚有四个环节缺一不可。第一个环节是被控对象也就是直流减速电机本身。第二个是执行机构即驱动模块和PWM波形它把控制器算出来的控制量转换成电机的驱动电压。第三个是测量环节这里用的是霍尔编码器加上定时器的编码器模式把机械转速转换成数字脉冲。第四个是控制器也就是跑在STM32里的PID算法。这四个环节的时序配合很关键。整个系统是一个周期运行的循环采样当前转速、和目标比较、算误差、PID计算、更新PWM占空比。这个循环的运行周期就是采样周期我后续的代码用的是10ms也就是系统的控制频率是100Hz实测下来控制效果不错。1.3 方案选型为什么用霍尔编码器测速测电机转速常见的方法有几种测频法M法、测周法T法以及两者结合的M/T法。测频法是在固定时间内数脉冲适合高速测量测周法是测量一个脉冲的时间间隔适合低速。STM32的编码器模式本质上可以做到类似M法的效果而且正交解码还能判断方向。我用的电机是常见的带霍尔编码器的JGB37-520直流减速电机减速比30:1霍尔编码器每圈产生13个脉冲。因为减速器输出的机械轴转一圈电机轴要转30圈所以轮子转一圈编码器一共输出13×30390个脉冲。如果TIM配置成4倍频就是1560个计数。这个参数后面换算转速会用到。选带编码器的电机是有讲究的不要买那种只有两根电源线的普通TT马达那种没有测速条件做不了闭环。霍尔编码器电机通常会引出6根线电机正负极两根编码器电源5V和GND两根编码器A相和B相两根。A相和B相相位差90度正是正交编码器天然适合STM32的编码器接口模式。2. 硬件准备与基础工程搭建2.1 硬件清单这套项目的硬件非常常见我列一个清单照单买就行STM32F103C8T6最小系统板一块就是俗称的“蓝Pill”性价比高。带霍尔编码器的直流减速电机一个最好选自带减速箱的输出轴扭矩大。电机驱动模块推荐TB6612FNG比L298N效率高、体积小。L298N压降太大发热严重。电源给驱动模块供电的电池或实验室电源电压根据电机额定电压选比如6V或者12V。杜邦线若干面包板一块调试用。接线关系是STM32的PWM输出引脚接驱动模块的PWMA、AIN1、AIN2编码器的A相和B相接STM32定时器的CH1和CH2编码器供电5V接板子的5V引脚。我用的是TIM1产生PWMTIM2配置成编码器模式读编码器。这样分工有个好处两个定时器互不干扰PWM频率和编码器计数率都可以独立设置。2.2 PWM产生与电机驱动接线STM32F103的定时器资源丰富我选择TIM1作为PWM输出通道。TIM1是高级定时器挂在APB2总线上时钟是72MHz。我配置PWM频率为20kHz这个频率超声波听不到而且开关损耗也不大。配置要点是设置TIM1的PSC和ARR。系统时钟72MHz我设置PSC为1分频即预分频值为1得到36MHz的计数时钟。如果要20kHz的PWMARR应该设为36MHz/20kHz-11799。实际配置中我习惯把PSC设置为72-1得到1MHz的计数时钟ARR设置为50得到20kHz这样CCR的单位正好是1%占空比的整数倍便于计算。驱动模块TB6612FNG的逻辑是这样的同样一个通道比如A通道通过设置AIN1和AIN2的电平组合实现正转、反转和刹车。PWM引脚给不同占空比就能控制速度。我用的控制逻辑是AIN1拉高、AIN2拉低PWMA给PWM波形电机正转反之反转。2.3 编码器接线与定时器配置编码器A相接TIM2_CH1也就是PA0引脚编码器B相接TIM2_CH2也就是PA1引脚。注意不是随便接都能用的必须接到支持编码器功能的定时器通道上。TIM2的编码器初始化代码可以这样写TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; 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 0; 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 0; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE);这段代码核心是把TIM2配置为编码器接口模式计数器会随着电机正转增加、反转减少。TIM_EncoderMode_TI12表示在TI1和TI2的上升沿和下降沿都计数即4倍频模式。一个容易犯的错误是编码器模式初始化后每次读取计数器值之后必须清零否则计数值会一直累加。正确做法是每次读取TIM2-CNT后把值的符号考虑进去再清零。3. PID算法核心代码实现3.1 位置式PID与增量式PID的区别PID算法的离散化表达有两种经典形式。位置式PID直接计算当前执行机构的绝对位置量它的输出是当前时刻应该给到执行器的值。增量式PID计算的是这一次相对于上一次需要改变的量执行器不断累加这个增量。在电机调速场景里我推荐用增量式PID原因有三个第一增量式输出的是增量不会产生积分饱和导致的剧烈超调第二它天然自带记忆功能系统断电重启后输出不会跳变第三增量式的计算不需要累加所有历史误差CPU开销小。不过位置式PID也有它的用武之地比如控制舵机角度这种需要输出绝对位置的场景。我在下面把两个版本都贴出来你可以对比着看。3.2 PID结构体定义与封装首先定义一个PID控制器的结构体统一管理所有参数和中间变量。这个结构体要考虑通用性不仅能用在电机上以后换个被控对象也能复用。typedef struct { float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 float target; // 目标值单位RPM float actual; // 当前实际值单位RPM float err; // 当前误差 float err_last; // 上一次误差 float err_prev; // 上上次误差 float integral; // 积分累加值 float output; // PID输出 float integral_limit; // 积分限幅防止积分饱和 float output_max; // 输出限幅上限 float output_min; // 输出限幅下限 } pid_t;结构体里的integral_limit很多人会忽略但实际调参时一旦发现转速过冲严重多半就是它在作怪。我把积分限幅默认设为100输出限幅默认设为-1000到1000对应PWM的千分比数值。3.3 增量式PID的C语言实现增量式PID的计算公式是Δu Kp * (e(n) - e(n-1)) Ki * e(n) Kd * (e(n) - 2*e(n-1) e(n-2))对应的C函数如下float PID_Incremental(pid_t *pid, float target, float actual) { pid-target target; pid-actual actual; pid-err target - actual; float increment pid-kp * (pid-err - pid-err_last) pid-ki * pid-err pid-kd * (pid-err - 2 * pid-err_last pid-err_prev); pid-output increment; // 输出限幅 if (pid-output pid-output_max) pid-output pid-output_max; if (pid-output pid-output_min) pid-output pid-output_min; // 更新历史误差 pid-err_prev pid-err_last; pid-err_last pid-err; return pid-output; }注意这里我把输出累加放在限幅之前限幅之后才做误差移位顺序不能乱。如果先更新误差再用新的误差参与计算结果会差一个周期导致控制滞后。3.4 位置式PID的C语言实现位置式PID的计算公式是u(n) Kp * e(n) Ki * Σe(i) Kd * (e(n) - e(n-1))对应的C函数如下float PID_Position(pid_t *pid, float target, float actual) { pid-target target; pid-actual actual; pid-err target - actual; // 积分项累加并做限幅 pid-integral pid-err; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; pid-output pid-kp * pid-err pid-ki * pid-integral pid-kd * (pid-err - pid-err_last); // 输出限幅 if (pid-output pid-output_max) pid-output pid-output_max; if (pid-output pid-output_min) pid-output pid-output_min; pid-err_last pid-err; return pid-output; }位置式PID和增量式PID在参数整定上有个重要区别位置式的积分项是累加的历史误差如果不对积分限幅一旦电机长时间堵转积分项会飙升到非常大等转动恢复时瞬间输出超大量出现剧烈超调。增量式没有这个问题但增量式的输出累加同样需要限幅否则长期运行可能溢出。4. 闭环控制搭建与主循环逻辑4.1 转速采样与换算公式编码器计数值本身是一堆脉冲个数要换算成转速RPM转每分钟才能作为PID的实际值参与计算。换算公式并不复杂实际转速RPM (采样周期内的脉冲数 / 电机每转一圈的脉冲数) * (60 / 采样周期秒数)以我的电机参数为例编码器4倍频后每圈输出13 × 4 52个脉冲减速比30:1所以电机输出轴转一圈对应 52 × 30 1560 个计数。采样周期我设定为10ms即0.01秒。换算公式就是RPM (delta_count / 1560) * (60 / 0.01) delta_count * 3.846。这个3.846倍就是把计数差值直接转换成RPM的系数在代码中直接用一个宏替代减少运行时的除法运算。需要注意的是delta_count是有符号数用int16_t类型强制转换。因为编码器模式下TIM2-CNT寄存器是16位范围0到0xFFFF正转从0往上加、反转从0xFFFF往下减所以不能直接用无符号数读出来当增量必须转成有符号类型。4.2 定时器中断周期的设定控制周期我选择10ms这个周期的选择有考究。太快了比如1msPID计算本身没问题但电机是机电惯性系统转一圈都要几十毫秒你1ms算一次控制量对最终转速没什么卵用反而增加CPU负担。太慢了比如100ms系统的响应速度不够负载突变时转速恢复得慢。10ms在大多数20kHz PWM频率和普通直流电机场景下是比较稳妥的中间值。我用TIM4做10ms周期中断中断服务函数里只做一件事设置一个10ms标志位。真正的PID计算和PWM更新放在主循环中执行。这样做的好处是避免在中断里做太多事影响实时性也避免编码器读取和PWM更新与主循环的其他逻辑冲突。void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); g_control_flag 1; } }4.3 主循环控制逻辑主循环的伪代码如下int main(void) { // 初始化 SystemInit(); GPIO_Config(); TIM1_PWM_Init(); // PWM输出 TIM2_Encoder_Init(); // 编码器模式 TIM4_TimeBase_Init(); // 10ms中断 USART1_Config(); // 串口打印调试 pid_t motor_pid; PID_Init(motor_pid, 0.3f, 0.05f, 0.01f, 1000.0f, -1000.0f); motor_pid.target 300.0f; // 目标转速 300 RPM while (1) { if (g_control_flag) { g_control_flag 0; // 1. 读取编码器计数 int16_t cnt (int16_t)TIM2-CNT; TIM2-CNT 0; // 2. 计算实际转速 float rpm (float)cnt * RPM_PER_COUNT; // 3. 调用PID计算 float output PID_Incremental(motor_pid, motor_pid.target, rpm); // 4. 输出到PWM Motor_SetSpeed((int)output); // 5. 串口打印调试信息 printf(Target: %.1f, Actual: %.1f, Output: %.1f\r\n, motor_pid.target, rpm, output); } } }Motor_SetSpeed函数需要把PID输出的数值映射到PWM的比较寄存器上。我的输出范围是-1000到1000正值表示正转占空比从0到100%负值表示反转。具体实现可以根据驱动模块的接线方式灵活调整核心只需要保证输出0时PWM占空比为0输出最大时占空比为100%中间线性对应即可。4.4 PWM输出的方向控制处理对于只要求单方向调速的场景AIN1和AIN2固定PID输出只取0到1000即可。但如果需要正反转控制就需要根据PID输出的符号切换方向引脚。这个逻辑要注意死区问题当PID输出在-10到10之间时PWM占空比很小电机可能因为静摩擦力根本不转这时候更合理的做法是直接输出0防止电机处于“似转非转”的抖动状态。我的代码是这么处理的void Motor_SetSpeed(int speed) { if (speed 1000) speed 1000; if (speed -1000) speed -1000; if (speed 20) // 正转 { AIN1_HIGH(); AIN2_LOW(); TIM_SetCompare1(TIM1, speed * PWM_PERIOD / 1000); } else if (speed -20) // 反转 { AIN1_LOW(); AIN2_HIGH(); TIM_SetCompare1(TIM1, (-speed) * PWM_PERIOD / 1000); } else // 死区 { AIN1_LOW(); AIN2_LOW(); TIM_SetCompare1(TIM1, 0); } }这里PWM_PERIOD是ARR的值加上1我代码里是50所以满占空比对应TIM_SetCompare1(TIM1, 50)。5. 调参实战与常见问题排查5.1 试凑法调参的标准流程很多人把PID调参想得非常玄学其实是有章可循的。最经典的试凑法流程是这样的先把积分和微分都设为0只保留比例项从小到大慢慢增加Kp观察电机转速响应。当Kp很小时转速上升慢稳不下来离目标转速差一截就上不去了。随着Kp增大响应变快但你也会发现当Kp大到某个值转速开始出现持续的上下波动也就是振荡。找到那个刚好开始振荡的临界Kp值然后往回调小到它的一半左右这是一个比较稳妥的起点。接着加积分项Ki从很小的值开始比如Kp的十分之一观察稳态误差是否被消除。积分的作用是缓慢地把“差的那一点”补上所以Ki太大会导致系统反复过冲甚至产生低频振荡。我通常最后加微分项Kd从更小的值开始用于抑制超调尤其当你会先看到一个明显的尖峰然后回落时加点Kd能明显改善。我在调试过程中总结的初始参数是Kp0.3Ki0.05Kd0.01这组参数在300RPM目标转速下稳定后空载误差在±10RPM以内带轻度负载也能在几秒内恢复到目标转速附近。5.2 常见的几个坑与排查方法我在实际调试中遇到过的典型问题有下面几类整理成表格方便排查现象可能原因排查方法电机完全不动PWM引脚或驱动模块接线有误先用固定占空比测试开环确认PWM通路正常后再接闭环电机转速乱跳读数不稳定编码器A/B相接线接反或接触不良检查编码器信号确认正交波形换用带内部上拉的GPIO模式转速始终达不到目标值PID输出达到限幅积分没起到作用增大输出限幅或者增大Ki检查PWM是否已经满占空比转速剧烈振荡Kp过大减小Kp或者增大采样周期检查输出限幅是否过小从启动瞬间就大幅超调积分饱和或微分项被干扰开启积分限幅对微分项做低通滤波换向时有尖锐抖动死区设置不合理增大PWM死区或者在速度过零时直接输出0还有一个常见但容易被忽略的问题用串口打印调试信息时printf的数据格式如果调试和主循环共用同一个串口可能导致主循环卡顿影响控制周期。建议只在调试阶段打印正式运行时关掉打印或者把打印放到一个较低的优先级逻辑中。5.3 积分饱和与微分波动的处理心得积分饱和这个问题我在做位置式PID时吃过亏。电机堵转状态下误差一直很大积分项涨到几十万一旦堵转解除PID瞬间输出最大PWM电机会猛地冲出去。应对方法就是积分限幅让累计的积分最多只能占到总输出的一部分这样即使遇到极端情况输出也不会失控。微分项在电机控制中容易被噪声干扰。编码器计数本身就存在量化误差相邻两个采样周期的速度差值可能因为一个计数的抖动出现较大波动微分项对这个波动最敏感容易放大噪声。我在实际代码里做了个简单的一阶低通滤波把微分输入平滑一下效果立竿见影。如果你不想引入滤波器可以适当减小Kd或者把采样到实际转速再算微分之前的原始值做一次移动平均。5.4 负载突变场景下的效果验证调参最终要落到实际场景验证。我会用手捏住电机输出轴模拟负载突变观察转速曲线捏住瞬间转速下降PID应该快速加大PWM把转速拉回目标松手瞬间转速突然上升PID又应该降低PWM避免超调太猛。实测下来Kp0.3、Ki0.05、Kd0.01这组参数在这个负载扰动下转速最大跌落大约30RPM恢复时间大约1秒虽然没有做到完全没有波动但作为通用电机调速系统已经够用了。如果你对动态响应要求很高可以进一步缩小采样周期、优化参数或者使用串级PID结构把电流环或者速度环再做细分。6. 项目扩展方向与最终经验总结6.1 从单电机到多电机的扩展思路这个项目虽然只控制了一个电机但整套代码的架构稍作修改就能扩展到双轮差速小车。你需要做的无非是再建一个PID实例分别控制左右电机然后根据差速算法给两个PID各自设定不同的目标转速。关于多电机扩展我有几个建议。一是每路PWM和编码器定时器要单独用一组避免通道冲突二是要多路PID实例共享同一套PID函数结构体里的参数各自独立即可三是如果左右电机特性不完全一样两边的PID参数要分别微调不要指望同一组参数两边效果完全一致。6.2 STM32定时器资源的合理分配用STM32F103C8T6做这类项目定时器资源需要提前规划。F103C8T6有TIM1到TIM4再加一个基本定时器TIM6和TIM7。我建议PWM输出用TIM1编码器输入用TIM2心跳或控制周期用TIM4。如果你的项目需要同时控制三四个电机那就要考虑用TIM1的多个通道输出PWM或者用不同定时器的编码器模式。还有个trick是如果编码器通道不够可以用外部中断加定时器计数的方式做测速虽然没有芯片的编码器模式方便但胜在灵活。不过我还是推荐优先使用编码器模式因为硬件层面的正交解码比软件判断A/B相更准确。6.3 我在实际项目中的体会这个项目做完之后我最大的体会是PID本身并不难写难的是参数怎么调、系统怎么搭。很多人上来就把PID公式背得滚瓜烂熟结果一接上电机发现完全控制不住就开始怀疑代码有问题。其实问题往往出在采样周期不合理、编码器数据没换算对、或者输出限幅没设好这些看起来不起眼的细节上。另外我建议调试时一定要用串口或者OLED把目标转速和实际转速实时显示出来只看眼睛估计转速是看不准的。有了数据曲线调参才能有的放矢不然就是在瞎试。我自己的习惯是手边放一张纸把每组Kp、Ki、Kd记录清楚再记下对应的现象调参就成了一次次对照游戏而不是玄学。最后再分享一个小技巧如果你发现PID总是调不到理想状态先别急着怀疑算法先确认开环下电机能不能稳定输出你想要的转速区间如果开环时转速就忽快忽慢那可能是电源功率不够或者PWM频率太低先解决硬件问题再来碰算法。这套“先硬件后软件、先开环后闭环”的顺序能帮你少走很多弯路。
返回列表