别再瞎调PID了!手把手教你用STM32 HAL库搞定电机速度闭环(附完整代码)
STM32 HAL库实战:从PID理论到电机速度闭环的完整实现
第一次接触PID控制时,很多人都会被各种公式和参数搞得晕头转向。纸上谈兵容易,但当你真正要在STM32上实现电机速度闭环时,却发现理论算法和硬件配置之间存在着巨大的鸿沟。本文将带你跨越这道鸿沟,从零开始构建一个完整的电机速度控制系统。
1. PID控制的核心:从抽象公式到具体PWM
PID控制的核心思想很简单:通过比例(P)、积分(I)、微分(D)三个环节的组合,不断调整输出使系统达到期望状态。但在嵌入式系统中,我们需要解决几个关键问题:
- 物理量转换:如何将PID计算出的抽象数值转换为具体的PWM占空比或定时器计数值?
- 采样周期:如何确定合适的采样频率?
- 参数整定:如何设置Kp、Ki、Kd这三个神秘系数?
以一个典型的直流电机速度控制为例,假设我们使用STM32的TIM1产生PWM,TIM2作为编码器接口读取电机转速。系统的工作流程如下:
// 伪代码示例:PID控制循环 while(1) { current_speed = read_encoder(); // 读取编码器获取当前速度 error = target_speed - current_speed; // 计算误差 pid_output = pid_calculate(error); // PID计算 set_pwm_duty(pid_output); // 调整PWM输出 delay(control_period); // 等待下一个控制周期 }1.1 PWM与速度的比例关系
在电机控制中,PWM占空比与电机转速通常呈近似线性关系。我们需要确定两个关键参数:
- PWM范围:STM32定时器的ARR值决定了PWM分辨率
- 速度范围:电机在最大占空比下的转速
假设测试得到:
- 100%占空比(ARR=1000,CCR=1000)对应电机转速为1000 RPM
- 0%占空比对应0 RPM
那么比例系数为:
速度(RPM) = PWM占空比 × 1.0 (RPM/%)在实际项目中,这个关系需要通过实验校准。一个简单的校准方法:
- 设置PWM为50%占空比,记录稳定后的转速
- 重复测试多个占空比点
- 绘制占空比-转速曲线,计算比例系数
1.2 PID输出与PWM的映射
PID计算出的输出值需要映射到PWM的占空比。常见的两种方式:
| 映射方式 | 优点 | 缺点 |
|---|---|---|
| 绝对位置式 | 直接输出目标占空比 | 需要精确校准 |
| 增量式 | 输出占空比变化量 | 抗干扰能力强 |
在HAL库中,设置PWM占空比的典型代码:
// 设置TIM1通道1的PWM占空比 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty_cycle);2. 硬件配置:STM32的外设设置要点
正确的硬件配置是PID控制的基础。我们需要配置三个关键外设:
2.1 PWM生成配置
以TIM1为例,配置步骤:
- 选择时钟源和分频系数,确定定时器频率
- 设置ARR(自动重装载值)决定PWM周期
- 配置PWM模式(通常为模式1或模式2)
- 设置CCR(捕获/比较寄存器)初始值
- 启用PWM输出通道
// PWM初始化示例 TIM_HandleTypeDef htim1; void PWM_Init(void) { htim1.Instance = TIM1; htim1.Init.Prescaler = 71; // 72MHz/(71+1)=1MHz htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 999; // PWM频率=1MHz/1000=1kHz htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim1); TIM_OC_InitTypeDef sConfigOC; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; // 初始占空比0% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); }2.2 编码器接口配置
编码器用于测量电机实际转速。STM32的定时器支持正交编码器模式:
// 编码器接口配置示例 TIM_HandleTypeDef htim2; void Encoder_Init(void) { htim2.Instance = TIM2; htim2.Init.Prescaler = 0; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 0xFFFF; // 16位最大值 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Encoder_Init(&htim2); TIM_Encoder_InitTypeDef sConfig; sConfig.EncoderMode = TIM_ENCODERMODE_TI12; sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler = TIM_ICPSC_DIV1; sConfig.IC1Filter = 0; sConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler = TIM_ICPSC_DIV1; sConfig.IC2Filter = 0; HAL_TIM_Encoder_ConfigChannel(&htim2, &sConfig, TIM_CHANNEL_ALL); HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); }2.3 定时器中断配置
PID控制需要精确的时间间隔,通常使用定时器中断:
// 定时器中断配置示例 TIM_HandleTypeDef htim3; void Timer_Init(uint16_t period_ms) { htim3.Instance = TIM3; htim3.Init.Prescaler = 7199; // 72MHz/(7199+1)=10kHz htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = period_ms * 10 - 1; // 10kHz下,100=10ms HAL_TIM_Base_Init(&htim3); HAL_TIM_Base_Start_IT(&htim3); } // 中断回调函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM3) { PID_Control(); // 执行PID控制 } }3. PID算法的实现与优化
3.1 位置式PID实现
位置式PID直接计算输出值:
typedef struct { float Kp, Ki, Kd; // PID系数 float integral; // 积分项 float prev_error; // 上一次误差 float output; // 输出值 float out_max; // 输出上限 float out_min; // 输出下限 } PID_Controller; float PID_Compute(PID_Controller *pid, float setpoint, float input) { float error = setpoint - input; pid->integral += error; // 积分限幅 if(pid->integral > pid->out_max) pid->integral = pid->out_max; else if(pid->integral < pid->out_min) pid->integral = pid->out_min; float derivative = error - pid->prev_error; pid->output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅 if(pid->output > pid->out_max) pid->output = pid->out_max; else if(pid->output < pid->out_min) pid->output = pid->out_min; pid->prev_error = error; return pid->output; }3.2 增量式PID实现
增量式PID计算输出变化量:
typedef struct { float Kp, Ki, Kd; float prev_error; float prev_prev_error; } IncPID_Controller; float IncPID_Compute(IncPID_Controller *pid, float setpoint, float input) { float error = setpoint - input; float delta = pid->Kp * (error - pid->prev_error) + pid->Ki * error + pid->Kd * (error - 2*pid->prev_error + pid->prev_prev_error); pid->prev_prev_error = pid->prev_error; pid->prev_error = error; return delta; }3.3 抗积分饱和处理
积分饱和是PID控制中的常见问题,解决方法:
- 积分分离:误差较大时关闭积分项
- 积分限幅:限制积分项的最大值
- 反向抑制:当输出饱和时停止积分
// 带积分分离的PID实现 float PID_Compute_AntiWindup(PID_Controller *pid, float setpoint, float input) { float error = setpoint - input; // 积分分离:误差较大时不积分 if(fabs(error) > 50) { pid->integral = 0; } else { pid->integral += error; } // ...其余计算与普通PID相同 }4. 参数整定与系统调试
4.1 手动整定PID参数
经典的Ziegler-Nichols整定方法:
- 先将Ki和Kd设为0,逐渐增大Kp直到系统开始振荡
- 记录此时的临界增益Ku和振荡周期Tu
- 根据下表设置PID参数:
| 控制类型 | Kp | Ki | Kd |
|---|---|---|---|
| P | 0.5Ku | 0 | 0 |
| PI | 0.45Ku | 0.54Ku/Tu | 0 |
| PID | 0.6Ku | 1.2Ku/Tu | 0.075KuTu |
4.2 调试技巧
- 先调P:增大Kp直到系统响应迅速但不过度振荡
- 再调I:加入Ki消除稳态误差,但不宜过大
- 最后调D:加入Kd抑制超调和振荡
- 观察指标:
- 上升时间:系统达到目标值的时间
- 超调量:最大超出目标值的百分比
- 稳定时间:系统稳定在目标值附近的时间
4.3 常见问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 系统完全不响应 | PWM配置错误 | 检查定时器和GPIO配置 |
| 电机转速不稳定 | 采样周期不合适 | 调整控制频率 |
| 持续振荡 | Kp过大或Kd过小 | 减小Kp或增大Kd |
| 稳态误差大 | Ki不足 | 适当增大Ki |
| 响应迟缓 | Kp过小 | 增大Kp |
5. 完整代码实现
下面是一个基于STM32 HAL库的完整PID电机控制实现:
// pid_motor_control.h typedef struct { float Kp, Ki, Kd; float integral; float prev_error; float output; float out_max; float out_min; } PID_Controller; void PID_Init(PID_Controller *pid, float Kp, float Ki, float Kd, float out_max, float out_min); float PID_Compute(PID_Controller *pid, float setpoint, float input); void Motor_Control_Init(void); void Motor_Set_Speed(float speed); float Motor_Get_Speed(void);// pid_motor_control.c #include "pid_motor_control.h" #include "tim.h" PID_Controller speed_pid; void PID_Init(PID_Controller *pid, float Kp, float Ki, float Kd, float out_max, float out_min) { pid->Kp = Kp; pid->Ki = Ki; pid->Kd = Kd; pid->integral = 0; pid->prev_error = 0; pid->output = 0; pid->out_max = out_max; pid->out_min = out_min; } float PID_Compute(PID_Controller *pid, float setpoint, float input) { float error = setpoint - input; pid->integral += error; // 积分限幅 if(pid->integral > pid->out_max) pid->integral = pid->out_max; else if(pid->integral < pid->out_min) pid->integral = pid->out_min; float derivative = error - pid->prev_error; pid->output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅 if(pid->output > pid->out_max) pid->output = pid->out_max; else if(pid->output < pid->out_min) pid->output = pid->out_min; pid->prev_error = error; return pid->output; } void Motor_Control_Init(void) { // 初始化PID控制器 PID_Init(&speed_pid, 1.0, 0.01, 0.1, 1000, 0); // 初始化PWM和编码器 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); HAL_TIM_Base_Start_IT(&htim3); // 10ms定时中断 } void Motor_Set_Speed(float speed) { // 将PID输出转换为PWM占空比 uint16_t duty = (uint16_t)speed_pid.output; __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty); } float Motor_Get_Speed(void) { // 读取编码器值并转换为转速 static int32_t last_count = 0; int32_t current_count = (int32_t)TIM2->CNT; TIM2->CNT = 0; // 重置计数器 // 编码器每转脉冲数 × 控制周期(秒) × 60(秒→分钟) float rpm = (current_count - last_count) / (500.0 * 0.01) * 60; last_count = current_count; return rpm; } // 在定时器中断中调用 void PID_Control_Loop(float target_rpm) { float current_rpm = Motor_Get_Speed(); PID_Compute(&speed_pid, target_rpm, current_rpm); Motor_Set_Speed(speed_pid.output); }6. 进阶优化技巧
6.1 自适应PID控制
根据系统状态动态调整PID参数:
void Adaptive_PID_Tuning(PID_Controller *pid, float error) { // 根据误差大小调整参数 if(fabs(error) > 100) { // 大误差时增强P,减弱I pid->Kp = 2.0; pid->Ki = 0.0; } else if(fabs(error) > 10) { // 中等误差时平衡P和I pid->Kp = 1.0; pid->Ki = 0.01; } else { // 小误差时增强I消除静差 pid->Kp = 0.5; pid->Ki = 0.05; } }6.2 前馈控制
结合前馈可以提高响应速度:
float Feedforward_Control(float target_rpm) { // 前馈控制:根据目标速度直接计算初始PWM return target_rpm * 0.8; // 需要根据系统特性校准 } void Enhanced_PID_Control(float target_rpm) { float feedforward = Feedforward_Control(target_rpm); float pid_output = PID_Compute(&speed_pid, target_rpm, Motor_Get_Speed()); Motor_Set_Speed(feedforward + pid_output); }6.3 滤波器设计
对编码器信号进行滤波可以减少噪声影响:
#define FILTER_WEIGHT 0.2 float LowPass_Filter(float new_value, float old_value) { return old_value * (1 - FILTER_WEIGHT) + new_value * FILTER_WEIGHT; } float Get_Filtered_Speed(void) { static float filtered_rpm = 0; float raw_rpm = Motor_Get_Speed(); filtered_rpm = LowPass_Filter(raw_rpm, filtered_rpm); return filtered_rpm; }在实际项目中,PID控制器的性能很大程度上取决于对系统的理解和调试经验。建议从简单的P控制开始,逐步加入I和D项,每次只调整一个参数,并记录系统响应变化。
