正交编码器与M/T混合测速学习笔记
从一个最朴素的需求说起:ESP32 编码器测速与 M/T 混合闭环实战
想让机器人走得稳,就得用 PID 闭环控制轮速。闭环听起来玄乎,拆开就一句话:你得先知道轮子现在转多快,才能知道该加油门还是收油门。
目标速度是软件里设的,好办。但“现在转多快”是物理世界的状态,软件看不见,只能靠传感器。这就是为什么电机尾巴上要拖几根线——正交编码器。
原理不难:码盘上刻了一圈缝,电机一转,输出引脚上的电压就在 0V 和 3.3V 之间反复横跳,输出两路相位差 90° 的方波:
┌──┐ ┌──┐ ┌──┐ ┌──┐
A ─┘ └───┘ └───┘ └───┘ └── (方波)
┌──┐ ┌──┐ ┌──┐ ┌──
B ───┘ └───┘ └───┘ └───┘ └ (滞后 90°)
────────────────────────────→ 正转方向
这串方波里其实藏着三个关键信息:
- 转速(频率):单位时间内方波脉冲越多,说明转得越快;
- 转向(相位):A 相超前 B 相为正转,B 相超前 A 相则为反转;
- 位置(累计计数):脉冲累加的总量,直接对应轴旋转过的绝对物理角度。
我手头这颗小电机 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 19 (B相) 为低电平 →\rightarrow→
- GPIO 18 (A相) 出现下降沿时:
- 若 GPIO 19 (B相) 为高电平 →\rightarrow→
count + 1 - 若 GPIO 19 (B相) 为低电平 →\rightarrow→
count - 1
- 若 GPIO 19 (B相) 为高电平 →\rightarrow→
- (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⋅Ts2π⋅N(rad/s)
或换算成 RPM:
RPMM=NC⋅Ts×60\text{RPM}_M = \frac{N}{C \cdot T_s} \times 60RPMM=C⋅TsN×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 ms | 2.2 | ≈50%\approx 50\%≈50% | 10 ms (极低延迟) | 响应极快,但测速反馈乱跳,小车低速发抖 |
| 100 ms | 22 | ≈4.5%\approx 4.5\%≈4.5% | 100 ms (已有感知) | 低速平稳度改善,但遇到外力阻挡时反应迟缓 |
| 500 ms | 110 | ≈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⋅tp2π(rad/s)
或换算成 RPM:
RPMT=60C⋅tp{RPM}_T = \frac{60}{C \cdot t_p}RPMT=C⋅tp60
其中 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 法也有自己的阿喀琉斯之踵
它有两个跟精度无关的硬伤:
- 高速中断吃不消:转速越高,tpt_ptp 越短。如果在极高转速(比如电机空载飙到 3000 RPM)下,tp≈15 μst_p \approx 15\,\mu\text{s}tp≈15μs,意味着 CPU 每 15 μs15\,\mu\text{s}15μs 就要进一次中断,系统资源全被中断挤占了;
- 静止死锁问题:当电机完全停止时,引脚再也没有电平跳变,捕获中断永远不会触发。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π×每秒脉冲数TsNωT=角度刻度C2π×每秒脉冲数tp1
常数项 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∘。
拆开看就很清晰:
- 两个公式结构完全相同:转速=角度刻度×每秒脉冲到达率\text{转速} = \text{角度刻度} \times \text{每秒脉冲到达率}转速=角度刻度×每秒脉冲到达率;
- 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⋅Ts60 | δT=1Mt=C⋅n60⋅fclk\delta_T = \frac{1}{M_t} = \frac{C \cdot n}{60 \cdot f_{\text{clk}}}δT=Mt1=60⋅fclkC⋅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⋅Ts60=60⋅fclkC⋅n
两边解出理论交叉转速 nthn_{th}nth:
nth=60CfclkTsn_{th} = \frac{60}{C} \sqrt{\frac{f_{\text{clk}}}{T_s}}nth=C60Tsfclk
代入我手头这组参数(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 法在这个系统里其实承担着两个不可替代的兜底任务:
- 可靠判停:若电机刹停,T 法没有新的边沿中断,速度会死锁在上一时刻的值;而 M 法窗口内计数值为 0,能瞬间识别静止;
- 卸载高转速开销:在高速段,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;
}
}
数据流向全景
这套东西换来了什么
把三种方案放在小车真实的运行工况下比对:
| 评估工况 | 单用 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 还几乎零开销。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)