零知派ESP32-S3-智能小车控制系统(4)-测速传感器联动风扇控制
目录
概述
本文基于 ESP32-S3,使用红外测速传感器(GPIO4)采集脉冲,经过 EMA 滤波与二次平滑得到速度值,再驱动 GPIO20 上的风扇实现「检测到转动→开风扇,受阻停止→关风扇」的联动逻辑。代码不依赖电机/Web/摄像头等模块,可作为独立测试程序,也可直接移植进整车工程。
一、系统接线部分
1.1 硬件清单
|
序号 |
名称 |
型号/规格 |
数量 |
说明 |
|---|---|---|---|---|
|
1 |
主控板 |
零知派ESP32-S3 |
1 |
注意避开 PSRAM 占用的 GPIO33~37 |
|
2 |
测速传感器 |
红外对射/反射式光电测速模块(带比较器输出 DO) |
1 |
输出数字脉冲信号 |
|
3 |
编码盘 |
槽型/黑白条纹码盘 |
1 |
与电机/轮子同轴 |
|
4 |
L9110 风扇模块 |
L9110 风扇模块 |
1 |
GPIO 直接控制,无需另搭驱动电路 |
|
5 |
杜邦线 |
公对母/公对公 |
若干 |
— |
|
6 |
电源 |
5V/2A |
1 |
模块电源与主控共地 |
1.2 接线方案表
L9110 风扇模块通常引出 4 个脚:VCC、GND,以及两个控制输入(常见标注 INA/INB,也有标 IA/IB 或 A-1A/A-1B)。风扇单向运转,只用一个输入做开关:一个输入脚接 GPIO20 控制,另一个输入脚接 GND。
|
模块 |
模块引脚 |
接到 |
说明 |
|---|---|---|---|
|
测速传感器 |
VCC |
ESP32 3V3 |
部分模块需 5V,看模块标注 |
|
测速传感器 |
GND |
ESP32 GND |
共地 |
|
测速传感器 |
DO(数字输出) |
ESP32 GPIO4 |
上升沿中断采集脉冲 |
|
L9110 风扇模块 |
VCC |
5V |
按模块说明,多数 2.5~12V |
|
L9110 风扇模块 |
GND |
ESP32 GND |
必须与主控共地 |
|
L9110 风扇模块 |
INA(输入 A) |
ESP32 GPIO20 |
HIGH 转,LOW 停 |
|
L9110 风扇模块 |
INB(输入 B) |
ESP32 GND |
接地→固定单向运转 |
GPIO20 在 ESP32-S3 上默认复用为 USB D+,烧录代码时改引脚会拉高,在此期间风扇会持续转动。若上电时该脚未被显式拉低,模块输入悬空可能导致风扇「无故常转」。本文代码已在
setup()中先digitalWrite(FAN_PIN, LOW)接管该脚。
1.3 连接示意图

1.4 具体接线图

二、安装与使用教程
2.1 开源平台-搜索"测速"-代码下载自动打开

2.3 连接 - 验证 - 上传

三、代码讲解部分
本节按功能模块分段讲解,每段代码紧跟说明,便于逐块理解。
3.1 参数与全局变量
// ===== 测速参数 =====
#define PIN_SPEED_SENSOR 4
#define CM_PER_PULSE 0.4045
#define EMA_ALPHA 0.3
#define SPEED_CALC_INTERVAL 100 // 速度计算周期(ms)
#define MIN_PULSE_FOR_SPEED 2 // 最小脉冲数阈值
// ===== 风扇参数 =====
#define FAN_PIN 20
#define SPEED_STOP_THRESHOLD 1.0 // 平滑速度低于此值(cm/s)视为停止
#define FAN_OFF_DELAY_MS 500 // 连续停止多久后才关风扇(防抖)
// ===== 中断相关 =====
volatile unsigned long pulseCount = 0;
volatile unsigned long lastIsrTime = 0;
// ===== 速度计算变量 =====
float currentSpeed = 0;
float smoothedSpeed = 0;
float filteredPulse = 0;
unsigned long lastCalcTime = 0;
// ===== 风扇状态 =====
bool fanState = false;
unsigned long lastMovingTime = 0; // 上次检测到"在转"的时间
参数集中用 #define 管理,方便标定时统一调整。其中 CM_PER_PULSE 是每脉冲对应的距离系数,SPEED_STOP_THRESHOLD 和 FAN_OFF_DELAY_MS 是风扇联动的两个关键阈值。
pulseCount 和 lastIsrTime 用 volatile 修饰,因为它们在中断与主循环之间共享,告诉编译器每次都从内存读取、不要优化进寄存器,否则主循环可能读到过期的旧值。
3.2 中断采集脉冲
void IRAM_ATTR isrHandler() {
unsigned long now = micros();
if (now - lastIsrTime > 1000) { // 1ms 软件去抖
pulseCount++;
lastIsrTime = now;
}
}
测速传感器每产生一个上升沿就进一次中断。micros() 配合 lastIsrTime 做 1ms 软件去抖,避免单个脉冲因机械/电气抖动被重复计数。
IRAM_ATTR 让中断函数常驻 IRAM(片上 RAM),避免运行时还要从 Flash 取指令而出错——这是 ESP32 写中断的标准做法。中断函数要尽量短,所以这里只做计数,速度计算全部放到主循环里。
3.3 风扇开关封装
void setFan(bool on) {
if (fanState == on) return; // 状态没变就不重复操作
fanState = on;
digitalWrite(FAN_PIN, on ? HIGH : LOW);
Serial.println(on ? " >> [风扇] 开启" : " >> [风扇] 关闭");
}
把风扇控制封装成一个函数,统一管理状态。开头的 if (fanState == on) return; 是个小技巧:状态没变就直接返回,既不重复写 GPIO,也不重复打印,串口输出更干净,只在「真正切换」时才提示一次。
3.4 初始化 setup()
void setup() {
Serial.begin(115200);
delay(500);
// ... 省略串口提示打印 ...
pinMode(FAN_PIN, OUTPUT);
digitalWrite(FAN_PIN, LOW); // 上电先确保风扇关闭
fanState = false;
pinMode(PIN_SPEED_SENSOR, INPUT);
// ... 各变量清零 ...
lastCalcTime = millis();
lastMovingTime = millis();
attachInterrupt(digitalPinToInterrupt(PIN_SPEED_SENSOR), isrHandler, RISING);
}
这里有个关键点:一定要先 pinMode(FAN_PIN, OUTPUT) 并 digitalWrite(FAN_PIN, LOW) 接管 GPIO20。GPIO20 在 ESP32-S3 上默认复用为 USB D+,不初始化时电平不确定,会导致风扇上电常转(这正是单独跑测速代码时风扇乱转的原因)。
attachInterrupt 注册上升沿中断,RISING 表示只在电平由低到高的瞬间触发。
3.5 主循环:取脉冲与临界区
void loop() {
unsigned long now = millis();
if (now - lastCalcTime >= SPEED_CALC_INTERVAL) {
noInterrupts();
unsigned long raw = pulseCount;
pulseCount = 0;
interrupts();
// ... 后续计算 ...
}
}
主循环用 millis() 非阻塞计时,每 SPEED_CALC_INTERVAL(100ms)处理一次,不用 delay 卡死程序。
noInterrupts()/interrupts() 包住「取出 + 清零」pulseCount 这两步,构成临界区,保证它们原子完成——否则可能在取值和清零之间又来一个中断,导致这个脉冲被漏计。
3.6 速度计算与滤波
if (raw >= MIN_PULSE_FOR_SPEED) {
// EMA 滤波
filteredPulse = (EMA_ALPHA * raw) + ((1.0 - EMA_ALPHA) * filteredPulse);
// 换算成 cm/s
float newSpeed = (filteredPulse * (1000.0 / SPEED_CALC_INTERVAL)) * CM_PER_PULSE;
// 二次平滑
smoothedSpeed = (0.7 * newSpeed) + (0.3 * smoothedSpeed);
currentSpeed = newSpeed;
} else {
// 脉冲太少,逐渐衰减(视为接近停止)
filteredPulse *= 0.5;
currentSpeed = filteredPulse * (1000.0 / SPEED_CALC_INTERVAL) * CM_PER_PULSE;
smoothedSpeed *= 0.7;
}
这是核心算法,分两级滤波:
-
EMA(指数移动平均)滤波:
filteredPulse = α·raw + (1-α)·旧值,α=0.3 偏向历史值,平滑掉单个周期里的脉冲跳变。 -
速度换算:脉冲数 ×(1000/周期)得到「脉冲/秒」频率,再 ×
CM_PER_PULSE得到 cm/s。 -
二次平滑
smoothedSpeed:在 EMA 之上再做一层加权(0.7 新 + 0.3 旧),专门用于显示和风扇判定,数值更稳。
当脉冲数低于阈值时,走 else 分支让数值乘系数逐渐衰减,平滑地趋近于「停止」,而不是骤降为 0。
3.7 风扇联动逻辑
if (smoothedSpeed >= SPEED_STOP_THRESHOLD) {
lastMovingTime = now; // 在转:刷新时间戳
setFan(true);
} else {
// 连续停止超过延时阈值才关风扇(防抖动误关)
if (now - lastMovingTime >= FAN_OFF_DELAY_MS) {
setFan(false);
}
}
这是联动的关键。判断依据用平滑速度而非原始脉冲,避免偶尔丢一两个脉冲就让风扇抖动。
-
检测到转动(平滑速度 ≥ 阈值):立即开风扇,并刷新
lastMovingTime。 -
速度低于阈值:不立刻关,要连续低速持续超过
FAN_OFF_DELAY_MS(500ms)才真正关闭。这个「关闭防抖」避免脉冲间断时风扇一闪一闪。
四、项目结果演示
零知派ESP32-S3 演示 测速+风扇功能
烧录后打开串口监视器,典型输出如下:
========================================
ESP32-S3 测速+风扇联动测试
GPIO4=测速 GPIO20=风扇
========================================
初始化完成:检测到转动->开风扇,受阻停止->关风扇
脉冲=0 瞬时=0.00 cm/s 平滑=0.00 cm/s 风扇=关
脉冲=8 瞬时=9.71 cm/s 平滑=6.80 cm/s 风扇=关
>> [风扇] 开启
脉冲=12 瞬时=13.84 cm/s 平滑=11.30 cm/s 风扇=开
脉冲=11 瞬时=13.10 cm/s 平滑=12.62 cm/s 风扇=开
脉冲=0 瞬时=6.55 cm/s 平滑=8.83 cm/s 风扇=开
脉冲=0 瞬时=3.27 cm/s 平滑=6.18 cm/s 风扇=开
>> [风扇] 关闭
脉冲=0 瞬时=0.41 cm/s 平滑=0.43 cm/s 风扇=关
可以看到:转动编码盘后速度上升并触发风扇开启;卡住码盘后速度逐级衰减,延时一小段后风扇关闭,整个过程无抖动反复。
五、各模块技术原理讲解
红外测速原理:编码盘随轴转动,对射/反射光电模块在码盘缝隙处通断,比较器在通断瞬间输出方波。转得越快,单位时间脉冲越多,频率正比于转速。
外部中断脉冲计数:相比轮询 digitalRead,中断在脉冲到来时立即响应、不丢边沿,CPU 也无需空转等待,是高频脉冲采集的标准做法。RISING 表示只在上升沿触发。
EMA(指数移动平均)滤波:y = α·x + (1-α)·y_prev。α 越小越平滑但响应越慢,α 越大越灵敏但越抖。本项目两级滤波兼顾稳定与响应。
软件去抖:机械/电气抖动会在一次真实脉冲附近产生多个杂边沿,用「两次中断时间间隔须大于 1ms」过滤,等效于一个低通门限。
L9110 风扇模块:模块内部含 H 桥功率管,能直接带动风扇。对外引出两个逻辑输入(INA/INB),输入的高低组合决定风扇的通断与转向。本项目风扇单向运转,固定一路输入接地、另一路(INA)接 GPIO20 当开关:GPIO20 给高电平→风扇转,给低电平→风扇停。MCU 只提供逻辑电平,功率驱动和续流保护都在模块内部,所以不用再外搭三极管/MOS 和续流二极管。
GPIO20 默认电平问题:ESP32-S3 的 GPIO19/20 默认复用为 USB 功能,未初始化时电平不确定,可能让风扇上电常转。务必在 setup() 中 pinMode 为输出并先拉低接管该脚。
六、常见问题解答(FAQ)
Q1:只跑测速代码时,GPIO20 的风扇为什么会无故转? A:测速代码里没有初始化 GPIO20。该脚在 ESP32-S3 上默认复用为 USB D+,未被显式拉低时电平不确定,使 L9110 风扇模块的输入悬空,被误触发导致风扇常转。本文代码在 setup() 里先 pinMode(FAN_PIN, OUTPUT) 并 digitalWrite(FAN_PIN, LOW) 接管,问题即解决。
Q2:风扇频繁开/关、抖个不停? A:可能是判定阈值或防抖没设好。适当调大 FAN_OFF_DELAY_MS(如 800~1000ms),并根据实测转速合理设置 SPEED_STOP_THRESHOLD,让正常转速明显高于阈值。
Q3:转动码盘但脉冲一直是 0? A:检查测速模块 DO 是否接到 GPIO4、模块供电是否正确、码盘是否真正遮挡到光路;用万用表/示波器确认转动时 DO 有电平跳变;确认 attachInterrupt 用的是 RISING,若模块输出极性相反可改 FALLING。
Q4:风扇接上 L9110 风扇模块后 ESP32-S3 复位或烧录失败? A:多半是风扇启动电流拉垮供电或干扰。确认模块的电源与 ESP32 共地;模块电源尽量独立供电、不要全靠主控的 5V 硬扛;电源处加退耦电容。另外烧录时 GPIO20 复用 USB,若发现下载异常,可在烧录时先断开模块的 INA 接线。
Q5:能不能改成「单次遮挡」而不是「持续转动」来控制? A:可以。把判定改成直接 digitalRead(PIN_SPEED_SENSOR) 读电平,高/低对应通/阻,而不走速度计算这套逻辑——适用于物体经过即触发的场景。
Q6:为什么用 smoothedSpeed 判断而不是直接用 raw 脉冲? A:原始脉冲偶尔丢一两个就会跳变,直接用会让风扇闪烁。平滑速度滤掉了瞬时波动,判定更稳定。
Q7:CM_PER_PULSE 这个系数怎么定? A:等于「轮子周长 ÷ 码盘每圈脉冲数」。先量轮径算周长,数清码盘缝隙数,相除即得;本文 0.4045 为示例值,需按你的实际机械结构标定。
更多推荐


所有评论(0)