做零知派 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- 端的参考阈值电压。换句话说,它决定了"多强的反射才算白、多弱才算黑"这条分界线画在哪。

旋动电位器,你实际在做三件事:

  1. 改变黑白判定的临界点不同场地的黑线深浅、底色亮度都不一样。通过调节阈值,让模块在你当前这块场地上能干脆利落地区分黑与白。

  2. 补偿不同的安装高度 / 探测距离传感器离地越远,反射光越弱。安装高度变了,就用电位器把阈值拉回合适的位置。

  3. 抵抗环境光干扰强光直射、阴影、不同色温的灯光都会影响读数。微调阈值可以避免误判。

一句话总结:

可调电阻 = 灵敏度旋钮 = 给比较器设定"黑白分界线"。

注意: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 读的是 eGPA5S5 读的是 eGPA1,逻辑编号和扩展口编号是倒着对的。这是因为接线时的物理顺序和期望的"S1=最左、S5=最右"刚好相反,于是在代码里调过来对齐,而不用重新焊线。这一步不影响功能,纯粹是让 S1~S5 对应车头的左右方向,方便后面"偏左/偏右"判断符合直觉。

5. 状态判断

activeCount 统计有几路压线:全 0 是脱线,全亮(≥5)多半是十字路口或全黑区,只有 S3 亮表示车身居中,靠 S1/S2 或 S4/S5 判断偏左偏右。这一段主要用来确认接线方向和居中——如果你把模块装反了,"偏左""偏右"会和实际相反,靠这个一眼就能看出来。


七、可调电阻校准实操

        引入 MCP23017 不改变校准方式——阈值还是靠模块上的电位器调。五路模块每一路通常都有独立的电位器,必须一路一路单独校准

7.1 准备

把模块固定到小车实际运行的安装高度(离地高度不同,反射强度就不同)。

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

烧入上面的测试代码,打开串口监视器(115200),边调边看 0/1 输出。

        用串口实时盯着五路 0/1,比只看板载指示灯更直观——尤其能立刻发现哪一路阈值卡在临界点上抖动。

7.2 单路校准步骤

对每一路重复以下操作:

  1. 把这一路探头对准白色区域,用小螺丝刀缓慢旋动它对应的电位器,让串口对应位稳定显示 0(板载指示灯通常随之熄灭/点亮,看模块定义)。

  2. 把探头移到黑线上,确认这一位翻转成 1

  3. 在白与黑之间反复移动,微调电位器,让 0/1 切换得干脆、稳定、不抖

  4. 找到那个能稳定切换的"中间甜区"后,再换下一路。

7.3 五路一致性

五路全部调完后,把整个模块平移过黑线,观察串口输出的 1 是否随位置整齐地从一端扫到另一端。如果某一路明显比别人迟钝或敏感,回头单独再修那一路。

        校准口诀:白显 0、黑显 1、来回扫、切得脆。

7.4 演示视频

零知派ESP32-S3 TCRT500五路循迹模块组装与调试


八、常见问题排查

现象

可能原因

解决办法

MCP23017 初始化一直失败

接线松动 / 地址不对

检查 SDA(8)、SCL(9);确认I2C地址为0x20;

所有路都读不到变化

I2C 没通 / 模块没供电

i2c_scanner 扫描确认 0x20 在线;万用表量模块 VCC

怎么调都无法区分黑白

安装高度不对 / 黑线与底色对比度太低

调整离地高度(几 mm 到 1cm 左右最佳);换对比更强的黑线

某一路始终不变

该路电位器到极限 / 探头损坏 / 虚焊

回中再调;检查焊点;万用表量对管

输出疯狂抖动

阈值正好卡在临界点

把阈值往任一侧稍微多调一点,留出余量

"偏左/偏右"和实际相反

模块装反了 / GPA 顺序接反

调换接线顺序,或在代码里把 S1~S5 映射对调

换场地就失灵

阈值是按上个场地调的

循迹模块对环境敏感,换场地后重新校准是常态

SDA/SCL 接反

Wire.begin 参数顺序记错

确认是 Wire.begin(SDA, SCL),即 (8, 9)

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐