自制IR乐高火车控制器:从RC5协议分析到Arduino最小实现

发布时间:2026/8/26 21:43:11
自制IR乐高火车控制器:从RC5协议分析到Arduino最小实现 我一直觉得乐高火车是个很矛盾的东西拼搭过程能让你沉浸一下午可真的把它放到轨道上控制体验还停留在上世纪。官方红外遥控器的反应总有一股“慢半拍”的劲而且一台遥控器只能绑定一个通道想同时跑两台车就得频繁拨码。所以我干脆决定自己做一个基于IR的乐高火车控制器IR LEGO Train Controller改动尽可能少不动火车本体不换接收器只用一块单片机加一个红外发射管把官方遥控器的信号模拟出来。这篇文章是项目的第一部分目标是先跑通“发射红外信号-火车接收-正常行驶”这条最小链路。这个项目适合谁如果你是玩乐高Power Functions火车系列的玩家或者单纯对红外遥控协议、嵌入式信号模拟感兴趣都可以参考。第一部分不追求炫技重心放在协议分析、硬件搭建、代码实现和实测验证上完全不需要你先把整条轨道铺完再动手。1. 为什么放着官方遥控器不用非要自己做1.1 官方方案的三个硬伤先说结论官方遥控器不是不能用而是当你手里的车多起来之后它的局限性会非常明显。第一个问题是响应延迟。按下遥控器按键之后火车不是立刻响应而是有明显的“一卡一卡”的感觉。这和你手指的力度没关系因为红外协议本身要发送完整数据帧火车接收器还要做连续两帧校验整个链路下来体感延迟会变得很明显。如果你只是让孩子在客厅里玩一圈这个延迟勉强可以接受但如果你把火车当成一个需要精确控制的平台比如做自动停站、多车调度这种响应速度就不够了。第二个问题是控制通道太少。官方红外遥控器靠通道拨码来区分不同的车拨码组合就那么多想同时控制三辆以上火车就得频繁蹲下去拨码。而且每次更换控制对象之后还要重新对表现场非常混乱。第三个问题是没法集成到自动化系统里。你想用电脑或者手机去控制火车官方遥控器完全不给你这个机会它就是一个封闭的遥控器。自己做一个控制器就不一样了后面接上蓝牙串口、WiFi模块甚至直接跑一套脚本都能通过单片机把这套红外发送逻辑调用起来。这也是我决定自己折腾的根本原因。1.2 Part 1的边界先把最小链路跑通这个项目规划了好几部分但第一部分没必要一开始就做得很花哨。我给自己定的目标是从零开始用一块Arduino Nano、一个红外发射管、一个限流电阻再配逻辑分析仪和红外接收头实现以下功能解析乐高PF火车接收器使用的红外协议搞清楚遥控器发出来的每一帧数据里藏着什么信息。用单片机程序生成同样的红外信号让火车接收器以为是我在按遥控器。实现前进、后退、加速、停止这几个最基本的控制命令。通过串口或者按键在真车上验证信号发送是否成功。这个范围很克制。多通道调度、App控制、自动巡航这些都是Part 1跑通之后才能逐步叠加的能力。先别急着改方案把最小闭环做出来能让你对后续扩展有更清晰的感觉。1.3 这个项目适合谁如果你属于下面这几类人这篇Part 1会很对胃口有乐高火车尤其是Power Functions系列觉得官方遥控器不够好玩想自己动手改。对红外遥控协议、RC5编码、曼彻斯特编码有好奇心想做点实物验证。业余时间玩单片机想找一个“能看得见移动效果”的项目而不是只点个LED灯。想给孩子的乐高火车加一点定制控制能力又不想去改火车内部结构。2. 先搞懂乐高火车在“听”什么红外协议拆解2.1 38kHz载波与红外遥控的基本套路绝大多数红外遥控器本质就是一个“编码”加“载波”的组合。编码负责表达信息载波负责让信息在空气中传输。你可以把载波想象成快递车的发动机声音编码则是快递单上写的地址和内容。红外LED发出来的光就是载波它不停地亮灭闪烁频率通常在38kHz左右人眼看不到但接收器能感知到。接收器前面有个透镜内置了光电二极管和解调芯片它会把收到的38kHz载波还原成高低电平也就是解调之后的数字信号。乐高Power Functions火车接收器使用的载波频率也是38kHz这一点和市面上大多数家电遥控器是一样的。所以后面的电路搭建、驱动方式都可以参考常见红外遥控器的做法。载波本身不能直接携带信息能携带信息的是载波存在的时间长短和出现的位置。协议的本质就是把0和1编码成一段段不同的“亮灭时间模式”这样接收器端就能翻译出你按下的是哪个键、要给哪个通道发什么命令。2.2 乐高PF到底用的是哪种编码网上很多资料会把乐高PF协议直接说成“RC5协议”这个说法大概正确但实际用起来会发现它和标准的飞利浦RC5编码有一些细节差异。更准确地说乐高PF的帧格式在结构上借鉴了RC5的思路但在起始位、数据位顺序和重复帧方面有自己的一套约定。RC5协议的核心特征是每一位都采用曼彻斯特编码bi-phase编码。曼彻斯特编码的精髓在于每个数据位中间必定有一次电平翻转电平是从低到高还是从高到低决定了这一位是0还是1。这样接收器就不需要精确测量每一段脉冲的绝对时长只要判断位中间的翻转方向就够了抗干扰能力很强。乐高PF在RC5基础上做了一些变化帧结构里除了起始位还要包含通道地址address和命令字command。通道地址决定了控制的是哪个通道、哪个输出口命令字则包含了停止、前进、后退以及速度等级等信息。2.3 地址、命令与重复帧三个最容易搞混的点第一个容易搞混的是地址。乐高PF接收器提供四个颜色通道红、蓝、绿、黄每个通道又分出A/B两个输出口所以总共可以寻址8个位置。在RC5编码里这部分信息被塞进5位地址中。我这里根据实测抓包和社区资料整理了一张常用地址表通道输出口A输出口B红色0x100x11蓝色0x120x13绿色0x140x15黄色0x160x17第二容易搞混的是命令字。6位命令字并不只是简单的“1代表前进2代表后退”它是方向、速度、停止控制的组合编码。一个比较常见的对应关系是命令值动作0x01前进速度10x02前进速度20x03前进速度30x04前进速度40x05前进速度50x06前进速度60x07前进速度70x08停止刹车0x09后退速度10x0A后退速度20x0B后退速度30x0C后退速度40x0D后退速度50x0E后退速度60x0F后退速度7第三容易搞混的是重复帧。乐高PF接收器在执行命令之前要求连续接收到两帧完全一致的数据。这个设计是为了防止误触发因为红外的散射和反射很容易产生残影信号。如果只发一帧接收器会当作干扰忽略掉。这也是为什么官方遥控器每按一次按键实际发送的红外信号会是“一帧、稍等、再一帧”的节奏。搞清楚这三个点协议层面的障碍基本就扫清了。接下来要做的就是动手把硬件搭起来。3. 硬件准备成本不到50块的零件清单与驱动电路3.1 主控选型Arduino Nano起步不用太纠结做这种红外模拟项目主控不需要很强的算力我选择Arduino Nano主要是因为三点价格便宜、资料多、足够完成38kHz载波输出。你手头有Arduino Uno或者兼容板也完全没问题引脚编号对应调整一下就行。如果你以后想接入WiFi或蓝牙也可以直接上ESP32但Part 1阶段没必要一上来就用ESP32因为ESP32的开发环境配置对新手来说多了一道门槛。先用最熟悉的环境把协议跑通后面再换平台损失也最小。主控的主要任务是两件事算法上把地址和命令字按照曼彻斯特编码展开成一个位序列物理上驱动红外发射管产生38kHz载波并且按照位序列控制载波的发出和停止。3.2 红外发射电路限流电阻、三极管与IR drop的计算红外发射电路是整个项目中最容易翻车的地方。很多人直接把人眼用的LED接到单片机GPIO上以为能亮就行结果火车一点反应都没有。原因很简单普通LED的光谱和红外LED完全不同而且红外LED的驱动电流需求更高GPIO口直接驱动往往力不从心。我的做法是加一个NPN三极管比如2N2222作为开关驱动红外LED。GPIO只负责给三极管基极提供开关信号真正的供电电流从5V电源经限流电阻流向红外LED和三极管。这样既能保护单片机又能提供足够大的电流。这里就要提到“IR drop”了。很多人看到这个词第一反应是芯片电源网络中的IR电压降也就是VIR那个IR。但在红外发射电路里它同样存在红外LED正常工作时会有约1.2V到1.5V的正向压降三极管饱和导通时还有约0.2V左右的Vce(sat)压降这两个电压都需要在限流电阻的计算里减掉。举个例子如果供电是5V红外LED正向压降取1.4V三极管饱和压降取0.2V那么电阻上的电压就是Vr 5 - 1.4 - 0.2 3.4V如果想让脉冲电流达到100mA左右红外LED可以承受短时间大电流但要看数据手册限流电阻就是R 3.4 / 0.1 34Ω取一个常见的33Ω电阻就可以。如果你用更小的电阻电流会更大发射距离更远但三极管和红外LED的发热也会更明显不建议一味加大。同时在红外LED旁边并联一个10Ω左右的电阻或者串一个小电阻来限制峰值电流也能减少对三极管的冲击。这个电路看起来简单但实测下来很多“发射距离特别短”的问题源头都在限流电阻算错。3.3 逻辑分析仪的用途不用也行但有它快10倍这是整个项目里我最推荐添置的工具。逻辑分析仪不需要很贵市面上几十块的24MHz/8通道版本就够用。它的作用不是实时调试而是把官方遥控器发出的红外信号完整记录下来然后你就能像看“波形病历”一样把每一位0和1都读出来。如果你没有逻辑分析仪也可以通过串口打印或者面包板上的LED灯来判断高/低电平但那种方式效率极低特别是当你想搞清楚“为什么火车没反应”的时候信号到底发成了什么样都不知道只能瞎猜。我手头用的是常见的USB逻辑分析仪配一个简单的软件界面抓取解调后的红外信号波形再把波形放大逐段分析整个过程非常直观。3.4 一辆测车用的乐高火车和接收器这一项反而容易被忽略。测试用车不需要多豪华只要有一个Power Functions火车接收器、一台电机和几节轨道的路轨就行。接收器需要先通过官方遥控器给它设定好通道比如红色通道后面所有自制的控制器都发送红色通道对应的地址这样才能对得上。如果你手上没有乐高官方火车只有PF马达也可以把PF马达接上PF接收器再用一套履带或者轮子做成简易小车不影响信号验证。核心是接收器要能收到38kHz的红外信号并驱动马达转动。4. 逆向官方遥控器用逻辑分析仪把波形“翻译”成人话4.1 接线方法红外接收头怎么和逻辑分析仪连这一步的核心是抓取官方遥控器发出的信号。直接用逻辑分析仪去抓红外LED的原始光信号会比较麻烦因为38kHz载波会占用大量采样点分析起来很费劲。更聪明的办法是先用一个一体化的红外接收头比如TSOP38238把光信号解调成普通的数字电平信号。TSOP38238有三只脚电源、地、输出。把它接到面包板上输出脚接到逻辑分析仪的通道0然后按下官方遥控器任意按键就能在逻辑分析仪上看到一段解调后的数字波形。要注意的是TSOP38238的输出极性和红外LED的原始信号是反相的。有载波时输出低电平没有载波时输出高电平。这点在看波形的时候一定要心里有数否则很容易把0和1搞反。4.2 按键抓波一次按下能看到什么当我第一次在逻辑分析仪上看到波形时最大的感受是以为是零散的一段红外信号实际上接收器看到的是非常整齐的两帧重复帧。按一次遥控器按键逻辑分析仪上会看到两段几乎相同的波形中间有一段约几毫秒的间隔。这就是前面说的重复帧机制。每一帧长度在二十几毫秒左右帧内由若干个等宽的位时间组成每个位时间内有一次明显的电平翻转。我按的是官方遥控器上的“前进”键当时遥控器通道拨在红色输出口拨在A。于是这一帧里应该包含红色A通道的地址和前进的命令字。这个预期在后面逐段解读时完全对得上。4.3 波形逐段解读从起始位到命令位把波形放大之后就轮到RC5帧结构的分析上场了。一帧数据大约由以下部分组成起始位一个特殊的高电平脉冲加低电平对用来告诉接收器“我要开始发数据了”。切换位toggle bit这个位会在每次按键状态变化时翻转作用是让接收器区分“连续按同一个键”和“按住不放”。地址位共5位高位在前对应通道和输出口。命令位共6位高位在前对应具体的控制命令。曼彻斯特编码的判读规则是看每个位时间的中间位置如果电平从低变成高这一位就是1如果电平从高变成低这一位就是0。有些资料会说“前半周期为低、后半周期为高代表1”本质是一回事。我把抓到的二进制位拆出来对照官方遥控器的通道拨码地址位和前面那张表完全吻合命令位也和“前进速度4”的预期一致。到这里协议分析基本就闭环了官方遥控器发出来的不是神秘的天书而是一段可解读、可复现的数字序列。4.4 实测中反复踩的协议坑这个过程中我踩过几个坑印象很深。第一个坑是极性反相。一开始我没注意TSOP38238输出是反相的直接按照“高电平是1”去读波形结果读出来的命令字完全不对。后来把波形反转再读数据一下就正常了。第二个坑是地址位和命令位之间的边界。RC5帧里地址和命令都是等宽的位时间如果不提前知道地址是5位、命令是6位很容易数错位数。我的解决办法是先通过已知命令字反推地址边界比如按“前进”时命令字是固定的剩下部分自然就是地址。第三个坑是连发帧的间隔。接收器对两帧之间的间隔有一定的容错范围太短会把两帧当成一帧太长又会判定为超时。我后来在程序里把两帧间隔设置在10ms左右实测很稳定。5. 写一个能用的发射程序从曼彻斯特编码到跑起火车5.1 最小发射函数载波PWM与delayMicroseconds写代码之前先明确一下单片机要怎么产生38kHz载波。最简单的方法是用一个while循环在极短的时间内把GPIO拉高、拉低形成方波。下面这段代码就是我在项目里用的最小载波函数#define IR_PIN 3 void sendCarrier(int us) { unsigned long start micros(); while (micros() - start us) { digitalWrite(IR_PIN, HIGH); delayMicroseconds(8); digitalWrite(IR_PIN, LOW); delayMicroseconds(8); } }这里delayMicroseconds(8)配合GPIO翻转的额外耗时实际产生的方波频率大概在36kHz到40kHz之间正好落在接收器可接受的范围。这个方法虽然在时序上不完美但胜在简单直观每一段载波的时长也是由参数us精确控制的非常符合曼彻斯特编码的调试需求。如果你后续想做得更精细可以用定时器硬件PWM来生成38kHz但那是优化方向Part 1先用这个方案完全没问题。5.2 曼彻斯特编码与帧组装有了载波函数接下来就是把bit序列变成红外的亮灭模式。曼彻斯特编码的每一位时间大约为1.778ms分前后两半。逻辑1的前半段是空闲无载波后半段是载波逻辑0则相反。代码可以这么写void sendBit(int bit) { if (bit) { delayMicroseconds(889); sendCarrier(889); } else { sendCarrier(889); delayMicroseconds(889); } }889是1.778ms的一半单位是微秒。需要说明的是delayMicroseconds在循环调用多次之后会积累一点误差但接收端对RC5的位时间容差大约在正负10%左右这个误差完全在容忍范围内。我实测下来连续发几十帧都没有丢码。帧组装就按之前协议的规则来。先发起始位然后发切换位再依次发5位地址和6位命令。起始位比较特殊它是“载波889us、空闲889us”然后立刻进入数据位void sendFrame(int toggle, int address, int command) { sendCarrier(889); delayMicroseconds(889); sendBit(toggle); for (int mask 0x10; mask; mask 1) { sendBit((address mask) ? 1 : 0); } for (int mask 0x20; mask; mask 1) { sendBit((command mask) ? 1 : 0); } }地址和命令都按MSB first的次序发送循环里用掩码从高位往低位扫。5.3 主程序设计前进、加速、停止的完整演示有了帧发送函数主程序就变得很直白。我设计了一个演示逻辑上电后给火车发送“前进速度4”的命令三秒后发送“前进速度7”再过三秒发送“停止”。每次发送都重复两帧确保接收器能正确识别。代码如下int toggle 0; void sendPFCommand(int address, int command) { for (int i 0; i 2; i) { sendFrame(toggle, address, command); toggle 1 - toggle; delay(10); } } void setup() { pinMode(IR_PIN, OUTPUT); digitalWrite(IR_PIN, LOW); // 红色通道A输出口地址0x10 sendPFCommand(0x10, 0x04); // 前进速度4 } void loop() { delay(3000); sendPFCommand(0x10, 0x07); // 前进速度7 delay(3000); sendPFCommand(0x10, 0x08); // 刹车停止 delay(1000); }这里toggle位并不是严格意义上的按键翻转我把它定义为每次发送后翻转一次模拟遥控器切换状态的效果。实际上乐高PF接收器对toggle位的要求不如标准RC5严格但加上它不会出错。5.4 上真车验证如果没反应检查顺序把程序烧录进Arduino Nano之后红外发射管对向乐高火车接收器如果一切正常火车应该先以速度4起步然后加速最后刹停。但在你满怀期待地打开火车电源之前我建议按下面这个顺序自查先确认红外发射管真的在发光。用手机摄像头对准红外LED按下遥控器或者运行程序时手机屏幕里应该能看到紫色的光点。如果看不到说明电路或者代码还没进入发送流程。确认接收器的通道设置。你的乐高接收器拨码是否在红色通道A如果拨在蓝色那发送红色地址当然没反应。确认发射距离和角度。红外接收头一般有个指向性发射管最好正对接收器前方的透镜距离控制在半米以内。如果还是不行回到逻辑分析仪看看程序发出的波形是不是和官方遥控器抓到的波形基本一致。我第一次上真车测试时就是因为红外LED没有加限流电阻直接把单片机引脚拉垮了导致信号完全发不出去。后来换成三极管驱动电路问题立刻消失。6. 常见故障排查我花了三个晚上才搞定的几个问题6.1 发射距离不到20cm检查驱动电流和波形占空比这是个典型问题。明明红外LED在手机摄像头里看着闪得很欢但火车就是只能贴近了才响应。原因通常是两种一是驱动电流不够红外LED输出光功率太低二是载波占空比不对接收器灵敏度下降。先说电流。我算过如果GPIO直接驱动输出电流最多也就20mA左右这在近距离还行远了就白搭。用三极管驱动之后把脉冲电流提到80到100mA实测把发射管对准接收器两米之内都能稳定触发。再说占空比。理想情况下38kHz载波的占空比接近50%也就是高低电平时间大致相等。如果程序里高低电平时间偏移太大接收器内部的带通滤波器会衰减信号。用逻辑分析仪抓发射管输出端波形就能看到占空比情况。我调了delayMicroseconds的延时参数让高低电平尽量平衡效果立竿见影。6.2 火车偶尔响应慢半拍连续两帧的间隔问题如果火车能动但总是“偶尔不听话”有时候按一下没反应下一秒又自己动起来十有八九是两帧之间的间隔不对。乐高PF接收器要求在很短的时间内连续收到两帧一致的数据这个间隔如果太短接收器可能还在处理前一帧的尾部数据新帧就被吞掉了如果太长接收器会把两帧当成两个独立的误触发信号。我实测下来帧间隔在8ms到15ms之间最稳程序中用的delay(10)就是多次调试之后的结果。另外还要注意两帧之间的toggle位是否应该保持一致。标准RC5里同一按键状态下的两帧toggle位应该是相同的因为切换位只在按键状态改变时翻转。我的代码里为了让两帧“完全一致”发送两帧时用的其实是同一个toggle值也就是发送前记录一下两帧都发同一个值发完再翻转。上面那段代码中我是在sendPFCommand内部发两帧但toggle在每次循环之后才翻转效果是对的。6.3 频道A/B总是串地址位的组合问题有朋友用我的代码去控制自己的车结果车没动另一台车反而动了一下。这种情况多半是地址位搞混了。乐高接收器每个通道有A/B两个输出口地址从0x10到0x17。如果你把地址发成了相邻通道或者相反的输出口接收器收到的就是另一台车的控制信号。我在前面那张地址表里写得很清楚了大家在代码里改地址时一定要对照自己的拨码位置。另外乐高接收器上有时候会有拨码开关来设置通道不同批次的产品拨码标识略有差异有的是颜色贴纸有的是数字。如果你在逻辑分析仪上抓到的地址和网上资料对不上一切以抓包得到的数据为准不要迷信表格。6.4 给“1号火车”和“2号火车”独立控制的思路虽然Part 1只做了单通道控制但地址表已经为多车控制铺好了路。想分开控制两辆火车只需要给它们分配不同的地址然后在程序里定义两个“通道句柄”。void controlTrain(int addr, int speedCmd) { sendPFCommand(addr, speedCmd); } // 控制一号车红色A地址0x10 controlTrain(0x10, 0x04); // 控制二号车蓝色A地址0x12 controlTrain(0x12, 0x04);这个思路在Part 2会有更多演进的玩法——比如在代码里维护一个“列车列表”循环调度不同的地址实现对多列火车的定时发车、自动停车。但现在先确保单列火车能被你的代码稳稳控制住这是所有后续功能的地基。