实战课题

第 28 课:倒车雷达

距离分三档,越近响得越急——第一次同时指挥三个模块

我是吴老师。这节课我们做的东西,你在每一辆车上都听过。

倒车的时候,那个声音:

嘀……嘀……嘀…… (还远)

嘀嘀嘀嘀嘀嘀! (快撞上了!)

你不用看屏幕,闭着眼睛听声音,就知道车屁股离墙还有多远。

这节课我们把它做出来。 用超声波测距、蜂鸣器发声、LED 闪——三个模块一起上。

一、这节课有个新麻烦

前两个课题,你都是"一个传感器 + 一个执行器",输入进来、输出出去,干干净净。

这一课不一样了。 三个模块,四根信号线,而且它们之间还互相牵制:

麻烦 为什么
超声波读数会跳 第 15 课讲过:材质、角度、斜面都会影响它。它给你的是"毛数据"
蜂鸣器只有一个音调 有源蜂鸣器,做不出"音调越来越高"的效果
蜂鸣器还不能忍 它会把超声波的每一次跳动原原本本喊出来——你总不能让蜂鸣器跟着读数一起抽风吧(第 15 课原话)

所以这一课真正的功夫,不在"怎么测距离",在"怎么把毛数据变成能用的信号"。

这就是实战课题跟模块课的分水岭:模块课教你"这个零件怎么用",实战课教你"怎么让它在一个真实的东西里好好干活"。

二、这个课题的三个环节

环节 这个课题里是什么 具体到引脚
传感器(看) 超声波模块,一直在问"前面那个东西离我几厘米" TrigGPIO6EchoGPIO7
判断(想) 距离落在哪一档?(>50 / 20~50 / <20) if / else if / else
执行器(做) 蜂鸣器响的节奏 + LED 亮灭 蜂鸣器→GPIO5,LED→GPIO3

注意第二行——这次的"判断"不是简单的"是/不是",是三档。 这就是这节课的核心。

距离分档:小于 20 厘米蜂鸣器急响,20 到 50 厘米慢响,大于 50 厘米不响,节奏越快说明越近

三、材料清单

套件里拿这些:

元件 数量 干什么用
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

三个模块的 VCCGND 全都接到拓展板的 3.3VGND 上。 这点很重要,第五节会专门讲。

为什么超声波换到 GPIO6 / GPIO7 了?

第 15 课我们用的是 GPIO4GPIO5。但这一课 GPIO5 要留给蜂鸣器(第 17 课就是它),一个脚不能干两件事。所以超声波整体往后挪两格。这两个脚都在 GPIO3~GPIO7 那一段里,是第 4 课说的"最安全"的那一批。

注意 TrigEcho 别接反。 第 15 课说过:接反不会烧坏,但读数一定是错的——通常就是一个 0,或者一个吓死人的大数。

五、分步实现

第 1 步:先把距离读出来

跟第 15 课一样的代码,只是引脚号换了。

C++
#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 个取平均。

这个套路你熟不熟? 电视上那些比赛打分——评委打一堆分,去掉一个最高分、去掉一个最低分,剩下的算平均。 一模一样的道理。

C++
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 都被当成"最高分"和"最低分"扔掉了。

两个细节:

  1. 为什么是 5 次? 3 次太少(扔两个只剩一个),7 次太慢(读一次要好几十毫秒)。5 次刚好。
  2. minV 为什么一开始设 9999? 因为要保证第一次比较的时候,任何一个真实的距离都比它小。设 0 就永远不会被替换了——这是"找最小值"的通用套路:给它一个足够大的初始值。

还有一句要紧的:这个办法治不了"一直不准"。 它只是把偶尔蹦出来的野数据扔掉。如果 5 次读出来都离谱,那是接线或者环境的问题——去翻第 15 课那张排查表。

第 4 步:分档——从"一个阈值"到"三个档"

这是这节课的灵魂。

新手的第一反应是这么写:if (cm < 20) { 响 } else { 不响 }能用,但很蠢。

因为人在倒车的时候,需要知道的是"还有多远",不是"近不近"。

打个比方:如果倒车雷达只有"近了就响",那它跟"撞上了才响"没什么区别。

真正有用的信息是"渐变的紧迫感"——还远的时候慢悠悠嘀一下,让你知道"它在工作";中等距离嘀得勤一点,你开始留意;很近的时候连成一片,你条件反射地踩刹车。

档位 距离 蜂鸣器 LED
安全 大于 50cm 不响
注意 20 ~ 50cm 慢响:嘀——嘀—— 跟着响一起闪
危险 小于 20cm 急响:嘀嘀嘀嘀 常亮

代码就是一个 if 加一个 else if

C++
#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 次全部打印出来:

C++
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 VCCGND 3.3V → 电源没问题
3 通断档量模块 GND 和板子 GND 不响 → 地没接上
4 通断档量这根线两头,再拔出来重插 线是好的 → 是插的问题

记住第 1 步和第 3 步。

  • 第 1 步教的是:"症状像某个已知问题",不等于"就是那个问题"。 别急着套答案,先看数据。
  • 第 3 步教的是:"有电"和"有地"是两件事,得分开查。

自测

单选题为什么"越近越急"只能靠改节奏,不能靠让声音变高?
单选题为什么要"连读 5 次,去掉最大最小,剩下取平均"?
单选题串口读数大部分是 0,偶尔蹦一个 300,蜂鸣器跟着乱叫。最可能是什么问题?
问答题这个课题的闭环是哪三个环节?为什么"加一个分档"比"一个阈值就响"要好?
拓展真正的汽车倒车雷达,为什么车屁股上要装好几个探头?装一个不行吗?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"倒车雷达 超声波 探头 数量 探测范围"。想一想:超声波的声波是喇叭形散出去的(第 15 课那张图),它能看清一根细柱子吗? 如果装在正中间,车屁股的左右两个角会怎么样?

拓展超声波测不到毛巾、衣服这些软的东西(第 15 课试过)。那扫地机器人是怎么避免从楼梯上掉下去的?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"扫地机器人 悬崖传感器 红外 原理"。想一想:它不往前看,它往下看——那它看的是什么?为什么往下看就没有"软材料吸音"这个问题了?

本节你要动手做的事

  • [ ] 接好线:超声波 TrigGPIO6EchoGPIO7;蜂鸣器 I/OGPIO5;LED SignalGPIO3先断电再接
  • [ ] 先跑通单次测距,拿尺子验证一次(第 15 课那个动作)
  • [ ] 亲眼见一次"毛数据":把书固定住不动,盯串口 10 秒
  • [ ] 加上去掉最高最低的滤波,再盯 10 秒,对比一下
  • [ ] 写分档代码:>50 安静、20~50 慢响、<20 急响
  • [ ] 推着书走一遍,从头到尾听一次"嘀……嘀——嘀嘀嘀嘀嘀!"
  • [ ] 搭冰棒棍支架:三根棍、三个角上胶、模块粘顶角、朝前
  • [ ] 用万用表跑一遍第七节那个排查表(就算没坏也走一遍,练手)
  • [ ] 做完自测题

这节课你做的是三个模块一起干活的东西。

但我希望你带走的最重要的一件事,是第七节那半句话:

"症状像某个已知问题",不等于"就是那个问题"。

你手上有一些真知识——超声波读数会跳、红外只有几厘米、有源蜂鸣器只有一个音。这些知识值钱,但它们有一个副作用:它们会让你在出故障的时候,第一时间就把现象塞进一个熟悉的解释里,然后停止观察。

高手和新手的区别就在这儿。 新手看到乱跳的数字,说"哦,抖动,滤波一下"。高手会先把原始数据打出来看一眼,然后说:"等一下,这不是抖动,这是根本没读到。"

这句话,你以后会用到无数次。

另外,你今天是第一次同时指挥三个模块。你会发现它们之间会互相牵制——超声波要时间,蜂鸣器要时间,两者还会打架。 这种"资源要分配"的感觉,是往系统集成走的第一步。

下一课我们做调光台灯——把"开关"变成"旋钮"。

第 6 课我提过一句:"你家那个灯要么开要么关,没有'开一半'这一说。('开一半'是另一回事,叫 PWM。)"下一课就是那个"另一回事"。

而且那一课有个新麻烦:LED 只有两种状态,而你要做的是一个能连续变亮变暗的灯。怎么用"两种状态"做出"连续变化"?下一节课说。