实战课题
第 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 那两根线:SDA 接 GPIO8,SCK 接 GPIO9。这就是板子默认的 I²C 脚,第 19 课用过。记住这个对应关系,等下排查的时候要用。
接线之前先断电,三个模块一个一个接。 别贪快一把全插上——一次接一个、接完立刻上电试一次,这样出错的时候你马上就知道是哪一根。这是"分步来"用在了接线上。
四、分步实现
第一步:先把距离读出来(串口版)
第 15 课那段代码,原样搬过来:
#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)。循环里每量一次距离,就更新一次屏幕:
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的。现在就去跑一遍,把结果记在笔记里,等下排查的时候它是最省事的一个证据。
第三步:先做个"笨版本"——近了就响
先跑通最丑的版本,再往上加东西。 这一步只有几行:
#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 秒——那是你真的趴下去了,该响了。
看出来了吗?阈值只回答"现在够不够近",秒表才回答"够近多久了"。 两件事合起来,才能做出一个不烦人的提醒器。
#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 就是只能装 true 或 false 的盒子。它的作用是记住"上一轮循环,你算不算趴太近"。没有它,你就分不清"刚开始趴"和"已经趴了一会儿"。
为什么是 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 的边缘,别糊在那两个金属筒上。
那两个筒是它的"嘴"和"耳朵"。你把胶糊上去,等于给它塞了棉花——它照样通电、照样有读数,就是测不准。而且这种故障看着特别像"模块坏了"。
调角度(这才是造物环节的真正难点)。 超声波不是激光,它散出去是个喇叭形,偏一点回波就弱一截,再偏一点读数就开始乱跳。所以角度不能"猜",得调:
- 把支架摆在桌子靠你这一侧或者桌角上,让超声波朝着你坐的位置。坐好,看着串口(或者 OLED)上的数字,慢慢转横臂。
- 读数稳(一直在 62、63、64 之间小晃)→ 正对了;读数乱跳(一会儿 60 一会儿 200 一会儿 0)→ 歪了,或者打到你衣服上了。
- 找到最稳的那个角度,在关节那儿点一点热熔胶固定。
看到没有——你又是靠"先测,再定"解决的。 这次你测的不是电压,是"读数稳不稳",但方法一模一样:不猜,看现象。
顺便提醒:别对着自己的衣服测。 第 15 课讲过,软材料会吸声音,棉质睡衣最坑,校服好一点。 你要是发现换了件衣服数字就变了——那不是你代码的问题。
装好了,就把它立在你桌角上。 坐正,看它安安静静的样子。然后趴下去,等两秒。"嘀"。
六、【排查】屏幕一片黑,但别的都正常
先看症状,这个症状很有意思。
你把东西装好了,上电。串口里,距离数字在正常地变;趴下去两秒,蜂鸣器"嘀"了。超声波、蜂鸣器、代码——全都是好的。只有那块 OLED,从头到尾一片黑。
这时候你心里想的是什么? "是不是屏坏了?""是不是地址不对?""是不是 oled.begin 那句话写错了?"
先别改代码。 电 → 信号 → 代码,老顺序。
第一步:先量电
万用表拨到直流电压档。黑表笔搭 OLED 的 GND,红表笔点在 VDD 针上。3.3V,正常。
好,供电排除了。 而且你顺带确认了另一件事:板子能给这个模块供电,不是它的电源问题。
第二步:那问题在信号线上
OLED 有四根线:VDD、GND、SDA、SCK。前两根刚量过,剩下两根。
这里要用通断档,但用法跟平时不一样,我要专门说。 平时你量通断,问的是"这根线断没断"。今天要问的是另一个问题:
"这根线,到底通到板子的哪个脚去了?"
把万用表当成追踪器用。 具体这么做:
第一量: 黑表笔搭 OLED 的 SDA 针,红表笔搭板子的 GPIO8。不响。
等等——按接线表,SDA 就该接 GPIO8,它为什么不响?
第二量: 黑表笔还搭在 OLED 的 SDA 上,红表笔挪到板子的 GPIO9。响了。
第三量: 黑表笔搭 OLED 的 SCK,红表笔搭 GPIO8。也响了。
结论已经很清楚了:这两根线互换了位置——SDA 跑到 GPIO9 上去了,SCK 跑到 GPIO8 上去了。把两根线在板子这头对调一下,上电,屏幕亮了。
根因
SCK 和 SDA 接反了。
这两根线是孪生兄弟——一样粗、一样长、插头长得一模一样,很多套件里连颜色都一样。你在板子那头插的时候,插错一格是完全正常的事。它不烧,也不报错,屏幕就是死的。
这是我在这个课题里故意留的坑,也是真实调试里天天发生的事。
我留它,是因为你差一点就要去改代码了——你已经在怀疑地址、怀疑库、怀疑屏幕坏了。而代码一个字都没错。
记住今天这个动作:通断档不光能查"断没断",还能查"通到哪儿去了"。 一根线接错了位置,从外面看跟接对了完全一样——不会冒烟、不会报错、不会松动。只有万用表能告诉你它到底通到哪。
顺便:你要是刚才真跑了一遍 I²C 扫描——它会告诉你"总线上什么都没找到"。 那也是个有用的证据:扫描不到设备,说明线根本没连上,而不是地址写错了。
自测
答案与解析
笨版本判断的是"现在够不够近"——只要有一瞬间穿过阈值就响。而真实的坐姿问题是"保持这个姿势多久了"。趴一秒钟不是事,趴两分钟才是事。 加上"持续多久"这个条件,误报才会消失。这不是精度问题,是判断条件本身不完整。
millis() 单独用有意义吗?为什么?答案与解析
millis() 是板子内部一块从开机就在跑、永不停止的秒表,单位是毫秒。单独读它,你只知道"开机到现在过了 14325 毫秒",这跟你要判断的事没关系。真正的用法是两个动作:按下秒表(startTime = millis())和再看一次表再相减(millis() - startTime)。一减,就得到了"过了多久"。 注意它不是"当前时间"——板子不知道现在几点,除非你另外告诉它。
if (cm < 2 || cm > 400) cm = 999;?答案与解析
第 15 课讲过:pulseIn 等不到回声会等一秒然后返回 0,对着空气、打到软材料、角度歪了都会这样。这些读数不是"远",是"无效"。 直接拿去判断就会误报。cm = 999 是个省事的处理办法——当成"很远",也就是"不报警"。这也是第 15 课说的"传感器的原始数据都是毛的,变干净是你的活"。
答题要点
答题要点应该包含:先看症状排除范围——超声波和蜂鸣器都好,说明板子、供电、代码都正常,问题只可能在 OLED 这条链上。然后按"电 → 信号 → 代码":① 电压档量 OLED 的 VDD 对 GND 有没有 3.3V;② 通断档查那两根信号线——这里的用法不是"看断没断",而是"看它到底通到哪个脚":黑笔固定在 OLED 的某一针上,红笔依次去点板子上的 GPIO8 和 GPIO9,看它响在哪一个,就能判断有没有接反;③ 前两步都排除了,最后才回头怀疑地址(跑 I²C 扫描让板子自己报)和代码。通断档在这里是"追踪器",因为接反的线从外面看跟接对了一模一样,只有量了才知道。
拓展为什么趴着趴着就想趴得更低?这里面有生理原因吗?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"坐姿 腰椎 压力 研究"、"低头 颈椎 负荷 角度"。想一想:如果这个装置真的能让你少趴一点,它改变的是你的习惯,还是你的肌肉?顺便想想:为什么"提醒"比"强制"更容易让人接受?
拓展除了"持续两秒",还有什么办法能让提醒器不误报?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"去抖动 滤波 传感器 误报"、"滑动平均 中值滤波"。想一想:如果连着读 5 次距离、去掉最大最小取平均,那条上蹿下跳的蓝线会不会变平一点?代价是什么?(提示:读 5 次要花多长时间,这段时间里你动了怎么办?)
本节你要动手做的事
- [ ] 先跑通超声波:
Trig→GPIO4,Echo→GPIO5,串口里看到距离在变 - [ ] 标定你的两个数:坐直时多少、趴下时多少,阈值取中间——记在纸上
- [ ] 让 OLED 亮起来:
SDA→GPIO8,SCK→GPIO9,屏幕上显示出距离 - [ ] 跑一遍 I²C 扫描:把报出来的地址记在笔记里,排查时能省你半小时
- [ ] 先做笨版本:
if (cm < NEAR)就响——然后亲身体会一下它有多烦 - [ ] 加秒表:
millis()记一次、减一次,改成"趴够 2 秒才响",再调调HOLD(试试 1000 / 2000 / 5000) - [ ] 做冰棒棍支架:底座加配重 → 立柱(接缝贴夹板)→ 回形针做转轴
- [ ] 调角度:一边转横臂一边看读数,读数最稳的时候点胶固定
- [ ] 粘超声波注意:胶只点在 PCB 边缘,别糊那两个金属筒
这个课题你做的东西,比前面几个都"像个产品"。 它有嘴(OLED),有嗓子(蜂鸣器),有眼睛(超声波),还站在一个你自己搭的架子上。
但最值钱的不是这些零件,是你今天第一次处理了"时间"。 前面所有的判断都是"现在"——现在是高是低、现在是亮是暗。从今天起,你的板子有了"刚才"和"已经多久了"。
这个能力一打开,能做的事就多了:水开了多久没人管、门开了多久没关、你离开座位多久了。它们全都是"持续了多长时间"这一类问题。
还有那个坑。 屏幕黑掉的时候你没有去改代码,而是拿万用表去追那两根线。接反的线从外面看跟接对的一模一样,你把它揪出来了。 这个判断力,比这个装置本身值钱。
下一节课,我们要让东西动起来——而且是追着光动。
一个光敏 + 一个舵机,能做出一个自己会找最亮地方的"云台"。 而且你会发现一件很有意思的事:只用 1 个光敏,居然能做到"两个光敏"才能做的事。 那是个特别漂亮的设计,是我在这套课里最喜欢的一个课题。
不过丑话说在前面:舵机一转,你的板子可能会重启——第 18 课那个坑,它会换个面孔回来找你。下节课见。