3. 编码器接口与速度测量:正交编码器原理、STM32配置与速度滤波
做电机控制,速度反馈是闭环的灵魂。没有准确的速度测量,PID调得再好也是白搭。这一章我们就来啃下编码器这块硬骨头——从原理到STM32配置,再到速度计算和滤波,一条龙讲清楚。
3.1 正交编码器到底是个啥?
正交编码器,说白了就是两个相位差90°的方波信号。一个叫A相,一个叫B相。电机转一圈,A和B各输出N个脉冲。通过判断A和B的先后顺序,就能知道电机是正转还是反转。
我刚开始做项目时,总觉得编码器就是个计数器,后来发现没那么简单。你想想看,如果只有一路脉冲,你只能知道转了多少,但方向呢?不知道。正交编码器就是靠两路信号的相位关系来判方向的。
核心概念:
- 分辨率:编码器每转一圈输出的脉冲数(PPR)。比如500线编码器,一圈500个脉冲。
- 倍频:利用A、B的上升沿和下降沿,可以把分辨率翻4倍。500线变2000线。
- Z相(零位):每圈一个脉冲,用于绝对位置校准。我一般只在需要回零时才用。
正交编码器的输出波形长这样:A相和B相相差90°电角度。正转时A领先B,反转时B领先A。就这么简单。
3.2 STM32编码器模式配置
STM32的定时器有个专门的编码器模式,用起来非常方便。你只需要把A相和B相接定时器的CH1和CH2,配置好模式,硬件自动帮你计数和判向。
我个人习惯用TIM2或TIM3来做编码器接口,因为这两个定时器在大部分STM32上都有,而且引脚位置比较顺手。
3.2.1 配置步骤
- 使能时钟:GPIO时钟和定时器时钟都要打开。
- 配置GPIO:A相和B相配置为复用推挽输出?不对,是浮空输入或上拉输入。这里容易搞错,我刚开始就栽过跟头。
- 配置定时器:设置编码器模式(TI1和TI2都计数),配置ARR(自动重装载值)。
- 使能定时器:调用TIM_Cmd()或HAL_TIM_Encoder_Start()。
我曾经踩过的坑:编码器模式下,定时器的预分频器(PSC)必须设为0。否则计数会丢步。还有,ARR要设得比编码器最大计数值大,不然会溢出。我一般设成0xFFFF,用16位模式。
3.2.2 代码示例(HAL库)
// 编码器初始化
void Encoder_Init(void)
{
TIM_Encoder_InitTypeDef sEncoderConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
// 使能时钟
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// GPIO配置:PA6(TIM3_CH1), PA7(TIM3_CH2)
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 定时器配置
htim3.Instance = TIM3;
htim3.Init.Prescaler = 0; // 必须为0
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 0xFFFF; // 16位最大
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);
// 启动编码器模式
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}
// 读取编码器计数值
int16_t Encoder_GetCount(void)
{
return (int16_t)__HAL_TIM_GET_COUNTER(&htim3);
}
小技巧:用int16_t读取计数值,这样正反转自动带符号。正转增加,反转减少。省去手动判向的麻烦。
3.3 速度计算:M法、T法、M/T法
有了编码器计数值,怎么算速度?常用的有三种方法。我根据项目需求选,没有银弹。
| 方法 | 原理 | 适用场景 | 优缺点 |
|---|---|---|---|
| M法(测频法) | 固定时间内计脉冲数 | 高速(>100 RPM) | 低速时误差大 |
| T法(测周法) | 测量两个脉冲的时间间隔 | 低速(<100 RPM) | 高速时精度下降 |
| M/T法 | 结合M法和T法 | 全速范围 | 实现稍复杂,精度最高 |
我做项目时,大部分情况用M法就够了。电机额定转速3000RPM,编码器500线,4倍频后每转2000个脉冲。采样周期1ms,算下来每个周期能捕获几十个脉冲,精度完全够用。
3.3.1 M法速度计算
// M法速度计算
// 输入:当前计数值,上次计数值,采样周期(ms),编码器线数,倍频数
float Speed_Calc_M(int16_t current_cnt, int16_t last_cnt,
uint32_t period_ms, uint16_t ppr, uint8_t multiplier)
{
int16_t delta = current_cnt - last_cnt; // 脉冲增量
float speed_rpm;
// 速度 = (脉冲数 / (每转脉冲数)) / 时间(分钟)
// 时间 = period_ms / 60000 (分钟)
speed_rpm = (float)delta * 60000.0f / (float)(period_ms * ppr * multiplier);
return speed_rpm;
}
注意:M法在低速时,如果采样周期内只捕获到1-2个脉冲,速度计算误差会很大。比如1ms内只抓到1个脉冲,算出来速度可能跳变50%。这时候要么加大采样周期,要么换T法。
3.4 速度滤波:让数据更平滑
算出来的速度直接给PID用?不行。编码器本身有机械抖动,加上电磁干扰,原始速度数据毛刺很多。我一般做两级滤波。
3.4.1 一阶低通滤波
最简单也最常用。说白了就是「这次的值 = 上次的值 × 系数 + 新测的值 × (1 - 系数)」。
// 一阶低通滤波
typedef struct {
float last_value;
float alpha; // 滤波系数 0.0~1.0,越小滤波越强
} LowPassFilter_t;
float LowPass_Update(LowPassFilter_t *filter, float input)
{
filter->last_value = filter->alpha * filter->last_value +
(1.0f - filter->alpha) * input;
return filter->last_value;
}
// 使用示例
LowPassFilter_t speed_filter = {0.0f, 0.8f}; // alpha=0.8,滤波较弱
float filtered_speed = LowPass_Update(&speed_filter, raw_speed);
我的经验:alpha值选0.7~0.9比较合适。太小了响应慢,太大了滤不掉毛刺。我一般先设0.8,看波形再调。如果电机机械振动大,就降到0.6。
3.4.2 滑动平均滤波
取最近N个速度值的平均。N越大越平滑,但延迟也越大。
// 滑动平均滤波
#define FILTER_SIZE 8
typedef struct {
float buffer[FILTER_SIZE];
uint8_t index;
float sum;
} MovingAverage_t;
float MovingAverage_Update(MovingAverage_t *ma, float input)
{
ma->sum -= ma->buffer[ma->index];
ma->buffer[ma->index] = input;
ma->sum += input;
ma->index = (ma->index + 1) % FILTER_SIZE;
return ma->sum / FILTER_SIZE;
}
滑动平均的优点是实现简单,缺点是有固定延迟。N=8时,延迟大约4个采样周期。如果你的控制周期是1ms,那延迟4ms,对大部分电机控制来说可以接受。
3.5 完整的速度测量流程
把上面这些串起来,一个完整的速度测量流程是这样的:
嗯,到这里编码器接口和速度测量就讲完了。实际项目中,我建议你先用示波器看编码器波形,确认A/B相没问题,再配置STM32。别一上来就写代码,硬件先调通,后面省心很多。