目录

概述

一、系统接线部分

1.1 硬件清单

1.2 接线方案表

1.3 连接示意图

1.4 具体接线图

二、安装与使用教程

2.1 开源平台-搜索"测速"-代码下载自动打开

2.3 连接 - 验证 - 上传

三、代码讲解部分

3.1 参数与全局变量

3.2 中断采集脉冲

3.3 风扇开关封装

3.4 初始化 setup()

3.5 主循环:取脉冲与临界区

3.6 速度计算与滤波

3.7 风扇联动逻辑

四、项目结果演示

五、各模块技术原理讲解

六、常见问题解答(FAQ)


概述

本文基于 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_THRESHOLDFAN_OFF_DELAY_MS 是风扇联动的两个关键阈值。

pulseCountlastIsrTimevolatile 修饰,因为它们在中断与主循环之间共享,告诉编译器每次都从内存读取、不要优化进寄存器,否则主循环可能读到过期的旧值。

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 为示例值,需按你的实际机械结构标定。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐