从一个最朴素的需求说起:ESP32 编码器测速与 M/T 混合闭环实战

想让机器人走得稳,就得用 PID 闭环控制轮速。闭环听起来玄乎,拆开就一句话:你得先知道轮子现在转多快,才能知道该加油门还是收油门。

目标速度是软件里设的,好办。但“现在转多快”是物理世界的状态,软件看不见,只能靠传感器。这就是为什么电机尾巴上要拖几根线——正交编码器。

原理不难:码盘上刻了一圈缝,电机一转,输出引脚上的电压就在 0V 和 3.3V 之间反复横跳,输出两路相位差 90° 的方波:

   ┌──┐   ┌──┐   ┌──┐   ┌──┐
A ─┘  └───┘  └───┘  └───┘  └──   (方波)
     ┌──┐   ┌──┐   ┌──┐   ┌──
B ───┘  └───┘  └───┘  └───┘  └   (滞后 90°)
  ────────────────────────────→ 正转方向

这串方波里其实藏着三个关键信息:

  1. 转速(频率):单位时间内方波脉冲越多,说明转得越快;
  2. 转向(相位):A 相超前 B 相为正转,B 相超前 A 相则为反转;
  3. 位置(累计计数):脉冲累加的总量,直接对应轴旋转过的绝对物理角度。

我手头这颗小电机 PPR=11\text{PPR} = 11PPR=11,经过 1:301:301:30 减速箱后,输出轴每转一圈 =11×30=330= 11 \times 30 = 330=11×30=330 个物理脉冲。再叠上“正交解码”的加成——A、B 两相的上升沿和下降沿统统计数,分辨率还能再翻 4 倍:

每转总计数 C=330×4=1320 count/rev\text{每转总计数 } C = 330 \times 4 = 1320 \text{ count/rev}每转总计数 C=330×4=1320 count/rev

传感器有了,脉冲也有了。接下来才是真正让我卡壳的地方。


问题一:这些脉冲是怎么“进”到芯片里的?

这块我以前一直似懂非懂:编码器输出的明明就是一根导线上的电压跳变,ESP32 是怎么“知道”它变了、还顺便把数给数了的?

拆开看其实是层层递进的三层机制:

第一层:编码器产生电信号

编码器内部是码盘加红外对管,轴一转,输出引脚上的电压就在高低电平之间切换:

编码器 A 相输出引脚上的电压:
3.3V ──┐    ┌───┐    ┌───
       │    │   │    │
 0V    └────┘   └────┘

这就是一根普通的电线,上面的物理电压在变化,仅此而已。

第二层:接到 GPIO

A、B 两相各接一根线到 ESP32 的两个引脚。但注意:GPIO 本身只是一个引脚,它不会主动做任何事——它就像一扇窗户,被动地反映窗外的电平状态。

第三层:PCNT 硬件电路接管

这是最关键的一环。PCNT(Pulse Counter,脉冲计数器) 是 ESP32 芯片内部的一块专用硬件外设——不是软件,不是 CPU 在死循环跑代码。

配置它的时候,相当于直接给底层硬件交代任务:

“你盯住 GPIO 18,每当它的电平出现边沿跳变,就看一眼 GPIO 19 此时是高还是低,然后硬件自动决定计数器是 +1+1+1 还是 −1-1−1。”

配置完成后,这整块电路就自动、持续、实时地工作,完全不占 CPU 资源:

芯片内部:
GPIO 18 (A相) ──→ ┌──────────────────────┐
                  │     PCNT 硬件电路     │
                  │                      │
                  │  边沿检测器 ──→ 判断  │──→ count 寄存器
GPIO 19 (B相) ──→ │  电平采样器          │    (一个数字,如 +527)
                  └──────────────────────┘
   这整块电路独立运行,CPU 该干嘛干嘛,
   想知道转了多少时读一下 count 即可。

具体的硬件逻辑判断长这样(这也是四倍频的来源):

  • GPIO 18 (A相) 出现上升沿时:
    • 若 GPIO 19 (B相) 为低电平 →\rightarrow→ count + 1(正转)
    • 若 GPIO 19 (B相) 为高电平 →\rightarrow→ count - 1(反转)
  • GPIO 18 (A相) 出现下降沿时:
    • 若 GPIO 19 (B相) 为高电平 →\rightarrow→ count + 1
    • 若 GPIO 19 (B相) 为低电平 →\rightarrow→ count - 1
  • (B 相的边沿做对称检测,4 个边沿全计数 →\rightarrow→ 四倍频达成)

💡 一个想通这件事的生动类比:
反过来想,如果芯片上没有 PCNT(很多便宜的低端 MCU 确实没有),就只能让 CPU 自己干——用 GPIO 外部中断:
每当引脚电平一跳变 →\rightarrow→ CPU 正在跑的业务被打断 →\rightarrow→ 跑中断函数,手动 count++ 或 count-- →\rightarrow→ 退出中断。
问题在于:电机高速转动时,每秒几万次中断狂轰滥炸,CPU 光是处理进出中断就被榨干了,其他任务全得卡死。
PCNT 的价值就在这:把“盯边沿、数脉冲”从 CPU 彻底卸载给硬件小弟,CPU 的开销从每秒几万次中断直接降到了 0。

到这里,硬件的“账本”有了。但账本只记了总脉冲数,怎么从这串数字里算出实时转速?


问题二:有了脉冲计数,转速怎么算?最直观的就是 M 法

最符合直觉的做法:固定一个时间窗口,数窗口里来了几个脉冲。这就是 M 法(频率法)。

时间窗口 T_s(比如 10ms)
├────────────────────────────────────┤
│  ↑   ↑   ↑   ↑   ↑   ↑   ↑   ↑  ↑  │   ← 编码器脉冲
│            数到 N = 9 个           │
└────────────────────────────────────┘

转速计算公式:

ωM=2π⋅NC⋅Ts(rad/s)\omega_M = \frac{2\pi \cdot N}{C \cdot T_s} \quad (\text{rad/s}) ωM​=C⋅Ts​2π⋅N​(rad/s)
或换算成 RPM:
RPMM=NC⋅Ts×60\text{RPM}_M = \frac{N}{C \cdot T_s} \times 60RPMM​=C⋅Ts​N​×60

其中 NNN 是窗口内数到的脉冲数,CCC 是每转总计数(1320),TsT_sTs​ 是采样周期。

高速时它的表现相当好,拿 300 RPM 代入算一下:
N=1320×30060×0.01=66 个脉冲N = 1320 \times \frac{300}{60} \times 0.01 = 66 \text{ 个脉冲}N=1320×60300​×0.01=66 个脉冲
量化分辨率=166≈1.5%✅ 挺准,完全够用\text{量化分辨率} = \frac{1}{66} \approx 1.5\% \quad \text{✅ 挺准,完全够用}量化分辨率=661​≈1.5%✅ 挺准,完全够用

但速度一慢,当场就露馅了:

  • 低速 10 RPM 爬行:
    N=1320×1060×0.01=2.2N = 1320 \times \frac{10}{60} \times 0.01 = 2.2N=1320×6010​×0.01=2.2
    物理世界的脉冲是离散的,实际只能数到整数 2 或 3:
    量化误差=12=50%❌ 误差直接爆炸!\text{量化误差} = \frac{1}{2} = 50\% \quad \text{❌ 误差直接爆炸!}量化误差=21​=50%❌ 误差直接爆炸!
  • 极低速 1 RPM 微调:
    N=1320×160×0.01=0.22N = 1320 \times \frac{1}{60} \times 0.01 = 0.22N=1320×601​×0.01=0.22
    绝大部分窗口里数出来的都是 N=0N = 0N=0,算出来的转速直接就是 0 ❌ 彻底失效\quad \text{❌ 彻底失效}❌ 彻底失效

根源剖析:NNN 只能是整数,而脉冲真实的到达率往往带着小数。掐头去尾带来的 ±1\pm 1±1 量化截断误差是固定存在的,NNN 越小,这个 ±1\pm 1±1 所占的相对比例就越吓人。数到 66 个脉冲时差 1 个无伤大雅,数到 2 个脉冲时差 1 个就是直接腰斩。


问题三:那把时间窗口拉长不就完了?

我的第一反应也是这个:M 法不是窗口越小越不准吗?把 TsT_sTs​ 从 10ms 拉大到 100ms 甚至 500ms,NNN 自然就大了。动手一算,数学上还真成立:

  • Ts=10ms→N=2.2→T_s = 10\text{ms} \rightarrow N = 2.2 \rightarrowTs​=10ms→N=2.2→ 只能数出 2 或 3 →\rightarrow→ 误差 ≈50%\approx 50\%≈50% ❌
  • Ts=100ms→N=22→T_s = 100\text{ms} \rightarrow N = 22 \rightarrowTs​=100ms→N=22→ 误差 ≈4.5%\approx 4.5\%≈4.5% ✅ 好多了
  • Ts=500ms→N=110→T_s = 500\text{ms} \rightarrow N = 110 \rightarrowTs​=500ms→N=110→ 误差 ≈0.9%\approx 0.9\%≈0.9% ✅ 看着非常准

数学公式上虽然成立,但自己实际调车时踩中了一个要命的坑:TsT_sTs​ 不只是测速窗口,它往往同时还是 PI 速度控制器的控制周期!

PI 控制器的工作方式是每个 TsT_sTs​ 执行一轮闭环:测速 → 算误差 → PI 调节 → 刷新 PWM。如果为了测速好看把 TsT_sTs​ 拉大到 500ms:

时刻 0ms:      测速 → PI 计算 → 输出 PWM 驱动
时刻 0~500ms:  这 500ms 内发生了什么,PI 控制器完全瞎了
               ↓ 小车撞到斜坡阻力 / 突然打滑
               → 此时急需立刻猛踩油门或刹车
               → 但 PI 还在按 500ms 前的速度旧账驱动电机!
时刻 500ms:    终于执行下一次测速 → 发现速度早变了 → 疯狂猛补 → 剧烈超调!

控制系统里管这个叫延迟 / 死区时间——TsT_sTs​ 越大,小车反应越迟钝,调过车的朋友都知道,这时候小车就会像喝醉了一样前后剧烈晃荡,根本稳不住。

三种窗口尺寸对比起来就很清晰:

采样窗口 TsT_sTs​10 RPM 采样脉冲数 NNN速度量化误差控制延迟与死区时间实车闭环表现
10 ms2.2≈50%\approx 50\%≈50%10 ms (极低延迟)响应极快,但测速反馈乱跳,小车低速发抖
100 ms22≈4.5%\approx 4.5\%≈4.5%100 ms (已有感知)低速平稳度改善,但遇到外力阻挡时反应迟缓
500 ms110≈0.9%\approx 0.9\%≈0.9%500 ms (灾难级)延迟严重,电机会剧烈超调振荡甚至发散失控

矛盾的本质:测速想要大窗口(为了精度),控制闭环却想要小周期(为了及时响应)。
M 法把这两件事死死捆在同一个 TsT_sTs​ 上了。

那能不能把这两件事彻底拆开?


问题四:换个思路——不数个数,改量时间,这就是 T 法

M 法问的是“这段时间内来了几个脉冲”,我们换个角度反过来问:“两个脉冲之间到底隔了多久?” 这就是 T 法(周期法)。

      ↑                 ↑        ← 两个相邻脉冲上升沿
      │←───── t_p ─────→│
      │  用高精度定时器   │
      │  测量这段时间     │

转速计算公式:

ωT=2πC⋅tp(rad/s)\omega_T = \frac{2\pi}{C \cdot t_p} \quad (\text{rad/s}) ωT​=C⋅tp​2π​(rad/s)

或换算成 RPM:

RPMT=60C⋅tp{RPM}_T = \frac{60}{C \cdot t_p}RPMT​=C⋅tp​60​

其中 tpt_ptp​ 是相邻脉冲边沿之间的时间差。

妙处立刻显现出来了:T 法的时间精度跟控制周期 TsT_sTs​ 完全无关!
它每来一个脉冲就记下一次硬件时间戳,脉冲到来的那一瞬间测速就已经完成了。控制循环爱什么时候来取数据都行。测速的时间分辨率和控制的更新频率,就此彻底解耦。

算一下精度(ESP32 定时器主频 80MHz,时钟周期分辨率 12.5ns12.5\text{ns}12.5ns):

  • 低速 10 RPM:
    tp=6010×1320≈4.545 mst_p = \frac{60}{10 \times 1320} \approx 4.545\text{ ms}tp​=10×132060​≈4.545 ms
    定时器计数值 Mt=4.545ms12.5ns=363,636 tick\text{定时器计数值 } M_t = \frac{4.545\text{ms}}{12.5\text{ns}} = 363{,}636 \text{ tick}定时器计数值 Mt​=12.5ns4.545ms​=363,636 tick
    量化分辨率=1363636≈0.0003%✅ 精度高得吓人!\text{量化分辨率} = \frac{1}{363636} \approx 0.0003\% \quad \text{✅ 精度高得吓人!}量化分辨率=3636361​≈0.0003%✅ 精度高得吓人!
  • 高速 600 RPM:
    tp=60600×1320≈75.76 μst_p = \frac{60}{600 \times 1320} \approx 75.76\,\mu\text{s}tp​=600×132060​≈75.76μs
    定时器计数值 Mt=6,061 tick\text{定时器计数值 } M_t = 6{,}061 \text{ tick}定时器计数值 Mt​=6,061 tick
    量化分辨率≈0.017%✅ 依然表现良好\text{量化分辨率} \approx 0.017\% \quad \text{✅ 依然表现良好}量化分辨率≈0.017%✅ 依然表现良好

低速下 M 法有 50% 的离谱误差,T 法却做到了 0.0003%,高下立判。

但 T 法也有自己的阿喀琉斯之踵

它有两个跟精度无关的硬伤:

  1. 高速中断吃不消:转速越高,tpt_ptp​ 越短。如果在极高转速(比如电机空载飙到 3000 RPM)下,tp≈15 μst_p \approx 15\,\mu\text{s}tp​≈15μs,意味着 CPU 每 15 μs15\,\mu\text{s}15μs 就要进一次中断,系统资源全被中断挤占了;
  2. 静止死锁问题:当电机完全停止时,引脚再也没有电平跳变,捕获中断永远不会触发。T 法自己根本没办法区分“电机是在极慢爬行”还是“电机已经停转了”。

问题五:都说是 ±1 量化误差,这个 ±1 到底是什么?

要划出一条清晰的切换分界线,得先搞懂底层的误差机制。都说两种方法的误差“来自 ±1\pm 1±1 量化”,这个 ±1\pm 1±1 到底卡在哪里?

先把两个公式拆开,提取公共项:

ωM=2πC⏟角度刻度×NTs⏟每秒脉冲数ωT=2πC⏟角度刻度×1tp⏟每秒脉冲数\omega_M = \underbrace{\frac{2\pi}{C}}_{\text{角度刻度}} \times \underbrace{\frac{N}{T_s}}_{\text{每秒脉冲数}} \qquad \omega_T = \underbrace{\frac{2\pi}{C}}_{\text{角度刻度}} \times \underbrace{\frac{1}{t_p}}_{\text{每秒脉冲数}}ωM​=角度刻度C2π​​​×每秒脉冲数Ts​N​​​ωT​=角度刻度C2π​​​×每秒脉冲数tp​1​​​

常数项 2πC=2π1320≈0.0048 rad≈0.27∘\frac{2\pi}{C} = \frac{2\pi}{1320} \approx 0.0048\text{ rad} \approx 0.27^\circC2π​=13202π​≈0.0048 rad≈0.27∘。计数每 +1+1+1,轴就转动 0.27∘0.27^\circ0.27∘。

拆开看就很清晰:

  1. 两个公式结构完全相同:转速=角度刻度×每秒脉冲到达率\text{转速} = \text{角度刻度} \times \text{每秒脉冲到达率}转速=角度刻度×每秒脉冲到达率;
  2. 2πC\frac{2\pi}{C}C2π​ 是固定物理常数,没有任何误差。所有误差全都在后面的整数里——M 法数的整数是 NNN(脉冲个数),T 法数的整数是 MtM_tMt​(定时器 tick 数)。两种方法的相对误差,都严格等于 111 除以你数到的那个整数。

M 法的 ±1:卡在窗口边界上的“半个脉冲”

脉冲序列:    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
                  │←────  窗口 T_s  ────→│
                  ↓窗口打开               ↓窗口关闭
               上一个脉冲刚过去,        这个脉冲只来了一半,
               开头损失了"半个"          结尾又损失"半个"

采样窗口何时开闭由 CPU 软件定时器决定,脉冲何时到来由电机物理旋转决定,两者没有共同的时钟基准。边界处不可避免地会被截断,导致计数值总有 ±1\pm 1±1 的起伏。换算到速度上,就是一条固定宽度的速度误差带:

±1 count/10ms=11320×0.01×60≈±4.5 RPM\pm 1 \text{ count} / 10\text{ms} = \frac{1}{1320 \times 0.01} \times 60 \approx \pm 4.5 \text{ RPM}±1 count/10ms=1320×0.011​×60≈±4.5 RPM

无论电机转多快,M 法永远自带 ±4.5 RPM\pm 4.5\text{ RPM}±4.5 RPM 的速度不确定度。在 300 RPM 时它只占 1.5%,而在 10 RPM 爬行时它占了近一半,PI 控制器只能跟着噪音疯狂振动。

T 法的 ±1:卡在时钟节拍间的边沿

定时器刻度:  ──|────|────|────|────|────|──→
                            ↑
                   脉冲边沿真实到达时刻
                   (落在两个刻度中间)
                   硬件只能锁存到整格精度
                   → 时间戳产生最多 1 格 tick 偏差

脉冲到达是连续物理世界的事,而芯片内部定时器是按 80MHz 一格一格走的数字时钟。边沿落在两格中间,锁存下来的时间戳必然带着 ±1 tick=±12.5ns\pm 1\text{ tick} = \pm 12.5\text{ns}±1 tick=±12.5ns 的固定偏差。

把两者的对称性放在一起,一目了然:

维度对比M 法(频率法)T 法(周期法)
被测量的物理量时间窗口内的脉冲个数 NNN(量角度)相邻脉冲间的时钟节拍数 MtM_tMt​(量时间)
±1\pm 1±1 误差根源脉冲边沿被采样窗口边界切断脉冲边沿落在定时器时钟刻度网格之间
绝对不确定度恒定为 ±1\pm 1±1 脉冲计数(≈0.27∘\approx 0.27^\circ≈0.27∘)恒定为 ±1\pm 1±1 个时钟 Tick(=12.5 ns= 12.5\text{ ns}=12.5 ns)
相对误差表达式δM=1N=60C⋅n⋅Ts\delta_M = \frac{1}{N} = \frac{60}{C \cdot n \cdot T_s}δM​=N1​=C⋅n⋅Ts​60​δT=1Mt=C⋅n60⋅fclk\delta_T = \frac{1}{M_t} = \frac{C \cdot n}{60 \cdot f_{\text{clk}}}δT​=Mt​1​=60⋅fclk​C⋅n​
误差随转速变化转速越低越糟糕(n↓  ⟹  N↓n \downarrow \implies N \downarrown↓⟹N↓)转速越高越吃亏(n↑  ⟹  Mt↓n \uparrow \implies M_t \downarrown↑⟹Mt​↓)
优势区间高速区低速区

问题六:一条误差下降、一条上升,切换点就在交叉处

把两条误差曲线随转速 nnn 的变化画出来,答案便自然显现:

误差
↑
│    M 法误差 (1/N)        T 法误差 (1/Mt)
│        ↘                   ↗
│          ↘              ↗
│             ↘        ↗
│                  ✕  ← 理论交叉点:两方法精度等同
│             ↗       ↘
│          ↗             ↘
│       ↗                   ↘
└────────────────────────────────────→ 转速 n
       低速区                 高速区
     (T 法胜出)           (M 法胜出)

令两者相对误差相等:

60C⋅n⋅Ts=C⋅n60⋅fclk\frac{60}{C \cdot n \cdot T_s} = \frac{C \cdot n}{60 \cdot f_{\text{clk}}}C⋅n⋅Ts​60​=60⋅fclk​C⋅n​

两边解出理论交叉转速 nthn_{th}nth​:

nth=60CfclkTsn_{th} = \frac{60}{C} \sqrt{\frac{f_{\text{clk}}}{T_s}}nth​=C60​Ts​fclk​​​

代入我手头这组参数(C=1320C = 1320C=1320,fclk=80MHzf_{\text{clk}} = 80\text{MHz}fclk​=80MHz,Ts=10msT_s = 10\text{ms}Ts​=10ms):

nth=601320×80,000,0000.01≈4066 RPMn_{th} = \frac{60}{1320} \times \sqrt{\frac{80{,}000{,}000}{0.01}} \approx 4066 \text{ RPM}nth​=132060​×0.0180,000,000​​≈4066 RPM

算出这个数字的时候我愣了一下:4066 RPM?
我这颗小电机满打满算极限转速也就 300∼600 RPM300 \sim 600\text{ RPM}300∼600 RPM。

也就是说,在电机的整个机械量程内,单纯比精度,T 法自始至终都在碾压 M 法! 理论交叉点早就飞到外太空去了。

这就引出了最值得探讨的一问:


问题七:既然全程 T 法更准,为什么还要 M 法?

这一问逼着我想明白一个道理:在实际写代码做项目时,精度从来不是唯一的评价指标。

回忆前文提到的 T 法硬伤,M 法在这个系统里其实承担着两个不可替代的兜底任务:

  1. 可靠判停:若电机刹停,T 法没有新的边沿中断,速度会死锁在上一时刻的值;而 M 法窗口内计数值为 0,能瞬间识别静止;
  2. 卸载高转速开销:在高速段,M 法通过 ESP32 PCNT 纯硬件计数,CPU 只管 10ms 读一次,中断开销直接为 0。

因此,实际写代码时最舒服的选择是 M/T 混合策略:

转速 ──→  ┌──────────────────────────────────────────┐
          │           M/T 混合测速策略                │
          │                                          │
          │   低速区        切换阈值        高速区      │
          │  ← T 法 →       N_th        ← M 法 →    │
          │                                          │
          │  0 RPM ─────── 5~10脉冲 ────── 最大 RPM  │
          └──────────────────────────────────────────┘

落地:两个硬件外设,一个简单的判断

由 ESP32 内部两个独立外设协同分工:

  • PCNT:纯硬件正交解码,负责持续累计位置脉冲,并在高速时提供测速;
  • MCPWM Capture:硬件捕捉边沿到来的时间戳,负责低速高精度测速。

1. M 法代码(PCNT 周期读取)

// 定时器周期回调(比如每 10ms 执行一次)
void encoder_timer_cb(void *arg) {
    int pulse_count = 0;
    pcnt_unit_get_count(pcnt_unit, &pulse_count);
    pcnt_unit_clear_count(pcnt_unit);   // 读完清零,重新开始数下一个周期的
    
    // M 法速度计算
    float rpm_m = (float)pulse_count / (CPR * T_S) * 60.0f;
}

2. T 法代码(MCPWM Capture 边沿中断)

// MCPWM Capture 回调(每个编码器有效边沿硬件自动触发)
static bool capture_callback(mcpwm_cap_channel_handle_t cap_chan,
                             const mcpwm_capture_event_data_t *edata,
                             void *user_data) {
    static uint32_t last_capture = 0;
    uint32_t current_capture = edata->cap_value;      // 80MHz 高精度时间戳
    uint32_t delta = current_capture - last_capture;  // 脉冲周期占用的 tick 数量
    last_capture = current_capture;

    if (delta > 0) {
        float period_sec = (float)delta / CAPTURE_CLOCK_HZ;   // 80MHz 时钟
        float rpm_t = 60.0f / (CPR * period_sec);
        atomic_store(&g_rpm_t_method, rpm_t);  // 原子存入共享变量,供控制循环取用
    }
    return false;
}

3. 控制层无缝融合判定

实际落地时,我们根本不需要算复杂的根号公式。M 法窗口内的脉冲数 NNN 本身就是精度的天然指示器:

  • NNN 足够大:说明当前处于高速区,M 法精度足够高,直接用 M 法;
  • NNN 很小:说明 M 法误差过大,立即切换为 T 法计算值;
  • N=0N = 0N=0 且捕获超时:确认电机已经完全停稳。
#define N_THRESHOLD 5   // 经验阈值:N < 5 时 M 法误差会超过 20%

float get_motor_speed(void) {
    int pulse_count = 0;
    pcnt_unit_get_count(pcnt_unit, &pulse_count);
    pcnt_unit_clear_count(pcnt_unit);

    int abs_count = abs(pulse_count);

    if (abs_count == 0 && capture_timeout()) {
        // 1. 静止判断:M 法计数值为 0 且捕获定时器发生超时
        return 0.0f;
    } else if (abs_count < N_THRESHOLD) {
        // 2. 低速工况:采用 T 法高精度转速,方向由 PCNT 计数的正负号提供
        float rpm = atomic_load(&g_rpm_t_method);
        return (pulse_count >= 0) ? rpm : -rpm;
    } else {
        // 3. 高速工况:采用纯硬件 M 法,CPU 零开销
        return (float)pulse_count / (CPR * T_S) * 60.0f;
    }
}

数据流向全景

控制层

软件层

硬件层(零 CPU 开销)

A 相

B 相

A 相

读取 count 寄存器

触发捕获中断

时间戳差值 tp

是(高速)

否(低速)

驱动电机转动

正交编码器
A相 + B相

PCNT 外设
硬件正交解码
自动加减计数

MCPWM Capture
硬件边沿时间戳
80MHz 精度

定时器回调
每 10ms

Capture ISR
每个脉冲边沿

abs(N) >= 阈值?

M 法计算
RPM = N/(CPR×Ts)×60

T 法计算
RPM = 60/(CPR×tp)

最终融合转速 RPM

PI 速度控制器
Error = 设定值 - 实际值

MCPWM 生成 PWM
→ 电机驱动板


这套东西换来了什么

把三种方案放在小车真实的运行工况下比对:

评估工况单用 M 法 (10ms 周期)单用 T 法 (80MHz 捕获)M/T 混合策略 (本文方案)
0 RPM(完全静止)✅ 瞬时识别(N=0N=0N=0)❌ 无法判断(没有脉冲跳变,状态死锁)✅ 瞬时精准判停(N=0N=0N=0 + 超时确认)
1~10 RPM(低速蠕行对齐)❌ 误差 50%∼100%50\% \sim 100\%50%∼100%,电机剧烈抖动✅ 误差 <0.001%< 0.001\%<0.001%,低速极平滑✅ 极平稳爬行,彻底告别低速抽搐
300~600 RPM(高速冲刺)✅ 误差 <1.5%< 1.5\%<1.5%,表现稳定⚠️ 精度很高,但高频中断狂吃 CPU✅ 硬件直接计数,零中断开销
CPU 中断负载极低(仅 10ms 读一次寄存器)极高(高速下每秒数万次中断)全速域极低(低速中断少,高速免中断)
闭环动态响应10ms 刷新率,响应灵敏每次脉冲到达即刷新,响应极快兼顾灵敏响应与高信噪比

落到小车上,变化立竿见影:低速测速精度从原本的 ±50%\pm 50\%±50% 骤升到 ±0.03%\pm 0.03\%±0.03%,PI 控制器输出曲线瞬间平直,电机再也不哼哧哼哧地原地发抖了——这就是折腾这套方案的全部意义。


几个我后来自己补问自己的问题

Q1:切换阈值为什么实际取 5~10 个脉冲,而不是直接算出来的理论交叉点?

理论交叉点算出来是 4066 RPM,早就超过了小电机的物理红线。如果在代码里死板照搬理论,等于全程都用 T 法。但我们还要考虑“静止检测”和“避免高速中断过多”。当 N≥5N \ge 5N≥5 时,±1\pm 1±1 带来的量化误差已经降到了 20%20\%20% 以内,而且随着速度加快误差还会急剧缩小;此时完全可以安心把活交还给 PCNT 硬件,省心省力。

Q2:T 法怎么确认电机真正停了?

通过**“超时判定 + PCNT 计数值为 0”**双保险。如果超过预设时间(比如 50~100ms)都没有收到任何新的边沿捕获,并且 PCNT 读出的 N=0N = 0N=0,就能 100% 确认电机已经彻底停稳。

Q3:为什么用 PCNT 而不是普通的 GPIO 中断计数?

PCNT 是芯片内部的专用硬件计数外设,从引脚跳变到寄存器计数完全走数字逻辑,不需要 CPU 插手。如果用普通的 GPIO 外部中断,电机飞转时 CPU 会被成千上万次中断打得千疮百孔,连正常的控制任务都跑不顺畅。

Q4:为什么测周期用 MCPWM Capture 而不用通用定时器?

MCPWM 的 Capture 子模块带有硬件边沿锁存器,在脉冲跳变到达引脚的瞬间,硬件会自动瞬间冻结并保存当时的计数器时间戳,精度达到 12.5ns12.5\text{ns}12.5ns,完全不会受到软件进中断排队与指令延迟的干扰。

Q5:四倍频在物理上到底是怎么来的?

正交编码器的 A、B 两相输出相差 90° 的方波,在一整个脉冲周期中共有 4 个边沿(A 相上升/下降、B 相上升/下降)。硬件对所有 4 个跳变沿都进行方向判断并计数,原本物理一圈 330 个脉冲的编码器,就能数出 330×4=1320330 \times 4 = 1320330×4=1320 个计数,分辨率直接在硬件层面提升 4 倍。


写在最后

回头看,整个认知链条其实是一环扣一环递进的:

想知道小车转多快 
→ 挂了正交编码器产生脉冲 
→ 脉冲怎么进芯片?(PCNT 纯硬件计数,不占 CPU)
→ 固定时间数个数算速度(M 法最直观,但低速直接爆雷)
→ 能否拉长窗口?(不行,窗口绑定了 PI 控制周期,会导致小车滞后振荡)
→ 改量脉冲时间(T 法实现测速分辨率与控制周期彻底解耦)
→ 深入本质:两个 ±1 到底卡在哪?(一个卡在采样时间窗口边缘,一个卡在离散时钟刻度网格)
→ 误差交叉点算出来在 4000+ RPM,为什么还要 M 法?(判停兜底 + 卸载高转速中断)
→ 两个外设分工协作,软件靠脉冲数 N 无缝自适应切换。

一句话总结:
高速用 PCNT 数脉冲省算力,低速用 Capture 量周期保精度;把测速的时间分辨率与控制的采样周期彻底解耦——全速域兼顾高精度,CPU 还几乎零开销。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐