零知派ESP32-S3-智能小车控制系统(1)-五路可调节黑白循迹模块使用
做零知派 ESP32-S3 循迹小车时,引脚往往不够用——舵机、摄像头一上来,GPIO 立马捉襟见肘。这时候把五路循迹塞进一颗 MCP23017 I/O 扩展芯片,用 I2C 两根线就能多出 16 个 IO,是非常实用的一招。这篇文章把"五路循迹 + MCP23017 扩展"这套组合彻底讲透:传感器怎么工作、那颗可调电阻到底在调什么、MCP23017 怎么接、代码怎么读、电位器怎么一路一路校准,以及踩坑后怎么排查。
一、前言
很多人第一次玩循迹小车,都会遇到这样的困境:
-
程序逻辑明明没错,车子却在黑线上抽风式扭动;
-
换了个场地,原来好好的车突然集体失明;
-
五路传感器有的灵敏有的迟钝,输出乱七八糟;
-
外设一多,ESP32-S3 的引脚根本不够分。
前三个问题,根子在传感器没校准好——而校准的核心就是模块上那几颗不起眼的可调电阻(电位器)。最后一个问题,则可以用 MCP23017 I/O 扩展芯片优雅地解决。
本文以常见的 TCRT5000 五路循迹模块搭配 MCP23017、跑在 零知派 ESP32-S3 上为例,把这套组合彻底讲明白。
二、循迹模块的工作原理
2.1 红外对管:发射 + 接收
每一路探头本质上是一对红外对管:
-
发射管:持续发出人眼不可见的红外光;
-
接收管:接收从地面反射回来的红外光。
关键点在于不同颜色对红外光的反射率不同:
|
表面颜色 |
红外反射 |
接收管收到的光 |
输出电压趋势 |
|---|---|---|---|
|
白色/浅色 |
强反射 |
收到大量红外 |
一种状态 |
|
黑色/深色 |
几乎吸收 |
收到极少红外 |
另一种状态 |
车子之所以能"看见"黑线,就是靠这个反射率差异。
2.2 比较器 LM393:把模拟量变成数字量
接收管输出的是一个连续变化的模拟电压,但数字 IO 只认高低电平。于是模块上几乎都会有一颗 LM393 电压比较器来做转换:
+------- LM393 比较器 -------+
接收管电压 ──┤ IN+ │
│ OUT ──────┼──► 输出 D0(高/低电平)
可调电阻 ───┤ IN-(参考阈值电压) │
+---------------------------+
比较器干的事很简单:
如果 接收管电压 > 参考阈值,输出一种电平;否则输出另一种电平。
这样,连续的反射强度就被"切"成了清晰的 0 / 1。本文这套硬件里,黑线时探头输出 LOW,白底时输出 HIGH,代码里再取反,把"检测到黑线"统一表示成逻辑 1。
三、可调电阻到底在调什么?
那颗蓝色的小方块电位器,调的不是别的,正是比较器 IN- 端的参考阈值电压。换句话说,它决定了"多强的反射才算白、多弱才算黑"这条分界线画在哪。
旋动电位器,你实际在做三件事:
-
改变黑白判定的临界点不同场地的黑线深浅、底色亮度都不一样。通过调节阈值,让模块在你当前这块场地上能干脆利落地区分黑与白。
-
补偿不同的安装高度 / 探测距离传感器离地越远,反射光越弱。安装高度变了,就用电位器把阈值拉回合适的位置。
-
抵抗环境光干扰强光直射、阴影、不同色温的灯光都会影响读数。微调阈值可以避免误判。
一句话总结:
可调电阻 = 灵敏度旋钮 = 给比较器设定"黑白分界线"。
注意:MCP23017 只负责把比较器已经判定好的数字电平读进 MCU,它不参与灵敏度调节。黑白阈值始终由模块上的电位器决定,跟接不接扩展芯片没有任何关系。
四、为什么用 MCP23017?
ESP32-S3 本身 GPIO 不算少,但智能小车一旦把舵机、OV2640 摄像头、循迹全堆上去,可用引脚很快见底。MCP23017 正好解决这个痛点。
-
I2C 接口,只占 2 根线(SDA / SCL),就能扩展出 16 个 IO(GPA0~7、GPB0~7);
-
通过 A0/A1/A2 三个地址脚,一条 I2C 总线上最多挂 8 颗,理论上扩出 128 个 IO;
-
每个引脚可独立配置输入/输出,自带上拉,读循迹这种数字信号绰绰有余。
把五路循迹挂到 MCP23017 上,等于用两根线换回五个宝贵的原生 GPIO,留给更需要的高速外设。
⚠️ 关于读取速度:MCP23017 走 I2C,单次读取有微秒级延迟,比直接读原生 GPIO 慢。对循迹这种 200ms 级别的轮询完全够用,但如果你追求极高频采样,要把这点延迟考虑进去(后面会提到优化思路)。
五、硬件接线
5.1 MCP23017 与 ESP32-S3
|
MCP23017 引脚 |
接到 ESP32-S3 |
说明 |
|---|---|---|
|
VDD |
3.3V |
供电 |
|
GND |
GND |
地 |
|
SDA |
GPIO 8 |
I2C 数据 |
|
SCL |
GPIO 9 |
I2C 时钟 |
5.2 五路循迹模块与 MCP23017
本文把五路输出接到 MCP23017 的 GPA1 ~ GPA5(对应库里的
eGPA1~eGPA5)。注意硬件接线顺序与逻辑上的 S1~S5 是反过来的——这是为了让代码里的 S1~S5 对齐传感器从左到右的物理排列:
|
循迹模块(物理位置) |
逻辑编号 |
MCP23017 引脚 |
|---|---|---|
|
最左 |
S1 |
GPA5 |
|
左 |
S2 |
GPA4 |
|
中间 |
S3 |
GPA3 |
|
右 |
S4 |
GPA2 |
|
最右 |
S5 |
GPA1 |
|
VCC |
— |
3.3V |
|
GND |
— |
GND |
💡 接线顺序无所谓正反,关键是让代码里的 S1~S5 和车头朝向的物理左右一致。本文采用 S1=GPA5、S5=GPA1 的反向映射就是为此。如果你的实际左右是反的,把
loop()里的eGPA编号对调即可——不用动接线。
⚠️ 电平注意:很多循迹模块按 5V 设计,输出高电平可能是 5V。MCP23017 也要在 3.3V 系统下统一供电,确保模块输出不超过 3.3V,避免灌坏芯片。务必确认模块支持 3.3V 供电。
5.3 连接示意图

六、测试代码与逐行解析
/**************************************************************************************
* 文件: /LineTracker5/LineTracker5.ino
* 作者:零知派(深圳市在芯间科技有限公司)
* -^^- 零知派,让电子制作变得更简单! -^^-
* 时间: 2026-06-15
* 说明: 这段代码基于 ESP32-S3 主控,结合 MCP23017 扩展芯片(地址 0x20,SDA=8、SCL=9)读取五路循迹传感器,
* 节省 GPIO。setup 中初始化 I2C 与芯片,用 while 卡住失败初始化以便排查接线,并把 GPA1~GPA5 设为输入。
* loop 每 200ms 读取一次,因黑线输出低电平故取反后用 1 表示压线,S1~S5 反向映射到 GPA5~GPA1 以对齐物理左右,
* 再统计压线路数输出脱线、居中、偏左、偏右等提示,配合串口确认接线方向并辅助校准每路阈值。
***************************************************************************************/
#include <Wire.h>
#include <DFRobot_MCP23017.h>
DFRobot_MCP23017 mcp(Wire, 0x20);
void setup() {
Serial.begin(115200);
delay(500);
Wire.begin(8, 9); // SDA=8, SCL=9
while (mcp.begin() != 0) {
Serial.println("MCP23017 初始化失败,请检查接线和 I2C 地址(0x20)!");
delay(1000);
}
Serial.println("MCP23017 初始化成功");
mcp.pinMode(mcp.eGPA1, INPUT);
mcp.pinMode(mcp.eGPA2, INPUT);
mcp.pinMode(mcp.eGPA3, INPUT);
mcp.pinMode(mcp.eGPA4, INPUT);
mcp.pinMode(mcp.eGPA5, INPUT);
Serial.println("=== 循迹模块测试开始 ===");
Serial.println("把传感器分别移到黑线/白底上,观察 0/1 变化");
Serial.println("(1=检测到黑线, 0=白底)");
Serial.println("S1 S2 S3 S4 S5 | 状态");
Serial.println("---------------------------------");
}
void loop() {
// 白底黑线:黑线输出 LOW,取反后为 1
int s1 = !mcp.digitalRead(mcp.eGPA5);
int s2 = !mcp.digitalRead(mcp.eGPA4);
int s3 = !mcp.digitalRead(mcp.eGPA3);
int s4 = !mcp.digitalRead(mcp.eGPA2);
int s5 = !mcp.digitalRead(mcp.eGPA1);
int activeCount = s1 + s2 + s3 + s4 + s5;
Serial.printf("%d %d %d %d %d | ", s1, s2, s3, s4, s5);
// 简单状态提示,方便确认接线方向和居中
if (activeCount == 0) Serial.println("脱线/全白");
else if (activeCount >= 5) Serial.println("十字路口/全黑");
else if (s3 && !s1 && !s2 && !s4 && !s5) Serial.println("居中(S3压线)");
else if (s1 || s2) Serial.println("偏左");
else if (s4 || s5) Serial.println("偏右");
else Serial.println("过渡状态");
delay(200);
}
1. I2C 初始化
Wire.begin(8, 9); // SDA=8, SCL=9
ESP32-S3 的 I2C 引脚可以灵活映射,这里指定 SDA=8、SCL=9。注意 Wire.begin() 的参数顺序是 (SDA, SCL),别接反。
2. 用 while 循环卡住初始化
while (mcp.begin() != 0) { ... delay(1000); }
mcp.begin() 返回非 0 表示失败。用 while 卡在这里,是为了接线没插好时直接停下报错,而不是带着一颗没初始化的芯片往下跑、读出一堆垃圾数据。这是嵌入式里很值得养成的习惯——外设初始化失败就别往下走。
3. 引脚枚举 eGPA1~eGPA5
DFRobot 库用 mcp.eGPA1 这样的枚举来表示扩展引脚,而不是裸数字,可读性更好,也避免记错编号。
4. 取反 + 反向映射
int s1 = !mcp.digitalRead(mcp.eGPA5); // S1 对应 GPA5
int s2 = !mcp.digitalRead(mcp.eGPA4); // S2 对应 GPA4
int s3 = !mcp.digitalRead(mcp.eGPA3); // S3 对应 GPA3
int s4 = !mcp.digitalRead(mcp.eGPA2); // S4 对应 GPA2
int s5 = !mcp.digitalRead(mcp.eGPA1); // S5 对应 GPA1
这里有两层处理:
-
取反:这套硬件白底黑线、检测到黑线时输出 LOW。取反后,1 统一代表"压在黑线上",后面写循迹逻辑心智负担更小。如果你的模块定义相反(黑线输出 HIGH),把
!去掉即可。 -
反向映射:
S1读的是eGPA5、S5读的是eGPA1,逻辑编号和扩展口编号是倒着对的。这是因为接线时的物理顺序和期望的"S1=最左、S5=最右"刚好相反,于是在代码里调过来对齐,而不用重新焊线。这一步不影响功能,纯粹是让 S1~S5 对应车头的左右方向,方便后面"偏左/偏右"判断符合直觉。
5. 状态判断
activeCount 统计有几路压线:全 0 是脱线,全亮(≥5)多半是十字路口或全黑区,只有 S3 亮表示车身居中,靠 S1/S2 或 S4/S5 判断偏左偏右。这一段主要用来确认接线方向和居中——如果你把模块装反了,"偏左""偏右"会和实际相反,靠这个一眼就能看出来。
七、可调电阻校准实操
引入 MCP23017 不改变校准方式——阈值还是靠模块上的电位器调。五路模块每一路通常都有独立的电位器,必须一路一路单独校准。
7.1 准备
把模块固定到小车实际运行的安装高度(离地高度不同,反射强度就不同)。

准备好实际要跑的场地——同样的黑线、同样的底色。

烧入上面的测试代码,打开串口监视器(115200),边调边看 0/1 输出。
用串口实时盯着五路 0/1,比只看板载指示灯更直观——尤其能立刻发现哪一路阈值卡在临界点上抖动。
7.2 单路校准步骤
对每一路重复以下操作:
-
把这一路探头对准白色区域,用小螺丝刀缓慢旋动它对应的电位器,让串口对应位稳定显示 0(板载指示灯通常随之熄灭/点亮,看模块定义)。
-
把探头移到黑线上,确认这一位翻转成 1。
-
在白与黑之间反复移动,微调电位器,让 0/1 切换得干脆、稳定、不抖。
-
找到那个能稳定切换的"中间甜区"后,再换下一路。
7.3 五路一致性
五路全部调完后,把整个模块平移过黑线,观察串口输出的 1 是否随位置整齐地从一端扫到另一端。如果某一路明显比别人迟钝或敏感,回头单独再修那一路。

校准口诀:白显 0、黑显 1、来回扫、切得脆。
7.4 演示视频
零知派ESP32-S3 TCRT500五路循迹模块组装与调试
八、常见问题排查
|
现象 |
可能原因 |
解决办法 |
|---|---|---|
|
MCP23017 初始化一直失败 |
接线松动 / 地址不对 |
检查 SDA(8)、SCL(9);确认I2C地址为0x20; |
|
所有路都读不到变化 |
I2C 没通 / 模块没供电 |
用 |
|
怎么调都无法区分黑白 |
安装高度不对 / 黑线与底色对比度太低 |
调整离地高度(几 mm 到 1cm 左右最佳);换对比更强的黑线 |
|
某一路始终不变 |
该路电位器到极限 / 探头损坏 / 虚焊 |
回中再调;检查焊点;万用表量对管 |
|
输出疯狂抖动 |
阈值正好卡在临界点 |
把阈值往任一侧稍微多调一点,留出余量 |
|
"偏左/偏右"和实际相反 |
模块装反了 / GPA 顺序接反 |
调换接线顺序,或在代码里把 S1~S5 映射对调 |
|
换场地就失灵 |
阈值是按上个场地调的 |
循迹模块对环境敏感,换场地后重新校准是常态 |
|
SDA/SCL 接反 |
|
确认是 |
更多推荐
所有评论(0)