STM32直流有刷电机PID速度闭环控制:从编码器测速到算法实现
2026/9/4 13:43:33 网站建设 项目流程

简介:本资源是一套基于STM32F10x系列微控制器实现直流有刷电机闭环速度控制的完整嵌入式开发工程,面向嵌入式初学者、电机控制实践者及高校电类专业课程设计学生,解决直流电机转速测量不稳、PID参数整定困难、软硬件协同调试复杂等典型问题。压缩包共135个文件,含31个头文件(.h)定义外设与PID结构体、29个C源文件(.c)涵盖TIM编码器测速、ADC采样、PWM驱动及核心PID算法实现,另有编译中间文件(.o/.d)、调试配置(.dbgconf)、链接脚本(.sct)及可执行镜像(.hex/.axf),整体体积仅1.35MB,轻量易部署。已有555人学习下载,工程采用标准STM32固件库架构,模块清晰——从stm32f10x_tim.c等底层驱动到自定义speed_pid.c控制逻辑,附带Bak备份文件与多版本工程配置,便于对比调试与参数迭代,是理解电机测速原理、PID离散化实现及STM32实时控制流程的优质实操范例。

1. 项目背景与核心目标

最近在做一个两轮差速小车的项目,核心任务之一就是让小车能稳定地跑直线,或者按照设定的速度巡航。这听起来简单,但实际一上手就发现,用STM32驱动直流有刷电机,想让它的转速精准可控,真不是给个PWM信号就完事了。电机负载一变,或者电池电压波动,速度马上就飘了。为了解决这个问题,我决定从最基础的测速开始,一步步搭建一个完整的PID速度闭环控制系统。

这个“直流有刷电机测速 -PID控制”项目,本质上就是解决嵌入式运动控制中最经典的问题:如何让电机“听话”。它非常适合正在学习STM32、想从点灯进阶到实际控制应用的朋友,或者那些在做智能车、机器人底盘,但被电机速度波动困扰的开发者。整个过程会涉及硬件选型(编码器)、定时器的高级应用(编码器模式、输入捕获)、PID算法的原理与实现,以及如何将这三者有机整合,形成一个稳定的控制回路。通过这个项目,你不仅能学会让电机转起来,更能理解如何让它转得又快又稳。

2. 硬件方案选型与电路设计要点

要让电机“听话”,首先得知道它“现在转得多快”。这就离不开速度反馈传感器。对于直流有刷电机,最常用、性价比最高的方案就是增量式光电编码器。市面上常见的编码器有100线、200线、500线甚至更高,线数越高,测速分辨率越高,但价格也越贵,对MCU的计数能力要求也越高。对于一般的智能小车应用,200线或500线的编码器是完全足够的。

编码器一般输出A、B两相正交脉冲信号,有些还会带一个Z相(零位)信号。A、B两相信号相位差90度,通过判断两者的相位关系,我们可以知道电机的旋转方向;通过计算单位时间内的脉冲数,我们就可以算出转速。STM32的定时器有个非常强大的“编码器接口模式”,可以硬件自动识别A、B相的边沿,并据此自动增减计数器的值,大大减轻了CPU的负担。

硬件连接上,编码器的A、B相需要接到STM32的定时器通道上,并且这两个引脚必须属于同一个定时器的两个特定通道(例如TIM2的CH1和CH2)。电机的驱动我选用的是经典的TB6612FNG驱动芯片,它比L298N效率高、发热小。STM32通过PWM信号控制TB6612的输入引脚,从而控制电机的速度和方向。这里有一个细节:给电机供电的电源一定要和给STM32及编码器供电的电源做好隔离,至少要通过磁珠或电感进行滤波,否则电机启停时产生的大电流波动会严重影响MCU和编码器信号的稳定性,导致系统异常复位或测速不准。我的做法是使用两路独立的DC-DC降压模块,一路给电机驱动供电,另一路给控制板供电。

注意:编码器的信号线建议使用双绞线,并且靠近MCU的一端最好加上拉电阻(通常10kΩ即可)和滤波电容(几十到一百皮法),可以有效抑制长线传输带来的振铃和毛刺,提高计数可靠性。

3. STM32定时器编码器模式配置详解

配置编码器接口是测速准确与否的关键。我们以STM32F103系列,使用TIM2的CH1和CH2为例。在STM32CubeMX中配置非常简单,但理解背后的寄存器操作更有助于排查问题。

首先,将连接到编码器A、B相的GPIO口配置为复用功能模式(Alternate Function),并选择对应的定时器通道。然后,进入定时器配置:

  1. 编码器模式(Encoder Mode):选择“Encoder Mode TI1 and TI2”。这个模式意味着定时器会在TI1(CH1)和TI2(CH2)的每个边沿都计数。
  2. 极性(Polarity):通常选择“Rising Edge”,但具体要看你的编码器输出信号在空闲状态是高电平还是低电平。确保电机正转时计数器是增加的。
  3. 预分频器(Prescaler):设置为0,即不对计数时钟进行分频。
  4. 计数周期(Counter Period):设置为最大值65535(对于16位定时器)。这个值决定了计数器的溢出上限,在测速计算中很重要。
  5. 自动重装载值(AutoReload):同样设置为65535。当计数器达到此值时,会产生更新事件并清零(或重装载),同时会触发溢出中断。

配置完成后,启动定时器(调用HAL_TIM_Encoder_Start())。此时,定时器的计数器(CNT)寄存器就会随着编码器的脉冲实时变化。正转时CNT增加,反转时CNT减少。

但是,这里有一个巨大的“坑”:计数器溢出处理。编码器高速旋转时,16位的计数器很容易就计满了。如果只是简单地读取CNT值做差,一旦发生溢出,计算出的脉冲数就是错误的。例如,本次读数为65000,下次读数为1000,看似减少了64000个脉冲,实际可能是正向溢出了(增加了2000个脉冲)。为了解决这个问题,我们必须扩展计数器的位数。

一个经典的做法是:在定时器的更新中断(溢出中断)服务函数中,用一个有符号的32位甚至64位全局变量(如g_encoder_total_ticks)来记录溢出的次数。当正向溢出时,这个变量加上65536;反向溢出(下溢)时,减去65536。这样,我们获取的总脉冲数就是:total_ticks = g_encoder_total_ticks + TIMx->CNT。这个total_ticks是一个可以连续变化的大范围有符号数,真实反映了电机累计转过的角度。

// 在定时器更新中断回调函数中 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2)) { // 判断计数方向,向下计数代表正转(取决于编码器接线) g_encoder_total_ticks += 65536; // 正向溢出 } else { g_encoder_total_ticks -= 65536; // 反向溢出 } } }

4. 速度计算:从脉冲数到转速(RPM)

有了精确的累计脉冲数,下一步就是在固定时间间隔内计算速度。我们定义一个速度计算周期,比如10ms(100Hz)。在每个周期开始时,记录当前的total_ticksticks_last;周期结束时,再次读取total_ticksticks_now

那么,在这个周期内产生的脉冲数delta_ticks = ticks_now - ticks_last

转速的计算公式为:速度 (RPM) = (delta_ticks / (编码器线数 * 4)) / (采样周期(秒)) * 60

这里需要解释几个关键点:

  1. 编码器线数 * 4:这是因为我们使用了定时器的编码器模式,在A、B相每个边沿都计数。对于一个200线的编码器,电机转一圈,A相会产生200个脉冲,B相也会产生200个脉冲,且边沿数是脉冲数的2倍。在“Encoder Mode TI1 and TI2”模式下,定时器会对两个信号的所有边沿(上升沿和下降沿)进行计数,因此电机转一圈,计数器会计数200线 * 4倍频 = 800个计数。这就是所谓的“四倍频”,它大大提高了测角分辨率。
  2. 采样周期:单位是秒。如果我们的计算周期是10ms,那么采样周期就是0.01秒。
  3. 乘以60:将“转每秒”转换为更常用的“转每分钟”(RPM)。

所以,对于一个200线编码器,10ms采样周期,公式简化为:速度 (RPM) = delta_ticks / (200 * 4) / 0.01 * 60 = delta_ticks * 0.75速度 (RPM) = delta_ticks * (60 / (200*4*0.01)) = delta_ticks * (60 / 8) = delta_ticks * 7.5

这个计算过程最好放在一个定时中断服务函数中完成,以确保采样周期的绝对准确。我通常使用一个基本定时器(如TIM6)来产生精确的10ms中断,在这个中断里进行速度采样和计算,并将计算出的速度值存入一个全局变量供PID控制器读取。

实操心得:速度值最好进行一下软件滤波。因为机械振动或偶尔的脉冲丢失/干扰,会导致单次计算的速度值有毛刺。一个简单有效的一阶低通滤波(或称指数加权平均)就能让速度曲线平滑很多:filtered_speed = 0.8 * filtered_speed_last + 0.2 * current_raw_speed。系数可以根据实际效果调整。

5. PID控制算法原理与离散化实现

得到了电机的实时速度,我们就可以用PID控制器来调节PWM输出,使速度趋近于我们设定的目标值。PID是比例(Proportional)、积分(Integral)、微分(Derivative)控制的缩写。它的思想非常直观:

  • 比例P:当前误差有多大,我就按比例给多大的“力”。误差越大,输出越大。它决定了系统的响应速度,但纯比例控制会有静差(稳态误差)。
  • 积分I:把历史上所有的误差累积起来。它能消除静差,因为只要误差不为零,积分项就会不断累积,直到输出足够大来抵消这个误差。但积分太强会导致系统超调甚至振荡。
  • 微分D:预测误差未来的变化趋势。如果误差正在快速减小,我就提前减小输出,防止“冲过头”。它能增加系统稳定性,抑制超调,但对噪声非常敏感。

在数字系统中,我们需要离散化的PID算法。最常用的是位置式PID和增量式PID。位置式PID直接计算控制量的绝对大小,而增量式PID计算的是控制量的增量。对于电机PWM控制,我更喜欢用位置式PID,因为PWM占空比本身就是一个绝对量。

离散化的位置式PID公式如下:u(k) = Kp * e(k) + Ki * sum(e(j)) + Kd * [e(k) - e(k-1)]其中:

  • u(k)是当前时刻的控制输出(即PWM占空比)。
  • e(k)是当前时刻的误差,e(k) = 目标速度 - 当前测量速度
  • sum(e(j))是误差的累加和,即积分项。
  • [e(k) - e(k-1)]是本次误差与上次误差的差值,近似为微分。

在代码中,我们需要维护几个全局变量:上次误差error_last、误差累计和error_sum。同时,必须对积分项进行抗饱和处理(Integral Anti-windup),这是PID调参稳定与否的关键。当输出已经达到极限(比如PWM最大占空比100%)时,如果误差依然存在,积分项会无限制地累加(“饱和”),导致系统退出饱和区时产生巨大的超调。简单的处理方法是:在计算输出后,如果输出饱和了,则停止积分项的累加。

typedef struct { float target; // 目标值 float measure; // 测量值 float error; // 当前误差 float error_last; // 上次误差 float error_sum; // 误差积分和 float kp, ki, kd; // PID参数 float output; // 输出值 float output_max; // 输出上限 float output_min; // 输出下限 float integral_max; // 积分限幅 } PID_Controller; float PID_Calculate(PID_Controller *pid) { pid->error = pid->target - pid->measure; // 比例项 float p_out = pid->kp * pid->error; // 积分项(带抗饱和) pid->error_sum += pid->error; // 积分限幅 if (pid->error_sum > pid->integral_max) pid->error_sum = pid->integral_max; if (pid->error_sum < -pid->integral_max) pid->error_sum = -pid->integral_max; float i_out = pid->ki * pid->error_sum; // 微分项 float d_out = pid->kd * (pid->error - pid->error_last); // 计算总输出 pid->output = p_out + i_out + d_out; // 输出限幅 if (pid->output > pid->output_max) pid->output = pid->output_max; if (pid->output < pid->output_min) pid->output = pid->output_min; // 更新上次误差 pid->error_last = pid->error; return pid->output; }

6. PID参数整定:从理论到实践的调参过程

PID参数整定是个经验活,但遵循一定步骤可以少走弯路。我的调参顺序是:先P,再I,最后D

  1. 初始化:将Ki和Kd设为0,输出限幅设置为PWM的有效范围(如0-1000对应0%-100%占空比)。设定一个较低的目标速度(例如50 RPM)。
  2. 调节比例系数Kp:逐渐增大Kp,直到系统开始出现持续的、小幅度的振荡。此时系统的响应很快,但无法稳定在目标值。记下这个使系统开始振荡的Kp值,称为“临界Kp”。
  3. 引入积分系数Ki:将Kp设置为“临界Kp”的60%左右。然后逐渐增加Ki。积分的作用是消除静差。你会发现随着Ki增大,系统能慢慢稳定到目标值,但响应可能会变慢,或者出现超调后缓慢回落的现象。调整Ki,使系统既能消除静差,又不会引起明显的超调或振荡。如果出现振荡,说明Ki太大了。
  4. 引入微分系数Kd(可选):如果系统在加入PI后,达到稳定的速度较慢,或者超调量比较大,可以尝试加入微分。微分项对噪声敏感,所以实际使用时,微分项常常会和一个低通滤波器一起使用(不完全微分)。从很小的Kd值开始(比如Kp的十分之一),观察系统响应。合适的Kd能有效抑制超调,让曲线更平滑地接近目标值。但Kd过大反而会引入高频抖动。
  5. 现场微调:上述是在固定负载下的调试。实际小车运行中,负载(如爬坡)、地面摩擦都会变。因此,需要在各种典型场景下(空载、负重、启动、急停)测试,对参数进行微调,找到一个在大多数情况下都表现稳健的折中值。

一个实用的技巧是动态调整积分限幅。在电机启动初期,误差很大,积分项会快速累积,容易导致启动过冲。我们可以设置一个较小的初始积分限幅,或者在一开始将积分项清零,等速度接近目标值后再开启积分,这被称为“积分分离”。

7. 系统整合与闭环控制流程搭建

现在,我们把测速和PID控制两个模块串起来,形成一个完整的闭环。整个系统的运行基于两个定时器中断:

  • 定时器A(高优先级):负责高速、精确的电机PWM生成。通常使用高级定时器(如TIM1)的PWM输出模式,频率设置在10kHz-20kHz之间,这个频率远超电机响应频率,且人耳听不到噪音。
  • 定时器B(低优先级):负责速度采样与PID计算。我使用TIM6基本定时器,产生10ms的周期性中断。在这个中断服务函数中,依次执行以下步骤:
    1. 速度采样:调用速度计算函数,获取当前滤波后的电机转速current_speed
    2. PID计算:将current_speedtarget_speed传入PID计算函数,得到本次的控制量pwm_duty
    3. 输出限幅与更新:确保pwm_duty在有效范围内,然后更新PWM比较寄存器的值(如__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pwm_duty)),从而改变电机驱动芯片的输入信号。
    4. (可选)数据记录:将目标速度、实际速度、PWM占空比等数据存入数组,方便后续通过串口发送到上位机进行波形分析。这是调参的利器。

整个流程形成了一个负反馈闭环:速度低了 -> 误差为正 -> PID输出增大 -> PWM占空比增大 -> 电机加速 -> 速度升高 -> 误差减小。如此循环,最终将速度稳定在目标值附近。

踩坑实录:中断优先级设置不当会导致系统卡死。务必确保PID计算中断(TIM6)的优先级低于编码器计数中断(TIM2更新中断)。因为编码器计数需要及时响应,如果被PID计算长时间阻塞,会导致脉冲丢失,测速不准。同时,PID计算中断本身不能执行太长时间的计算,避免影响其他任务。如果发现系统反应迟钝,可以用逻辑分析仪或示波器检查中断是否被意外阻塞。

8. 上位机调试与波形分析:让调参可视化

“盲调”PID参数是非常痛苦的。我们必须让数据“说话”。一个简单的方法是使用STM32的串口,将每个控制周期(10ms)的目标速度、实际速度、PWM输出值打包发送到电脑。在电脑端,我们可以用任何能绘制曲线的工具来接收并显示,比如SerialPlotMATLAB,或者自己用Python(matplotlib库)写一个简单的绘图程序。

通过观察波形,我们可以清晰地看到:

  • 阶跃响应:突然改变目标速度,看实际速度跟踪的快慢、超调大小、稳定时间。
  • 抗扰性能:在电机稳定运行时,用手轻轻捏住车轮增加负载,看速度下跌多少,恢复速度有多快。
  • 稳态误差:稳定后,实际速度与目标速度的差值是否在可接受范围内。

例如,如果响应曲线上升很慢,说明Kp太小;如果超调严重且振荡,说明Kp太大或Ki太大;如果负载变化后速度恢复很慢,说明Ki不够大。有了波形作为依据,调参就从“玄学”变成了“科学”。

我常用的数据打包格式是简单的CSV:目标值,测量值,输出值\n。在串口中断中快速发送,在上位机按逗号分割即可。为了减少串口数据量对系统实时性的影响,也可以每5个或10个周期发送一次数据。

9. 常见问题排查与性能优化

在实际部署中,你可能会遇到以下问题:

问题一:电机低速时速度测量跳动大,甚至出现反转计数。

  • 原因:编码器信号质量差,在电平临界点抖动。电机低速时,脉冲间隔时间长,抖动的影响更明显。
  • 解决
    1. 硬件上,确保编码器供电稳定,信号线加上拉电阻和滤波电容。
    2. 软件上,除了前面提到的一阶滤波,可以尝试“速度死区”处理。当计算出的速度绝对值小于某个阈值(如5 RPM)时,直接认为速度为零。或者,使用M法测速(测量固定时间内的脉冲数)在低速时误差大,可以切换到T法测速(测量相邻两个脉冲的时间间隔),但T法在高速时精度差。更高级的方法是M/T法,结合两者优点。

问题二:PID输出控制电机时,电机发出“滋滋”声或抖动。

  • 原因:PWM频率不在合适的范围内。频率太低(如1kHz以下),电机会有明显的噪音和振动;频率太高,驱动芯片的开关损耗会增大。
  • 解决:将电机驱动PWM频率设置在10kHz到20kHz之间。这个频率段通常能兼顾效率和静音。检查你的定时器预分频和自动重装载值设置是否正确。

问题三:系统响应很快,但一加上负载速度就掉下去,恢复很慢。

  • 原因:积分项Ki不足,导致系统对抗负载扰动的能力(鲁棒性)差。
  • 解决:适当增大Ki。但要注意,增大Ki的同时,可能需要重新微调Kp,并检查积分限幅是否合理,防止积分饱和。

问题四:代码跑起来后,电机完全不受控,或者PWM输出异常。

  • 排查链路
    1. 查硬件:用万用表或示波器检查PWM输出引脚是否有波形,电压幅值是否正确。检查电机驱动芯片的输入逻辑和输出端。
    2. 查初始化:确认定时器(编码器、PWM、采样定时器)是否都成功启动(HAL_TIM_Base_Start()HAL_TIM_PWM_Start())。
    3. 查中断:确认采样定时器中断是否正常进入。可以在中断服务函数里翻转一个LED灯来测试。
    4. 查数据流:在PID计算函数前后打印出关键变量(目标值、测量值、误差、输出值),看计算逻辑是否正确。特别检查测量速度值是否正常,如果速度一直为0,闭环就无法建立。

性能优化建议

  • 将PID计算中的浮点运算改为定点数运算(Q格式),可以大幅提升在无FPU的Cortex-M3/M4内核上的计算速度。
  • 如果控制多个电机,可以考虑将它们的PID计算分散在不同的定时器中断中,避免一个中断内计算量过大。
  • 对于更复杂的场景(如位置控制、速度规划),可以考虑引入更高级的控制算法,如模糊PID、前馈控制等,但经典PID对于大多数速度控制场景已经足够强大和稳定。

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

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

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

立即咨询