Micro:bit沙漏计时器:从软件循环到动画显示的嵌入式实践

Micro:bitMakeCode嵌入式开发
于 2026-06-01 13:03:49 修改
·本内容遵循CC 4.0 BY-SA版权协议

1. 项目概述:一个看得见时间的“沙漏”

在嵌入式开发的学习和实践中,计时器是一个绕不开的经典项目。它看似简单,却串联起了状态管理、用户交互、动画显示和中断处理等多个核心概念。今天,我想分享一个基于Micro:bit的“鸡蛋计时器”项目。这个项目的特别之处在于,它没有采用枯燥的数字倒计时,而是用一块5x5的LED点阵,模拟了一个沙漏中沙子缓缓流下的动画,让时间的流逝变得直观而有趣。无论是用于厨房烹饪计时,还是作为桌面上的一个专注工具,它都能在完成任务时播放一段悦耳的旋律作为提醒。

这个项目非常适合刚接触Micro:bit和嵌入式编程的朋友。你不需要复杂的电路,只需要一块Micro:bit V1或V2开发板,以及微软的MakeCode在线图形化编程环境。我们将通过它,一步步实现从1到9分钟的可调计时、动画显示、按钮控制以及低功耗显示管理。整个过程,我会详细拆解每一个代码块背后的逻辑,并分享我在调试过程中积累的一些实用技巧和容易踩坑的地方。

2. 核心设计思路与方案选型

2.1 为什么选择Micro:bit与MakeCode?

Micro:bit是一款为教育设计的微型计算机,其集成的5x5 LED点阵和两个物理按钮(A和B),为我们实现这个可视化计时器提供了完美的硬件基础。我们无需外接任何显示屏或输入设备,极大地降低了入门门槛和项目复杂度。

在编程环境上,我选择了MakeCode。对于初学者而言,其积木块式的图形化编程界面非常友好,能避免语法错误,让人更专注于逻辑构建。同时,MakeCode也支持一键转换为JavaScript或Python代码,方便学习者后续向文本编程过渡。这个项目的所有逻辑,都可以通过清晰的积木块组合来完成,包括变量操作、循环判断和事件响应。

2.2 计时方案:软件循环 vs. 硬件定时器

实现计时的核心有两种思路:硬件定时器中断和软件延时循环。硬件定时器精度高、不占用CPU资源,但配置相对复杂。对于这个“鸡蛋计时器”项目,时间精度要求到分钟级别即可,不需要毫秒级的精确度。因此,我们采用了更直观易懂的软件循环计时方案。

具体来说,我们通过一个无限循环块来构建主程序框架。在循环中,我们检查计时是否启动,然后通过累加一个时间变量来模拟时间的流逝。关键在于,我们需要校准循环一次的实际时间。项目文档中提到,通过控制沙漏动画每一帧的显示时间(默认500毫秒)和总帧数(10帧),可以计算出完成一次“沙漏流空”动画的周期是5秒。那么,循环12次就是1分钟。这个计算过程是理解整个计时逻辑的钥匙,我将在后续的实操部分详细展开。

2.3 用户交互与状态机设计

一个友好的计时器需要清晰的状态和明确的交互反馈。本项目定义了三个核心状态:设置时间计时运行计时结束/空闲。我们使用两个按钮来在这几个状态间切换:

  • 按钮A:在空闲状态下,循环增加预设时间(1-9分钟),并在点阵上显示当前设置的数字。
  • 按钮B:作为开始/暂停键。在空闲时按下开始计时并播放动画;在计时中按下则暂停,动画和计时停止。
  • 按钮A+B:无论当前处于何种状态,同时按下都执行全局复位,将所有变量恢复初始值,并显示“RST”提示。

这种设计模仿了真实物理计时器的操作逻辑,简单直观。在代码实现上,我们需要用几个布尔型变量(如计时启动)来标记当前状态,并在无限循环按钮事件中根据这些状态变量来决定执行何种操作。

3. 项目构建详解:从变量到动画

3.1 变量初始化与程序基石

任何程序都需要一个稳定的起点。在MakeCode中,当开机时积木块就是程序的起点。这里我们需要完成所有变量的初始化工作,这就像盖房子前打好地基。

首先,创建并初始化核心变量:

  • 预设时间:设置为1,代表默认计时1分钟。
  • 已过时间:设置为0,用于记录已经走了多久。
  • 计时启动:设置为,表示初始状态为停止。
  • 显示暗淡:设置为,用于控制是否进入省电的屏幕暗淡模式。

对于Micro:bit V2,它内置了扬声器,我们需要在开机时打开声音。而对于V1版本,则需要将声音模式设为关闭,因为它必须外接扬声器才能发声。这个版本判断和设置非常重要,否则V1用户可能会发现程序无法运行或报错。

注意:变量的命名尽量使用中文或清晰的英文,如“timeSet”、“elapsedTime”,这样在复杂的积木逻辑中更容易辨认。一个良好的初始化习惯是,把所有变量创建和赋初值的积木都放在当开机时里,避免出现变量未定义就使用的错误。

3.2 灵魂所在:沙漏动画序列的绘制

沙漏动画是这个项目的视觉灵魂。在5x5的有限像素里表现沙子流动,需要一点巧思。我们不是真的让一个光点从上往下移动,而是设计一系列(10张)静态图像,快速连续播放,利用人眼的视觉暂留形成动画。

动画的设计思路是“自上而下地填充”:

  1. 第一帧:只有最顶部中央的LED点亮。
  2. 后续帧:顶部亮起的LED逐渐减少,同时底部亮起的LED逐渐增多。
  3. 最后一帧:只有最底部中央的LED点亮。 这样,就形成了沙子从上半部分“落”到下半部分的视觉效果。

在MakeCode中,我们可以使用显示图案积木,并点击5x5的网格来手动“点亮”LED,绘制每一帧的图像。需要创建10个这样的图像变量,例如frame0, frame1... frame9。在当开机时初始化它们,然后在循环中按顺序显示。

JAVASCRIPT
// 这是MakeCode生成的JavaScript代码片段,用于理解动画帧定义
let frame0 = images.createImage(`
. . # . .
. . . . .
. . . . .
. . . . .
. . . . .
`)
let frame1 = images.createImage(`
. . . . .
. . # . .
. . . . .
. . . . .
. . . . .
`)
// ... 以此类推定义frame2到frame8
let frame9 = images.createImage(`
. . . . .
. . . . .
. . . . .
. . . . .
. . . # .
`)

3.3 用户输入:按钮功能的逻辑实现

按钮响应是交互的核心,我们使用当按钮A被按下时当按钮B被按下时当按钮A+B被按下时这三个事件块。

按钮A(设置时间): 每次按下,让预设时间变量增加1。但我们需要将其限制在1到9之间。逻辑是:如果预设时间已经等于9,就将其重置为1;否则就加1。同时,立即在LED点阵上显示这个数字,给用户即时反馈。在这个模式下,应确保显示暗淡模式被关闭,让用户能看清设置。

按钮B(开始/暂停): 这是一个“翻转开关”。我们使用一个如果...那么...否则...的判断:

  • 如果当前计时启动,则将其设为,并可能重置已过时间为0(如果是全新开始),然后启动沙漏动画。
  • 如果当前计时启动,则将其设为,暂停动画和计时,并可能触发一个延迟暗淡屏幕的计时。

按钮A+B(复位): 这是最高优先级的操作。无论程序在什么状态,一旦同时按下A和B,就执行复位:将预设时间已过时间重置为初始值(如1和0),将计时启动设为,并清除屏幕显示一个“RST”图案,提示用户复位成功。

4. 核心循环与计时逻辑剖析

4.1 “无限循环”:

程序的主引擎

MakeCode中的无限循环块内的代码会以尽可能快的速度重复执行。我们的所有动态逻辑——计时、动画更新、屏幕暗淡——都发生在这里。循环体的执行速度决定了我们计时的精度。

首先,循环内要判断计时启动变量是否为。如果为假,则大部分计时和动画逻辑都会被跳过,程序可能只执行检查屏幕暗淡等后台任务。如果为真,则进入核心计时流程:

  1. 更新动画:根据当前已过时间对应的进度,计算并显示沙漏动画的某一帧。
  2. 更新时间:执行我们校准后的“延时”,然后增加已过时间(可能是秒或循环次数的计数)。
  3. 检查超时:判断已过时间是否达到预设时间。如果达到,则停止计时(计时启动设为),播放提示旋律,并准备进入屏幕暗淡模式。

4.2 计时校准:让循环“一秒”真实一秒

这是项目的关键难点。无限循环跑得很快,我们如何让它精确地代表真实时间呢?文档给出了公式:1分钟 = 12次循环

推导过程如下:

  1. 沙漏动画有10帧图像。
  2. 每帧图像显示时间设为imageTime(例如500毫秒)。
  3. 每帧之间可以插入一个pause(暂停)用于微调,默认0毫秒。
  4. 那么,播放完一轮完整的10帧动画所需时间是:10 * (imageTime + pause)
  5. 假设imageTime = 500ms, pause = 0ms,则一轮动画耗时 10 * 500ms = 5000ms = 5秒
  6. 要让已过时间变量增加1分钟(代表现实60秒),就需要循环 60秒 / 5秒 = 12 次。

因此,我们在循环内部,不能简单地用暂停(60000)来等待一分钟,因为那样会阻塞动画。而是应该在每次循环中,让程序“忙碌”大约5秒钟(通过连续显示10帧动画来实现),然后才让已过时间增加1个单位(代表1分钟)。这样,动画和计时就同步了。

在代码中,我们可以用一个变量循环计数来实现。每完成一轮10帧动画,循环计数加1。当循环计数达到12时,就让已过时间加1(分钟),并将循环计数归零。

4.3 屏幕暗淡与电源管理

为了节省电量(虽然Micro:bit耗电极低)并在计时结束后提供更柔和的视觉提示,我们加入了屏幕暗淡功能。

我们使用一个变量暗淡计数器(或叫Z,如文档所述)来实现。当计时结束或用户暂停时,可以将一个标志位开始暗淡设为。在无限循环中,如果开始暗淡为真,则每次循环都让暗淡计数器减少1。暗淡计数器初始值设为255(最大亮度),每次减1,直到0(完全熄灭)。

Micro:bit的显示亮度积木可以接受一个0-255的值。我们将暗淡计数器的值赋给显示亮度,就能实现平滑的淡出效果。从255减到0,如果每次循环间隔约25毫秒,那么总共需要约6.4秒(255*0.025)完成暗淡过程,这与文档描述吻合。

5. 功能集成与最终操作流程

将以上所有模块组合起来,完整的操作流程如下:

  1. 上电启动:Micro:bit显示一个初始的沙漏图案(可能是半满状态)或直接显示数字“1”。
  2. 设置时长:按A键,LED点阵上显示的数字会从1递增到9,再按则回到1。这个数字就是你想要的计时分钟数。
  3. 开始计时:按B键,沙漏动画开始播放,沙子(亮起的LED)从上方向下方“流淌”。动画会循环进行。
  4. 暂停/继续:在计时过程中,再次按下B键,动画和计时会暂停。屏幕可能保持当前画面。第三次按下B键,会从暂停处继续计时和动画。
  5. 计时结束:当设定的时间到达时,动画停止,屏幕会逐渐变暗(约6秒后熄灭)。同时,Micro:bit V2会播放一段内置的旋律(如“生日快乐”前奏)。对于V1,如果你连接了外置扬声器到P0和GND引脚,也能听到声音。
  6. 复位:在任何时候,同时按下A键和B键,计时器会完全复位。屏幕显示“RST”,所有变量回到初始状态,准备下一次设置。

6. 常见问题与调试心得

在实现这个项目时,你可能会遇到以下几个典型问题:

1. 计时不准,过快或过慢

  • 原因:这是最常见的问题,根源在于循环周期的校准。
  • 排查:检查无限循环内一次循环所做的事情。如果除了播放10帧动画,你还加入了其他耗时的操作(比如复杂的计算或额外的暂停),那么循环总时间就会超过5秒,导致现实时间过了1分钟,但你的程序还没循环完12次。
  • 解决:文档中提到的pause变量就是用于微调的。如果你发现计时慢了(现实时间过了,程序还没走完),可以适当减少pause的值,甚至将每帧图像的显示时间imageTime从500毫秒略微调低,比如490毫秒。反之则调高。你可以用手机秒表进行校准。

2. 按钮响应不灵敏或连击

  • 原因:Micro:bit的按钮检测是软件查询式的,在无限循环中如果某些操作(如长暂停)阻塞了程序,就可能错过快速的按键。
  • 解决:确保无限循环内没有使用长时间的暂停积木。所有的延时都应通过动画帧的显示间隔来实现。MakeCode的按钮事件是硬件中断驱动的,相对可靠,但要确保你的主循环不会长时间“卡死”。

3. Micro:bit V1 没有声音

  • 原因:V1版本没有内置扬声器。
  • 解决:必须按照文档提示,在初始化时将扬声器模式设为关闭。然后,你需要一个外接的无源蜂鸣器或小扬声器。将它的正极(或信号线)连接到Micro:bit的P0引脚,负极连接到GND引脚。这样,当程序播放旋律时,声音就会从外接设备发出。

4. 动画显示闪烁或不流畅

  • 原因:可能在显示下一帧图像之前,使用了清除屏幕积木,造成了短暂的黑屏。
  • 解决:直接使用显示图案积木来显示下一帧,它会自动覆盖上一帧的内容,无需手动清屏。确保10帧图像之间的切换是连续的,没有不必要的停顿。

5. 复位功能(A+B)有时不生效

  • 原因:可能是在当按钮A被按下时当按钮B被按下时的事件处理程序中,做了某些阻止复位判断的事情。
  • 解决当按钮A+B被按下时是一个独立的事件,优先级最高。确保在这个事件处理程序中,直接、无条件地执行所有变量的重置和屏幕“RST”显示,不要依赖其他复杂的条件判断。

我的调试心得: 在连接硬件之前,先充分利用MakeCode的模拟器。模拟器可以完美模拟按钮点击和LED显示,对于调试动画逻辑和基本状态流转非常高效。我将计时的一分钟循环缩短为几秒钟(比如修改为循环2次代表1分钟)来进行快速功能验证,等所有逻辑都正确后,再改回准确的12次循环。对于变量的值,善用串口写入数值积木,将预设时间已过时间循环计数等变量实时输出到电脑的串口监视器上,这是洞察程序内部状态、定位计时不准问题的利器。最后,代码的模块化很重要,把动画控制、计时逻辑、亮度控制分别用不同的函数或代码片段组织,会让调试和后续修改清晰很多。这个沙漏计时器项目,虽然小,但五脏俱全,很好地体现了嵌入式系统开发中状态、时间和交互的核心思想。