实战课题

第 30 课:坐姿提醒器

第一次让板子学会看表——不是"近了就响",而是"近了多久才响"

我是吴老师。这个课题是实战单元里第一个"三模块"的,也是第一个带"时间"的。

你有没有被家里人念过"坐直一点""头抬起来""眼睛离远点"?念的人累,被念的人也累。

这节课咱们做个东西替你管这件事:一个立在桌上的小支架,你趴下去,它先不理你;你要是趴着不动几秒钟,它就"嘀"一声。 你坐直,它闭嘴。

注意上面那句里我说了"几秒钟"。这就是这个课题的全部新东西。

前面二十几节课,板子判断的都是"现在":现在亮不亮、现在近不近、现在按没按。它没有"刚才"这个概念。 而这个课题要判断的是"已经多久了"——板子要学会看表。

一、【闭环】这个课题的三个环节

环节 这个课题里是什么
传感器(看) 超声波模块——你的胸口离它多远
判断(想) 是不是太近了?而且近了多久了?
执行器(做) 蜂鸣器报警 + OLED 把数字显示出来

注意执行器那一栏有两个东西。 前面几个课题都是"一个传感器 + 一个执行器",这次不一样了。

为什么要有 OLED? 因为蜂鸣器只会"嘀",它不会告诉你"现在几厘米"。没有 OLED,你根本不知道这个装置在想什么——调试的时候只能干瞪眼。它是作品的"嘴"。

二、材料清单

套件里:ESP32-C3 主控 + 拓展板、超声波模块、蜂鸣器模块、OLED 显示屏、杜邦线若干(12 根左右,多备几根)。

身边要准备:

东西 上哪儿找
冰棒棍 雪糕棍,攒 15 根左右,吃完洗净晾干
热熔胶枪 + 胶棒 必备
剪刀 / 小锯条 锯冰棒棍。没锯条就"用剪刀剪一圈再掰"
厚纸皮一小块 做底座配重,或者用两枚硬币代替
橡皮筋 + 回形针 + 针 临时固定、做可调的转轴、在冰棒棍上扎孔

三、接线

这次要接三个模块,引脚得提前规划——第 4 课那句话,今天真的用上了。

模块 针脚 接到
超声波 VCC 3.3V
超声波 GND GND
超声波 Trig GPIO4
超声波 Echo GPIO5
蜂鸣器 VCC 3.3V
蜂鸣器 GND GND
蜂鸣器 I/O GPIO6
OLED VDD 3.3V
OLED GND GND
OLED SDA GPIO8
OLED SCK GPIO9

两个地方解释一下。

第一,蜂鸣器为什么用 GPIO6,不是第 17 课的 GPIO5 因为 GPIO5 被超声波的 Echo 占了。引脚是要抢的——手上的模块越多,越得提前排。这次刚好三个模块七个脚,不打架。

第二,OLED 那两根线SDAGPIO8SCKGPIO9这就是板子默认的 I²C 脚,第 19 课用过。记住这个对应关系,等下排查的时候要用。

接线之前先断电,三个模块一个一个接。 别贪快一把全插上——一次接一个、接完立刻上电试一次,这样出错的时候你马上就知道是哪一根。这是"分步来"用在了接线上。

四、分步实现

第一步:先把距离读出来(串口版)

第 15 课那段代码,原样搬过来:

C++
#define TRIG 4
#define ECHO 5

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 duration = pulseIn(ECHO, HIGH);
  float cm = duration * 0.034 / 2;      // 那个 ÷2,别忘了
  Serial.println(cm);
  delay(200);
}

烧进去,手在模块前面慢慢靠近、慢慢移开,看数字跟着变。

先别往下走,做一件正事:标定。 还没有支架,就把手当成你的胸口:放到离模块 60 厘米的地方,记下读数(这叫"坐直");再放到 30 厘米,记一个(这叫"趴下")。把这两个数记在纸上,中间取个值就是你的阈值。这个课题我暂时用 45 厘米,但你要用你自己量出来的那个数(第 11 课:别抄别人的参数)。

为什么一定要自己量? 因为阈值跟你的支架高度、桌子宽度、你坐的位置都有关系。别人在别人桌上量出来的数,搬到你这儿可能永远不触发,或者一直在响。

第二步:把数字显示到屏上

复习第 19 课那三步套路:擦干净 → 重新画 → 推上屏setup() 里照第 19 课抄这几行:Wire.begin(8, 9)(先 SDA 后 SCL)、oled.begin(SSD1306_SWITCHCAPVCC, 0x3C),再加一句 oled.setTextSize(2)。循环里每量一次距离,就更新一次屏幕:

C++
  oled.clearDisplay();               // 1. 擦干净
  oled.setCursor(0, 0);              // 2. 重新画
  oled.print("Dist");
  oled.setCursor(0, 26);
  oled.print(cm, 0);                 // 那个 0 是"不要小数"
  oled.print("cm");
  oled.display();                    // 3. 推上屏

手上上下下,屏幕上的数字跟着跳。 到这一步,你的装置有"嘴"了。

oled.begin 那一行的地址,如果屏幕死活不亮,先别怀疑线——跑一个 I²C 扫描程序,让板子自己把地址报出来(第 19 课讲过)。多数是 0x3C,也有印 0x3D 的。现在就去跑一遍,把结果记在笔记里,等下排查的时候它是最省事的一个证据。

第三步:先做个"笨版本"——近了就响

先跑通最丑的版本,再往上加东西。 这一步只有几行:

C++
#define BUZZ 6
#define NEAR 45          // 阈值:厘米。用你自己量出来的那个数

// setup 里加一句 pinMode(BUZZ, OUTPUT);
// loop 里,显示完距离之后:
  if (cm < NEAR) digitalWrite(BUZZ, HIGH);   // 太近了,响
  else           digitalWrite(BUZZ, LOW);    // 坐直了,停

烧进去,趴下去试试。它会响。你坐直,它停。

成了吗?成了。但这个版本有个大问题,你自己试试就知道——

低头捡个笔,它响。伸个懒腰,它响。扭头看一眼窗外,它响。 甚至你呼吸时胸口起伏一下,它都可能"嘀"一声。

一天下来,你会想把它砸了。

第四步:给它加一块秒表(这一步是重点)

问题出在哪? 出在"现在"这两个字上。板子现在判断的是"这一刻够不够近"。但真实的坐姿问题是"你保持这个姿势多久了"。趴一秒钟不算事,趴两分钟才是事。

所以我们要给板子加一个新东西:时间。 板子里有一块看不见的秒表,叫 millis()

它的比喻特别好记:这是一块从开机就开始跑、永远不停的秒表。 你问它一次,它就回答"从开机到现在过了多少毫秒"。单独看这个数一点用都没有——它只会一直变大。

有用的是两个动作按下秒表(把这一刻的数记下来),和再看一次表(把两个数一减)。一减,就是"从按下到现在过了多久"。 这就是全部的秘诀。

距离读数两次穿过阈值线,只有持续低于阈值两秒以上的那次才触发报警

看这张图:蓝线是距离读数,红线是阈值,它穿过阈值线两次。第一次在下面待了 0.4 秒——那是你低头捡笔,不理它;第二次待了 2.5 秒——那是你真的趴下去了,该响了

看出来了吗?阈值只回答"现在够不够近",秒表才回答"够近多久了"。 两件事合起来,才能做出一个不烦人的提醒器。

C++
#define TRIG  4
#define ECHO  5
#define BUZZ  6
#define NEAR  45          // 阈值(厘米),用你自己量的
#define HOLD  2000        // 要趴够多久才响(毫秒)

unsigned long startTime = 0;    // 记下"开始趴近"的时刻
bool tooClose = false;          // 现在算不算趴太近

void setup() {
  Serial.begin(115200);
  pinMode(TRIG, OUTPUT);
  pinMode(ECHO, INPUT);
  pinMode(BUZZ, OUTPUT);
}

void loop() {
  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
  digitalWrite(TRIG, LOW);
  long duration = pulseIn(ECHO, HIGH);
  float cm = duration * 0.034 / 2;

  if (cm < 2 || cm > 400) cm = 999;     // 野数据当成"很远"

  if (cm < NEAR) {                      // 太近了
    if (!tooClose) {                    // 刚从"坐直"变成"趴下"——按下秒表
      tooClose = true;
      startTime = millis();
    } else if (millis() - startTime > HOLD) {   // 已经趴够 2 秒
      digitalWrite(BUZZ, HIGH);         // 响!
    }
  } else {                              // 坐直了
    tooClose = false;                   // 松开秒表
    digitalWrite(BUZZ, LOW);
  }

  // 这里再补上第二步那几行 OLED:clearDisplay → print → display
  delay(50);
}

几个地方要讲清楚。

bool tooClose 是什么? bool 就是只能装 truefalse 的盒子。它的作用是记住"上一轮循环,你算不算趴太近"。没有它,你就分不清"刚开始趴"和"已经趴了一会儿"。

为什么是 if (!tooClose) ! 读作"不"。"如果之前不算近"——也就是你这一下才刚趴过来。这时候要做的事只有一件:按下秒表为什么用 else if 因为你刚趴下的那一瞬间不该响——秒表才刚按下,得给它时间跑。

millis() - startTime > HOLD 就是"两个数一减"。 2500 减 400 等于 2100,大于 2000——趴够 2 秒了,响。

cm = 999 那句是干嘛的? 第 15 课说过,超声波会蹦野数据(pulseIn 等不到回声会返回 0)。不处理的话,蜂鸣器会被一个野数据吓得乱叫。 这句的意思很简单:读数不合理,就当"很远",别报警。

烧进去,试这三件事: ① 趴下去数两秒——第二秒末尾它才"嘀",前两秒是安静的;② 快趴快起——它一声都不吭,这就是你想要的;③ 坐直——立刻安静。

你做出一个"不烦人"的提醒器了。 HOLD 定多少合适?2000 毫秒只是个起点,你自己调1000 太敏感,5000 又太宽容(你都快趴到桌上了它还不吭声)。你家人的容忍度是多少,就定多少。

五、组装:冰棒棍支架

这个支架要解决一个问题:让超声波正对着你的胸口。 那意味着它得够高、还得能调角度。

底座。 3 根冰棒棍并排(间隔 1 厘米),上面横压 2 根,点胶粘死——得到一块 11×4cm 的平板。底下再粘一小块厚纸皮或两枚硬币当配重,不加配重一碰就倒。

立柱。 2 根冰棒棍叠着粘成一根更硬的杆,再拿 2 根接着往上粘。接缝处正反两面各贴一小段冰棒棍当"夹板"——这是木工的老办法,不贴就会断在接缝上。立柱粘在底座正中间,总高大概 30 厘米(你坐直时胸口差不多就这个高度)。

横臂(难点一)。 拿 1 根冰棒棍当横臂,先别粘死。用针在横臂一端和立柱顶端各扎一个小孔,把回形针掰直了穿过去,做成一个能上下转的关节。超声波模块粘在横臂另一端。

粘超声波有个讲究:胶只点在模块 PCB 的边缘,别糊在那两个金属筒上。

那两个筒是它的"嘴"和"耳朵"。你把胶糊上去,等于给它塞了棉花——它照样通电、照样有读数,就是测不准。而且这种故障看着特别像"模块坏了"。

调角度(这才是造物环节的真正难点)。 超声波不是激光,它散出去是个喇叭形,偏一点回波就弱一截,再偏一点读数就开始乱跳。所以角度不能"猜",得

  1. 把支架摆在桌子靠你这一侧或者桌角上,让超声波朝着你坐的位置。坐好,看着串口(或者 OLED)上的数字慢慢转横臂
  2. 读数稳(一直在 62、63、64 之间小晃)→ 正对了;读数乱跳(一会儿 60 一会儿 200 一会儿 0)→ 歪了,或者打到你衣服上了。
  3. 找到最稳的那个角度,在关节那儿点一点热熔胶固定。

看到没有——你又是靠"先测,再定"解决的。 这次你测的不是电压,是"读数稳不稳",但方法一模一样:不猜,看现象。

顺便提醒:别对着自己的衣服测。 第 15 课讲过,软材料会吸声音,棉质睡衣最坑,校服好一点。 你要是发现换了件衣服数字就变了——那不是你代码的问题。

装好了,就把它立在你桌角上。 坐正,看它安安静静的样子。然后趴下去,等两秒。"嘀"。

六、【排查】屏幕一片黑,但别的都正常

先看症状,这个症状很有意思。

你把东西装好了,上电。串口里,距离数字在正常地变;趴下去两秒,蜂鸣器"嘀"了。超声波、蜂鸣器、代码——全都是好的。只有那块 OLED,从头到尾一片黑。

这时候你心里想的是什么? "是不是屏坏了?""是不是地址不对?""是不是 oled.begin 那句话写错了?"

先别改代码。 电 → 信号 → 代码,老顺序。

第一步:先量电

万用表拨到直流电压档。黑表笔搭 OLED 的 GND,红表笔点在 VDD 针上。3.3V,正常。

好,供电排除了。 而且你顺带确认了另一件事:板子能给这个模块供电,不是它的电源问题。

第二步:那问题在信号线上

OLED 有四根线:VDDGNDSDASCK。前两根刚量过,剩下两根。

这里要用通断档,但用法跟平时不一样,我要专门说。 平时你量通断,问的是"这根线断没断"。今天要问的是另一个问题:

"这根线,到底通到板子的哪个脚去了?"

把万用表当成追踪器用。 具体这么做:

第一量: 黑表笔搭 OLED 的 SDA 针,红表笔搭板子的 GPIO8不响。

等等——按接线表,SDA 就该接 GPIO8,它为什么不响?

第二量: 黑表笔还搭在 OLED 的 SDA 上,红表笔挪到板子的 GPIO9响了。

第三量: 黑表笔搭 OLED 的 SCK,红表笔搭 GPIO8也响了。

结论已经很清楚了:这两根线互换了位置——SDA 跑到 GPIO9 上去了,SCK 跑到 GPIO8 上去了。把两根线在板子这头对调一下,上电,屏幕亮了。

根因

SCKSDA 接反了。

这两根线是孪生兄弟——一样粗、一样长、插头长得一模一样,很多套件里连颜色都一样。你在板子那头插的时候,插错一格是完全正常的事。它不烧,也不报错,屏幕就是死的。

这是我在这个课题里故意留的坑,也是真实调试里天天发生的事。

我留它,是因为你差一点就要去改代码了——你已经在怀疑地址、怀疑库、怀疑屏幕坏了。而代码一个字都没错。

记住今天这个动作:通断档不光能查"断没断",还能查"通到哪儿去了"。 一根线接错了位置,从外面看跟接对了完全一样——不会冒烟、不会报错、不会松动。只有万用表能告诉你它到底通到哪。

顺便:你要是刚才真跑了一遍 I²C 扫描——它会告诉你"总线上什么都没找到"。 那也是个有用的证据:扫描不到设备,说明线根本没连上,而不是地址写错了。

自测

单选题为什么"近了就响"这个笨版本一定要改掉?
单选题millis() 单独用有意义吗?为什么?
单选题为什么代码里要写 if (cm < 2 || cm > 400) cm = 999;
问答题屏幕上什么都不显示,但超声波和蜂鸣器都正常。你会怎么查?为什么用通断档?
拓展为什么趴着趴着就想趴得更低?这里面有生理原因吗?

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

提示:搜"坐姿 腰椎 压力 研究"、"低头 颈椎 负荷 角度"。想一想:如果这个装置真的能让你少趴一点,它改变的是你的习惯,还是你的肌肉?顺便想想:为什么"提醒"比"强制"更容易让人接受?

拓展除了"持续两秒",还有什么办法能让提醒器不误报?

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

提示:搜"去抖动 滤波 传感器 误报"、"滑动平均 中值滤波"。想一想:如果连着读 5 次距离、去掉最大最小取平均,那条上蹿下跳的蓝线会不会变平一点?代价是什么?(提示:读 5 次要花多长时间,这段时间里你动了怎么办?)

本节你要动手做的事

  • [ ] 先跑通超声波TrigGPIO4EchoGPIO5,串口里看到距离在变
  • [ ] 标定你的两个数:坐直时多少、趴下时多少,阈值取中间——记在纸上
  • [ ] 让 OLED 亮起来SDAGPIO8SCKGPIO9,屏幕上显示出距离
  • [ ] 跑一遍 I²C 扫描:把报出来的地址记在笔记里,排查时能省你半小时
  • [ ] 先做笨版本if (cm < NEAR) 就响——然后亲身体会一下它有多烦
  • [ ] 加秒表millis() 记一次、减一次,改成"趴够 2 秒才响",再调调 HOLD(试试 1000 / 2000 / 5000)
  • [ ] 做冰棒棍支架:底座加配重 → 立柱(接缝贴夹板)→ 回形针做转轴
  • [ ] 调角度:一边转横臂一边看读数,读数最稳的时候点胶固定
  • [ ] 粘超声波注意:胶只点在 PCB 边缘,别糊那两个金属筒

这个课题你做的东西,比前面几个都"像个产品"。 它有嘴(OLED),有嗓子(蜂鸣器),有眼睛(超声波),还站在一个你自己搭的架子上。

但最值钱的不是这些零件,是你今天第一次处理了"时间"。 前面所有的判断都是"现在"——现在是高是低、现在是亮是暗。从今天起,你的板子有了"刚才"和"已经多久了"。

这个能力一打开,能做的事就多了:水开了多久没人管、门开了多久没关、你离开座位多久了。它们全都是"持续了多长时间"这一类问题。

还有那个坑。 屏幕黑掉的时候你没有去改代码,而是拿万用表去追那两根线。接反的线从外面看跟接对的一模一样,你把它揪出来了。 这个判断力,比这个装置本身值钱。

下一节课,我们要让东西动起来——而且是追着光动

一个光敏 + 一个舵机,能做出一个自己会找最亮地方的"云台"。 而且你会发现一件很有意思的事:只用 1 个光敏,居然能做到"两个光敏"才能做的事。 那是个特别漂亮的设计,是我在这套课里最喜欢的一个课题。

不过丑话说在前面:舵机一转,你的板子可能会重启——第 18 课那个坑,它会换个面孔回来找你。下节课见。