
1. 项目概述为什么选择红外遥控与ATtiny在嵌入式项目里给设备加个遥控功能听起来挺酷但很多朋友一上手就被各种复杂的库和协议搞懵了。红外遥控特别是基于NEC协议的那种其实是个非常成熟、成本极低的无线控制方案。你家里电视、空调的遥控器十有八九用的就是它。这个项目的核心就是抛开那些庞大臃肿的通用库用最“裸”的方式在ATtiny这类小巧、便宜的微控制器上实现一套从发射到接收的完整红外遥控系统。我选择ATtiny85/84芯片原因很简单便宜、省电、管脚够用。对于大多数只需要控制几个开关、调节几个档位的个人项目比如桌面小风扇、智能灯、模型车一个几块钱的ATtiny85完全能胜任没必要上Arduino Uno甚至ESP32。整个方案的精髓在于“够用就好”——用最少的代码、最简单的电路实现最可靠的功能。接下来我会拆解NEC协议到底是怎么工作的然后手把手带你搭建发射器和接收器最后分享几个我踩过坑才总结出来的调试技巧和优化方案。2. NEC红外协议深度解析不只是0和1很多教程只告诉你NEC协议用38kHz载波逻辑0是560us脉冲加560us间隔逻辑1是560us脉冲加1680us间隔。但这只是表象真正要自己写代码实现必须理解其背后的时序逻辑和容错机制。2.1 协议帧结构起手式与数据体一个完整的NEC协议数据帧远不止32位数据那么简单。它是一套严谨的“对话”规则引导码一个9ms的持续高电平脉冲载波开启紧接着一个4.5ms的低电平间隔载波关闭。这个独特的“长-长”组合是接收端识别一帧数据开始的唯一标志。任何干扰脉冲只要不符合这个时长比例都会被硬件接收头过滤掉。用户码与命令码紧随其后的是32位数据。最初的标准NEC格式是8位地址码 8位地址反码 8位命令码 8位命令反码。这种“原码反码”的校验方式非常简单有效接收端可以通过对比快速判断数据在传输中是否发生了单比特错误。后来为了扩展地址空间演变成了16位地址码前两个字节加8位命令码和8位命令反码。结束位与连发码在单次按键后会有一个560us的脉冲作为结束位。如果用户持续按住按键则不再发送完整帧而是发送一个特殊的“重复码”通常是一个9ms高电平加2.25ms低电平再接一个560us的脉冲。这个机制是为了节省带宽和功耗。注意市面上很多家电特别是国产的使用的是NEC协议的变种。它们可能修改了引导码的时长、载波频率比如40kHz或者完全自定义了32位数据的含义。在解码时第一步应该是用“解码器”工具下文会做抓取真实遥控器的波形而不是死磕标准参数。2.2 38kHz载波的奥秘为什么是这个频率38kHz不是一个随意选择的数字。红外接收头如VS1838B、HS0038内部有一个带通滤波器其中心频率就设计在38kHz左右。这意味着只有当入射的红外光是以约38kHz的频率明暗闪烁时接收头才会输出低电平如果是稳定的红外光或者频率偏差太大接收头会维持高电平输出。这种设计带来了巨大优势抗干扰。日常环境中的太阳光、白炽灯、LED灯都含有红外成分但它们是稳定或低频变化的。38kHz的载波就像给我们的指令数据加了一把唯一的“声音锁”只有接收头这把“锁”能解开极大避免了误触发。在发射端我们需要用微控制器产生一个占空比约为1/3的38kHz方波高电平约8-9us低电平约17-18us并用这个方波去“门控”即开关红外LED的电流。数据中的“脉冲”实际上是让LED在这560us期间以38kHz的频率闪烁而“间隔”期间则完全关闭LED。3. 硬件设计与选型平衡距离、功耗与成本3.1 红外发射器电路如何让遥控距离更远发射电路的核心目标是用有限的电源通常是两节AAA电池或一颗3.7V锂电驱动红外LED发出足够强的、被38kHz调制过的光信号。基础电路分析我采用的经典电路是ATtiny的PWM引脚 - 基极电阻R21kΩ - NPN三极管如2N3904 - 红外LED串联限流电阻R1到电源。三极管的作用ATtiny的IO引脚驱动能力有限通常20mA左右无法直接让红外LED产生足够的峰值电流。三极管在这里作为开关管当PWM引脚输出高电平时导通让电池的电流直接流过LED电流大小由R1决定。限流电阻R1的计算这是决定发射功率和距离的关键。红外LED的正向压降Vf通常在1.2V-1.4V。假设使用一颗满电电压4.2V的14500锂电池三极管饱和压降Vce_sat约为0.2V。 期望的LED峰值电流I_led设为100mA这是一个在脉冲工作下较安全的强驱动值。 计算公式R1 (V_battery - V_led - Vce_sat) / I_led代入R1 (4.2V - 1.3V - 0.2V) / 0.1A 2.7V / 0.1A 27Ω电阻功率P I² * R (0.1)² * 27 0.27W因此应选择至少0.5W1/2W的电阻。我的实测经验与优化电阻功率与散热最初我用了一个1/4W的27Ω电阻在连续测试发射时烫得厉害不久就烧毁了。后来换用两个56Ω的1/4W电阻并联等效28Ω但总功率承受能力变为0.5W问题解决。教训脉冲电流下的功率计算不能只看平均值必须留足余量。LED选择与角度普通的5mm红外LED如TSAL6200其发射角度较宽约±40度能量不集中。如果追求指向性和更远距离应选择发射角度更小的型号如±20度。我在一个需要10米以上距离的项目中换用了OSRAM的SFH4546窄角度LED效果立竿见影。电源退耦在电池靠近电路的位置并联一个100μF的电解电容和一个100nF的陶瓷电容这对于维持发射大电流脉冲时的电压稳定至关重要能有效避免因电压瞬间跌落导致的单片机复位。3.2 红外接收器电路简单但易踩坑接收电路极其简单红外接收头三个脚VCC接5VGND接地OUTPUT接ATtiny或Arduino的任意数字IO口。但简单的地方最容易出问题。接收头选型要点确认是38kHz购买时务必确认型号支持38kHz载波解调。HS0038、VS1838、TL1838等都是常见型号。注意引脚顺序不同封装直立、贴片和不同厂家的接收头其VCC、GND、OUT三个引脚的顺序可能完全不同最稳妥的方法是上电前用万用表测量找出VCC和GND引脚通常接收头内部有电源标志或者查阅具体型号的数据手册。供电电压多数接收头工作电压范围是2.7V-5.5V与ATtiny和Arduino兼容。但务必确保电压稳定噪声过大会导致接收灵敏度下降。一个关键的硬件技巧在接收头的OUTPUT引脚和单片机IO口之间串联一个100Ω-470Ω的电阻。这个电阻与单片机IO口内部的保护二极管构成一个简单的限流和抗扰电路可以有效抑制因长导线引入的静电或噪声毛刺保护单片机引脚。我在多次户外项目应用中加上这个电阻后随机误触发的现象基本消失。4. 软件实现ATtiny上的极简代码哲学在资源受限的ATtiny85只有8KB Flash512B RAM上编程必须精打细算。我们的目标是写出不依赖millis()、micros()等可能带来时序不确定性的函数完全通过寄存器操作和精准延时来实现协议。4.1 发射端代码精讲精准的硬件PWM与门控ATtiny85的Timer0和Timer1可以产生硬件PWM。我们使用Timer1因为它可以输出频率更稳定的PWM。核心设置步骤配置38kHz PWM将Timer1设置为快速PWM模式工作在约38kHz频率占空比50%。这通过配置TCCR1和GTCCR寄存器实现。代码中会关闭溢出中断我们只利用其硬件比较输出功能。实现“门控”PWM波形会持续产生。我们的“门控”操作实际上是通过动态改变对应引脚如PB1即OC1A的数据方向寄存器DDRB来实现的。当DDRB的对应位设为1输出模式时PWM波形就输出到引脚驱动三极管当设为0输入模式且内部上拉关闭时引脚呈高阻态PWM波形被“阻断”三极管关闭。这种方法比用软件控制引脚高低电平要干净、精准得多。用delayMicroseconds()构建数据帧在发送引导码、数据位时我们通过delayMicroseconds()来精确控制“门”打开输出PWM和关闭高阻态的时长。虽然delayMicroseconds()在中断禁用时最准但在我们这个简单程序中影响微乎其微。关键代码片段与解析// 定义关键时长微秒 #define NEC_HDR_MARK 9000 #define NEC_HDR_SPACE 4500 #define NEC_BIT_MARK 560 #define NEC_ONE_SPACE 1680 #define NEC_ZERO_SPACE 560 void sendNECBit(bool bitVal) { // 发送560us的脉冲载波 DDRB | (1 IR_OUT_PIN); // “开门”输出PWM delayMicroseconds(NEC_BIT_MARK); // 发送间隔 DDRB ~(1 IR_OUT_PIN); // “关门”停止输出 if(bitVal) { delayMicroseconds(NEC_ONE_SPACE); } else { delayMicroseconds(NEC_ZERO_SPACE); } }实操心得delayMicroseconds()在延时小于5us时极不准确。但对于560us以上的延时其误差在ATtiny内部8MHz时钟下通常小于5%完全在NEC接收头的容错范围内通常为±20%。不必过度追求绝对精确稳定可靠更重要。4.2 接收端代码精讲状态机解码法接收端的核心任务是测量接收头输出信号中低电平对应发射端的脉冲和高电平对应间隔的宽度。绝不能使用pulseIn()这类阻塞函数它会独占CPU。我们采用中断驱动状态机的方法。解码状态机设计IDLE状态等待下降沿接收头输出从高变低表示可能收到引导脉冲。MARK状态收到下降沿后记录当前时间micros()并切换到等待上升沿。SPACE状态收到上升沿后计算本次低电平的持续时间。如果这个时间在9ms左右引导脉冲则进入“接收数据”模式如果是在2.25ms左右重复码则处理重复按键如果是在560us左右则根据前一个间隔的长度判断是数据0还是数据1并存入缓冲区。DATA状态循环接收32个数据位每接收完一位判断是否已收齐32位。收齐后进行地址/命令反码校验校验通过则视为一帧有效数据存入全局变量供主循环使用。中断服务程序ISR示例框架volatile unsigned long lastTime 0; volatile uint32_t irData 0; volatile uint8_t bitIdx 0; volatile enum { IDLE, MARK, SPACE } state IDLE; ISR(PCINT0_vect) { // 引脚变化中断 uint8_t pinVal PINB (1 IR_RX_PIN); unsigned long currentTime micros(); unsigned int duration currentTime - lastTime; lastTime currentTime; switch(state) { case IDLE: if (!pinVal) { // 下降沿开始一个脉冲 state MARK; } break; case MARK: if (pinVal) { // 上升沿脉冲结束 if(duration 8000 duration 10000) { // ~9ms 引导码 bitIdx 0; irData 0; state SPACE; // 下一个要测量的是4.5ms间隔 } else if(duration 2000 duration 2500) { // ~2.25ms 重复码 // 处理重复按键 state IDLE; } else if(duration 400 duration 700) { // ~560us 数据位脉冲 // 需要结合下一个间隔判断是0还是1 state SPACE; } else { // 不是有效脉冲重置 state IDLE; } } break; case SPACE: if (!pinVal) { // 下降沿间隔结束新的脉冲开始 if(bitIdx 32) { // 根据上一个脉冲后的间隔长度判断数据位 if(duration 1400 duration 1900) { // ~1.68ms, 逻辑1 irData | (1UL bitIdx); } else if(duration 400 duration 700) { // ~0.56ms, 逻辑0 // irData对应位默认为0无需操作 } bitIdx; } state MARK; // 回去测量下一个脉冲宽度 if(bitIdx 32) { // 一帧接收完成可以设置一个标志位通知主循环 state IDLE; } } break; } }注意事项micros()函数在中断中使用其本身可能被其他中断影响。在ATtiny上如果开启了其他中断如看门狗需要仔细考虑时序。一个更激进但更稳定的做法是使用一个独立的硬件定时器在引脚中断触发时读取定时器计数器的值来计算时间这完全不受其他中断影响。5. 系统集成与调试实战从理论到可靠产品5.1 搭建调试环境你的第一台红外“逻辑分析仪”在动手焊接最终电路前强烈建议先用Arduino Nano或Uno搭建一个红外解码显示器。这个工具无比重要硬件将红外接收头的OUT引脚接到Arduino的任意数字引脚如D2同时接一个1602 LCD屏幕显示结果。软件使用一个现成的、经过验证的库如IRremote来解码。当你用这个工具对准你的ATtiny发射器或者任何一个家用遥控器按下按键时LCD屏幕上会显示出完整的32位十六进制码如0xFF00FF00。作用验证发射器确认你的ATtiny发射电路是否工作发出的码值是否符合预期。学习协议抓取你家各种电器的遥控码了解其地址码规律。排查问题当接收端不响应时用这个工具可以快速判断是发射没信号还是接收解码有问题。5.2 功耗优化让电池续航翻倍对于发射器遥控器功耗直接决定换电池的频率。休眠模式ATtiny85在空闲模式下功耗可以降到1mA以下。我们可以在代码中当没有按键按下超过一定时间如60秒后让单片机进入SLEEP_MODE_PWR_DOWN深度睡眠。此时功耗仅约0.1μA。任何一个按键按下都会触发引脚变化中断PCINT将芯片唤醒。硬件省电将未使用的IO口设置为输出低电平或输入并启用内部上拉根据具体电路决定避免引脚悬空产生漏电流。如果指示灯LED不是常亮其限流电阻可以适当加大到2kΩ-5kΩ降低工作电流。确保三极管在关闭时完全截止。检查基极电阻确保在单片机引脚输出低电平时0V三极管基极电压确实为0V没有因漏电流导致轻微导通。5.3 抗干扰与可靠性提升软件去抖与协议校验按键去抖不仅要在发射端做接收端也要做。我通常会在接收端解码得到一帧数据后要求连续两次收到相同的数据帧才确认为有效按键这能过滤掉绝大部分因干扰产生的错误码。严格进行反码校验。如果地址或命令的反码校验不通过直接丢弃整帧数据。接收端“沉默期”处理在成功解码一帧数据后设置一个100-150ms的“沉默期”或叫“屏蔽期”。在此期间忽略所有红外信号。这可以有效过滤掉NEC协议本身发送的重复码以及一些设备如荧光灯可能产生的周期性红外噪声。供电隔离如果接收端和受控设备如电机、继电器共用电源务必做好电源隔离。电机启停会在电源线上产生巨大的毛刺。最简单的办法是使用独立的LDO为单片机和控制电路供电并在电源入口处增加大容量100μF电解电容和104瓷片电容。6. 常见问题排查与解决实录即使按照上述步骤操作你可能还是会遇到一些问题。下面是我在多次项目中遇到的典型问题及解决方法。问题现象可能原因排查步骤与解决方案发射器完全无反应LED不闪1. 电源问题2. ATtiny未正确编程3. 三极管引脚接错1. 用万用表测量电池电压确认3V。2. 检查编程时是否选择了正确的芯片型号ATtiny85和时钟内部8MHz。3. 用万用表二极管档检查三极管e、b、c极是否对应电路连接正确。接收器能解码家用遥控但解不了自制的发射器1. 载波频率偏差太大2. 发射功率不足距离太远3. 发射波形时序不准1. 用示波器或频率计测量ATtiny PWM引脚输出频率调整定时器分频值使其接近38kHz。2. 将发射器和接收器靠近至10厘米内测试。检查R1阻值是否太大红外LED是否老化。3. 用“红外解码显示器”抓取自制发射器的码值与标准库解码结果对比检查引导码和各位时序。接收不稳定时好时坏1. 环境光干扰太阳、强LED灯2. 电源噪声3. 软件解码容错范围太窄1. 给红外接收头套上一个黑色的热缩管或将其置于暗处避免直射光。2. 在接收头VCC和GND之间并联一个47μF电解电容和一个100nF瓷片电容紧贴接收头引脚焊接。3. 适当放宽代码中判断脉冲宽度的范围如将560us的判断从500-600us放宽到450-700us。按键反应迟钝长按无效1. 接收端“沉默期”设置过长2. 发射端连发码逻辑未实现或不对1. 将接收端解码成功后的屏蔽时间从150ms减少到80ms试试。2. 确认发射端在检测到按键持续按下时是否在发送完第一帧完整数据后开始发送重复码9ms脉冲2.25ms间隔560ms脉冲。通信距离非常短1米1. 发射端限流电阻R1过大2. 红外LED性能差或安装方向不对3. 接收头灵敏度低或型号不对1. 在不超过LED最大峰值电流的前提下减小R1阻值。可以用可调电阻调试出一个最佳值。2. 更换质量更好的窄角度红外发射管并确保其指向接收头。3. 尝试更换另一个批次的接收头不同厂家灵敏度差异很大。一个高级调试技巧用手机摄像头观察红外光几乎所有手机摄像头的CMOS传感器对近红外光敏感。打开手机相机将遥控器或你的发射器红外LED对准摄像头按下按键你会在手机屏幕上看到发射LED发出微弱的白光或紫光。这是一个快速判断发射电路是否在工作的零成本方法。但注意这只能证明有红外光发出不能证明38kHz调制是否正确。7. 项目扩展从遥控开关到智能家居网关掌握了基础的收发这个系统可以玩出很多花样多设备控制与地址过滤在发射端为不同设备设定不同的用户地址码如0x01、0x02。在接收端只有解码出的地址码与自身预设地址匹配时才执行命令。这样你就可以用一个遥控器控制房间里多个不同的设备而互不干扰。红外学习功能让ATtiny接收端具备学习能力。设计一个“学习模式”在此模式下接收端将收到的一帧完整红外码32位保存到EEPROM中。之后在正常工作模式下当收到与此存储码相匹配的信号时就触发动作。这样你的自制设备就能复制任何家用遥控器的功能了。与无线网络结合用ESP8266或ESP32作为接收端解码红外信号后通过Wi-Fi将按键信息发送到MQTT服务器或Home Assistant。这样一个传统的红外遥控器就变成了智能家居的触发源。反过来也可以让ESP32通过网络接收指令然后驱动红外LED发射从而用手机App控制老式的红外空调、电视。红外遥控是一个经典、稳定且充满乐趣的技术切入点。它不像射频那样复杂也不像蓝牙需要配对其“所见即所得”的直线传播特性反而在需要明确指向控制的场景下成为优点。通过这个项目你真正吃透的不仅仅是一个协议更是一种在资源受限环境下进行精准时序控制和软硬件协同设计的思维方式。当你用自己亲手焊接的电路板和编写的代码成功在几米外控制一个设备动作时那种成就感是无可替代的。希望这份详细的指南和其中的经验教训能帮你少走弯路更快地享受到创造的乐趣。如果在实现过程中遇到新的问题不妨回头看看硬件连接和软件中的时序判断那往往是问题的根源所在。