4. 编码器原理与接口:AB相编码器工作原理、STM32定时器编码器模式配置、位置与速度读取
做电机控制,你第一个要面对的问题就是:我怎么知道电机转了多少?
我刚开始搞这个的时候,以为随便接两根线读个脉冲就行。结果电机一抖,数据乱跳,位置根本对不上。后来才明白,编码器不是简单的「数脉冲」,它有自己的脾气。
这一章,我们就把它彻底搞明白。
4.1 AB相编码器工作原理
AB相编码器,也叫增量式编码器。它输出两路方波信号,相位差90°。我习惯叫它们A相和B相。
核心概念:通过A、B两相的相位关系,就能判断电机的旋转方向。
- A相领先B相90° → 正转
- B相领先A相90° → 反转
你想想看,如果只有一路脉冲,你只能知道转了多少,但不知道往哪转。有了AB相,方向和位置就都有了。
另外,很多编码器还有一个Z相(零位信号),每转一圈输出一个脉冲。我一般在做位置归零时用它,精度很高。
4.1.1 倍频技术
STM32的定时器可以检测A、B相的上升沿和下降沿。这样一来,一个脉冲周期内能捕获4个边沿变化。
举个例子:
- 编码器物理分辨率:1000线/圈
- 4倍频后:4000个计数/圈
- 角度分辨率:360° / 4000 = 0.09°
嗯,这个精度对于大多数电机控制场景已经够用了。
我的经验:实际项目中,编码器线数不要盲目选高。线数越高,信号频率越高,对硬件抗干扰要求也越高。1000~2500线是工业上最常用的范围。
4.2 STM32定时器编码器模式配置
STM32F407的定时器有一个专门的编码器模式。说白了,就是硬件帮你把AB相的边沿检测、方向判断、计数增减全做了。你只需要配置好,然后读寄存器就行。
我一般用TIM2或TIM3来做编码器接口,因为它们支持编码器模式3(最常用的模式)。
4.2.1 配置步骤
- 使能时钟:RCC_APB1PeriphClockCmd() 或 RCC_APB2PeriphClockCmd()
- 配置GPIO:将A、B相引脚设置为复用功能(AF1或AF2,具体看定时器)
- 配置定时器:设置SlaveMode为编码器模式3,配置计数方向、滤波器等
- 配置中断(可选):如果需要溢出中断或捕获中断
下面是我常用的配置代码,你可以直接拿去用:
// 编码器模式配置示例(TIM3,PA6=CH1,PA7=CH2)
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
// 2. 配置GPIO
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM3);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_TIM3);
// 3. 配置定时器时基
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 最大计数值
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// 4. 配置编码器模式(模式3:双沿检测)
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0F; // 滤波,抗干扰
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
// 5. 使能定时器
TIM_Cmd(TIM3, ENABLE);
}
注意:编码器模式配置完成后,定时器的计数器会自动根据AB相脉冲增减。你不需要手动干预。但要注意,计数器是16位的,最大65535。如果电机转得快,很容易溢出。我一般配合溢出中断来处理。
4.3 位置与速度读取
配置好之后,读取位置和速度就很简单了。但简单归简单,坑也不少。
4.3.1 位置读取
位置就是当前计数器的值。直接读TIMx->CNT就行。
// 读取当前位置(单位:脉冲数)
int32_t Get_Encoder_Position(void)
{
return (int32_t)TIM3->CNT;
}
// 转换为角度(假设4倍频,1000线编码器)
float Get_Encoder_Angle(void)
{
int32_t pulse = Get_Encoder_Position();
return (float)pulse * 360.0f / 4000.0f;
}
这里有个细节:计数器是有符号的。正转时增加,反转时减少。但如果你配置的Period是0xFFFF,反转时计数器会从0跳到0xFFFF。所以读取时最好用int32_t做强制转换,避免符号问题。
4.3.2 速度读取
速度读取有两种常用方法:
| 方法 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| M法(测频法) | 固定时间内统计脉冲数 | 中高速 | 低速时精度差 |
| T法(测周法) | 测量两个脉冲之间的时间 | 低速 | 高速时精度差 |
| M/T法 | 结合M法和T法 | 宽速域 | 实现稍复杂 |
我个人的习惯是:低速用T法,中高速用M法。如果项目要求全速域都准,那就上M/T法。
下面是一个M法的实现:
// M法测速(每10ms调用一次)
float Get_Encoder_Speed_Method(void)
{
static int32_t last_pulse = 0;
int32_t current_pulse = Get_Encoder_Position();
int32_t delta = current_pulse - last_pulse;
last_pulse = current_pulse;
// 处理溢出
if(delta > 30000) delta -= 65536;
else if(delta < -30000) delta += 65536;
// 速度 = 脉冲差 / 时间间隔(单位:脉冲/秒)
float speed_pulse = (float)delta / 0.01f;
// 转换为RPM(假设1000线,4倍频)
float speed_rpm = speed_pulse / 4000.0f * 60.0f;
return speed_rpm;
}
避坑指南:我曾经在读取速度时忘记处理计数器溢出,结果电机正转时速度突然变成负的。排查了半天才发现是溢出问题。所以,溢出处理一定要做,哪怕你觉得电机转得没那么快。
4.4 知识结构总览
下面这张图把编码器相关的核心逻辑串起来了,你可以对照着看:
4.5 常见问题与避坑
- 编码器线接反了怎么办?——A、B相交换一下就行。或者软件里把计数方向取反。
- 电机抖动时计数乱跳?——加硬件滤波(RC低通)或软件滤波(定时器自带的ICFilter)。
- 上电后位置不对?——编码器是增量的,上电时位置是随机的。需要做一次归零操作。
- 速度值波动大?——采样时间太短了。我一般用10ms~50ms的采样窗口。
重要提醒:编码器的地线一定要和STM32共地。我见过有人没共地,结果信号全是噪声,位置数据完全没法用。另外,编码器线缆尽量用屏蔽线,屏蔽层单端接地。