一个红外遥控项目,其实就是三根线加一个库。HX1838 接收头的信号脚接到 Arduino 任意一支数字脚,IRremote 库为每一次按键交给你一个数字,而你的程序不过是针对这个数字的一个 switch。几乎每一句「我的 Arduino 红外遥控代码跑不起来」,追到最后都是库版本的问题:今天的库管理器装的是 4.x,而照 2.x 写的程序用的是一套已经被换掉的 API。
做一个 Arduino 红外遥控项目需要哪些零件?
HX1838 红外遥控套件把「接收」这一半整包配齐:一支 17 键的哑光黑遥控器、一小块黑色 PCB 上的接收头模块、一条四线杜邦线,还有一颗留着以后发射用的透明红外 LED。其余的都是被控制的对象 —— 两颗 LED 用来证明解码真的成功了,一个继电器用来证明它能切换东西。
材料清单 —— 用 Arduino 和 HX1838 套件做红外遥控
选配 —— 从零开始的话
| Item | Price | Qty | |
|---|---|---|---|
Electronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KITELECTRK | RM39.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 开始,只有一个全局的 IrReceiver,decode() 不带参数,解出来的一帧落在 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 照样编译得过,因为库里就为这种情况附了一层向后兼容的过渡代码。这层过渡代码会抛出七条编译警告,而且只填三个字段 —— value、bits 和 decode_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,并不代表哪里坏了。

从别的网页抄来的十六进制对照表,不会对上你手上这支遥控器,甚至可能对不上你自己早些时候打印出来的那一份。差异有两层。一是遥控器型号本身的地址和命令就不一样。二是同一次按下,在不同库版本里打印出来也不一样: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 的一端换个地方接。跳到 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。

我们从真实客户那里看到的常见错误
串口监视器里跳出一大段英文,而不是一个码。程序和接线都没问题;那是 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 兼容层里。要么迁移到 IrReceiver 和 decodedIRData,要么装 2.6.0。
为什么网上找到的十六进制码对不上我的遥控器?
两个原因叠在一起。一是遥控器型号本来就不同,二是不同库版本对同一次按下打印出来的东西也不同 —— 2.x 给的是完整的 32 位字、高位在前,4.x 给的是 8 位命令、低位在前。打印你自己的对照表。
HX1838 套件也能发射红外信号吗?
能。袋子里那颗透明的 5 mm LED 就是发射管,同一个库用 IrSender.sendNEC() 就能发。串一颗电阻去驱动它,你就能把解出来的码重放出去 —— 这是万能遥控器的基础,也是语音控制和蓝牙控制的近亲。
最后更新:2026 年 8 月。有问题?WhatsApp 联系我们。



HX1838 IR Remote Control Set Infrared Receiver Module Kit with Remote Controller for Arduino Pi DIY Projects
Arduino Compatible UNO R3 FT232 Development Board Arduino Compatible CH340 Alternative
Relay Module 3.3V 5V 12V 1/2/4/8 Ways Optocoupler Trigger Relay Module 1 2 4 8 Channel Relay Module - 5V RELAY MODULE(1WAY)
Round Head LED 3mm / 5mm / 8mm - Red/Yellow/Blue/Green/White - 5mm LED (RED)
Round Head LED 3mm / 5mm / 8mm - Red/Yellow/Blue/Green/White - 5mm LED (GREEN)
400 pcs 1/4W Resistor Pack Resistor Kit 20 Common Value with 20 each
MB102 Breadboard 170 400 830 Holes Breadboard Donut Board Arduino Prototype Multi Color - BREADBOARD (400 HOLES)
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-M 20CM
Data Cable Type-A Type-C MicroUSB Type-B 0.5m 1m 30cm 0.3m 100cm Data Transfer Upload Code - TYPE-A TO TYPE-B (1.0M)
Electronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KIT