红外发射代码实战:从NEC协议到38kHz载波与硬件驱动全解析 📅 发布时间:2026/9/2 10:18:39 👁 浏览次数: 简介红外编码发射是红外通信的关键环节网上解码资料较多编码发送代码相对稀缺针对STC8单片机的完整实现尤为珍贵适合嵌入式学习者和单片机开发者。压缩包共15个文件大小仅35KB以C源文件、头文件、Keil工程配置以及hex、obj、lst、m51等编译产物为主可直接查看工程结构和烧录文件。代码按NEC协议完成帧编码覆盖前导码、地址码、命令码和校验码生成并基于STC8定时器与GPIO输出PWM脉冲序列包含初始化、编码、发送等关键函数。累计已有488人学习下载适合正在做遥控器、家电控制或智能家居项目的开发者参考也可作为理解STC8中断、定时器和GPIO配合的入门示例。 整理资料时翻出这份“红外发射代码.rar”有点感慨。早年做智能家居项目为了搞定一个红外转发器前前后后踩了不少坑压缩包里封装的就是那套从零写的红外发射代码包含NEC协议编码、38kHz载波生成、定时器配置和完整的发送函数。红外发射代码这个东西原理上并不难但真正做起来你会发现卡你的往往不是协议本身而是时序精度、载波调制和硬件驱动这三个环节。这篇文章就围绕这套代码展开把红外发射从协议层到电路层完整过一遍适合正在做单片机遥控、智能家居联动、自制红外转发器的开发者参考。1. 红外发射项目整体设计与思路拆解1.1 这套红外发射代码解决了什么问题先说清楚这套红外发射代码的实际用途。它解决的场景非常具体用单片机当时用的是STM8S003和Arduino Nano两块平台去模拟任意红外遥控器的按键信号从而实现对空调、电视、机顶盒这类家电的统一控制。听起来挺简单但实际操作中有几个隐藏难点。第一个难点是协议兼容性。市面上的家电遥控器用的协议五花八门最常见的NEC协议如大多数电视、机顶盒、RC5协议飞利浦系设备、Sony SIRC协议索尼设备每种协议的帧格式、脉冲宽度、重复码规则都不一样。如果每种协议都临时查资料写代码再分别调时序效率很低。第二个难点是载波调制。红外遥控信号并不是简单的电平高低变化而是需要把逻辑电平加载到38kHz的载波上。这就意味着除了协议编码还得处理高频载波生成这在纯软件delay实现的方案里尤其容易出问题。第三个难点是硬件驱动。红外发射LED的驱动电流不小工作峰值电流通常在50mA到100mA之间单片机GPIO直接驱动基本带不动必须加三极管或MOS管放大电路。很多新手代码写对了但硬件上发光管不够亮导致发射距离只有几厘米还以为是自己代码写错了。这套代码的核心价值就是把以上三个难点打包解决了。协议层做了统一的编码接口驱动层用定时器硬件生成38kHz载波硬件层附带了驱动电路原理图调用方只需要传入待发送的遥控码和协议类型即可。1.2 方案选型为何选用定时器生成38kHz载波在设计红外发射代码时载波生成方案是首先要确定的因为它直接决定了代码的结构和运行效率。我当时对比了三种方案纯软件翻转IO、定时器PWM输出、以及外部有源晶振方案。纯软件翻转IO的做法最简单代码里写个while循环控制GPIO按13微秒左右的间隔翻转产生近似的38kHz方波。这种方式在低速单片机或非实时系统里极不稳定一旦被中断打断载波频率就会偏移接收端解调就会失败。我实测过在Arduino平台上用delayMicroseconds(13)翻转IO测出来的载波频率会漂到36kHz到40kHz之间虽然部分接收头能容忍这个偏差但可靠性完全没法保证。定时器PWM方案则完全不同。它把载波生成交给硬件定时器输出引脚自动翻转完全不由软件干预。MCU只需要在发送逻辑电平的时间段内打开PWM输出在空闲时段关闭即可。这个方案的优势在于载波频率稳定不占用CPU资源时序精度只取决于定时器的时钟源对于NEC这类对时间参数要求严格的协议非常合适。外部有源晶振方案更适合大批量生产的独立红外发射模块但在单片机项目里属于过度设计还要多出几颗物料成本我最终没有采用。最终我采用的方案是用STM8S003的TIM1产生38kHz PWM或者Arduino平台用IRremote库自带的PWM驱动。这两个平台下的核心逻辑是一样的代码里只是抽象成了统一的发送接口。2. 红外发射协议深度解析2.1 NEC协议帧格式与时间参数NEC协议是红外遥控领域最常见的一种协议我最早也是从它入手的。理解NEC的工作方式之前首先要明确一个基本概念红外遥控发送的并不是数据包而是一连串严格定时的脉冲串接收端通过测量脉冲的宽度来还原逻辑0和逻辑1。NEC协议一个完整的帧包含以下几个部分组成时间参数说明引导码AGC脉冲9ms高电平 4.5ms低电平标记一帧的开始地址码8位低位在前设备地址地址反码8位用于校验命令码8位低位在前按键功能码命令反码8位用于校验结束位560μs高电平帧结束标志逻辑电平的定义是逻辑0 560μs载波 560μs空闲总时长1.12ms逻辑1 560μs载波 1690μs空闲总时长2.25ms。可以看到两者的载波持续长度是一样的区别在于空闲时间的长短这就是接收端区分0和1的依据。还有一个很多新手会忽略的细节重复码。当你长按遥控器按键时NEC协议并不是重复发送完整帧而是每隔约110ms发送一个重复码重复码的结构是9ms高电平 2.25ms低电平 560μs高电平后面不再跟任何数据位。如果代码里没有实现重复码长按功能比如空调温度连续调节就无法实现。我在代码里单独实现了sendNECRepeat()函数专门处理这个情况。2.2 主流红外协议对比与代码抽象除了NEC还有一些其他常见协议。我在做这套红外发射代码时最初只写了NEC支持后来发现家里有一台老功放用的是RC5协议只好又补了RC5的编码模块紧接着Sony电视又对应SIRC协议。最后干脆设计了一个统一接口每种协议各自实现编码函数向外暴露统一发送函数调用方只需要指定协议类型即可。三种协议的核心差异我在表格里做了对比协议载波频率逻辑表示方式帧结构特点常见设备NEC38kHz脉宽调制PWM引导码地址数据反码电视、机顶盒、风扇RC536kHz曼彻斯特编码起始位控制位系统地址命令飞利浦系设备、功放Sony SIRC40kHz脉冲宽度调制5位地址7位命令12位模式索尼电视、播放器RC5协议是最让我头疼的因为它用的是曼彻斯特编码每一位的中间都有一个电平跳变逻辑0是从高到低逻辑1是从低到高。这意味着编码时不能把每一bit简单映射成一段高电平和一段低电平而必须按照bit位在当前时钟周期的位置来动态决定电平翻转。写起来虽然不算难但调试时用逻辑分析仪逐个bit核对相当费时间。代码抽象上我的设计如下每种协议一个c文件统一暴露一个发送函数比如void IR_NEC_Send(uint32_t data, uint8_t repeat)、void IR_RC5_Send(uint16_t data)、void IR_SIRC_Send(uint16_t data, uint8_t address)。上层调用根本不用关心协议细节只要传入对应的数据即可。这套抽象在后面接入智能家居平台时省了很大功夫。3. 核心代码实现与关键参数计算3.1 38kHz载波生成与定时器配置38kHz这个频率不是随便定的它对应的是红外接收头内部带通滤波器的中心频率。市面上绝大多数红外接收头比如VS1838B、TL1838都内置了中心频率为38kHz的带通滤波器只允许38kHz附近的信号通过其他频率一概忽略这能有效过滤掉环境光和其他红外噪声的干扰。所以发射端必须精确产生38kHz载波。如果频率偏差超过±2kHz接收灵敏度会明显下降偏差超过±5kHz时基本就无法接收了。在STM8S003上我使用TIM1的PWM输出模式配置代码如下void IR_Carrier_Init(void) { // 输入时钟16MHz目标载波38kHz // 分频16MHz / 421 ≈ 38.0kHz // ARR420, 占空比50% TIM1_DeInit(); TIM1_TimeBaseInit(0, TIM1_COUNTERMODE_UP, 420, 0); TIM1_OC1Init(TIM1_OCMODE_PWM1, 210, TIM1_OCPOLARITY_HIGH); TIM1_OC1PreloadConfig(ENABLE); TIM1_Cmd(ENABLE); // PA0作为PWM输出引脚 GPIO_Config(GPIOA, GPIO_PIN_0, GPIO_MODE_OUT_PP_HIGH_FAST); }这里分频后的计算逻辑是16MHz时钟源经过8位预分频器这里设0表示不分频后进入计数器计数器对时钟脉冲计数当计数达到420时归零重新开始。所以PWM频率 16MHz / (420 1) ≈ 38.0kHz占空比通过比较值210设置正好50%。这个参数组合我是算了半天才确定的记录在这里供参考。在Arduino平台上IRremote库里已经封装了38kHz载波的定义默认使用定时器2的PWM输出使用起来省心很多。但如果用的是非官方核心比如一些国产STM32 Arduino核心定时器和引脚的对应关系可能不一样需要自己查看核心源码确认。3.2 NEC协议编码发送代码实现下面给出这套代码中NEC协议发送的核心实现。这段代码可以直接在Arduino上编译运行也可以在STM8上稍作修改后使用#include Arduino.h #define IR_LED_PIN 3 // 连接到红外LED驱动电路的控制引脚PWM输出 // 基于硬件的38kHz载波发送函数 // 参数duration为载波持续时间的微秒数 void sendCarrier(uint16_t duration) { unsigned long start micros(); while (micros() - start duration) { // 手动翻转实现38kHz近似载波实际项目中建议 // 使用硬件PWM生成此写法仅作演示 digitalWrite(IR_LED_PIN, HIGH); delayMicroseconds(13); digitalWrite(IR_LED_PIN, LOW); delayMicroseconds(13); } } // 发送逻辑0560μs载波 560μs空闲 void sendNEC_0() { sendCarrier(560); delayMicroseconds(560); } // 发送逻辑1560μs载波 1690μs空闲 void sendNEC_1() { sendCarrier(560); delayMicroseconds(1690); } // 发送NEC帧数据32位 void sendNECFrame(uint32_t data) { // 引导码9ms载波 4.5ms空闲 sendCarrier(9000); delayMicroseconds(4500); // 发送32位数据从最低位开始 for (uint8_t i 0; i 32; i) { if (data 0x1) { sendNEC_1(); } else { sendNEC_0(); } data 1; } // 结束位560μs载波 sendCarrier(560); } // 发送NEC重复码 void sendNECRepeat() { sendCarrier(9000); delayMicroseconds(2250); sendCarrier(560); }有一点必须说明上面代码里sendCarrier()使用软件延时翻转IO的方式只是为了演示协议时序实际项目里我强烈推荐改用硬件PWM。原因前面讲过——软件延时的载波不够稳定delayMicroseconds()本身的精度偏差加上中断干扰很容易导致接收端解调失败。实际代码中sendCarrier()内部是通过拉起PWM输出并延时而delayMicroseconds()的空闲段则需要关闭PWM输出保持GPIO为低电平。完整的NEC发送流程是调用方传入完整的32位数据码 → 函数先发引导码 → 逐位发送32位数据 → 发送结束位。其中32位数据码的构造方式是地址码(8bit) 地址反码(8bit) 命令码(8bit) 命令反码(8bit)高低位顺序为LSB first。3.3 数据码格式与校验逻辑很多初学者容易在这里栽跟头从网上找到一个遥控码值比如0x00FF45BA直接丢进发送函数却发现设备没反应。原因在于这个值的bit顺序和发送顺序不匹配。NEC协议的数据发送是从最低位开始的也就是LSB first。举个例子假设我们要发送命令码0x45二进制01000101发送顺序是1、0、1、0、0、0、1、0从最低位到最高位。如果把0x45当普通整数处理通过data 0x01循环移位的方式来取位就像上面的代码那样发送顺序正好符合NEC要求。但如果你用某些库函数直接按MSB first发送就会全部错位接收端解析出的数据完全不对。地址码的反码作用是对传输内容做校验。接收端算出地址码和地址反码相加的结果如果等于0xFF则认定地址传输正确。命令码和命令反码同理。如果反码不匹配绝大多数接收设备会直接丢弃这个帧。所以代码里如果手动构造数据码别忘了算反码。我在代码包里留了一个脚本工具用来从逻辑分析仪导出的遥控码波形自动提取NEC数据帧方便批量采集家里遥控器的码值#!/usr/bin/env python3 # 解析逻辑分析仪导出的NEC帧数据 # 输入csv文件每行两个值时间戳(us), 电平 import csv import sys def parse_nec_pulses(filename): pulses [] with open(filename, r) as f: reader csv.reader(f) last_time 0 last_level 0 for row in reader: t int(row[0]) level int(row[1]) if level ! last_level: pulses.append(t - last_time) last_time t last_level level pulses.append(0) # 结束标记 return pulses def decode_nec(pulses): # 跳过引导码 idx 0 # 寻找9ms的引导标记 while idx len(pulses) - 1: if pulses[idx] 8000 and pulses[idx 1] 4000: break idx 1 idx 2 data 0 for i in range(32): if idx len(pulses) - 1: break high_time pulses[idx] low_time pulses[idx 1] if low_time 1000: data | (1 i) # 逻辑1 idx 2 return data if __name__ __main__: pulses parse_nec_pulses(sys.argv[1]) code decode_nec(pulses) print(NEC Code: 0x{:08X}.format(code))这个脚本的判定逻辑很直接NEC的逻辑1空闲期是1690μs逻辑0空闲期是560μs以1000μs为阈值即可清晰区分。采集上来的码值可以直接用在sendNECFrame()里。4. 硬件电路搭建与调试验证4.1 发射电路设计要点代码写得再好硬件电路不过关也一样白搭。红外发射电路看起来极其简单——一个LED、一个电阻、一个三极管但参数选择有讲究。这是我验证过多次的标准电路VCC(5V) ── 限流电阻(10Ω~22Ω) ── 红外LED正极 红外LED负极 ── 三极管(2N2222/S8050)集电极 三极管发射极 ── 地 单片机GPIO ── 基极限流电阻(1kΩ) ── 三极管基极几个关键参数说明限流电阻的大小直接影响发射功率。红外LED在脉冲状态下的最大允许电流通常为100mA左右瞬时峰值甚至可以到1A但平均功率有限制。如果限流电阻太大LED亮度不够发射距离只有几十厘米如果太小LED会过热烧毁。以5V供电、2N2222导通压降约0.3V、红外LED正向压降约1.2V计算限流电阻取10Ω时电流约为(5V - 0.3V - 1.2V) / 10Ω 350mA——这已经是脉冲发射状态下的合理峰值了但不建议持续发送现实中平均电流受占空比限制完全在安全范围内。基极限流电阻的选择保证三极管进入饱和导通。2N2222的直流放大倍数在100左右集电极电流320mA时基极电流需要至少3.2mA。单片机GPIO输出高电平约3.3V或5V减去基极-发射极压降0.7V1kΩ电阻产生约2.6mA到4.3mA的基极电流足够驱动三极管饱和。如果使用STM8这类3.3V单片机基极电阻可以适当减小到680Ω保证足够的驱动电流。省略三极管的直连方案也不是不行——I/O直接驱动红外LED但电路里的静态电流全部由GPIO承受对于手机红外发射器这种低功耗场景可以考虑稍大功率的发射塔比如10米以上的红外发射器就必须加放大级了直连驱动绝对不够用。4.2 实测波形与信号调试硬件搭建好之后第一件事不是直接去控制空调而是用逻辑分析仪看波形。这一步能节省大量调错时间强烈建议必须做。我用的是Saleae逻辑分析仪国产16MHz采样版本就够了把探头夹在红外LED的正极抓取发送时的波形。正确的NEC帧波形应该是这样的最前面一个约9ms的高电平包实际上因为载波频率远高于逻辑分析仪采样率高电平包看起来像一团密集的方波毛刺接着约4.5ms完全低电平然后由32段“载波脉冲空闲”组成的密集数据区最后是一个560μs的载波脉冲结束如果只看到没有任何高频分量的方波即一个纯粹的9ms高电平、4.5ms低电平的开关波说明载波没发出来问题出在PWM配置或者GPIO模式上。如果波形的载波部分出现了明显断断续续、高电平内部有缺口说明代码里的中断嵌套导致载波被打断。这里有一个我踩过的很典型的坑在Arduino上IRremote库默认使用定时器2如果在中断服务函数里也调用了delay()或者串口打印会严重干扰PWM输出。我当时做红外转发器串口每100ms打印一次调试日志结果红外信号时不时被串口中断打断接收距离骤降到原来的三分之一。排查了一晚上最后把串口打印全部去掉才恢复正常。5. 常见问题与排查技巧速查5.1 发射距离短或接收不到信号这是出现频率最高的问题。先按下面顺序排查大多数情况能快速定位现象可能原因排查方向距离只有几厘米红外LED驱动电流不足检查限流电阻是否过大、三极管是否饱和、供电电压是否正常正对接收头也无效载波频率偏移过大用逻辑分析仪测PWM频率确认是否接近38kHz偶尔能控制有时不行时序被中断干扰关闭中断、检查串口打印和delay函数调用换一个设备就失效协议码值不对确认设备用的是NEC还是RC5等其他协议红外LED的发射角度也需要注意。普通5mm直插式红外LED的发射半角大约在15°到30°之间超出这个角度指向性衰减非常严重。如果做的是需要覆盖整个客厅的红外转发器建议用多个LED按不同朝向布置或者选用广角型的贴片红外LED。实测下来驱动电流在100mA左右时NEC协议在空旷环境的控制距离一般能达到8到12米。如果超过这个预期很多检查一下是不是LED选错了——有些RGB LED里也包含红外发光芯片但性能和专用红外LED差得很远。5.2 协议码型正确但设备无反应码值用逻辑分析仪验证过是对的波形也完美但设备就是没反应。这种情况我遇到过两次每次的原因都不一样。第一次是载波极性反了。NEC协议要求载波高电平有效即发射管在逻辑高电平时发光。如果把逻辑反了接收端解调出的信号会完全取反——引导码时间参数还勉强对得上数据位就会全部变成相反。这个问题的根源是驱动电路用了PNP三极管且基极控制信号反相。解决方案很简单要么改电路要么在代码里把GPIO输出电平整体取反。第二次是载波占空比不对。理论占空比50%但如果用某些低端定时器配置占空比可能偏到30%或者70%。接收头对占空比有一定容忍度太低会影响解调输出的电平摆幅导致后级解码芯片误判。我用示波器实测发现占空比低于25%时某些型号的接收头开始出现解调异常。配置PWM时比较值一定要设为定时器周期的一半不要想当然。5.3 多设备控制与码库管理经验项目从单一设备扩展到全屋设备之后码值管理变成了新的问题。几十台设备每台可能有多达几十个按键功能码值总量轻松突破上千条存储和查找都要认真设计。我的做法是先在PC端把码值按“设备类型-品牌-型号-按键”的层级整理成JSON再用脚本生成C语言的const数组直接烧进Flash。每条码值记录还附带了一个校验值CRC8读取时先校验再使用防止Flash损坏导致发送出错误码型。对于内存受限的单片机用按键扫描码替代完整C数组也能减少大量存储开销——实际使用中我测试发现一个按键只要存下“协议类型 32位数据”就够了有些协议甚至只需要24位。精简后的单个按键码仅占用6到8个字节上千个按键的码库也只在Flash里占用不到10KB。个人实操体验小结最后再分享一点自己的体会。红外发射这个方向技术门槛不高但它特别考验人对细节的把控能力。代码层面的位顺序、时间参数硬件层面的驱动电流、载波极性任何一个环节出问题表象都是“遥控器没反应”但排查路径却完全不同。我现在拿到一份红外发射代码第一件事永远是看三处定时器PWM配置对不对、协议位序是不是LSB first、驱动电路是共射还是共集。这三个问题确认完八成问题都已经有了眉目。如果你也在做类似的项目建议手头常备一个逻辑分析仪几十块钱的基础型号就够用了。很多看似诡异的现象——比如特定角度才能控制、某个按键偶尔失灵、同一份代码在两块不同开发板上表现不一致——在波形面前都会变得非常直白。调试红外发射代码本质上就是让代码产生的波形和接收端预期完全对齐除此之外没有别的捷径。本文还有配套的精品资源点击获取