从零开始:用STM32定时器与PWM打造智能小车电机控制系统
从零开始:用STM32定时器与PWM打造智能小车电机控制系统
智能小车作为嵌入式领域的经典项目,其核心控制模块往往依赖于电机的精准调速与方向控制。对于初学者而言,如何利用STM32的定时器与PWM技术实现高效电机驱动,是迈向嵌入式开发实战的重要一步。本文将基于STM32F103C8T6(Cortex-M3内核)平台,通过实际项目案例,详细解析从硬件连接到软件配置的全流程,帮助读者构建完整的智能小车电机控制系统。
1. 硬件基础与电机驱动原理
在开始代码编写前,我们需要先了解智能小车电机控制的基本硬件架构。典型的直流电机驱动方案通常采用H桥电路,通过PWM信号控制电机的转速和方向。
核心硬件组件:
- STM32F103C8T6最小系统板(主控芯片)
- L298N或TB6612FNG电机驱动模块
- 直流减速电机(带编码器反馈可选)
- 电源管理系统(锂电池+稳压模块)
电机驱动的关键参数包括工作电压、电流容量和PWM频率。一般来说,小型直流电机的PWM频率建议设置在1-20kHz之间,以避免可闻噪声并保证响应速度。对于STM32F103系列,定时器时钟最高可达72MHz,为生成精准PWM提供了充足精度。
实际项目中,我曾遇到电机响应迟滞的问题,最终发现是PWM频率设置过高导致驱动芯片切换损耗过大。将频率从20kHz降至5kHz后,不仅解决了发热问题,还提高了扭矩响应速度。
2. STM32定时器系统深度解析
STM32的定时器系统是其外设中的精华部分,理解其工作原理是实现精准电机控制的基础。STM32F103C8T6包含4个通用定时器(TIM2-TIM5)、2个高级定时器(TIM1、TIM8)和1个基本定时器(TIM6)。
定时器关键功能对比:
| 定时器类型 | 分辨率 | PWM输出 | 编码器接口 | 适用场景 |
|---|---|---|---|---|
| 高级定时器 | 16位 | 6通道+互补输出 | 支持 | 电机控制、电源转换 |
| 通用定时器 | 16位 | 4通道 | 支持 | 通用PWM生成、输入捕获 |
| 基本定时器 | 16位 | 无 | 不支持 | 时基生成、简单定时 |
对于电机控制,我们通常选择通用定时器或高级定时器。以TIM3为例,这是一个16位通用定时器,具有4个PWM输出通道,完全满足双电机控制需求。
定时器时钟配置步骤:
- 使能TIM3时钟:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE) - 配置时基单元:设置预分频器(PSC)和自动重载值(ARR)
- 设置PWM模式:选择输出比较模式
- 使能定时器:
TIM_Cmd(TIM3, ENABLE)
// TIM3 PWM初始化示例
void TIM3_PWM_Init(uint16_t arr, uint16_t psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 时基单元配置
TIM_TimeBaseStructure.TIM_Period = arr; // 自动重载值
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频系数
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
// 通道1配置
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
}
3. PWM生成与电机调速实战
PWM(脉冲宽度调制)是电机调速的核心技术,通过调节占空比来控制平均电压,从而实现转速调节。STM32的定时器提供了灵活的PWM生成能力,支持边沿对齐和中心对齐模式。
PWM参数计算公式:
- PWM频率 = 定时器时钟 / [(PSC+1) * (ARR+1)]
- 占空比 = CCR / (ARR+1) * 100%
假设我们需要生成频率为10kHz,占空比可调的PWM信号:
- 定时器时钟:72MHz
- 设置PSC=71,ARR=99
- 频率 = 72,000,000 / (72*100) = 10,000Hz
- 占空比调节范围:0-100%(通过修改CCR值实现)
电机方向控制实现: 对于典型的H桥驱动,需要两个GPIO控制方向,一个PWM通道控制速度:
// 电机控制结构体定义
typedef struct {
GPIO_TypeDef* DIR1_Port;
uint16_t DIR1_Pin;
GPIO_TypeDef* DIR2_Port;
uint16_t DIR2_Pin;
TIM_TypeDef* TIMx;
uint16_t PWM_Channel;
} Motor_TypeDef;
// 电机控制函数
void Motor_SetSpeed(Motor_TypeDef* motor, int16_t speed)
{
// 限制速度范围
speed = (speed > 100) ? 100 : (speed < -100) ? -100 : speed;
// 设置方向
if(speed >= 0) {
GPIO_WriteBit(motor->DIR1_Port, motor->DIR1_Pin, Bit_SET);
GPIO_WriteBit(motor->DIR2_Port, motor->DIR2_Pin, Bit_RESET);
} else {
GPIO_WriteBit(motor->DIR1_Port, motor->DIR1_Pin, Bit_RESET);
GPIO_WriteBit(motor->DIR2_Port, motor->DIR2_Pin, Bit_SET);
speed = -speed;
}
// 设置PWM占空比
switch(motor->PWM_Channel) {
case 1:
TIM_SetCompare1(motor->TIMx, speed);
break;
case 2:
TIM_SetCompare2(motor->TIMx, speed);
break;
// 其他通道类似
}
}
在实际调试中,我发现电机在低速时存在死区问题,即占空比低于某个阈值时电机无法启动。通过添加最小启动占空比补偿,有效解决了这个问题:
// 添加死区补偿
#define MOTOR_DEAD_ZONE 15 // 实测死区占空比
void Motor_SetSpeed_WithCompensation(Motor_TypeDef* motor, int16_t speed)
{
if(abs(speed) < MOTOR_DEAD_ZONE && speed != 0) {
speed = (speed > 0) ? MOTOR_DEAD_ZONE : -MOTOR_DEAD_ZONE;
}
Motor_SetSpeed(motor, speed);
}
4. 编码器反馈与闭环控制
为实现精准的电机控制,集成编码器反馈是必不可少的一步。STM32的定时器编码器接口可以直接读取正交编码器信号,无需外部中断处理,大大简化了软件设计。
编码器接口配置要点:
- 使用TIMx的CH1和CH2通道接收编码器A、B相信号
- 设置编码器模式为TI1和TI2边沿计数
- 配置滤波器减少噪声影响
void Encoder_Init(TIM_TypeDef* TIMx)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 时基配置
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_Period = 65535; // 16位最大值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIMx, &TIM_TimeBaseStructure);
// 编码器接口配置
TIM_EncoderInterfaceConfig(TIMx, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising,
TIM_ICPolarity_Rising);
// 输入捕获配置
TIM_ICInitStructure.TIM_ICFilter = 6; // 适当滤波
TIM_ICInit(TIMx, &TIM_ICInitStructure);
TIM_Cmd(TIMx, ENABLE);
}
速度计算算法: 通过定期读取编码器计数值变化,可以计算电机实际转速:
int32_t Get_Motor_Speed(TIM_TypeDef* TIMx, uint32_t sample_time_ms)
{
static int16_t last_count = 0;
int16_t current_count = TIM_GetCounter(TIMx);
int16_t delta = current_count - last_count;
last_count = current_count;
// 考虑计数器溢出情况
if(delta > 32767) delta -= 65536;
else if(delta < -32768) delta += 65536;
// 转换为转速值(RPM)
// 假设编码器线数为13,减速比为30:1
const float counts_per_rev = 13 * 30 * 4; // 4倍频
float rpm = (delta * 60000.0) / (counts_per_rev * sample_time_ms);
return (int32_t)rpm;
}
PID控制器实现: 结合编码器反馈,我们可以实现完整的PID闭环控制:
typedef struct {
float Kp, Ki, Kd;
float integral;
float prev_error;
uint32_t last_time;
} PID_Controller;
float PID_Update(PID_Controller* pid, float setpoint, float measured)
{
float error = setpoint - measured;
uint32_t now = HAL_GetTick();
float dt = (now - pid->last_time) / 1000.0f;
pid->last_time = now;
// 比例项
float P = pid->Kp * error;
// 积分项(抗积分饱和)
pid->integral += error * dt;
if(pid->integral > 1000) pid->integral = 1000;
else if(pid->integral < -1000) pid->integral = -1000;
float I = pid->Ki * pid->integral;
// 微分项
float D = pid->Kd * (error - pid->prev_error) / dt;
pid->prev_error = error;
return P + I + D;
}
在实际调试PID参数时,我建议采用如下步骤:
- 先将Ki和Kd设为0,逐渐增大Kp直到系统出现轻微振荡
- 将Kp设为当前值的50%,然后逐渐增加Ki直到静差消除
- 最后加入Kd抑制超调,但不宜过大以免引入噪声
5. 系统集成与实战调试技巧
将各个模块整合成完整的电机控制系统需要考虑多个实际因素,包括电源管理、噪声抑制和实时性保证。
电源系统设计注意事项:
- 电机电源与MCU电源完全隔离,使用不同的稳压模块
- 在电机电源输入端添加大容量电解电容(100-1000μF)缓冲电流突变
- 信号线路上使用磁珠或小电感滤除高频噪声
软件架构优化: 建议采用模块化设计,将电机控制相关功能封装成独立模块:
// 电机控制模块接口
typedef struct {
Motor_TypeDef hardware;
PID_Controller speed_pid;
int32_t target_speed;
int32_t current_speed;
uint32_t last_update;
} Motor_Control_Module;
void Motor_Module_Init(Motor_Control_Module* module,
GPIO_TypeDef* dir1_port, uint16_t dir1_pin,
GPIO_TypeDef* dir2_port, uint16_t dir2_pin,
TIM_TypeDef* pwm_tim, uint16_t pwm_ch,
TIM_TypeDef* enc_tim)
{
// 硬件初始化
module->hardware.DIR1_Port = dir1_port;
module->hardware.DIR1_Pin = dir1_pin;
// ... 其他硬件初始化
// PID参数初始化
module->speed_pid.Kp = 0.5f;
module->speed_pid.Ki = 0.1f;
module->speed_pid.Kd = 0.01f;
module->speed_pid.integral = 0;
module->speed_pid.prev_error = 0;
}
void Motor_Module_Update(Motor_Control_Module* module)
{
// 每10ms更新一次
if(HAL_GetTick() - module->last_update < 10) return;
// 读取实际速度
module->current_speed = Get_Motor_Speed(module->hardware.enc_tim, 10);
// PID计算
float control = PID_Update(&module->speed_pid,
module->target_speed,
module->current_speed);
// 设置电机速度
Motor_SetSpeed_WithCompensation(&module->hardware, (int16_t)control);
module->last_update = HAL_GetTick();
}
常见调试问题与解决方案:
-
电机抖动或噪声过大
- 检查PWM频率是否合适(建议5-10kHz)
- 增加编码器滤波器设置
- 检查电源电压是否稳定
-
响应速度慢或有明显超调
- 调整PID参数,增加Kp或Kd
- 检查编码器采样周期是否过长
- 确认电机机械负载是否过重
-
低速控制精度差
- 启用死区补偿
- 提高PWM分辨率(增加ARR值)
- 考虑使用FOC(磁场定向控制)算法
在实际项目中,我还发现温度对电机性能影响显著。特别是在长时间运行后,电机电阻变化会导致特性改变。为此,可以添加温度补偿算法,或者定期自动校准PID参数。
通过串口调试工具实时监控电机状态也是提高调试效率的有效方法:
void Motor_Debug_Print(Motor_Control_Module* module)
{
printf("Target: %d, Actual: %d, PWM: %d\r\n",
module->target_speed,
module->current_speed,
TIM_GetCompare1(module->hardware.TIMx));
}
最后提醒一点,在进行任何参数调整时,务必记录每次修改的效果。我习惯使用如下格式的记录表格:
| 调试时间 | Kp | Ki | Kd | 超调量 | 调节时间 | 静差 | 备注 |
|---|---|---|---|---|---|---|---|
| 09:30 | 0.5 | 0.0 | 0.0 | 45% | 2.1s | 12RPM | 首次调试 |
| 09:45 | 0.3 | 0.05 | 0.01 | 15% | 1.5s | 3RPM | 增加积分 |
这种系统化的调试方法能够显著提高开发效率,避免盲目调整参数。
更多推荐
所有评论(0)