红外遥控器在咱家里是隐藏的主力电视、空调、风扇、机顶盒几乎每天都离不开。可这玩意儿一旦丢了或者坏了原厂配件又不好买那台只能手动按面板的设备就尴尬了。更不用说想把几个遥控器合成一个、或者让智能家居系统帮你发红外指令的场景。我最近就做了一个小项目英文叫Build Your Own Infrared Signal Recorder说白了就是自制一台红外信号记录器。它能把任意红外遥控器发出的信号完整“抄写”下来然后原样重新发射出去。整台设备核心成本不到二十块用一块Arduino开发板加两个红外元件就能搭出来。我把整个过程中涉及的硬件选型、红外协议原理、代码实现和踩坑记录都整理出来了。想做电子DIY的朋友可以把这篇文章当成一份完整复现手册如果你只是好奇红外遥控背后是怎么回事读前两章也能建立起系统认识。我们直接开始。1. 项目能做什么为什么要做一台红外信号记录器1.1 红外遥控与信号记录的应用场景先说说这个项目最实在的用途。我自己做它的直接动机是家里的空调遥控器被孩子摔坏了。换一个原装遥控器几十上百副厂万能遥控器要对着型号表一个个试码运气不好还会遇到某几个功能没法用。这时候如果有一台红外信号记录器我只需要拿邻居同型号的遥控器把“开机、制冷26度、上下扫风”这几个按键信号抄下来再通过发射端原样打出去空调就乖乖听话了。实际应用场景远不止这一个。很多人喜欢折腾智能家居希望实现“进门自动开灯”“温度到了自动关空调”但空调本身不带网络功能市面上支持空调的万能红外遥控模块又未必覆盖所有老型号。用自制的红外记录器把老空调每个按键的真实波形记录下来转成标准红外命令流再配合Home Assistant或者Node-RED这类平台做定时和联动整套系统就活了。还有一些“批量复制”需求。比如教室、会议室、餐厅里的投影仪和电视机遥控器经常丢管理员手头只有一台备用机。用这个记录器把管理机上所有常用按键信号提取成数据文件存在电脑里。下一次丢遥控器照着文件里的波形重新发一遍就行根本不用等采购。1.2 技术方案记录原波形还是解码协议做红外信号记录器业内其实有两条路线原始波形记录和协议解码记录。协议解码方案是先获得完整的遥控编码数据再按某个特定协议比如NEC、Sony SIRC重新编码发射。IRremote 这类库可以直接解码出地址码和命令码存下来也就是几字节的事情非常紧凑甚至能塞进EEPROM里。缺点是红外遥控的协议非常多不同品牌甚至同品牌不同系列都有自创协议。想完美支持所有设备就得不断往库里加协议维护量大遇到一个冷门协议就是无底洞。原始波形记录方案则完全不吃这一套。它不关心你发的是什么协议只记录红外信号每一段高电平、低电平持续的微秒数。这就有点像录音时不管你是普通话还是方言先把声音波形存下来再说。原始波形方案的优点是通吃所有协议任何遥控器只要能发出信号它就一定能“抄”缺点是一个按键的完整波形可能有几十上百个时间参数存储开销大处理起来也比直接解码要费些功夫。我这台设备选的是原始波形记录路线。理由是它无需维护协议表对一个自制的、依赖“抄作业”的设备来说通用性比存储效率重要得多。后面章节讲的采样、存储、重放全都围绕这个方案展开。2. 硬件选型与电路连接2.1 核心元器件清单与选型理由做这个项目不需要高端器件我在普通电子市场加网购凑齐了一套总成本在20元以内。完整的清单如下元器件型号/规格数量作用主控板Arduino Uno / Nano1块负责信号采样、存储和发射控制红外接收头VS1838B或 TSOP382381个把红外光信号还原成电脉冲红外发射LED940nm5mm圆头1个把电脉冲还原成红外光信号三极管S8050 NPN1个放大发射管驱动电流提升发射距离电阻220Ω、1kΩ各1只2只发射管限流、三极管基极限流面包板 杜邦线通用若干搭电路方便调试USB线数据线不是充电线1根给Arduino供电并烧录程序主控板选Arduino最主要是因为生态成熟pulseIn()函数可以方便地测量脉冲宽度不用自己折腾定时器中断。如果你手上是ESP32、STM32甚至树莓派Pico思路完全一样只是底层API要换。红外接收头我推荐VS1838B它是台湾厂商的经典型号到处都买得到默认中心频率38kHz工作电压2.7V到5.5VTTL电平输出直接接Arduino引脚没问题。注意这型号有三个脚引脚间距标准2.54mm刚好能插面包板。红外发射LED选940nm波长的这是绝大多数家电遥控器的标准波长。别买成可见光的红外LED那种发光偏红、光强也不对。视角尽量选20度到30度左右的普通圆头兼顾对准和散射。2.2 红外接收与发射电路原理及接线全部电路其实只用了两个GPIO口加电源和地很简单。接线表如下组件引脚连接目标VS1838B输出OUTArduino D2VS1838B电源VCC5VVS1838B地GNDGNDS8050基极BArduino D3串1kΩ电阻S8050发射极EGNDS8050集电极C红外LED负极红外LED正极220Ω电阻电阻另一端接5VVS1838B的引脚定义从接收面凸起的圆弧面朝自己、从左往右是OUT、GND、VCC。我当初第一次接线就按网上流传的针脚图插反了结果串口一直没数据折腾半天才发现是引脚顺序记错。所以这里多说一句不同品牌型号的接收头引脚定义可能不一样接线前一定查对应型号手册或者从元件本体上找小圆点标记。VS1838B的针脚顺序虽然常见是OUT-GND-VCC但也有部分批次是VCC-GND-OUT别想当然。发射部分为什么加三极管红外LED正常工作电流约20mA到100mAArduino的GPIO口直接推20mA没问题但直接推100mA就有烧引脚的风险而且驱动能力不足会导致发射距离只有二三十厘米。用S8050做开关管D3输出高电平通过1kΩ电阻给基极灌电流三极管导通集电极到地形成回路5V电源通过220Ω电阻给红外LED供电LED电流可以由外部电源驱动发射距离能到三四米控制设备的接收窗完全够用。限流电阻的计算也顺手说一下。5V电源红外LED正向压降约1.2V到1.5V这里取1.3V三极管饱和压降约0.2V。则电阻两端电压为5 - 1.3 - 0.2 3.5V。如果想让电流25mA左右电阻值就是3.5V / 0.025A 140Ω取标准值220Ω电流约16mA。想让电流更大可以换成100Ω但别低于50Ω否则LED寿命会明显缩短。2.3 供电与电平匹配注意事项Arduino Uno的5V供电来自USBVS1838B和红外LED都从5V取电这个没问题。如果你的红外LED电流开到100mA以上注意不要直接从Arduino板载的稳压器取电最好外接5V电源否则板载稳压器过热会触发保护重启。还有一个容易忽略的细节接收头的输出是集电极开路结构有些型号内部没有上拉电阻需要把输出脚加一个10kΩ上拉电阻到VCC。VS1838B是推挽输出可以不用加但如果你换成了别的型号接收头发现逻辑不稳定先补一个上拉电阻试试。3. 红外协议原理先搞清楚“信号”是什么3.1 38kHz载波调制与解调在看代码之前我强烈建议先搞懂红外遥控的物理信号长什么样。遥控器发出去的红外光并不是持续点亮一整段时间而是以约38kHz的频率闪烁。每秒闪烁38000次这个高频闪烁就是“载波”。每个载波周期内LED亮约1/3的时间熄灭约2/3的时间这是一种脉冲位置调制和幅度调制的组合目的是用有限的功率提供足够大的信噪比。家电接收端为什么非要接收38kHz左右的载波因为接收头内部有一个中心频率38kHz的带通滤波器。如果直接把LED亮度按信号本身的节奏变化旁边一个白炽灯、阳光或者荧光灯的杂散红外光谱很容易把信号淹没。而把信号调制到38kHz上接收头只对38kHz附近的光强变化敏感自然光等低频干扰就被滤掉了。这就像两个人约定用特定暗号交流旁人听的只是普通对话只有对方能捕捉到那个频率的确切节奏。接收头内部通常是这么工作的光电二极管检测到的光信号经过自动增益控制、带通滤波、包络检波最后输出整形成TTL电平。特别注意接收头输出的电平和红外光有无是反相的检测到38kHz载波时输出低电平没有检测到载波时输出高电平。所以你在Arduino引脚上看到的一段段低电平对应的正是遥控器LED发光的时段。3.2 NEC协议帧结构解析家电遥控里最常用的是NEC协议很多国产电视、机顶盒、风扇都在用。理解NEC协议对你读懂后面记录的波形数据非常有帮助但原始波形记录不需要解码协议照样能玩转。这里讲NEC是为了让你能识别出数据里的引导码和二进制位方便验证。NEC协议的一帧信号通常是这样的结构引导码先是9ms的载波低电平然后4.5ms的无载波高电平。32位数据8位地址码、8位地址反码、8位命令码、8位命令反码按LSB先行最低位先发发送。数据位逻辑0560us的载波 560us的无载波。数据位逻辑1560us的载波 1680us的无载波。结束位560us的载波。也就是说一个按键按下去接收头输出端先看到一个大约9ms的低电平对应引导码的载波接着4.5ms高电平然后是32组“低-高”交替的脉冲对。每一组中载波持续时间都在560us左右拉开差距的是后面的无载波时间560us代表01680us代表1。有些遥控器按住按键不放会每隔110ms左右发一帧重复码重复码是9ms载波2.25ms无载波560us载波没有完整数据位。3.3 其他常见协议与兼容性说明除了NEC市面上还有索尼的SIRC协议、飞利浦的RC-5/RC-6协议以及各种国产厂商自定的协议。这些协议在载体频率上基本都是38kHz或36kHz/40kHz附近区别主要是帧格式、数据位宽度、引导码长度。你如果选原始波形记录方案就完全不用区分这些协议。你记录的是引导码、数据位、间隔的所有高低电平时序重放时原样还原。需要注意的只是接收头的带通频率是否跟你目标遥控器匹配。VS1838B中心频率38kHz对36kHz和40kHz的载波也有一定衰减但通常还是能稳定接收实测下来绝大多数遥控器都能正常记录下来。4. 代码实现从采样到重放4.1 接收端用脉冲计时记录完整时序现在开始写代码。整个项目最核心的就是“记录”这一步我先把接收端代码放出来。// ir_recorder.ino - 红外信号记录器接收端 #define IR_RECV_PIN 2 const int MAX_PULSES 600; uint16_t pulses[MAX_PULSES]; uint16_t pulseCount 0; void setup() { Serial.begin(115200); pinMode(IR_RECV_PIN, INPUT); Serial.println(IR recorder ready, press any remote button); } void loop() { // 等待信号起始空闲时接收头输出为高检测到载波跳变为低 while (digitalRead(IR_RECV_PIN) HIGH) { ; } pulseCount 0; while (pulseCount MAX_PULSES) { uint16_t lowUs pulseIn(IR_RECV_PIN, LOW, 20000); if (lowUs 0) break; pulses[pulseCount] lowUs; uint16_t highUs pulseIn(IR_RECV_PIN, HIGH, 20000); if (highUs 0) break; pulses[pulseCount] highUs; } Serial.print(pulseCount); Serial.println( pulses:); for (uint16_t i 0; i pulseCount; i) { Serial.print(pulses[i]); if (i pulseCount - 1) Serial.print(, ); } Serial.println(); delay(500); }代码的核心是pulseIn()。这个函数会测量指定引脚在指定电平状态下的持续时间单位是微秒最大测量宽度由第三个参数指定我这里设成20000us也就是20ms超过这个时间就返回0表示一帧信号结束。采样时我采用“先等低电平、再交替读低读高”的方式。因为接收头空闲是高电平信号一来必然先变成低电平而且对于NEC这类协议第一个脉冲确实是载波段也就是低电平。读一个低电平持续时间后马上读下一个高电平持续时间如此交替把所有“载波/无载波”对记录下来直到超时退出。用pulseIn()的优点是代码直观不依赖任何库。但缺点是阻塞式测量如果信号中间有某个极窄的抖动导致超时判断错误可能漏掉后续脉冲。对于大多数红外遥控信号间隔足够长实测下稳定。如果想做得更可靠可以改成外部中断定时器记录边沿时间戳我在后面章节会说优化思路。4.2 存储与序列化把时序变成可复用的数据接收端把脉冲数组打印到串口后怎么变成最终能发射的数据我建议采用“串口输出 - 网页/文本编辑 - 生成数组”的方式。具体就是观察串口监视器输出的脉冲序列复制到编辑器中稍作整理就能生成一个新的发射程序。一次按键记录比如电视遥控器的电源键串口输出可能是这样64 pulses: 8976, 4512, 560, 560, 560, 560, 560, 1688, 560, 560, ...第一对数字8976和4512就是引导码约9ms载波、4.5ms间隔。后面每两个数字表示一个数据位560, 560是逻辑0560, 1688是逻辑1。如果你只想把它存成一个小数组直接放进发射代码完全够用。如果数据量比较大或者你想积累几十个按键的码库存在Arduino的EEPROM里更合适。ATmega328P有1024字节EEPROM一个典型NEC帧包含几十个uint16_t每个占2字节大约能存十条左右的完整波形。具体写入时先把pulseCount放在地址0然后依次写入每个脉冲值。EEPROM写入有擦写寿命约10万次写码库不频繁使用没问题。我这里先提供最直接的方式串口复制粘贴生成数组。后续代码就以这个数组为发射来源所以存储部分不需要额外硬件方便你先把项目跑通。4.3 发送端按时序驱动红外LED发射下面这段是发射端的核心代码。思路完全相反遍历数组遇到偶数下标对应原载波段就输出38kHz调制波持续时间为数组值遇到奇数下标对应无载波间隔就直接延时。// ir_sender.ino - 红外信号记录器发送端 #define IR_EMIT_PIN 3 // 从串口复制下来的脉冲数组单位微秒 const uint16_t rawSignal[] { 8976, 4512, 560, 560, 560, 560, 560, 1688, 560, 560, 560, 1688, 560, 1688, 560, 560, 560, 1688, 560, 1688, 560, 560, 560, 1688, 560, 560, 560, 560, 560, 1688, 560, 560, 560, 1688, 560, 560, 560, 1688, 560, 560, 560, 560, 560, 1688, 560, 560, 560, 560, 560, 1688, 560, 1688, 560, 560, 560, 1688, 560, 1688, 560, 560 }; const int signalLen sizeof(rawSignal) / sizeof(rawSignal[0]); void setup() { pinMode(IR_EMIT_PIN, OUTPUT); digitalWrite(IR_EMIT_PIN, LOW); // 留1秒时间给你把开发板放到被控设备附近 delay(1000); for (int i 0; i 3; i) { sendRaw(rawSignal, signalLen); delay(100); } } void loop() { } void sendRaw(const uint16_t *buf, int len) { for (int i 0; i len; i) { unsigned int us buf[i]; if (i % 2 0) { // 载波段在us微秒内翻转38kHz方波 unsigned long start micros(); while (micros() - start us) { digitalWrite(IR_EMIT_PIN, HIGH); delayMicroseconds(8); digitalWrite(IR_EMIT_PIN, LOW); delayMicroseconds(18); } } else { // 空闲段保持LED熄灭 delayMicroseconds(us); } } }这里要详细说下38kHz载波是怎么产生的。正常情况下一个38kHz周期约26.3us。我的代码占据高电平8us、低电平18us算下来载波频率约38.46kHz接近38kHz。这个精度足以让接收头的带通滤波器识别。不过需要注意digitalWrite()本身有一些执行时间实际载波周期会比理论略长但这在红外接收的可容忍范围之内。如果想更精确可以改用硬件PWM。发射数组里偶数下标为什么一定是载波因为我们接收时是从低电平开始的。接收头空闲为高载波来临时输出低所以数组第一个元素就是载波持续时间后续交替。发射时我按同样的顺序偶数下标输出载波奇数下标保持熄灭正好把原始波形“倒”回去。如果接收过程不是从低电平开始或者你手动处理过数组需要保持一致否则发出去就是错的。5. 完整实操过程从面包板到成功复制遥控器5.1 硬件搭建与验证先把电路搭在面包板上。接好线后我习惯先做一步最简单的“光路验证”用普通手机摄像头对着红外发射LED在暗光下看能不能拍出紫色光斑。手机摄像头能看到近红外光这一步可以快速判断发射LED是否工作不需要任何代码。再用代码验证接收头是否正常。烧录接收端代码打开串口监视器随便拿一个遥控器对着接收头按一下键。如果串口能打印出脉冲序列接收链路就通了。如果没反应先检查接线特别是接收头的OUT引脚接的是不是D2以及GND和VCC是否接反。5.2 用真实遥控器完成一轮“抄-放”测试我以最常见的电视遥控器为例跑一遍完整流程。把接收端程序烧进Arduino打开串口监视器把遥控器对准VS1838B按一下电源键。串口立刻输出类似下面的数据64 pulses: 8976, 4512, 560, 504, 560, 508, 560, 1632, 560, 504, ...留意第一对数字一个在9000us附近一个在4500us附近这就是NEC引导码。因为每个制造商的电阻电容精度不同实际值会和理论值有一些偏差完全正常不需要修正。把完整数组复制下来粘贴到发射端代码的rawSignal[]数组里修改signalLen为实际脉冲个数。把发射端代码烧录进去Arduino上电后1秒内自动发三次。把红外LED对准电视接收窗口距离半米左右按键按下的一瞬间电视的开关状态就会切换。如果没反应不要着急。先检查发射LED是否点亮手机摄像头看再看看接收窗口和发射LED之间有没有障碍物然后把距离缩短到20厘米再试。通常这轮操作能解决90%的问题。5.3 优化加入物理按键和状态指示直接自动发射的代码适合测试但长期用不方便。我建议修改一下发送端加一个触发按键短按一次按键发一次信号同时在板载LED上闪一下表示发送中。#define TRIGGER_PIN 4 #define STATUS_LED 13 void setup() { pinMode(TRIGGER_PIN, INPUT_PULLUP); pinMode(STATUS_LED, OUTPUT); pinMode(IR_EMIT_PIN, OUTPUT); digitalWrite(IR_EMIT_PIN, LOW); Serial.begin(9600); } void loop() { if (digitalRead(TRIGGER_PIN) LOW) { // 按键按下低电平有效 delay(50); // 简单消抖 while (digitalRead(TRIGGER_PIN) LOW) { delay(10); } digitalWrite(STATUS_LED, HIGH); sendRaw(rawSignal, signalLen); digitalWrite(STATUS_LED, LOW); Serial.println(Signal sent); } }这样一台独立的“红外信号复制器”就成型了。你还可以把多个按键的波形存成多个数组用不同按键触发不同命令做成一个小型的学习型遥控器。甚至可以把数据和固件做进一个项目里让用户通过串口命令选择信号。5.4 进阶方向用硬件PWM提高发射稳定性软件调制载波虽然能用但在低端开发板上有个小问题delayMicroseconds()在中断或者高频循环里精度有限而且长时间占用CPU无法同时做其他事。如果你的项目中还要同时驱动显示屏、处理网络请求建议把载波发生交给硬件。在Arduino Uno上可以用Timer1的快速PWM模式在D9引脚上输出38kHz。设置寄存器如下void setup38kHzPWM() { pinMode(9, OUTPUT); TCCR1A _BV(COM1A1) | _BV(WGM10); // 10-bit Fast PWMOC1A输出 TCCR1B _BV(WGM12) | _BV(CS10); // 预分频1 OCR1A 419; // 16MHz / 38000 - 1 ≈ 420 }这样D9引脚会持续输出38kHz方波。发射时先用一个数字引脚控制三极管的供电或直接控制IR LED的接地端在需要载波时打开在空闲时关闭。这种方式的载波频率精确、占空比稳定特别适合制作长期运行的红外发射器。缺点是你需要改接D9作为发射引脚同时准备好一个控制开关。这里我先不展开全部代码给一个思路参考。6. 常见问题与排查技巧实录6.1 完全接收不到信号这个现象最常见的原因排序是接线错误、接收头损坏、串口波特率不对、接收头带通频率不匹配。我建议按这个顺序排查先用万用表确认接收头VCC和GND之间的电压是5V左右然后确认OUT引脚接到了D2。如果是先用杜邦线接的检查杜邦线是否接触不良面包板上氧化生锈的插孔也会导致信号不导通可以换一根线重插。再看串口监视器波特率是不是115200别用9600去看一个115200的输出那只会看到乱码。如果你手头正好有示波器可以看一下OUT引脚的波形在无信号时应该是高电平按遥控器时应该有低电平脉冲。没示波器的话用Arduino写一个最简单的程序不断打印digitalRead(IR_RECV_PIN)然后按遥控器看是否出现0。这样也能排查接收头是否工作。6.2 录下来了但重放无效这是最让人头疼的情况但它通常有几类固定原因。第一类是发射LED方向或位置不对。很多家电的红外接收窗是深色的你不能背对它发信号。对准接收窗、距离缩短到30厘米再试一次往往就好了。第二类是红外LED驱动电流不足。如果没接三极管直接用GPIO推LED发射角度和功率都不够。对着接收窗近距离可能行远一点就不行。建议按前面电路加S8050驱动。第三类是数组里的脉冲顺序或者截断不完整。有些协议在重复发送时需要整帧信号如果你的采样在某个长间隔处提前结束了会导致整帧数据缺失。检查脉冲数组最后是否有几个异常大的数比如超过10000us说明这是停止标志可以在重放时把它去掉或保留都行。第四类是载波频率严重偏离。如果软件调制的载波频率偏到了30kHz以下接收头的带通滤波器衰减很大发射距离骤减。可以试着把delayMicroseconds(8)和delayMicroseconds(18)微调成delayMicroseconds(9)和delayMicroseconds(17)让载波更接近38kHz。6.3 距离太短和环境干扰如果你在室内的正常使用距离下遥控不了设备很可能不是记录器的问题而是接收端灵敏度受限。红外接收头对环境光敏感强光直射时AGC电路会自动降低增益导致发射信号传不远。尽量在室内正常光线下使用避免阳光直射接收头。发射距离受电流影响很大。如果三极管驱动后仍然只有二三十厘米可以尝试减小限流电阻到100Ω或者改用两个红外LED并联扩大发射角度。注意电流不要超过LED最大额定值长时间工作温度会上升。6.4 实测踩坑记录总结我把这几次制作过程中真正踩过的坑整理成一张表给后面的人参考现象原因解决办法接收不到信号VS1838B引脚顺序搞反对光面朝自己引脚左到右为OUT-GND-VCC并查手册确认串口输出全是一串255波特率设置不对接收端代码和串口监视器都设为115200发射无反应D3没接三极管电流太小加S8050三极管驱动用5V电源和220Ω限流发射距离太短软件载波频率偏了调整延时常数到约8us/18us或改用硬件PWM部分空调按键没用空调信号需要连续帧录制时按住遥控器按键2秒录取完整多帧序列重放时循环发多次重放一次没反应两次才生效信号边界没处理干净在发送前加一小段空闲延时让接收端复位7. 收获与下一步玩法把这台红外信号记录器跑通之后我最大的感受是很多看似“高科技”的无线控制底层原理其实比想象中朴素。我们日常用的红外遥控器本质上就是用一个LED以38kHz的节奏闪烁然后接收端把这个闪烁序列还原成数字信号仅此而已。我自己做完之后又叠了两个玩法一是把录制好的码库整理成JSON文件存在电脑上以后换主控板直接把码库导入就行二是把记录器挂在ESP8266上通过WiFi接收命令来发射红外信号再接上小爱同学或者语音助手就能实现“喊一声把电视关掉”。这些扩展需要的核心能力都是这篇文章里讲到的记录和重放基础。如果你也动手做建议第一轮只用面包板和Arduino跑通流程确认能复制你家里最常见的那个遥控器之后再考虑做PCB、加外壳、改成电池供电这些锦上添花的部分。毕竟这个项目最值得享受的是“我竟然连遥控器都能抄一份”的那种满足感。