编码器读到的到底是什么:从脉冲到速度

经典三问

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",走到"轮子实际留下了多少脉冲",小车才第一次有了可以相信的运动记录。

相关推荐
笨笨饿3 小时前
140_AI新手村MCP与Skills是干嘛的
开发语言·人工智能·python·stm32·单片机·嵌入式硬件·物联网
沐欣工作室_lvyiyi13 小时前
基于云平台的输液智能监测控制系统设计与实现(论文+源码)
stm32·单片机·单片机毕业设计·电子信息工程毕业设计
EatFan14 小时前
当 CubeMX 遇上 AI Agent:用 MCP 让 AI 直接生成 STM32 HAL 工程
人工智能·stm32·嵌入式硬件·cubemx·stm32cubemx·hal·mcp
千秋岁rr14 小时前
13.SPI
stm32
LCG元15 小时前
STM32L431 QSPI驱动W25Q256实战:内存映射模式配置、4字节地址模式陷阱与读写吞吐实测
stm32·单片机·嵌入式·flash
lhbweilai15 小时前
深挖FreeRTOS内核实现(上)
stm32·单片机·链表
Brilliantwxx15 小时前
【STM32】 DMA 源码分析与实战(下):DMA 收发状态机与 HAL 分层
android·开发语言·stm32·单片机·嵌入式硬件·mcu
铅笔小新z16 小时前
【stm32】DMA 源码分析与实战
stm32·单片机·嵌入式硬件
笨笨饿17 小时前
#141_ Codex 本地 AI 代理工具链工作流重构:Headroom、Scrapling、Archify 统一接入
开发语言·人工智能·stm32·单片机·嵌入式硬件·ui·重构