基于Circuit Playground Express的红外通信实践:从解码到发射
1. 项目概述红外遥控器这东西我们几乎天天用从电视、空调到风扇它无处不在。但你是否想过这个不起眼的小玩意儿背后其实藏着一套相当精巧的无线通信系统更酷的是我们完全可以自己动手用一块小小的开发板来“窃听”甚至“伪造”这些红外指令实现各种自动化控制。今天我就以 Adafruit 的 Circuit Playground Express后面简称 CPX这块板子为例带你从零开始深入红外通信的腹地用 C 玩转红外信号的收发与解码。CPX 是一块非常适合教育和原型的开发板它集成了加速度计、光线传感器、麦克风、电容触摸当然还有我们今天的主角——一个红外发射 LED 和一个红外接收器。这意味着它天生就具备了“看”和“说”红外光的能力。我们将借助 Adafruit 官方提供的Adafruit_CircuitPlayground库这个库内部封装了一个精简版的IRLib2库它能帮我们处理红外通信中最繁琐的部分协议编解码。你不用再去纠结那些微秒级的脉冲宽度库函数会帮你把接收到的光脉冲翻译成一个个有意义的数字代码也能把数字代码变回标准的光脉冲发出去。整个实践过程我们会从最基础的信号接收和解码开始逐步深入到用遥控器控制板载的 NeoPixel 灯环再到主动发射信号控制其他设备最后实现一个简单的红外信号“学习与重放”功能。无论你是想做一个自定义的万能遥控器还是想让你的创客项目响应家里的电视遥控器这篇文章都能给你一套完整、可落地的方案。我会把每一步的原理、代码细节、可能遇到的坑以及我的调试心得都摊开来讲让你不仅能照着做出来更能明白为什么要这么做。2. 红外通信核心原理与硬件基础在动手写代码之前我们得先搞清楚红外通信到底是怎么一回事。这能帮你理解后面代码里每一个函数调用背后的意义当出现问题时你也能更快地定位。2.1 红外信号的物理本质红外通信说白了就是用一种人眼看不见的光——红外光——来传递信息。发射端比如遥控器的核心是一个红外发光二极管IR LED它就像个特殊的“手电筒”只不过发出的光波长在 850nm 到 950nm 左右超出了人眼的可视范围。接收端则是一个红外接收头内部集成了光电二极管、前置放大器和解调电路。这里有个关键点调制。红外 LED 并不是简单地亮灭来代表 0 和 1。如果那样做环境光比如太阳光、白炽灯中大量的红外成分会瞬间把信号淹没造成误触发。为了解决这个问题工程师们想出了一个巧妙的办法载波调制。以最常见的 38kHz 载波为例。当要发送一个逻辑“1”或者说“Mark”状态时红外 LED 会以 38kHz 的频率高速闪烁亮-灭-亮-灭而不是常亮。这个频率是接收头预先设定好并能够识别的。接收头内部的电路就像一个非常挑剔的“门卫”它只对以 38kHz 频率闪烁的红外光敏感并将其解调为一个持续的低电平或高电平信号。环境光虽然也含有红外线但其强度变化频率远低于 38kHz因此被接收头内部的滤波电路无情地过滤掉了。这就好比在嘈杂的菜市场里你只听得懂那个用特定哨音频率叫你名字的人。2.2 协议红外世界的“语言”解决了抗干扰问题接下来就是编码信息。不同的设备制造商索尼、松下、NEC 等定义了不同的“语言”来组织这些经过调制的脉冲这就是红外协议。协议规定了信号的完整结构主要包括引导码一个长长的 Mark 和 Space 组合用来告诉接收器“注意我要开始发送数据了”。它有两个作用一是让接收器的自动增益控制电路调整到合适的灵敏度二是帮助接收器识别这是哪个协议家族发来的信号。数据码真正要传递的命令信息比如“开机”、“音量加”。数据由一系列的 Mark 和 Space 组成不同的协议用不同的方式来区分 0 和 1。脉冲宽度编码Mark 的时长固定通过 Space 的时长长短来区分 0 和 1例如 NEC 协议。脉冲位置编码通过脉冲Mark出现的位置来编码例如 Philips RC-5 协议。脉冲距离编码通过两个脉冲之间的间隔来编码。结束码/重复码标示一帧数据发送完毕。很多遥控器在按键按住时不会重复发送完整的数据帧而是发送一个特殊的、很短的“重复码”以节省电力并提高响应速度。IRLib2库的强大之处在于它内置了对十几种主流协议NEC, Sony, RC5, RC6, Samsung 等的解码和编码支持。对于 CPXAdafruit_CircuitPlayground库已经集成了这个功能我们只需要调用简单的接口就能拿到解码后的协议类型和一个 32 位的整数值这个值就对应遥控器上那个按键的唯一“身份证”。2.3 Circuit Playground Express 的红外硬件CPX 板载的红外硬件是开箱即用的红外接收器位于板子中央靠近麦克风。它已经连接到了微控制器的一个支持外部中断的引脚上这使得库能够精确地测量每个脉冲的时长。红外发射 LED位于板子边缘靠近电源开关。它是一个高输出型的 LED需要由晶体管驱动库函数已经处理好了驱动逻辑。注意红外光的传播是定向的且容易被障碍物阻挡。在测试时尽量让发射端和接收端“面对面”距离在几米内效果最佳。深色、特别是黑色的表面会吸收红外光而浅色表面可能会反射但反射信号通常很弱且不稳定。3. 开发环境搭建与库安装工欲善其事必先利其器。要让 CPX 跑起我们的红外程序需要先搭建好 Arduino IDE 环境并安装必要的支持包和库。3.1 安装 Arduino IDE 与 CPX 支持包首先确保你安装了最新版的 Arduino IDE1.8.x 或 2.x 均可。打开 IDE 后我们需要添加 Adafruit 的板卡支持。打开文件 - 首选项。在“附加开发板管理器网址”一栏中填入以下 URL如果已有其他用逗号隔开https://adafruit.github.io/arduino-board-index/package_adafruit_index.json点击“好”保存。打开工具 - 开发板 - 开发板管理器。在搜索框中输入“Adafruit SAMD”找到Adafruit SAMD Boards并安装。这个包包含了 CPX 所需的芯片支持。安装完成后在工具 - 开发板菜单下你就能找到Adafruit Circuit Playground Express选中它。3.2 安装 Adafruit_CircuitPlayground 库CPX 的红外功能集成在Adafruit_CircuitPlayground这个综合库中。安装它有两种方法推荐使用库管理器最简单。打开工具 - 管理库...。在搜索框中输入“Adafruit CircuitPlayground”。关键点务必在“Circuit”和“Playground”之间加一个空格否则可能搜不到。找到Adafruit CircuitPlayground库点击安装。安装时会自动安装其依赖的其他库如 Adafruit NeoPixel 等一并确认即可。3.3 连接板卡与端口选择用 Micro-USB 数据线将 CPX 连接到电脑。第一次连接时电脑可能需要一点时间来安装驱动程序Windows 用户可能会弹出驱动安装提示通常会自动完成。在 Arduino IDE 中工具 - 开发板确认已选择Adafruit Circuit Playground Express。工具 - 端口选择新出现的端口在 Windows 上通常是COMx如COM3在 Mac 上通常是/dev/cu.usbmodemxxxx。端口名里通常会带有“Circuit Playground”字样这是最可靠的识别标志。实操心得如果你连接后找不到正确的端口或者上传时总是失败可以尝试以下步骤拔掉 USB 线按一下 CPX 上的复位按钮RESET然后迅速双击它。这时板载的 NeoPixel 灯环会变成绿色并且电脑会识别出一个名为“CPLAYBOOT”的U盘。这表示板子进入了引导加载模式。在 Arduino IDE 的端口列表中你可能会看到一个新的串口出现选择它进行上传。上传完成后CPX 会自动复位并运行新程序。这个“双击复位”大法是解决 SAMD21 系列板卡上传问题的万能钥匙务必记住。4. 红外信号接收与解码实战环境准备好了让我们从最简单的开始让 CPX 成为一个红外信号的“监听者”。4.1 Infrared_Read 程序详解打开 Arduino IDE依次点击文件 - 示例 - Adafruit CircuitPlayground - Infrared_Read。这个示例程序展示了如何接收并解码红外信号。我们先完整地看一遍代码并逐段解析#include Adafruit_CircuitPlayground.h #if !defined(ADAFRUIT_CIRCUITPLAYGROUND_M0) #error Infrared support is only for the Circuit Playground Express, it doesnt work with the Classic version #endif void setup() { CircuitPlayground.begin(); Serial.begin(9600); while (!Serial); // Wait until serial console is opened CircuitPlayground.irReceiver.enableIRIn(); // Start the receiver Serial.println(Ready to receive IR signals); } void loop() { //Continue looping until you get a complete signal received if (CircuitPlayground.irReceiver.getResults()) { CircuitPlayground.irDecoder.decode(); //Decode it CircuitPlayground.irDecoder.dumpResults(false); //Now print results. Use false for less detail CircuitPlayground.irReceiver.enableIRIn(); //Restart receiver } }头文件与版本检查#include Adafruit_CircuitPlayground.h引入了所有必要的功能。#if !defined...#error这行是一个编译时检查确保你编译的目标是 CPXM0内核而不是旧版的 Circuit Playground ClassicATmega32u4因为 Classic 版没有红外硬件。setup()函数CircuitPlayground.begin()初始化板子的所有功能包括红外。Serial.begin(9600)启动串口通信用于在电脑的串口监视器上打印信息。while (!Serial);这行代码会阻塞程序直到你打开 Arduino IDE 的串口监视器。在实际产品中要删掉它但调试时非常有用能确保你不会错过最初的打印信息。CircuitPlayground.irReceiver.enableIRIn()这是关键它启动红外接收器并配置相关的中断开始监听红外信号。loop()函数if (CircuitPlayground.irReceiver.getResults())这个函数检查是否已经接收完一帧完整的红外信号。它内部通过中断记录脉冲时间当检测到一段足够长的空闲时间表示一帧结束后返回true。CircuitPlayground.irDecoder.decode()如果收到了完整信号就调用解码器尝试解码。它会分析脉冲时序匹配已知的协议。CircuitPlayground.irDecoder.dumpResults(false)将解码结果以可读格式打印到串口。参数false表示简洁模式只输出最关键的信息。CircuitPlayground.irReceiver.enableIRIn()解码并处理完后必须重新启用接收器准备接收下一个信号。这是一个容易忘记但至关重要的步骤如果不调用接收器将“聋掉”。4.2 运行与结果分析将代码上传到 CPX然后打开工具 - 串口监视器确保波特率设置为 9600。你会看到“Ready to receive IR signals”的提示。现在拿起任何一个红外遥控器电视、空调、机顶盒或者 Adafruit 的迷你遥控器对准 CPX 板子中央的红外接收器按下一个按钮。串口监视器会立即打印出类似这样的信息Ready to receive IR signals Decoded NEC(1): Value:FD807F Adrs:0 (32 bits)这行信息是宝藏它告诉我们Decoded NEC(1)成功解码协议是 NEC协议编号是 1。Value:FD807F解码得到的 32 位数据值是0xFD807F十六进制。这个值唯一对应你刚才按下的那个按键。Adrs:0地址码为 0。某些协议如 Samsung36会将数据分为地址和命令两部分NEC 协议通常不使用这个字段。(32 bits)该协议的数据长度为 32 位。如果指向一个不被支持的设备或者信号太差你可能会看到Decoded Unknown(0): Value:0 Adrs:0 (0 bits)这表示库无法识别该信号的协议。4.3 深入信号细节Verbose 模式如果你对底层时序感兴趣想看看原始的脉冲宽度数据可以修改dumpResults的参数为trueCircuitPlayground.irDecoder.dumpResults(true);。重新上传后按同一个按键你会得到一份非常详细的报告Ready to receive IR signals Decoded NEC(1): Value:FD807F Adrs:0 (32 bits) Raw samples(68): Gap:52026 Head: m9014 s4560 0:m538 s602 1:m567 s574 2:m565 s577 3:m563 s577 ... Extent67563 Mark min:533 max:569 Space min:573 max:1737Raw samples(68)这一帧信号总共由 68 个 Markm和 Spaces间隔组成。Gap:52026从开始监听到信号到来之间的空闲时间单位是微秒。Head: m9014 s4560引导码一个 9014 微秒的 Mark 和一个 4560 微秒的 Space。这是 NEC 协议的典型特征。0:m538 s602 ...后续是 32 对 Mark/Space代表 32 个数据位。可以看到 Mark 的时长都在 538 微秒左右而 Space 的时长有两种短的约 600 微秒长的约 1700 微秒。在 NEC 协议中短 Space 代表逻辑 0长 Space 代表逻辑 1。Extent整帧信号的总时长。Mark/Space min/max统计了所有 Mark 和 Space 的最小、最大值用于协议分析。这份数据对于逆向工程一个未知协议极其有用。但在绝大多数应用场景下我们只需要关注解码出的协议类型和数值就够了。5. 红外控制 NeoPixel 灯环项目理解了如何接收信号我们就可以做点有趣的了用遥控器来控制 CPX 上那圈炫酷的 NeoPixel LED。Infrared_NeoPixel示例程序就是一个很好的起点。5.1 程序工作机制剖析打开示例文件 - 示例 - Adafruit CircuitPlayground - Infrared_NeoPixel。这个程序逻辑比Infrared_Read复杂一些因为它要持续显示动画同时还要响应红外信号。程序的核心是一个状态机它维护着几个控制动画的变量pattern当前图案编号、direction旋转方向、brightness亮度、speedDelay速度延迟。loop()函数的主要流程如下渲染显示调用Show_Pattern()函数根据当前状态变量的值计算出每一颗 NeoPixel 应该显示的颜色并刷新。更新动画相位增加phase变量实现动画的连续运动。检查红外信号调用CircuitPlayground.irReceiver.getResults()检查是否有新信号。解码与响应如果有信号则尝试解码。如果解码成功且协议是 NEC对应 Adafruit 迷你遥控器就进入一个switch语句根据解码出的数值即按键码来改变状态变量pattern,direction,brightness,speedDelay。重启接收器处理完信号后调用enableIRIn()重启接收。这种设计模式非阻塞式检查 状态机在嵌入式开发中非常经典。它确保了动画的流畅性不会因为等待红外信号而卡住。5.2 适配你自己的遥控器这个示例默认是为 Adafruit 迷你遥控器使用 NEC 协议编写的。如果你用的是其他遥控器比如你家电视的就需要修改代码。步骤如下识别你的遥控器协议和键值运行之前的Infrared_Read程序。用你的遥控器对准 CPX按下你打算使用的按键例如音量、播放、数字1等。在串口监视器中记录下每个按键对应的协议名称或编号和Value十六进制值。例如Decoded SONY(2): Value:4AB Adrs:0 (12 bits)。修改Infrared_NeoPixel程序修改协议检查在代码中找到if (! CircuitPlayground.irDecoder.protocolNum NEC) {这一行。如果你的遥控器是 Sony就需要把NEC改成SONY。NEC,SONY这些是库中定义的常量你可以在Adafruit_CircuitPlayground/utility/IRLibProtocols.h文件中找到完整的列表如#define NEC 1,#define SONY 2。更稳妥的写法是直接使用数字if (CircuitPlayground.irDecoder.protocolNum ! 2) { // 2 代表 SONY。修改按键映射找到switch (CircuitPlayground.irDecoder.value)语句。里面的每个case对应一个功能。例如case ADAF_MINI_VOLUME_UP:对应音量加键。你需要把这些ADAF_MINI_XXX替换成你记录下来的十六进制值。方法一推荐清晰在程序开头用#define定义你自己的常量。#define MY_REMOTE_VOL_UP 0x4AB // 假设这是你 Sony 遥控器音量的键值然后将case ADAF_MINI_VOLUME_UP:改为case MY_REMOTE_VOL_UP:。方法二直接直接写数值case 0x4AB:。注意事项不同协议的数值范围和数据长度可能不同。NEC 是 32 位Sony 可能是 12、15 或 20 位。IRLib2库会统一返回一个uint32_t类型的值对于位数不足 32 位的协议数据存放在低位高位补零。所以直接比较value是没问题的。但要注意有些遥控器在长按时会发送特殊的“重复码”其值与单次按键不同如果你的应用需要处理长按就要额外判断。5.3 功能扩展思路这个示例给了我们一个框架你可以在此基础上大展拳脚更多动画模式在Show_Pattern()函数里添加新的case实现彩虹渐变、呼吸灯、频谱可视化结合麦克风等效果。控制其他传感器不局限于 NeoPixel。你可以用红外信号控制板载扬声器播放不同音调或者根据信号改变电容触摸的灵敏度。创建宏命令定义一个特殊的按键如“*”键按下后依次执行一系列操作比如先切换灯光模式再播放一段声音。6. 红外信号发送与“学习型遥控”实现CPX 不仅能接收还能发射红外信号这就让它从一个“监听者”变成了一个“控制者”。6.1 Infrared_Send 程序解析打开文件 - 示例 - Adafruit CircuitPlayground - Infrared_Send。这个示例非常简单它演示了如何通过按下板载的左右按键来发送特定的红外指令。//Defines for a Samsung TV using NECx protocol #define MY_PROTOCOL NECX #define MY_BITS 32 #define MY_MUTE 0xE0E0F00F #define MY_POWER 0xE0E040BF void loop() { // If the left button is pressed send a mute code. if (CircuitPlayground.leftButton()) { CircuitPlayground.irSend.send(MY_PROTOCOL,MY_MUTE,MY_BITS); while (CircuitPlayground.leftButton()) {}//wait until button released } // If the right button is pressed send a power code. if (CircuitPlayground.rightButton()) { CircuitPlayground.irSend.send(MY_PROTOCOL,MY_POWER,MY_BITS); while (CircuitPlayground.rightButton()) {}//wait until button released } }核心发送函数是CircuitPlayground.irSend.send(protocol, value, bits)。你需要提供三个参数protocol协议类型使用IRLibProtocols.h中定义的常量如NEC,SONY,RC5等。value要发送的指令值就是你用Infrared_Read读出来的那个十六进制数。bits该协议的数据位数对于 NEC 是 32对于 Sony 可能是 12、15 或 20。while (CircuitPlayground.leftButton()) {}这行代码实现了按键释放检测防止在按住按键期间连续不断地发射信号这既耗电也可能导致接收设备处理异常。6.2 Infrared_Record实现信号学习与重放这是最有趣的一个应用——制作一个简易的“学习型遥控器”。文件 - 示例 - Adafruit CircuitPlayground - Infrared_Record展示了如何实现。程序逻辑清晰分为两部分学习阶段在loop()中持续检查红外接收器。一旦收到并成功解码一个信号就将解码得到的protocolNum、value和bits保存到三个全局变量中。重放阶段同时在loop()中也检查左按键是否被按下。如果被按下并且之前已经成功学习过一个信号IR_protocol ! 0就调用irSend.send()函数将保存的信号原样发送出去。这个简单的程序揭示了一个万能遥控器的基本原理。你可以扩展它比如增加存储使用 CPX 的 EEPROM非易失性存储来保存多个学习到的信号即使断电也不会丢失。增加触发方式不限于按键可以用电容触摸、拍手声音传感器、特定的手势加速度计来触发发送不同的红外指令。逻辑组合实现一个“宏”按一个键依次发送“电视开机”、“切换到 HDMI1”、“音响开机”三个指令。6.3 发送实战与调试技巧当你写好发送程序后如何验证它是否工作正常最直接的测试将 CPX 的红外发射 LED 对准你的目标设备如电视按下触发按钮看设备是否有反应。注意对准距离在 1-3 米内。使用另一块 CPX 接收验证这是最可靠的调试方法。让另一块运行着Infrared_Read程序的 CPX或任何支持IRLib2的 Arduino作为接收端放置在发射端旁边。当主 CPX 发送信号时在接收端的串口监视器上观察解码结果。这能精确验证你发送的协议、数值是否正确。使用手机摄像头辅助观察大多数手机摄像头的 CMOS 传感器对红外光敏感。在昏暗环境下用手机摄像头对准 CPX 的红外发射 LED当你触发发送时你应该能在手机屏幕上看到发射 LED 发出微弱的白光或紫光这是红外光在传感器上的成像。这只能证明 LED 在闪烁不能证明信号正确但可以用于快速排查硬件连接问题。常见问题排查设备无反应协议或地址不对确保发送的协议和数值与设备原装遥控器完全一致。有些设备如某些品牌的空调有复杂的地址码需要一并发送。发射功率不足CPX 板载的 IR LED 功率有限距离太远或角度偏差太大可能导致信号太弱。尝试靠近并对准。电池电量低USB 供电一般没问题但如果使用电池电量不足会导致发射功率下降。接收端解码为 Unknown载波频率不匹配绝大多数设备使用 38kHz但极少数可能使用 36kHz, 40kHz 等。IRLib2库发送时默认使用 38kHz。目前 CPX 库的发送部分可能不支持修改频率这是一个限制。信号格式错误确保bits参数设置正确。一个 20 位的 Sony 信号如果按 32 位发送接收端是无法解码的。7. 高级应用与自定义协议处理虽然IRLib2库已经支持了十几种主流协议但世界上的红外设备千千万你仍然有可能遇到一个“非主流”的遥控器。这时你就需要了解如何分析和支持自定义协议。7.1 使用 Infrared_Testpattern 进行协议测试Infrared_Testpattern这个示例程序是一个强大的调试和教学工具。它允许你手动选择或遍历所有IRLib2支持的协议并发送一个特定的测试码。运行这个程序并打开串口监视器。它会提示你输入 1 到 12 的数字来选择协议或者输入 -1 来遍历所有协议。同时用另一块运行Infrared_Read的板子接收。通过对比发送和接收到的协议编号、数值你可以验证你的发射/接收链路是否正常工作。直观感受不同协议的数据格式差异。例如你会看到 Sony 协议有 12/15/20 位变种RC6 协议有特殊的引导头等。7.2 分析未知协议如果你遇到了一个库不支持的遥控器Infrared_Read的详细输出模式dumpResults(true)就是你最好的武器。你需要收集多个按键至少包括 0, 1 这样能产生明显 0/1 序列的键的原始时序数据。分析步骤通常包括识别引导码找到最开始的那个超长的 Mark 和 Space记录它们的时长。这是协议的“指纹”。确定逻辑表示法观察后续的 Mark/Space 对。是 Mark 固定、Space 变化脉宽编码还是脉冲位置变化对比按“0”键和“1”键的原始数据找出区别在哪里。计算时钟基准测量最短的 Mark 或 Space 的时长这通常是协议的“单位时间”或“时钟周期”。推断数据结构和位数数一列引导码之后有多少对 Mark/Space这很可能就是数据位的数量。看看是否有明显的分隔符如一个较长的 Space来区分地址码和命令码。这个过程需要耐心和一些经验。IRLib2的手册在其 GitHub 仓库的manuals文件夹中提供了编写自定义编解码器的详细指南。对于 CPX 项目由于使用的是集成库添加自定义协议需要修改底层库文件难度较高。一个更可行的方案是如果你确定要用某个特殊协议可以尝试在标准的 Arduino 环境中使用完整的IRLib2库它提供了更灵活的自定义接口。7.3 项目集成与优化建议当你把红外功能集成到一个更大的项目中时以下几点经验可能对你有帮助电源管理红外发射 LED 工作时电流较大。如果项目是电池供电要避免频繁或长时间发射。可以在发送代码前后加入低功耗模式的切换。防信号冲突如果你的项目同时需要接收和发送要处理好时序避免自己发射的信号被自己接收误判。通常的做法是在发送期间短暂关闭接收中断发送完成后再开启。代码结构化将红外相关的操作如解码、发送、协议映射封装成独立的函数或类使主程序逻辑更清晰。例如可以创建一个RemoteController类内部维护一个按键值到功能函数的映射表。用户体验提供反馈。当 CPX 接收到一个有效信号时可以让 NeoPixel 闪烁一下绿色当学习到一个新信号时闪烁蓝色发送信号时闪烁白色。这样用户就能直观地知道设备的状态。红外通信是一个看似简单但内涵丰富的领域。通过 Circuit Playground Express 和Adafruit_CircuitPlayground库我们获得了一个绝佳的实验平台能够以很低的门槛探索从信号解码、设备控制到简单协议分析的完整流程。从“读懂”遥控器开始到创造出能控制周围设备的智能小工具这个过程充满了动手的乐趣和解决问题的成就感。希望这篇详细的实践指南能成为你红外探索之路上的得力助手当你看到自己编写的代码成功让一排灯光随着遥控器起舞或者让旧电器焕发新的智能生命时那种感觉正是创客精神的魅力所在。