ESP32 实战教程:用脉冲计数测风扇转速、流量和频率
传感器不会直接告诉你“现在是 1260 RPM”,它通常只会老老实实地吐出一串脉冲。本文让 ESP32 负责数脉冲,再根据时间窗口计算转速、频率或流量。听起来像小学数学,实际会遇到上拉、电平、毛刺、每转脉冲数和版本 API 等一整套工程问题,正好适合把基础打牢。

一、脉冲计数的通用模型
假设霍尔传感器的磁铁每转产生 2 个下降沿,在 1 秒内我们数到 42 个脉冲,那么转速就是:
频率 Hz = 脉冲数 / 时间(秒)
RPM = 频率 × 60 / 每转脉冲数
RPM = 42 × 60 / 2 = 1260
流量计也类似,只是数据手册会给出“每升多少脉冲”或“频率与流量的线性关系”。只要明确单位,代码本身很简单。真正容易出错的是脉冲边沿被重复计数、机械抖动产生额外脉冲、计数器溢出,或者传感器输出是开漏结构而你忘了上拉。
二、硬件输入先做干净
常见的霍尔模块输出 0/3.3 V 方波,可以直接接 GPIO;工业流量计可能输出开漏或 5 V,必须确认电平兼容。开漏输出需要一个上拉电阻,通常 4.7 kΩ 到 10 kΩ,不能把 5 V 信号直接送进 ESP32 GPIO。
如果传感器线很长,建议在输入端增加 RC 滤波、施密特触发器或隔离模块。软件毛刺滤波可以解决短脉冲,但不能替代合理的电气设计。先让示波器看到干净的边沿,再讨论代码里的 debounce,顺序会轻松很多。
三、为什么用 PCNT 而不是普通中断
用 attachInterrupt() 计数当然可以,但高频脉冲、Wi-Fi 通信和文件写入同时出现时,中断服务函数容易变成系统的“兼职会计”。PCNT 是 ESP32 的专用脉冲计数外设,可以硬件计数,并提供毛刺过滤和上下限事件。CPU 只需要在时间窗口结束时读出累计值。
不同芯片和 Arduino Core 对 PCNT 的封装不完全一致。旧版代码常见 driver/pcnt.h,新版 ESP-IDF 还提供 driver/pulse_cnt.h 的对象化驱动。下面先用清晰的测量逻辑说明方法;落地时要根据自己的芯片和 Core 版本选择对应头文件,别把新旧 API 的结构体混成一锅粥。
四、固定时间窗口的测量方法

固定 1000 ms 窗口最容易理解,也方便与手持转速表对照。转速变化很快时,可以使用 100 ms 窗口再做滑动平均;低速场景则适合测量相邻脉冲之间的周期。窗口越短,响应越快但抖动越大;窗口越长,读数平滑但反应迟钝。
下面是旧版 PCNT 驱动风格的示例,适合经典 ESP32 和许多 Arduino Core 2.x 工程:
#include <Arduino.h>
#include "driver/pcnt.h"
constexpr gpio_num_t PULSE_GPIO = GPIO_NUM_34;
constexpr pcnt_unit_t UNIT = PCNT_UNIT_0;
constexpr int PULSES_PER_REV = 2;
void setupPcnt() {
pcnt_config_t cfg = {};
cfg.pulse_gpio_num = PULSE_GPIO;
cfg.ctrl_gpio_num = PCNT_PIN_NOT_USED;
cfg.channel = PCNT_CHANNEL_0;
cfg.unit = UNIT;
cfg.pos_mode = PCNT_COUNT_INC;
cfg.neg_mode = PCNT_COUNT_DIS;
cfg.lctrl_mode = PCNT_MODE_KEEP;
cfg.hctrl_mode = PCNT_MODE_KEEP;
cfg.counter_h_lim = 30000;
cfg.counter_l_lim = -30000;
pcnt_unit_config(&cfg);
pcnt_set_filter_value(UNIT, 100); // 与 tick 有关,按实际波形调整
pcnt_filter_enable(UNIT);
pcnt_counter_pause(UNIT);
pcnt_counter_clear(UNIT);
pcnt_counter_resume(UNIT);
}
float readRpm(uint32_t windowMs) {
int16_t count = 0;
pcnt_counter_clear(UNIT);
delay(windowMs);
pcnt_get_counter_value(UNIT, &count);
float seconds = windowMs / 1000.0f;
return (count / seconds) * 60.0f / PULSES_PER_REV;
}
这里把计数器清零后等待窗口再读取,适合教学和低频设备。生产代码应避免 delay() 长时间阻塞主循环,可以用 millis() 维护窗口,或把计数和计算放入 FreeRTOS 任务。读取和清零之间要考虑并发,必要时使用 PCNT 的暂停/恢复或临界区。
五、计数器溢出与高频输入
PCNT 是有限位宽计数器,窗口太长、输入频率太高时可能溢出。做工程估算时先算最大值:最大频率 × 窗口秒数,确保它小于计数器范围。如果超出,可以缩短窗口、使用溢出事件累计高位,或者直接使用脉冲周期测量。
毛刺滤波阈值也要谨慎。阈值太小,机械抖动会被算进去;阈值太大,真实的高速脉冲会被吞掉。先用示波器测出最短有效脉宽,再给滤波器留一点余量。不要照抄“100”这个数字,因为 PCNT filter 的单位与 APB 时钟 tick 相关,不同芯片和驱动配置下意义可能不同。
六、把结果做成稳定的工程数据
原始 RPM 往往会抖动,可以采用指数移动平均:
float filtered = 0;
float smooth(float input) {
constexpr float alpha = 0.25f;
filtered = alpha * input + (1 - alpha) * filtered;
return filtered;
}
如果传感器的每转脉冲数会改变,例如换了不同齿数的编码盘,不要把它写死在算法深处,放进配置结构并在网页上显示。流量计还要考虑零点漂移和安装方向,必要时用已知体积的水做现场标定,再把系数保存到 NVS。
七、三种场景的计算例子
风扇转速
磁铁每转 2 脉冲,1 秒读到 50 个脉冲,RPM = 1500。若风扇是四极磁环,每转 4 个脉冲,就必须改成 750 RPM。读数相差一倍通常不是 ESP32 数错,而是“每转几个脉冲”理解错了。
水流量
假设流量计标定为 450 脉冲/升,10 秒读到 900 个脉冲,则流量为 2 L/10 s,也就是 12 L/min。现场校准时最好用量筒实测,不要只相信包装上的典型值。
频率计
对于方波信号,1 秒内 1000 个上升沿就是约 1 kHz。要测占空比,还需要同时捕获高电平和低电平持续时间,PCNT 只能告诉你边沿数量,不能代替逻辑分析仪。
八、排错清单
- 一直是 0:检查 GPIO 是否支持输入、传感器是否共地、开漏输出是否上拉;
- 数值翻倍:检查是否同时统计了上升沿和下降沿,或每转脉冲数写错;
- 低速时忽高忽低:延长窗口或使用相邻脉冲周期测量;
- 高速时偏低:滤波阈值过大、计数器溢出或输入电平上升沿太慢;
- 只有接上 USB 才正常:重点查电源、地线和传感器供电噪声;
- 升级 Core 后编译失败:对照当前芯片的
pcnt驱动文档,不要把旧头文件 API 和新对象 API 混用。
九、现场标定和低速测量
实验室里可以拿手转风扇,但交付现场通常要面对不同的叶片、磁铁间距和安装角度。建议先把传感器固定好,给电机设置三个已知档位,分别记录手持转速表和 ESP32 的结果,然后用一个简单的比例系数修正。修正系数不要偷偷写死在代码里,最好放进配置页面,并在串口启动日志中打印出来。
低速时固定窗口可能长时间读到 0,随后突然跳到一个较大的数。这不是“传感器有脾气”,而是窗口内脉冲太少。此时可以改用周期法:记录最近两次有效边沿的时间差 periodUs,频率约为 1000000.0 / periodUs。周期法响应快,但遇到丢脉冲时会暂时保持旧值,所以通常需要同时设置超时清零策略。
uint32_t lastEdgeUs = 0;
float rpmFromPeriod(uint32_t nowUs) {
if (lastEdgeUs == 0) {
lastEdgeUs = nowUs;
return 0;
}
uint32_t period = nowUs - lastEdgeUs;
lastEdgeUs = nowUs;
if (period == 0 || period > 2000000UL) return 0;
float hz = 1000000.0f / period;
return hz * 60.0f / PULSES_PER_REV;
}
周期法的代码可以放在 GPIO 中断或捕获回调里,但回调里只记录时间戳,不要做浮点计算、串口打印和网络请求。把原始时间戳交给任务处理,系统在 Wi-Fi 忙、Flash 写入或网页刷新时仍能保持可预测的响应。
十、把计数结果送到 Web 或 MQTT
测量任务产生的结果可以整理成一个快照结构体:包含当前值、窗口长度、累计脉冲数、最后一次更新时间和传感器状态。Web 页面读取快照,MQTT 任务按固定周期发布。这样“测量”和“展示”解耦,浏览器刷新再频繁也不会改变计数逻辑。
还可以增加异常状态:连续三个窗口没有脉冲时标记 NO_SIGNAL;读数超过机械上限时标记 OVER_RANGE;传感器恢复后记录恢复时间。把状态和数值分开,远端系统就不会把“0 RPM,因为传感器断线”和“电机真的停了”混为一谈。
十一、从一次测量到一个可用仪表盘
下一步可以在 ESP32 上加 WebServer,实时显示 RPM、频率、累计流量和最大值;用 WebSocket 推送数据时,计数任务只负责产生最新快照,网页任务负责发送,避免网络阻塞影响计数。也可以加入异常规则,例如转速低于阈值持续 5 秒就报警,流量突然为零时记录故障时间。
这个项目教会我的最重要的一点是:测量结果从来不是“读到一个数字”这么简单。你要同时说明输入是什么、计数了哪种边沿、窗口多长、单位怎么换算、滤波会不会吞掉有效信号。把这些前提写清楚,数字才真正有工程意义。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号