前阵子整理移动硬盘翻出了我大学时做的那套51单片机红外遥控设计资料烧录文件、Keil工程、原理图快照、调试照片都还在。重温一遍发觉这个项目虽然老但简直是把单片机外设榨干的一个经典定时器、外部中断、串口、GPIO全部用上了还捎带手把NEC协议这种工业级通信格式给搞明白了。这篇文章就专门复盘一下这个项目从方案拆解、硬件电路、软件解码到联调踩坑把能落地的细节一次讲透。如果你是刚接触51单片机、想找一个能串起整个知识体系的项目或者已经写完流水灯、数码管想往上走一步这个红外遥控课题都是非常合适的中间站。它不像WiFi模块那种“黑盒”方案所有通信波形都是自己用定时器一笔一画生成的做完之后你对时序、中断、协议的理解会扎实很多。下面按我实际开发的顺序来聊。1. 项目整体方案拆解这个经典课题到底在解决什么问题1.1 红外遥控为什么能“隔空传信”红外遥控本质上是“红外光调制”两个概念的组合。红外光就是波长大约940nm的近红外光人眼看不见方向性强、穿墙能力弱正好适合家用遥控这种几米到十几米的直视通信场景。但光本身不能直接传数据还得靠调制——就像一个人拿着手电筒在夜空中按灯光的明暗长短发摩斯电码红外遥控就是把0和1用特定时长的“亮”和“灭”来编码最后统一挂到38kHz的载波上发射出去。为什么非要用38kHz而不是直接通直流电因为一体化接收头里有带通滤波器只对38kHz附近的信号敏感。如果直接用直流点亮红外发射管接收头会把环境光太阳光、白炽灯、节能灯一并收进来数据就没法看了。所以发射端真正送出去的是“38kHz的载波脉冲串”接收端只挑这个频率的信号放大、解调再把原始的电平脉冲剥出来交给单片机。这一步和收音机选频的原理很像。1.2 整体方案选型系统由哪几块组成这个项目我拆成两个独立子系统来处理发射端按键输入 51单片机 38kHz载波生成 红外发射管驱动接收端一体化红外接收头 51单片机解码 显示/执行单元LED、数码管、继电器都可以为什么选51单片机因为片上自带的定时器、外部中断、串口、GPIO在这个项目里全都能派上用场基本不需要额外外设。NEC解码用“外部中断定时器计时”就能实现发射用“定时器中断IO翻转”就能生成载波串口还能把解码结果打印出来验证一套流程下来单片机的主要外设就被通通摸过一遍。这对初学者来说是性价比极高的练手方式。2. 硬件电路设计与元件选型要点2.1 关键器件对比与选择主控我用的是STC89C52RC配12MHz晶振12T模式下一个机器周期正好是1us做微秒级延时非常舒服。这里有个选型提醒如果项目里没有串口精确波特率的需求红外遥控优先选12MHz。11.0592MHz适合串口通信但做38kHz载波时序时要跟小数点较劲没必要自找麻烦。一体化接收头我选了VS1838B跟HS0038B基本兼容三只脚分别是VCC、GND、OUT内部把接收、放大、解调、整形全包了不需要再搭放大电路。这个元件有个必须记住的特性输出是反相的。无信号时OUT输出高电平收到载波时输出低电平。也就是说发射端发出“高脉冲”时接收头OUT输出的是“低脉冲”。这个特性直接影响后面的解码逻辑很多人第一次调通才发现数据全反了。发射部分用一个5mm红外发射管加上一只S8550三极管做开关驱动。单片机的IO口驱动能力有限直接点亮红外管亮度不足、距离也近加了三极管之后发射距离很容易做到8米以上。限流电阻一般取20到30欧姆把峰值电流控制在50到100mA。不要为了追求距离把电流调得太大红外发射管长时间超额定电流工作会加速老化。2.2 电路连接与PCB布局避坑发射端电路很简洁P1.0接一个IO口到三极管基极基极串1k电阻限流发射管正极接5V负极通过限流电阻接到三极管集电极三极管发射极接地。注意S8550是PNP管基极拉低时导通如果换NPN管比如8050逻辑要反过来。我个人习惯用PNP管因为单片机复位期间IO口默认高电平PNP管会导通但这只是瞬时状态靠软件初始化拉高就能解决。接收端电路更重要VS1838B的OUT脚直接接P3.2也就是INT0外部中断引脚VCC和GND之间必须就近放一个0.1uF的去耦电容否则电源纹波稍大一点接收距离就明显缩水。很多人在面包板上搭电路时接收头离电源和单片机都很近结果解码时好时坏最后发现是电源噪声干扰。接收头的位置尽量靠近板边方便对准遥控器也尽量远离晶振和继电器这类干扰源。PCB布局时还有一点值得注意红外发射管和接收头不要靠得太近否则发射管的高亮信号会直接“打亮”接收头形成自激干扰。如果做的是收发一体的板子两者之间最好留出物理隔离或者用黑色热缩管套住接收头只留正面进光窗口。我的经验是发射管和接收头夹角小于30度且距离小于5cm时自激概率很高务必避开。2.3 载波频率的定时器计算38kHz怎么来NEC协议规定载波频率是38kHz周期约26.3us占空比通常为1/3或1/2。我用单片机IO翻转来生成载波也就是说每隔半个周期翻转一次IO半周期就是13.16us。12MHz晶振、12T模式下机器周期 12 / 12MHz 1us定时器每计1个数就是1us。13us的翻转周期最接近理论值。以定时器0工作在8位自动重装模式为例初值 256 - 13 243换算成十六进制就是0xF3。每次定时器溢出进入中断翻转一次发射IO同时重装初值。参数值计算依据目标载波频率38kHzNEC协议标准载波周期26.3us1 / 38kHz半周期13.15us周期/2实际翻转间隔13us取整定时器初值0xF3256 - 13实际载波38.46kHz1 / (26us)实际载波38.46kHz和理论值偏差约1.2%完全在一体化接收头的带宽范围里接收头通常允许正负2kHz左右的偏差。我实测下来12MHz方案接收稳定度比11.0592MHz方案高不少就是这个原因。如果给的是6MHz晶振半周期变成26us载波只有19.2kHz可能会超出部分接收头的带宽需要改成软件分频方案尽量别用低频晶振。3. 软件实现NEC协议解析与51单片机编码实战3.1 NEC协议格式逐位拆解NEC协议是红外遥控里最常见的帧格式很多家电遥控器都在用。它的帧结构固定为引导码 4字节数据数据按LSB先发。完整帧的32位数据分成四段地址码8位、地址反码8位、命令码8位、命令反码8位。反码的意义在于校验如果硬件解码得到的地址码和地址反码不一致说明这帧数据被干扰或者解码有误可以直接丢弃能有效防止误触发。这个设计在现在很多无线协议里也能看到是低成本自校验的经典手法。时间参数上引导码是9ms载波加4.5ms低电平用来告诉接收端“我要开始发数据了”。逻辑1是560us载波加1690us低电平逻辑0是560us载波加560us低电平。注意逻辑1和逻辑0的区别在于低电平时间而不是高电平时间这一点对解码程序设计至关重要。还有一种重复码只有9ms载波加2.25ms低电平加560us载波如果遥控器一直按住按键就靠重复码告诉接收端“按键没松开”。3.2 发射端代码定时器中断生成38kHz载波发射端软件分两部分载波生成和时序编码。载波生成我用定时器0中断每次中断翻转一次发射IO口就能得到连续的38kHz方波// 定时器0中断服务函数8位自动重装模式 void timer0_isr() interrupt 1 { IR_TX !IR_TX; // 翻转发射管IO TH0 0xF3; // 重装初值 TL0 0xF3; }初始化也很简单void Carrier_Init(void) { TMOD 0xF0; // 保持定时器1配置不变 TMOD | 0x02; // 定时器08位自动重装模式 TH0 0xF3; TL0 0xF3; ET0 1; // 开启定时器0中断 EA 1; }发送一个位的逻辑是先启动定时器让载波持续560us然后关掉定时器并强制IO置低再根据是逻辑1还是逻辑0延时不同的低电平时间。这里有个容易犯的错关定时器之后必须立刻把IO拉低否则IO会停在最后一个翻转状态可能是高电平那“无载波”的低电平时段就变成高电平了。void SendNecBit(unsigned char bit_val) { TR0 1; // 开载波 DelayUs(560); // 载波持续560us TR0 0; // 关载波 IR_TX 0; // 强制拉低 if (bit_val) { DelayUs(1690); // 逻辑1低电平1690us } else { DelayUs(560); // 逻辑0低电平560us } }DelayUs这类微秒延时函数如果用普通循环写必须确认反汇编后每条语句的实际周期不要在Debug模式下肉眼估算。我常用的做法是用示波器标定一次或者在循环体里放一条空汇编指令作为基准再多复制几遍凑微秒。嫌麻烦的话也可以直接用定时器0做“单次计时”来延时精度更可靠代价是占用了定时器资源。发射端本来就要用定时器0所以延时我直接用一个独立的小延时函数配合示波器标定够用。发送完整命令时按序发送引导码、地址字节和命令字节void SendNecFrame(unsigned char addr, unsigned char cmd) { // 引导码9ms载波 4.5ms低电平 TR0 1; DelayUs(9000); TR0 0; IR_TX 0; DelayUs(4500); // 发送地址、地址反码、命令、命令反码 SendNecByte(addr); SendNecByte(~addr); SendNecByte(cmd); SendNecByte(~cmd); // 结束停止位确保最后也是低电平 SendNecBit(0); }SendNecByte就是按LSB顺序逐位调用SendNecBitvoid SendNecByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { SendNecBit(dat 0x01); // 取最低位 dat 1; // 右移切换下一位 } }这段代码用按键扫描触发按键按下去就发一次帧松开再按再发。如果希望持续按住时不断重复可以在主循环里检测按键按下后每隔100ms左右发一帧重复码。这样其他设备也能收到“按住”的语义比如音量连续调节。3.3 接收端代码外部中断加定时器解码接收端是整个项目最有意思的部分。因为一体化接收头空闲时输出高电平收到信号时输出低电平所以接收头的OUT接P3.2利用INT0下降沿触发中断。中断触发一次就说明接收头收到了一段脉冲的边缘。解码的核心思想是每次进入中断时读取定时器的当前计数值算出与上一次中断的时间间隔再根据时间间隔判断刚才收到的是引导码、逻辑1还是逻辑0。用定时器0工作在16位模式持续计数每次中断时把计数值清零重新开始。初始化代码void Decoder_Init(void) { TMOD 0x0F; TMOD | 0x01; // 定时器016位手动清零模式 TH0 0; TL0 0; TR0 1; // 启动计数 IT0 1; // INT0下降沿触发 EX0 1; EA 1; }中断服务函数按时间阈值判断unsigned char rx_buf[4]; unsigned char rx_cnt; unsigned char rx_ok; void int0_isr() interrupt 0 { unsigned int dt; unsigned char i; dt TH0; dt (dt 8) | TL0; // 读取当前计数值 TH0 0; TL0 0; // 清零重新计时 if ((dt 12000) (dt 16000)) { // 引导码9ms载波 4.5ms低电平约13.5ms // 由于反相和脉冲边沿差异用一个宽范围 rx_cnt 0; // 收到引导码开始接收数据 } else if ((dt 2000) (dt 2800)) { // 逻辑1560us载波 1690us低电平约2.25ms i rx_cnt 3; rx_buf[i] | (unsigned char)(1 (rx_cnt 0x07)); rx_cnt; if (rx_cnt 32) rx_ok 1; } else if ((dt 800) (dt 1600)) { // 逻辑0560us载波 560us低电平约1.12ms i rx_cnt 3; rx_buf[i] ~(unsigned char)(1 (rx_cnt 0x07)); rx_cnt; if (rx_cnt 32) rx_ok 1; } else { // 其他时间判定为干扰重新同步 rx_cnt 0; } }这个实现只用了三个判断分支看起来简单但已经能稳定解码绝大多数NEC遥控器。需要留意的是阈值范围不能太靠近理论值因为不同遥控器晶振可能有偏差实测时间会在理论值上下浮动。我的经验引导码取12000到16000us逻辑1取2000到2800us逻辑0取800到1600us留足了余量。如果你解码的是某品牌原装遥控器可以先发一帧通过串口打印每次中断的时间间隔再微调这几个窗口。收到32位后主程序里检查rx_ok标志再把rx_buf里的地址码和地址反码做校验。如果校验通过取出命令码执行对应动作比如点亮某个LED或者切换继电器。完整的主循环大概长这样void main(void) { unsigned char addr, cmd; Decoder_Init(); Uart_Init(); // 初始化串口 while (1) { if (rx_ok) { rx_ok 0; addr rx_buf[0]; cmd rx_buf[2]; if (addr (unsigned char)(~rx_buf[1])) { // 地址校验通过执行命令 printf(CMD:%02X\r\n, cmd); } } } }4. 联调实录与波形验证4.1 接线与基础点测焊接完第一版板子不要急着烧程序先做基础点测。三用表量一下VCC和GND之间有没有短路再确认晶振两脚对地电压正常应该是电源电压的一半左右12MHz起振后。接收头OUT脚在没有信号时应该读到高电平如果读到低电平要么接收头焊反了要么被异常信号压住。发射管两端反向测一下压降正常红外管压降在1.2V左右如果测出来是普通二极管的0.6V多半买错了。烧录程序后用USB转TTL模块接串口把接收端的解码结果打印出来这一步是调试的“眼睛”。一开始不要急着遥控接收先直接发一帧完整的NEC波形到接收头输入端看串口是否打印出预期的地址和命令。这样能把通信问题隔离到发射端或接收端单侧。4.2 用手机摄像头观察发射波形红外光是看不见的但有个土办法非常有效手机摄像头。现在大多数手机摄像头没有红外截止滤光片把它对准正在闪烁的红外发射管屏幕里能看到淡紫色或白色的光点。如果发射管在闪说明载波和时序逻辑在跑。如果没有光先查IO口配置和定时器有没有启动。这个方法不花钱基本能完成“发射端有没有在工作”这一级检查。想看得更细就用逻辑分析仪。把探头夹在发射管负极或接收头OUT脚上采样率设到1MHz以上按下按键就能看到完整的NEC波形。正常波形应该是一段密集的载波脉冲引导码9ms接着一段4.5ms低电平然后是一串宽度不同的脉冲。如果看到载波段明显断断续续检查定时器初值是否重装失败如果载波段长度不对检查延时函数是否为整数量级偏差。接收头的OUT脚波形是反相放大的脉冲串和发射端波形正好高低对调。这个反相特性在逻辑分析仪上看得一清二楚看到两个波形高低对调时不要慌这正是正确工作状态。4.3 接收解码验证串口打印数据帧我调试时习惯用串口打印完整解码过程而不只打印最终命令。具体做法是在中断服务函数里把dt值压到一个缓冲区主循环统一打印。比如打印一帧结束后每个间隔占多少毫秒再用我自己的判断窗口比对一遍。这个习惯帮我发现过很多奇怪问题。最有意思的一次测试是拿着某品牌家电的原装遥控器按按键接收端串口打印出地址码和命令码我再用这个解码值构建发射程序拿51单片机发射端去控制同样的设备居然直接成功了。这说明我的NEC解码协议理解没有偏差也侧面验证了这套资料的可移植性。如果解码某个遥控器不成功先打印原始时间间隔大概率会发现它的引导码长度或某个位的时序与标准NEC有差异这时候调整判断窗口就能解决。5. 常见问题排查与实用避坑经验5.1 问题速查表现象可能原因排查与解决发射管不发光IO口未配置成推挽输出检查P1口初始化必要时设P1M0/P1M1为推挽发射距离只有几十厘米三极管接法错误或限流电阻过大用示波器看发射管两端波形确认电流足够接收头OUT始终低电平电源干扰或晶振靠近接收头加0.1uF去耦改变布局缩短走线解码逻辑1和逻辑0串位判断窗口和实际时间偏移打印实际dt值重新调整窗口范围按下按键收到重复码重复码未单独处理在中断里增加重复码分支或判断特殊时间遥控器按一次收到两帧接收帧结束判定过早检查rx_ok置位时机确保32位完整接收串口打印乱码波特率计算错误用11.0592MHz晶振或调整波特率寄存器初值5.2 一些容易忽略的实操心得第一个心得是“解码窗口宁宽勿窄”。我之前总想把时间阈值卡在理论值附近觉得那样最精确。结果不同牌子的遥控器晶振偏差一累加好多原装遥控器解码失败。后来放宽到理论值正负20%左右市面上绝大多数NEC协议的遥控器都能解出来。判断窗口不是越精确越好而是越大越好只要不会把不同类型错判成同一个就行。第二个心得是“发射时定时器中断优先级要慎重”。如果项目里同时启用了串口中断和定时器中断发射载波期间如果被串口中断打断哪怕只是几微秒也可能导致载波段长度失真。我的做法是发射期间EA 0暂时关总中断依赖延时函数递推完成时序等发射完再重新开中断。这样牺牲一点点CPU实时性换来载波波形稳定特别适合这种“发广播式”场景。第三个心得是关于“按键去抖和NEC重复码配合”。如果主循环里既做按键扫描又做NEC发送按下按键时触点抖动会触发多次读键导致同一帧发好几遍。我的处理是不做短去抖而是检测到按键按下后发帧然后等待按键释放释放后再做一次长延时从根源上避免重复触发。原装遥控器长按发重复码的语义如果应用不需要也可以直接忽略重复码只识别完整帧能减少很多判断分支。第四个心得最重要一定要给接收端解码加同步机制。我看过很多入门代码只判断“逻辑1”和“逻辑0”没处理引导码结果一上电遇到环境光干扰就乱码。正确处理方式就是我在中断里那样先把dt判断成“引导码”才会清零rx_cnt否则帧中途进来信号一律丢弃。这个设计看似简单实际能挡住绝大多数误帧是整个接收端稳定性的基石。最后再分享一个小技巧调试时如果手头没有原装遥控器可以直接用另一个51开发板跑发射程序把两个板子对接调试。发射板按一次按键接收板串口打印一帧两边代码都是自己写的出了问题就能定位到具体函数。等收发链路都通了再拿真实家电遥控器来检验兼容性成功率会高得多。红外遥控这项目做下来我最大的体会是“协议比代码重要”。一开始我纠结于怎么写中断、怎么算延时后来才发现真正决定成败的是对NEC协议每一个时间参数的理解。把这层窗户纸捅破之后换协议、换单片机、换任何平台都只是工程问题。这个项目现在看起来技术含量不高但它逼着我养成了看时序、看波形的习惯后来做编码器、做无线模块都受益于此。如果你也打算拿这个项目练手建议从模块化入手发射程序一个文件、接收程序一个文件、应用层一个文件以后想扩展成万能遥控器或者学习型遥控器直接在对应层改代码就行不用推倒重来。
阅读完成 · 觉得有帮助?