概述
本文基于 ESP32-S3,使用两个红外对射避障传感器(GPIO20 / GPIO4)分别检测车头左右两侧目标,经过 INPUT_PULLUP 输入处理得到稳定的双目触发信号,再驱动跟随逻辑实现「单侧触发 → 转向对准、双侧触发 → 停车、目标丢失 → 停车等待」的跟随行为。整套功能封装为独立的 FollowControl 类,通过 CarDrive 统一电机接口驱动底盘,不依赖 Web、摄像头等其他模块,可作为独立测试程序,也可直接移植进整车工程。
需要特别说明本次改动绕不开的引脚取舍:ESP32-S3 版整车方案新增了摄像头模块(OV2640,并行接口),光是
XCLK、PCLK、VSYNC、HREF、SIOD、SIOC加 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 上 GPIO0、GPIO3、GPIO45、GPIO46 是芯片启动模式的 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 操作的细节。私有成员变量 isLost 与 lostStartTime 记录目标丢失的起始时刻,为后续扩展(例如丢失超时后触发旋转搜索)预留接口。头文件本身不出现任何具体引脚号,引脚全部集中在 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;
}
丢失后的行为扩展:原地旋转搜索
当前实现的丢失策略是停车等待,isLost 与 lostStartTime 两个成员变量已经记录了丢失的起始时刻。如果需要在目标丢失超过一定时间后触发原地旋转搜索,只需在 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.h 里 IR_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,说明 GPIO20 与 GPIO4 接反了,对调两根杜邦线即可。如果双侧都是 0,先排查供电是否正常,再顺时针旋转电位器提高灵敏度。
Q2:红外传感器在阳光下失效怎么办?
该模块对环境光有一定适应性,但在强阳光直射下,阳光中的红外分量会干扰接收管,导致误触发。解决方法:逆时针调节电位器降低灵敏度,同时给传感器加装遮光外壳(可用黑色热缩管套住模块两侧)。
Q3:进入跟随模式后小车完全没有反应,串口也没有输出,是什么问题?
FollowControl::run() 需要在主循环里持续被调用,而不是只在收到 "G" 命令时执行一次。检查主循环逻辑,确认跟随模式激活期间每帧都执行了 Follower.run();同时确认 Follower.begin() 已在 setup() 里调用过。
Q4:去掉测速和风扇灭火模块之后,整车还能正常运行测速相关的调试命令吗?
不能。测速和 PID 直线行驶相关的命令、灭火相关的命令在本方案里已从代码中移除,对应的 GPIO20 / GPIO4 被红外跟随模块占用。如果之后想恢复这两个功能,需要重新给摄像头之外的功能规划一套互不冲突的引脚表,不能与红外跟随模块共用 GPIO20 / GPIO4。