Arduino 红外遥控(HX1838):解码按键、控制 LED 和继电器

Cartoon of a large matte-black IR remote with blank white keys and a blue arrow pad sending teal infrared arcs to a small black receiver module with a silver shield can, beside a red LED, a green LED and a red relay board

一个红外遥控项目,其实就是三根线加一个库。HX1838 接收头的信号脚接到 Arduino 任意一支数字脚,IRremote 库为每一次按键交给你一个数字,而你的程序不过是针对这个数字的一个 switch。几乎每一句「我的 Arduino 红外遥控代码跑不起来」,追到最后都是库版本的问题:今天的库管理器装的是 4.x,而照 2.x 写的程序用的是一套已经被换掉的 API。

做一个 Arduino 红外遥控项目需要哪些零件?

HX1838 红外遥控套件把「接收」这一半整包配齐:一支 17 键的哑光黑遥控器、一小块黑色 PCB 上的接收头模块、一条四线杜邦线,还有一颗留着以后发射用的透明红外 LED。其余的都是被控制的对象 —— 两颗 LED 用来证明解码真的成功了,一个继电器用来证明它能切换东西。

选配 —— 从零开始的话

ItemPriceQty
Electronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KITElectronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KITELECTRKRM39.95

这个电子入门套件里已经有面包板、杜邦线、各色 LED 和一包电阻,所以如果你是从零开始搭第一套桌面器材,可以直接拿这一盒,替掉上面的面包板、杜邦线、LED 和电阻那几行。

零件能不能接得上,取决于杜邦线的公母。套件里那条线两端都是母头,所以它能把接收头的针脚接到像 Raspberry Pi 这类外露排针的板子上,但在 Uno 上就无处可夹 —— Uno 板边同样是母座。接收头模块的三支脚是 2.54 mm 的公针,所以它改成直接插进400 孔面包板;从面包板再接到 Uno 的母座,每一条线都是公对公,同一包线也够接继电器模块 —— 它的螺丝端子正好能夹住一支裸露的公针。整台机器一包公对公就够了。这块 Uno 是用全尺寸 USB-B 烧录的,而且出厂不附线,所以还要加一条 A 转 B 数据线

每颗 LED 都要串一颗限流电阻,而这个值是算出来的,不是抄来的。Arduino 的输出脚大约是 5 V,一颗红色 5 mm LED 的压降大约 2.0 V,所以 220 Ω 通过的电流是 (5 − 2.0) / 220 ≈ 14 mA —— 够亮,也远低于 ATmega328P 数据手册给出的「单支 I/O 脚 40 mA」绝对最大值。绿色 LED 是同一道算式换个颜色。抽屉里任何一颗 220 Ω 电阻都行;电阻包和入门套件里都带。

Arduino 红外遥控该装哪一个库?

IRremote 库 —— Ken Shirriff 最早写的那一个,之后由 Armin Joachimsmeyer 接手维护 —— 就是这篇教程用的库。请从 IDE 里面装:Sketch → Include Library → Manage Libraries,输入 IRremote,在那一条上按 Install。要从库管理器装,而不要抓论坛帖子里附的 ZIP,因为 ZIP 永远停在写那篇帖子那天的版本 —— 「拿 2.x 的代码去对 4.x 的文档」正是这么来的。另外,搜这个词还会跳出 IRremoteESP8266,那是另一个库、另一套 API,是给 ESP 系列板子写的,所以请确认你装的那一条就是纯粹的 IRremote。

为什么我以前的 Arduino 红外遥控代码现在跑不起来了?

IRremote 库在第 3 版重写了接收端的 API,并把这套写法一路保留到 4.x,所以从 2.x 年代的博客抄来的程序,和今天库管理器装下来的库,讲的是两套不同的话。在 2.x 里,你要自己建一个接收对象,再把一个 results 结构体传进 decode()。从 3.x 开始,只有一个全局的 IrReceiverdecode() 不带参数,解出来的一帧落在 IrReceiver.decodedIRData 里。

程序要做的事 IRremote 2.x IRremote 4.x
头文件 #include <IRremote.h> #include <IRremote.hpp>
建立接收对象 IRrecv irrecv(2); 不用建 —— IrReceiver 已经存在
存放解码结果 decode_results results; 不用建 —— 直接读 IrReceiver.decodedIRData
开始接收 irrecv.enableIRIn(); IrReceiver.begin(2, ENABLE_LED_FEEDBACK);
判断收到没有 if (irrecv.decode(&results)) if (IrReceiver.decode())
读出按了哪个键 results.value(32 位,高位在前) IrReceiver.decodedIRData.command
判断按键被按住 results.value == 0xFFFFFFFF flags & IRDATA_FLAGS_IS_REPEAT
等下一帧 irrecv.resume(); IrReceiver.resume();

你会看到哪一种症状,取决于那份旧程序留着的是哪一个头文件,而两种症状都不会自己承认「这是版本问题」。把 #include <IRremote.h> 原样留着,4.x 照样编译得过,因为库里就为这种情况附了一层向后兼容的过渡代码。这层过渡代码会抛出七条编译警告,而且只填三个字段 —— valuebitsdecode_type。它还会在你等着看十六进制码的串口监视器里,直接打印十八行的升级通知;而且是在第一次调用 decode() 时就打出来,那时你连按键都还没按下去。反过来,如果只把头文件改成 #include <IRremote.hpp>、2.x 的代码原封不动,那编译器是过了,链接器却不放行:undefined reference to 'IRrecv::decode(decode_results*)' —— 因为带参数的那个 decode(),除了那层过渡代码之外根本没有函数本体。

老实的解法有两条。一是照上面那张表把程序迁移过去 —— 这也是唯一顺带能拿到协议名称、地址和重复标志的一条路。二是干脆把库钉在旧版本:在库管理器里选中 IRremote,打开版本下拉框,装 2.6.0,也就是它自己那段提示所指的版本。这适合一个你不想再去惊动的旧项目,但当默认选择并不好,因为你之后读到的所有资料都是照 4.x 写的。如果你宁可看别人改一遍,也不想对着表逐行读,下面这段影片把一份 2.x 程序逐行迁移了一遍 —— 4.x 要的仍然是同一套改动:

怎样找出你自己那支遥控器的按键码?

HX1838 接收头在你的程序看到任何东西之前,就已经把最难的一段做完了。遥控器上的发射管以 38 kHz 的载波闪烁;带屏蔽罩的模块只放大这一个频率,把载波剥掉,然后在 S 脚上给出干净的逻辑电平 —— 载波在的时候是低电平。正是这一层很窄的滤波,让日光不会被当成按键按下。

骑在载波上的是 NEC 协议,这一类套件遥控器几乎都用它。按一下先发一段 9 ms 的引导脉冲和 4.5 ms 的间隔,接着送 32 个位,每个位都是一段 560 µs 的脉冲,后面跟一段间隔:560 µs 代表 0,1690 µs 代表 1。这 32 个位就是四个字节:地址、地址取反、命令、命令取反。取反的那两份是协议自带的校验字节,所以一帧解出来要么是对的、要么根本不出现,而不会悄悄错掉。IRremote 也靠这个校验去分辨变体:当命令和它的反码对不上时,它会把这一帧报成 Onkyo、命令 16 位 —— 所以协议名不是 NEC,并不代表哪里坏了。

卡通时序图:上方是完整的红外按键帧,先是一段长引导脉冲,再是一串窄窄的数据位;下方是短得多的重复帧,只有引导脉冲和一个收尾脉冲;一端画着黑色遥控器,另一端是带银色屏蔽罩的黑色接收模块
按一下(上):长引导脉冲,接着是数据位,每个位靠它后面那段间隔的宽度来表示 0 或 1。按住不放(下):同样的引导脉冲、更短的间隔、一个收尾脉冲,没有数据 —— 按固定间隔一直重复。

从别的网页抄来的十六进制对照表,不会对上你手上这支遥控器,甚至可能对不上你自己早些时候打印出来的那一份。差异有两层。一是遥控器型号本身的地址和命令就不一样。二是同一次按下,在不同库版本里打印出来也不一样:2.x 报的是整个 32 位字、高位在前的一个数,而 4.x 把地址和命令分开报,而且低位在前。拿一个 2.x 的值 0x00FFA25D 来看:它的四个字节是 0x00、0xFF、0xA2、0x5D —— 地址、地址取反、命令 0xA2、命令取反 0x5D。把 0xA2(1010 0010)的位序倒过来,得到 0100 0101,也就是 0x45 —— 正好是 4.x 对同一次按下打印出来的值。一个按键,外面流传着三个数字。请打印你自己的对照表。

按住不放并不会把整帧重发一次。遥控器改发一段很短的重复帧 —— 同样的 9 ms 引导脉冲、更短的 2.25 ms 间隔、一个收尾的 560 µs 脉冲,没有数据 —— 每 110 ms 一次,直到你松手。2.x 把它表现成哨兵值 0xFFFFFFFF,这就是你见过的每一行 if (results.value == 0xFFFFFFFF) 的由来。4.x 则是在 decodedIRData.flags 里置上 IRDATA_FLAGS_IS_REPEAT,并把上一次的地址和命令一起复制进来,所以命令依然读得到,怎么处理由你决定:做开关就忽略重复帧,做长按调光就正好拿它来用。

decodedIRData 里的字段 它存的是什么 你拿它来做什么
protocol NEC、SONY、RC5…… 或 UNKNOWN UNKNOWN 通常表示信号太弱,或者被挡掉了一部分
address 是哪一支遥控器发的 同一支遥控器上每个按键都一样 —— 可以不管它,也可以用它挡掉邻居家的电视遥控器
command 按下的是哪一个键 这就是你的 switch 要拿来比对的那个数
flags 位字段,其中包含 IRDATA_FLAGS_IS_REPEAT 用它来分辨「按一下」和「按住不放」

红外接收头、LED 和继电器该怎么接线?

HX1838 接收头模块把引脚名用白色丝印印在板上:一边是 S,另一边是 ,夹在中间那支没有标注的就是 VCC。顺序是 S、VCC、GND,也就是电源在正中间 —— 模块插反了,5 V 就会送到本该接地的那支脚上。

1WRELAY 模块的控制侧不是排针,而是一个蓝色的三位螺丝端子,标着 IN、DC− 和 DC+。这个端子和线圈之间还隔着一颗光耦,而这改变了你那支 Arduino 引脚在做的事:它并不驱动继电器线圈。它点亮的是黑色光耦芯片里面一颗小小的 LED,正对着这颗 LED 的光敏三极管再去控制一只三极管,由这只三极管让 5 V 电源把电流推过线圈。Songle SRD-05VDC-SL-C 的线圈标称约 70 Ω,所以 5 V 会推出大约 70 mA —— 这是单支 GPIO 该给的好几倍,也正是 DC+ 必须接到 5 V、不能空着的原因。

光耦隔开的是引脚和线圈电流,而不是两路电源:这里的 DC+ 用的就是 Uno 自己的 5 V。但这依然是它的意义所在,因为一个继电器加两颗点亮的 LED 加起来大约 100 mA,USB 供电毫无压力 —— 真正需要另外一路 5 V 的,是多路继电器板那种好几个线圈同时吸合的场合。如果你的项目本来就有一条 12 V 电源,同一系列也有 12 V 线圈的版本

红色继电器模块的卡通剖面图:青色的输入回路点亮黑色光耦芯片里的一颗橙色 LED,光线跨过一道空气间隙照到光敏三极管上,由它接通另一条深蓝色回路去驱动蓝色的继电器线圈,三针排针上套着一个黄色跳线帽
Arduino 的引脚只是点亮光耦里面那颗小 LED。线圈的电流来自空气间隙另一侧的 5 V 电源。

板子下缘那排三针、跳线帽跨在其中两针上的排针,选的是触发电平,而它的做法正是把同一颗光耦 LED 的一端换个地方接。跳到 H 时,LED 的另一端接在 DC− 上,所以你的引脚要输出高电平、往里面灌几毫安,回路才通。跳到 L 时,那一端改接 DC+,于是同一颗 LED 要在你的引脚拉低、把这股电流吸进去的时候才亮。线圈和触点都没有变 —— 变的只是哪一种电平能闭合这个回路,所以继电器动作方向反了,是跳线帽的位置问题,不是你程序里的 bug。下面那份程序按 H 来写。

板子另一侧的触点就是一个纯粹的机械开关:COM 和 NO 自己不带电,所以负载必须把自己的电源接进这个回路 —— 电源正极进 COM,NO 出来接负载,负载再回到那路电源的负极。NC 是同一个开关反过来,继电器没吸合时它是闭合的。这里请把这个回路限制在低压直流;牵涉到市电时会有哪些不同,我们的继电器模块安全接线教程讲得很清楚。

装配请从电源轨开始,而不是从零件开始。Uno 上只有一个 5V 插孔和三个 GND 插孔,而这台机器要在两处取 5 V(接收头,以及继电器的 DC+),要在四处回地(接收头、两颗 LED 的负极,还有 DC−)。所以先用一条公对公跳线把 Uno 的 5V 接到面包板边上的红色电源轨,再用一条把 GND 接到蓝色轨,之后所有东西都从这两条轨上取。同一条轨上每一个孔都是同一个节点,于是这条轨就成了 Uno 排针给不了的那个汇合点。

接到 为什么
Uno 5V 面包板红色电源轨 Uno 上只有一个 5 V 插孔,却有两样东西要喂
Uno GND 面包板蓝色电源轨 Uno 上三个 GND 插孔,要收四条回地线
接收头 S Uno D2 解调后的信号 —— 任意数字脚都行
接收头中间那支脚 面包板红色电源轨 电源。它夹在 S 和 − 中间,所以先看丝印再插
接收头 面包板蓝色电源轨 地参考
Uno D4 → 220 Ω → 红色 LED 长脚 LED 短脚 → 蓝色轨 长脚是正极;经过电阻约 14 mA
Uno D5 → 220 Ω → 绿色 LED 长脚 LED 短脚 → 蓝色轨 同样的接法,换个颜色
Uno D7 继电器 IN 螺丝端子 只负责点亮光耦里的 LED
面包板红色电源轨 继电器 DC+ 螺丝端子 提供线圈那约 70 mA 的电流
面包板蓝色电源轨 继电器 DC− 螺丝端子 线圈电流的回路
负载自己的电源 → COM,负载 → NO 只限低压直流 继电器没吸合前是断开的;触点本身不带电

完整的 Arduino 红外遥控代码长什么样?

这份 Arduino 红外遥控程序两件事一起做,所以不必先烧一份用完就丢的试探程序:每解出一帧,它都会打印协议、地址和命令,同时已经有三个命令接上了动作。上传,把串口监视器开到 9600 baud,按你的按键,把打印出来的值抄进最上面那三个常量,再上传一次。

// Arduino 红外遥控:先解出你自己的按键码,再拿它们来用。
// HX1838 接收头 + 17 键遥控器,两颗 LED 和一个单路继电器模块。
// 按 IRremote 4.x 写(IrReceiver / decodedIRData)。旧的 2.x 程序用的是
// IRrecv + decode_results,行为并不一样 - 见上文。

#include <IRremote.hpp>   // 注意是 .hpp - 这是 4.x 的头文件

// ---- 引脚 ----
// 接收头接哪一支数字脚都行:在 Uno 上,库是靠 Timer2 中断去采样这支脚,
// 而不是靠外部中断脚。
const uint8_t IR_RECEIVE_PIN = 2;
const uint8_t LED_RED_PIN    = 4;
const uint8_t LED_GREEN_PIN  = 5;
const uint8_t RELAY_IN_PIN   = 7;   // 继电器跳线在 H = 输出高电平就吸合

// ---- 「你那支」遥控器的数字 ----
// 先跑一次这份程序,盯着串口监视器,按下你想用的那三个键,
// 把它们打印出来的 "Command" 值抄进下面这三行。
// 下面这些值只是占位用的 - 别当成你手上这一套的值。
const uint16_t CMD_RED   = 0x45;
const uint16_t CMD_GREEN = 0x46;
const uint16_t CMD_RELAY = 0x40;

bool relayOn = false;

void setup() {
  pinMode(LED_RED_PIN, OUTPUT);
  pinMode(LED_GREEN_PIN, OUTPUT);
  pinMode(RELAY_IN_PIN, OUTPUT);
  digitalWrite(RELAY_IN_PIN, LOW);      // 一上电先让继电器保持释放

  Serial.begin(9600);
  IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
  Serial.println(F("Ready. Point the remote at the receiver and press a button."));
}

void loop() {
  // 在一整帧收齐并解读出来之前,decode() 一直是 false。
  if (!IrReceiver.decode()) {
    return;
  }

  // 这一帧带来的所有东西,都在这一个结构体里。
  const uint16_t command  = IrReceiver.decodedIRData.command;
  const uint16_t address  = IrReceiver.decodedIRData.address;
  const uint8_t  flags    = IrReceiver.decodedIRData.flags;
  const bool     isRepeat = (flags & IRDATA_FLAGS_IS_REPEAT) != 0;

  // 立刻把接收器交还回去,免得我们还在打印的时候
  // 漏掉下一帧。
  IrReceiver.resume();

  if (IrReceiver.decodedIRData.protocol == UNKNOWN) {
    // 这一版认不出来的协议:通常是信号太弱、有阳光干扰,
    // 或者遥控器用的协议在编译时被关掉了。
    Serial.println(F("Unknown frame - move closer and try again."));
    return;
  }

  // 这就是你的按键对照表。把每个键打印出来的东西记下来。
  Serial.print(F("Protocol: "));
  Serial.print(IrReceiver.getProtocolString());
  Serial.print(F("  Address: 0x"));
  Serial.print(address, HEX);
  Serial.print(F("  Command: 0x"));
  Serial.print(command, HEX);
  if (isRepeat) {
    Serial.print(F("  (repeat - button still held)"));
  }
  Serial.println();

  // 按住不放时,大约每 110 ms 会发一帧短短的重复帧。忽略重复帧,
  // 就把「按住」还原成「按一下」,这正是开关式动作要的。
  if (isRepeat) {
    return;
  }

  switch (command) {
    case CMD_RED:
      digitalWrite(LED_RED_PIN, !digitalRead(LED_RED_PIN));
      break;
    case CMD_GREEN:
      digitalWrite(LED_GREEN_PIN, !digitalRead(LED_GREEN_PIN));
      break;
    case CMD_RELAY:
      relayOn = !relayOn;
      digitalWrite(RELAY_IN_PIN, relayOn ? HIGH : LOW);
      Serial.println(relayOn ? F("Relay ON") : F("Relay OFF"));
      break;
    default:
      // 其他按键:一样解出来、一样打印,只是还没接上任何动作。
      break;
  }
}

照现在这样写,它编译出来是 9,408 字节,占 Uno 闪存的 29%,因为 IRremote 把它带的每一种协议解码器都打开了。等串口监视器确认协议是 NEC 之后,在 include 上面加一行 #define DECODE_NEC,同一份程序就掉到 4,614 字节、14% —— 一行换回将近 5 kB。

卡通图:白色面包板上插着一颗带银色屏蔽罩的黑色红外接收模块、一颗亮着的红色 LED 和一颗没亮的绿色 LED 以及蓝色电阻,用青色杜邦线接到一块红色继电器板,也接到画面边缘只露出一角的深蓝色 Arduino 板,前方摆着一支哑光黑遥控器
桌面上做好的样子:接收头和两颗 LED 在面包板上,继电器接在螺丝端子那一侧,遥控器就在手边。

我们从真实客户那里看到的常见错误

串口监视器里跳出一大段英文,而不是一个码。程序和接线都没问题;那是 2.x 兼容层在自我介绍,出现在程序第一次调用 decode() 的时候,而那时你还没按任何键。把头文件改掉就好。

按哪个键都完全没反应。先把遥控器的电池托拉出来看看:遥控器出厂时会用一片透明塑料膜把电池隔开来运输,这片东西一定要抽掉。接着让板子帮你把问题分成两半。IrReceiver.begin() 里的 ENABLE_LED_FEEDBACK 会让库在有红外信号进来时闪烁 Uno 板载的 13 脚 LED,所以你按键时那颗 LED 会闪,就说明遥控器、接收头和接线三样都在做事,问题出在后面的解码或者你的 switch;如果它一直不亮,就是根本没有信号到达那支脚 —— 检查接收头中间那支脚是不是真的在 5 V 上,以及它那颗圆顶透镜是不是朝着你,而不是朝着面包板。

红外本来是好的,加了蜂鸣器或马达之后就不灵了。在 Uno 上,IRremote 是用 Timer2 去采样接收脚 —— 而 tone() 会把这个定时器接管过去,脚 3 和脚 11 的 PWM 背后也是同一个定时器。所以 tone() 会悄无声息地把接收掐掉,直到后面跟上一句 IrReceiver.restartTimer();在脚 3 或脚 11 上用 analogWrite() 也一样。把 PWM 负载挪到脚 5、6、9 或 10。

按一下算成两下,或者菜单一次跳两格。那是重复帧被当成新的按键读进去了。哪怕只按住五分之一秒,也会发出一帧再跟上一两帧重复帧,间隔 110 ms,所以请像上面那份程序一样去测 IRDATA_FLAGS_IS_REPEAT 然后提前 return。用一句 delay(200) 去糊住它,看起来像是好了,接着就会开始漏掉真正的按键:库一次只保留一帧,于是这段延时让一帧过期的重复帧占住了那个位置,而真正的第二次按下跟在后面,无处可落。

常见问题

IRremote 库我应该装哪一个版本?

装现在的 4.x,然后照 4.x 写代码。只有在要让一个旧项目继续活着的时候才选 2.6.0 —— 库自己那段提示指的就是这个版本,而库管理器的版本下拉框就能装。

为什么我的程序报 “undefined reference to IRrecv::decode(decode_results*)”?

因为你是 2.x 的代码配 4.x 的头文件。IRremote.hpp 里没有带参数的 decode();它只存在于旧的 IRremote.h 兼容层里。要么迁移到 IrReceiverdecodedIRData,要么装 2.6.0。

为什么网上找到的十六进制码对不上我的遥控器?

两个原因叠在一起。一是遥控器型号本来就不同,二是不同库版本对同一次按下打印出来的东西也不同 —— 2.x 给的是完整的 32 位字、高位在前,4.x 给的是 8 位命令、低位在前。打印你自己的对照表。

HX1838 套件也能发射红外信号吗?

能。袋子里那颗透明的 5 mm LED 就是发射管,同一个库用 IrSender.sendNEC() 就能发。串一颗电阻去驱动它,你就能把解出来的码重放出去 —— 这是万能遥控器的基础,也是语音控制蓝牙控制的近亲。

最后更新:2026 年 8 月。有问题?WhatsApp 联系我们

Leave a Reply

Your email address will not be published. Required fields are marked *