1. 项目概述与核心思路我一直对天文和硬件制作充满热情詹姆斯·韦伯太空望远镜JWST的发射和展开过程堪称工程奇迹总想动手做个东西来致敬它。与其做一个静态模型不如让它“活”起来——既能作为一件精致的桌面摆件又能显示准确的时间。这个想法催生了这个“动画时钟”项目一个会周期性地模拟望远镜展开过程并在展开的“副镜臂”末端显示网络同步时间的装置。这个项目的核心目标很明确用机械动画讲述科学故事用精准时间赋予实用价值。它本质上是一个融合了机械结构、电子控制和网络功能的物联网IoT装置。对于硬件爱好者和创客来说它涉及了几个关键技能点使用Arduino进行多舵机协同控制、利用Wi-Fi模块Wemos D1获取网络时间NTP、整合3D打印的定制结构件以及将LED显示模块嵌入动态机械结构中。整个过程就像完成一次小型的系统工程从构思、建模、电路搭建到代码调试每一步都充满挑战和乐趣。我选择Arduino Nano搭配舵机扩展板来控制三个舵机一个MG996负责主镜臂展开两个MG90s负责两侧副镜折叠主要是看中了其生态的成熟和易用性。对于需要平滑运动的动画效果直接使用标准的Servo.h库可能会让动作显得生硬、跳跃。因此我引入了VarSpeedServo库它可以非常方便地设定舵机的运动速度和目标角度实现类似缓动动画的效果让望远镜的展开过程看起来更逼真、更有仪式感。时间显示部分我放弃了传统的DS1302/DS3231等RTC实时时钟模块因为需要定期校准夏令时和时钟漂移很麻烦。直接使用Wemos D1基于ESP8266连接Wi-Fi通过NTP协议从互联网时间服务器获取时间就成了一劳永逸的方案。它自动处理时区、夏令时精度极高。显示器件则选用了带TM1667驱动芯片的4位7段LED数码管模块这种模块只需要两根信号线CLK, DIO即可通过串行通信控制大大简化了布线特别适合在需要活动的机械臂上使用。整个项目的难点在于机械结构与电子控制的协同。如何将舵机、导线、显示模块巧妙地隐藏或布置在有限的空间内并确保机械运动流畅、不卡线是设计和组装阶段需要反复推敲的。接下来我将从材料准备开始详细拆解每个步骤。2. 材料准备与核心部件解析工欲善其事必先利其器。这个项目的材料清单可以分为结构材料、电子元件和工具三大类。有些材料是现成的有些则需要根据设计进行加工或打印。2.1 结构材料清单与处理要点主镜面基板一块3mm厚的胶合板。这是整个望远镜模型的“骨架”。选择3mm厚度是在强度和重量之间取得平衡太薄易变形太厚则笨重。我用曲线锯Jigsaw将其切割成大致六边形的轮廓尺寸大约为30cm宽。切割后务必用砂纸打磨边缘防止木刺伤手也为后续粘贴提供平整表面。六边形镜片单元50个木质六边形片边长约2cm。这是模拟JWST那18块六边形镀金主镜的简化版。直接购买成品比自己切割省时省力得多。我最初计划用金色箔纸包裹它们来增加反光效果但实测发现即使用哑光黑板漆涂成深色在光照下木质纹理和金色反光也能形成不错的质感。如果你追求更高的镜面效果可以使用真的镜面贴纸或亚克力镜片。镜臂与支撑轴一根直径12mm的硬木圆棒榉木或桦木都行。它负责连接主镜面和下方的太阳帆板Sun Shield是整个模型的中央支柱。它的垂直度和强度至关重要我选用硬木是为了避免长期受力后弯曲。太阳帆板基板另一块3mm胶合板的下脚料。将其切割成JWST标志性的五层太阳帆板简化形状一个类似风筝的多边形。这一步没有严格尺寸我主要是参考图片比例确保它与上方的主镜面大小协调视觉上不头重脚轻。舵机联动机构材料主镜臂Boom两条12mm宽、2mm厚的铝条。铝材质轻且有一定刚性是制作可展开臂的理想材料。我在网上购买了预切割好的条状铝材。连接件与舵机座这是需要3D打印的核心结构件。包括连接铝条和舵机摆臂的铰链、固定MG996舵机的底座、以及用于推动副镜的MG90s舵机延长臂。使用PLA材料打印即可强度足够。粘合剂本项目会用到多种粘合剂各有其职木工胶用于永久性粘合木质部件如将六边形片粘到基板上。但干燥慢在需要快速定位时不够用。热熔胶快速固定、临时定位、填充小缝隙的利器。我大量用于固定电线、初步粘合舵机等。优点是速干缺点是长期受力或高温环境下可能失效。CA胶快干胶/401/502用于粘合3D打印件与小面积的金属、塑料部件固化速度快强度高。涂料哑光黑板漆。用于将主镜面基板的边缘、背面以及太阳帆板涂成深空黑色吸收杂散光让观众的视线聚焦在“镜面”和机械结构上。注意关于锂电池的安全警示。原文作者提到了因故障锂电池引发的 workshop fire。这给我们所有人敲响了警钟。无论何时在给任何锂电池包括航模电池、充电宝电芯等充电时务必使用专业的防爆充电袋并在无人值守时远离易燃物。对于本项目我们使用的是5V直流电源适配器供电不涉及高压电池但安全习惯必须养成。2.2 电子元件选型与功能解析主控制器Arduino Nano 舵机扩展板为什么用NanoArduino Nano体积小巧引脚功能与Uno兼容价格便宜非常适合嵌入到这种空间有限的项目中。它负责运行控制三个舵机动画的核心逻辑。为什么需要舵机扩展板直接驱动多个舵机尤其是MG996这种扭力大的对Arduino的5V引脚是巨大负担可能导致板子重启或损坏。舵机扩展板自带独立的电源输入接口和稳压电路可以为舵机提供充足且稳定的电流需外接5V/3A以上的电源同时通过信号线隔离控制保护了主控板。我使用的扩展板可以直接堆叠在Nano上方节省空间。网络时间控制器Wemos D1 Mini核心优势它本质上是一个集成了ESP8266 WiFi芯片的开发板可以直接用Arduino IDE进行编程。其内置的Wi-Fi功能使得获取NTP时间变得非常简单无需额外的网络模块。同时它的5V耐受引脚可以直接从舵机扩展板的5V取电整合非常方便。舵机MG996R用于主镜臂这是一款标准舵机金属齿轮扭力大约10kg·cm速度中等。选择它是因为展开主镜臂需要克服一定的初始静摩擦和结构阻力需要足够的扭力来保证动作可靠。MG90S用于两侧副镜这是一款微型舵机同样是金属齿轮扭力较小约1.8kg·cm但体积小、重量轻。副镜的折叠动作负载很小对扭力要求不高但需要精确的角度控制MG90S完全胜任且其小巧的体积便于隐藏在主镜结构侧面。显示模块TM1667 LED 4位7段数码管驱动芯片TM1667的优势传统的7段数码管需要占用大量GPIO引脚4位数码管动态扫描至少需要12个引脚。TM1667是一款LED驱动控制专用电路采用I2C-like的两线串行接口CLK, DIO只需要2个引脚就能控制多位LED极大简化了连接。模块通常自带限流电阻和滤波电容即插即用。供电考虑该模块工作电压一般为3.3V-5V。由于Wemos D1的IO口是3.3V电平而模块是5V供电为了确保信号稳定最好使用一个简单的双向电平转换器或者确认该模块在3.3V信号下能可靠工作很多TM1667模块可以。电源整个系统需要一个稳定的5V直流电源。计算总电流两个MG90S空闲约100mA工作峰值可达500mA一个MG996R空闲约150mA工作峰值可达1.5AWemos D1约200mALED模块峰值约200mA。理论上峰值电流可能接近2.5A。因此我选择了一个输出为5V/3A的DC电源适配器并连接到舵机扩展板的电源输入端确保留有充足余量避免供电不足导致舵机抖动或控制器复位。3. 机械结构设计与组装实战机械部分是整个项目的骨架也是最考验耐心和精细度的环节。我的设计原则是在保证运动功能的前提下尽可能简化结构便于组装和调试。3.1 主镜面与副镜铰链制作首先处理核心视觉部件——主镜面。布局与粘贴在3mm胶合板上用铅笔轻轻勾勒出由18个六边形组成的近似圆形轮廓。将木质六边形片按照JWST的排列方式中心一个周围环绕用木工胶粘贴上去。这里有个关键技巧不要一次性在所有六边形背面涂胶。先固定中心及关键位置的几片等其初步固化定位后再填充周围的片子这样可以边贴边调整确保整体排列整齐。我最初尝试全部涂胶后一起贴上结果有些片子滑动错位清理起来非常麻烦。强化固定木工胶完全干燥需要数小时。为了在后续加工中保持稳固我在每个六边形片的四周缝隙处点了一些热熔胶进行辅助固定。注意热熔胶用量要少避免溢出影响正面观感。涂装边缘用遮盖胶带美纹纸仔细贴住所有六边形片的边缘保护其正面。然后用小号板刷将露出的胶合板基板边缘和背面涂上哑光黑板漆。涂装要薄而均匀待第一遍干透后再涂第二遍以达到深邃的黑色效果。制作副镜铰链两侧的副镜各由3个六边形组成需要能够向内折叠。我采用了最简易但有效的“布基胶带铰链”法。将副镜部分平放在桌面上在正面接缝处贴上遮盖胶带临时固定。将整个组件翻转在背面的接缝处沿着缝隙粘贴一条宽度约2cm的黑色布基胶带Gaffer Tape。布基胶带纤维强度高反复弯折不易断裂是理想的DIY铰链材料。粘贴时确保胶带平整无气泡并且接缝正好位于胶带宽度的中心线上。为了隐藏胶带的颜色我只有灰色我用小笔刷蘸取黑板漆将铰链内侧折叠时会露出的部分涂黑使其在视觉上融为一体。3.2 太阳帆板与中央支柱组装太阳帆板是静态部分主要起装饰和配重作用。切割与打磨根据设计图样用曲线锯或带锯切割出五边形太阳帆板形状。用砂纸将所有边缘打磨光滑特别是表面因为任何凹凸都会在贴箔后非常明显。粘贴银色箔纸我使用了带背胶的银色铝箔胶带。粘贴时从一个角开始慢慢撕开背纸一边用刮板或银行卡推平一边前进尽量避免产生气泡和褶皱。遇到曲面边缘时可以将箔纸裁成小条分段粘贴。完成后用锋利的美工刀沿边缘修整。确定角度与钻孔参考JWST实物图片太阳帆板与主镜面有一个夹角。我在作为底座的方形木块由山毛榉制成提供稳定配重上以一个约15度的倾斜角钻出一个直径略小于12mm圆棒例如11.5mm的孔。这样圆棒插入后会非常紧配合木工胶可以形成牢固的楔形连接。整体连接将12mm圆棒一端涂胶后插入底座。另一端穿过太阳帆板中心的孔此孔需垂直钻透并涂胶与太阳帆板固定。关键一步在主镜面背面的中心位置确定一个点将主镜面“套”在圆棒上并用热熔胶从背面多点固定。此时需要借助水平尺或肉眼观察调整主镜面使其处于水平状态等待热熔胶固化。热熔胶在这里起到了快速定位和调整的作用后期还可以用更牢固的环氧树脂进行加固。3.3 舵机传动机构设计与安装这是实现动画的核心需要精确的测量和耐心的调试。主镜臂MG996R安装设计思路主镜臂由两条平行铝条构成一端铰接在镜面底部两侧固定点另一端铰接在承载LED屏幕的“副镜”结构上。MG996R舵机安装在主镜面背面顶部中央其摆臂通过一个连杆推动整个平行四连杆机构实现镜臂的展开与收拢。3D打印连接件我设计了三种连接件a) 用于将铝条端部与固定点/副镜结构连接的“U型夹”b) 用于连接两条铝条保持平行的“横撑”c) 用于将舵机摆臂与连杆连接的“球头连杆座”。所有连接均使用M3螺丝和防松螺母方便调节和拆卸。安装步骤先将两个U型夹用螺丝固定在主镜面底部预设的位置对称。将铝条组装上。然后将MG996R用螺丝固定在主镜面背面顶部的3D打印底座上。连接舵机摆臂和连杆。调试关键在舵机通电回中位90度时手动调整摆臂和连杆的长度使主镜臂处于完全收拢状态。然后通过代码控制舵机转到0度和180度观察镜臂展开和收拢的极限位置是否顺畅、无干涉。可能需要微调固定点位置或连杆长度。副镜驱动MG90s安装初始方案与问题我最初设想用遥控飞机上常用的钢丝推拉线来驱动副镜舵机放在中心位置。但实际测试发现钢丝的柔性导致传动存在虚位无法精确控制副镜折叠到预定角度且回弹力不足。改进方案改为将MG90s舵机直接安装在副镜背面的侧面舵机轴心尽可能靠近副镜的旋转轴心。3D打印一个加长的舵机摆臂摆臂的末端通过一个“滑动槽”机构与副镜背面连接。滑动槽机构在副镜背面固定一个小块上面开一个弧形槽。舵机摆臂的末端连接一个销钉销钉卡在这个弧形槽里。当舵机转动时摆臂推动销钉在槽内滑动从而带动副镜旋转。这个设计的妙处在于它将舵机的圆周运动转换成了更适合推动平板旋转的近似直线运动并且通过槽的长度限制了副镜的旋转角度起到了物理限位的作用比直接用摆臂顶推要可靠得多。安装与调试用热熔胶或CA胶将舵机底座粘在副镜背面。连接摆臂和滑动槽。上电后在代码中慢慢测试舵机从0到180度的运动观察副镜是否从完全折叠贴合主镜平滑运动到完全展开约30度角。记录下这两个极限位置对应的舵机角度值用于后续编程。4. 电路连接与系统集成电路部分的目标是清晰、可靠并便于后期检修。所有接线都应做好标签。4.1 电源与控制器接线图整个系统的供电拓扑如下5V/3A DC电源适配器 | V [舵机扩展板 - 电源输入端子] | (提供5V和GND) |------------------- [Arduino Nano] (通过扩展板排母取电) |------------------- [Wemos D1 Mini] (连接扩展板的5V和GND引脚) |------------------- [MG996R舵机] (连接扩展板舵机接口1) |------------------- [MG90s舵机 x2] (连接扩展板舵机接口2, 3) | V (通过扩展板的5V输出引脚) [TM1667 LED模块] (VCC, GND)接线细节与注意事项电源极性连接DC电源适配器到扩展板时务必确认正负极中心正极为常见。接反会烧毁扩展板。舵机信号线信号线通常是黄色或白色连接到扩展板上标有“S”的引脚。MG996R接在接口1两个MG90s分别接在接口2和3。记下这个对应关系代码里会用到。Wemos D1供电直接从扩展板的5V和GND引脚取电。注意虽然Wemos D1的工作电压是3.3V但其VIN引脚可以接受5V输入内部有降压电路。不要将其3.3V引脚接到5V上。LED模块连接TM1667模块的VCC和GND接扩展板的5V和GND。其CLK和DIO引脚分别连接到Wemos D1的D5和D6引脚这两个引脚在ESP8266上对应GPIO14和GPIO12并且启动时不是特殊功能引脚适合用于通信。电平匹配如前所述如果担心3.3V的Wemos D1驱动5V的TM1667信号不稳定可以在CLK和DIO线上各加一个1kΩ的上拉电阻到3.3V或者使用电平转换模块。我实测我手头的模块在3.3V信号下工作正常。4.2 布线技巧与绝缘处理在活动部件上布线是一大挑战。主镜臂走线连接TM1667模块的四根线VCC, GND, CLK, DIO需要从主镜面背面沿着铝制主镜臂走到末端的显示屏。我使用了细径的排线或四根独立的细导线。用黑色的电工胶布或缠绕管将导线束紧贴在铝条的内侧朝向模型中心的一侧每隔5-10cm用一小段胶布固定。这样在镜臂运动时线缆会随之整齐地弯曲不会缠绕或拉扯。舵机线管理三个舵机的线缆也汇集到主镜面背面。使用扎带或线扣将它们捆扎在一起并留出一定的余量确保舵机在运动范围内不会绷紧线缆。多余的线可以盘绕起来固定好。绝缘与测试所有焊接点如果有必须用热缩管保护。在通电进行整体测试前务必用万用表通断档检查所有电源线5V和GND之间是否有短路。确认无误后再上电。5. 核心代码编写与逻辑剖析代码分为两部分运行在Wemos D1上的NTP时钟程序和运行在Arduino Nano上的舵机动画控制程序。两者独立工作。5.1 Wemos D1NTP时间获取与LED显示// 基于Wemos D1 (ESP8266) 的NTP时钟 for TM1667 #include NTPClient.h #include WiFiUdp.h #include TM1667.h // 定义TM1667引脚 (D5CLK, D6DIO) TM1667 display(14, 12); // 注意D5对应GPIO14, D6对应GPIO12 // WiFi凭证 const char* ssid Your_SSID; const char* password Your_PASSWORD; // 定义NTP客户端 WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, pool.ntp.org, 8*3600, 60000); // 东八区60秒更新一次 int lastMinute -1; // 记录上一分钟用于分钟变化时刷新显示 bool colonState true; // 冒号闪烁状态 void setup() { Serial.begin(115200); display.init(); // 初始化TM1667 display.set(BRIGHT_TYPICAL); // 设置亮度 // 连接Wi-Fi WiFi.begin(ssid, password); Serial.print(Connecting to WiFi); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nConnected!); timeClient.begin(); // 启动NTP客户端 } void loop() { timeClient.update(); // 更新NTP时间 int currentHour timeClient.getHours(); int currentMinute timeClient.getMinutes(); // 只在分钟变化或需要闪烁冒号时更新显示减少不必要的通信 if (currentMinute ! lastMinute) { lastMinute currentMinute; updateDisplay(currentHour, currentMinute, colonState); } // 每秒闪烁一次冒号 delay(500); colonState !colonState; updateDisplay(currentHour, currentMinute, colonState); delay(500); } void updateDisplay(int hour, int minute, bool colonOn) { // 将时分拆分成单个数字 int hourTens hour / 10; int hourOnes hour % 10; int minuteTens minute / 10; int minuteOnes minute % 10; // 使用TM1667库的函数显示数字 // display.display(position, digit); position从0开始最左 display.display(0, hourTens); display.display(1, hourOnes); display.display(2, minuteTens); display.display(3, minuteOnes); // 控制中间冒号的显示TM1667的冒号通常由某个特定段控制这里假设是点阵 // 有些TM1667模块的库有专门控制冒号的函数例如 display.point(true); // 这里用通用方法如果库支持直接调用否则可能需要直接操作段数据。 // 假设我们使用的库支持 display.colon(on); display.colon(colonOn); }代码要点解析时区设置NTPClient timeClient(ntpUDP, pool.ntp.org, 8*3600, 60000);中的8*3600表示UTC8中国标准时间。请根据你所在的时区修改。更新策略NTP客户端默认每小时向服务器请求一次时间本地每分钟用update()函数校准一次。60000是更新间隔毫秒。显示优化updateDisplay函数只在时间数字变化或冒号需要闪烁时才被调用避免了不必要的I2C通信使系统更稳定。TM1667库你需要安装对应的TM1667库。不同库的API可能略有不同上述display.colon()函数可能需要根据你实际使用的库进行调整。5.2 Arduino Nano多舵机平滑动画控制// Arduino Nano 多舵机平滑动画控制 #include VarSpeedServo.h // 使用VarSpeedServo库实现速度控制 VarSpeedServo servoBoom; // 主镜臂舵机 (MG996R) VarSpeedServo servoWingL; // 左侧副镜舵机 (MG90s) VarSpeedServo servoWingR; // 右侧副镜舵机 (MG90s) // 定义舵机引脚对应舵机扩展板上的接口 const int pinBoom 9; const int pinWingL 10; const int pinWingR 11; // 定义舵机角度需要根据你的实际安装调试确定 const int boomStowed 85; // 主镜臂收拢角度 const int boomDeployed 160; // 主镜臂展开角度 const int wingClosed 105; // 副镜折叠角度 const int wingOpen 60; // 副镜展开角度 // 动画序列状态 enum AnimationState { IDLE, DEPLOY_BOOM, DEPLOY_WING_L, DEPLOY_WING_R, RETRACT_ALL }; AnimationState currentState IDLE; unsigned long stateStartTime 0; const unsigned long stateDuration 60000; // 整个动画周期60秒 void setup() { Serial.begin(9600); // 将舵机对象关联到引脚 servoBoom.attach(pinBoom); servoWingL.attach(pinWingL); servoWingR.attach(pinWingR); // 初始化位置全部收拢 servoBoom.write(boomStowed, 30, true); // 速度30等待完成 servoWingL.write(wingClosed, 30, true); servoWingR.write(wingClosed, 30, true); delay(2000); // 等待系统稳定 Serial.println(初始化完成等待动画开始...); } void loop() { unsigned long currentTime millis(); unsigned long elapsedTime currentTime - stateStartTime; // 状态机控制动画流程 switch (currentState) { case IDLE: // 空闲状态等待进入下一个动画周期 if (elapsedTime stateDuration) { Serial.println(开始展开动画...); currentState DEPLOY_BOOM; stateStartTime currentTime; // 缓慢展开主镜臂 servoBoom.write(boomDeployed, 15, false); // 速度15不等待异步 } break; case DEPLOY_BOOM: // 等待主镜臂展开完成约需 2000ms / 15 133ms每度这里需要计算 // 更可靠的方法是检查舵机是否到达目标位置VarSpeedServo库的read()可读当前角度 // 这里简化用延时判断 if (elapsedTime 3000) { // 假设展开需要3秒 Serial.println(主镜臂展开完成展开左侧副镜...); currentState DEPLOY_WING_L; servoWingL.write(wingOpen, 20, false); // 速度20展开左副镜 } break; case DEPLOY_WING_L: if (elapsedTime 2000) { // 假设左副镜展开需2秒 Serial.println(左侧副镜展开完成展开右侧副镜...); currentState DEPLOY_WING_R; servoWingR.write(wingOpen, 20, false); } break; case DEPLOY_WING_R: if (elapsedTime 2000) { // 假设右副镜展开需2秒 Serial.println(右侧副镜展开完成保持展开状态...); // 保持展开状态一段时间比如40秒 if (elapsedTime 45000) { // 从动画开始总共过了45秒 Serial.println(开始收拢全部部件...); currentState RETRACT_ALL; // 同时收拢所有部件速度可以快一些 servoBoom.write(boomStowed, 25, false); servoWingL.write(wingClosed, 25, false); servoWingR.write(wingClosed, 25, false); } } break; case RETRACT_ALL: if (elapsedTime 3000) { // 假设收拢需3秒 Serial.println(收拢完成进入空闲状态。); currentState IDLE; stateStartTime currentTime; // 重置计时开始下一个周期等待 } break; } // 必须调用VarSpeedServo的update()函数以处理异步运动 servoBoom.update(); servoWingL.update(); servoWingR.update(); delay(10); // 短延时释放CPU控制权 }代码逻辑深度解析VarSpeedServo库的优势servo.write(angle, speed, wait)函数是核心。speed参数控制了舵机从当前位置运动到目标位置的速度值越小越慢。wait参数为true时该函数会阻塞直到运动完成为false时舵机开始运动程序继续执行这是实现多舵机协同动画的基础。状态机设计使用enum和switch-case构建了一个清晰的状态机。它定义了动画的完整流程空闲 - 展开主臂 - 展开左翼 - 展开右翼 - 保持展示 - 收拢全部 - 回到空闲。每个状态都有明确的进入条件、执行动作和退出条件。时间控制通过millis()记录状态开始时间并与预设的持续时间比较来控制状态切换。这种方式是非阻塞的比使用delay()更高效允许在动画过程中处理其他任务虽然本项目没有。角度调试boomStowed、boomDeployed等角度常量必须通过实际调试确定。上传代码后可以先用简单的servo.write()测试每个舵机的运动范围找到机械极限对应的角度值再填入代码。异步运动与更新当wait参数为false时需要定期调用servo.update()来更新舵机位置。主循环中的delay(10)给了舵机控制器处理时间也使状态判断不至于过于频繁。6. 系统调试、问题排查与优化心得将机械、电子、代码全部组装起来后真正的挑战才开始。以下是我在调试过程中遇到的主要问题及解决方法。6.1 机械运动问题排查问题舵机抖动、啸叫或无法到达指定位置。原因A供电不足。这是最常见的问题。单个MG996R在堵转时峰值电流可超过1A。如果电源功率不够电压会被拉低导致所有舵机和控制器工作不稳定。解决使用万用表测量在舵机动作时扩展板5V引脚上的电压。如果低于4.8V说明电源不足。务必更换为额定电流更大的电源如5V/3A或4A。原因B机械负载过重或卡死。舵机在运动终点遇到硬性阻碍。解决首先断电用手轻轻转动舵机摆臂检查整个传动机构是否顺畅有无干涉点。如果有调整连接件或清理障碍。在代码中确保设定的目标角度在舵机的机械极限和机构的运动范围之内可以留出几度的余量。原因C信号干扰。长信号线可能引入噪声。解决尽量缩短舵机信号线。在舵机电源正负极之间并联一个100-470μF的电解电容注意极性可以吸收瞬间电流波动稳定电压。问题副镜展开不同步或角度不一致。原因两个MG90s舵机存在个体差异或者安装位置、摆臂长度有微小偏差。解决不要假设两个舵机的“90度”位置完全一样。在代码中为每个舵机单独设置wingClosed和wingOpen的角度值。通过串口监视器发送调试命令微调每个舵机的角度直到两侧副镜的折叠和展开位置视觉上对称。6.2 电子与通信问题排查问题Wemos D1无法连接Wi-Fi。解决检查ssid和password是否正确。确保路由器是2.4GHz频段ESP8266不支持5GHz。查看串口监视器输出如果一直连接不上尝试在代码中增加WiFi.setSleepMode(WIFI_NONE_SLEEP);禁用Wi-Fi休眠模式有时能解决连接不稳的问题。问题TM1667显示屏不亮或显示乱码。原因A接线错误。CLK和DIO接反或者电源接反。解决对照模块手册仔细检查接线。原因B引脚冲突。ESP8266的某些引脚如GPIO0, GPIO2, GPIO15在上电时有特殊状态用作普通IO可能导致启动问题。解决确保使用的引脚如D5/D6是“安全”的GPIO。我使用的GPIO14和GPIO12是通用IO。原因C库不兼容或初始化问题。解决尝试在setup()中display.init()后增加一小段延时delay(100)。或者换用其他TM1667库试试。问题NTP时间获取失败或不更新。解决检查网络连接是否正常。可以尝试更换NTP服务器地址如cn.pool.ntp.org或time.windows.com。增加timeClient.setUpdateInterval(60000);来确保更新间隔。如果是在防火墙后可能需要配置网络。6.3 代码逻辑与动画优化问题动画动作生硬不流畅。解决充分利用VarSpeedServo库的速度控制。不要只用write(angle)而是使用write(angle, speed)。对于主镜臂展开这样的大范围运动使用较慢的速度如15-20。对于副镜的小角度运动速度可以稍快如25-30。通过调整速度值可以让动画看起来更逼真、更有机械感。问题动画周期结束后舵机有轻微“嗡嗡”声。原因舵机在到达目标位置后仍会不断微调以抵抗外力保持位置产生持续电流和噪音。解决在动画序列完全结束后所有舵机到达目标位置并保持可以调用servo.detach()函数来断开舵机信号。这会让舵机完全放松消除噪音和功耗。在下一个动画周期开始前再重新attach()。但注意detach()后舵机可以自由转动如果机械结构有自重可能会导致位置偏移。需要评估你的机械设计是否允许。优化更智能的状态判断。上述示例代码用简单的延时来判断动作是否完成这不精确。更好的方法是在发出舵机运动指令writewithwaitfalse时记录目标角度。在主循环中定期检查servo.read()返回的当前角度。当当前角度与目标角度的差值小于一个阈值如2度时才认为动作完成进入下一个状态。这样动画时序会更准确。7. 外观美化与最终效果提升当所有功能调试无误后最后一步是让作品看起来更精致。线缆隐藏使用黑色电工胶布、缠绕管或热缩管将所有外露的线缆整理成束并沿着结构件的背面或内侧走线。对于主镜臂上的线可以用与铝条同色的喷漆轻微喷涂使其视觉上更隐蔽。统一涂装对3D打印的白色连接件、铝条等非装饰部分统一喷涂哑光黑色或深灰色。这能让观众的注意力集中在“镜面”、机械运动和时钟显示上而不是杂乱的零件。增加环境光效可选在模型底座内部或太阳帆板下方嵌入一条暖白色的LED灯带。通过一个光敏电阻或简单的定时器控制在环境光变暗时自动点亮可以为整个模型营造出悬浮在星空中的氛围感。注意灯光要柔和避免直射人眼或影响LED数码管的可读性。校准与最终测试将所有部件固定好进行长达数小时的连续运行测试。观察时钟是否准时动画周期是否稳定机械结构有无松动舵机有无异常发热。确保一切运行如丝般顺滑。这个项目从构思到完成花费的时间和精力远超预期但看到詹姆斯·韦伯望远镜的模型在桌面上缓缓展开臂膀亮出准确的时间那种将创意变为现实的满足感是无与伦比的。它不仅仅是一个时钟更是一个关于工程、编程和太空探索的小小致敬。希望这份详细的指南能帮助你绕过我踩过的那些坑顺利创造出属于你自己的、会动的星辰。
基于Arduino与ESP8266的JWST动画时钟:多舵机协同与NTP网络授时实践
发布时间:2026/5/29 1:39:06
1. 项目概述与核心思路我一直对天文和硬件制作充满热情詹姆斯·韦伯太空望远镜JWST的发射和展开过程堪称工程奇迹总想动手做个东西来致敬它。与其做一个静态模型不如让它“活”起来——既能作为一件精致的桌面摆件又能显示准确的时间。这个想法催生了这个“动画时钟”项目一个会周期性地模拟望远镜展开过程并在展开的“副镜臂”末端显示网络同步时间的装置。这个项目的核心目标很明确用机械动画讲述科学故事用精准时间赋予实用价值。它本质上是一个融合了机械结构、电子控制和网络功能的物联网IoT装置。对于硬件爱好者和创客来说它涉及了几个关键技能点使用Arduino进行多舵机协同控制、利用Wi-Fi模块Wemos D1获取网络时间NTP、整合3D打印的定制结构件以及将LED显示模块嵌入动态机械结构中。整个过程就像完成一次小型的系统工程从构思、建模、电路搭建到代码调试每一步都充满挑战和乐趣。我选择Arduino Nano搭配舵机扩展板来控制三个舵机一个MG996负责主镜臂展开两个MG90s负责两侧副镜折叠主要是看中了其生态的成熟和易用性。对于需要平滑运动的动画效果直接使用标准的Servo.h库可能会让动作显得生硬、跳跃。因此我引入了VarSpeedServo库它可以非常方便地设定舵机的运动速度和目标角度实现类似缓动动画的效果让望远镜的展开过程看起来更逼真、更有仪式感。时间显示部分我放弃了传统的DS1302/DS3231等RTC实时时钟模块因为需要定期校准夏令时和时钟漂移很麻烦。直接使用Wemos D1基于ESP8266连接Wi-Fi通过NTP协议从互联网时间服务器获取时间就成了一劳永逸的方案。它自动处理时区、夏令时精度极高。显示器件则选用了带TM1667驱动芯片的4位7段LED数码管模块这种模块只需要两根信号线CLK, DIO即可通过串行通信控制大大简化了布线特别适合在需要活动的机械臂上使用。整个项目的难点在于机械结构与电子控制的协同。如何将舵机、导线、显示模块巧妙地隐藏或布置在有限的空间内并确保机械运动流畅、不卡线是设计和组装阶段需要反复推敲的。接下来我将从材料准备开始详细拆解每个步骤。2. 材料准备与核心部件解析工欲善其事必先利其器。这个项目的材料清单可以分为结构材料、电子元件和工具三大类。有些材料是现成的有些则需要根据设计进行加工或打印。2.1 结构材料清单与处理要点主镜面基板一块3mm厚的胶合板。这是整个望远镜模型的“骨架”。选择3mm厚度是在强度和重量之间取得平衡太薄易变形太厚则笨重。我用曲线锯Jigsaw将其切割成大致六边形的轮廓尺寸大约为30cm宽。切割后务必用砂纸打磨边缘防止木刺伤手也为后续粘贴提供平整表面。六边形镜片单元50个木质六边形片边长约2cm。这是模拟JWST那18块六边形镀金主镜的简化版。直接购买成品比自己切割省时省力得多。我最初计划用金色箔纸包裹它们来增加反光效果但实测发现即使用哑光黑板漆涂成深色在光照下木质纹理和金色反光也能形成不错的质感。如果你追求更高的镜面效果可以使用真的镜面贴纸或亚克力镜片。镜臂与支撑轴一根直径12mm的硬木圆棒榉木或桦木都行。它负责连接主镜面和下方的太阳帆板Sun Shield是整个模型的中央支柱。它的垂直度和强度至关重要我选用硬木是为了避免长期受力后弯曲。太阳帆板基板另一块3mm胶合板的下脚料。将其切割成JWST标志性的五层太阳帆板简化形状一个类似风筝的多边形。这一步没有严格尺寸我主要是参考图片比例确保它与上方的主镜面大小协调视觉上不头重脚轻。舵机联动机构材料主镜臂Boom两条12mm宽、2mm厚的铝条。铝材质轻且有一定刚性是制作可展开臂的理想材料。我在网上购买了预切割好的条状铝材。连接件与舵机座这是需要3D打印的核心结构件。包括连接铝条和舵机摆臂的铰链、固定MG996舵机的底座、以及用于推动副镜的MG90s舵机延长臂。使用PLA材料打印即可强度足够。粘合剂本项目会用到多种粘合剂各有其职木工胶用于永久性粘合木质部件如将六边形片粘到基板上。但干燥慢在需要快速定位时不够用。热熔胶快速固定、临时定位、填充小缝隙的利器。我大量用于固定电线、初步粘合舵机等。优点是速干缺点是长期受力或高温环境下可能失效。CA胶快干胶/401/502用于粘合3D打印件与小面积的金属、塑料部件固化速度快强度高。涂料哑光黑板漆。用于将主镜面基板的边缘、背面以及太阳帆板涂成深空黑色吸收杂散光让观众的视线聚焦在“镜面”和机械结构上。注意关于锂电池的安全警示。原文作者提到了因故障锂电池引发的 workshop fire。这给我们所有人敲响了警钟。无论何时在给任何锂电池包括航模电池、充电宝电芯等充电时务必使用专业的防爆充电袋并在无人值守时远离易燃物。对于本项目我们使用的是5V直流电源适配器供电不涉及高压电池但安全习惯必须养成。2.2 电子元件选型与功能解析主控制器Arduino Nano 舵机扩展板为什么用NanoArduino Nano体积小巧引脚功能与Uno兼容价格便宜非常适合嵌入到这种空间有限的项目中。它负责运行控制三个舵机动画的核心逻辑。为什么需要舵机扩展板直接驱动多个舵机尤其是MG996这种扭力大的对Arduino的5V引脚是巨大负担可能导致板子重启或损坏。舵机扩展板自带独立的电源输入接口和稳压电路可以为舵机提供充足且稳定的电流需外接5V/3A以上的电源同时通过信号线隔离控制保护了主控板。我使用的扩展板可以直接堆叠在Nano上方节省空间。网络时间控制器Wemos D1 Mini核心优势它本质上是一个集成了ESP8266 WiFi芯片的开发板可以直接用Arduino IDE进行编程。其内置的Wi-Fi功能使得获取NTP时间变得非常简单无需额外的网络模块。同时它的5V耐受引脚可以直接从舵机扩展板的5V取电整合非常方便。舵机MG996R用于主镜臂这是一款标准舵机金属齿轮扭力大约10kg·cm速度中等。选择它是因为展开主镜臂需要克服一定的初始静摩擦和结构阻力需要足够的扭力来保证动作可靠。MG90S用于两侧副镜这是一款微型舵机同样是金属齿轮扭力较小约1.8kg·cm但体积小、重量轻。副镜的折叠动作负载很小对扭力要求不高但需要精确的角度控制MG90S完全胜任且其小巧的体积便于隐藏在主镜结构侧面。显示模块TM1667 LED 4位7段数码管驱动芯片TM1667的优势传统的7段数码管需要占用大量GPIO引脚4位数码管动态扫描至少需要12个引脚。TM1667是一款LED驱动控制专用电路采用I2C-like的两线串行接口CLK, DIO只需要2个引脚就能控制多位LED极大简化了连接。模块通常自带限流电阻和滤波电容即插即用。供电考虑该模块工作电压一般为3.3V-5V。由于Wemos D1的IO口是3.3V电平而模块是5V供电为了确保信号稳定最好使用一个简单的双向电平转换器或者确认该模块在3.3V信号下能可靠工作很多TM1667模块可以。电源整个系统需要一个稳定的5V直流电源。计算总电流两个MG90S空闲约100mA工作峰值可达500mA一个MG996R空闲约150mA工作峰值可达1.5AWemos D1约200mALED模块峰值约200mA。理论上峰值电流可能接近2.5A。因此我选择了一个输出为5V/3A的DC电源适配器并连接到舵机扩展板的电源输入端确保留有充足余量避免供电不足导致舵机抖动或控制器复位。3. 机械结构设计与组装实战机械部分是整个项目的骨架也是最考验耐心和精细度的环节。我的设计原则是在保证运动功能的前提下尽可能简化结构便于组装和调试。3.1 主镜面与副镜铰链制作首先处理核心视觉部件——主镜面。布局与粘贴在3mm胶合板上用铅笔轻轻勾勒出由18个六边形组成的近似圆形轮廓。将木质六边形片按照JWST的排列方式中心一个周围环绕用木工胶粘贴上去。这里有个关键技巧不要一次性在所有六边形背面涂胶。先固定中心及关键位置的几片等其初步固化定位后再填充周围的片子这样可以边贴边调整确保整体排列整齐。我最初尝试全部涂胶后一起贴上结果有些片子滑动错位清理起来非常麻烦。强化固定木工胶完全干燥需要数小时。为了在后续加工中保持稳固我在每个六边形片的四周缝隙处点了一些热熔胶进行辅助固定。注意热熔胶用量要少避免溢出影响正面观感。涂装边缘用遮盖胶带美纹纸仔细贴住所有六边形片的边缘保护其正面。然后用小号板刷将露出的胶合板基板边缘和背面涂上哑光黑板漆。涂装要薄而均匀待第一遍干透后再涂第二遍以达到深邃的黑色效果。制作副镜铰链两侧的副镜各由3个六边形组成需要能够向内折叠。我采用了最简易但有效的“布基胶带铰链”法。将副镜部分平放在桌面上在正面接缝处贴上遮盖胶带临时固定。将整个组件翻转在背面的接缝处沿着缝隙粘贴一条宽度约2cm的黑色布基胶带Gaffer Tape。布基胶带纤维强度高反复弯折不易断裂是理想的DIY铰链材料。粘贴时确保胶带平整无气泡并且接缝正好位于胶带宽度的中心线上。为了隐藏胶带的颜色我只有灰色我用小笔刷蘸取黑板漆将铰链内侧折叠时会露出的部分涂黑使其在视觉上融为一体。3.2 太阳帆板与中央支柱组装太阳帆板是静态部分主要起装饰和配重作用。切割与打磨根据设计图样用曲线锯或带锯切割出五边形太阳帆板形状。用砂纸将所有边缘打磨光滑特别是表面因为任何凹凸都会在贴箔后非常明显。粘贴银色箔纸我使用了带背胶的银色铝箔胶带。粘贴时从一个角开始慢慢撕开背纸一边用刮板或银行卡推平一边前进尽量避免产生气泡和褶皱。遇到曲面边缘时可以将箔纸裁成小条分段粘贴。完成后用锋利的美工刀沿边缘修整。确定角度与钻孔参考JWST实物图片太阳帆板与主镜面有一个夹角。我在作为底座的方形木块由山毛榉制成提供稳定配重上以一个约15度的倾斜角钻出一个直径略小于12mm圆棒例如11.5mm的孔。这样圆棒插入后会非常紧配合木工胶可以形成牢固的楔形连接。整体连接将12mm圆棒一端涂胶后插入底座。另一端穿过太阳帆板中心的孔此孔需垂直钻透并涂胶与太阳帆板固定。关键一步在主镜面背面的中心位置确定一个点将主镜面“套”在圆棒上并用热熔胶从背面多点固定。此时需要借助水平尺或肉眼观察调整主镜面使其处于水平状态等待热熔胶固化。热熔胶在这里起到了快速定位和调整的作用后期还可以用更牢固的环氧树脂进行加固。3.3 舵机传动机构设计与安装这是实现动画的核心需要精确的测量和耐心的调试。主镜臂MG996R安装设计思路主镜臂由两条平行铝条构成一端铰接在镜面底部两侧固定点另一端铰接在承载LED屏幕的“副镜”结构上。MG996R舵机安装在主镜面背面顶部中央其摆臂通过一个连杆推动整个平行四连杆机构实现镜臂的展开与收拢。3D打印连接件我设计了三种连接件a) 用于将铝条端部与固定点/副镜结构连接的“U型夹”b) 用于连接两条铝条保持平行的“横撑”c) 用于将舵机摆臂与连杆连接的“球头连杆座”。所有连接均使用M3螺丝和防松螺母方便调节和拆卸。安装步骤先将两个U型夹用螺丝固定在主镜面底部预设的位置对称。将铝条组装上。然后将MG996R用螺丝固定在主镜面背面顶部的3D打印底座上。连接舵机摆臂和连杆。调试关键在舵机通电回中位90度时手动调整摆臂和连杆的长度使主镜臂处于完全收拢状态。然后通过代码控制舵机转到0度和180度观察镜臂展开和收拢的极限位置是否顺畅、无干涉。可能需要微调固定点位置或连杆长度。副镜驱动MG90s安装初始方案与问题我最初设想用遥控飞机上常用的钢丝推拉线来驱动副镜舵机放在中心位置。但实际测试发现钢丝的柔性导致传动存在虚位无法精确控制副镜折叠到预定角度且回弹力不足。改进方案改为将MG90s舵机直接安装在副镜背面的侧面舵机轴心尽可能靠近副镜的旋转轴心。3D打印一个加长的舵机摆臂摆臂的末端通过一个“滑动槽”机构与副镜背面连接。滑动槽机构在副镜背面固定一个小块上面开一个弧形槽。舵机摆臂的末端连接一个销钉销钉卡在这个弧形槽里。当舵机转动时摆臂推动销钉在槽内滑动从而带动副镜旋转。这个设计的妙处在于它将舵机的圆周运动转换成了更适合推动平板旋转的近似直线运动并且通过槽的长度限制了副镜的旋转角度起到了物理限位的作用比直接用摆臂顶推要可靠得多。安装与调试用热熔胶或CA胶将舵机底座粘在副镜背面。连接摆臂和滑动槽。上电后在代码中慢慢测试舵机从0到180度的运动观察副镜是否从完全折叠贴合主镜平滑运动到完全展开约30度角。记录下这两个极限位置对应的舵机角度值用于后续编程。4. 电路连接与系统集成电路部分的目标是清晰、可靠并便于后期检修。所有接线都应做好标签。4.1 电源与控制器接线图整个系统的供电拓扑如下5V/3A DC电源适配器 | V [舵机扩展板 - 电源输入端子] | (提供5V和GND) |------------------- [Arduino Nano] (通过扩展板排母取电) |------------------- [Wemos D1 Mini] (连接扩展板的5V和GND引脚) |------------------- [MG996R舵机] (连接扩展板舵机接口1) |------------------- [MG90s舵机 x2] (连接扩展板舵机接口2, 3) | V (通过扩展板的5V输出引脚) [TM1667 LED模块] (VCC, GND)接线细节与注意事项电源极性连接DC电源适配器到扩展板时务必确认正负极中心正极为常见。接反会烧毁扩展板。舵机信号线信号线通常是黄色或白色连接到扩展板上标有“S”的引脚。MG996R接在接口1两个MG90s分别接在接口2和3。记下这个对应关系代码里会用到。Wemos D1供电直接从扩展板的5V和GND引脚取电。注意虽然Wemos D1的工作电压是3.3V但其VIN引脚可以接受5V输入内部有降压电路。不要将其3.3V引脚接到5V上。LED模块连接TM1667模块的VCC和GND接扩展板的5V和GND。其CLK和DIO引脚分别连接到Wemos D1的D5和D6引脚这两个引脚在ESP8266上对应GPIO14和GPIO12并且启动时不是特殊功能引脚适合用于通信。电平匹配如前所述如果担心3.3V的Wemos D1驱动5V的TM1667信号不稳定可以在CLK和DIO线上各加一个1kΩ的上拉电阻到3.3V或者使用电平转换模块。我实测我手头的模块在3.3V信号下工作正常。4.2 布线技巧与绝缘处理在活动部件上布线是一大挑战。主镜臂走线连接TM1667模块的四根线VCC, GND, CLK, DIO需要从主镜面背面沿着铝制主镜臂走到末端的显示屏。我使用了细径的排线或四根独立的细导线。用黑色的电工胶布或缠绕管将导线束紧贴在铝条的内侧朝向模型中心的一侧每隔5-10cm用一小段胶布固定。这样在镜臂运动时线缆会随之整齐地弯曲不会缠绕或拉扯。舵机线管理三个舵机的线缆也汇集到主镜面背面。使用扎带或线扣将它们捆扎在一起并留出一定的余量确保舵机在运动范围内不会绷紧线缆。多余的线可以盘绕起来固定好。绝缘与测试所有焊接点如果有必须用热缩管保护。在通电进行整体测试前务必用万用表通断档检查所有电源线5V和GND之间是否有短路。确认无误后再上电。5. 核心代码编写与逻辑剖析代码分为两部分运行在Wemos D1上的NTP时钟程序和运行在Arduino Nano上的舵机动画控制程序。两者独立工作。5.1 Wemos D1NTP时间获取与LED显示// 基于Wemos D1 (ESP8266) 的NTP时钟 for TM1667 #include NTPClient.h #include WiFiUdp.h #include TM1667.h // 定义TM1667引脚 (D5CLK, D6DIO) TM1667 display(14, 12); // 注意D5对应GPIO14, D6对应GPIO12 // WiFi凭证 const char* ssid Your_SSID; const char* password Your_PASSWORD; // 定义NTP客户端 WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, pool.ntp.org, 8*3600, 60000); // 东八区60秒更新一次 int lastMinute -1; // 记录上一分钟用于分钟变化时刷新显示 bool colonState true; // 冒号闪烁状态 void setup() { Serial.begin(115200); display.init(); // 初始化TM1667 display.set(BRIGHT_TYPICAL); // 设置亮度 // 连接Wi-Fi WiFi.begin(ssid, password); Serial.print(Connecting to WiFi); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nConnected!); timeClient.begin(); // 启动NTP客户端 } void loop() { timeClient.update(); // 更新NTP时间 int currentHour timeClient.getHours(); int currentMinute timeClient.getMinutes(); // 只在分钟变化或需要闪烁冒号时更新显示减少不必要的通信 if (currentMinute ! lastMinute) { lastMinute currentMinute; updateDisplay(currentHour, currentMinute, colonState); } // 每秒闪烁一次冒号 delay(500); colonState !colonState; updateDisplay(currentHour, currentMinute, colonState); delay(500); } void updateDisplay(int hour, int minute, bool colonOn) { // 将时分拆分成单个数字 int hourTens hour / 10; int hourOnes hour % 10; int minuteTens minute / 10; int minuteOnes minute % 10; // 使用TM1667库的函数显示数字 // display.display(position, digit); position从0开始最左 display.display(0, hourTens); display.display(1, hourOnes); display.display(2, minuteTens); display.display(3, minuteOnes); // 控制中间冒号的显示TM1667的冒号通常由某个特定段控制这里假设是点阵 // 有些TM1667模块的库有专门控制冒号的函数例如 display.point(true); // 这里用通用方法如果库支持直接调用否则可能需要直接操作段数据。 // 假设我们使用的库支持 display.colon(on); display.colon(colonOn); }代码要点解析时区设置NTPClient timeClient(ntpUDP, pool.ntp.org, 8*3600, 60000);中的8*3600表示UTC8中国标准时间。请根据你所在的时区修改。更新策略NTP客户端默认每小时向服务器请求一次时间本地每分钟用update()函数校准一次。60000是更新间隔毫秒。显示优化updateDisplay函数只在时间数字变化或冒号需要闪烁时才被调用避免了不必要的I2C通信使系统更稳定。TM1667库你需要安装对应的TM1667库。不同库的API可能略有不同上述display.colon()函数可能需要根据你实际使用的库进行调整。5.2 Arduino Nano多舵机平滑动画控制// Arduino Nano 多舵机平滑动画控制 #include VarSpeedServo.h // 使用VarSpeedServo库实现速度控制 VarSpeedServo servoBoom; // 主镜臂舵机 (MG996R) VarSpeedServo servoWingL; // 左侧副镜舵机 (MG90s) VarSpeedServo servoWingR; // 右侧副镜舵机 (MG90s) // 定义舵机引脚对应舵机扩展板上的接口 const int pinBoom 9; const int pinWingL 10; const int pinWingR 11; // 定义舵机角度需要根据你的实际安装调试确定 const int boomStowed 85; // 主镜臂收拢角度 const int boomDeployed 160; // 主镜臂展开角度 const int wingClosed 105; // 副镜折叠角度 const int wingOpen 60; // 副镜展开角度 // 动画序列状态 enum AnimationState { IDLE, DEPLOY_BOOM, DEPLOY_WING_L, DEPLOY_WING_R, RETRACT_ALL }; AnimationState currentState IDLE; unsigned long stateStartTime 0; const unsigned long stateDuration 60000; // 整个动画周期60秒 void setup() { Serial.begin(9600); // 将舵机对象关联到引脚 servoBoom.attach(pinBoom); servoWingL.attach(pinWingL); servoWingR.attach(pinWingR); // 初始化位置全部收拢 servoBoom.write(boomStowed, 30, true); // 速度30等待完成 servoWingL.write(wingClosed, 30, true); servoWingR.write(wingClosed, 30, true); delay(2000); // 等待系统稳定 Serial.println(初始化完成等待动画开始...); } void loop() { unsigned long currentTime millis(); unsigned long elapsedTime currentTime - stateStartTime; // 状态机控制动画流程 switch (currentState) { case IDLE: // 空闲状态等待进入下一个动画周期 if (elapsedTime stateDuration) { Serial.println(开始展开动画...); currentState DEPLOY_BOOM; stateStartTime currentTime; // 缓慢展开主镜臂 servoBoom.write(boomDeployed, 15, false); // 速度15不等待异步 } break; case DEPLOY_BOOM: // 等待主镜臂展开完成约需 2000ms / 15 133ms每度这里需要计算 // 更可靠的方法是检查舵机是否到达目标位置VarSpeedServo库的read()可读当前角度 // 这里简化用延时判断 if (elapsedTime 3000) { // 假设展开需要3秒 Serial.println(主镜臂展开完成展开左侧副镜...); currentState DEPLOY_WING_L; servoWingL.write(wingOpen, 20, false); // 速度20展开左副镜 } break; case DEPLOY_WING_L: if (elapsedTime 2000) { // 假设左副镜展开需2秒 Serial.println(左侧副镜展开完成展开右侧副镜...); currentState DEPLOY_WING_R; servoWingR.write(wingOpen, 20, false); } break; case DEPLOY_WING_R: if (elapsedTime 2000) { // 假设右副镜展开需2秒 Serial.println(右侧副镜展开完成保持展开状态...); // 保持展开状态一段时间比如40秒 if (elapsedTime 45000) { // 从动画开始总共过了45秒 Serial.println(开始收拢全部部件...); currentState RETRACT_ALL; // 同时收拢所有部件速度可以快一些 servoBoom.write(boomStowed, 25, false); servoWingL.write(wingClosed, 25, false); servoWingR.write(wingClosed, 25, false); } } break; case RETRACT_ALL: if (elapsedTime 3000) { // 假设收拢需3秒 Serial.println(收拢完成进入空闲状态。); currentState IDLE; stateStartTime currentTime; // 重置计时开始下一个周期等待 } break; } // 必须调用VarSpeedServo的update()函数以处理异步运动 servoBoom.update(); servoWingL.update(); servoWingR.update(); delay(10); // 短延时释放CPU控制权 }代码逻辑深度解析VarSpeedServo库的优势servo.write(angle, speed, wait)函数是核心。speed参数控制了舵机从当前位置运动到目标位置的速度值越小越慢。wait参数为true时该函数会阻塞直到运动完成为false时舵机开始运动程序继续执行这是实现多舵机协同动画的基础。状态机设计使用enum和switch-case构建了一个清晰的状态机。它定义了动画的完整流程空闲 - 展开主臂 - 展开左翼 - 展开右翼 - 保持展示 - 收拢全部 - 回到空闲。每个状态都有明确的进入条件、执行动作和退出条件。时间控制通过millis()记录状态开始时间并与预设的持续时间比较来控制状态切换。这种方式是非阻塞的比使用delay()更高效允许在动画过程中处理其他任务虽然本项目没有。角度调试boomStowed、boomDeployed等角度常量必须通过实际调试确定。上传代码后可以先用简单的servo.write()测试每个舵机的运动范围找到机械极限对应的角度值再填入代码。异步运动与更新当wait参数为false时需要定期调用servo.update()来更新舵机位置。主循环中的delay(10)给了舵机控制器处理时间也使状态判断不至于过于频繁。6. 系统调试、问题排查与优化心得将机械、电子、代码全部组装起来后真正的挑战才开始。以下是我在调试过程中遇到的主要问题及解决方法。6.1 机械运动问题排查问题舵机抖动、啸叫或无法到达指定位置。原因A供电不足。这是最常见的问题。单个MG996R在堵转时峰值电流可超过1A。如果电源功率不够电压会被拉低导致所有舵机和控制器工作不稳定。解决使用万用表测量在舵机动作时扩展板5V引脚上的电压。如果低于4.8V说明电源不足。务必更换为额定电流更大的电源如5V/3A或4A。原因B机械负载过重或卡死。舵机在运动终点遇到硬性阻碍。解决首先断电用手轻轻转动舵机摆臂检查整个传动机构是否顺畅有无干涉点。如果有调整连接件或清理障碍。在代码中确保设定的目标角度在舵机的机械极限和机构的运动范围之内可以留出几度的余量。原因C信号干扰。长信号线可能引入噪声。解决尽量缩短舵机信号线。在舵机电源正负极之间并联一个100-470μF的电解电容注意极性可以吸收瞬间电流波动稳定电压。问题副镜展开不同步或角度不一致。原因两个MG90s舵机存在个体差异或者安装位置、摆臂长度有微小偏差。解决不要假设两个舵机的“90度”位置完全一样。在代码中为每个舵机单独设置wingClosed和wingOpen的角度值。通过串口监视器发送调试命令微调每个舵机的角度直到两侧副镜的折叠和展开位置视觉上对称。6.2 电子与通信问题排查问题Wemos D1无法连接Wi-Fi。解决检查ssid和password是否正确。确保路由器是2.4GHz频段ESP8266不支持5GHz。查看串口监视器输出如果一直连接不上尝试在代码中增加WiFi.setSleepMode(WIFI_NONE_SLEEP);禁用Wi-Fi休眠模式有时能解决连接不稳的问题。问题TM1667显示屏不亮或显示乱码。原因A接线错误。CLK和DIO接反或者电源接反。解决对照模块手册仔细检查接线。原因B引脚冲突。ESP8266的某些引脚如GPIO0, GPIO2, GPIO15在上电时有特殊状态用作普通IO可能导致启动问题。解决确保使用的引脚如D5/D6是“安全”的GPIO。我使用的GPIO14和GPIO12是通用IO。原因C库不兼容或初始化问题。解决尝试在setup()中display.init()后增加一小段延时delay(100)。或者换用其他TM1667库试试。问题NTP时间获取失败或不更新。解决检查网络连接是否正常。可以尝试更换NTP服务器地址如cn.pool.ntp.org或time.windows.com。增加timeClient.setUpdateInterval(60000);来确保更新间隔。如果是在防火墙后可能需要配置网络。6.3 代码逻辑与动画优化问题动画动作生硬不流畅。解决充分利用VarSpeedServo库的速度控制。不要只用write(angle)而是使用write(angle, speed)。对于主镜臂展开这样的大范围运动使用较慢的速度如15-20。对于副镜的小角度运动速度可以稍快如25-30。通过调整速度值可以让动画看起来更逼真、更有机械感。问题动画周期结束后舵机有轻微“嗡嗡”声。原因舵机在到达目标位置后仍会不断微调以抵抗外力保持位置产生持续电流和噪音。解决在动画序列完全结束后所有舵机到达目标位置并保持可以调用servo.detach()函数来断开舵机信号。这会让舵机完全放松消除噪音和功耗。在下一个动画周期开始前再重新attach()。但注意detach()后舵机可以自由转动如果机械结构有自重可能会导致位置偏移。需要评估你的机械设计是否允许。优化更智能的状态判断。上述示例代码用简单的延时来判断动作是否完成这不精确。更好的方法是在发出舵机运动指令writewithwaitfalse时记录目标角度。在主循环中定期检查servo.read()返回的当前角度。当当前角度与目标角度的差值小于一个阈值如2度时才认为动作完成进入下一个状态。这样动画时序会更准确。7. 外观美化与最终效果提升当所有功能调试无误后最后一步是让作品看起来更精致。线缆隐藏使用黑色电工胶布、缠绕管或热缩管将所有外露的线缆整理成束并沿着结构件的背面或内侧走线。对于主镜臂上的线可以用与铝条同色的喷漆轻微喷涂使其视觉上更隐蔽。统一涂装对3D打印的白色连接件、铝条等非装饰部分统一喷涂哑光黑色或深灰色。这能让观众的注意力集中在“镜面”、机械运动和时钟显示上而不是杂乱的零件。增加环境光效可选在模型底座内部或太阳帆板下方嵌入一条暖白色的LED灯带。通过一个光敏电阻或简单的定时器控制在环境光变暗时自动点亮可以为整个模型营造出悬浮在星空中的氛围感。注意灯光要柔和避免直射人眼或影响LED数码管的可读性。校准与最终测试将所有部件固定好进行长达数小时的连续运行测试。观察时钟是否准时动画周期是否稳定机械结构有无松动舵机有无异常发热。确保一切运行如丝般顺滑。这个项目从构思到完成花费的时间和精力远超预期但看到詹姆斯·韦伯望远镜的模型在桌面上缓缓展开臂膀亮出准确的时间那种将创意变为现实的满足感是无与伦比的。它不仅仅是一个时钟更是一个关于工程、编程和太空探索的小小致敬。希望这份详细的指南能帮助你绕过我踩过的那些坑顺利创造出属于你自己的、会动的星辰。