1. STM32F103定时器编码器模式深度解析
在嵌入式系统开发中,电机控制是一个常见且重要的应用场景。STM32F103系列单片机内置的定时器编码器模式为电机转速测量提供了硬件级的解决方案,相比软件处理方式具有显著优势。本文将详细介绍增量式编码器的工作原理、STM32定时器编码器模式的配置方法,以及实际项目中的代码实现和优化技巧。
1.1 增量式编码器工作原理详解
增量式旋转编码器是电机转速检测的核心部件,其工作原理基于光电或磁电感应。以常见的AB相编码器为例:
-
机械结构:编码器通常由码盘、发光二极管和光敏接收器组成。码盘上刻有均匀分布的透光缝隙,当电机转动时,码盘随之旋转,光线周期性通过缝隙,在接收端产生脉冲信号。
-
信号特征:
- A相和B相信号在静止状态下保持稳定电平(高或低)
- 顺时针旋转时,B相信号相位领先A相90°
- 逆时针旋转时,A相信号相位领先B相90°
典型的信号相位关系如下表所示:
| 旋转方向 | A相边沿 | B相电平 | 计数方向 |
|---|---|---|---|
| 顺时针 | 上升沿 | 高 | +1 |
| 顺时针 | 下降沿 | 低 | +1 |
| 逆时针 | 上升沿 | 低 | -1 |
| 逆时针 | 下降沿 | 高 | -1 |
注意:不同厂商的编码器相位关系可能相反,使用时务必查阅具体型号的数据手册确认信号特性。
1.2 硬件接口设计要点
在STM32F103系统中,编码器接口应连接到定时器的特定通道:
- 引脚分配:
- TIM3_CH1对应PB4
- TIM3_CH2对应PB5
- 硬件连接:
- 推荐使用带屏蔽的双绞线连接编码器
- 信号线上应加10kΩ上拉电阻
- 在靠近MCU引脚处放置0.1μF去耦电容
对于长距离传输或工业环境,建议增加以下保护措施:
- 使用光耦隔离编码器信号
- 添加TVS二极管防止浪涌
- 采用差分信号传输(如RS422)
2. CubeMX配置详解
2.1 定时器基础参数配置
使用STM32CubeMX配置编码器模式时,关键参数设置如下:
-
时钟配置:
- 确保APB1定时器时钟正确使能
- 典型时钟频率为72MHz(根据系统配置可能不同)
-
定时器参数:
- Prescaler(预分频器):0
- Counter Mode(计数模式):Up
- Counter Period(自动重装载值):65535(16位最大值)
- Auto-reload preload:Disable
-
编码器模式选择:
- Encoder Mode:选择"Encoder Mode TI1 and TI2"
- IC1/IC2 Polarity:保持默认的Rising Edge
2.2 高级功能配置
为获得更稳定的测量结果,可以调整以下高级参数:
-
输入滤波器:
- 设置适当的输入滤波值(如0x4)可消除信号抖动
- 计算公式:滤波时间 = N * tCK_INT,其中N为滤波值,tCK_INT为定时器时钟周期
-
捕获/比较通道配置:
- Pulse:保持默认0
- Input Filter:建议设置为4-8个时钟周期
- Polarity:Rising Edge
配置完成后生成代码时,注意勾选"Generate peripheral initialization as a pair of '.c/.h' files per peripheral",这样定时器配置会生成独立的文件,便于维护。
3. 编码器模式软件实现
3.1 初始化流程
完整的编码器初始化应包含以下步骤:
c复制void Encoder_Init(void)
{
// 1. 启动定时器编码器接口
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
// 2. 清零计数器初始值
__HAL_TIM_SET_COUNTER(&htim3, 0);
// 3. 初始化全局变量
Encoder_Count = 0;
Total_Encoder_Count = 0L;
// 4. 配置NVIC(可选)
HAL_NVIC_SetPriority(TIM3_IRQn, 0, 1);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
3.2 数据采集与处理
高效的编码器数据处理应考虑以下方面:
-
采样周期选择:
- 根据应用需求确定采样频率
- 典型值为10-100ms
- 过短会导致计算负担增加,过长会降低响应速度
-
速度计算算法:
c复制float Calculate_Speed(int16_t pulse_count, uint32_t sample_period_ms)
{
// 每转脉冲数(根据编码器规格)
const uint16_t PULSE_PER_REV = 11;
// 转换为转速(RPM)
float rpm = (pulse_count * 60.0f * 1000.0f) / (PULSE_PER_REV * sample_period_ms);
return rpm;
}
- 方向判断逻辑:
c复制MotorDirection Get_Motor_Direction(int16_t pulse_count)
{
if(pulse_count > 0) {
return DIR_CW;
} else if(pulse_count < 0) {
return DIR_CCW;
} else {
return DIR_STOP;
}
}
3.3 抗溢出处理策略
对于长时间运行的计数器,需要考虑溢出问题:
- 32位扩展计数:
c复制// 在定时器溢出中断中处理
void TIM3_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE) != RESET)
{
if(__HAL_TIM_GET_IT_SOURCE(&htim3, TIM_IT_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(&htim3, TIM_IT_UPDATE);
// 根据计数方向调整32位计数器
if(htim3.Instance->CR1 & TIM_CR1_DIR) {
Total_Encoder_Count += 65536L;
} else {
Total_Encoder_Count -= 65536L;
}
}
}
}
- 速度计算补偿:
c复制int32_t Get_Total_Pulse_Count(void)
{
int32_t total = Total_Encoder_Count;
total += (int32_t)__HAL_TIM_GET_COUNTER(&htim3);
return total;
}
4. 实际应用中的问题与解决方案
4.1 常见问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 计数方向相反 | AB相接线错误 | 交换A相和B相信号线 |
| 计数不准确 | 信号抖动 | 增加输入滤波值 |
| 高速时丢步 | 采样频率过低 | 缩短采样周期或使用DMA |
| 计数器频繁溢出 | 自动重装载值设置过小 | 增大ARR值或启用溢出中断 |
| 转速计算波动大 | 机械振动影响 | 增加软件滤波算法 |
4.2 性能优化技巧
-
DMA数据传输:
- 配置DMA将CNT寄存器值定期传输到内存
- 减少CPU干预,提高系统响应能力
-
滑动平均滤波:
c复制#define FILTER_WINDOW_SIZE 5
int16_t speed_filter_buffer[FILTER_WINDOW_SIZE];
uint8_t filter_index = 0;
int16_t Apply_Speed_Filter(int16_t new_speed)
{
speed_filter_buffer[filter_index] = new_speed;
filter_index = (filter_index + 1) % FILTER_WINDOW_SIZE;
int32_t sum = 0;
for(int i=0; i<FILTER_WINDOW_SIZE; i++) {
sum += speed_filter_buffer[i];
}
return (int16_t)(sum / FILTER_WINDOW_SIZE);
}
- 动态采样调整:
- 根据转速自动调整采样周期
- 低速时延长采样时间提高精度
- 高速时缩短采样时间提高响应
5. 进阶应用:位置速度双闭环控制
将编码器反馈应用于电机控制系统时,典型实现方案如下:
- 位置环设计:
c复制typedef struct {
int32_t target_position;
int32_t current_position;
float kp;
float ki;
float kd;
int32_t error_integral;
int32_t last_error;
} PositionController;
int32_t Position_Control_Update(PositionController* ctrl, int32_t current_pos)
{
ctrl->current_position = current_pos;
int32_t error = ctrl->target_position - current_pos;
// P项
int32_t p_term = (int32_t)(ctrl->kp * error);
// I项(带抗饱和)
ctrl->error_integral += error;
if(ctrl->error_integral > 1000) ctrl->error_integral = 1000;
else if(ctrl->error_integral < -1000) ctrl->error_integral = -1000;
int32_t i_term = (int32_t)(ctrl->ki * ctrl->error_integral);
// D项
int32_t d_term = (int32_t)(ctrl->kd * (error - ctrl->last_error));
ctrl->last_error = error;
return p_term + i_term + d_term;
}
- 速度环设计:
c复制typedef struct {
int32_t target_speed;
float kp;
float ki;
float kd;
float error_integral;
float last_error;
} SpeedController;
float Speed_Control_Update(SpeedController* ctrl, float current_speed)
{
float error = ctrl->target_speed - current_speed;
// P项
float p_term = ctrl->kp * error;
// I项
ctrl->error_integral += error;
if(ctrl->error_integral > 100.0f) ctrl->error_integral = 100.0f;
else if(ctrl->error_integral < -100.0f) ctrl->error_integral = -100.0f;
float i_term = ctrl->ki * ctrl->error_integral;
// D项
float d_term = ctrl->kd * (error - ctrl->last_error);
ctrl->last_error = error;
return p_term + i_term + d_term;
}
- 系统集成:
c复制void Motor_Control_Loop(void)
{
// 1. 获取当前位置
int32_t current_pos = Get_Total_Pulse_Count();
// 2. 位置环计算
int32_t speed_target = Position_Control_Update(&pos_ctrl, current_pos);
// 3. 获取当前速度
float current_speed = Calculate_Speed(Encoder_Count, SAMPLE_PERIOD);
// 4. 速度环计算
float pwm_duty = Speed_Control_Update(&speed_ctrl, current_speed);
// 5. 应用PWM输出
Set_Motor_PWM(pwm_duty);
}
在实际调试过程中,我发现先调好速度环再调位置环的效果更好。建议先用阶跃响应测试速度环的响应特性,确保速度跟踪无超调、无振荡后,再加入位置环控制。对于大多数小车应用,位置环的积分项可以设置得较小,主要依靠比例控制来避免过大的超调。