
1. 项目概述为什么我们需要一个“多协议”红外库如果你玩过Arduino和红外遥控大概率用过IRremote这个库。它几乎是Arduino生态里红外接收和发送的“标准答案”让点亮一个LED或者控制一个舵机变得像按一下电视遥控器那么简单。但用久了尤其是当你从简单的玩具项目转向更实际的智能家居改造或者设备集成时一个痛点就会浮现兼容性。你从旧家电上拆下来的遥控器可能用的是NEC协议你新买的空调遥控可能是格力专用的编码而一些风扇或者灯具的遥控又可能是奇怪的RC5或者RC6协议。原版的IRremote库虽然支持不少协议但面对市面上成百上千种私有、变种或者小众协议常常显得力不从心。这时Arduino IRremote多协议红外遥控扩展库通常指IRremoteESP8266或类似增强版分支的价值就凸显出来了。它不是一个全新的轮子而是在经典之上通过社区的力量集成了对海量红外协议的支持尤其深度优化了对各种品牌空调、电视、机顶盒等复杂设备的支持。简单说这个扩展库的核心目标就是**“通吃”**。它让你手头的ESP8266、ESP32或者Arduino板子从一个只能识别几种标准协议的红外玩具升级为一个能学习、解析并模拟绝大多数民用红外遥控信号的“万能遥控器”大脑。这对于想要统一家中所有红外设备控制、打造离线智能家居中枢或者进行设备逆向工程的朋友来说简直是神器。接下来的内容我会带你彻底拆解这个库从设计思路到实战应用分享我趟过的坑和积累的技巧。2. 核心设计思路与协议生态解析2.1 从“标准”到“生态”协议支持的哲学转变原版IRremote库的设计哲学是“提供经典协议支持”。它完美覆盖了像NEC、Sony SIRC、Philips RC5/RC6这些在消费电子早期非常流行的标准协议。这些协议的特点是公开、文档齐全、逻辑相对简单。库的实现方式也较为直接为每个协议编写独立的发送和接收类。而多协议扩展库以IRremoteESP8266为代表则转向了“构建协议生态”的思路。它的诞生与ESP8266/ESP32这类Wi-Fi物联网芯片的普及紧密相关。人们不再满足于控制单个设备而是希望用一块联网的开发板通过MQTT、HTTP或物理按钮去控制客厅里所有品牌、所有型号的红外设备。因此它的设计首要目标是广度和可维护性。为了实现这一点它的代码架构通常采用更模块化的设计协议抽象层定义统一的编码/解码接口每个协议都是一个独立的、实现该接口的模块。庞大的协议数据库社区驱动不断添加对新品牌、新设备协议的支持。目前主流的扩展库支持超过100种协议从常见的格力、美的、海尔空调到小众的先锋音响、大金空调等。面向复杂设备特别强化了对空调协议的支持。空调遥控器的信号远比电视开关复杂它包含了模式制冷、制热、温度、风速、扫风等大量状态信息。扩展库会为这些设备提供高级的、面向对象的数据结构让你可以用setTemperature(25)这样直观的方法而不是去手动计算一串神秘的十六进制码。2.2 关键协议深度剖析与选择逻辑面对上百种协议你不需要全部掌握但了解几个核心类别能帮助你在遇到问题时快速定位。第一类消费电子标准协议NEC协议这是你最常见到的。它的编码逻辑简单一个起始码9ms高电平4.5ms低电平后跟32位数据地址码地址反码命令码命令反码。反码用于校验抗干扰能力强。几乎所有的山寨电视盒、DVD机遥控器都是NEC或其变种。Sony SIRC特点是脉冲宽度恒定通过脉冲间隔来区分0和1。协议长度有12、15、20位等多种常见于索尼老式设备。Philips RC5/RC6使用双相编码曼彻斯特编码信号中没有明确的高低电平区分“1”和“0”而是用电平的跳变沿来表示。优点是抗干扰能力极强但解码逻辑稍复杂。常见于飞利浦和部分品牌的电视、音响。注意原版库对这些标准协议支持很好。扩展库在兼容它们的同时更重要的贡献在于处理了大量的“变种”。例如很多设备使用“NEC扩展”协议只是地址码长度或重复码的间隔不同扩展库会将其识别为独立的协议如NEC_LIKE避免误解码。第二类空调专用协议这是扩展库的“主战场”。空调协议通常具有以下特点帧长度长可达100位以上包含大量状态位。校验复杂除了字节反码还可能包含CRC校验、求和校验等。状态编码温度、风速等信息不是简单的二进制数而是映射到特定的位组合上。厂商私有格力、美的、海尔、大金等都有自己的私有协议互不兼容。扩展库的价值在于它已经为这些主流空调品牌编写了专用的解析器和发送器。例如对于格力空调库中会有一个Gree类它内部封装了格力协议的所有细节。你只需要调用Gree.setMode(kGreeCool)、Gree.setTemp(25)库就会自动生成正确的、长长的红外码。这省去了你手动分析波形、计算校验和的巨大工作量。第三类小众与自定义协议对于一些实在没有现成支持的设备扩展库也提供了强大的工具链来帮助你“学习”和“模拟”。核心是它的RAW码录制与回放功能。这个功能不关心协议是什么它只是高精度地记录下红外信号的所有高低电平持续时间以微秒为单位然后原样发送出去。这相当于一个“波形克隆”对于任何协议都有效。3. 硬件选型、环境搭建与核心API详解3.1 硬件选型不只是Arduino Uno虽然标题叫Arduino库但它的主战场是ESP系列芯片。ESP8266 (NodeMCU, Wemos D1 mini)这是性价比最高的选择。它自带Wi-Fi可以轻松接入家庭网络通过网页或HomeAssistant控制。其主频80/160MHz和内存足以流畅处理复杂的协议解码和网络任务。强烈推荐作为主力平台。ESP32性能更强有蓝牙如果需要更复杂的多任务或蓝牙辅助配网ESP32是更好的选择。但在纯红外收发场景下ESP8266已完全够用。Arduino Uno/Nano经典AVR芯片。可以用但有明显局限内存小无法存储太长的RAW码缺乏网络功能对于部分非常复杂的空调协议解码过程可能会吃力。仅适合最简单的、离线的单设备控制场景。红外收发硬件接收头常用的是VS1838B、TSOP38238等。重点是电压要匹配通常是3.3V或5V并且载波频率要对绝大多数民用设备是38kHz。扩展库能自动适配不同引脚但建议将接收头的数据脚接到支持外部中断的引脚上如ESP8266的GPIO4/D2以获得更稳定的接收效果。发射管普通红外发射二极管即可。关键点在于驱动电流。单片机IO口的输出电流有限通常20mA直接驱动发射管会导致信号弱、距离短。必须使用三极管如8050或MOS管进行电流放大。一个典型的电路是IO口 - 限流电阻100-220Ω - 三极管基极VCC5V - 发射管阳极 - 发射管阴极 - 三极管集电极三极管发射极接地。3.2 库的安装与核心对象初识在Arduino IDE中通常可以通过库管理器搜索IRremoteESP8266进行安装。安装后你会看到大量示例代码从最简单的接收解码到完整的空调控制。库的核心是几个关键类IRrecv用于接收和解码红外信号。IRsend用于发送红外信号。IRac空调控制抽象类。这是扩展库的精华它提供了一个统一的接口来控制不同品牌的空调。其下有为各品牌实现的子类如IRGree、IRHaier等。decode_results一个结构体用于存放解码结果。里面包含了协议类型、解码后的数据、RAW码数组等重要信息。3.3 核心API实战与代码拆解让我们通过两段核心代码来理解如何使用。场景一万能解码——识别未知遥控器#include IRrecv.h #include IRremoteESP8266.h const uint16_t kRecvPin 14; // ESP8266的GPIO14/D5 const uint16_t kCaptureBufferSize 1024; // RAW码缓冲区大小 IRrecv irrecv(kRecvPin, kCaptureBufferSize); decode_results results; void setup() { Serial.begin(115200); irrecv.enableIRIn(); // 启动接收 Serial.println(红外接收器就绪请按下遥控器按键...); } void loop() { if (irrecv.decode(results)) { // 打印人类可读的摘要 serialPrintUint64(results.value, HEX); Serial.print( (); Serial.print(typeToString(results.decode_type)); Serial.print()); Serial.print( 位长: ); Serial.println(results.bits); // 打印详细的RAW码用于分析未知协议 Serial.println(RAW码微秒:); for (uint16_t i 1; i results.rawlen; i) { Serial.print(results.rawbuf[i] * kRawTick); if (i results.rawlen - 1) Serial.print(, ); } Serial.println(); Serial.println(); irrecv.resume(); // 准备接收下一个信号 } }这段代码是红外分析的“瑞士军刀”。当你不确定遥控器协议时就运行它。它会尝试用所有已知协议解码并打印出协议名称、十六进制码值和位长。最关键的是它会打印出RAW码即原始的高低电平时间序列。如果协议不被支持这个RAW码就是你后续进行“克隆”发送的唯一依据。场景二精准控制——发送特定协议信号假设我们已经知道客厅的灯是NEC协议地址码是0xFF00开灯命令是0x45。#include IRsend.h const uint16_t kIrLedPin 4; // 红外发射管连接的引脚 IRsend irsend(kIrLedPin); void setup() { irsend.begin(); } void loop() { Serial.println(发送NEC开灯信号...); irsend.sendNEC(0xFF00FF45, 32); // 注意库通常期望完整的32位数据地址和命令的反码会自动处理或需要包含在内。 delay(5000); // 等待5秒 }这里sendNEC函数是协议相关的发送函数。对于空调这类复杂设备更推荐使用高级的IRac接口#include IRac.h #include IRGree.h // 以格力为例 IRGree ac(kIrLedPin); // 创建格力空调控制器对象 void setup() { ac.begin(); ac.setPower(true); ac.setMode(kGreeCool); ac.setTemp(25); ac.setFan(kGreeFanAuto); ac.send(); // 将所有设置组合并发送出去 }这种面向对象的方式代码意图清晰不易出错是控制复杂设备的首选。4. 实战构建一个离线学习的万能红外遥控中枢现在我们整合以上知识打造一个实用的项目一个具备学习功能的离线万能遥控器。它可以通过物理按钮学习任意遥控器的按键并将编码保存在ESP8266的Flash中之后通过另一个按钮触发发送。4.1 系统设计与材料清单功能设计学习模式长按“学习键”3秒进入此时按下外部遥控器按键设备记录并保存该信号。发射模式短按“发射键”依次发送已学习的所有信号例如用来一键打开电视、机顶盒、音响。状态指示通过一个RGB LED或不同颜色的LED指示当前模式学习/就绪和状态成功/失败。材料清单ESP8266开发板NodeMCU x1红外接收头VS1838B x1红外发射管 x1NPN三极管如S8050 x1、220Ω电阻 x2、1kΩ电阻 x1轻触按键 x2LED或RGB LED x1-3面包板及杜邦线若干4.2 电路连接详解电路连接是稳定工作的基础有几个关键点红外接收头VCC接3.3VGND接地OUT接GPIO14D5。接收头非常怕电源噪声务必确保其供电稳定可以在VCC和GND之间并联一个10-100uF的电解电容。红外发射电路这是最容易出问题的地方。正确接法如下ESP8266的GPIO4D2 - 1kΩ电阻 - 三极管基极(B)。三极管发射极(E) - 接地。VIN5V - 红外发射管阳极 - 红外发射管阴极 - 三极管集电极(C)。原理当GPIO4输出高电平时三极管导通5V电源通过红外管和三级管形成回路红外管以大电流发射。电阻用于限制基极电流保护GPIO。按键一端接地另一端接GPIO如D1 D6并在该GPIO与3.3V之间接一个10kΩ的上拉电阻。这样按键未按下时GPIO读到高电平按下时读到低电平。LED指示通过一个220Ω限流电阻接到GPIO上即可。4.3 核心代码实现与逻辑剖析我们将使用IRremoteESP8266库并利用ESP8266的EEPROM实际上是模拟的Flash区域来存储学习到的RAW码数据。#include Arduino.h #include IRrecv.h #include IRremoteESP8266.h #include IRac.h #include IRutils.h #include EEPROM.h // 引脚定义 #define RECV_PIN 14 // D5 #define LED_PIN 2 // 板载LED低电平点亮 #define LEARN_BUTTON_PIN 5 // D1 #define SEND_BUTTON_PIN 12 // D6 #define IR_LED_PIN 4 // D2 // 红外接收设置 const uint16_t kCaptureBufferSize 1024; IRrecv irrecv(RECV_PIN, kCaptureBufferSize); decode_results results; // EEPROM地址管理 struct StoredSignal { uint16_t rawData[200]; // 存储RAW码 uint16_t length; // RAW码长度 decode_type_t protocol; // 协议类型 uint64_t value; // 解码值如果协议已知 }; StoredSignal learnedSignal; int eepromAddress 0; // 按钮状态 bool learnMode false; unsigned long learnModeStartTime 0; const unsigned long learnModeTimeout 10000; // 学习模式超时10秒 void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); // 初始熄灭 pinMode(LEARN_BUTTON_PIN, INPUT_PULLUP); pinMode(SEND_BUTTON_PIN, INPUT_PULLUP); irrecv.enableIRIn(); // 启动红外接收 EEPROM.begin(512); // 初始化EEPROM申请512字节空间 // 尝试从EEPROM读取已学习的信号 loadSignalFromEEPROM(); } void loop() { // 1. 处理学习按钮 if (digitalRead(LEARN_BUTTON_PIN) LOW) { // 按钮按下 delay(50); // 简单防抖 if (digitalRead(LEARN_BUTTON_PIN) LOW) { unsigned long pressStart millis(); while (digitalRead(LEARN_BUTTON_PIN) LOW) { // 等待释放 delay(10); } unsigned long pressDuration millis() - pressStart; if (pressDuration 3000) { // 长按3秒进入学习模式 enterLearnMode(); } } } // 2. 如果在学习模式尝试接收信号 if (learnMode) { if (irrecv.decode(results)) { // 成功接收到信号 saveSignal(results); exitLearnMode(true); // 成功退出 irrecv.resume(); } // 检查学习模式超时 if (millis() - learnModeStartTime learnModeTimeout) { exitLearnMode(false); // 超时退出 } } // 3. 处理发送按钮 if (digitalRead(SEND_BUTTON_PIN) LOW) { delay(50); if (digitalRead(SEND_BUTTON_PIN) LOW) { sendLearnedSignal(); while (digitalRead(SEND_BUTTON_PIN) LOW) { // 等待按钮释放 delay(10); } } } } void enterLearnMode() { learnMode true; learnModeStartTime millis(); digitalWrite(LED_PIN, LOW); // 点亮LED表示进入学习模式 Serial.println(进入学习模式请对准接收头按下遥控器按键...); } void exitLearnMode(bool success) { learnMode false; digitalWrite(LED_PIN, HIGH); // 熄灭LED if (success) { Serial.println(信号学习成功并已保存); // 可以快速闪烁LED以示成功 for(int i0; i3; i) { digitalWrite(LED_PIN, LOW); delay(100); digitalWrite(LED_PIN, HIGH); delay(100); } } else { Serial.println(学习模式超时或取消。); } } void saveSignal(decode_results *src) { learnedSignal.protocol src-decode_type; learnedSignal.value src-value; learnedSignal.length src-rawlen - 1; // 将RAW码数据复制到结构体中 // 注意rawbuf[0]是第一个间隙通常忽略我们从1开始 for (uint16_t i 1; i src-rawlen (i-1) 200; i) { // kRawTick是单位转换因子将库的内部计数转换为微秒 learnedSignal.rawData[i-1] src-rawbuf[i] * kRawTick; } // 保存到EEPROM EEPROM.put(eepromAddress, learnedSignal); EEPROM.commit(); // 必须调用commit才能使写入生效 Serial.println(信号已保存至EEPROM。); } void loadSignalFromEEPROM() { EEPROM.get(eepromAddress, learnedSignal); // 简单的有效性检查长度不为0且在合理范围内 if (learnedSignal.length 0 learnedSignal.length 200) { Serial.println(从EEPROM加载已学习信号成功。); Serial.print(协议: ); Serial.println(typeToString(learnedSignal.protocol)); } else { learnedSignal.length 0; // 标记为未学习 Serial.println(EEPROM中无有效信号。); } } void sendLearnedSignal() { if (learnedSignal.length 0) { Serial.println(错误未学习任何信号无法发送。); return; } Serial.println(正在发送已学习的信号...); digitalWrite(LED_PIN, LOW); // 发送时点亮LED // 创建IRsend对象用于发送 IRsend irsend(IR_LED_PIN); irsend.begin(); // 根据协议类型选择发送方式 if (hasACState(learnedSignal.protocol)) { // 如果是空调等复杂协议且我们存储了解码值可以尝试用高级方式发送 // 这里简化处理统一用RAW码发送兼容性最好 irsend.sendRaw(learnedSignal.rawData, learnedSignal.length, 38); // 38kHz载波 } else if (learnedSignal.protocol ! decode_type_t::UNKNOWN) { // 对于已知协议可以尝试用标准方法发送更精确 // 例如irsend.sendNEC(learnedSignal.value, 32); // 但为了通用性这里也使用RAW码发送 irsend.sendRaw(learnedSignal.rawData, learnedSignal.length, 38); } else { // 未知协议只能用RAW码发送 irsend.sendRaw(learnedSignal.rawData, learnedSignal.length, 38); } digitalWrite(LED_PIN, HIGH); Serial.println(信号发送完毕。); }代码逻辑深度解析EEPROM存储我们定义了一个StoredSignal结构体来打包存储信号的所有关键信息。使用EEPROM.put和EEPROM.get可以方便地对整个结构体进行读写。切记ESP8266的EEPROM实际上是Flash上划出的一块区域有擦写寿命约10万次频繁保存同一信号需谨慎。EEPROM.commit()是必须的否则数据不会真正写入。防抖与长按检测按钮检测中加入了delay(50)的简单防抖并通过计算millis()差值来区分短按和长按这是嵌入式开发中处理机械开关的经典方法。RAW码发送的普适性在sendLearnedSignal函数中无论学习到的信号是已知协议还是未知协议我们都优先选择sendRaw函数进行发送。这是因为RAW码记录了最原始的波形能100%还原信号兼容性最强。虽然对于已知协议使用如sendNEC等函数可能在理论上更精确占空比控制更好但sendRaw的可靠性在实际项目中经过了我的反复验证。状态反馈通过板载LED的常亮、闪烁来指示学习模式、成功、失败等状态对于没有屏幕的设备来说这是至关重要的用户体验设计。5. 高频问题排查与性能优化实战录即使按照上述步骤操作在实际搭建中你仍可能会遇到一些问题。下面是我在多个项目中总结出的“踩坑实录”和解决方案。5.1 信号接收不稳定或完全收不到这是最常见的问题90%以上是硬件和电源问题。症状遥控器按下没反应或者时好时坏Serial端口打印不出解码信息。排查步骤供电检查首先怀疑电源。ESP8266在Wi-Fi工作时峰值电流可能超过200mA劣质的USB线或手机充电头供电不足会导致系统不稳定。务必使用输出5V/1A以上的优质电源并尽量直接供电而非通过电脑USB口特别是笔记本电脑。接收头验证编写一个最简单的测试程序只初始化IRrecv并打印decode结果。用手电筒非红外照射接收头如果程序有反应会误触发说明接收头和数据线基本是好的。也可以用手机摄像头观察红外发射管按下遥控器时在手机屏幕里应能看到发射管发出白紫色光点。引脚与中断冲突确保接收头连接的引脚支持中断。对于ESP8266几乎所有GPIO都支持但要避开一些特殊功能引脚如GPIO16。另外避免在接收红外信号时进行密集的串口打印或网络操作这可能会阻塞进程导致丢包。可以尝试在loop中只做红外解码一件事。环境光干扰强烈的日光灯、太阳光都含有红外成分会干扰接收。尝试在较暗的环境下测试或者给接收头套上一小段黑色热缩管作为遮光罩。5.2 信号发送了但设备无反应距离短、方向性强问题通常出在发射电路。症状代码运行正常发送无报错但必须把发射管几乎贴到设备接收窗上才有反应。解决方案驱动电流不足这是首要原因。万用表测量发射管两端电压在发送时电压应接近电源电压如5V。如果电压被拉得很低如只有2V说明驱动管三极管的基极电流不够或三极管本身放大倍数太低。可以尝试减小连接GPIO到基极的电阻从1kΩ降到470Ω或者更换放大倍数更高的三极管。发射管数量与布局单个发射管功率有限。可以并联2-3个发射管需确保驱动电路能提供足够总电流。将发射管放置在透明或半透明的外壳中并尽量朝向多个方向以扩大覆盖角度。载波频率偏差虽然绝大多数设备是38kHz但个别设备可能有细微偏差。sendRaw函数的最后一个参数是载波频率kHz可以尝试微调如改为37或39。信号格式错误对于已知协议确保你发送的数据格式完全正确。例如NEC协议需要发送完整的32位数据包括地址反码和命令反码。使用库提供的sendNEC等函数通常比自己拼数据更可靠。一个调试技巧用手机摄像头观察你的发射管同时观察一个正常工作的遥控器发射管。你的发射管闪烁的亮度和频率应该与正常遥控器看起来几乎一致。如果明显更暗或闪烁模式不同就是驱动电路问题。5.3 学习到的信号无法控制设备协议识别错误症状学习过程成功保存了信号但回放时设备不响应。排查RAW码录制不完整有些遥控器信号很长或者有特殊的头码和重复码。确保kCaptureBufferSize设置得足够大如1024甚至更大。学习时观察串口打印的RAW码长度如果接近缓冲区大小可能需要增大它。协议误判库可能将你的私有协议误判为某个标准协议。学习时仔细看串口打印的协议类型。如果显示UNKNOWN那么用RAW码回放是唯一选择。如果显示了一个已知协议如NEC但控制失败可以尝试强制用RAW码发送如我们示例代码中所做因为标准发送函数可能忽略了协议的某些私有变种细节。信号头尾被裁剪在保存RAW码时我们通常从i1开始。但对于某些特殊协议第一个间隙rawbuf[0]可能包含重要信息。如果遇到问题可以尝试将rawbuf[0]也保存下来并在发送时通过irsend.sendRaw的第三个参数initialGap来指定它。5.4 性能与内存优化技巧当项目复杂需要存储多个信号或集成网络功能时资源管理变得重要。减少RAW码存储占用RAW码数组非常占内存每个元素是uint16_t。如果信号很长存储多个信号可能耗尽内存。可以考虑在存储前进行简单的“压缩”例如如果协议已知就只存储协议类型和解码值value而不是整个RAW数组。发送时再用标准函数生成。这能节省大量空间但牺牲了对未知协议的支持。使用IRac抽象层对于空调等设备务必使用IRac相关的类。它不仅代码简洁而且底层发送效率更高因为它直接生成优化的信号序列而不是通过RAW码回放。避免在中断服务程序(ISR)中处理复杂逻辑虽然IRremoteESP8266库的中断处理已经优化但如果你自己注册了其他硬件中断要确保ISR尽可能短小不要在里面进行Serial.print或复杂计算否则会影响红外接收的时序精度导致解码失败。管理Wi-Fi与红外的资源竞争ESP8266的Wi-Fi功能在收发数据时CPU占用较高。如果发现开启Wi-Fi后红外控制变迟钝可以尝试在发送关键红外指令前短暂暂停或降低Wi-Fi任务优先级例如使用delay(0)或yield()让系统处理一下后台任务。对于实时性要求极高的场景可以考虑使用更强大的ESP32其双核架构可以将红外任务和网络任务分到不同核心上执行。