经典三问
Q: 增量式编码器是什么?
A: 它会把轴的转动变成一串脉冲,程序数一数这些脉冲,就能知道轴转过多少;A、B 两相信号还能分辨转动方向。
Q: 为什么需要编码器?
A: PWM 只表示给电机多少驱动力,不等于轮子真的转了多少、转得多快。编码器把轮子的实际运动交回给程序。
Q: 编码器常见使用场景?
A: 测量轮速、记录转过的距离、判断正反转,以及让左右轮尽量保持一致。
我在前面的实验里就碰到过这个困惑:两边电机明明给了同样的 PWM,实际转起来的速度却不一样。
左轮像有点着急,右轮却慢半拍;两边都写 30%,小车还是会慢慢往一侧偏。把数值改成 40、50,两个轮子当然都会更有劲,可它们依然不同速。
回想整个链路,我本质上就是提供了一个 PWM。PWM 决定驱动器在一个周期里给电机通电多久,它给的是驱动力的比例,关键就在这,PWM 没有说一定是"请以每分钟多少转运行"的命令。电机本身的差异、减速箱摩擦、轮胎压紧程度、电池电压和地面阻力,都会让同一个 PWM 变成不同的速度。
这时候就需要编码器出场啦,如果没有编码器,程序只知道"我给左右轮都写了 30%";至于它们到底转没转、谁快谁慢,只能靠眼睛猜。有了编码器,轴每转过一点,就留下一次脉冲。程序把这些脉冲数起来,就知道轮子实际留下的痕迹。
六根线里,哪四根不是给电机的
这次使用六线霍尔编码器电机。两根较粗的线属于电机本体,接到电机驱动板;另外四根线属于编码器:
| 线的用途 | 常见标记 | 本例连接位置 |
|---|---|---|
| 电机两线 | M+、M-,或两根无标记粗线 |
驱动板的电机输出端 |
| 编码器电源 | VCC |
按编码器资料供电,本例为 5V |
| 编码器地 | GND |
STM32、驱动板和电源的公共地 |
| A 相 | A、CHA |
PB6 / TIM4_CH1 |
| B 相 | B、CHB |
PB7 / TIM4_CH2 |
颜色没有统一标准。某个商品图里的红线是 VCC,不代表另一台电机的红线也一定是 VCC。先看卖家的引脚说明、插头丝印或产品资料;线序没有确认以前,不要急着上电。
本例把 PB6、PB7 配成内部上拉输入:
c
gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
gpio.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &gpio);
内部上拉适合开漏输出的编码器:编码器没有主动拉低时,引脚保持高电平;它拉低时,STM32 就读到低电平。
A、B 不是两根重复的脉冲线
先不让电机自己转,断开电机驱动的功率输出,只给编码器供电。手慢慢转动输出轴,A、B 两根线上都会出现高低变化;它们的区别在于,变化时间故意错开了一点。
text
一个方向:
A __|----|____|----|__
B ____|----|____|----|__
反方向:
A ____|----|____|----|__
B __|----|____|----|__
哪一相先变化,方向就不同。A、B 交换后,程序看见的方向通常也会反过来。至于"先到的是 A 就叫正转"还是"先到的是 B 就叫正转",没有脱离安装方向的标准答案:左、右电机的安装朝向不同,轮子装反了,或者插头的 A/B 标记相反,都会让正负号翻过来。
先转一圈,看计数朝哪个方向变化;再约定"小车向前时应当是正数"还是负数。方向的名字可以在软件里统一,A、B 的先后关系不能靠猜。
只接 A 相也能看到脉冲,却少了另一相提供的先后关系。这样还能做单向计数,却无法可靠判断正反转;对于需要知道轮子往哪边转的小车,A、B 两根都要接。
让 TIM4 自己数脉冲
最直观的做法,是 A 相每跳一次就进入一次中断,再去读 B 相判断方向。手转时很容易看见效果;电机转快以后,CPU 会频繁停下手里的事去数脉冲。
STM32 的通用定时器已经准备好另一种做法。TIM4 的 CH1、CH2 正好在 PB6、PB7 上。把 TIM4 设成编码器模式后,硬件会持续观察 A/B 的先后关系,自行给计数器加一或减一,CPU 不必为每一个边沿进入中断。
c
void Encoder_Init(void)
{
GPIO_InitTypeDef gpio;
TIM_TimeBaseInitTypeDef timer;
TIM_ICInitTypeDef input;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
gpio.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &gpio);
timer.TIM_Prescaler = 0;
timer.TIM_CounterMode = TIM_CounterMode_Up;
timer.TIM_Period = 0xFFFF;
timer.TIM_ClockDivision = TIM_CKD_DIV1;
timer.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM4, &timer);
TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising,
TIM_ICPolarity_Rising);
TIM_ICStructInit(&input);
input.TIM_Channel = TIM_Channel_1;
input.TIM_ICFilter = 6;
TIM_ICInit(TIM4, &input);
input.TIM_Channel = TIM_Channel_2;
TIM_ICInit(TIM4, &input);
TIM_SetCounter(TIM4, 0);
TIM_Cmd(TIM4, ENABLE);
}
TIM_EncoderMode_TI12 表示 TIM4 同时使用 CH1 和 CH2,也就是同时使用 A、B 两相。TIM_Period = 0xFFFF 让这只 16 位计数器使用完整范围:从 0 到 65535。
TIM_ICFilter = 6 是输入数字滤波。导线较长、附近有电机电源线时,A/B 信号上可能出现很短的毛刺;滤波会忽略一部分短到不合理的跳变。数值不是越大越好,滤波越重,真实边沿也会更晚被看见。出现跳数时,先检查供电、接线和共地,再决定是否调整滤波。
取走一段时间里的脉冲
TIM4 一直在数,程序不必每次脉冲都去碰它。等 100ms,读一次当前值,再把计数器清零,拿到的便是这 100ms 内的增量:
c
int16_t Encoder_ReadDelta(void)
{
int16_t delta = (int16_t)TIM_GetCounter(TIM4);
TIM_SetCounter(TIM4, 0);
return delta;
}
反转时,TIM4 会向下数。寄存器里的 16 位数字从 0 再减一,会回到 65535;把它看成 int16_t 时,65535 正好表示 -1。因此 delta 是有正负号的:正数和负数分别代表两个相反方向,0 代表这段时间没有数到脉冲。
这不是计数器永远不会绕回的魔法。一次取数的时间太长、轮子又转得很快时,计数器可能绕回不止一次,正负号和数量都会失去意义。最高转速、每圈计数和取数周期要放在一起考虑。
第一次观察时,直接用已经写好的 Delay_ms(100) 等 100ms,再取一次计数并刷新 OLED:
c
int main(void)
{
int16_t delta;
int32_t total_count = 0;
Encoder_Init();
OLED_Init();
OLED_Clear();
Delay_Init();
while (1)
{
Delay_ms(100);
delta = Encoder_ReadDelta();
total_count += delta;
OLED_ShowSignedNum(0, 0, delta, 5, OLED_6X8);
OLED_ShowSignedNum(0, 8, total_count, 8, OLED_6X8);
OLED_Update();
}
}
第一行 delta 是最近 100ms 内数到的脉冲数,第二行 total_count 是从上电到现在的累计数。手转时,delta 忽大忽小很正常,手的速度本来就不稳定;只要正转时累计数持续往一个方向走,反转时能够掉头,A/B 两相和 TIM4 的方向判断就已经正常工作了。
三个亲手能确认的现象
正转和反转
先不接电机功率端,手转输出轴一圈。观察 OLED 的累计数:一个方向会持续增加,换方向后会持续减少。符号和心里定义的"前进"相反不算故障,只需要在左右电机真正装好后统一方向约定。
交换 A、B
断电后交换 A、B 两根编码器线,再用同一个方向手转。累计数的正负通常会翻转。重新断电,把线换回原位。这个小实验能直接看见:方向不是某一根线上的高或低,而是两根线变化的先后。
静止时不该自己记数
轮子静止时,连续观察累计数一段时间,它应当保持不变。若数字自己跳动,先确认编码器电源、公共地和 A/B 接线;再检查编码器线是否和电机供电线长距离缠在一起。
一圈到底是多少个脉冲
原始计数已经足够回答"轮子有没有转"和"哪个方向转"。要把它换成每分钟转速,还需要知道输出轴转一整圈会产生多少计数。
text
每分钟转速 = 本次增量 × 60000 / 输出轴每圈实际计数 / 取数周期(毫秒)
例如 100ms 内读到 220 个计数,确认输出轴每圈是 1320 个计数:
text
每分钟转速 = 220 × 60000 / 1320 / 100 = 100 rpm
rpm 是每分钟转数(revolutions per minute)。这里一定要写"输出轴每圈实际计数"。商品页上常常同时出现 PPR、减速比、电机轴转速和输出轴转速;其中一个 11 PPR,不一定就是 TIM4 在输出轴转一圈时读到的计数。
先看编码器手册里的数字对应电机轴还是减速后的输出轴,再手动让输出轴完整转一圈,记录累计数实际变化了多少。两者对得上,再把这个值用于换算。不要在没有确认前随便填一个数字,算出来的小数点后面再漂亮,也不会变成真实速度。
取数周期从 100ms 改成 200ms 时,同样速度下的 delta 大约会变成两倍,因为收集脉冲的时间翻倍了。公式中的取数周期也必须一起从 100 改成 200;否则轮子没变快,算出来的转速却会刚好偏一倍。
最后橘猫说
同一个 PWM 不能保证两个轮子同速,因为 PWM 交出去的是驱动力比例,不是速度测量。编码器把转动翻译成 A、B 两相脉冲,TIM4 再把脉冲变成有方向的计数。从"我给了多少 PWM",走到"轮子实际留下了多少脉冲",小车才第一次有了可以相信的运动记录。