零知派ESP32-S3-智能小车控制系统(4.2)-红外双目跟随模块使用

概述

本文基于 ESP32-S3,使用两个红外对射避障传感器(GPIO20 / GPIO4)分别检测车头左右两侧目标,经过 INPUT_PULLUP 输入处理得到稳定的双目触发信号,再驱动跟随逻辑实现「单侧触发 → 转向对准、双侧触发 → 停车、目标丢失 → 停车等待」的跟随行为。整套功能封装为独立的 FollowControl 类,通过 CarDrive 统一电机接口驱动底盘,不依赖 Web、摄像头等其他模块,可作为独立测试程序,也可直接移植进整车工程。

需要特别说明本次改动绕不开的引脚取舍:ESP32-S3 版整车方案新增了摄像头模块(OV2640,并行接口),光是 XCLKPCLKVSYNCHREFSIODSIOC 加 8 位数据线就占用了十几个 GPIO,叠加电机驱动、舵机、超声波、循迹等原有模块的引脚需求后,整板空闲 GPIO 已不够用。综合评估后项目做了取舍:去掉霍尔编码器测速与 PID 直线行驶功能,以及风扇灭火模块,红外双目跟随模块复用这两个模块原本占用的引脚------本文里红外传感器接的 GPIO20 / GPIO4,在原方案中分别是霍尔编码器信号脚和风扇驱动控制脚。如果你的项目仍保留测速和灭火功能,需自行重新规划一组空闲 GPIO 给红外跟随模块,不能照抄本文引脚号。

这篇教程会把传感器原理、模块电路、类封装设计、调试过程和遇到的坑完整梳理一遍,包括项目里这个模块走弯路的调试历程。


一、系统接线部分

1.1 硬件清单

名称 数量 说明
ESP32-S3 开发板 1 建议选带 USB CDC 的型号,方便直接用 USB 口调试串口
红外双目跟随模块 2 左、右各一个,检测距离 2~30cm 可调
电机驱动板 + TT 直流电机 1 套 沿用原有底盘方案
杜邦线若干、小车底盘、电池 --- 常规耗材

本方案中不再包含霍尔编码器测速模块和风扇+火焰传感器灭火模块,原因见「概述」中的引脚冲突说明。

1.2 接线方案表

每个红外跟随模块有 3 个引脚:

模块 引脚 ESP32-S3 开发板引脚 说明
左红外传感器 VCC 5V 电源正极
左红外传感器 GND GND 电源地
左红外传感器 OUT GPIO20 信号输出,原霍尔编码器信号脚(IR_LEFT_PIN
右红外传感器 VCC 5V 电源正极
右红外传感器 GND GND 电源地
右红外传感器 OUT GPIO4 信号输出,原风扇驱动控制脚(IR_RIGHT_PIN

1.3 连接示意图

两个红外传感器在车头两侧呈八字形安装,传感器正前方偏内约 15°,与车身中轴线形成一定夹角:

左传感器检测到目标,说明目标在左侧,小车左转对准;右传感器检测到目标,说明目标在右侧,小车右转对准;两侧同时检测到,说明目标已在正前方且距离很近,停车。如果左右接反,readLeft()readRight() 的含义会颠倒,导致目标在右侧时车反而向左转,这也是最常见的接线故障。

1.4 具体接线图

引脚定义与工程中 config.h 完全一致:

复制代码
// config.h(红外跟随相关节选,ESP32-S3 版)
// 注意:GPIO20 / GPIO4 原本分配给霍尔编码器测速与风扇模块,
// 现因摄像头占用大量引脚,改为红外跟随模块复用
#define IR_LEFT_PIN   20    // 左侧红外传感器 OUT(原编码器信号脚)
#define IR_RIGHT_PIN  4     // 右侧红外传感器 OUT(原风扇驱动脚)

// ================= 跟随参数 =================
#define FOLLOW_SPEED_TURN 170   // 跟随转向速度

关于引脚选择的说明。 ESP32-S3 上 GPIO0GPIO3GPIO45GPIO46 是芯片启动模式的 strapping 引脚,不建议接常态外设------本方案中这几个脚已被摄像头并行数据线占用,红外跟随模块已予以回避。GPIO19 / GPIO20 在很多板子上是 ESP32-S3 的原生 USB D-/D+,但若开发板通过独立 USB 转串口芯片下载、不依赖原生 USB CDC,这两个脚即可当普通 GPIO 使用------本方案中 GPIO20 用作红外左输入、GPIO19 用于电机驱动,均实测可用。GPIO4 是常规数字 IO,从已下线的风扇模块腾出,用作红外右输入没有问题。


二、安装与使用教程

2.1 开源平台-搜索"红外双目"-代码下载自动打开

2.3 连接 - 验证 - 上传


三、代码讲解部分

3.1 独立测试程序(ESP32-S3 版)

下面这个程序只依赖两个红外传感器,不需要电机、超声波等其他模块,在接入整车工程之前先用它验证传感器的方向和灵敏度。引脚号与 config.h 保持一致(左 GPIO20、右 GPIO4),烧录后建议打开串口监视器,方便直接用 USB 口看输出。

复制代码
/**************************************************************************************
 * 文件: IR_Follow_Standalone_Test_ESP32S3.ino
 * 平台:ESP32-S3 (Arduino-ESP32 框架)
 * 功能:红外双目跟随模块 独立测试程序
 *       引脚定义与主工程 config.h 完全一致
 **************************************************************************************/

#define IR_LEFT_PIN   20    // 左侧红外传感器 OUT
#define IR_RIGHT_PIN  4     // 右侧红外传感器 OUT

void setup()
{
    Serial.begin(115200);
    delay(200);
    pinMode(IR_LEFT_PIN, INPUT_PULLUP);
    pinMode(IR_RIGHT_PIN, INPUT_PULLUP);

    Serial.println(F("========================================"));
    Serial.println(F(" 红外双目跟随模块 独立测试程序 (ESP32-S3)"));
    Serial.println(F("========================================"));
    Serial.println(F("L=1 表示左传感器检测到物体,R=1 表示右传感器检测到物体"));
}

void loop()
{
    int leftVal  = digitalRead(IR_LEFT_PIN);
    int rightVal = digitalRead(IR_RIGHT_PIN);

    // 传感器检测到物体输出 LOW,取反后 1 表示检测到
    bool leftDetected  = (leftVal == LOW);
    bool rightDetected = (rightVal == LOW);

    Serial.print(F("L="));
    Serial.print(leftDetected ? 1 : 0);
    Serial.print(F(" R="));
    Serial.print(rightDetected ? 1 : 0);

    if (leftDetected && rightDetected) {
        Serial.println(F("  → 停车(目标居中且已近)"));
    } else if (leftDetected && !rightDetected) {
        Serial.println(F("  → 左转(目标偏左)"));
    } else if (!leftDetected && rightDetected) {
        Serial.println(F("  → 右转(目标偏右)"));
    } else {
        Serial.println(F("  → 停车(目标丢失,等待)"));
    }

    delay(100);
}

3.2 FollowControl 类定义(FollowControl.h)

FollowControl.h 是整套跟随功能的接口层。它对外只暴露四个公有方法:begin() 负责引脚初始化与状态清零,run() 供主循环每帧调用、执行一次完整的感知与决策,stop() 退出跟随模式并停车,isActive() 供上层查询当前是否处于跟随态。感知部分由两个私有方法 readLeft() / readRight() 完成,对调用方屏蔽了 GPIO 操作的细节。私有成员变量 isLostlostStartTime 记录目标丢失的起始时刻,为后续扩展(例如丢失超时后触发旋转搜索)预留接口。头文件本身不出现任何具体引脚号,引脚全部集中在 config.h 里管理,更换引脚时只需修改宏定义,类代码无需改动。

复制代码
#ifndef FOLLOW_CONTROL_H
#define FOLLOW_CONTROL_H

#include <Arduino.h>
#include "config.h"
#include "SpeedDrive.h"

class FollowControl {
public:
    void begin();   // 初始化引脚与状态
    void run();     // 主循环每帧调用:读传感器 → 决策 → 驱动电机
    void stop();    // 退出跟随模式,停车
    bool isActive();

private:
    bool active = false;
    unsigned long lostStartTime = 0;  // 记录目标丢失起始时刻(预留扩展用)
    bool isLost = false;

    bool readLeft();   // 左传感器是否检测到目标
    bool readRight();  // 右传感器是否检测到目标
};

extern FollowControl Follower;

#endif

3.3 FollowControl 跟随逻辑实现(FollowControl.cpp)

实现文件把感知与决策放在同一个类里,逻辑简洁清晰。begin() 把两个 IR 引脚配置为 INPUT_PULLUP(无物体时保持 HIGH,避免悬空导致误触发),并清零所有状态标志。readLeft() / readRight() 把「检测到物体输出 LOW」翻译成布尔 true,调用方无需关心原始电平逻辑。run() 每帧读取双目信号,按四种组合决策:双侧触发停车、左侧触发左转、右侧触发右转、双侧丢失则停车并记录丢失起始时刻。stop() 退出跟随模式时同步停车并重置标志,确保退出状态干净。

复制代码
#include "FollowControl.h"

FollowControl Follower;

void FollowControl::begin() {
    // INPUT_PULLUP:无物体时保持 HIGH,避免悬空导致误触发
    pinMode(IR_LEFT_PIN, INPUT_PULLUP);
    pinMode(IR_RIGHT_PIN, INPUT_PULLUP);

    active = false;
    isLost = false;
    lostStartTime = 0;

    Serial.println("[跟随] 红外双目模块初始化完成");
}

bool FollowControl::readLeft() {
    // 传感器检测到物体输出 LOW
    return digitalRead(IR_LEFT_PIN) == LOW;
}

bool FollowControl::readRight() {
    return digitalRead(IR_RIGHT_PIN) == LOW;
}

void FollowControl::run() {
    if (!active) {
        active = true;
        isLost = false;
        lostStartTime = 0;
        Serial.println("[跟随] 模式启动");
    }

    bool leftDetected  = readLeft();
    bool rightDetected = readRight();

    if (leftDetected && rightDetected) {
        // 目标居中且已近 -> 停车
        CarDrive.stop();
        isLost = false;
        Serial.println("[跟随] 目标居中 -> 停车");
    }
    else if (leftDetected && !rightDetected) {
        // 目标偏左 -> 左转
        CarDrive.run(-FOLLOW_SPEED_TURN, FOLLOW_SPEED_TURN);
        isLost = false;
        Serial.println("[跟随] 目标偏左 -> 左转");
    }
    else if (!leftDetected && rightDetected) {
        // 目标偏右 -> 右转
        CarDrive.run(FOLLOW_SPEED_TURN, -FOLLOW_SPEED_TURN);
        isLost = false;
        Serial.println("[跟随] 目标偏右 -> 右转");
    }
    else {
        // 两侧都未检测到 -> 丢失目标,停车等待
        if (!isLost) {
            isLost = true;
            lostStartTime = millis();
        }
        CarDrive.stop();
        Serial.println("[跟随] 目标丢失 -> 停车等待");
    }
}

void FollowControl::stop() {
    if (active) {
        CarDrive.stop();
        active = false;
        isLost = false;
        Serial.println("[跟随] 模式停止");
    }
}

bool FollowControl::isActive() {
    return active;
}

丢失后的行为扩展:原地旋转搜索

当前实现的丢失策略是停车等待,isLostlostStartTime 两个成员变量已经记录了丢失的起始时刻。如果需要在目标丢失超过一定时间后触发原地旋转搜索,只需在 run()else 分支里根据 millis() - lostStartTime 判断超时,然后驱动电机旋转即可,其余代码不受影响。下面是一段最简单的扩展示例:

复制代码
// FollowControl.cpp --- run() 里 else 分支的扩展写法
// 需要在 config.h 中额外定义:
//   #define FOLLOW_LOST_TIMEOUT_MS  2000   // 停车等待时长(ms),超过后开始搜索
//   #define FOLLOW_SEARCH_SPEED     120    // 搜索旋转速度
else {
    if (!isLost) {
        isLost = true;
        lostStartTime = millis();
    }

    unsigned long lostMs = millis() - lostStartTime;

    if (lostMs < FOLLOW_LOST_TIMEOUT_MS) {
        // 丢失时间较短:先停车等待,目标可能只是短暂晃出视野
        CarDrive.stop();
        Serial.println("[跟随] 目标丢失 -> 停车等待");
    } else {
        // 丢失时间超过阈值:原地旋转搜索
        CarDrive.run(FOLLOW_SEARCH_SPEED, -FOLLOW_SEARCH_SPEED);
        Serial.println("[跟随] 目标丢失超时 -> 旋转搜索");
    }
}

调试历程

问题现象 原因分析 解决方法
上电后串口读数持续乱跳 0/1,传感器前无遮挡也频繁误触发 引脚模式最初用的是普通 INPUT,没有上拉;传感器未接时 GPIO 悬空,引脚电平随环境噪声漂移 改为 INPUT_PULLUP,无物体时引脚稳定保持 HIGH,误触发消失
跟随方向反了,目标在左侧时车向右转 GPIO20 接的是右传感器、GPIO4 接的是左传感器,与 config.hIR_LEFT_PIN / IR_RIGHT_PIN 的定义左右对调了 对调两根杜邦线,用独立测试程序确认「手挡左侧 → L=1」后再接整车
对准时来回抖动,小车在目标正前方摆个不停 FOLLOW_SPEED_TURN=170 转向力度偏大,每次转向都过冲,矫枉过正地触发另一侧 适当降低转向速度,同时检查两传感器安装间距是否过大(双触发区间过窄会加剧抖动)
进入跟随模式后小车完全不响应,串口无任何输出 run() 未被主循环周期性调用,或上层发送 "G" 命令后没有持续触发 Follower.run() 确认主循环里跟随模式激活期间每帧都调用 Follower.run(),而不是只在收到命令时调用一次

参数调优指南

问题现象 可能原因 调整方案
对准时过冲,来回抖动 FOLLOW_SPEED_TURN 过大 适当降低转向速度,建议从 140 开始逐步上调
转向太慢,目标侧移后跟不上 FOLLOW_SPEED_TURN 过小 适当提高转向速度
双触发区间太窄,小车总在目标正前方左右摆 两传感器安装间距过大 缩小两传感器之间的横向间距
传感器始终触发,无法进入停车状态 灵敏度过高,探测范围过大 逆时针调节电位器,降低灵敏度
目标很近才能触发,跟随距离过短 灵敏度过低 顺时针调节电位器,提高灵敏度
阳光直射下频繁误触发 环境红外分量干扰接收管 逆时针调节电位器降低灵敏度,并给模块加装黑色遮光外壳

与避障模式(B 命令)的功能共用说明

同一对 IR 传感器在跟随与避障两种模式下复用,底层读取接口相同,上层决策逻辑完全相反:

对比项 跟随模式(G) 避障辅助(B)
传感器安装推荐角度 正前方,偏内 ±15° 斜前方 45°/135°
触发含义 目标在那一侧,应转向靠近 那一侧有障碍,应转向远离
转向决策 转向触发侧(左触发 → 左转) 转离触发侧(左触发 → 右转)
速度控制 慢速跟随,双触发停车 紧急规避,直接转向

readLeft() / readRight() 的读取逻辑在两种模式下完全相同,上层行为由各自的控制类单独实现,体现了感知与决策分离的设计价值------这也是本次去掉编码器测速和风扇灭火模块、把引脚腾给红外跟随时,改动量能控制在「只改 config.h 宏定义」这一层的原因。


四、项目结果演示

零知派ESP32-S3 演示 双目跟随 功能

零知派ESP32-S3 演示 双目跟随 功能

演示内容建议包括以下几段:

  • 独立测试程序串口输出。 用手在左右传感器前方交替遮挡,展示 L= / R= 状态实时变化,以及对应的判断结果(左转/右转/停车/丢失等待)。

  • 灵敏度调节演示。 用螺丝刀旋转电位器,观察触发距离随调节方向的变化。

  • 整车跟随效果。 发送 "G" 命令进入跟随模式,手持目标物在小车前方移动,展示小车能够平滑跟随、目标靠近时停车、目标离开视野后停车等待的完整过程。

  • 引脚复用后的整车联调。 由于本方案去掉了测速和灭火模块,建议同时演示一次完整上电自检,确认摄像头、循迹、超声波云台、红外跟随几个保留模块之间没有引脚冲突或相互干扰。


五、模块技术原理讲解

红外双目跟随模块使用的是主动式红外避障传感器,每个传感器内部集成了一个红外发射管(发射红外光)和一个红外接收管(接收反射回来的红外光)。这部分硬件原理与主控芯片无关,STM32 也好、ESP32-S3 也好,模块本身的工作方式完全一样。

5.1 红外发射+光电接收

  • 红外发射管由红外发光二极管组成发光体,用红外辐射效率高的材料(常用砷化镓)制成 PN 结,正向偏压向 PN 结注入电流激发红外光。

  • 红外接收管将红外线光信号变成电信号,其核心部件是一个特殊材料的 PN 结,随着红外光强度的增加,电流也随之增大。

核心检测原理:发射管持续发出 940nm 红外脉冲光,目标物体反射红外光(反射率与目标材质、颜色相关),接收管检测反射光强度,LM393 比较器将模拟信号转为数字电平输出。

不同表面对红外光的反射特性:

表面特性 红外反射 接收管状态 输出电平
浅色/光滑表面 强反射 导通 LOW(检测到物体)
深色/粗糙表面 弱反射/吸收 截止 HIGH(未检测到)
黑色物体 几乎全部吸收 截止 HIGH(未检测到)

当探测到障碍物在检测范围内时,模块输出低电平(LOW)。该模块检测距离为 2~30cm,检测角度约 35°,距离可通过电位器调节。

注意: 不同厂家、不同批次的模块,输出电平与检测状态的对应关系可能相反。本项目实测确认:检测到物体 → 输出 LOW(0),无物体 → 输出 HIGH(1)。代码中 readLeft() / readRight()== LOW 判断即基于此结论,如果你的模块行为相反,将判断改为 == HIGH 即可。

5.2 LM393 比较器原理

LM393 是一款双差分比较器 IC,内部包含两个独立的比较器。比较器对两路输入电压进行比较:当同相输入端(+)电压高于反相输入端(−)电压时,比较器输出高电平;当同相输入端(+)电压低于反相输入端(−)电压时,输出低电平。

内部电路上,Q1、Q2 构成同相输入端的输入级,Q3、Q4 构成反相输入端的输入级,Q5、Q6 用作电流镜,Q7、Q8 为开关级。各级晶体管均由恒流源供电。一旦同相输入端的输入电压高于反相输入端的电压,输出端晶体管 Q8 就会截止,通过上拉电阻引出高电平;反之,同相输入端电压等于或低于反相输入端电压时,输出端晶体管导通,引出低电平。电压变化发生在反相输入端还是同相输入端对开关功能没有影响,唯一关键的因素是两个输入端之间的电压差值。

5.3 电位器调节原理

模块上蓝色方形可调 3362 电位器本质上是可调分压器,旋转旋钮改变分压比例,从而改变送入 LM393 比较器的参考电压(阈值):

调节方向 参考电压变化 检测距离变化
顺时针旋转 阈值降低 检测距离增大(更灵敏)
逆时针旋转 阈值升高 检测距离减小(更不灵敏)

5.4 单路电路原理

典型的 IR333 + PT333 + LM393 红外避障模块,单路电路核心元件:

元件 作用
100Ω 限流电阻 R3 限制 IR333 发射管电流,通常 100Ω ~ 200Ω
10kΩ 上拉电阻 R5 LM393 集电极开路输出需要上拉到 VCC,通常 10kΩ
10kΩ 电位器 R2 调节比较器参考阈值电压,通常 10kΩ 3362 型

5.5 双目探测逻辑

两个红外传感器在车头两侧呈八字形安装:左传感器检测到目标意味着目标在左侧,小车应左转对准;右传感器检测到目标意味着目标在右侧,小车应右转对准。

左传感器 右传感器 判断结果 执行动作
检测到(LOW) 未检测(HIGH) 目标在左侧 左转
未检测(HIGH) 检测到(LOW) 目标在右侧 右转
检测到(LOW) 检测到(LOW) 目标在正前方 停车(目标已近)
未检测(HIGH) 未检测(HIGH) 目标丢失 停车等待

如果方向反了,小车会朝着远离目标的方向转,永远跟不住。用独立测试程序确认「手挡左传感器 → 串口显示 L=1」是最快的方向验证方法。


六、常见问题解答(FAQ)

Q1:传感器接好了,跟随时小车原地不动或方向反了,是什么问题?

首先用独立测试程序检查:把手放在左侧传感器前,串口显示的是 L=1 还是 R=1。如果显示 R=1,说明 GPIO20GPIO4 接反了,对调两根杜邦线即可。如果双侧都是 0,先排查供电是否正常,再顺时针旋转电位器提高灵敏度。

Q2:红外传感器在阳光下失效怎么办?

该模块对环境光有一定适应性,但在强阳光直射下,阳光中的红外分量会干扰接收管,导致误触发。解决方法:逆时针调节电位器降低灵敏度,同时给传感器加装遮光外壳(可用黑色热缩管套住模块两侧)。

Q3:进入跟随模式后小车完全没有反应,串口也没有输出,是什么问题?

FollowControl::run() 需要在主循环里持续被调用,而不是只在收到 "G" 命令时执行一次。检查主循环逻辑,确认跟随模式激活期间每帧都执行了 Follower.run();同时确认 Follower.begin() 已在 setup() 里调用过。

Q4:去掉测速和风扇灭火模块之后,整车还能正常运行测速相关的调试命令吗?

不能。测速和 PID 直线行驶相关的命令、灭火相关的命令在本方案里已从代码中移除,对应的 GPIO20 / GPIO4 被红外跟随模块占用。如果之后想恢复这两个功能,需要重新给摄像头之外的功能规划一套互不冲突的引脚表,不能与红外跟随模块共用 GPIO20 / GPIO4

相关推荐
智者知已应修善业13 小时前
【proteus交通灯故障检测的完善方案】2025-4-29
经验分享·笔记·单片机·嵌入式硬件·proteus
LingzhiPi15 小时前
零知ESP32--RC522NFC考勤打卡系统
c++·单片机·嵌入式硬件
炸膛坦客15 小时前
常见代码问题与解答:(二)因外部晶振频率改变导致 MCU 锁死的原因及处理方法
单片机·嵌入式硬件
ShiMetaPi17 小时前
ShiMetaPi Pico-G1:百元级黑光全彩AI ISP开发板
人工智能·嵌入式硬件·pico·isp
Lzh编程小栈17 小时前
嵌入式深度精讲|STM32引脚全方位底层原理、配置逻辑、硬件规范、高频面试题
c语言·stm32·单片机·嵌入式硬件
ch3nyuyu1 天前
单片机实验2
单片机·嵌入式硬件
mk0151 天前
20V输入,12V 3A输出 效率96%DCDC降压芯片
驱动开发·单片机·硬件工程·智能硬件·pcb工艺
达子6661 天前
Zephyr RTOS 学习系统:第一阶段 —— 入门篇
stm32·单片机·嵌入式硬件
做萤石二次开发的哈哈1 天前
萤石 ERTC Windows SDK 包含哪些核心 API 能力?
stm32·单片机·嵌入式硬件