Administrator
发布于 2026-09-25 / 0 阅读
0
0

编码器测速三种方法实测对比:M 法、T 法、M/T 法到底该怎么选

一句话结论:高速用 M 法(数脉冲),低速用 T 法(测脉宽),全速域都在用就用 M/T 法——它在低速和高速下误差都最小。

一、完整工程下载

压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。

下载 encoder-speed.zip (8.9 KB,共 7 个文件)

.gitignore
Makefile
README.md
include/
  encoder.h
platformio.ini
src/
  encoder.c
test/
  test_encoder.c

编码器给的是什么

增量式编码器(光电/霍尔/磁编)输出 A、B 两路正交方波:

正转:   A  ──┐  ┌──┐  ┌──┐  ┌──
            └──┘  └──┘  └──┘
        B  ──┐  ┌──┐  ┌──┐  ┌──       B 比 A 滞后 90°
            └──┘  └──┘  └──┘

反转:   A/B 相位关系反过来
  • 数脉冲 得到位移(配合四倍频,分辨率 ×4)
  • 测频率 得到速度(单位时间内的脉冲数)
  • 判相位 得到方向

解码:为什么建议四倍频

只数 A 的上升沿 = 一倍频;用上 A、B 的上升沿和下降沿 = 四倍频:

A/B 组合的 4 个状态:00 -> 10 -> 11 -> 01 -> 00 循环
每跳变一次计数 ±1,一圈的计数 = 编码器线数 × 4

好处:分辨率 ×4,而且在极低速(甚至单步)时也能分辨运动。

三种测速方法的本质区别

设编码器一圈 PPR 个脉冲(四倍频后为 4·PPR),测速周期 T 秒。

方法公式适用缺点
M 法n = 60·ΔM/(PPR_x4·T)高速低速时 ΔM 只有几个数,量化误差巨大
T 法n = 60/(PPR_x4·Δt)低速高速时 Δt 只有几十 μs,定时器分辨率不够
M/T 法n = 60·(M−1)/(PPR_x4·(t_M − t_0))全速域实现稍复杂

量化误差直观感受

M 法在 60rpm、测速周期 10ms、PPR_x4 = 1300 时:

ΔM = 60·1300·0.01/60 = 13 个脉冲

±1 个脉冲就是 ±7.7% 的误差!而 T 法在同样条件下测的是脉宽, 定时器 1μs 分辨率下脉宽约 46ms,误差只有 ±0.002%。

反过来在 3000rpm 时,T 法脉宽只有 0.9ms,μs 定时器误差 ±0.1%, 而 M 法 ΔM = 650,误差 ±0.15%,两者接近;但再快 T 法就撑不住了。

M/T 法为什么准

M/T 法的精髓:用两个信号交叉计时。

  • 用一个已知周期 T 的门控信号和脉冲序列做与运算
  • 门控结束时:数出这段时间里的脉冲数 M;同时用一个高频时钟测出

「从第一个脉冲到第 M 个脉冲之间的精确时间 t」

  • n = 60·(M−1) / (PPR_x4 · t)

这样分母不是「近似 T」,而是实测的精确时间, 低速时 M 小但 t 测得准,高速时 M 大误差被平均掉,全速域误差都在 ±0.1% 量级。

工程内容

  • encoder.c/h:四倍频正交解码(含非法跳变检测)+ M/T/M-T 三种测速 + 一阶滤波
  • test/test_encoder.c:用仿真脉冲流跑 30rpm / 300rpm / 3000rpm 三档,

打印三种方法的实测值和相对误差,直接看出该选哪个

调试要点

  • A/B 接反了:现象是正转时转速为正但方向标志为负。不必重接线,

在解码表里交换 A/B 即可

  • 计数丢脉冲:读取 count 时如果中断里也在改它,会读到「半个」更新值。

用 int32_t 一次原子读取,或 __disable_irq()/__enable_irq() 包一下

  • 机械安装偏心会导致一圈内测速周期性波动,这不是算法问题,

用一圈低通(如 10Hz)能压下去

  • 别用主循环计时:用定时器硬件做测速窗口,否则窗口抖动直接变成速度噪声
  • 编码器电源也要干净:霍尔编码器对电机换向干扰很敏感,加 0.1μF 陶瓷电容

进阶方向

  • 用 STM32 定时器的编码器模式(TI1/TI2 双通道),硬件自动四倍频计数,CPU 零开销
  • 把测速接到 PID 里做速度环(配合本站 PID 一文)
  • 加方向死区与断线检测(A/B 长时间不变 → 判定传感器故障)

完整代码

Makefile

CC      ?= gcc
CFLAGS  ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS  ?= -lm
SRC      = src/encoder.c
TEST     = test/test_encoder.c

ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)

all: run

$(BIN): $(SRC) $(TEST)
	@mkdir -p build
	$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)

run: $(BIN)
	@$(BIN)

clean:
	rm -rf build

.PHONY: all run clean

include/encoder.h

/**
 * encoder.h - 增量式正交编码器解码 + 三种测速方法
 */
#ifndef ENCODER_H
#define ENCODER_H

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef enum {
    ENC_M = 0,  /* M 法:固定时间数脉冲 */
    ENC_T,      /* T 法:测两个脉冲之间的时间 */
    ENC_MT      /* M/T 法:两者交叉 */
} enc_method_t;

typedef struct {
    /* 配置 */
    uint32_t ppr;        /* 编码器线数(一圈脉冲数,未四倍频) */
    uint16_t gear;       /* 减速比(分子),如 30:1 填 30 */
    uint32_t tick_hz;    /* 时基频率,例如定时器 1MHz = 1000000 */
    uint32_t window_ms;  /* M 法与 M/T 法的测速窗口 */

    /* 解码状态 */
    uint8_t ab;          /* 上一次 A/B 状态 */
    int32_t count;       /* 四倍频累积计数(带方向) */
    int8_t dir;          /* +1 正转,-1 反转 */
    uint32_t illegal;    /* 非法跳变次数(丢步/干扰指示) */

    /* M 法状态 */
    int32_t last_count;
    uint32_t last_window_tick;

    /* T 法状态 */
    uint32_t last_pulse_tick;
    uint32_t last_period;      /* 上一次两脉冲间隔(tick) */
    bool have_pulse;
    uint32_t pulse_timeout;    /* 长时间无脉冲 -> 速度归零 */

    /* M/T 法状态 */
    uint32_t mt_first_tick;    /* 窗口内第一个脉冲的时刻 */
    uint32_t mt_last_tick;
    int32_t mt_first_count;
    bool mt_started;

    /* 输出 */
    float rpm_m;
    float rpm_t;
    float rpm_mt;
    float rpm_filt;      /* 对 rpm_mt 做一阶低通的输出 */
} encoder_t;

void encoder_init(encoder_t *e, uint32_t ppr, uint16_t gear,
                  uint32_t tick_hz, uint32_t window_ms);

/** 编码器 A/B 任一跳变时调用;a、b 为当前电平(0/1) */
void encoder_update_ab(encoder_t *e, uint8_t a, uint8_t b);

/**
 * 一次脉冲边沿到达(在 A/B 变化后调用,用于 T 法与 M/T 法记时刻)
 * 真实项目里通常放在 EXTI 中断的最后一行。
 */
void encoder_on_edge(encoder_t *e);

/** 由定时器以 tick_hz 频率调用(通常放在 1MHz 的定时器中断里) */
void encoder_tick(encoder_t *e, uint32_t now_tick);

/** 取当前转速(rpm,已按减速比折算到输出轴) */
float encoder_get_rpm(const encoder_t *e);

#ifdef __cplusplus
}
#endif

#endif /* ENCODER_H */

platformio.ini

[platformio]
default_envs = bluepill

[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
    -Wall
    -Wextra
    -Isrc
lib_ldf_mode = deep+

src/encoder.c

#include "encoder.h"

#include <string.h>

/* 四倍频状态转移表:索引 = (prev_ab << 2) | cur_ab,值 = ±1 / 0(非法) */
static const int8_t s_trans[16] = {
    0,  -1,  1,  0,   /* prev=00 */
    1,   0,  0, -1,   /* prev=01 */
    -1,  0,  0,  1,   /* prev=10 */
    0,   1, -1,  0    /* prev=11 */
};

void encoder_init(encoder_t *e, uint32_t ppr, uint16_t gear,
                  uint32_t tick_hz, uint32_t window_ms)
{
    memset(e, 0, sizeof(*e));
    e->ppr = (ppr == 0) ? 1 : ppr;
    e->gear = (gear == 0) ? 1 : gear;
    e->tick_hz = (tick_hz == 0) ? 1000000u : tick_hz;
    e->window_ms = (window_ms == 0) ? 10u : window_ms;
    e->dir = 1;
}

void encoder_update_ab(encoder_t *e, uint8_t a, uint8_t b)
{
    uint8_t cur = (uint8_t)(((a & 1u) << 1) | (b & 1u));
    uint8_t idx = (uint8_t)((e->ab << 2) | cur);
    int8_t d = s_trans[idx & 0x0Fu];

    if (e->ab == cur) {
        return; /* 没有变化 */
    }
    if (d == 0) {
        e->illegal++; /* 两路同时跳变 = 干扰或丢步 */
        e->ab = cur;
        return;
    }
    /* 记录 M/T 法需要的时刻由 encoder_tick 里的 s_now 提供 */
    e->count += d;
    e->dir = (d > 0) ? 1 : -1;
    e->ab = cur;
}

/* 当前 tick,由 encoder_tick 更新;供脉冲边沿记录使用 */
static uint32_t s_now = 0;

void encoder_tick(encoder_t *e, uint32_t now_tick)
{
    uint32_t dt;
    uint32_t win_ticks;
    uint32_t ppr4;
    float rev;

    s_now = now_tick;
    ppr4 = e->ppr * 4u;              /* 四倍频后一圈的计数 */
    win_ticks = (e->tick_hz / 1000u) * e->window_ms;
    if (win_ticks == 0) {
        win_ticks = 1;
    }

    /* ---------- T 法:靠脉冲边沿更新,这里只做「超时归零」 ---------- */
    if (e->have_pulse) {
        if ((uint32_t)(now_tick - e->last_pulse_tick) > (win_ticks * 4u)) {
            e->rpm_t = 0.0f;
            e->last_period = 0;
            e->have_pulse = false;
        }
    }

    /* ---------- 窗口到期:算 M 法与 M/T 法 ---------- */
    dt = (uint32_t)(now_tick - e->last_window_tick);
    if (dt < win_ticks) {
        return;
    }
    e->last_window_tick = now_tick;

    rev = (float)e->gear; /* 输出轴转数 = 电机轴转数 / 减速比 */

    /* M 法 */
    {
        int32_t dm = e->count - e->last_count;
        e->last_count = e->count;
        e->rpm_m = ((float)dm / (float)ppr4) * (60.0f * (float)e->tick_hz / (float)dt) / rev;
    }

    /* M/T 法 */
    if (e->mt_started && e->mt_last_tick > e->mt_first_tick) {
        int32_t dm = e->count - e->mt_first_count;
        uint32_t t = e->mt_last_tick - e->mt_first_tick;
        if (dm != 0 && t > 0) {
            /* t 是「第一个边沿到最后一个边沿」的实测时间,这期间的间隔数正好是 dm,
             * 所以用 dm 而不是 (M-1):分母用的是实测精确时间,不是近似的窗口 T */
            e->rpm_mt = ((float)dm / (float)ppr4) *
                       (60.0f * (float)e->tick_hz / (float)t) / rev;
            /* 一阶低通,抑制机械偏心引起的周期性波动 */
            if (e->rpm_filt == 0.0f) {
                e->rpm_filt = e->rpm_mt;
            } else {
                e->rpm_filt = 0.3f * e->rpm_mt + 0.7f * e->rpm_filt;
            }
        }
    }
    e->mt_started = false;
    e->mt_first_tick = e->mt_last_tick = now_tick;
}

/**
 * 脉冲边沿到达(由你的中断在 A/B 变化时调用,内部会记时刻)
 * 说明:真实项目里可以直接在 encoder_update_ab 后调用它。
 */
void encoder_on_edge(encoder_t *e)
{
    uint32_t period;

    /* T 法 */
    if (e->have_pulse) {
        period = (uint32_t)(s_now - e->last_pulse_tick);
        if (period > 0) {
            e->last_period = period;
            /* 一个脉冲对应 1/(ppr*4) 转 */
            e->rpm_t = (60.0f * (float)e->tick_hz) /
                       ((float)e->ppr * 4.0f * (float)period * (float)e->gear);
        }
    }
    e->last_pulse_tick = s_now;
    e->have_pulse = true;

    /* M/T 法:窗口内第一个脉冲记下起点,之后持续更新终点 */
    if (!e->mt_started) {
        e->mt_started = true;
        e->mt_first_tick = s_now;
        e->mt_first_count = e->count;
    }
    e->mt_last_tick = s_now;
}

float encoder_get_rpm(const encoder_t *e)
{
    return e->rpm_filt;
}

test/test_encoder.c

/**
 * 主机端测试:用仿真的正交脉冲流对比三种测速方法
 * gcc -std=c99 -Wall -Wextra -Iinclude src/encoder.c test/test_encoder.c -o build/test -lm
 */
#include <math.h>
#include <stdio.h>

#include "encoder.h"

#define TICK_HZ 1000000u   /* 1MHz 时基 */
#define PPR 1300u          /* 13 线霍尔编码器 × 100 减速比 = 1300 线/输出轴圈 */
#define GEAR 1u

void encoder_on_edge(encoder_t *e);

/* 仿真:以 rpm 转速跑 seconds 秒,按合法正交(格雷)序列翻转 A/B
 *
 * 注意这里必须是格雷码序列 00 -> 10 -> 11 -> 01 -> 00,
 * 相邻状态只能有 1 位变化;如果写成 00->01->10->11,
 * 就会出现「两路同时跳变」的非法状态,解码器会把它当成丢步。
 * 这是本工程写测试时踩过的坑。
 */
static void simulate(encoder_t *e, float rpm, float seconds, enc_method_t which)
{
    static const uint8_t seq[4] = {0u, 2u, 3u, 1u}; /* 00,10,11,01 */
    double counts_per_sec = (double)rpm * (double)PPR * 4.0 / 60.0;
    double step = 0.0;
    double acc = 0.0;
    uint32_t total = (uint32_t)((double)TICK_HZ * seconds);
    int dir = (counts_per_sec >= 0.0) ? 1 : -1;
    uint8_t idx = 0;

    (void)which;
    encoder_init(e, PPR, GEAR, TICK_HZ, 10);

    if (counts_per_sec < 0.0) {
        step = -counts_per_sec / (double)TICK_HZ;
    } else {
        step = counts_per_sec / (double)TICK_HZ;
    }

    for (uint32_t t = 0; t < total; t++) {
        uint8_t ph;
        acc += step;
        while (acc >= 1.0) {
            acc -= 1.0;
            idx = (uint8_t)((idx + dir + 4) & 3);
            ph = seq[idx];
            encoder_update_ab(e, (uint8_t)((ph >> 1) & 1u), (uint8_t)(ph & 1u));
            encoder_on_edge(e);   /* 真实项目里由 EXTI 中断调用 */
        }
        encoder_tick(e, t);
    }
}

int main(void)
{
    encoder_t e;
    const float speeds[] = {30.0f, 300.0f, 3000.0f};

    printf("===== 编码器测速方法对比(PPR=%u 线,四倍频,窗口 10ms,时基 1MHz)=====\n\n",
           (unsigned)PPR);
    printf("  真实转速      M 法           T 法          M/T 法(滤波后)\n");
    printf("             实测   误差     实测   误差     实测   误差\n");
    printf("  ----------------------------------------------------------------\n");

    for (unsigned i = 0; i < sizeof(speeds) / sizeof(speeds[0]); i++) {
        float rpm = speeds[i];
        float em;
        float et;
        float emt;

        simulate(&e, rpm, 3.0f, ENC_MT);
        em = e.rpm_m;
        et = e.rpm_t;
        emt = e.rpm_filt;
        printf("  %6.0f rpm  %8.1f %6.2f%%  %8.1f %6.2f%%  %8.1f %6.2f%%\n",
               rpm,
               em, 100.0f * (em - rpm) / rpm,
               et, 100.0f * (et - rpm) / rpm,
               emt, 100.0f * (emt - rpm) / rpm);
    }

    printf("\n[低速细看] 30rpm 时 M 法为什么不准\n");
    {
        simulate(&e, 30.0f, 1.0f, ENC_M);
        printf("  窗口 10ms 内脉冲数 = %.2f 个\n",
               30.0f * (float)PPR * 4.0f / 60.0f * 0.01f);
        printf("  ±1 个脉冲 -> 相对误差 ±%.2f%%\n",
               100.0f / (30.0f * (float)PPR * 4.0f / 60.0f * 0.01f));
        printf("  (本例表中 M 法显示 0.00%% 是因为仿真转速刚好使每个窗口都是整数个脉冲;\n");
        printf("    真实系统中窗口边界与脉冲不同步,误差就按上面这个数值量级出现)\n");
    }

    printf("\n[反向] 反转 300rpm,方向与速度符号\n");
    {
        simulate(&e, -300.0f, 2.0f, ENC_MT);
        printf("  rpm_mt = %.1f,direction = %d,累积计数 = %d\n",
               e.rpm_mt, (int)e.dir, (int)e.count);
    }

    printf("\n[静止] 30rpm 后立刻停车,T 法的超时归零\n");
    {
        simulate(&e, 300.0f, 0.5f, ENC_T);
        printf("  停车瞬间 rpm_t = %.1f\n", e.rpm_t);
        for (uint32_t t = 0; t < 200000u; t += 1000u) {
            encoder_tick(&e, t);
        }
        printf("  超过 4 个窗口无脉冲后 rpm_t = %.1f(应归零)\n", e.rpm_t);
    }

    printf("\n[非法跳变] 模拟 A/B 两路同时变化的干扰(机械抖动/丢步)\n");
    {
        encoder_init(&e, PPR, GEAR, TICK_HZ, 10);
        encoder_update_ab(&e, 0, 0);   /* 基准态 00 */
        encoder_update_ab(&e, 0, 1);   /* 00 -> 01,合法 */
        for (int i = 0; i < 5; i++) {
            encoder_update_ab(&e, 1, 0);   /* 01 -> 10,两路同时跳 = 非法 */
            encoder_update_ab(&e, 0, 1);   /* 10 -> 01,两路同时跳 = 非法 */
        }
        printf("  非法跳变计数 = %u(>0 说明有干扰或丢步,正常应为 0)\n", e.illegal);
    }

    printf("\n结论(按上表实测数据):\n");
    printf("  * 高速端:T 法在 3000rpm 时误差已达 28%%(脉宽只剩 0.9ms,\n");
    printf("    1MHz 时基下 ±1 个计数就放大成几十转),高速必须用 M 法或 M/T 法\n");
    printf("  * 低速端:M 法的量化误差正比于 1/ΔM(30rpm 时 ±1 个脉冲 = ±3.85%%),\n");
    printf("    低速应该用 T 法或 M/T 法\n");
    printf("  * 全速域:M/T 法,三档误差都 ≤0.01%% —— 这是它唯一多花的那点代码换来的\n");
    printf("\n===== 以上数据由本机 gcc 实编译实运行产生 =====\n");
    return 0;
}

实测输出

下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):

===== 编码器测速方法对比(PPR=1300 线,四倍频,窗口 10ms,时基 1MHz)=====

  真实转速      M 法           T 法          M/T 法(滤波后)
             实测   误差     实测   误差     实测   误差
  ----------------------------------------------------------------
      30 rpm      30.0   0.00%      30.0   0.16%      30.0   0.00%
     300 rpm     300.0   0.00%     303.6   1.21%     300.0   0.01%
    3000 rpm    3000.0   0.00%    3846.2  28.21%    3000.0   0.00%

[低速细看] 30rpm 时 M 法为什么不准
  窗口 10ms 内脉冲数 = 26.00 个
  ±1 个脉冲 -> 相对误差 ±3.85%
  (本例表中 M 法显示 0.00% 是因为仿真转速刚好使每个窗口都是整数个脉冲;
    真实系统中窗口边界与脉冲不同步,误差就按上面这个数值量级出现)

[反向] 反转 300rpm,方向与速度符号
  rpm_mt = -300.0,direction = -1,累积计数 = -52000

[静止] 30rpm 后立刻停车,T 法的超时归零
  停车瞬间 rpm_t = 303.6
  超过 4 个窗口无脉冲后 rpm_t = 0.0(应归零)

[非法跳变] 模拟 A/B 两路同时变化的干扰(机械抖动/丢步)
  非法跳变计数 = 10(>0 说明有干扰或丢步,正常应为 0)

结论(按上表实测数据):
  * 高速端:T 法在 3000rpm 时误差已达 28%(脉宽只剩 0.9ms,
    1MHz 时基下 ±1 个计数就放大成几十转),高速必须用 M 法或 M/T 法
  * 低速端:M 法的量化误差正比于 1/ΔM(30rpm 时 ±1 个脉冲 = ±3.85%),
    低速应该用 T 法或 M/T 法
  * 全速域:M/T 法,三档误差都 ≤0.01% —— 这是它唯一多花的那点代码换来的

===== 以上数据由本机 gcc 实编译实运行产生 =====

评论