1. 项目概述与红外通信基础
红外通信这玩意儿,听起来挺“复古”,毕竟现在满世界都是Wi-Fi和蓝牙。但如果你玩过嵌入式开发或者物联网硬件,就会知道,在特定的场景下,红外(IR)依然是简单、可靠且成本极低的无线通信方案。它不像射频通信那样需要复杂的配对和协议栈,本质上就是“看得见就通,看不见就断”的光通信,特别适合做简单的遥控、触发和短指令传输。
这次我们用的核心硬件是Adafruit的Circuit Playground Express(后面简称CPX)。这块板子很有意思,它定位是教育板和快速原型开发板,把你能想到的常用传感器、LED、按钮都集成上去了,其中就包含了红外发射管(TX)和接收头(RX)。这意味着你不需要任何额外的杜邦线和模块,拿两块板子就能直接开始玩红外对话。我们的目标很明确:用CircuitPython写代码,让一块CPX的按钮按下时,通过红外光发送一组编码,另一块CPX收到后,能解码并执行相应的动作,比如点亮LED或者播放一个声音。
为什么选NEC编码?这是消费电子领域,特别是电视、空调遥控器里最普及的编码协议之一。它定义了一套清晰的脉冲宽度调制规则,用来表示逻辑“0”和“1”。协议本身不算复杂,但足够稳定,有很多现成的库支持。在CPX上,Adafruit提供的adafruit_irremote库已经帮我们封装好了NEC编码的解析和生成,让我们可以跳过枯燥的底层时序分析,直接关注应用逻辑。
2. 硬件准备与开发环境搭建
2.1 核心硬件清单
要完整复现本文的所有实验,你需要准备以下硬件:
- Circuit Playground Express开发板 (2块):这是绝对的主角。确保你拿到的是Express版本,而不是经典的Circuit Playground,因为只有Express版本才板载了红外收发器。
- USB数据线 (2根):用于给CPX供电和上传代码。Type-C接口的线现在更通用。
- 电脑一台:Windows, macOS, Linux均可,用于编写和上传CircuitPython代码。
- 可选:Adafruit NEC红外遥控器:型号通常是“Mini Remote Control”。这不是必须的,但有一个实体遥控器来做初步测试和信号抓取,会非常直观,有助于你理解红外通信的“语言”。
两块CPX是必须的,因为我们要实现双向通信演示。如果你只有一块,也可以先用遥控器测试接收功能,但就无法体验完整的“板对板”通信了。
2.2 CircuitPython固件与驱动安装
CPX出厂可能运行其他固件,要使用CircuitPython,第一步是“刷机”。
- 下载固件:访问CircuitPython官网,找到Circuit Playground Express的页面,下载最新的
.uf2格式固件文件。 - 进入引导加载模式:用USB线连接CPX到电脑。先按住板子上的“复位”按钮(Reset),然后快速按一下旁边的“用户”按钮(User),之后松开复位按钮。此时,电脑上会弹出一个名为
CPLAYBOOT的U盘。 - 刷入固件:将下载好的
.uf2文件直接拖入CPLAYBOOTU盘。拖入后,U盘会自动弹出,板子将重启,并出现一个名为CIRCUITPY的新U盘。这表示CircuitPython固件已成功刷入。
CIRCUITPY这个U盘就是你的代码存储和运行空间。之后你所有的Python代码文件都放在这里。
2.3 必要库文件的获取与安装
CircuitPython的强大在于其丰富的库生态。我们需要用到两个核心库:
adafruit_irremote:负责红外信号的编码与解码。adafruit_circuitplayground:提供访问CPX板载资源(按钮、LED、蜂鸣器等)的高级接口。
获取库文件最简单的方法是访问Adafruit的CircuitPython库包发布页面,下载最新的“Library Bundle”。这个压缩包里包含了所有官方和维护的库。解压后,找到我们需要的两个库文件夹(例如adafruit_irremote.mpy和adafruit_circuitplayground),将它们整体复制到CIRCUITPYU盘根目录下的lib文件夹中。如果还没有lib文件夹,就新建一个。
注意:务必确保库的版本与你的CircuitPython固件版本大致匹配。通常下载最新版的库包与最新固件配合使用,可以避免兼容性问题。
3. 红外信号接收:解码遥控器的指令
在让两块板子对话之前,我们先用一个标准的红外遥控器来测试接收功能。这相当于先学会“听”,再学“说”。这个步骤能验证你的红外接收硬件和基础解码代码是否工作正常。
3.1 代码解析:监听与解码
将以下代码保存为CIRCUITPY盘根目录下的code.py,板子会自动运行。
# SPDX-FileCopyrightText: 2018 Kattni Rembor for Adafruit Industries # SPDX-License-Identifier: MIT import pulseio import board import adafruit_irremote # 创建一个脉冲输入对象,监听IR_RX引脚上的信号 # maxlen=120 设定脉冲缓冲区大小,idle_state=True 表示空闲时引脚为高电平 pulsein = pulseio.PulseIn(board.IR_RX, maxlen=120, idle_state=True) # 创建一个通用解码器对象 decoder = adafruit_irremote.GenericDecode() while True: # 读取一组脉冲 pulses = decoder.read_pulses(pulsein) try: # 尝试将脉冲解码为数字码 received_code = decoder.decode_bits(pulses) except adafruit_irremote.IRNECRepeatException: # 捕获到NEC重复码(短码),通常为长按按钮时发出,此处忽略 # print("NEC repeat!") continue except adafruit_irremote.IRDecodeException as e: # 解码失败,可能是信号失真或非NEC编码 # print("Failed to decode: ", e.args) continue print("NEC红外编码接收: ", received_code) # 根据接收到的编码执行不同动作(以Adafruit迷你遥控器为例) if received_code == [255, 2, 255, 0]: print("接收到 NEC 音量-") if received_code == [255, 2, 127, 128]: print("接收到 NEC 播放/暂停") if received_code == [255, 2, 191, 64]: print("接收到 NEC 音量+")代码关键点解读:
PulseIn对象:这是底层硬件抽象。红外接收头输出的是一连串高低电平变化的脉冲,PulseIn负责精确记录每个高电平和低电平的持续时间(以微秒为单位)。GenericDecode:这个解码器默认适配NEC编码的脉冲时序。它知道如何将一串时长不同的脉冲,解析成对应的逻辑位(0和1),并最终组合成我们看到的4字节数组(如[255, 2, 191, 64])。- 异常处理:这是工业级代码的体现。
IRNECRepeatException处理的是遥控器长按时发出的“重复码”,它是一个简短的信号,告诉设备“继续执行上一个指令”。IRDecodeException则处理所有其他解码错误,比如信号受到日光灯干扰、距离太远信号弱、或者根本不是NEC协议。
3.2 实操测试与信号抓取
- 将代码上传到CPX后,打开串行监视器(推荐使用Mu编辑器,它内置了串行监视器,或者使用Thonny、VS Code with CircuitPython插件)。
- 将遥控器对准CPX板子中心附近的红外接收头(标有RX)。
- 按下遥控器上的按键。你会在串行监视器中看到类似
NEC红外编码接收: [255, 2, 191, 64]的输出,以及对应的按钮提示。 - 重要技巧:如果你使用的是其他品牌遥控器,可能无法直接显示按钮名称。但解码出的数组(如
[12, 34, 56, 78])就是该按键的“身份证”。记录下每个按键对应的数组,你就可以在代码的if语句中替换它们,从而自定义你的遥控器映射。
避坑指南:测试时,确保没有强烈的直射光源(特别是白炽灯、太阳光)照射在红外接收头上,这些光源包含丰富的红外光谱,会产生噪声脉冲,导致解码失败。在室内日光灯环境下通常问题不大。
4. 红外信号发射:让CPX成为发送端
学会了“听”,现在来学“说”。我们将编写另一段代码,让CPX通过板载按钮来触发红外信号的发射,模拟一个遥控器。
4.1 代码解析:编码与发射
将以下代码保存到第二块CPX的code.py中。
# SPDX-FileCopyrightText: 2018 Kattni Rembor for Adafruit Industries # SPDX-License-Identifier: MIT import time from adafruit_circuitplayground.express import cpx import adafruit_irremote import pulseio import board # 创建一个脉冲输出对象,用于从IR_TX引脚发射38KHz载波的红外信号 # 38KHz是红外遥控的通用载波频率,duty_cycle=2**15设置50%占空比 pulseout = pulseio.PulseOut(board.IR_TX, frequency=38000, duty_cycle=2 ** 15) # 创建一个通用编码器,配置为NEC协议格式 encoder = adafruit_irremote.GenericTransmit( header=[9000, 4500], # NEC起始信号:9ms高脉冲,4.5ms低脉冲 one=[560, 1700], # 逻辑“1”:560us高,1700us低 zero=[560, 560], # 逻辑“0”:560us高,560us低 trail=0 # 结束后的低电平时间 ) while True: if cpx.button_a: print("按钮 A 被按下!\n") cpx.red_led = True # 点亮红色LED作为发送指示 # 发射代表“音量-”的NEC编码 encoder.transmit(pulseout, [255, 2, 255, 0]) cpx.red_led = False time.sleep(0.2) # 短暂延时,防止按键抖动和给接收端处理时间 if cpx.button_b: print("按钮 B 被按下!\n") cpx.red_led = True # 发射代表“音量+”的NEC编码 encoder.transmit(pulseout, [255, 2, 191, 64]) cpx.red_led = False time.sleep(0.2)核心原理剖析:
PulseOut与载波:红外发射二极管不能直接发送数字信号。它需要被一个38KHz的方波(载波)“调制”。PulseOut对象在后台以38KHz的频率快速开关红外管。当我们调用transmit时,编码器控制的是这个38KHz载波的“包络”,即让载波按照NEC协议规定的时序(如9ms有载波,4.5ms无载波)来发射。人眼看不见38KHz的闪烁,但红外接收头专门检测这个频率,从而过滤掉环境中的其他红外噪声。GenericTransmit参数:这行代码是NEC协议的“密码本”。header:起始信号,像一个大声的“注意!我要开始说话了!”,帮助接收端同步。one和zero:定义了逻辑1和逻辑0的脉冲宽度组合。NEC协议使用“脉冲位置调制”,通过低电平的持续时间来区分1和0(1700us vs 560us)。trail:单个信号结束后的低电平时间。
- 四字节数据:
[255, 2, 191, 64]这样的四字节数组是NEC协议的标准数据格式。它通常包含地址码、命令码及其反码,用于错误校验。在实验中,我们直接使用了从遥控器抓取到的完整四字节码。
4.2 双向通信测试
现在你有了两块CPX:一块运行3.1节的接收代码(接收端),一块运行上面的发射代码(发射端)。
- 用两根USB线将两块板子分别连接到电脑,打开两个串行监视器窗口。
- 将两块板子的红外发射头和接收头大致对准(不需要非常精确,有一定角度容差)。
- 按下发射端(第二块板子)的A按钮。你会在发射端的串口看到
按钮 A 被按下!,同时在接收端的串口看到NEC红外编码接收: [255, 2, 255, 0]和接收到 NEC 音量-。 - 这个实验验证了从编码、发射、空中传输到接收、解码的完整链路是通的。红色LED的闪烁提供了直观的发送视觉反馈。
5. 红外信号应用:触发多彩互动反馈
通信链路打通了,接下来就是赋予它实际意义。我们将升级接收端的代码,让它在收到特定红外指令时,不再只是在串口打印文字,而是驱动板载的NeoPixel LED和蜂鸣器做出反应。
5.1 代码解析:从信号到动作
替换第一块CPX(接收端)上的code.py为以下代码:
# SPDX-FileCopyrightText: 2018 Kattni Rembor for Adafruit Industries # SPDX-License-Identifier: MIT import pulseio import board import adafruit_irremote from adafruit_circuitplayground.express import cpx pulsein = pulseio.PulseIn(board.IR_RX, maxlen=120, idle_state=True) decoder = adafruit_irremote.GenericDecode() while True: pulses = decoder.read_pulses(pulsein) try: received_code = decoder.decode_bits(pulses) except adafruit_irremote.IRNECRepeatException: continue except adafruit_irremote.IRDecodeException as e: continue print("NEC红外编码接收: ", received_code) if received_code == [255, 2, 255, 0]: # 对应发射端按钮A print("接收到按钮A信号") # 将所有10个NeoPixel LED设置为紫色 (R, G, B) cpx.pixels.fill((130, 0, 100)) cpx.pixels.brightness = 0.1 # 可选:调低亮度保护眼睛 if received_code == [255, 2, 191, 64]: # 对应发射端按钮B print("接收到按钮B信号") # 关闭所有LED cpx.pixels.fill((0, 0, 0)) # 播放一个262Hz(中央C)的音调,持续1秒 cpx.play_tone(262, 1)应用层设计要点:
- 事件驱动:整个程序的核心是一个等待红外信号的循环。一旦解码成功,就进入
if判断分支,执行对应的动作。这是一种典型的事件驱动编程模型,在物联网和交互设备中非常常见。 - 资源调用:
cpx.pixels.fill()和cpx.play_tone()是adafruit_circuitplayground库提供的上层API,它隐藏了控制WS2812 LED和压电蜂鸣器的底层PWM细节,让我们用一行代码就能实现复杂功能。 - 扩展性:这两个
if分支就是你的“动作触发器”。你可以轻松地扩展它:cpx.pixels[i] = (R, G, B)控制单个LED。cpx.start_tone(freq)和cpx.stop_tone()实现更灵活的声音控制。- 引入
adafruit_motor库来控制舵机转动。 - 甚至可以通过
import storage和audioio来播放存储在本地的WAV文件。
5.2 项目构思与扩展
现在,你已经掌握了一个完整的无线触发与控制链路。可以构思一些有趣的项目:
- 无线门铃/提醒器:将发射端放在门口,接收端放在书房。按下按钮,书房里的CPX亮灯并响铃。
- 简易遥控小车:用CPX控制一个小车底盘(需连接电机驱动模块)。按钮A/B分别控制左转/右转或前进/后退。
- 智能家居触发器:接收端收到信号后,通过串口或Wi-Fi模块(如AirLift)向家庭服务器发送一个HTTP请求,从而打开智能灯或播放音乐。
- 多人游戏控制器:制作多个发射端,用一个接收端来判断谁先按下按钮,实现抢答器功能。
关键在于,红外通信在这里扮演了一个低成本、低延迟、高可靠性的私有无线触发通道的角色。对于不需要传输大量数据(如图像、音频流),只需要发送简单控制指令的应用,它是绝佳选择。
6. 深度调试与常见问题排查
在实际操作中,你可能会遇到信号收不到、解码错误等问题。下面是一个基于经验的排查清单,可以帮助你快速定位问题。
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 串口无任何输出 | 1. 代码未运行 2. 串口监视器未正确连接 3. 板子供电不足 | 1. 检查CIRCUITPY盘根目录下是否有code.py,确认板载LED是否正常亮起。2. 关闭并重新打开串口监视器,确认选择了正确的串口端口(如 COM3,/dev/ttyACM0)。3. 尝试更换USB线或连接到电脑不同USB口,确保供电稳定。 |
| 按下遥控器/按钮,串口有输出但解码失败 | 1. 环境红外干扰强 2. 距离太远或角度偏差太大 3. 发射与接收未对准 4. 电池电量不足(遥控器) | 1. 移至远离窗户、白炽灯的环境下测试。 2. 将遥控器或发射板靠近接收头(10厘米内)再试。 3. 确保发射管的TX与接收头的RX大致在同一直线上。 4. 更换遥控器电池。 |
| 接收端持续打印乱码或重复解码 | 1. 环境光(如日光灯)产生稳定干扰脉冲 2. 接收端代码中未正确过滤重复码 | 1. 这是最常见的问题。尝试用手或纸板在接收头周围稍作遮挡,看乱码是否停止。如果停止,说明环境光干扰严重,需为接收头加装遮光罩(如一小段热缩管)。 2. 确认代码中 IRNECRepeatException异常已被捕获并continue。 |
| 发射端LED亮,但接收端无反应 | 1. 发射端代码载波频率设置错误 2. 接收头损坏或型号不匹配(非38KHz) 3. 发射的数据编码与接收端预期不匹配 | 1. 确认发射代码中PulseOut的frequency=38000。2. 这是硬件问题。CPX板载的接收头是标准的38KHz一体化接收头,一般不会坏。可用手机摄像头(大部分手机摄像头能感应红外光)对准发射管,按下按钮时观察摄像头屏幕,应能看到发射管发出微弱的紫白色光点。 3. 检查发射 encoder.transmit中的数据数组是否与接收端if判断里的数组完全一致。 |
| 通信距离非常短(<10厘米) | 1. 发射管驱动电流不足 2. 接收头灵敏度下降 | 1. CPX板载的红外发射管驱动能力有限,这是其物理限制。如需增加距离,可以考虑外接一个红外发射模块,并用三极管放大驱动电流。 2. 检查接收头前是否有污渍遮挡。 |
一个高级调试技巧:原始脉冲分析如果问题复杂,可以修改接收端代码,在decoder.read_pulses(pulsein)之后,直接打印pulses这个列表。这个列表里存储的是原始的高电平、低电平持续时间(微秒)。你可以将这个数据复制出来,与标准的NEC协议时序(起始头9ms/4.5ms,逻辑1为560us/1700us,逻辑0为560us/560us)进行对比。这能帮你判断是发射的信号本身就不对,还是在传输过程中受到了干扰。