news 2026/8/20 6:14:52

基于Arduino与WS2812B的智能沙漏计时器:从硬件选型到程序逻辑全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Arduino与WS2812B的智能沙漏计时器:从硬件选型到程序逻辑全解析

1. 项目缘起:从沙漏到数字,一个经典计时器的现代重构

不知道你有没有过这样的经历:在厨房里煮个鸡蛋,或者在工作间隙想专注25分钟,手边没有一个简单、直观又有点仪式感的计时工具。手机上的倒计时功能虽然方便,但屏幕一亮,各种通知也跟着来了,专注力瞬间就被打散。传统的机械沙漏很美,但时间固定,功能单一。于是,一个想法就冒了出来:能不能用我们手边常见的电子元件,做一个既有沙漏的优雅观感,又能灵活设定时间的智能计时器?

这就是“Arduino Hourglass Timer”项目的核心。它本质上是一个基于Arduino微控制器的数字沙漏计时器。与传统沙漏沙子流动的物理过程不同,它通过LED灯带(如WS2812B)模拟“沙粒”从上到下“流动”的过程,当时间归零时,还会有声光提示。这个项目完美融合了硬件搭建、基础编程和创意设计,无论是Arduino新手想找一个有趣的入门项目,还是老手想做一个有格调的桌面小工具,都非常合适。

我最初做这个,就是想解决自己“番茄工作法”时总被手机干扰的问题。市面上成品要么太贵,要么功能花哨不实用,自己动手做一个,成本可控,功能完全自定义,那种成就感是买不来的。接下来,我就把自己从构思、选型、搭建到编程调试的完整过程,以及中间踩过的坑和总结的经验,毫无保留地分享出来。

2. 核心硬件选型与电路设计思路

做一个电子沙漏,我们需要让“沙子”(灯光)流动起来,并且能精确控制时间。这决定了我们的硬件核心必须包含:一个大脑(主控)、一条能逐颗控制的“沙流”(可寻址LED灯带)、一个设定时间的“阀门”(输入模块),以及一个提醒时间到的“铃铛”(输出模块)。

2.1 主控板选择:为什么是Arduino Uno?

在众多Arduino开发板中,我最终选择了经典的Arduino Uno R3。这个选择基于几个非常实际的考虑:

首先,生态与兼容性无敌。Uno拥有最庞大的社区支持和资料库。无论你遇到什么问题,几乎都能找到现成的代码示例和解决方案。对于本项目需要的库,如控制LED灯带的Adafruit_NeoPixelFastLED,对Uno的支持都是最完善、最稳定的。

其次,引脚资源与驱动能力足够。Uno有14个数字I/O口和6个模拟输入口。我们只需要1个数字口驱动LED灯带,2~3个数字口连接按钮,1个数字口连接蜂鸣器,资源绰绰有余。其5V/40mA的单引脚驱动能力,也足以直接驱动一小段LED灯带(对于更长灯带需外接电源)。

最后,成本与入门门槛低。Uno板价格相对低廉,且其基于ATmega328P的架构简单直观,非常适合初学者理解微控制器的工作原理。相比之下,ESP32虽然功能更强大(有Wi-Fi/蓝牙),但复杂度更高,对于纯本地计时器项目属于“杀鸡用牛刀”,不仅增加了不必要的成本和功耗,其复杂的电源管理和引脚定义对新手也不够友好。

注意:如果你手头只有Arduino Nano,它和Uno核心相同,只是体积更小,完全可以平替。但要注意Nano的3.3V和5V版本,驱动5V的WS2812B灯带时,可能需要电平转换模块。

2.2 “沙流”的载体:WS2812B可寻址LED灯带

模拟沙粒流动,最理想的元件就是WS2812B灯带。它最大的特点是“可寻址”:每个LED灯珠都有一个独立的控制芯片,我们只需用单片机的一个数据引脚,就能控制整条灯带上成百上千颗灯珠的亮灭和颜色,实现复杂的动态效果。

我选择的是每米60灯珠的软灯条。对于沙漏造型,我们通常需要将灯带弯曲排列成两个三角形(代表上下沙仓)和一个狭窄的通道。60灯珠/米的密度足以让“沙流”看起来连续。你可以根据想要的沙漏大小决定购买的长度,一般30-50厘米就足够了。

为什么不用普通LED?如果使用普通LED,每个LED都需要一个独立的控制引脚,要模拟几十颗“沙粒”的流动,就需要几十个引脚和相应的限流电阻,电路会变得异常复杂和臃肿。WS2812B完美解决了这个问题。

供电是关键:WS2812B灯珠在全白最亮时,单颗电流可达60mA。即使我们只点亮一部分,整条灯带的电流也可能轻松超过Arduino板载稳压芯片的极限(约1A)。因此,必须为LED灯带提供独立的外部5V电源。Arduino的5V引脚仅用于给灯带的数据引脚提供信号参考电平。

2.3 用户交互与反馈模块

  1. 时间设定(输入):我选择了最直观的旋转编码器。它集成了旋转和按下两种操作。旋转可以增减时间(比如每格1分钟),按下可以开始/暂停计时。相比多个独立按钮,它操作更符合直觉,节省引脚,外观也更简洁。编码器通常有A、B、SW(按键)三个信号引脚,分别接Arduino的数字引脚。
  2. 时间到提醒(输出):一个有源蜂鸣器足够了。它内部有振荡电路,只需给一个高电平信号就会持续发声,编程简单。接一个数字引脚即可。如果你想追求更好的音效,可以连接一个无源蜂鸣器或小喇叭,通过PWM播放简单的旋律,但代码会复杂一些。
  3. 可选:状态显示。为了在设定时更直观地看到具体时间,可以增加一个0.96寸OLED显示屏(I2C接口)。它只需占用A4(SDA)和A5(SCL)两个引脚,就能显示数字时间、状态等信息。这对于调试和提升产品完成度很有帮助。

2.4 最终电路连接图与接线清单

整个系统的接线逻辑如下:

  • 电源部分:外部5V电源(如手机充电器+USB线)的正极(+5V)同时接入Arduino的VIN引脚(或通过DC插座)和LED灯带的+5V。所有GND(外部电源、Arduino、LED灯带、编码器、蜂鸣器)必须共地,这是电路正常工作的基础。
  • 信号部分
    • LED灯带DIN-> ArduinoD6(可任选一个PWM引脚,习惯上用6,7,8等)。
    • 旋转编码器CLK->D2,DT->D3,SW->D4(使用2,3引脚是因为它们支持外部中断,可以更稳定地检测旋转)。
    • 蜂鸣器I/O->D5
    • OLED显示屏SDA->A4,SCL->A5

材料清单

  • Arduino Uno R3 开发板 x1
  • WS2812B LED灯带 (60灯/米, 30-50cm) x1
  • 旋转编码器模块(带按键) x1
  • 有源蜂鸣器模块(5V) x1
  • 0.96寸 I2C OLED显示屏(可选) x1
  • 5V/2A以上外部电源适配器 x1
  • USB数据线(用于烧录程序) x1
  • 杜邦线(公对公、母对母)若干
  • 面包板(用于原型搭建) x1
  • 如果需要最终成品,还需准备亚克力板、激光切割外壳、导线、焊台等。

3. 沙漏动态效果的程序逻辑剖析

硬件是骨架,程序才是灵魂。如何让一排LED灯珠像沙子一样“流”下来,是编程的核心。这里面的逻辑,我把它拆解为三个层次:底层驱动、核心算法和状态管理。

3.1 驱动层:与WS2812B灯带高效通信

Arduino社区有两个最常用的库来驱动WS2812B:Adafruit_NeoPixelFastLED。我选择Adafruit_NeoPixel,因为它更轻量,API更简单直观,对于本项目完全够用。

首先需要在Arduino IDE中安装库:点击“工具”->“管理库”,搜索“NeoPixel”并安装Adafruit的库。

初始化灯带非常简单:

#include <Adafruit_NeoPixel.h> #define LED_PIN 6 #define LED_COUNT 30 // 假设我们使用30颗灯珠 Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); void setup() { strip.begin(); strip.show(); // 初始化后清空灯带 strip.setBrightness(100); // 设置亮度(0-255),避免太刺眼 }

NEO_GRB + NEO_KHZ800是大多数WS2812B灯带的配置。strip.setBrightness()非常重要,它不仅保护眼睛,也显著降低了整体电流,避免电源过载。

3.2 核心算法:如何模拟“沙粒”流动?

这是最有趣的部分。我们假设灯带的前15颗灯珠(索引0-14)代表“上沙仓”,后15颗(15-29)代表“下沙仓”。中间可以空出几颗作为“细颈”。

1. 静态映射法(我的首选方案): 这是最直观的方法。我们预先定义好每一颗“沙粒”的位置。例如,总时间设为300秒(5分钟),我们有30颗“沙粒”,那么每颗沙粒代表10秒。计时开始后,每过10秒,我们就将上沙仓最底部的一颗灯珠熄灭,同时将下沙仓最顶部的一颗灯珠点亮成沙色(比如暖黄色)。

int totalSeconds = 300; int grains = 30; int secondsPerGrain = totalSeconds / grains; // 10秒/粒 int upperGrains = grains; // 初始上仓沙粒数 int lowerGrains = 0; unsigned long lastUpdateTime = 0; int elapsedSeconds = 0; void updateHourglass() { if (millis() - lastUpdateTime >= 1000) { // 每秒更新一次 lastUpdateTime = millis(); elapsedSeconds++; // 计算当前应有的沙粒分布 int grainsShouldHaveMoved = elapsedSeconds / secondsPerGrain; int newUpperGrains = grains - grainsShouldHaveMoved; int newLowerGrains = grainsShouldHaveMoved; // 如果沙粒数发生变化,则更新灯带 if (newUpperGrains != upperGrains) { // 清空灯带 for (int i = 0; i < LED_COUNT; i++) { strip.setPixelColor(i, strip.Color(0, 0, 0)); } // 绘制上沙仓 for (int i = 0; i < newUpperGrains; i++) { strip.setPixelColor(i, strip.Color(150, 120, 50)); // 沙色 } // 绘制下沙仓(从末尾倒序绘制) for (int i = 0; i < newLowerGrains; i++) { strip.setPixelColor(LED_COUNT - 1 - i, strip.Color(150, 120, 50)); } strip.show(); upperGrains = newUpperGrains; lowerGrains = newLowerGrains; } } }

这种方法逻辑清晰,计算简单。但缺点是“沙粒”移动是跳跃式的(每10秒跳一次),不够平滑。

2. 动态流动法(增强视觉效果): 为了让流动更平滑,我们可以引入“当前正在下落的沙粒”概念。即除了计算已固定的沙粒,还用一颗高亮或不同颜色的灯珠,从上到下扫描,模拟一粒沙正在下落的过程。

int flowingGrainPos = -1; // -1表示没有正在下落的沙粒 unsigned long flowStartTime = 0; int flowDuration = 1000; // 下落动画持续1秒 void updateSmoothHourglass() { // ... 计算固定沙粒部分同上 ... // 处理流动沙粒动画 if (elapsedSeconds % secondsPerGrain == 0 && elapsedSeconds < totalSeconds) { // 到达一个沙粒该下落的时间点 flowingGrainPos = 0; // 从上仓顶部开始 flowStartTime = millis(); } if (flowingGrainPos >= 0) { unsigned long flowElapsed = millis() - flowStartTime; if (flowElapsed < flowDuration) { // 计算流动沙粒的当前位置(从上仓底部移动到下仓顶部) float progress = (float)flowElapsed / flowDuration; int totalFlowPath = (LED_COUNT / 2) + 2; // 假设的流动路径长度 int currentPos = progress * totalFlowPath; // 先绘制固定沙粒... // 再在currentPos位置绘制一颗高亮的流动沙粒 strip.setPixelColor(map(currentPos, 0, totalFlowPath, 0, LED_COUNT-1), strip.Color(255, 220, 100)); strip.show(); } else { flowingGrainPos = -1; // 动画结束 } } }

这种方法视觉效果更好,但逻辑更复杂,需要更精细的状态管理和动画插值计算。

3.3 状态机:让程序有条不紊地运行

一个健壮的程序必须能清晰地区分不同状态。我们的沙漏计时器至少有以下几个状态:

  • STATE_SETTING:设定时间状态。此时旋转编码器调整时间,LED灯带可能显示一个进度条或全部熄灭。
  • STATE_RUNNING:计时运行状态。LED开始流动,编码器按下可暂停。
  • STATE_PAUSED:暂停状态。LED保持当前状态,编码器按下可继续。
  • STATE_FINISHED:计时结束状态。蜂鸣器报警,LED可能闪烁或变色,按编码器重置。

使用enum定义状态,并用一个全局变量currentState来记录当前状态。在loop()函数中,通过一个switch-case语句来根据不同状态执行不同的逻辑和响应不同的输入。

enum TimerState { SETTING, RUNNING, PAUSED, FINISHED }; TimerState currentState = SETTING; void loop() { switch (currentState) { case SETTING: handleSetting(); // 处理编码器旋钮,调整时间 displaySettingTime(); // 在OLED上显示设定时间 break; case RUNNING: updateHourglass(); // 更新沙漏动画 checkIfFinished(); // 检查是否计时结束 handlePauseButton(); // 检查是否按下暂停 break; case PAUSED: handleResumeButton(); // 检查是否按下继续 break; case FINISHED: triggerAlarm(); // 触发蜂鸣器和灯光警报 handleResetButton(); // 检查是否按下重置 break; } // 其他公共处理,如读取编码器值 }

这种状态机模式让程序结构非常清晰,易于调试和扩展。比如未来你想增加一个“倒计时结束后自动开始下一个周期”的功能,只需要在FINISHED状态里增加一个分支逻辑即可。

4. 旋转编码器的可靠读取与防抖处理

旋转编码器是交互的核心,但它也是硬件消抖的重点对象。机械编码器在触点闭合和断开时会产生毛刺信号(抖动),如果不处理,一次旋转可能会被误读成多次。

4.1 硬件消抖与软件消抖的结合

硬件消抖(推荐):最简单的办法是在编码器的A、B引脚与GND之间各接一个0.1uF的陶瓷电容。电容可以吸收瞬间的电压抖动。很多集成了电阻电容的编码器模块已经帮你做了这件事。

软件消抖(必须):无论硬件是否处理,软件消抖都是最后一道防线。核心思路是:在检测到电平变化后,等待一小段时间(比如几毫秒),再次读取引脚状态,如果状态稳定,才确认是一次有效的动作。

对于编码器,我们通常采用状态机读取法,并结合延时消抖。但更优雅的方式是利用Arduino的外部中断状态判断

#define ENC_A 2 #define ENC_B 3 #define ENC_SW 4 volatile int encoderPos = 0; // 用于中断服务程序修改的变量必须加volatile int lastEncoded = 0; void setup() { pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); pinMode(ENC_SW, INPUT_PULLUP); // 为A、B相启用外部中断,在电平变化时触发 attachInterrupt(digitalPinToInterrupt(ENC_A), updateEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_B), updateEncoder, CHANGE); } void updateEncoder() { // 简单的防抖:记录触发时间,短时间内多次触发忽略 static unsigned long lastInterruptTime = 0; unsigned long interruptTime = millis(); if (interruptTime - lastInterruptTime > 5) { // 防抖间隔5ms int MSB = digitalRead(ENC_A); int LSB = digitalRead(ENC_B); int encoded = (MSB << 1) | LSB; // 将A、B状态组合成一个2位二进制数 int sum = (lastEncoded << 2) | encoded; // 将上次和本次状态组合 // 根据状态变化表判断方向。常见编码器顺序:00->01->11->10->00(顺时针) if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderPos++; else if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderPos--; lastEncoded = encoded; } lastInterruptTime = interruptTime; }

这段代码将A、B相的四种状态(00,01,11,10)组成一个序列,通过判断序列的变化方向来确定旋转方向。volatile关键字告诉编译器这个变量可能被中断程序修改,防止优化出错。5ms的防抖时间窗口在实践中效果很好。

4.2 按键处理与长按功能

编码器上的按键(SW)通常用于开始/暂停/确认。我们同样需要消抖,并且可以实现短按长按的不同功能。

bool buttonPressed = false; unsigned long pressStartTime = 0; const unsigned long longPressDuration = 1000; // 长按判定为1秒 void checkButton() { int buttonState = digitalRead(ENC_SW); if (buttonState == LOW) { // 按键按下(假设按下为低电平) if (!buttonPressed) { buttonPressed = true; pressStartTime = millis(); // 记录按下时刻 } else { // 按键持续按下中 unsigned long holdTime = millis() - pressStartTime; if (holdTime > longPressDuration) { // 触发长按动作,例如重置计时器 // 注意:这里要防止长按期间重复触发,可以设置一个标志位 } } } else { // 按键释放 if (buttonPressed) { buttonPressed = false; unsigned long holdTime = millis() - pressStartTime; if (holdTime < 50) { // 忽略过短的抖动,可能是噪声 } else if (holdTime < longPressDuration) { // 触发短按动作 handleShortPress(); } // 长按动作在释放时也可以触发,取决于设计 } } }

handleShortPress()函数里,根据currentState来决定行为:在SETTING状态下按=开始计时;在RUNNING状态下按=暂停;在PAUSED状态下按=继续;在FINISHED状态下按=重置回SETTING

5. 系统集成、调试与外壳制作

当所有模块的代码都测试通过后,就需要将它们整合到一个完整的sketch中,并进行系统级调试。

5.1 主程序框架与整合要点

一个完整的ArduinoHourglass.ino文件结构大致如下:

#include <Adafruit_NeoPixel.h> #include <Wire.h> // 如果使用OLED #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> // 如果使用OLED // 引脚定义 #define LED_PIN 6 #define ENC_A 2 ... // 常量与变量定义 const int TOTAL_GRAINS = 30; ... enum TimerState { SETTING, RUNNING, PAUSED, FINISHED }; TimerState currentState; int setTimeSeconds; // 设定的总时间 int remainingSeconds; // 剩余时间 ... // 对象初始化 Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); // Adafruit_SSD1306 display(128, 64, &Wire, -1); // OLED对象 void setup() { Serial.begin(115200); // 用于调试输出 strip.begin(); strip.show(); pinMode(ENC_A, INPUT_PULLUP); // ... 其他初始化 attachInterrupt(...); currentState = SETTING; setTimeSeconds = 300; // 默认5分钟 } void loop() { checkEncoder(); // 读取编码器旋转和按键,更新全局变量 updateDisplay(); // 更新OLED显示(如果有) switch (currentState) { case SETTING: // 根据编码器旋转更新 setTimeSeconds // 根据setTimeSeconds计算并显示LED预览(比如用灯带长度表示比例) break; case RUNNING: updateHourglassAnimation(); // 核心动画函数 remainingSeconds = calculateRemainingTime(); if (remainingSeconds <= 0) { currentState = FINISHED; } break; case PAUSED: // 保持LED状态不变 break; case FINISHED: playAlarm(); // 控制蜂鸣器和LED闪烁 break; } } // 以下是各个功能函数的具体实现 void updateHourglassAnimation() { ... } void checkEncoder() { ... } ...

整合关键点

  1. 变量作用域:确保在需要的地方能访问到关键变量,如remainingSecondscurrentState。通常定义为全局变量。
  2. 时间管理:避免使用delay()函数,它会阻塞整个程序。所有计时(如动画帧、按钮防抖、倒计时)都应使用millis()函数进行非阻塞式判断。
  3. 中断与主循环的共享变量:在中断服务程序(如updateEncoder())中修改的变量(如encoderPos),在主循环中读取时,如果变量大于1字节(如int),可能需要暂时关闭中断来安全读取,或者使用原子操作。对于本项目,encoderPos只是简单的增减,风险较低。

5.2 常见问题排查与调试技巧

在集成过程中,你几乎一定会遇到一些问题。以下是我踩过的坑和解决方法:

问题1:LED灯带闪烁、颜色错乱或部分不亮。

  • 检查电源:这是最常见的原因。确保使用独立5V电源为灯带供电,且电源功率足够(至少2A)。用万用表测量灯带输入端的电压,在全部点亮白色时,电压不应低于4.8V。
  • 检查共地:务必把外部电源的GND、Arduino的GND、灯带的GND连接在一起。
  • 检查数据线:数据线长度不宜过长(最好小于50cm),且远离电源线,避免干扰。可以在数据引脚和灯带数据输入之间串联一个100-500欧姆的电阻,有助于抑制信号振铃。
  • 检查代码:确保strip.show()只在完成一帧所有LED颜色设置后才调用,频繁调用会导致刷新异常。

问题2:旋转编码器读数不准,左右旋转识别错误或跳动。

  • 确认接线:确保A、B相没有接反。可以尝试交换A、B引脚的接线。
  • 调整防抖时间:在updateEncoder()函数中,增加防抖判断的延时值,比如从5ms调到10ms。
  • 检查中断引脚:Uno只有引脚2和3支持外部中断。确保编码器A、B相接在了这两个引脚上。
  • 逻辑分析:打开串口监视器,打印出MSBLSB的值,观察旋转时输出的序列是否符合预期(00->01->11->10为顺时针)。如果序列不对,可能是编码器质量或接线问题。

问题3:程序运行一段时间后卡死或无响应。

  • 内存泄漏:检查是否在loop()中不断创建新的对象或字符串,导致内存耗尽。尽量使用全局或静态变量。
  • 中断冲突millis()delay()函数依赖定时器中断。如果你使用了其他库或代码修改了定时器配置,可能会导致这些函数失效。确保使用的库兼容。
  • 电源不稳定:尝试用电脑USB直接供电,排除外部电源问题。

调试利器——串口监视器:在代码关键位置加入Serial.print()语句,输出变量值(如currentStateremainingSecondsencoderPos),是追踪程序逻辑流最有效的方法。

5.3 从面包板到成品:外壳设计与制作

一个精致的项目离不开好外壳。对于沙漏,外观至关重要。

设计思路

  1. 结构:设计一个垂直的、中间有狭窄通道的柱体。内部两侧粘贴LED灯带。可以用激光切割3mm亚克力板来制作,结构稳定且透光性好。
  2. 光扩散:直接看LED灯珠会很刺眼。需要在灯带前方加一层磨砂亚克力板乳白色的匀光板,让光线柔和均匀,形成真正的“沙粒”光斑。
  3. 开孔:在外壳侧面或底部为旋转编码器开孔,顶部或侧面为蜂鸣器开出声孔。为Arduino的USB口和电源接口留出开口。
  4. 固定:使用亚克力胶水或螺丝螺母固定。内部用热熔胶或尼龙柱固定电路板和电池。

制作流程

  1. 建模:使用Fusion 360、SketchUp或甚至简单的Inkscape(用于激光切割二维图纸)设计外壳零件。
  2. 加工:将设计文件交给激光切割服务商,切割亚克力板。
  3. 组装:先组装内部结构,固定Arduino、电源模块、灯带。连接所有导线,并用扎带整理。
  4. 测试:组装过程中,每完成一步就通电测试一次,确保没有压坏线或接错。
  5. 最终封装:最后盖上扩散板和前面板,上紧螺丝。

一个贴心的细节:可以在外壳底部增加四个小小的橡胶脚垫,防止刮花桌面,也更稳固。

6. 功能扩展与进阶玩法

基础版本完成后,你可以根据自己的想法添加更多功能,让它变得更智能、更个性化。

6.1 增加OLED显示屏提升交互

加上一个I2C接口的小OLED屏,用户体验会提升一个档次。它可以显示:

  • 设定时间(SETTING状态):Set: 05:00
  • 剩余时间(RUNNING/PAUSED状态):Remaining: 04:23
  • 状态提示:RUNNINGPAUSEDTIME'S UP!
  • 进度条:用图形化方式显示剩余时间比例。

使用Adafruit_SSD1306Adafruit_GFX库可以轻松实现。注意在setup()中初始化Wiredisplay对象,并在loop()中根据状态更新显示内容。记得避免在loop中频繁调用display.clearDisplay()display.display(),只更新需要变化的部分区域以提升性能。

6.2 多种计时模式与预设

让沙漏不止能倒计时5分钟。

  • 模式切换:通过长按编码器按键,在“番茄工作法(25分钟)”、“短休息(5分钟)”、“长休息(15分钟)”等预设模式间循环切换。
  • 自定义存储:使用Arduino的EEPROM(电可擦可编程只读存储器)来保存用户最后设定的时间,即使断电也不会丢失。
#include <EEPROM.h> void saveTimeToEEPROM(int time) { EEPROM.put(0, time); // 将时间存入地址0 } void readTimeFromEEPROM() { int savedTime; EEPROM.get(0, savedTime); if (savedTime > 0) setTimeSeconds = savedTime; }

6.3 联网与高级控制(使用ESP32)

如果你对网络功能感兴趣,可以将主控换成ESP32。这打开了新世界的大门:

  • Web配置界面:让沙漏连接Wi-Fi,你可以在手机或电脑的浏览器上输入一个IP地址,打开一个网页来设置时间、选择灯光颜色和模式,无需物理旋钮。
  • NTP网络对时:从互联网时间服务器获取精确时间,可以做一个永远准确的“时钟沙漏”。
  • MQTT与智能家居联动:计时结束时,通过MQTT协议向Home Assistant等平台发送消息,自动打开窗帘、播放音乐等。
  • 无线更新(OTA):直接通过Wi-Fi更新程序,无需插线。

当然,这需要你学习ESP32的开发、网络编程和相应的库,项目复杂度会大大增加,但可玩性也呈指数级上升。

6.4 灯光效果与声音的个性化

  • 多彩沙粒:不一定只用黄色。可以让“沙粒”的颜色随时间渐变,比如从绿色(时间充裕)到橙色(时间过半)再到红色(即将结束)。
  • 结束特效:计时结束时,可以让所有LED像烟花一样闪烁,或者跑一个彩虹循环。
  • 更悦耳的提示音:用无源蜂鸣器配合tone()函数,播放一小段简单的旋律,比如《欢乐颂》的前几个音符,比单调的蜂鸣声悦耳得多。

这个项目最吸引人的地方,就在于它从一个简单的想法出发,通过硬件和代码的不断打磨,最终能变成一个既实用又有温度的个性化作品。从点亮第一颗LED,到沙粒顺畅流动,再到装上亲手制作的外壳,每一步都充满了创造的乐趣。希望这份详细的指南能帮你少走弯路,顺利做出属于你自己的Arduino沙漏计时器。如果在制作过程中遇到任何问题,不妨回头检查一下电源、接地和代码中的状态逻辑,大多数问题都出在这几个地方。祝你制作愉快!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/20 6:14:40

SSM框架构建医院招聘考试管理系统实战解析

1. 项目概述&#xff1a;医院招聘考试管理系统的核心价值医院作为专业技术密集型机构&#xff0c;每年都需要通过严格的招聘考试筛选人才。传统纸质考试或简单电子化方案存在组卷效率低、防作弊弱、数据统计难等痛点。这个基于SSM框架的医院招聘考试管理系统&#xff0c;正是为…

作者头像 李华
网站建设 2026/8/20 6:12:58

基于BW21-CBV-Kit与HC-SR04的超声波测距实战指南

1. 项目概述&#xff1a;用BW21-CBV-Kit玩转超声波测距最近在捣鼓一个基于BW21-CBV-Kit开发板的超声波测距项目&#xff0c;核心就是用那个经典的HC-SR04超声波模块来测量距离。这听起来像是电子爱好者的入门项目&#xff0c;但实际做下来&#xff0c;你会发现从接线、编程到数…

作者头像 李华
网站建设 2026/8/20 6:11:27

TomTom与ParkWhiz深度集成:智能停车如何重构地图导航与出行服务生态

1. 从“找车位”到“停好车”&#xff1a;一次出行体验的底层重构如果你在北美开过车&#xff0c;尤其是在旧金山、纽约、多伦多或者芝加哥这样的核心都市区&#xff0c;那你一定对“找车位”这件事深恶痛绝。这绝不仅仅是多绕几圈路的体力消耗&#xff0c;更是一场对时间、金钱…

作者头像 李华
网站建设 2026/8/20 6:06:56

STAR框架:构建失败感知的多智能体马尔可夫路由机制

1. 项目缘起&#xff1a;当多智能体在时空迷宫中“迷路”最近在折腾一个多智能体协同推理的项目&#xff0c;核心场景是让一群智能体在复杂的时空环境中&#xff08;比如一个动态变化的虚拟城市地图&#xff0c;或者一个实时更新的物流网络&#xff09;协作完成一个任务&#x…

作者头像 李华
网站建设 2026/8/20 6:06:56

DIY动漫主题U盘全攻略:从3D建模到软件定制的个性化数据存储方案

1. 项目概述&#xff1a;当U盘遇上“两面宿傩”最近在DIY圈和二次元爱好者社区里&#xff0c;一个结合了实用性与收藏价值的项目悄然走红——“Thumb Drive- Ryomen Sukuna Edition”&#xff0c;直译过来就是“两面宿傩版U盘”。这不仅仅是一个简单的U盘&#xff0c;它是一个将…

作者头像 李华
网站建设 2026/8/20 6:00:59

基于价值驱动的多智能体模拟:构建教育社会动力学计算模型

1. 项目概述&#xff1a;当教育遇上多智能体模拟最近在琢磨一个挺有意思的事儿&#xff1a;我们总说教育是个复杂的系统工程&#xff0c;师生互动、生生互动、家校关系&#xff0c;这些动态交织在一起&#xff0c;构成了一个活生生的“教育社会”。但怎么去量化、去模拟、去预测…

作者头像 李华