实战课题
第 28 课:倒车雷达
距离分三档,越近响得越急——第一次同时指挥三个模块
我是吴老师。这节课我们做的东西,你在每一辆车上都听过。
倒车的时候,那个声音:
嘀……嘀……嘀…… (还远)
嘀嘀嘀嘀嘀嘀! (快撞上了!)
你不用看屏幕,闭着眼睛听声音,就知道车屁股离墙还有多远。
这节课我们把它做出来。 用超声波测距、蜂鸣器发声、LED 闪——三个模块一起上。
一、这节课有个新麻烦
前两个课题,你都是"一个传感器 + 一个执行器",输入进来、输出出去,干干净净。
这一课不一样了。 三个模块,四根信号线,而且它们之间还互相牵制:
| 麻烦 | 为什么 |
|---|---|
| 超声波读数会跳 | 第 15 课讲过:材质、角度、斜面都会影响它。它给你的是"毛数据" |
| 蜂鸣器只有一个音调 | 有源蜂鸣器,做不出"音调越来越高"的效果 |
| 蜂鸣器还不能忍 | 它会把超声波的每一次跳动原原本本喊出来——你总不能让蜂鸣器跟着读数一起抽风吧(第 15 课原话) |
所以这一课真正的功夫,不在"怎么测距离",在"怎么把毛数据变成能用的信号"。
这就是实战课题跟模块课的分水岭:模块课教你"这个零件怎么用",实战课教你"怎么让它在一个真实的东西里好好干活"。
二、这个课题的三个环节
| 环节 | 这个课题里是什么 | 具体到引脚 |
|---|---|---|
| 传感器(看) | 超声波模块,一直在问"前面那个东西离我几厘米" | Trig→GPIO6,Echo→GPIO7 |
| 判断(想) | 距离落在哪一档?(>50 / 20~50 / <20) | if / else if / else |
| 执行器(做) | 蜂鸣器响的节奏 + LED 亮灭 | 蜂鸣器→GPIO5,LED→GPIO3 |
注意第二行——这次的"判断"不是简单的"是/不是",是三档。 这就是这节课的核心。
三、材料清单
套件里拿这些:
| 元件 | 数量 | 干什么用 |
|---|---|---|
| ESP32-C3 主控 + 拓展板 | 1 | 大脑 |
| 超声波模块 | 1 | 测距 |
| 蜂鸣器模块 | 1 | 发声 |
| LED 灯模块 | 1 | 闪 |
| 母对母杜邦线 | 10 根 | 接线(超声波自己要 4 根) |
身边自己准备:
| 东西 | 干什么用 |
|---|---|
| 冰棒棍 | 3 根,搭支架(吃完雪糕别扔,洗一洗晾干) |
| 热熔胶枪 + 胶棒 | 粘 |
| 一本书或者一个纸盒 | 当"墙",测试用 |
四、接线
| 超声波模块 | 接到 |
|---|---|
| VCC | 3.3V |
| GND | GND |
Trig |
GPIO6 |
Echo |
GPIO7 |
| 蜂鸣器模块 | 接到 |
|---|---|
| VCC | 3.3V |
| GND | GND |
I/O |
GPIO5 |
| LED 灯模块 | 接到 |
|---|---|
| VCC | 3.3V |
| GND | GND |
Signal |
GPIO3 |
三个模块的 VCC 和 GND 全都接到拓展板的 3.3V 和 GND 上。 这点很重要,第五节会专门讲。
为什么超声波换到
GPIO6/GPIO7了?第 15 课我们用的是
GPIO4和GPIO5。但这一课GPIO5要留给蜂鸣器(第 17 课就是它),一个脚不能干两件事。所以超声波整体往后挪两格。这两个脚都在GPIO3~GPIO7那一段里,是第 4 课说的"最安全"的那一批。注意
Trig和Echo别接反。 第 15 课说过:接反不会烧坏,但读数一定是错的——通常就是一个 0,或者一个吓死人的大数。
五、分步实现
第 1 步:先把距离读出来
跟第 15 课一样的代码,只是引脚号换了。
#define TRIG 6
#define ECHO 7
void setup() {
Serial.begin(115200);
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
}
void loop() {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
long d = pulseIn(ECHO, HIGH);
float cm = d * 0.034 / 2; // 别忘了那个 / 2
Serial.print(cm);
Serial.println(" cm");
delay(100);
}烧进去。把一本书立在桌上,慢慢往模块跟前推。 数字跟着变小,对吧?
第 2 步:看它有多"毛"
现在做一件事:把书固定在 30 厘米的地方别动,盯着串口看 10 秒。
你会看到类似这样的东西:
30.1 cm
29.7 cm
312.4 cm ← 哎?
29.9 cm
0.0 cm ← 又怎么了?
29.6 cm第 15 课讲过这个,现在你亲眼见一次。 三个原因(第 15 课那张表):材质、角度、斜面。再加上偶尔一次回声太弱,模块干脆没听见,pulseIn 等了一秒返回 0。
这种数据,你敢直接喂给蜂鸣器吗?
一试就知道:读数蹦到 312 的那一瞬间,蜂鸣器会以为"非常远",安静下来;下一瞬间回到 30,它又开始响。 你的倒车雷达会像个神经病一样,嘀一下停一下,完全没有章法。
所以这一步不是"可选优化",是必须做的。 第 15 课结尾我说过:"传感器的原始数据都是毛的,变干净是你的活,不是传感器的活。"
第 3 步:洗干净——去掉最高分和最低分
办法特别土,但特别好用:连读 5 次,去掉最大和最小,剩下 3 个取平均。
这个套路你熟不熟? 电视上那些比赛打分——评委打一堆分,去掉一个最高分、去掉一个最低分,剩下的算平均。 一模一样的道理。
float readDistance() {
float minV = 9999; // 用来记"最小的一次"
float maxV = -1; // 用来记"最大的一次"
float sum = 0; // 用来记"加起来多少"
for (int i = 0; i < 5; i++) {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
long d = pulseIn(ECHO, HIGH);
float cm = d * 0.034 / 2;
if (cm < minV) minV = cm; // 比最小的还小?换掉
if (cm > maxV) maxV = cm; // 比最大的还大?换掉
sum = sum + cm;
delay(10);
}
// 总数减掉最大和最小,剩下的 3 个取平均
return (sum - minV - maxV) / 3.0;
}把 loop 改成调用它,再盯着串口看 10 秒。
数字稳多了吧? 那个 312 和 0 都被当成"最高分"和"最低分"扔掉了。
两个细节:
- 为什么是 5 次? 3 次太少(扔两个只剩一个),7 次太慢(读一次要好几十毫秒)。5 次刚好。
minV为什么一开始设 9999? 因为要保证第一次比较的时候,任何一个真实的距离都比它小。设 0 就永远不会被替换了——这是"找最小值"的通用套路:给它一个足够大的初始值。还有一句要紧的:这个办法治不了"一直不准"。 它只是把偶尔蹦出来的野数据扔掉。如果 5 次读出来都离谱,那是接线或者环境的问题——去翻第 15 课那张排查表。
第 4 步:分档——从"一个阈值"到"三个档"
这是这节课的灵魂。
新手的第一反应是这么写:if (cm < 20) { 响 } else { 不响 }。能用,但很蠢。
因为人在倒车的时候,需要知道的是"还有多远",不是"近不近"。
打个比方:如果倒车雷达只有"近了就响",那它跟"撞上了才响"没什么区别。
真正有用的信息是"渐变的紧迫感"——还远的时候慢悠悠嘀一下,让你知道"它在工作";中等距离嘀得勤一点,你开始留意;很近的时候连成一片,你条件反射地踩刹车。
| 档位 | 距离 | 蜂鸣器 | LED |
|---|---|---|---|
| 安全 | 大于 50cm | 不响 | 灭 |
| 注意 | 20 ~ 50cm | 慢响:嘀——嘀—— | 跟着响一起闪 |
| 危险 | 小于 20cm | 急响:嘀嘀嘀嘀 | 常亮 |
代码就是一个 if 加一个 else if:
#define TRIG 6
#define ECHO 7
#define BUZZER 5
#define LED 3
void setup() {
Serial.begin(115200);
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
pinMode(BUZZER, OUTPUT);
pinMode(LED, OUTPUT);
}
void loop() {
float cm = readDistance();
if (cm < 20) {
// 危险:嘀嘀嘀嘀嘀
digitalWrite(LED, HIGH);
digitalWrite(BUZZER, HIGH); delay(50);
digitalWrite(BUZZER, LOW); delay(50);
} else if (cm < 50) {
// 注意:嘀——嘀——
digitalWrite(LED, HIGH);
digitalWrite(BUZZER, HIGH); delay(50);
digitalWrite(BUZZER, LOW); delay(350);
digitalWrite(LED, LOW);
} else {
// 安全:安静
digitalWrite(LED, LOW);
digitalWrite(BUZZER, LOW);
}
}烧进去,拿书试。 60 厘米以外安静;推到 40 厘米"嘀——嘀——",灯跟着闪;推到 15 厘米"嘀嘀嘀嘀嘀!",灯常亮。
成了。这就是倒车雷达。
第 5 步:为什么"急"是靠节奏,不是靠音调
这一点我要专门讲,因为很多人会卡在这儿。
你可能会想:能不能让声音越来越高,像真的倒车雷达那样?不能。而且原因不在代码里。
第 17 课讲过:我们套件里的蜂鸣器是"有源"的。
"源"指的是振荡源——负责产生音调的那个部件。 有源 = 它自带音源,内部电路按一个固定速度振动,所以它只有一种音调。
你能控制的只有一件事:什么时候响、响多久、停多久。 所以"越近越急"只能靠改节奏:
| 档位 | 响 | 停 | 一个来回 |
|---|---|---|---|
| 注意 | 50ms | 350ms | 约 0.4 秒 |
| 危险 | 50ms | 50ms | 约 0.1 秒 |
差四倍,耳朵一耳朵就听出来了。
这也顺便解释了为什么这段代码里有那么多
delay——它们不是"等一等",它们就是声音的节奏本身。再说个小实话:
readDistance()读 5 次,本身要花掉大概 70 毫秒。所以"急响"最快也就到这儿了——你把delay(50)改成delay(10)也快不了多少,因为量距离这件事本身就要花时间。
第 6 步:调成你自己的
50 和 20 这两个数,是我随手划的。 你的"墙"很远想早点有反应,就把 50 改成 80;觉得慢响太慢,就把 delay(350) 改成 delay(200)。改完拿书试一遍,听到你觉得"对了"为止。
六、组装:冰棒棍支架
为什么要支架? 因为超声波模块不能平放在桌上。它要是躺着、仰着,声波就打到天花板上去了,测出来的是天花板到桌面的距离,不是墙到你的距离。 我们需要它"朝前看",所以把它架到大概离桌面 7 厘米的高度。
第 1 步,摆三角形。 拿三根冰棒棍,在桌上摆成一个三角形(大概等边就行,边长 7~8 厘米)。
第 2 步,点胶。 在三个角上各挤一小坨热熔胶,按住十几秒。
注意是三个角,不是三条边。 边上糊一条又费胶又难看。
按住的时候别松手。 热熔胶十几秒凝,但没凝之前它会滑。
第 3 步,立起来试稳不稳。 三角形立在桌上,用手指轻轻推一下。不倒就对了。
为什么不倒? 因为它的两个底角撑开了一段距离——底面越宽越稳。 这道理跟你骑自行车、跟你家桌子的四条腿是一样的。
第 27 课那个纸皮底座,我让你粘的那块"三角撑",也是同一个道理。 三角形是最稳的形状——这不是巧合,是力学。
第 4 步,粘模块。 在三角形顶角上挤一坨胶,把超声波模块按上去,两个金属筒朝前,别让胶糊到它们上面。
方向要想清楚:它要朝哪儿? 你做的是倒车雷达,那它就该朝着你要测的那面"墙"。粘之前先摆一摆,确认方向对了再上胶。
第 5 步,等胶干,通电,把"墙"摆到它前面。推近、推远,听声音变急变慢。
这一刻你会发现:你刚才做的不只是"把模块粘在棍子上"。
前面两个课题,光敏和红外都是"摊在那儿就行";超声波不行——它的朝向就是它的视野。装歪了,它就瞎了。
这是你的作品第一次需要"瞄准"。
七、排查:如果读数全是乱的
开始之前我要先说一句话:这一课的排查比前两课难。因为你会遇到一个"看起来像已知问题"的现象,而那个"已知问题"会把你带偏。
症状
你做完了,通电了。结果是:
- 串口读数大部分是 0,偶尔蹦一个 300 多
- 蜂鸣器一会儿狂叫,一会儿安静,完全没有章法
- LED 跟着乱闪
你的第一反应可能是:"哦,第 15 课说过,超声波读数天生会跳,加个滤波就好了。"
然后你就去改 readDistance()——读 7 次、读 10 次、改成各种花样。没用,一点用都没有。
为什么没用?因为你认错了敌人。
第一步:把滤波关掉,看原始读数
这一步是破案的关键。 把 readDistance() 里的滤波去掉,改成最原始的单次读取,连读 10 次全部打印出来:
void loop() {
for (int i = 0; i < 10; i++) {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
long d = pulseIn(ECHO, HIGH);
Serial.print(d * 0.034 / 2);
Serial.print(" ");
delay(50);
}
Serial.println();
delay(1000);
}盯着看:
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 312.4 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0看清楚了:绝大多数是 0。
这时候你要停下来想一件事:
"抖动"和"读不到",是两件完全不同的事。
- 抖动:数字在真实值附近晃。书在 30 厘米,读数在 28~32 之间跳——那叫抖。
- 读不到:数字根本不着边。全是 0,偶尔蹦个 300——那不叫抖,那叫"它压根没测到东西"。
第 15 课教你的那条真知识("超声波读数会跳"),在这一刻变成了障眼法。 你以为你知道原因,所以你不再去看现象了。
这是硬件调试里最常见的一种翻车方式:你脑子里那个"我知道",挡住了你眼前那个"实际是"。(第 5 课那句话怎么说的?"先测,再猜。")
第二步:量电——模块有没有吃到电
万用表拨直流电压档。先短接表笔确认表是好的。
- 黑表笔搭拓展板的
GND - 红表笔搭超声波模块的
VCC针
读数:3.3V 左右。有电。 好,电源没问题,往下走。
第三步:量地——这一步是关键
万用表拨通断档())))。先短接两支表笔,听一声"嘀"确认表是好的。
- 一支表笔搭超声波模块上的
GND针 - 另一支表笔搭拓展板上任意一个
GND针
听到了吗?
——没有嘀。
找到了。
等一下,为什么要专门量这个?
因为前面几步你只量了
VCC,那只能说明"电源送到了"。但一个模块要好好工作,它必须有"电"、也必须有"地"。地和电是两根线,得分别查。 很多人查故障只查电源,一量有 3.3V 就放心了,然后在地上翻了半天。
第四步:确认是"线断了"还是"没插好"
表笔别动。 把两根表笔都挪到同一根 GND 线的两头:一头搭这根线在模块上那一端,一头搭它在拓展板上那一端。
还有"嘀"吗?
- 有 → 线是好的,问题在插的位置(多半是没插到底,或者插歪了)
- 没有 → 线内部断了(第 4 课说过,新买的线真的有坏的),换一根
拔出来,对着丝印看清楚,重新插一次。再量第三步——这次"嘀"了。通电,看书,读数正常了。
根因
超声波模块的 GND 没有接上,它和板子没有"共地"。
听起来只是少接一根线,但后果比你想的严重得多:
超声波模块的 Echo 脚,是靠"输出电压的高低"跟板子说话的。 而这个"高"和"低",是相对于它自己的地来说的。板子要判断"这根线现在是高还是低",必须拿同一个地当参照。
两边的地不在一起,板子就完全判断不了。 于是 pulseIn 要么测出一个荒唐的数,要么一直等不到、等满一秒返回 0。
第 4 课的原话,你还记得吗?
"两个模块之间如果没有共同的地,它们的信号就是对牛弹琴——各说各话,谁也听不懂谁。"
你当时听到这句话,可能觉得是个抽象的道理。今天它是你眼前这堆乱跳的数字。
这个坑,是我在这个课题里故意留的。
它比前两课的坑都阴,因为它满足了一个条件:它伪装成一个你已经知道的问题。 前两课的坑,你至少还知道"出事了";这一课的坑,会让你以为自己知道原因,然后在错误的路上改半天代码。
再捋一遍这次排查的骨架:
| 顺序 | 量什么 | 结论 |
|---|---|---|
| 1 | 先看原始数据,判断是"抖动"还是"读不到" | 全是 0 → 不是抖动,是没测到 |
| 2 | 量 VCC 对 GND | 3.3V → 电源没问题 |
| 3 | 通断档量模块 GND 和板子 GND | 不响 → 地没接上 |
| 4 | 通断档量这根线两头,再拔出来重插 | 线是好的 → 是插的问题 |
记住第 1 步和第 3 步。
- 第 1 步教的是:"症状像某个已知问题",不等于"就是那个问题"。 别急着套答案,先看数据。
- 第 3 步教的是:"有电"和"有地"是两件事,得分开查。
自测
答案与解析
"有源"的"源"指的是振荡源——负责产生音调的那个部件。它内部电路按固定速度振动,你给的命令只有"通电/断电"两种(第 17 课讲过)。所以你能控制的只有节奏。想要音调变化,得换无源蜂鸣器——它没有内部音源,音调由你给的频率决定。
答案与解析
关键在**"偶尔"**两个字。超声波大多数时候是准的,只是偶尔因为材质、角度、回声太弱蹦一个野数——跟比赛打分一个道理。注意干扰项里"多读几次就能自动校准"是错的:它不会让模块变准,只是把离谱的值扔掉。 如果 5 次读出来都离谱,那是接线或者环境的问题。
答案与解析
小心第一个干扰项——它是个"听起来很懂"的答案。 第 15 课确实说过超声波读数会跳,但**"抖"是在真实值附近晃,"读不到"是根本不着边**。而且滤波治不了这个——你读 10 次,10 次都是 0,去掉最大最小也还是 0。正确动作是先把原始数据打印出来看一眼,再量 VCC,再量 GND。
答题要点
答题要点应该包含:传感器是超声波(Trig→GPIO6、Echo→GPIO7);判断是分档的 if / else if / else(>50 / 20~50 / <20);执行器是蜂鸣器(GPIO5)加 LED(GPIO3)。分档好在哪:① 一个阈值只能告诉你"近了",分档能告诉你**"还有多远";② 慢响那一档特别有用,还远的时候就"嘀——嘀——"提醒你"它在工作、它在靠近",让你有时间反应;③ 越近越急的节奏是耳朵能本能反应**的东西,你不用看屏幕。
拓展真正的汽车倒车雷达,为什么车屁股上要装好几个探头?装一个不行吗?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"倒车雷达 超声波 探头 数量 探测范围"。想一想:超声波的声波是喇叭形散出去的(第 15 课那张图),它能看清一根细柱子吗? 如果装在正中间,车屁股的左右两个角会怎么样?
拓展超声波测不到毛巾、衣服这些软的东西(第 15 课试过)。那扫地机器人是怎么避免从楼梯上掉下去的?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"扫地机器人 悬崖传感器 红外 原理"。想一想:它不往前看,它往下看——那它看的是什么?为什么往下看就没有"软材料吸音"这个问题了?
本节你要动手做的事
- [ ] 接好线:超声波
Trig→GPIO6、Echo→GPIO7;蜂鸣器I/O→GPIO5;LEDSignal→GPIO3(先断电再接) - [ ] 先跑通单次测距,拿尺子验证一次(第 15 课那个动作)
- [ ] 亲眼见一次"毛数据":把书固定住不动,盯串口 10 秒
- [ ] 加上去掉最高最低的滤波,再盯 10 秒,对比一下
- [ ] 写分档代码:>50 安静、20~50 慢响、<20 急响
- [ ] 推着书走一遍,从头到尾听一次"嘀……嘀——嘀嘀嘀嘀嘀!"
- [ ] 搭冰棒棍支架:三根棍、三个角上胶、模块粘顶角、朝前
- [ ] 用万用表跑一遍第七节那个排查表(就算没坏也走一遍,练手)
- [ ] 做完自测题
这节课你做的是三个模块一起干活的东西。
但我希望你带走的最重要的一件事,是第七节那半句话:
"症状像某个已知问题",不等于"就是那个问题"。
你手上有一些真知识——超声波读数会跳、红外只有几厘米、有源蜂鸣器只有一个音。这些知识值钱,但它们有一个副作用:它们会让你在出故障的时候,第一时间就把现象塞进一个熟悉的解释里,然后停止观察。
高手和新手的区别就在这儿。 新手看到乱跳的数字,说"哦,抖动,滤波一下"。高手会先把原始数据打出来看一眼,然后说:"等一下,这不是抖动,这是根本没读到。"
这句话,你以后会用到无数次。
另外,你今天是第一次同时指挥三个模块。你会发现它们之间会互相牵制——超声波要时间,蜂鸣器要时间,两者还会打架。 这种"资源要分配"的感觉,是往系统集成走的第一步。
下一课我们做调光台灯——把"开关"变成"旋钮"。
第 6 课我提过一句:"你家那个灯要么开要么关,没有'开一半'这一说。('开一半'是另一回事,叫 PWM。)"下一课就是那个"另一回事"。
而且那一课有个新麻烦:LED 只有两种状态,而你要做的是一个能连续变亮变暗的灯。怎么用"两种状态"做出"连续变化"?下一节课说。