
做创客教育这几年我见过太多“点亮数码管”的教程。教程一结束Mixly的积木块拖完数码管亮起来数字跳起来大家欢呼一下项目就结束了。可一旦有人提出“把它做成一台时钟”画风立刻变了时间走不准、显示闪烁、按键一按跑好几秒……卡在这一步的人特别多。这篇文章想聊的就是这个断点——从点亮一个数码管到用Mixly做出一台走时可靠、能调时、能整点报时的数码管时钟中间到底要跨过哪些坎。我会把显示原理、计时方案、完整接线和积木块逻辑、还有我实测踩过的坑全部写出来适合小学高年级到大学阶段的创客课程参考也适合零基础但是想认真做一个小作品的读者。1. 为什么这个项目值得做Mixly、数码管、时钟的黄金组合1.1 从点亮数码管到做一台时钟中间差了三件事很多教材把数码管驱动当成一个独立知识点讲清楚段码、扫描、共阴共阳然后给一个静态显示数字的示例就结束了。这个层级的知识本质上还是在“控制单个输出设备”。而时钟项目完全不一样它需要三件事同时成立输入按键得能用能调时、能切换模式处理时间数据要能被正确地读出来、拆开、转换成显示内容输出数码管显示要稳定、不闪烁、不串位。这三件事单独拎出来都不难但合在一起就变成了一个“系统级”的挑战。很多学生能轻松完成每部分却搞不定整体——这就是为什么我把这个项目定位成“从入门到进阶的分水岭”。它不要求你懂什么高深算法但它天然逼着你学会模块化思考时间模块管时间、显示模块管显示、按键模块管交互各司其职再用一个主循环把它们串起来。用Mixly来做这件事优势非常明显。Mixly是图形化编程环境拖拽积木块能直接生成Arduino代码学生不用被语法细节劝退可以先把主要精力放在“逻辑怎么搭”上。而且Mixly支持在图形块和代码之间切换同一段逻辑可以对比着看这对理解程序结构帮助非常大。1.2 硬件选型逻辑TM1637模块 DS3231模块的方案配套硬件我见过很多人一上来就买“裸”的七段数码管然后陷入引脚不够、扫描程序写崩、亮度不均的泥潭。我的建议是如果目标是做出一台能长期放在桌上跑的时钟直接买TM1637四位数码管模块和DS3231实时时钟模块这两个模块是这个项目里性价比最高、最省心的组合。硬件型号/规格作用主控板Arduino UNO 或 NanoMixly 支持即可项目大脑数码管模块TM1637 四位数码管共阴显示时间实时时钟DS3231 模块带电池座提供准确时间按键轻触开关×2调时、切换模式蜂鸣器无源蜂鸣器整点报时光敏电阻模块光敏传感器可选自动亮度其他面包板、杜邦线若干连接电路TM1637模块只占两根信号线CLK、DIO内部控制芯片帮你搞定了动态扫描显示4位数字只需要往寄存器里写数据即可。DS3231走的是I2C接口也是两根线SDA、SCL自带温度补偿晶振和电池备份掉电之后时间照走。两个模块加在一起主控板上连IO都不用省能省出一大块精力来处理逻辑和排错。1.3 这套方案的整体工作链路整个项目一句话介绍主控板通过I2C从DS3231读取“时、分、秒”拆开之后通过TM1637显示到数码管上按键负责进入调时模式和调整时间蜂鸣器在整点响一下。这个链路看起来简单但里面每个环节都有细节DS3231怎么初始化、时间数据怎么解析、显示的时候要不要补零、按键怎么消抖、调时状态怎么切换。接下来我会按这个顺序把每个环节都拆开讲清楚。2. 数码管显示原理段码、共阴共阳和动态扫描并没有那么难2.1 七段数码管内部到底是怎样的七段数码管说白了就是在一个“日”字形封装里装了8个LED横向的a段、b段、f段、g段纵向的c段、e段再加上正中间的那一竖g段和右下角的小数点dp。数字“8”能显示出来是因为这7段全亮了数字“1”只亮右上角的b段和右下角的c段。关键在于这两个极性的区别共阴极数码管所有LED的负极接在一起接地想让某一段亮就在对应的引脚输出高电平。共阳极数码管所有LED的正极接在一起接VCC想让某一段亮就在对应的引脚输出低电平。很多新手栽就栽在这里。同一个数字共阴和共阳的段码是完全相反的。买模块之前一定要先看清楚商家标注的是共阴还是共阳或者直接用模块的原理图判断。TM1637模块基本都是共阴配合的库函数已经处理好了但如果你自己直驱数码管这一步错后面全错。2.2 段码表0到9每个数字的点灯方案所谓的段码表就是预先算好“显示某个数字时a到g分别该亮还是灭”然后把8个LED的状态拼成一个字节。按照常见的接线顺序a、b、c、d、e、f、g、dp共阴极数码管显示0到9的十六进制段码如下数字a b c d e f g dp十六进制共阴十六进制共阳01 1 1 1 1 1 0 00x3F0xC010 1 1 0 0 0 0 00x060xF921 1 0 1 1 0 1 00x5B0xA431 1 1 1 0 0 1 00x4F0xB040 1 1 0 0 1 1 00x660x9951 0 1 1 0 1 1 00x6D0x9261 0 1 1 1 1 1 00x7D0x8271 1 1 0 0 0 0 00x070xF881 1 1 1 1 1 1 00x7F0x8091 1 1 1 0 1 1 00x6F0x90需要提醒一句这个表假设你按“a在最低位”的顺序排列段位。如果你自己直驱数码管把a接到了别的引脚这个表就不适用了你得按实际接线重新映射。这块我在第5章的排错部分还会细讲。2.3 为什么多个数码管必须动态扫描假设你有4位数码管直驱的话需要4×832个IO口Arduino UNO根本不够用。所以工程上几乎都用动态扫描把4个数码管同名的段选引脚全部并联到8个IO口上再用4个位选引脚分别控制哪一位通电。显示的时候CPU并不是让4位同时亮而是飞快地轮流点亮每一位先让第1位显示数字持续两三毫秒然后切到第2位显示下一组段码再切到第3位再切到第4位然后从头开始。只要一轮扫描的周期控制在16毫秒以内也就是刷新率高于60Hz人眼的视觉暂留就会让我们觉得4位数字是一起亮的。这个过程很像翻页动画每一页只画一帧快速连翻就动起来了。如果刷新率太低或者某一位停留时间特别长你的眼睛就会捕捉到“闪烁”和“亮度不均”。所以动态扫描的程序里尽量让每一位的点亮时间相等总周期越短越稳定。常见的做法是每位点亮2到3毫秒4位循环下来12毫秒左右肉眼完全看不出闪。2.4 TM1637模块帮你包办了哪部分工作如果你用的是TM1637模块那你上面的动态扫描基本不用亲手写。TM1637是一个专用LED驱动芯片内部就带了多位扫描逻辑。你只需要通过CLK和DIO两根线按它的通信协议把“第几位显示什么数字”告诉它芯片就会自己完成轮流扫描刷新。这大大降低了入门门槛但也带来一个副作用很多人用了TM1637就再也不关心动态扫描了等到哪天换回直驱数码管照样两眼一抹黑。我的建议是TM1637可以拿来提效率但第2章的原理一定要懂因为后面排错时很多“显示异常”的问题根子就在扫描时序上。3. 时钟走时方案内部计时和DS3231实时时钟差别有多大3.1 用Arduino内部计时做时钟的隐患很多新手的第一版代码是这样写的在loop里用delay(1000)每过1秒让秒数加160秒进一位存到变量里再显示出来。这个方案听起来很直观实际上坑非常多。先不说delay(1000)期间程序什么事情都干不了最大的问题是精度。Arduino主板上的时钟基准是16MHz晶振或陶瓷谐振器它的实际频率并不是完美的16000000Hz而是存在一定误差。这个误差通常用ppm百万分之一来表示100ppm意味着每秒钟可能偏差100微秒折算下来一天就是8.64秒。Arduino UNO用的是陶瓷谐振器误差可以到几十甚至上百ppm你做出来一台时钟放在桌上跑一晚上第二天早上已经快了或者慢了几秒这非常劝退。还有人会用millis()来计时这个方法比delay累计好一些因为它不会累计循环执行时间的误差。但millis()本身是拿系统时钟当基准的基准晶振偏多少它照样偏多少。而且millis()返回的是一个unsigned long大约49.7天后会溢出归零如果你不处理溢出问题时钟在那天就“归零重来”了。3.2 DS3231为什么是时钟项目的标配想要一台靠谱的时钟就得给主控板配一个“独立走时的心脏”——DS3231就是干这个的。它是一颗I2C接口的实时时钟芯片内置温度补偿晶振TCXO。普通晶振的频率会随温度漂移而DS3231会实时根据温度做补偿所以它的精度能达到±2ppm粗略换算下来一年的误差在一两分钟左右。这颗芯片除了走时还带了两个很实用的功能CR2032电池备份和内置温度传感器。电池备分意味着就算主控板断电了DS3231内部的时间照样在走重新上电后读出来的时间仍然是准确的温度传感器可以用来显示室内温度让时钟多一个功能。在电路接线上DS3231模块通常引出四个引脚SDA、SCL、VCC、GND。Arduino UNO上SDA对应A4SCL对应A5VCC接5VGND接GND。I2C地址是0x68这是芯片固定的库函数里已经封装好了不需要你手动处理。3.3 不同场景下走时方案怎么选如果你的目标只是课堂演示“我做了一个时钟”用millis()或delay计时都无所谓反正十分钟的展示不会看出误差。但如果你打算把它做成一个长期放在桌上的小作品我的建议非常明确直接上DS3231。多花十几块钱换来的是不跑偏的可靠性以及“断电也能继续走时”的意外之喜。而且DS3231的库非常成熟在Mixly扩展库里也有很多封装好的积木块调用成本很低。别在时间精度上节省这是这个项目最不值得省的地方。4. 搭硬件和写程序从接线到Mixly积木块的完整流程4.1 硬件接线下面这套接线以Arduino UNO为例子。如果你用的是Nano或者ESP32开发板引脚对应关系要自己看原理图调整I2C接口位置可能不同。模块模块引脚连接到ArduinoTM1637 数码管模块CLKD3TM1637 数码管模块DIOD2DS3231 时钟模块SDAA4DS3231 时钟模块SCLA5DS3231 时钟模块VCC5VDS3231 时钟模块GNDGND按键1一端D5另一端接GND按键2一端D6另一端接GND无源蜂鸣器正极D8无源蜂鸣器负极GNDTM1637 数码管模块VCC5VTM1637 数码管模块GNDGND按键接GND的目的是配合Arduino的内部上拉电阻。在Mixly里把D5、D6设为“输入上拉”那么平时引脚读到的是高电平“1”按下按键导通到GND读到低电平“0”。注意这时候“按下”对应的是低电平逻辑上要反过来判断很多新手在这里第一次踩坑。蜂鸣器留意一下如果是有源蜂鸣器通电就响用digitalWrite就能控制如果想播放旋律要选无源蜂鸣器用tone()函数输出不同频率。下面整点报时会用无源蜂鸣器所以接线就按无源的来。4.2 Mixly环境准备与扩展库加载打开Mixly之后先做两件事。第一在右上角选择正确的开发板型号和串口端口。Arduino UNO选“Arduino/Genuino Uno”插上USB线之后在设备管理器里确认COM口号选错端口上传必定失败。第二确认数码管和实时时钟的库有没有下载。不同版本的Mixly界面略有差异你可以在“管理库”或“扩展管理”里搜索“TM1637”“DS3231”“RTClib”等关键词。如果列表里有现成的“数码管- TM1637”和“实时时钟- DS3231”积木块直接用如果搜不到也不用慌Mixly有一个“执行代码”块你可以在里面直接写Arduino代码效果一样。我自己的习惯是库能搜到就用库库缺失就用“执行代码”块两种方式可以在同一个Mixly项目里混用。我建议你把Mixly切换到代码预览模式看一眼。每次拖拽积木块右侧都会同步生成对应的C代码。这个功能特别适合学习你能直观看到图形化逻辑对应的代码长什么样后面遇到问题排查起来也更有方向。4.3 主程序逻辑读时间、显示、扫按键主程序的核心逻辑可以用下面这段伪代码描述初始化设置TM1637引脚、初始化DS3231、设置按键引脚为输入上拉、设置数码管亮度、初始化蜂鸣器引脚。主循环通过DS3231读取当前时间得到小时、分钟、秒调用显示刷新函数把“时:分”显示到数码管或者“分:秒”也要看你的实际需求扫描两个按键判断是调时模式还是正常运行如果是调时模式根据按键操作更新时间并写回DS3231等待一小段时间再进入下一轮循环。在Mixly里时间读取这一块如果找到了DS3231扩展库通常会有类似“读取DS3231时间”的积木块里面能看到“年、月、日、时、分、秒”这些输出项。把它们分别存到变量里再用“设置TM1637显示数字”的积木块显示小时和分钟。如果库不支持用“执行代码”块放以下代码片段做参考#include Wire.h #include RTClib.h #include TM1637Display.h RTC_DS3231 rtc; TM1637Display display(3, 2); // CLKD3, DIOD2 int h, m, s; void setup() { Wire.begin(); rtc.begin(); display.setBrightness(4); // 第一次使用需要手动设置时间比如rtc.adjust(DateTime(2025, 6, 1, 12, 0, 0)); } void loop() { DateTime now rtc.now(); h now.hour(); m now.minute(); s now.second(); int displayValue h * 100 m; display.showNumberDec(displayValue, true, 4, 0); // 显示 HH:MM前导补零 delay(100); }这里要注意showNumberDec(displayValue, true, 4, 0)的含义第一个参数是要显示的数字第二个参数是是否补零第三个参数是显示长度第四个参数是从第几位开始。四位数码管显示时间最方便的做法就是用一个四位整数表示比如12点34分就用1234。4.4 调时状态机两个按键如何实现模式切换数值加一调时逻辑是这个项目里最考验逻辑设计的部分。我只用两个按键按键1作为模式键按键2作为加一键。正常运行状态下按一下模式键进入“调整小时”模式再按一下模式键切到“调整分钟”模式再按一下模式键退出调时回到正常运行。在调时模式下每按一次加一键当前正在调整的单位数值加1。小时加到23之后回0分钟加到59之后回0。这个状态转换用变量实现就行我通常用一个整数变量mode0表示正常运行1表示调小时2表示调分钟。每次按下模式键mode (mode 1) % 3正好在0、1、2之间循环。在Mixly里用“如果”积木块判断mode的值再决定加一键去修改小时变量还是分钟变量。有一个细节很重要调整完小时或分钟之后应该立即把新数值写回DS3231。用RTClib时可以通过读出现有时间再把小时或分钟替换掉来实现也可以直接用rtc.adjust()重新设置整个时间。比较省事的做法是DateTime now rtc.now(); rtc.adjust(DateTime(now.year(), now.month(), now.day(), h, m, 0));这段代码我通常会放在“退出调时模式”的分支里避免每加一键就写一次Flash。按键消抖的处理方法我放在下一章专门讲。5. 实测排错显示闪烁、段码反了、按键乱跳三大坑逐一拆解5.1 坑位一显示闪烁、有拖影排查链路的完整过程有次一个学生给我发来视频他做的时钟显示“12:34”看起来巧克力一样粘成一团数字边缘还带着淡淡的“尾巴”。我先问了三个问题供电是USB还是外接电源数码管是TM1637还是直驱刷新率设了多少他回答USB供电、TM1637模块、亮度调到最高。第一步我让他把USB口从USB Hub换到电脑原生接口问题没有根本改善。这说明不是供电不足的问题。第二步我让他把亮度从第7档降到第3档闪烁减轻了但还有拖影。这说明问题不在亮度而在扫描时序。可是TM1637模块是芯片自动扫描的他根本没有写扫描代码拖影是从哪来的第三步我让他翻出TM1637模块的数据手册重点看显示数据的发送频率。TM1637模块虽然自动扫描位但数据是通过CLK和DIO两根线串行发送的如果主控在发送数据时被打断或者两次发送之间的间隔太长芯片内部的显示缓冲就会有一小段时间处于“空数据”状态表现出来就是拖影。最后的结果有点出人意料他在主循环里放了一个delay(1000)用来做秒计数每秒只刷新一次显示。TMD1637的内部扫描频率本身不低但显示数据一秒钟才更新一次又赶上亮度拉满人眼对变化就很敏感。把delay(1000)改成delay(50)之后显示立刻稳定了。这个案例给我的教训是TM1637虽然简化了扫描但主循环的刷新频率会直接影响显示效果。如果你用的是直驱数码管还得额外注意位选切换顺序先全部熄灭、再输出新段码、再点亮下一位顺序反了就会看到上一位的“残影”。5.2 坑位二数字乱码、笔画反着亮先分清共阴共阳第二个高频问题是显示乱码。比如想让数字显示“2”结果数码管显示成乱糟糟的一堆笔画或者所有数字都是镜面反字。排查链路是这样的先把代码改成只显示一个“8”也就是所有段全亮。如果“8”都显示不对说明不是段码表的问题而是接线或极性错了。用万用表量一下引脚电平确定模块是共阴还是共阳。如果是共阳极数码管但程序按共阴段码表输出那亮的段和灭的段正好全部反相显示出来就像是“底片”。如果极性没错但个别数字不对多半是引脚顺序和段码表对不上。你可以写一段逐段点亮的测试程序先亮a段再亮b段以此类推把每一段实际对应的引脚记录下来再重新生成段码表。逐段点亮的测试代码很简单思路就是循环给每个段依次通电每段之间加个延时人眼能看清亮的是哪一段int segPins[8] {2, 3, 4, 5, 6, 7, 8, 9}; // 你的8个段引脚 for (int i 0; i 8; i) { pinMode(segPins[i], OUTPUT); } void loop() { for (int i 0; i 8; i) { digitalWrite(segPins[i], HIGH); delay(500); digitalWrite(segPins[i], LOW); } }跑一遍记录a到g分别对应哪一段再按记录的顺序重写段码表乱码问题一般就解决了。这个“逐段测试法”是我处理显示问题最常用的工具几乎每次都能精准定位。5.3 坑位三按键按一下变好几下串口打印帮你找到真凶按键问题几乎人人都会遇到。现象是调分钟时明明只按了一下加一键数字却连着加了好几下像是在抽风。原因很经典机械按键的触点在闭合和断开的时候并不是一次到位而是会在几毫秒到几十毫秒内快速抖动产生一连串脉冲。程序每一次脉冲都当成一次按下自然就“按一下变好几下”了。排查方法很直接用串口打印按键的状态变化。在Mixly里用串口打印模块把按键引脚读到的值实时打出来你会看到类似“0 1 0 1 1 1 0 1 0 1”这种混乱的序列这就是抖动。解决方案有两个层次第一软件消抖。检测到按键第一次变为低电平后等20毫秒再读一次如果仍然是低电平才判定为真正按下。Mixly里可以用“延时”积木块加“重复执行直到”实现。缺点是这个延时是阻塞的如果程序里有其他任务会卡顿。第二状态变化检测。只在“从未按下变为按下”的那一瞬间响应一次而不是按键保持低电平期间持续响应。这需要用一个变量记录上一次的按键状态每次循环比较当前状态和上一次状态是否不同。我推荐的完整做法是两者结合用状态变化检测做触发用延时做消抖。在Mixly里实现起来大概是读引脚状态如果和上一次不同且当前为低电平就延时20毫秒再读一次确认确认后计数加一更新上一次状态。这套逻辑写顺手了以后做任何按键交互都通用。5.4 坑位四时间越走越偏问题未必在你写的程序最后一个坑特别容易让人怀疑人生。时钟做好了功能全正常但跑两天后一看时间快了或者慢了十几秒。程序看起来完全没有问题问题出在哪问题几乎可以肯定出在计时基准上。如果你用的是Arduino内部计时或板载晶振这个误差是挨板子的。Arduino的时钟源不是高精度元件温度一变频率就漂几天累计下来误差非常可观。我在学校做过一个对比实验同一套TM1637显示和按键程序一块板子用millis()计时一块板子用DS3231计时跑了72小时后前者偏了7秒后者几乎看不出偏差。这个实验做完所有学生都老老实实换了DS3231。如果你已经用了DS3231但时间仍然偏差明显还有两个检查点一是电池有没有装好如果没装电池模块断电后会重置时间看起来就像“突然不走时了”二是模块本身是否为翻新或劣质板某些廉价DS3231模块用的不是原厂芯片精度完全达不到标称值。买模块的时候尽量选正规渠道多花几块钱买到的是省心。6. 让时钟更像产品冒号秒闪烁、自动亮度和整点报时6.1 冒号每秒闪一下的显示细节数码管时钟显示“时:分”中间那个冒号是最有“钟表感”的地方。TM1637四位数码管模块在中间位置有一个冒号段控制它和显示数字是分开的。在Mixly的TM1637库中通常有类似“设置冒号开关”或“显示冒号”的积木块调用它就能点亮或熄灭冒号。我的做法是在主循环里读秒数秒为偶数时点亮冒号秒为奇数时熄灭冒号。这样冒号就会像机械表的秒针一样每秒“滴答”闪动一下也能直观地告诉人“秒正在走”而不只是显示分钟。需要留意的坑是冒号切换不要直接和显示刷新放在同一个时刻。如果整秒边界同时发生“冒号切换”和“时间数字刷新”用户可能会看到一次不太明显的闪烁。我一般会做一个小的状态延迟让冒号切换在刷新的10毫秒之后执行画面就干净了。6.2 光敏电阻让亮度自动适应环境数码管固定最高亮度放在床头晚上会亮得刺眼固定最低亮度放在白天又看不清。加一个光敏电阻模块能解决这个问题。把光敏电阻模块接到Arduino的A0引脚它输出的是一个模拟电压值范围0到102310位ADC。在Mixly里用“读取模拟引脚A0”的积木块就能拿到这个数值。根据光照强度把数值映射到TM1637的亮度等级0到7上就能实现自动调光。这里有两个实操细节不同型号的光敏模块输出电压方向可能相反有的光越强电压越高有的光越强电压越低。建议先写一段代码把A0的值实时打印到串口监视器观察环境变化时的数值变化方向再决定映射逻辑。亮度切换不宜过于频繁。光线稍微改变就变亮度人眼会感觉屏幕在“呼吸”。我通常做一个简单的滞回判断亮度等级相隔两级以上才切换避免临界值附近的抖动。这个功能加上之后时钟的“成品感”立刻提升一个档次。白天阳光底下也能看清晚上又不刺眼而且整个过程完全自动体验非常好。6.3 蜂鸣器整点报时整点报时是很多同学最想加的功能。实现逻辑很简单读小时数和分钟数当分钟等于0、秒数落在0到1之间时触发一次蜂鸣器播放。需要注意的是加一个标志位确保警报只触发一次不然整点的第一秒内主循环可能跑了很多次蜂鸣器会连响好几声。无源蜂鸣器使用tone()函数播放音乐。比如整点报时我常用这样的旋律先1000Hz响200毫秒停100毫秒再1000Hz响200毫秒。在Mixly里没有现成的tone()积木块可以用“执行代码”块写也可以直接用音频积木块如果有。代码参考if (m 0 s 0 !alarmPlayed) { tone(8, 1000, 200); delay(300); tone(8, 1000, 200); alarmPlayed true; } if (m ! 0) { alarmPlayed false; }这段逻辑里标志位alarmPlayed保证了同一分钟内只响一次分钟变化后重置下次整点可再触发。蜂鸣器时间不要超过一两秒不然会非常吵很像闹钟而不是时钟。6.4 继续往上加东西NTP校时、温度显示、外壳设计到这里一台基础版的Mixly数码管时钟已经完成了。但它的生命周期远没有结束我见过很多学生在这个基础上继续魔改联网自动校时如果用ESP8266或ESP32做主控板可以通过NTP服务获取标准时间定时校准DS3231这样连走时误差都不用自己操心温度显示DS3231内部自带温度传感器在Mixly里读取它然后每隔几秒在数码管上切换显示“时间”和“温度”外壳设计用3D打印做一个带透光窗的外壳把电路板、电池、按键都装进去它就从一个实验板变成一台能放上桌面的小家电了。我个人的建议是在基础功能稳定运行一周之后再开始加扩展功能。每加一个功能之前先确认之前的显示、计时、按键没有被破坏。这个“先稳定、再扩展”的节奏会让你少掉很多头发。做完了这些你回看最开始那个只会点亮单个数码管的自己应该能明显感受到编程能力、硬件能力、调试能力都上了一个台阶。