基于micro:bit的双人刷牙计时器:状态机与LED动画设计实践 1. 项目概述与设计思路最近在辅导孩子养成良好生活习惯时发现让他们坚持刷满牙医推荐的三分钟是个老大难问题。口头计时不准手机计时又容易分心市面上专门的计时器要么功能单一要么价格不菲。正好手头有几块闲置的micro:bit开发板这玩意儿LED点阵屏能显示按钮能交互本身又是个微型电脑做个定制化的刷牙计时器再合适不过了。但家里有两个孩子作息时间还经常错开传统的单任务计时器就不够用了。于是我就琢磨着设计一个能支持双人独立计时的系统核心目标就两个第一让两个孩子能随时、独立地启动属于自己的三分钟刷牙计时互不干扰第二要用最直观有趣的动画吸引孩子把刷牙从“任务”变成“游戏”。这个项目的核心逻辑并不复杂但其中涉及几个在嵌入式编程特别是基于图形化编程环境如MakeCode开发时非常典型且实用的设计思路。首先是多任务模拟。micro:bit作为单核微控制器本身并不支持真正的操作系统级多任务并行。我们需要通过状态机和时间片轮询的思想在同一个主循环里管理两个独立的计时器状态和动画显示。其次是有限显示资源的复用。micro:bit的LED点阵只有5x5要在这么小的空间里同时展示两个进度动画还不能显得杂乱就需要巧妙的像素布局和动画设计。最后是低功耗考量。虽然micro:bit功耗本身不高但作为可能长期待机的设备养成良好的编程习惯在任务结束后主动关闭屏幕能显著延长电池寿命减少更换频率。整个方案的成本极低核心就是一块micro:bit V1开发板V2同样兼容通过USB线连接电脑进行编程完成后用两节AAA电池供电即可独立运行。软件上我们完全使用微软的MakeCode for micro:bit在线图形化编程平台无需安装任何本地环境对新手和家长极其友好。下面我就把从思路到实现的完整过程以及过程中踩过的坑和总结的技巧毫无保留地分享出来。2. 核心设计双独立计时器与LED动画2.1 双计时器的逻辑与变量设计实现双人独立计时的关键在于“状态隔离”。想象一下这就像在一个房间里为两个孩子各设一个独立的沙漏沙漏的启动、流动和结束都是各自独立的。在代码里我们通过创建两套完全独立的变量来实现这种隔离。在MakeCode中我们至少需要为每个计时器定义以下变量计时器激活状态一个布尔Boolean变量例如timer1_active和timer2_active。用于记录该计时器是否正在运行。这是最重要的状态标志后续所有逻辑判断都基于它。剩余时间一个数字Number变量例如time_left_1和time_left_2。用于记录该计时器还剩多少时间通常以秒或毫秒为单位。我们选择以秒为单位因为三分钟180秒对于孩子来说更直观计算也方便。最后更新时间戳一个数字变量例如last_update_1和last_update_2。用于记录该计时器上一次更新剩余时间是在哪个时刻。这是实现准确计时的核心我们通过对比当前运行时间running time与这个时间戳的差值来判断是否过去了一秒。为什么需要“最后更新时间戳”这是嵌入式计时中的一个经典方法。你不能简单地在循环里让变量time_left每秒减1因为循环执行的速度远远快于1秒。正确做法是在计时器启动时记录一个开始时间点。在主循环中不断用当前时间减去开始时间得到已流逝的时间再用总时长减去已流逝时间得到剩余时间。running time块返回的是系统上电后的毫秒数非常适用于做时间差计算。具体到操作上在MakeCode的“变量”类别中点击“创建一个变量”分别创建上述变量。命名建议清晰且有规律例如用后缀“_1”和“_2”来区分。良好的命名习惯在调试时能帮你省下大量时间。2.2 5x5 LED点阵上的双动画策略micro:bit的5x5 LED点阵是唯一的视觉输出设备要在上面同时展示两个进度必须精心规划像素空间。直接画两个完整的动画肯定会重叠导致画面混乱。这里我采用了“分区显示”和“简化图标”的策略。一个直观的分区方法是左右分屏将5列像素分为左3列和右2列或者左2列和右3列分别代表两个计时器。但经过实测5列像素平分左2右2中间1空会导致每个区域过于狭窄动画表现力弱。我最终采用的方案是行分区与动态结合状态指示区最上面一行Y坐标0或最下面一行Y坐标4的5个LED用于显示哪个计时器在运行。例如当仅计时器1运行时点亮最左侧一个LED仅计时器2运行时点亮最右侧一个LED两者都运行时点亮左右两个LED。主动画区中间三行Y坐标1, 2, 3的像素用于显示核心的刷牙动画。当只有一个计时器运行时动画可以占据全部15个像素。当两个计时器同时运行时则需要进行压缩。一种有效的压缩方法是交替显示在主循环中以一定频率如每500毫秒切换显示计时器1和计时器2的动画。虽然同一时刻只显示一个但由于切换频率够快人眼视觉暂留效应会让人觉得两个动画都在进行。进度条显示另一种更直观的方法是简化动画腾出空间做进度条。例如用一个静态的牙刷图标只占3-4个像素代表每个计时器旁边用一行或一列LED的亮灭来表示进度。比如计时器1用最左边一列从上到下点亮表示进度计时器2用最右边一列从下到上点亮。我最初尝试了交替显示但孩子反馈有点“眼花”。后来改用了静态图标动态进度条的方案接受度更高。具体实现是在屏幕中央画一个固定的、简化的笑脸或牙刷图标占用3x3像素。然后利用屏幕最左侧一列5个像素作为计时器1的垂直进度条最右侧一列作为计时器2的垂直进度条。计时开始时对应的进度条从底部第一个LED开始点亮随着时间推移点亮的LED向上蔓延三分钟结束时整条进度柱刚好全部点亮。这种设计非常直观孩子一眼就能看出还剩多少时间。2.3 按钮交互与状态机设计用户通过A、B两个按钮来启动各自的计时器。这里的逻辑不仅仅是“按下按钮就开始”还需要考虑各种边界情况形成一个简单的状态机。基本状态流转逻辑如下初始状态两个计时器均未激活 (active false)。屏幕显示待机画面如一个静态的牙齿图标。按下按钮A检查timer1_active是否为false。如果是false则启动计时器1设置timer1_active true记录开始时间start_time_1 running time初始化time_left_1 180秒。同时屏幕更新激活计时器1的动画和进度条。如果是true意味着计时器1已在运行则忽略此次按钮按下。或者可以设计为长按复位但这会增加复杂度对于孩子来说简单的“防误触忽略”更可靠。按下按钮B逻辑同按钮A但操作对象是计时器2的相关变量。运行中状态在主循环中不断检查每个激活的计时器。计算elapsed (running time - start_time_x) / 1000将毫秒转换为秒然后time_left_x 180 - elapsed。当time_left_x 0时表示时间到。结束状态当某个计时器时间到时将其active状态设为false并触发结束提示例如让对应的进度条所有LED闪烁三次。然后系统应自动关闭该计时器对应的显示部分以省电或者返回待机画面。注意事项按钮防抖机械按钮在按下时由于触点弹跳会在极短时间内产生多个通断信号程序可能会误判为多次按下。MakeCode的环境已经在一定程度上处理了这个问题但在要求严格的场合我们可以在代码中加入简单的软件防抖当检测到按钮按下后先等待几十毫秒再读取一次按钮状态如果仍然是按下才确认为有效按键。在“输入”类别中使用“当按钮A被按下”事件块其内部已经有一定的去抖处理对于本项目来说完全足够。3. 在MakeCode中的分步实现3.1 项目初始化与变量创建打开浏览器访问MakeCode for micro:bit官网创建一个新项目。首先我们需要建立项目的骨架。创建所有变量点击“变量”类别选择“创建一个变量”。创建timer1_active和timer2_active类型为布尔值真假值。创建start_time_1和start_time_2类型为数字。它们将用于存储计时开始时的running time毫秒。创建time_left_1和time_left_2类型为数字单位是秒。可选创建display_mode变量用于管理当前屏幕显示的是哪个计时器的动画在交替显示方案中用到。初始化程序从“基本”类别中拖出一个“当开机时”积木块。在这个块内部我们需要设置所有变量的初始值。将timer1_active和timer2_active都设为false。将time_left_1和time_left_2都设为 180。将start_time_1和start_time_2都设为 0。调用一个自定义的显示待机画面函数稍后创建显示一个欢迎或就绪图案。3.2 构建核心函数显示模块为了使代码结构清晰、易于调试和维护我们必须充分利用“函数”功能。我们将创建多个函数来负责不同的显示任务。函数绘制进度条这个函数根据传入的计时器编号1或2和剩余时间计算并点亮对应进度条上的LED。添加一个输入参数whichTimer。内部逻辑计算应点亮的LED数量。leds_to_light 5 - Math.round((time_left_x / 180) * 5)。这里Math.round是四舍五入确保进度是整数个LED。当time_left_x为180时leds_to_light为0当time_left_x为0时leds_to_light为5。使用“循环”和“LED”类别中的“点亮 x y”积木。例如对于计时器1左侧进度条x坐标始终为0y坐标从4开始向上递减点亮。注意micro:bit的坐标原点(0,0)在左上角。函数显示刷牙动画这个函数负责在屏幕中央绘制一个简单的动态刷牙图标。例如可以设计两帧动画一帧显示牙刷在牙齿左侧一帧显示在右侧。通过交替显示这两帧形成刷牙的动感。使用“基本”类别中的“显示图标”块虽然简单但无法自定义。因此我们需要使用“LED”类别中的“绘制图形”块或者更底层地使用“点亮/熄灭”每个像素。创建一个变量animation_frame用于记录当前是第几帧。在函数内使用“如果...那么...”判断animation_frame是0还是1然后分别用“绘制图形”块显示不同的5x5二进制图案。例如帧0的图案代码可能是0b00100\n01010\n00100\n00000\n11111这只是一个示例需要你自己设计一个看起来像牙刷和牙齿的图案。每次调用此函数后将animation_frame设置为1 - animation_frame这样下次调用就会切换到另一帧。函数更新屏幕这是最重要的显示调度函数。它根据timer1_active和timer2_active的状态决定当前屏幕显示什么。首先使用“LED”类别中的“清除屏幕”块清空上一帧的画面。然后使用“如果...那么...”进行判断如果timer1_active为真调用绘制进度条(1)。如果timer2_active为真调用绘制进度条(2)。如果只有一个计时器激活调用显示刷牙动画。如果两个计时器都激活可以有两种策略a) 调用显示刷牙动画动画本身较小给进度条让出空间b) 或者不显示中央动画只显示两个进度条和顶部的状态指示灯。最后根据激活状态点亮顶部状态指示行对应的LED。3.3 实现按钮事件与主循环逻辑按钮A事件从“输入”类别拖出“当按钮A被按下”块。内部逻辑如果timer1_active为false则设置timer1_active true并设置start_time_1 running time。然后调用更新屏幕函数立即刷新显示。按钮B事件同理复制按钮A的逻辑但所有变量和判断对象改为计时器2。主循环从“基本”类别拖出“无限循环”块。这里是程序的心跳所有动态更新都在这里发生。计时更新在循环内分别检查两个计时器。如果timer1_active为真则计算elapsed (running time - start_time_1) / 1000然后time_left_1 180 - elapsed。接着判断如果 time_left_1 0则说明时间到。此时设置timer1_active false并触发一个结束提示例如让左侧进度条所有LED闪烁三次使用“循环”和“暂停”块配合“点亮”和“熄灭”。对timer2_active执行完全相同的逻辑。屏幕更新在循环的末尾调用更新屏幕函数。但是这里有一个重要的优化点不需要在每次循环可能每秒几百上千次中都更新屏幕。这会造成无意义的计算和功耗。我们应该设置一个刷新率比如每秒更新10次对于动画和进度条来说足够流畅。实现方法创建变量last_screen_update记录上次更新屏幕的时间。在循环中判断如果 (running time - last_screen_update) 100即大于100毫秒则执行更新屏幕并更新last_screen_update running time。否则跳过屏幕更新。省电优化在计时器全部结束且屏幕更新函数被调用后如果发现timer1_active和timer2_active都为假并且已经显示过结束提示一段时间例如5秒后那么应该彻底关闭屏幕。可以使用“基本”类别中的“清屏”块。更彻底的做法是在“更新屏幕”函数开头如果两个计时器都不活跃直接清屏并返回不再执行后续任何点亮LED的操作。3.4 代码调试与模拟器测试MakeCode编辑器右侧有一个micro:bit模拟器这是极其强大的调试工具。像素级调试在编写绘制进度条和显示刷牙动画函数时每写完一部分就通过模拟器上的按钮触发观察LED点阵是否按预期点亮。可以故意设置不同的time_left值来测试进度条的计算是否正确。时间流测试模拟器上方可以调整运行速度。为了测试三分钟计时是否准确我们可以将180秒暂时改为5秒或10秒进行快速验证。观察进度条增长、结束提示触发以及状态重置是否正常。并发测试在模拟器中快速点击A、B按钮模拟两个孩子先后启动计时器的场景。观察两个进度条是否能独立、正确地显示和计时。逻辑边界测试测试当一个计时器还在运行时再次按下其对应的按钮程序是否按设计忽略该操作或执行复位。测试当一个计时器结束后屏幕显示是否正确回归到待机或仅显示另一个计时器的状态。4. 硬件连接、优化与扩展思路4.1 供电与便携化部署代码在模拟器测试无误后点击编辑器底部的“下载”按钮将编译好的.hex文件保存到电脑。用USB数据线连接micro:bit和电脑此时micro:bit会作为一个U盘出现。将下载的.hex文件拖入这个U盘micro:bit上的黄色指示灯会闪烁表示正在烧录程序完成后会自动运行。为了使其成为一个独立的设备你需要一个电池盒。标准的micro:bit电池盒使用两节AAA7号电池通过JST插头连接到板子背面的电源接口。这是最经济便携的方案。实操心得电池选择与续航使用优质的碱性电池在正常的计时器使用频率下每天早晚各用几分钟续航可以达到数月。关键就在于我们代码中的省电优化在非活动状态彻底关闭屏幕。你可以进一步优化在“当开机时”初始化后如果长时间没有按钮操作可以让micro:bit进入深度睡眠但这需要用到其加速度计等传感器来唤醒对于本项目来说必要性不大。一个更简单的“物理省电”技巧是不用时把电池盒的插头拔掉。4.2 功能优化与增强基础版本完成后可以根据孩子的反馈进行迭代增加趣味性和实用性音效提示micro:bit V2自带蜂鸣器V1可以通过引脚连接有源蜂鸣器。在计时开始、结束、以及最后一分钟时可以添加不同的提示音。MakeCode的“音乐”类别提供了简单的音调播放功能。例如计时结束时播放一段欢快的旋律比单纯的闪烁光效更有吸引力。随机鼓励语在刷牙过程中每隔30秒在LED点阵上以滚动文字的形式显示一句鼓励的话如“好棒”、“坚持住”、“牙齿亮晶晶”。这需要预先定义一个字符串数组然后随机选择显示。时间自定义通过同时按下AB按钮进入设置模式然后用A/B按钮来增减计时时长例如从2分钟到4分钟并将设置保存在micro:bit的“非易失存储”中这样断电后也不会丢失。这需要用到“游戏”类别中的“保存数字”和“读取数字”积木。数据记录利用micro:bit的“日志”功能将每次刷牙的日期和时长记录到其内部存储中。家长可以通过串口连接电脑读取日志数据了解孩子的刷牙习惯。这涉及到更高级的“串行通信”知识。4.3 常见问题与排查实录在开发和后续使用中你可能会遇到以下问题问题现象可能原因排查与解决方法按下按钮无反应1. 程序未成功下载。2. 按钮事件代码块被意外禁用或覆盖。3. 电池电量不足。1. 重新下载程序确认下载时黄灯闪烁。2. 检查代码确保“当按钮A被按下”和“当按钮B被按下”两个事件块是独立的且逻辑正确。3. 更换新电池测试。计时不准过快或过慢1. 时间计算逻辑错误单位混淆秒/毫秒。2. 主循环中屏幕刷新等操作耗时过长影响了时间判断的精度。1. 仔细检查公式elapsed (running time - start_time) / 1000。running time单位是毫秒除以1000才是秒。2. 确保主循环内没有不必要的“暂停”块。将屏幕刷新频率限制在10Hz每100毫秒一次足够不要更高。两个进度条显示混乱1.绘制进度条函数中坐标计算逻辑错误未区分whichTimer。2. 屏幕更新前未“清屏”导致图像残留。1. 在绘制进度条函数内用“如果 whichTimer1”和“否则”来分别计算左右进度条的坐标。2. 在更新屏幕函数的最开始第一行就放“清屏”块。结束后屏幕仍亮着省电逻辑未生效。在判断两个计时器都未激活后没有执行清屏操作或者清屏后又被后续代码点亮。在更新屏幕函数中开头判断如果两个active都为假则执行清屏并立即“返回”不再执行后续任何点亮LED的代码。模拟器正常实体板异常1. 下载了错误的.hex文件比如为V2编写的程序下载到V1。2. 硬件故障罕见。1. 在MakeCode中确认板子型号选择正确V1或V2。2. 尝试下载一个最简单的“显示笑脸”程序到板子上测试硬件基本功能是否正常。这个项目从构思到实现最深的体会是嵌入式开发的核心在于对有限资源的精细化管理。5x5的屏幕、两个按钮、单核的处理器如何在这样的约束下做出体验良好的双任务应用是对设计思维的很好锻炼。看着孩子们因为有了这个自己参与设计比如让他们选择动画图案的计时器而更主动地去刷牙那种满足感远超技术本身。如果你也想为孩子或为自己做一个不妨就从这里开始复制我的思路然后加入你自己的创意。