ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Arduino-IRremote红外遥控编解码实战:从原理到避坑指南

Arduino-IRremote红外遥控编解码实战:从原理到避坑指南

1. 从一个“吃灰”的红外接收头说起

如果你手头有一堆闲置的遥控器——电视的、机顶盒的、老式风扇的、甚至某个不知道什么设备的——然后你盯着它们想:能不能让我的开发板听懂这些按键在说什么?那这个项目就是为你准备的。Arduino-IRremote是一个在嵌入式圈子里流传了很久的红外遥控编解码库,它的核心使命只有一件事:让微控制器能够接收红外信号并解析出按键编码,同时也能够模拟发射红外信号去控制其他设备。说白了,它就是一个红外世界的翻译官,把遥控器发出的那一串肉眼看不见的闪烁,翻译成代码能理解的数字。

我第一次接触这个库是在做一个万能遥控器的小项目时。当时的需求很简单:用一块开发板接收家里旧空调遥控器的信号,然后通过串口把编码打印出来,再根据这些编码去控制空调的开关和温度调节。听起来像是三行代码就能搞定的事情,但实际踩进去才发现,红外编解码这件事远比想象中要复杂得多。不同品牌、不同年代的遥控器,使用的编码协议五花八门,载波频率有 38kHz 的、有 40kHz 的、还有 56kHz 的,编码方式有 NEC、Sony SIRC、RC5、RC6、Samsung、LG 等等,每一种协议的时序、帧结构、校验方式都不一样。Arduino-IRremote这个库的价值就在于,它把这一大堆让人头大的协议细节封装成了几个简单的 API,让你不需要从头去啃数据手册就能快速上手。

这篇文章适合哪些人看?如果你是完全没接触过红外通信的新手,我会从最基础的红外信号原理讲起,告诉你接收头和发射管怎么接线、库怎么装、代码怎么写。如果你已经用过这个库但总是遇到接收不稳定、解码错误、发射距离短之类的问题,我也会把我在实际项目中踩过的坑和解决方案整理出来。整篇文章不会只停留在“把库装上、跑个示例”这个层面,而是会深入到协议解析的细节、参数选择的依据、以及不同场景下的优化策略。

2. 红外通信的底层逻辑与这个库的定位

2.1 红外遥控到底在“说”什么

要理解Arduino-IRremote在做什么,首先得搞清楚红外遥控器发出的信号本质上是什么。遥控器前端的红外 LED 并不是持续发光的,它发出的是一连串经过调制的脉冲。这个调制过程包含两个层面:载波和编码。

载波是一个固定频率的方波信号,常见的是 38kHz。为什么要用载波?因为环境中有大量的红外噪声——阳光、白炽灯、人体热辐射都会产生红外线。如果直接用基带信号发射,接收端根本分不清哪些是遥控器发的、哪些是环境噪声。用一个特定频率的载波去调制信号,接收头内部就只需要对这个频率敏感,相当于给信号加了一个“身份标签”。38kHz 这个频率的选择也不是随便定的,它在成本、功耗和抗干扰之间取了一个平衡点,绝大多数消费电子遥控器都采用这个频率。

编码则是在载波的基础上,用脉冲的长短组合来表示二进制的 0 和 1。以最常见的 NEC 协议为例,它用一个 560 微秒的载波脉冲加上 560 微秒的空闲表示一个逻辑“0”,用一个 560 微秒的载波脉冲加上 1690 微秒的空闲表示逻辑“1”。一帧完整的 NEC 信号包含一个 9 毫秒的引导脉冲、8 位地址码、8 位地址反码、8 位命令码、8 位命令反码,最后以一个 560 微秒的结束脉冲收尾。地址反码和命令反码的作用是校验,接收端把地址码和地址反码做异或,如果结果不是全 1 就说明接收出错了。

Arduino-IRremote做的事情就是:通过一个红外接收头把光信号转换成电信号,然后用定时器精确测量每一个脉冲的持续时间,再根据预设的协议规则去匹配这些时间序列,最终还原出地址码和命令码。发射过程则是反过来,根据你要发送的地址和命令,按照协议规则生成对应的脉冲序列,驱动红外 LED 发射出去。

2.2 为什么选这个库而不是自己写

有人可能会想,红外解码不就是测脉冲宽度吗,我自己用pulseIn()函数也能做。确实,如果你只需要解码一种固定协议、对稳定性要求不高,自己写几十行代码也能跑起来。但一旦涉及到多协议兼容、中断处理、非阻塞接收、发射功能,自己从头造轮子的成本就会急剧上升。

Arduino-IRremote的优势体现在几个方面。第一,它支持的协议非常全,NEC、Sony、RC5、RC6、Samsung、LG、JVC、Panasonic、Denon、Sharp 等等主流协议都有现成的解码器,你不需要为每个品牌单独写解析逻辑。第二,它利用了硬件定时器中断来捕获脉冲边沿,而不是用阻塞式的pulseIn(),这意味着你的主程序可以在等待红外信号的同时处理其他任务。第三,它提供了统一的接口来获取解码结果,无论底层是什么协议,你拿到的都是地址、命令、协议类型这几个字段,上层逻辑不需要关心协议差异。第四,它同时支持接收和发射,接收到的编码可以直接存下来用于后续发射,做学习型遥控器非常方便。

当然,这个库也不是没有代价的。它占用的 Flash 和 RAM 资源在资源紧张的开发板上需要留意,尤其是当你开启了多个协议支持的时候。另外,它的定时器中断和某些其他库(比如舵机控制库)可能会产生冲突,因为两者都想独占定时器资源。这些问题在后面讲实操的时候会详细展开。

2.3 接收头的选型与硬件连接要点

红外接收头常见的有三种封装:三脚直插式(比如 TSOP1838)、贴片式、以及带金属屏蔽罩的抗干扰型。选型的时候主要看三个参数:载波频率、接收角度、输出极性。

载波频率必须和遥控器匹配。市面上绝大多数遥控器是 38kHz,但有些老式 Sony 设备用 40kHz,某些高速红外通信用 56kHz。如果你用 38kHz 的接收头去收 40kHz 的信号,距离近的时候可能还能解出来,但距离一远就完全没反应。所以如果你要做万能接收,最好备几种不同频率的接收头。接收角度方面,直插式的通常有 90 度左右的接收锥角,贴片式的角度更窄,安装位置需要对准遥控器方向。输出极性方面,绝大多数接收头是“低电平有效”——收到 38kHz 载波时输出低电平,没有载波时输出高电平。Arduino-IRremote默认就是按这个极性来设计的。

接线方面,三脚接收头一般是这样:面对接收头的受光面,从左到右依次是 OUT、GND、VCC。但这个顺序并不是绝对的,不同厂家可能不一样,接之前一定要查数据手册。我见过有人把 VCC 和 GND 接反导致接收头瞬间烧掉的案例,虽然接收头很便宜,但烧了还得等快递,耽误事。建议在 VCC 和 GND 之间并一个 100nF 的陶瓷电容,再串一个 100 欧姆的电阻到 VCC,这样可以滤掉电源上的高频噪声,对接收稳定性有明显提升。

发射部分就更简单了,一个红外 LED 加上一个限流电阻和一个驱动三极管(或者直接用开发板的 IO 口驱动,但电流不能超过 IO 口的最大输出能力)。红外 LED 和普通 LED 的区别在于发光波长,红外 LED 通常是 940nm 左右,肉眼看不到光,但用手机摄像头对着看能看到微弱的紫色光点。限流电阻的计算要看 LED 的正向电流和正向压降,一般红外 LED 的正向压降在 1.2V 到 1.5V 之间,正向电流取 20mA 到 100mA 不等。如果直接用 IO 口驱动,电流要限制在 20mA 以内,电阻大概在 150 欧姆到 220 欧姆之间。如果要增加发射距离,就需要用三极管扩流,把电流提到 100mA 以上,但要注意 LED 的功耗限制和占空比,不能长时间连续发射。

3. 库的安装、配置与核心 API 拆解

3.1 安装方式与版本选择

Arduino-IRremote的安装方式取决于你用的开发环境。如果你用的是 Arduino IDE,最省事的方式是通过库管理器搜索 “IRremote” 然后安装。但这里有一个坑:库管理器里可能存在多个名字相似的库,比如IRremote和IRremoteESP8266,后者是专门为 ESP8266/ESP32 系列优化的分支,功能更丰富但 API 略有不同。如果你用的是 ESP 系列芯片,建议直接用IRremoteESP8266,它对 WiFi 和红外共存的场景做了优化。如果你用的是传统的 AVR 系列(比如 Uno、Nano),用原版IRremote就可以了。

版本选择上,2.x 版本和 3.x 版本的 API 差异比较大。2.x 版本的接口是IRrecv irrecv(RECV_PIN);然后irrecv.decode(&results);,3.x 版本改成了IrReceiver.begin(RECV_PIN);和IrReceiver.decode();。网上很多老教程还是 2.x 的写法,如果你照着抄发现编译报错,大概率就是版本不匹配。我建议新项目直接用 3.x 或更新的版本,API 更清晰,功能也更完善。

手动安装的话,从代码托管平台下载 ZIP 包,然后在 Arduino IDE 里通过“项目 -> 加载库 -> 添加 .ZIP 库”导入。手动安装的好处是你可以直接修改库文件里的配置参数,比如调整接收缓冲区大小、开启或关闭某些协议支持,这在资源受限的板子上很有用。

3.2 接收端核心 API 与参数含义

接收端的核心流程分三步:初始化、解码、获取结果。初始化用IrReceiver.begin(pin, enableLEDFeedback),第一个参数是接收头连接的引脚号,第二个参数是是否启用板载 LED 反馈(接收时 LED 闪烁),调试的时候可以开,正式产品里建议关掉省电。

解码用IrReceiver.decode(),这个函数是非阻塞的,它在后台通过定时器中断捕获脉冲,主循环里调用它只是检查是否已经收到完整的一帧。如果返回true,说明有新的解码结果可用。然后通过IrReceiver.decodedIRData这个结构体来获取具体信息。这个结构体里包含的字段比较多,常用的有protocol(协议类型)、address(地址码)、command(命令码)、flags(标志位)、rawDataPtr(原始脉冲数据指针)。

protocol字段是一个枚举值,比如NEC、SONY、RC5等等。address和command的位宽取决于协议,NEC 是 8 位地址加 8 位命令,Sony 是 5 位地址加 7 位命令,RC5 是 5 位地址加 6 位命令。你在写上层逻辑的时候不能假设它们都是 8 位,要根据协议类型做不同的处理。flags字段里有一些有用的标志,比如IRDATA_FLAGS_IS_REPEAT表示这是一个重复帧(按住按键不放时遥控器会发送重复帧),IRDATA_FLAGS_IS_MSB_FIRST表示数据是高位在前。

获取完结果后,必须调用IrReceiver.resume()来重置接收状态,准备接收下一帧。如果你忘了调用这个函数,第二次解码就会失败。这是一个非常常见的初学者错误,我当初就因为这个卡了半天,以为接收头坏了,换了好几个才发现是漏了一行代码。

3.3 发射端核心 API 与参数含义

发射端的 API 在 3.x 版本里统一成了IrSender.sendNEC(address, command, repeats)这样的形式,每种协议有对应的发送函数。以 NEC 为例,address是 8 位地址,command是 8 位命令,repeats是重复发送的次数(0 表示只发一帧,1 表示发一帧加一个重复帧,以此类推)。

发射引脚并不是随便选一个 IO 口就行。在 AVR 系列上,库默认使用定时器来生成 38kHz 载波,而定时器的输出引脚是固定的。比如在 Uno 上,定时器 2 的输出引脚是 D3,所以你必须把红外 LED 接到 D3 上才能正常发射。如果你接到其他引脚上,要么完全没信号,要么频率不对。这个问题在库的文档里有说明,但很多人不看文档直接接线,然后发现发射没反应,以为是 LED 坏了。3.x 版本对这个问题做了一些改进,支持软件 PWM 发射,可以指定任意引脚,但软件 PWM 的时序精度不如硬件定时器,发射距离会打折扣。

repeats参数的使用场景是这样的:NEC 协议规定,当用户按住按键不放时,遥控器会先发送一帧完整的数据帧,然后每隔 108 毫秒发送一个重复帧,重复帧只包含引导脉冲和结束脉冲,不包含数据。接收端通过检测重复帧来判断用户是否在长按。所以如果你要模拟长按效果,就需要把repeats设成大于 0 的值,并且在每次发送之间保持适当的间隔。

4. 完整实操:从零搭建一个红外解码与回放系统

4.1 硬件准备与接线检查

先列一下我这次实操用到的物料:一块 Arduino Uno 兼容板、一个 38kHz 红外接收头(三脚直插式)、一个红外 LED(940nm)、一个 220 欧姆电阻、一个 100 欧姆电阻、一个 100nF 电容、若干杜邦线、一个面包板。接收头的接线是:OUT 接 D2,GND 接 GND,VCC 接 5V。在 VCC 和 GND 之间并上 100nF 电容,VCC 串联 100 欧姆电阻。发射 LED 的接线是:正极串联 220 欧姆电阻后接 D3,负极接 GND。

这里有一个细节值得展开:为什么接收头的 VCC 要串电阻并加电容?接收头内部是一个光敏二极管加上放大和解调电路,它对电源噪声非常敏感。开发板上的 5V 电源虽然看起来稳定,但实际上因为数字电路的高速开关,电源线上会有很多高频毛刺。这些毛刺会耦合到接收头的输出上,导致误触发。串一个 100 欧姆电阻和并一个 100nF 电容构成一个低通滤波器,可以把高频噪声滤掉。实测下来,不加这个滤波电路的时候,接收头在日光灯下会频繁输出错误的脉冲,加了之后误触发率大幅下降。

接线完成后,先不要急着写代码,用万用表测一下接收头的输出引脚电压。没有红外信号的时候应该是高电平(接近 5V),用遥控器对着接收头按按键的时候,电压应该有明显的下降波动。如果电压一直是低电平,说明接线有问题或者接收头坏了。这个简单的检查可以帮你排除掉大部分硬件故障。

4.2 接收端代码实现与串口输出

接收端的代码结构很清晰:引入库、定义引脚、初始化串口和接收器、主循环里检查解码结果并打印。下面是我实际使用的代码,加了详细的注释:

#include <IRremote.hpp> const uint8_t RECV_PIN = 2; void setup() { Serial.begin(115200); IrReceiver.begin(RECV_PIN, DISABLE_LED_FEEDBACK); Serial.println("IR receiver ready"); } void loop() { if (IrReceiver.decode()) { // 打印协议类型 Serial.print("Protocol: "); Serial.println(getProtocolString(IrReceiver.decodedIRData.protocol)); // 打印地址和命令,注意用十六进制方便对照 Serial.print("Address: 0x"); Serial.println(IrReceiver.decodedIRData.address, HEX); Serial.print("Command: 0x"); Serial.println(IrReceiver.decodedIRData.command, HEX); // 判断是否为重复帧 if (IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT) { Serial.println("(repeat frame)"); } Serial.println("---"); IrReceiver.resume(); } }

这段代码里有一个getProtocolString()函数,它是库提供的辅助函数,可以把协议枚举转换成可读的字符串。如果你用的版本里没有这个函数,可以自己写一个 switch-case 来映射。

烧录后打开串口监视器,把遥控器对准接收头按按键,你应该能看到类似这样的输出:

Protocol: NEC Address: 0x00 Command: 0x18 --- Protocol: NEC Address: 0x00 Command: 0x18 (repeat frame) ---

如果串口里什么都没有,先检查接收头的接线和电源,然后确认遥控器的电池有没有电。如果输出的是乱码或者协议显示为UNKNOWN,可能是载波频率不匹配或者遥控器使用了库不支持的协议。有些空调遥控器的编码非常长,一帧包含几十个字节,NEC 解码器处理不了,需要用到decodeHash()或者手动分析原始脉冲数据。

4.3 原始脉冲数据的获取与分析

当你遇到库无法自动识别的协议时,就需要把原始脉冲数据打印出来自己分析。IrReceiver.decodedIRData.rawDataPtr指向一个数组,里面存储了每个脉冲的高低电平和持续时间。库提供了一个IrReceiver.printIRResultRawFormatted()函数,可以把原始数据以微秒为单位格式化输出到串口。

我拿一个老式风扇遥控器做过测试,它的协议不在库的支持列表里。打印出来的原始数据是一串交替的正负数值,正数表示高电平持续时间,负数表示低电平持续时间。通过观察这些数值的规律,我发现它的引导脉冲是 4000 微秒高电平加 4000 微秒低电平,逻辑 0 是 500 微秒高电平加 500 微秒低电平,逻辑 1 是 500 微秒高电平加 1500 微秒低电平。这就是一个典型的非标准协议,我根据这个规律自己写了一个简单的解码函数,把脉冲序列转换成二进制,再转换成按键编码。

这个过程听起来很硬核,但实际操作起来并不难。你只需要把原始数据复制到电子表格里,用公式把正负值分开,然后根据阈值判断每个脉冲代表 0 还是 1。关键是找到引导脉冲和结束脉冲的特征,它们通常比数据脉冲长很多或者短很多,很容易识别。

4.4 发射端代码实现与编码回放

发射端的代码同样简洁。假设我们已经通过接收端获取了某个按键的地址和命令,现在要把它发射出去:

#include <IRremote.hpp> const uint8_t SEND_PIN = 3; void setup() { Serial.begin(115200); IrSender.begin(SEND_PIN); Serial.println("IR sender ready"); } void loop() { // 每隔 2 秒发送一次 NEC 编码 IrSender.sendNEC(0x00, 0x18, 0); delay(2000); }

IrSender.begin()的参数是发射引脚号。在 Uno 上,如果你用硬件定时器生成载波,这个引脚必须是 D3。如果你用的是支持软件 PWM 的版本,可以指定其他引脚,但发射距离会短一些。

发射距离的测试结果:直接用 IO 口驱动 LED,串联 220 欧姆电阻,在 5V 供电下,有效控制距离大约在 1 到 2 米之间。如果把电阻换成 100 欧姆,电流增大到约 35mA(超过了 IO 口的推荐值但还在绝对最大值以内),距离可以延伸到 3 米左右。如果要达到 5 米以上,就需要用三极管扩流,把 LED 电流提到 100mA 以上,同时用多个 LED 并联或者使用带透镜的 LED 来增加发射强度。

这里有一个实操心得:红外 LED 的发射角度很关键。普通的 5mm 红外 LED 发射角度大约在 20 到 30 度之间,如果你需要覆盖更大的角度,可以选用发射角度更宽的型号,或者用两个 LED 朝不同方向发射。我在做一个电视遥控器替代方案时,就因为 LED 角度太窄,导致必须对准电视才能控制,后来换成宽角度 LED 并调整了安装位置才解决。

5. 常见问题排查与避坑经验实录

5.1 接收不稳定、时好时坏的排查思路

接收不稳定是最高频的问题,表现是同一个按键有时候能解出来、有时候解不出来、有时候解出来的是错误的编码。这个问题可能的原因很多,我按照排查优先级列一个表:

现象可能原因排查方法解决方案
近距离正常,远距离失败接收头灵敏度不足或发射功率不够逐步拉远距离测试临界点增加发射 LED 电流,或更换高灵敏度接收头
日光灯下误触发频繁电源噪声或环境光干扰遮挡接收头看是否改善加电源滤波电路,或使用带屏蔽罩的接收头
同一按键编码偶尔错误定时器中断被其他任务打断检查是否有其他库占用定时器关闭冲突库,或调整中断优先级
完全无输出接线错误或引脚配置错误用万用表测接收头输出检查接线,确认引脚号与代码一致
只能解出一种协议库配置中未启用其他协议检查库的配置文件在配置文件中开启需要的协议宏

我遇到过一次很隐蔽的问题:接收头输出正常,但解码结果总是差一位。排查了很久才发现是电源纹波导致的。开发板通过 USB 供电,而 USB 线质量不好,5V 电源上有大约 200mV 的纹波。这个纹波叠加在接收头的输出上,导致脉冲边沿的触发时间偏移了几十微秒,刚好足以让解码器把某些位判错。换了一根带屏蔽的 USB 线之后问题就消失了。所以如果你遇到莫名其妙的解码错误,不妨换一根好一点的电源线试试。

5.2 定时器冲突与资源占用问题

Arduino-IRremote在 AVR 平台上默认使用定时器 2 来生成 38kHz 载波和捕获脉冲。定时器 2 同时被 Arduino 的tone()函数和某些舵机控制库使用。如果你在代码里同时用了这些功能,就会出现冲突——要么红外接收失效,要么舵机抖动,要么tone()发出的声音频率不对。

解决冲突的办法有几种。第一种是改用其他定时器,在库的配置文件里可以修改IR_USE_AVR_TIMER2这个宏,把它改成IR_USE_AVR_TIMER1,这样库就会使用定时器 1。但定时器 1 被Servo库占用了,所以如果你同时用舵机和红外,就需要更复杂的方案。第二种是使用软件 PWM 模式,在配置里开启IR_USE_AVR_TIMER2的替代方案,让库用micros()来做软件计时。软件计时的精度差一些,但在大多数场景下够用。第三种是换用 ESP32 或 STM32 这类有多个定时器的平台,从根本上避免资源冲突。

Flash 和 RAM 的占用也值得关注。在 Uno 上,如果开启了全部协议支持,库会占用大约 20KB 的 Flash 和 1KB 的 RAM。Uno 总共只有 32KB Flash 和 2KB RAM,这意味着你的主程序只剩下 12KB Flash 和 1KB RAM 可用。如果你的程序比较复杂,就需要在库的配置文件里关掉不需要的协议。比如你只做 NEC 解码,就把DECODE_SONY、DECODE_RC5这些宏都注释掉,可以省下不少空间。

5.3 发射距离短与信号方向性差

发射距离短的原因通常有三个:LED 驱动电流不足、载波频率偏差、LED 发射角度太窄。驱动电流的问题前面已经讲过,这里重点说载波频率偏差。

库默认的载波频率是 38kHz,但实际生成的频率会受到定时器时钟源精度的影响。开发板上的陶瓷谐振器或晶振的精度通常在 1% 到 5% 之间,这意味着实际载波频率可能在 36kHz 到 40kHz 之间波动。大多数接收头对载波频率的容忍度是正负 2kHz 左右,所以轻微的偏差不会导致完全失效,但会降低接收灵敏度。如果你发现发射距离明显短于预期,可以用示波器或者逻辑分析仪测一下发射引脚的波形,确认载波频率是否在 38kHz 附近。如果偏差太大,可以在库的配置文件里调整定时器的预分频系数来校准。

信号方向性差的问题在需要全向控制的场景下特别明显。红外 LED 的发光强度在正前方最强,偏离轴线 30 度之后强度就下降一半以上。如果你把 LED 朝向一个方向,遥控器就必须对准那个方向才能生效。解决方案是用多个 LED 组成阵列,每个 LED 朝向不同的方向,或者使用带散射透镜的 LED。我在一个项目中用了三个 LED,分别朝向正前方、左前方和右前方,覆盖角度从原来的 30 度扩展到了 90 度以上,用户体验好了很多。

5.4 空调遥控器长编码的处理技巧

空调遥控器的红外编码和电视遥控器完全不同。电视遥控器按一次键发一帧,一帧只有几个字节。空调遥控器按一次键会把当前的所有状态——温度、模式、风速、摆风、定时等等——打包成一帧发送,一帧可能有十几到几十个字节。Arduino-IRremote的 NEC 解码器处理不了这么长的帧,会报UNKNOWN或者只解出前面几个字节。

处理空调编码有几种思路。第一种是用decodeHash()函数,它不解析协议,只是把原始脉冲序列计算出一个哈希值。你可以把这个哈希值当作按键编码来用,接收端收到相同的哈希值就执行对应的操作。这种方法的优点是简单粗暴,缺点是哈希值不包含状态信息,你只能知道“用户按了某个键”,不知道具体是什么状态。第二种是手动分析原始脉冲数据,根据空调品牌的协议规范自己写解码器。这需要一些逆向工程的耐心,但一旦解出来,你就能获取完整的温度、模式等信息。第三种是直接用库的原始数据记录功能,把整个脉冲序列存下来,发射的时候原样回放。这种方法不需要理解协议,但存储空间占用大,而且换一个空调就要重新记录。

我个人的建议是:如果只是做简单的开关控制,用decodeHash()就够了。如果需要精确控制温度和模式,那就花时间手动分析协议。网上有一些开源项目已经逆向了不少常见空调品牌的协议,可以参考它们的分析结果,但要注意不同型号之间可能存在差异,不能直接照搬。

6. 进阶玩法与项目扩展方向

6.1 用红外接收做信号分析仪

Arduino-IRremote不仅能解码,还能把原始脉冲数据输出出来,这相当于一个简易的红外信号分析仪。你可以把接收到的原始数据通过串口发送到电脑,用 Python 或 Excel 画成波形图,直观地看到脉冲的时间分布。这对于分析未知协议非常有帮助。

我写过一个简单的 Python 脚本,从串口读取原始脉冲数据,然后用 matplotlib 画出波形。通过观察波形,可以很快判断出引导脉冲、数据位、结束脉冲的位置和宽度。如果你对某个遥控器的协议感兴趣,这个方法比查资料快得多。脚本的核心逻辑就是解析串口输出的文本,把正负数值分别提取出来,然后用plt.bar()画柱状图。虽然简陋,但足够用了。

6.2 结合网络模块做远程红外控制

把红外发射和网络模块结合起来,就可以做一个可以通过手机或电脑远程控制的红外网关。接收端用红外接收头学习遥控器编码,把编码存储在微控制器的 Flash 里,然后通过网络模块接收控制指令,根据指令发射对应的红外编码。

这个项目的技术难点在于编码的存储和管理。如果你要学习多个遥控器的多个按键,就需要一个结构化的存储方案。我的做法是用一个简单的键值对结构,键是“设备名+按键名”,值是对应的协议类型、地址和命令。存储用微控制器的 EEPROM 或者外部 Flash 芯片。网络模块可以用常见的 WiFi 模块,通过简单的 HTTP 接口或者 MQTT 协议来接收指令。

实际部署的时候要注意红外发射的覆盖范围。如果你把网关放在客厅角落,红外信号可能覆盖不到所有设备。我的解决方案是用多个红外发射管,通过延长线分布在房间的不同位置,或者用红外转发器把信号中继到更远的地方。

6.3 低功耗场景下的优化策略

如果你打算用电池供电做一个便携式的红外遥控器,功耗就是一个必须考虑的问题。Arduino-IRremote在接收模式下会持续运行定时器中断,功耗不低。在 3.3V 供电下,一块开发板加上接收头的待机电流大约在 10mA 到 20mA 之间,用普通电池撑不了太久。

降低功耗的思路有几个。第一,用休眠模式。在没有红外信号的时候让微控制器进入深度休眠,只保留外部中断唤醒。接收头的输出引脚可以连接到外部中断引脚,当有红外信号时,接收头输出电平变化,触发中断唤醒微控制器,然后再启动红外解码。第二,降低接收头的供电电压。大多数接收头的工作电压范围是 2.7V 到 5.5V,在 3.3V 下工作比在 5V 下工作省电不少,而且灵敏度下降不多。第三,减少解码协议的开启数量,只保留你实际需要的协议,这样可以降低 CPU 的处理负担,间接降低功耗。

我在一个电池供电的项目中实测过,用休眠模式加上 3.3V 供电,待机电流可以降到 1mA 以下,两节 AA 电池可以撑好几个月。唤醒响应时间大约在 10 毫秒左右,对于遥控器应用来说完全够用。

6.4 多协议共存时的内存管理

当你需要同时支持多种协议时,内存管理就变得重要了。每种协议的解码器都需要一定的 RAM 来存储中间状态和原始脉冲数据。库默认的原始脉冲缓冲区大小是 100 个条目,每个条目 2 个字节,也就是 200 字节。如果你开启了多个协议,每个协议可能还需要额外的状态变量。

在 RAM 紧张的板子上,可以通过修改库的配置文件来减小缓冲区大小。比如你只解码 NEC 协议,NEC 一帧最多 67 个脉冲,缓冲区设成 80 就足够了,不需要 100。另外,如果你不需要原始脉冲数据,可以在解码后立即释放缓冲区,或者干脆关闭原始数据记录功能。这些调整虽然看起来不起眼,但在资源受限的环境下能省出不少空间。

还有一个容易被忽略的点是栈空间。红外解码过程中会调用多层函数,如果栈空间不足,可能会导致程序跑飞或者复位。在 AVR 平台上,栈和堆是共享 RAM 的,如果你动态分配了很多内存,栈空间就会被压缩。建议在红外解码相关的代码中避免使用递归和大型局部数组,把大数组定义成全局变量或者用static修饰,让编译器把它分配到静态存储区而不是栈上。

7. 一些零散但实用的经验

关于接收头的选型,我再补充一点:金属屏蔽罩的接收头抗干扰能力明显强于塑料封装的,但价格也贵一些。如果你做的是产品原型或者对稳定性要求高的场景,多花几毛钱用屏蔽罩版本是值得的。另外,接收头的输出引脚上最好串一个 1k 欧姆的电阻再接到微控制器,这样可以限制电流,防止接收头内部电路故障时把微控制器的引脚烧掉。

关于代码调试,我强烈建议在开发阶段打开DECODE_IR_SERIAL或者类似的调试输出宏,把解码过程中的中间状态打印出来。虽然会占用一些串口带宽,但能帮你快速定位问题。比如你可以看到引导脉冲是否被正确识别、数据位的宽度是否在预期范围内、校验是否通过等等。等到功能稳定了再关掉调试输出。

关于遥控器的兼容性,有一个事实需要接受:没有任何一个库能支持所有遥控器。总会有一些冷门品牌或者老式设备使用非标准协议。遇到这种情况,不要死磕库的自动解码功能,直接上原始数据分析,自己写解码逻辑。红外通信的原理并不复杂,一旦你理解了脉冲和编码的关系,手动分析一个未知协议通常只需要一两个小时。

最后说一个关于发射的小技巧:如果你发现发射时好时坏,但代码和硬件都没问题,检查一下发射 LED 的供电。红外 LED 在发射瞬间的电流脉冲可能达到几百毫安,如果电源的内阻较大或者走线太细,电压会瞬间跌落,导致载波频率偏移或者发射功率不足。在 LED 的电源引脚附近并一个 100uF 的电解电容可以缓解这个问题,它能在发射瞬间提供足够的电荷储备。这个技巧在电池供电的场景下尤其重要,因为电池的内阻通常比稳压电源大得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进