尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于STM32与PID的直流有刷电机闭环速度控制实战指南

基于STM32与PID的直流有刷电机闭环速度控制实战指南 1. 项目概述从零开始搞定直流有刷电机驱动最近在做一个需要精确控制电机转速的小项目核心就是驱动一个24V的直流有刷电机。这听起来像是电子爱好者的入门课但真要把速度控得稳、响应快里面门道可不少。我选择了经典的STM32F103C8T6作为主控搭配PWM、编码器和PID这三件套算是把闭环控制的基础流程走了一遍。过程中踩了不少坑也积累了一些心得今天就把从硬件选型、软件配置到PID调参的完整过程以及那些容易忽略的细节系统地梳理出来。无论你是刚接触电机控制的在校学生还是需要在产品中实现稳定电机驱动的工程师这篇内容都能提供一个清晰、可复现的参考路径。直流有刷电机结构简单、成本低、驱动容易在机器人、智能小车、小型机械设备里应用非常广。但它的转速会随着负载和电压波动而变化开环控制很难满足精度要求。所以我们的目标就是构建一个闭环系统用STM32产生PWM波控制电机驱动芯片进而调节电机电压同时通过编码器实时读取电机转速将这个速度反馈给STM32STM32内部的PID算法会比较目标速度和实际速度的差异并动态调整PWM输出最终让实际速度紧紧跟随设定值。这个“感知-决策-执行”的闭环就是实现精准控制的核心。2. 核心硬件选型与电路设计思路驱动一个电机远不是单片机直接接上电机那么简单中间需要一系列硬件模块来搭建桥梁。选型不当轻则性能不达标重则烧毁芯片。我的方案是基于STM32F103C8T6最小系统板展开的这块板子资源足够性价比极高是很多项目的起点。2.1 主控芯片为什么是STM32F103C8T6STM32F103C8T6江湖人称“蓝色药丸”在爱好者圈子里经久不衰。选择它驱动电机理由很充分首先它基于ARM Cortex-M3内核72MHz的主频对于处理PID运算和编码器脉冲计数绰绰有余。其次它拥有丰富的外设特别是高级定时器如TIM1, TIM2, TIM3, TIM4这些定时器原生支持产生多路互补带死区的PWM输出以及编码器接口模式这简直是为电机控制量身定做的功能。最后其生态系统成熟HAL库、标准库资料海量用STM32CubeMX工具可以图形化配置极大降低了开发门槛。虽然现在有国产替代芯片如GD32系列性能也不错且更具性价比但STM32F103的社区支持和资料完备性目前还是无可比拟的对于学习和快速原型开发非常友好。2.2 电机驱动电路H桥与驱动芯片选型单片机GPIO口的驱动能力非常弱通常只有几十毫安而电机启动和堵转时电流可能高达数安培。因此我们必须使用专门的电机驱动芯片作为“功率开关”。最经典的方案是H桥电路它通过四个开关管的组合可以控制电机的正转、反转和刹车。对于中小功率的直流有刷电机比如工作电流在3A以内我强烈推荐使用集成H桥驱动芯片比如DRV8833、TB6612FNG或者L298N。它们将MOSFET和逻辑控制电路集成在一起使用起来非常方便。DRV8833这是我个人比较偏好的选择。它体积小效率高支持高达1.5A的连续驱动电流峰值更高内置了过流保护和过热保护逻辑电平兼容3.3V可以直接与STM32连接。其控制逻辑简单两个输入引脚IN1, IN2决定电机状态正转/反转/刹车/停止。TB6612FNG性能和DRV8833类似驱动能力稍强连续1.2A峰值3.2A同样内置保护电路也是3.3V逻辑兼容。L298N更老、更经典的模块驱动能力强单桥2A但它是双H桥设计体积大发热也相对严重效率不如前两者且需要外接散热片。它的逻辑电平是5V如果连接3.3V的STM32可能需要电平转换或谨慎确认兼容性。注意无论选择哪款驱动芯片务必仔细阅读数据手册关注其最大电压和连续/峰值电流参数必须留有余量比如电机额定电流1A最好选择连续电流≥2A的芯片。同时一定要在电机电源输入端并联一个大容量的电解电容如100uF-470uF和若干个小容量的陶瓷电容如0.1uF用于滤除电机产生的电流尖峰和噪声这对保护驱动芯片和单片机稳定工作至关重要。2.3 速度反馈编码器的选择与接口闭环控制的核心是反馈。我们需要一个传感器来告诉单片机“电机现在到底转得多快”。这就是编码器的任务。增量式光电编码器最常见的选择。它输出两路相位差90度的方波信号A相和B相。通过STM32定时器的编码器接口模式可以自动根据A、B相的边沿和相位关系进行计数从而得到位置信息。通过对位置进行差分计算单位时间内的脉冲数就能得到速度。它的优点是分辨率高几百到几千线每转、响应快。缺点是怕灰尘、油污。霍尔编码器原理类似但使用霍尔传感器检测磁极变化。通常分辨率较低几十线每转但结构坚固耐恶劣环境成本也较低。很多直流有刷电机本身就集成了霍尔传感器用于换向可以复用这些信号作为低分辨率编码器。磁编码器如AS5047P这是一种绝对位置传感器通过SPI等数字接口直接输出绝对角度值。精度可以非常高14位即16384步/圈。它的优势是绝对位置、非接触、抗干扰强。但成本相对增量式编码器高且需要处理SPI通信。如果你的应用需要知道电机的绝对初始位置或者空间受限无法安装光电编码器磁编码器是很好的选择。对于大多数速度控制场景增量式光电编码器性价比最高。STM32的定时器如TIM2, TIM3, TIM4硬件上直接支持编码器模式只需将编码器的A、B相分别接到定时器的两个通道上在CubeMX中配置为“Encoder Mode”就能自动实现四倍频计数几乎不占用CPU资源。3. 软件架构与STM32CubeMX基础配置硬件连接好后软件是让整个系统动起来的大脑。使用STM32CubeMX进行初始化配置可以节省大量底层寄存器操作的时间让我们更专注于应用逻辑。3.1 工程创建与外设初始化首先在CubeMX中选择正确的芯片型号STM32F103C8T6。然后我们需要配置几个关键外设系统时钟SYS将Debug设为Serial Wire方便后续调试和下载。时钟树RCC选择外部高速时钟HSE并将系统时钟SYSCLK通过PLL倍频到72MHz这是芯片的最高性能状态。定时器用于PWM生成选择一个高级定时器如TIM1或通用定时器如TIM2/3/4来产生PWM。以TIM1的通道1为例将TIM1_CH1对应的引脚如PA8功能设置为“PWM Generation CH1”。在TIM1的参数配置中设置Prescaler预分频器和Counter Period自动重装载值ARR。这两个值共同决定PWM的频率。PWM频率 定时器时钟 / ( (Prescaler1) * (ARR1) )。对于电机驱动PWM频率通常在1kHz到20kHz之间。频率太低电机会有啸叫太高则开关损耗增大。我通常选择10kHz左右。假设定时器时钟为72MHz要得到10kHz可以设置Prescaler71ARR99这样计算72,000,000 / ((711)*(991)) 10,000 Hz。将Pulse脉冲值即CCR1寄存器初始设置为0这是初始占空比。将CH Polarity设置为High表示有效电平为高电平。定时器用于编码器接口选择另一个通用定时器如TIM2用于连接编码器。将TIM2_CH1和TIM2_CH2对应的引脚如PA0, PA1功能设置为“Input Capture direct mode”。在TIM2的参数配置中将Combined Channels设置为“Encoder Mode”。在编码器模式下Encoder Mode通常选择“Encoder Mode TI1 and TI2”以实现对A、B两相信号的计数。设置Counter PeriodARR为最大值6553516位定时器或429496729532位定时器因为编码器是连续计数的我们期望它溢出后自动翻转。Polarity根据编码器信号的实际有效边沿选择通常选择“Rising Edge”。串口USART配置一个串口如USART1用于在调试时通过串口助手打印数据如当前速度、PWM值、PID参数等这对于PID调参至关重要。生成代码配置好所有外设后设置好工程路径和工具链MDK-ARM或STM32CubeIDE生成代码。3.2 PWM驱动代码实现在生成的工程中我们需要在main.c的合适位置通常在while(1)循环之前启动PWM输出。// 启动TIM1的PWM通道1输出 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 定义一个函数来设置PWM占空比 void Set_Motor_PWM(int32_t pwm_value) { // 限制PWM值在合理范围内例如0-1000对应0%-100%占空比 if (pwm_value 1000) pwm_value 1000; if (pwm_value -1000) pwm_value -1000; // 负值代表反转需要结合驱动芯片逻辑 // 假设我们使用单极性PWM控制驱动芯片由另一个GPIO控制方向 if (pwm_value 0) { HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_SET); // 设置正转方向 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, pwm_value); // 设置占空比 } else { HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_RESET); // 设置反转方向 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, -pwm_value); // 设置占空比取绝对值 } }这里需要注意的是很多集成驱动芯片如DRV8833有两个输入IN1和IN2。控制电机状态正转/反转/刹车/停止需要操作这两个引脚而不是简单地给一个PWM信号。例如一种常见的控制方式是IN1PWM IN20 表示正转IN10 IN2PWM 表示反转IN1IN21 或 0 表示刹车。你需要根据数据手册调整代码逻辑。3.3 编码器速度读取编码器计数值的读取相对简单因为定时器硬件已经帮我们完成了计数。// 启动TIM2的编码器接口模式 HAL_TIM_Encoder_Start(htim2, TIM_CHANNEL_ALL); // 定义一个函数获取当前编码器计数值并计算速度 int32_t Get_Motor_Speed(void) { static int32_t last_count 0; static uint32_t last_time 0; int32_t current_count, speed_rpm; uint32_t current_time; current_count (int32_t)TIM2-CNT; // 直接读取计数器寄存器注意数据类型转换 current_time HAL_GetTick(); // 获取当前系统毫秒时间戳 // 计算两次采样间的脉冲差和时间差 int32_t pulse_diff current_count - last_count; uint32_t time_diff current_time - last_time; // 处理定时器溢出16位定时器最大值65535 if (pulse_diff 32767) pulse_diff - 65536; else if (pulse_diff -32767) pulse_diff 65536; // 计算转速速度 (脉冲差 / 时间差) * (60000 ms/min) / (编码器线数 * 4) // 假设编码器为500线定时器四倍频后每转脉冲数 500 * 4 2000 if (time_diff 0) { // 避免除零 speed_rpm (pulse_diff * 60000) / (time_diff * 2000); } else { speed_rpm 0; } // 更新上一次的值 last_count current_count; last_time current_time; return speed_rpm; // 返回单位是RPM转/分钟 }这段代码的关键在于处理定时器计数器的溢出和回绕以及将脉冲数转换为直观的转速RPM。采样时间间隔time_diff不宜过短否则速度值会跳动剧烈也不宜过长否则系统响应慢。通常50ms到100ms是一个不错的折中。4. PID控制算法的原理与代码实现有了PWM输出能力和速度反馈接下来就是让它们智能协作的大脑——PID控制器。PID是比例Proportional、积分Integral、微分Derivative控制的缩写它通过计算目标值与实际值的误差并对其进行三种运算的加权和来产生控制输出。4.1 PID算法离散化公式在单片机中我们实现的是离散数字PID。其位置式PID公式如下Output Kp * error Ki * sum(error) Kd * (error - last_error)其中errortarget_speed-current_speed本次误差sum(error)是历次误差的累加积分项last_error是上一次的误差Kp,Ki,Kd就是我们需要调节的三个核心参数。在实际应用中更常用的是增量式PID它只计算本次输出的增量对系统冲击小且更易于处理积分饱和等问题。增量式PID公式为ΔOutput Kp * (error - last_error) Ki * error Kd * (error - 2*last_error last_last_error)然后本次Output 上次Output ΔOutput。4.2 代码实现与抗积分饱和处理下面是一个简单的增量式PID结构体和函数实现并加入了积分限幅和输出限幅这是工程实践中必不可少的。typedef struct { float Target; // 目标值 float Current; // 当前值 float Error; // 本次误差 float LastError; // 上次误差 float PrevError; // 上上次误差用于增量式 float Kp, Ki, Kd; // PID参数 float Integral; // 积分项 float MaxIntegral; // 积分限幅 float MaxOutput; // 输出限幅 } PID_TypeDef; void PID_Init(PID_TypeDef *pid, float kp, float ki, float kd, float max_i, float max_out) { pid-Kp kp; pid-Ki ki; pid-Kd kd; pid-MaxIntegral max_i; pid-MaxOutput max_out; pid-Target 0; pid-Current 0; pid-Error pid-LastError pid-PrevError 0; pid-Integral 0; } float PID_Calculate_Incremental(PID_TypeDef *pid) { float increment, output; pid-Error pid-Target - pid-Current; // 计算增量 increment pid-Kp * (pid-Error - pid-LastError) pid-Ki * pid-Error pid-Kd * (pid-Error - 2*pid-LastError pid-PrevError); // 假设有一个全局变量或静态变量保存上次输出 static float last_output 0; output last_output increment; // 输出限幅 if (output pid-MaxOutput) output pid-MaxOutput; if (output -pid-MaxOutput) output -pid-MaxOutput; // 更新误差历史 pid-PrevError pid-LastError; pid-LastError pid-Error; last_output output; return output; } // 在主循环中调用 void Motor_Control_Loop(void) { int32_t current_speed; float pid_output; int32_t pwm_duty; // 1. 获取当前速度 current_speed Get_Motor_Speed(); // 2. 更新PID控制器当前值并计算输出 motor_pid.Current (float)current_speed; pid_output PID_Calculate_Incremental(motor_pid); // 3. 将PID输出转换为PWM占空比 // 假设pid_output范围是-1000~1000对应PWM的-100%~100%方向占空比 pwm_duty (int32_t)pid_output; // 4. 设置电机PWM Set_Motor_PWM(pwm_duty); }实操心得积分饱和与“积分分离”当电机启动或者目标速度突变时误差会瞬间很大。如果积分项无限制累加会导致PID输出长时间处于极限值饱和即使误差减小了输出也下不来系统会产生很大的超调和震荡。这就是“积分饱和”。解决方法除了上面代码中的积分限幅MaxIntegral还可以采用“积分分离法”当误差大于某个阈值时取消积分作用令Ki0只使用PD控制当误差进入较小范围时再引入积分作用以消除静差。这能显著改善动态响应。5. PID参数整定与系统调试实战PID参数Kp Ki Kd调得好不好直接决定了系统性能。这是一个“理论指导实践为主”的过程。网上流行的口诀“先比例后积分再微分”是基本准则。5.1 手动调参步骤与观察初始化首先将Ki和Kd设置为0 MaxOutput设为PWM最大值如1000。通过串口实时打印当前速度Current和目标速度Target。调Kp比例系数给一个较小的Kp比如1.0设定一个目标速度如100 RPM。观察电机速度响应。如果根本启动不了或速度远低于目标缓慢增大Kp。随着Kp增大电机会开始转动并且实际速度会向目标值靠近。继续增大Kp直到系统出现小幅度的、频率较快的震荡比如速度在目标值上下快速波动。此时将Kp回调到震荡消失前的那个值记为Kp_critical。这个值大约是系统临界稳定的比例系数。调Ki积分系数将Kp设为0.5 * Kp_critical左右的一个值。引入一个较小的Ki比如0.1。积分的作用是消除静差稳态误差。观察系统达到稳定后实际速度是否与目标速度一致。如果仍有静差缓慢增大Ki。增大Ki会使系统更“努力”地消除误差但也会引入相位滞后可能使系统响应变慢或产生低频震荡。当出现缓慢的、幅度增大的震荡时说明Ki太大了需要减小。理想状态是系统能较快地达到目标速度且稳定后没有静差响应曲线平滑。调Kd微分系数微分项预测误差变化的趋势具有“阻尼”作用可以抑制超调和震荡。在前两步调好的基础上如果系统达到目标速度时超调明显或者有轻微的高频抖动可以尝试引入一个很小的Kd比如0.01。特别注意微分项对噪声非常敏感。编码器速度信号中的任何毛刺都会被Kd放大可能导致输出剧烈跳动。因此Kd通常取值很小或者需要先对速度信号进行低通滤波后再参与微分计算。如果引入Kd后系统反而变得不稳定、抖动加剧首先考虑减小Kd其次考虑滤波。5.2 调试工具与技巧串口绘图这是最强大的调试工具。将目标速度、实际速度、PWM输出值通过串口发送到电脑使用诸如Serial PlotterArduino IDE自带、CoolTerm或专门的串口绘图软件如vofa进行可视化。看着曲线调参数比看数字直观一万倍。你可以清晰地看到超调量、调节时间、稳态误差和震荡。分段调试不要一开始就追求高速高精度。先从低速比如50 RPM开始调参数调稳了再逐步提高目标速度观察系统在不同工况下的表现。负载变化测试用手轻轻捏住电机轴增加负载观察系统速度是否下跌后又能恢复。一个好的PID系统应具备较强的抗负载扰动能力。5.3 常见问题与排查实录即使按照步骤来调试过程也绝不会一帆风顺。下面是我遇到的一些典型问题及解决方法问题现象可能原因排查思路与解决方案电机完全不动1. 电源未接通或电压不足。2. 电机驱动芯片使能端未激活。3. PWM引脚配置错误或未启动。4. 驱动芯片逻辑输入电平不匹配。1. 用万用表测量电机供电电压和驱动芯片电源。2. 检查驱动芯片的使能ENABLE或休眠SLEEP引脚确保其为有效电平。3. 用示波器或逻辑分析仪检查STM32的PWM输出引脚是否有波形频率和占空比是否正确。4. 确认STM32的3.3V输出是否能可靠驱动驱动芯片的输入引脚必要时加电平转换电路。电机抖动或啸叫1. PWM频率过低通常在音频范围内。2. 机械连接不牢固产生共振。3. 电源功率不足导致供电电压被拉低。1. 将PWM频率提高到10kHz以上超出人耳听觉范围。2. 检查电机与负载的联轴器、固定螺丝等。3. 检查电源适配器额定电流是否足够测量电机工作时的电源电压是否稳定。速度控制不稳定大幅震荡1. 比例系数Kp过大。2. 积分系数Ki过大。3. 速度反馈信号噪声大编码器信号干扰。4. PID计算周期过快或过慢。1. 减小Kp。2. 减小Ki或加入积分限幅、积分分离。3. 检查编码器接线使用双绞线或屏蔽线在A、B相输入引脚对地加10-100pF电容滤波。在软件中对读取的速度值进行滑动平均滤波。4. 调整速度采样和PID计算的周期通常50-100ms比较合适。存在稳态误差静差积分作用不足或未起作用。1. 适当增大Ki。2. 检查是否设置了积分限幅导致积分项无法累积。3. 确认PID计算中积分项确实被累加。响应太慢加速无力1. 比例系数Kp过小。2. 积分系数Ki过小。3. PWM最大输出限幅MaxOutput设置得太小。4. 电机或驱动芯片本身功率不足。1. 增大Kp。2. 增大Ki。3. 检查并适当提高MaxOutput值确保在需要大力输出时不被限制。4. 核实电机和驱动芯片的电流、电压规格。编码器计数不准速度跳动大1. 编码器供电不稳或受到干扰。2. 编码器线数设置错误。3. 定时器编码器模式配置错误如极性。4. 软件速度计算采样周期内脉冲数太少。1. 给编码器提供干净的电源并良好接地。2. 核对编码器铭牌上的线数PPR并确认代码中计算转速时是否乘以了4四倍频。3. 用示波器观察编码器A、B相信号并核对CubeMX中定时器通道的极性设置是否与信号边沿匹配。4. 降低速度计算频率增大采样时间或者换用更高线数的编码器。6. 进阶优化与扩展思考当基本的PID速度控制稳定运行后可以考虑一些优化和扩展让系统更上一层楼。6.1 速度滤波的重要性直接从编码器计算出的速度值往往带有高频噪声尤其是低速时由于采样时间内脉冲数少量化误差会被放大。将这样的原始速度值直接喂给PID特别是微分环节会引起控制输出的不必要的抖动。一个简单但有效的改进是加入软件低通滤波比如一阶滞后滤波也称指数加权平均滤波。float LowPass_Filter(float current_value, float last_filtered_value, float alpha) { // alpha 采样周期 / (滤波时间常数 采样周期) 取值0~1越小滤波越强滞后越大。 return alpha * current_value (1 - alpha) * last_filtered_value; } // 在获取速度后使用 raw_speed Get_Motor_Speed(); filtered_speed LowPass_Filter((float)raw_speed, last_filtered_speed, 0.3); last_filtered_speed filtered_speed; // 将 filtered_speed 作为 PID 的 Current 输入滤波参数alpha需要根据系统响应速度和噪声情况折中调整。滤波过强会引入相位滞后影响系统动态性能。6.2 从单级PID到串级PID如果对电机的控制要求更高比如不仅要求速度稳定还要求位置精确例如让电机旋转到特定角度那么单级PID可能不够用。这时可以考虑位置-速度串级PID。外环位置环输入是目标位置和编码器反馈的累计位置即积分速度得到的位置输出是目标速度。内环速度环就是我们上面一直在调的速度PID环。它的输入是外环给出的目标速度输出是PWM占空比。串级控制的优势在于内环速度环可以快速响应负载变化等扰动而外环位置环则保证最终定位的准确性。调试时需要“先内后外”先把内环速度环的参数调好确保速度能快速、平稳地跟随指令然后再调外环位置环的参数通常外环的Kp、Ki会比内环小很多。6.3 资源管理与实时性考量在while(1)主循环中调用速度读取和PID计算其周期受循环内其他任务影响并不精确。对于要求高的应用应该使用定时器中断来确保控制的周期性。可以配置一个基本定时器如TIM6/TIM7使其每10ms产生一次中断。在中断服务函数中执行“读取编码器-计算速度-运行PID-更新PWM”这一套流程。这样可以保证控制频率的稳定提高系统的一致性。注意中断函数中不要做复杂运算或调用耗时的HAL函数避免中断阻塞。整个项目做下来最深的一点体会是电机控制是理论和实践紧密结合的领域。PID参数没有绝对的最优解只有针对特定电机、特定负载、特定应用场景的相对优解。调试过程就是不断观察、分析、调整、再观察的循环。务必善用可视化工具串口绘图让数据说话。另外硬件是基础一个干净的电源、可靠的接线、适当的滤波往往比软件算法更能解决一些玄学问题。当你第一次看到电机在PID的控制下稳稳地保持在你设定的转速上即使用手去扰动它也能迅速恢复时那种成就感就是对所有折腾最好的回报。这个项目框架具有很强的扩展性在此基础上你可以增加电流环需要电流采样电路实现力矩控制或者结合更复杂的轨迹规划算法让电机运动得更智能。
返回列表