3、增量式编码器原理与数据读取:ABZ编码器输出信号解析、定时器编码器模式配置(STM32)、读取位置与速度、方向判断
好,咱们今天聊聊增量式编码器。这东西在电机控制里太常见了,几乎每个做运动控制的工程师都得跟它打交道。我个人习惯把它叫做「电机的眼睛」——没有它,你根本不知道电机转到了哪里、转得有多快。
3.1 ABZ编码器输出信号解析
先说说增量式编码器到底输出什么信号。说白了,它就是个光栅盘加几个光耦,转起来之后输出几路脉冲。
A相和B相:这两路信号相位差90度。你想想看,如果电机正转,A相领先B相90度;反转的话,B相领先A相90度。这就是判断方向的依据。
Z相:也叫零位信号。编码器每转一圈,Z相输出一个脉冲。这个信号特别有用——可以用来做位置校准。
我在项目中遇到过一个问题:有个设备每次上电后位置都不对,后来发现是没做Z相归零。从那以后,我只要用增量式编码器,上电后第一件事就是找Z相。
关键点:A/B相用于计数和方向判断,Z相用于绝对位置参考。
3.2 定时器编码器模式配置(STM32)
STM32的定时器有个很贴心的功能——编码器模式。说白了,就是硬件帮你把A/B相的脉冲直接转换成计数值,不用你在中断里一个个数。
我刚开始做的时候,傻乎乎地用外部中断去读A/B相,结果发现高速时根本来不及。后来改用定时器编码器模式,CPU占用率直接降到了几乎为零。
配置其实不复杂,我一般这么干:
// 以STM32F407为例,配置TIM3为编码器模式
TIM_EncoderInterfaceConfigTypeDef sEncoderConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
// 使能定时器时钟
__HAL_RCC_TIM3_CLK_ENABLE();
// 配置GPIO:PA6为TIM3_CH1,PA7为TIM3_CH2
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置编码器模式
sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 双沿计数
sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sEncoderConfig.IC1Filter = 0;
sEncoderConfig.IC2Filter = 0;
HAL_TIM_Encoder_Init(&htim3, &sEncoderConfig);
// 启动编码器
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
嗯,这里要注意:TIM_ENCODERMODE_TI12 表示在A相和B相的双沿都计数。这样分辨率能提高4倍。比如一个1000线的编码器,用这个模式就能得到4000个计数/圈。
我的习惯:编码器模式配置好后,先用手转一下电机,看看计数值是不是在变化。如果不动,八成是GPIO配置错了。
3.3 读取位置与速度
位置读取很简单,直接读定时器的计数器值就行:
int32_t position = __HAL_TIM_GET_COUNTER(&htim3);
但速度计算就有点讲究了。我见过不少新手直接用位置差分来算速度,结果噪声大得没法看。
两种常用方法:
- M法(测频法):固定时间窗口内数脉冲个数。适合高速场合。
- T法(测周法):测量两个脉冲之间的时间。适合低速场合。
我个人习惯在项目中用M法,因为实现简单,而且大多数应用场景下电机转速都不算太低。
// M法测速示例
#define SPEED_SAMPLE_TIME_MS 100 // 采样周期100ms
uint32_t last_count = 0;
uint32_t current_count = 0;
float speed_rpm = 0;
// 在定时器中断中调用(每100ms一次)
void Speed_Calculate(void)
{
current_count = __HAL_TIM_GET_COUNTER(&htim3);
int32_t delta = current_count - last_count;
// 处理溢出
if(delta > 0x7FFFFFFF)
delta = delta - 0xFFFFFFFF;
else if(delta < -0x7FFFFFFF)
delta = delta + 0xFFFFFFFF;
// 假设编码器1000线,4倍频后4000计数/圈
// 速度 = (delta / 4000) / (100ms / 60000ms) = delta * 15 / 1000
speed_rpm = (float)delta * 15.0f / 1000.0f;
last_count = current_count;
}
避坑指南:我曾经因为没处理计数器溢出,导致速度值突然跳变。电机在高速运行时,计数器很容易溢出。一定要做溢出处理!
3.4 方向判断
方向判断其实不用你操心——STM32的编码器模式会自动处理。你只需要看计数值是增加还是减少就行。
但如果你非要用外部中断自己实现,原理也很简单:
- 在A相的上升沿读取B相的电平
- 如果B相为高,说明正转
- 如果B相为低,说明反转
不过说实话,有硬件支持就别自己折腾了。我早期做的一个项目就是自己写逻辑判断方向,结果在电机抖动的时候频繁误判。换成硬件编码器模式后,问题迎刃而解。
总结一下:
- ABZ信号:A/B相差90度用于计数和方向,Z相用于归零
- 定时器编码器模式:硬件自动处理,省心省力
- 位置读取:直接读计数器值
- 速度计算:M法适合高速,T法适合低速
- 方向判断:硬件自动完成,别自己写
好了,这一章就到这里。下一章咱们聊聊怎么用这些数据做闭环控制——那才是真正有意思的部分。