机器人避障实战:超声波/红外/ToF/激光雷达四种测距方案怎么选
- 说清避障的本质是「测前方距离 + 据距离改方向」,而不是某个神奇模块
- 横向对比四种测距方案的量程/精度/价格/抗干扰,知道什么场景该选哪个
- 写出能跑的 HC-SR04 测距 + 差速底盘避障状态机,并用舵机云台扫描选更空的方向
让小车自己别撞墙
你已经在 差速底盘那一篇 里让两个轮子能前进、后退、原地转了。现在的问题很现实:它只会闷头往前冲,撞到墙也不知道停。
避障没那么玄。它就两步:测出前方有多远的障碍,然后根据这个距离决定方向——够空就继续走,太近就停下来转个弯。难点全在第一步,也就是「测距」。一个机器人聪明不聪明,很大程度上取决于它的眼睛——测距传感器——靠不靠谱。
这篇我们把主流的四种测距方案摊开比一遍,然后用最便宜的超声波 HC-SR04 写一套真能跑的避障逻辑。HC-SR04 的接线和原理在 /sensor/hc-sr04/ 已经讲透了,这里不重复,直接拿来用。
四种测距方案,各有各的脾气
机器人测距常见的就这四类。它们测的物理量不一样,擅长的场景也完全不同。
① 超声波 HC-SR04——便宜大碗,但有脾气
发一束 40kHz 的声波,撞到障碍弹回来,数往返时间算距离。量程 2cm~400cm,几块钱一个,新手入门首选。
但它有三个坑你必须知道:
- 盲区:太近(< 2cm)测不准,因为回波还没发完就回来了。
- 波束宽:它发出去的不是一条线,而是一个约 15° 的锥形。意思是它「看到」的是前方一大片区域里最近的那个点,分不清是正前方还是斜前方。想精确定位障碍在哪,它做不到。
- 怕软、怕斜:海绵、窗帘这类软物会把声音吸掉,回波太弱测不到;倾斜的桌面会把声波反射到别处去,也收不到回波——于是小车以为前方是空的,直接撞上去。
② 红外测距——近距凑合,看脸色
红外发射管打一束光,接收管根据反射光的强弱或角度判断距离。便宜,响应快,但严重受环境光和物体颜色影响:大太阳底下容易失灵,黑色物体反射弱、测出来比实际远。我的建议是,红外用来做「有没有东西」的近距开关(比如防跌落、循迹)还行,用来做精确测距别指望。循迹的玩法见 循迹小车那篇。
③ ToF 激光 VL53L0X——小巧精准,我的首选
ToF 是 Time of Flight,飞行时间。它发一束红外激光,直接测光往返的时间(光速换算)。ST 的 VL53L0X 把这套东西塞进了一颗指甲盖大的芯片里,I2C 通信,量程几 mm 到 2m,精度毫米级,而且几乎不受物体颜色和环境光干扰(具体参数见文末 sources 里 ST 的官方页)。
它比超声波贵几倍,但波束窄、不怕软物、体积小。如果你的小车要在桌面上精细地躲杯子、躲书本,我会直接上 VL53L0X 而不是超声波。 唯一要注意的是它走 I2C,需要懂一点 I2C 地址的概念(模拟量和数字量、总线通信的基础可以回看 /principle/digital-analog/)。
④ 激光雷达 LiDAR——360° 扫描,贵,给 SLAM 用
LiDAR 本质是一个会转的 ToF——它一边发激光一边旋转,转一圈就得到周围 360° 一整圈的距离点云。有了点云,机器人才能「画」出周围环境的地图,这就是建图与定位(SLAM)的基础。
但它贵(便宜的几百,好的上千),功耗也高,新手做避障用不上。我把它放这儿,是为了让你知道:当你想让机器人不只是「躲」而是「认识房间、自己规划路线」时,LiDAR 才登场。那是 机器人接入 ROS 之后的世界,这篇先按下不表。
一张表看懂怎么选
| 方案 | 量程 | 精度 | 价格 | 抗干扰 | 适用场景 |
|---|---|---|---|---|---|
| 超声波 HC-SR04 | 2~400cm | 厘米级 | 极低(几元) | 怕软物/斜面,波束宽 | 入门避障、防撞,对精度要求不高 |
| 红外测距 | 几~80cm | 差 | 低 | 怕环境光/颜色 | 近距开关、防跌落、循迹 |
| ToF VL53L0X | 几mm~2m | 毫米级 | 中(几十元) | 强,不怕颜色 | 桌面精细避障、小型机器人首选 |
| 激光雷达 LiDAR | 数十cm~数十m | 厘米级 | 高(数百起) | 强 | SLAM 建图、自主导航 |
一句话取舍:学避障从 HC-SR04 起步,要做得靠谱升级 VL53L0X,要做导航再上 LiDAR。
完整代码:从测距到自己绕开
下面这套代码跑在 ESP32(或 Arduino,引脚号改一下即可)上。接线以 /sensor/hc-sr04/ 那篇为准,这里只写引脚定义,不重复画接线图。GPIO 的用法可参考文末 sources 里乐鑫的官方 API 文档。
第一步:HC-SR04 测一个距离出来
原理就是「给 Trig 一个 10μs 高脉冲触发,然后量 Echo 高电平持续了多久」。声速约 340m/s,即 0.034cm/μs。声波是往返,所以距离 = 时间 × 0.034 / 2。
// ---- HC-SR04 引脚(接线以 /sensor/hc-sr04/ 为准)----
const int TRIG = 5;
const int ECHO = 18;
void setup() {
Serial.begin(115200);
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
}
// 返回距离(cm);超时(没回波)返回 -1
float readDistanceCm() {
// 1. 先拉低确保干净的触发
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
// 2. 给 10μs 高脉冲触发一次测量
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
// 3. 量 Echo 高电平时长,单位 μs;30ms 超时(约 5m 外算无回波)
unsigned long us = pulseIn(ECHO, HIGH, 30000UL);
if (us == 0) return -1; // 超时,没测到障碍
// 4. 声速 0.034cm/μs,往返要除以 2
return us * 0.034f / 2.0f;
}
void loop() {
float d = readDistanceCm();
if (d < 0) Serial.println("无回波(前方很空或测到软物)");
else { Serial.print(d); Serial.println(" cm"); }
delay(100);
}
先单独烧这段,打开串口监视器,拿手在传感器前面晃,看数值跟着变。数值靠谱了,再往下加避障。
第二步:差速底盘的避障状态机
避障逻辑别写成一坨乱糟糟的 if。用状态机来组织最清爽:小车在「前进 / 后退 / 转向」几个状态间切换,每个状态干一件事。
差速底盘的电机控制函数(forward / backward / turnRight / stop)直接复用 差速底盘那篇 写好的,这里只贴避障主逻辑。
const float STOP_CM = 20.0f; // 近于 20cm 就认为有障碍
enum State { DRIVING, BACKING, TURNING };
State state = DRIVING;
unsigned long stateStart = 0;
void enter(State s) { state = s; stateStart = millis(); }
void loop() {
float d = readDistanceCm();
// 无回波(-1)当作前方空旷处理,避免误把软物当成墙后乱转
bool blocked = (d > 0 && d < STOP_CM);
switch (state) {
case DRIVING:
forward();
if (blocked) { stop(); enter(BACKING); } // 撞前停下
break;
case BACKING:
backward();
if (millis() - stateStart > 350) { stop(); enter(TURNING); } // 后退一小段
break;
case TURNING:
turnRight();
if (millis() - stateStart > 400) { // 转个弯换方向
// 转完再测一次,前方空了才恢复前进,否则继续转
if (!blocked) { stop(); enter(DRIVING); }
else stateStart = millis(); // 还堵着,再转一会
}
break;
}
}
注意两个设计:一是用 millis() 计时而不是 delay(),这样测距不会被卡住;二是转完弯先复测,确认前方真空了才走,否则在墙角里它会一直撞。
你应该看到什么
烧进去,把小车放地上开机。正常的话:
- 它一路向前开。
- 接近墙壁(约 20cm)时猛地停住。
- 然后倒一小段、原地右转。
- 转到前方空旷,继续前进。
- 走到墙角(两面都有墙)时,它会连转几次直到找到出路。
如果它该停的时候不停、直接撞上去,八成是测距没读对,回第一步单独调串口。
故障排查
| 现象 | 可能原因 | 怎么办 |
|---|---|---|
| 距离数值乱跳/不准 | 电机干扰电源、Echo 线太长 | 传感器和电机供电分离;Echo 线短一点、远离电机线 |
| 撞上软物(窗帘/沙发) | 声波被吸收,没回波 | 换 VL53L0X;或把「无回波」也当障碍保守处理 |
| 斜面/桌沿漏检直接掉 | 声波被斜面反射偏走 | 加红外做防跌落兜底,或低头装传感器 |
| 反应慢、停不住 | 测距太慢或阈值太小 | 测距间隔缩到 60ms,STOP_CM 调大到 25~30 |
| 转向后又立刻撞回去 | 转完没复测就走 | 用上面状态机里「转完复测」的写法 |
供电务必分离:电机启停的瞬间会把电压拉低,直接拖累传感器读数。给逻辑(ESP32+传感器)和电机各走各的电源,共地即可。这是避障小车最容易翻车的地方。
两个变体,让它更聪明
变体一:舵机云台扫描,选更空的方向走
固定朝前的超声波,撞墙了只能瞎转。更聪明的做法:把 HC-SR04 装在一个舵机上做成「云台」,撞墙后让舵机摆头左、中、右各测一次,哪边最空就往哪边转。舵机控制见 /guide/l2-servo/。
#include <ESP32Servo.h>
Servo pan;
const int PAN_PIN = 13;
void setup() { pan.attach(PAN_PIN); }
// 摆头扫三个方向,返回最空方向: 0=左 1=中 2=右
int scanBestDir() {
int angles[3] = {150, 90, 30}; // 左、中、右
float best = -1; int bestIdx = 1;
for (int i = 0; i < 3; i++) {
pan.write(angles[i]);
delay(300); // 等舵机摆到位
float d = readDistanceCm();
if (d < 0) d = 400; // 无回波视为很空
if (d > best) { best = d; bestIdx = i; }
}
pan.write(90); // 摆回正前方
delay(200);
return bestIdx;
}
把状态机里 TURNING 的固定右转,换成「先 scanBestDir() 再朝最空方向转」,小车就从「撞了乱转」进化成「看看哪边宽,往哪边走」。
变体二:把测距换成 VL53L0X
想让避障更准、不再怕软物,把 readDistanceCm() 整个换掉就行,上层状态机一行不用动——这就是把测距单独封装成一个函数的好处。
#include <Adafruit_VL53L0X.h>
Adafruit_VL53L0X lox;
void setupToF() { lox.begin(); } // I2C 默认地址 0x29
float readDistanceCm() {
VL53L0X_RangingMeasurementData_t m;
lox.rangingTest(&m, false);
if (m.RangeStatus == 4) return -1; // 超量程
return m.RangeMilliMeter / 10.0f; // mm 转 cm
}
接上 SDA/SCL 两根 I2C 线,上面那套避障状态机直接原样跑,而且测软物、测斜面都比超声波稳得多。
动手挑战
在地上随手摆一堆杂物:几个纸盒、一个抱枕(软的)、一本斜靠着的书。让你的小车从这头走到那头,中途不许被卡死、不许撞翻东西。
你大概率会发现:纯超声波版本会在抱枕和斜书上栽跟头(测不到)。这正是逼你升级的好时机——把测距换成变体二的 VL53L0X,或者加一个红外做兜底,再跑一遍。能稳稳走通,你就真正理解了「为什么一个机器人需要不止一种传感器」。
小结与下一步
避障的核心从来不是某个神奇模块,而是**「测距 + 决策」这两件事**。测距选什么传感器,取决于你要躲什么、预算多少:入门 HC-SR04,精细 VL53L0X,导航 LiDAR。决策这层我们用状态机搭了个能跑的骨架。
但你可能已经注意到一个毛病:小车的转向是「定时硬转」,转多转少全靠拍脑袋。要让它转得又快又稳、不超调,得让控制量跟着误差走——那就是 PID 控制 要解决的问题。如果你想直接跳到「让机器人自己建图导航」,那条路通向 接入 ROS 与 SLAM,LiDAR 也会在那儿正式上场。