1. 项目概述用红外线玩一场实体寻宝游戏如果你手头有几块Adafruit的Circuit Playground Express开发板除了点亮LED、播放声音这些基础操作有没有想过用它们来设计一个能跑能藏的实体互动游戏红外寻宝游戏就是一个绝佳的选择。这个项目的核心是利用每块板子上都自带的红外发射TX和接收RX模块让开发板之间能够“悄悄话”。一块板子扮演不断广播自己身份ID的“宝藏”另一块板子则扮演四处搜寻信号的“猎人”。当猎人接收到宝藏发出的特定红外信号时就算成功找到并通过板载的10颗NeoPixel LED给出视觉反馈全部找到后还有胜利动画。这不仅仅是复制一段代码而是理解如何将无线通信、状态管理和用户交互整合到一个具体的、好玩的嵌入式项目里。对于初学者这是一个从图形化编程MakeCode过渡到代码编程CircuitPython的完美桥梁对于有经验的开发者则能深入理解红外通信的底层协议处理、资源管理和游戏逻辑设计。你需要准备至少两块Circuit Playground Express开发板一猎一宝即可开玩但多块更有趣以及为其供电的电池组。整个项目不涉及复杂的焊接或外设核心就是软件逻辑。接下来我会拆解两种实现方案MakeCode和CircuitPython的每一个细节包括为什么这么设计、可能会遇到什么坑以及如何让游戏体验更好。2. 红外通信基础与硬件定位在动手写代码之前必须搞清楚我们手里的“武器”到底怎么工作。Circuit Playground Express后面简称CPX上的红外通信并非我们想象中像Wi-Fi或蓝牙那样的复杂双向数据流。它更接近于老式电视遥控器单向、简单、基于特定协议的数字脉冲传输。2.1 红外通信的本质光脉冲编码红外线是人眼不可见的光。CPX上的红外发射管TX本质上是一个能快速开关的红外LED通过控制其亮灭的频率和时长来编码“0”和“1”。接收管RX则是一个对特定红外光敏感的光电晶体管负责检测这些光脉冲并将其转换回电信号。这里的关键是“调制”。为了防止环境光如日光灯、太阳光的干扰红外信号通常被调制在一个特定的载波频率上例如38kHz。发射管以38kHz的频率闪烁代表“正在发送”不闪烁则代表“空闲”。接收器内部有一个带通滤波器只对38kHz附近的信号敏感从而有效滤除噪声。在CPX的编程中无论是MakeCode的红外发送数字积木还是CircuitPython的adafruit_irremote库都帮我们封装好了这个调制与解调的过程。我们只需要关心“我要发送数字几”和“我收到了数字几”。这种抽象极大地降低了开发难度但也让我们容易忽略一些底层限制比如通信距离、方向性和抗干扰能力这些在游戏部署时至关重要。2.2. CPX上的红外硬件与视觉辅助找到你CPX板子中央的复位按钮。紧贴着它的左右两侧有两个非常小的、类似黑色树脂封装的器件。复位按钮左侧、按键A的右边那个标着“TX”的就是红外发射管。复位按钮右侧、按键B的左边那个标着“RX”的就是红外接收管。它们的物理位置决定了通信的方向性最佳通信路径大致在板子正前方的一个锥形区域内。由于红外光不可见调试时会非常困难。你不知道宝藏是否在正常发射信号也不知道猎人是否对准了方向。这就是为什么项目中必须用NeoPixel来提供视觉反馈。在宝藏发送信号的瞬间我们点亮所有NeoPixel比如红色这样操作者就能明确知道“此刻正在广播”。同样猎人每找到一个宝藏就点亮一颗特定颜色的NeoPixel作为记录。没有这个视觉辅助整个游戏就会变成盲人摸象体验和可调试性都会极差。注意环境光干扰强烈的环境光特别是含有红外成分的光如白炽灯、太阳光会淹没微弱的红外信号。测试时如果发现通信不稳定或距离骤减首先检查环境光线。理想的游戏环境是室内普通光照避免阳光直射或强光灯泡近距离照射开发板。3. MakeCode方案全解析从积木到逻辑对于编程新手或希望快速原型验证的人来说微软的MakeCode图形化编程环境是首选。它的积木块方式让逻辑一目了然但要想玩得转也得理解每块积木背后的含义。3.1 宝藏端程序简约而不简单宝藏端的逻辑极其简单循环等待 - 发送ID - 视觉提示 - 继续等待。我们来看关键积木块当启动时 永久循环 红外发送数字 (1) 所有NeoPixel显示颜色 红色 等待 (0.25) 秒 所有NeoPixel清除 等待 (15) 秒这段代码里藏了几个设计要点发送数字红外发送数字 (1)这个积木位于“网络”类别下。它内部已经处理了38kHz调制和NEC协议编码。我们只需填入一个整数ID。这里填1意味着这块宝藏板的身份是“1号宝藏”。视觉提示发送前后对NeoPixel的操作点亮红色0.25秒后熄灭至关重要。它有两个作用一是给玩家提示二是给自己调试。如果你看到板子没亮红灯那肯定没在发送信号。等待间隔15秒这是游戏难度调节的关键参数。如果设置为1秒宝藏频繁广播猎人很容易找到设置为15秒或更长猎人就需要更耐心地守候或更精准地搜索。这个延迟也让宝藏的电池续航更久。实操心得如何制作多个宝藏原文提供了三个宝藏的代码链接其本质就是修改红外发送数字积木中的数字。你完全不需要下载三个不同的程序。正确做法是写好一个宝藏程序后在MakeCode编辑器中只修改那个数字比如改成2然后点击“下载”按钮将.uf2文件保存为“treasure2.uf2”。接着用数据线连接第二块CPX板将其重置进入Bootloader模式双击复位键会出现一个名为“CPLAYBOOT”的U盘把“treasure2.uf2”文件拖进去即可。重复此过程制作3号宝藏。这样效率最高也避免了管理多个项目的混乱。3.2 猎人端程序状态管理的艺术猎人端是游戏的大脑它需要完成初始化状态、持续监听红外信号、解码并比对ID、更新找到状态、检查胜利条件。我们拆解它的三个核心部分。3.2.1当启动时变量的初始化这里初始化了6个变量TREASURE_1,TREASURE_2,TREASURE_3: 分别设置为1, 2, 3。这是期望收到的宝藏ID用于后续比对。found_1,found_2,found_3: 布尔型变量初始设为假。用来记录对应编号的宝藏是否已被找到。清除所有NeoPixel确保游戏开始时所有灯是灭的。为什么不用列表而用多个独立变量在MakeCode的积木环境中使用列表进行复杂索引和比对操作反而更麻烦。用多个变量虽然看起来冗余但逻辑清晰易于用积木实现。这是图形化编程环境下的一种务实选择。3.2.2永久循环胜利条件检测这个循环只做一件事不断检查found_1、found_2、found_3是否都变为真。这里用了一个“与”运算积木如果 found_1 真 与 found_2 真 与 found_3 真。一旦条件满足就播放一个“ba ding”音效然后启动一个彩虹色循环动画。注意这个动画是在一个永久循环里的永久循环意味着进入胜利状态后程序就卡在这里了除非你按下复位键重启游戏。这是一种简单的游戏状态机实现。3.2.3当红外接收到数字(found_id)游戏的核心逻辑这是事件驱动编程的典型例子。每当红外接收器解码出一个有效的数字这个事件处理块就会被触发并将收到的数字存入变量found_id。当红外接收到数字(found_id) 如果 found_id TREASURE_1 那么 将 found_1 设为 真 NeoPixel 0 显示颜色 红色 否则 如果 found_id TREASURE_2 那么 将 found_2 设为 真 NeoPixel 1 显示颜色 绿色 否则 如果 found_id TREASURE_3 那么 将 found_3 设为 真 NeoPixel 2 显示颜色 蓝色逻辑是线性的比对先检查是不是1号宝藏如果不是再检查是不是2号以此类推。每找到一个就点亮一颗特定颜色和位置的NeoPixel0号灯红1号灯绿2号灯蓝作为进度提示。避坑指南红外接收的防抖与误触发在实际测试中我发现有时手在红外接收器前快速晃动或者有别的红外源如另一个遥控器干扰可能会触发误事件。MakeCode底层库已经做了一定过滤但为了更稳定可以在事件处理块最开头加一个等待 100 毫秒的简单防抖。虽然这会略微降低响应速度但能有效避免因单次误信号导致的重复记录。对于寻宝游戏来说可靠性比毫秒级的响应更重要。4. CircuitPython方案深度实现如果你已经熟悉了MakeCode的逻辑想更深入地控制硬件、学习更灵活的编程方式或者需要扩展更复杂的游戏规则比如更多宝藏、动态ID、信号强度指示等CircuitPython是必然选择。它用代码提供了更强大的表达能力和更直接的硬件访问。4.1 宝藏端代码解读底层脉冲输出让我们逐段分析CircuitPython的宝藏代码treasure.pyimport time import board import pulseio import adafruit_irremote import neopixel # 配置宝藏信息 TREASURE_ID 1 TRANSMIT_DELAY 15 # 创建NeoPixel对象用于状态指示 pixels neopixel.NeoPixel(board.NEOPIXEL, 10) # 创建一个pulseio输出以38KHz频率在红外发射器上发送信号 pwm pulseio.PWMOut(board.IR_TX, frequency38000, duty_cycle2 ** 15) pulseout pulseio.PulseOut(pwm) # 创建一个编码器将数字转换为IR脉冲序列 encoder adafruit_irremote.GenericTransmit(header[9500, 4500], one[550, 550], zero[550, 1700], trail0) while True: pixels.fill(0xFF0000) # 点亮所有NeoPixel为红色 encoder.transmit(pulseout, [TREASURE_ID]*4) # 发送ID time.sleep(0.25) pixels.fill(0) # 熄灭NeoPixel time.sleep(TRANSMIT_DELAY)关键点解析pulseio.PWMOut这是产生38kHz载波的关键。duty_cycle2**15表示占空比为50%因为2**15是16位计数器最大值32768的一半这是红外发射的典型设置。adafruit_irremote.GenericTransmit这个编码器配置了NEC协议的具体时序。参数header[9500, 4500]表示起始脉冲是9.5ms高电平加4.5ms低电平。one[550, 550]表示逻辑“1”是0.55ms高电平加0.55ms低电平zero[550, 1700]表示逻辑“0”是0.55ms高电平加1.7ms低电平。trail0是结束脉冲。除非你需要兼容其他红外设备否则不要修改这些参数这是NEC标准。[TREASURE_ID]*4这是代码中最容易让人困惑的地方。adafruit_irremote库目前主要支持NEC协议该协议规定每次传输是4个字节32位的数据。我们只想发送一个简单的ID如1但协议要求4字节。[1]*4会生成列表[1, 1, 1, 1]意味着我们将同一个ID重复四次发送出去以满足协议格式。这是一种简便的适配方法。4.2 猎人端代码解读字典、解码与状态管理猎人端代码hunter.py展示了更高级的Python编程技巧尤其是字典的使用。# 配置宝藏信息 # ID PIXEL COLOR TREASURE_INFO { (1,)*4 : ( 0 , 0xFF0000) , (2,)*4 : ( 1 , 0x00FF00) , (3,)*4 : ( 2 , 0x0000FF) } treasures_found dict.fromkeys(TREASURE_INFO.keys(), False)数据结构设计TREASURE_INFO是一个字典。键Key是宝藏的ID但注意它被表示为一个元组(1,)*4这等同于(1, 1, 1, 1)。为什么因为红外解码器decoder.decode_bits()返回的也是一个元组例如(1, 1, 1, 1)。直接用这个元组作为键可以方便地进行精确匹配。值Value是一个元组(像素索引, 颜色值)指定了当找到该宝藏时要点亮第几号NeoPixel以及用什么颜色。treasures_found是另一个字典它使用TREASURE_INFO.keys()来初始化确保两个字典的键完全一致初始值全部为False。这种设计非常优雅新增宝藏只需在TREASURE_INFO中添加一项treasures_found会自动同步无需修改多处代码。红外解码与错误处理while True: pulses decoder.read_pulses(pulsein) # 监听红外脉冲 try: received_code tuple(decoder.decode_bits(pulses, debugFalse)) except adafruit_irremote.IRNECRepeatException: continue # 忽略NEC重复码遥控器长按时发送的短码 except adafruit_irremote.IRDecodeException as e: continue # 解码失败可能是噪声或非NEC信号decoder.read_pulses(pulsein)会阻塞程序直到检测到一组完整的红外脉冲。这意味着猎人程序的主要精力都花在“等待信号”上。try...except结构至关重要。红外环境嘈杂很容易收到非预期信号如其他遥控器、环境光突变。IRNECRepeatException是NEC协议中“重复码”异常表示用户长按了遥控器按键在我们的游戏里可以安全忽略。IRDecodeException是通用解码失败可能是信号太弱、变形或者根本不是红外信号。直接continue跳过本次循环避免程序因异常而崩溃。状态更新与胜利检测if received_code in TREASURE_INFO.keys(): treasures_found[received_code] True p, c TREASURE_INFO[received_code] pixels[p] c if False not in treasures_found.values(): # 胜利动画...第一段用in操作符检查接收到的元组是否在宝藏字典的键中。如果匹配则更新对应宝藏的找到状态为True并解包(像素索引, 颜色)来点亮对应的NeoPixel。这里的代码比MakeCode的层层if-else更简洁、更易扩展。第二段if False not in treasures_found.values():这行代码是Pythonic的胜利条件检查。它检查treasures_found这个字典的所有值中是否不再存在False。只要有一个宝藏没找到条件就不成立。一旦全部找到就进入最终的无限循环展示胜利动画。高级技巧动态添加宝藏与内存考量CircuitPython方案扩展性极佳。如果你想增加第4个宝藏只需在TREASURE_INFO字典中添加一行(4,)*4 : (3, 0xFFFF00)并为它准备一块运行着TREASURE_ID 4的宝藏板即可。但要注意CPX的内存有限。pulsein pulseio.PulseIn(board.IR_RX, maxlen120, idle_stateTrue)中的maxlen120指定了存储脉冲的缓冲区大小。如果信号非常复杂通常不会可能需要调整这个值。更大的值会占用更多内存。5. 游戏部署、调试与进阶玩法代码写好只是第一步把游戏成功地跑起来并且玩得有趣才是项目的最终目标。5.1 硬件准备与供电方案你需要为每一块参与游戏的CPX板准备独立的电源。最经济可靠的选择是3节AAA电池盒搭配一个JST PH 2-pin连接线。CPX的工作电压范围是3.5V到6V3节碱性电池约4.5V正合适。切勿使用高于6V的电源会损坏板子。安装建议固定电池盒可以用橡皮筋、双面泡棉胶或尼龙扎带将电池盒固定在CPX背面。避免让电池盒悬空否则在移动和寻找过程中连接线容易受到拉扯导致接触不良或断裂。标识板子用彩色胶带、贴纸或记号笔在板子上明确标注“猎人”、“宝藏1号”、“宝藏2号”等。这在调试和多局游戏时能避免混淆。开关机习惯电池盒通常自带开关。游戏结束后务必关闭开关否则CPX会持续耗电尤其是NeoPixel全亮时电池很快耗尽。5.2 上传程序与批量操作技巧对于MakeCode用USB线连接第一块CPX到电脑。在MakeCode编辑器中完成猎人程序点击“下载”。这会在你的下载文件夹生成一个.uf2文件。此时不要断开USB线。快速双击CPX上的复位按钮。板子上的所有LED会变成红色然后电脑上会出现一个名为“CPLAYBOOT”的可移动磁盘。将刚才下载的.uf2文件通常叫circuitplayground-express.uf2拖入这个磁盘。文件复制完成后磁盘会自动弹出板子复位并运行新程序。重复步骤1-4为其他板子上传对应的宝藏程序。记得在上传前在MakeCode中修改宝藏ID并重新下载新的.uf2文件或者使用之前保存的不同ID的.uf2文件。对于CircuitPython确保你的CPX已经刷入了CircuitPython固件可从Adafruit官网下载。用USB线连接CPX电脑上会出现一个名为“CIRCUITPY”的磁盘。将写好的code.py猎人程序或treasure.py宝藏程序需重命名为code.py直接复制到该磁盘的根目录。CircuitPython会自动运行code.py。对于宝藏板你还需要修改code.py文件中的TREASURE_ID变量值。你可以用文本编辑器直接打开“CIRCUITPY”磁盘里的code.py进行修改并保存非常方便。排查技巧程序上传后没反应这是最常见的问题。首先检查电源电池是否有电开关是否打开对于USB供电确认数据线能传数据有些线只能充电。其次检查程序MakeCode版本确认下载的.uf2文件正确拖入了“CPLAYBOOT”磁盘拖入后磁盘会消失。CircuitPython版本确认文件名为code.py且没有语法错误错误时CIRCUITPY磁盘里可能会生成一个boot_out.txt文件记录错误。最后可以尝试按下复位键单击强制重启程序。5.3 游戏设置与难度调节基础玩法很简单藏好所有宝藏板打开电源猎人开始搜寻。但你可以通过以下参数大幅改变游戏体验广播间隔TRANSMIT_DELAY这是调节难度的最主要参数。15秒是中等难度。对于小孩子或小范围空间可以设为5-8秒对于大房子或户外可以设为20-30秒甚至更长增加搜寻的挑战性和偶然性。宝藏数量默认3个。你可以轻松增加到5个或更多受限于CPX的10个NeoPixel最多10个。增加数量会延长游戏时间并考验猎人的记忆力需要记住哪些颜色的灯亮了代表哪些宝藏还没找到。藏匿策略红外线不能穿透不透明物体但可以反射和衍射。不要把宝藏完全密封在金属盒或厚纸箱里那样信号完全出不来。可以藏在半开的抽屉里、窗帘后面、沙发垫缝隙或者放在一个开口朝向房间中央的杯子里。思考信号的传播路径是游戏设计的一部分。猎人反馈除了点亮NeoPixel你还可以添加声音反馈。在CircuitPython猎人代码中找到宝藏后可以用audioio播放一个简短的音效。在MakeCode中可以使用播放音调积木。声音提示在视线不佳时尤其有用。5.4 常见问题与故障排除实录在实际组织活动和教学过程中我遇到了不少典型问题这里汇总成表方便你快速排查问题现象可能原因解决方案猎人完全没反应灯不亮1. 电源未打开或电池耗尽。2. 程序未成功上传/运行。3. 红外接收器RX被遮挡或损坏。1. 检查电池盒开关更换电池。2. 重新上传程序确认上传流程正确。3. 确保RX前方无遮挡尝试用其他红外遥控器如电视遥控对准RX测试看串口是否有输出CircuitPython或事件是否触发MakeCode。猎人偶尔能发现但不稳定1. 环境光干扰太强如阳光直射。2. 通信距离过远或角度太偏。3. 宝藏发射瞬间猎人刚好在“忙”。1. 移至室内光线均匀处测试。2. 有效通信距离通常在几米内且要对准方向。缩短距离正对测试。3. 这是正常的因为红外接收是“监听”而非“轮询”错过就是错过了。确保猎人静止在可能的位置守候。猎人找到了宝藏但NeoPixel显示错误1. 宝藏ID设置错误或重复。2. 猎人代码中ID与像素/颜色的映射错误。3. 多个宝藏ID相同导致冲突。1. 逐一检查每块宝藏板的ID是否唯一且正确。2. 核对猎人代码字典或变量中的映射关系。3. 确保每块宝藏板使用不同的ID。CircuitPython版本报内存错误或死机1. 脉冲缓冲区maxlen设置过大。2. 代码中存在内存泄漏如无限增长的列表。3. 库版本不兼容。1. 尝试减小maxlen值例如从120改为100。2. 检查代码确保没有在循环中不断创建新对象。3. 更新CircuitPython固件和adafruit_irremote库到最新版本。胜利动画不触发1. 胜利条件判断逻辑有误。2. 某个宝藏的found状态未被正确设置为True。3. CircuitPython字典键匹配失败因为接收到的元组格式不对。1. 检查if判断条件是否覆盖了所有宝藏状态。2. 添加调试输出如打印found_id确认每次发现都正确执行了状态更新。3. 打印received_code确认其格式是否为(1, 1, 1, 1)这样的4元素元组。这个项目最吸引人的地方在于它从一个简单的通信demo演变成了一个充满可能性的游戏框架。理解了红外收发、状态管理和事件驱动的核心后你可以发挥创意进行魔改比如让宝藏的广播间隔随机变化让猎人在接近宝藏时通过蜂鸣器发出越来越急促的声音作为“热度提示”或者引入多个猎人进行竞赛模式。硬件和代码只是骨架真正的乐趣在于你赋予它的游戏规则和互动想象。