ESP32 循跡車:讓小車沿著黑線自己跑
循跡車是機器人競賽最經典的入門題目。兩個紅外線感測器、一段黑色膠帶,就能讓車子自己沿著路線跑,還能順便學會「根據感測器修正方向」這個控制觀念。
✓ 本文程式已在 2026/09/29 以 Arduino ESP32 開發板套件 3.3.12 編譯通過(開啟全部編譯警告,0 個警告),開發板選 ESP32 Dev Module。
1. 需要的零件
| 零件 | 數量 | 說明 |
|---|---|---|
| 避障小車車體(ESP32、L298N、馬達、電池) | 1 | 見 避障小車 |
| TCRT5000 紅外線循跡模組 | 2 | 有藍色可調電阻、DO 數位輸出的那種 |
| 黑色電工膠帶 | 1 捲 | 寬約 1.5–2 cm |
| 白色地板、白色海報紙或珍珠板 | 當場地 |
2. 運作原理
TCRT5000 發出紅外線,白色地面反射強、黑色膠帶反射弱。模組上的比較器把這個差異變成數位訊號:多數模組照到黑線時 DO 輸出 HIGH。
兩個感測器裝在車頭,左右各一,中間夾著黑線:
| 左感測器 | 右感測器 | 代表 | 動作 |
|---|---|---|---|
| 白 | 白 | 線在兩個感測器中間 | 直走 |
| 黑 | 白 | 車子偏右了 | 往左修正 |
| 白 | 黑 | 車子偏左了 | 往右修正 |
| 黑 | 黑 | 十字路口或衝出彎道 | 沿上次修正方向找線,找不到就停 |
3. 接線
馬達與 L298N 接法和避障小車完全相同,只要加上兩個感測器:
| 從 | 接到 | 備註 |
|---|---|---|
| 左 TCRT5000 DO | ESP32 GPIO 34 | GPIO 34/35 是純輸入腳,正好適合 |
| 右 TCRT5000 DO | ESP32 GPIO 35 | |
| 兩個模組 VCC | ESP32 3V3 | 用 3.3V 供電,輸出就是 3.3V,不用分壓 |
| 兩個模組 GND | GND |
安裝位置:感測器離地約 1 公分,兩個感測器的間距比黑線稍寬,約 2.5–3 公分。
4. 調整靈敏度(最重要的一步)
- 先上傳程式,把車子架空。
- 把感測器放在白色地面上,轉模組上的藍色可調電阻,直到板子上的指示燈剛好熄滅。
- 移到黑線上,指示燈應該亮起。兩個模組都要調。
- 如果剛好相反,把程式的
LINE_IS_HIGH改成false。
5. 程式
/*
* TinyBot Forge — ESP32 循跡小車(兩路紅外線)
* https://tinybotforge.com
*
* 硬體:ESP32 DevKit V1、TCRT5000 紅外線循跡模組 ×2、L298N、兩顆 TT 馬達
* 場地:白色地面貼 1.5–2 cm 寬黑色膠帶
* 環境:Arduino IDE 2.x + ESP32 開發板套件 3.x
*
* 多數 TCRT5000 模組:照到黑線時 DO 輸出 HIGH,照到白色輸出 LOW。
* 你的模組相反的話,把 LINE_IS_HIGH 改成 false。
*/
const int PIN_SENSOR_L = 34; // GPIO34/35 只能輸入,拿來接感測器剛好
const int PIN_SENSOR_R = 35;
const bool LINE_IS_HIGH = true;
const int PIN_ENA = 25, PIN_IN1 = 26, PIN_IN2 = 27;
const int PIN_ENB = 33, PIN_IN3 = 32, PIN_IN4 = 13;
const int PWM_FREQ = 1000, PWM_BITS = 8;
const int SPEED_STRAIGHT = 150;
const int SPEED_FAST = 170; // 修正方向時外側輪
const int SPEED_SLOW = -60; // 修正方向時內側輪(稍微倒轉,轉得比較急)
const unsigned long LOST_MS = 1500; // 兩邊都找不到線超過這麼久就停車
int lastTurn = 0; // -1 最後往左修、1 往右修
unsigned long lostSince = 0;
void setMotor(int pinEn, int pinA, int pinB, int speed) {
speed = constrain(speed, -255, 255);
digitalWrite(pinA, speed > 0 ? HIGH : LOW);
digitalWrite(pinB, speed < 0 ? HIGH : LOW);
ledcWrite(pinEn, abs(speed));
}
void drive(int left, int right) {
setMotor(PIN_ENA, PIN_IN1, PIN_IN2, left);
setMotor(PIN_ENB, PIN_IN3, PIN_IN4, right);
}
bool onLine(int pin) {
return digitalRead(pin) == (LINE_IS_HIGH ? HIGH : LOW);
}
void setup() {
Serial.begin(115200);
pinMode(PIN_SENSOR_L, INPUT);
pinMode(PIN_SENSOR_R, INPUT);
pinMode(PIN_IN1, OUTPUT); pinMode(PIN_IN2, OUTPUT);
pinMode(PIN_IN3, OUTPUT); pinMode(PIN_IN4, OUTPUT);
ledcAttach(PIN_ENA, PWM_FREQ, PWM_BITS);
ledcAttach(PIN_ENB, PWM_FREQ, PWM_BITS);
drive(0, 0);
delay(2000);
}
void loop() {
bool l = onLine(PIN_SENSOR_L);
bool r = onLine(PIN_SENSOR_R);
if (!l && !r) {
// 兩個感測器夾著黑線:直走
drive(SPEED_STRAIGHT, SPEED_STRAIGHT);
lostSince = 0;
} else if (l && !r) {
// 左邊壓到線:車子偏右了,往左修
drive(SPEED_SLOW, SPEED_FAST);
lastTurn = -1;
lostSince = 0;
} else if (!l && r) {
// 右邊壓到線:往右修
drive(SPEED_FAST, SPEED_SLOW);
lastTurn = 1;
lostSince = 0;
} else {
// 兩邊都是黑色:可能是十字路口或衝出線外,沿上次方向找線
if (lostSince == 0) lostSince = millis();
if (millis() - lostSince > LOST_MS) {
drive(0, 0);
} else if (lastTurn < 0) {
drive(SPEED_SLOW, SPEED_FAST);
} else {
drive(SPEED_FAST, SPEED_SLOW);
}
}
delay(5);
}
6. 場地製作
- 先貼一個大橢圓,彎道半徑至少 20 公分,跑順了再加 S 彎。
- 膠帶轉彎時分成幾小段貼,不要硬拉,否則邊緣會翹起來反光。
- 避開陽光直射,陽光含大量紅外線,會干擾感測器。
7. 常見問題
一過彎就衝出去
速度太快,來不及修正。把 SPEED_STRAIGHT 降到 120–130,或讓 SPEED_SLOW 更負,轉得更急。
直線上一直左右搖擺
兩個感測器間距太窄。稍微拉開,讓黑線能完整待在中間。
完全離開線之後車子一直往前開
兩路感測器的限制:兩邊都看到白色時,車子無法分辨「線在中間」還是「整台車跑出去了」。想更穩,下一步可以升級成三路或五路感測器,我們在教材裡會做。