NEC红外协议精讲:从时序原理到RK3576解码实战
前阵子帮朋友调一块RK3576开发板上的红外遥控板子明明收到了红外接收头吐出来的波形键值却怎么都对不上。折腾了一下午最后发现不是驱动问题而是对NEC协议的时序理解出了偏差——他把遥控器按键抬起时的重复码当成了一帧完整数据去解析。后来我把波形导出来一条一条比对问题一下就清晰了。这种事在玩IR的人里太常见了。红外协议里NEC协议可以说是最普及的一种编码规则。电视、机顶盒、空调、风扇、各种小家电的遥控器十台里至少七八台用的是它或它的变体。做嵌入式、搞单片机、做智能家居几乎都会跟它打交道。这篇文章我尽量用大白话把NEC协议讲透从载波、时序、帧结构到实际抓波形、写解码代码再到RK3576这类Linux平台怎么适配红外遥控全部过一遍。不管你是刚接触红外遥控的新手还是被某个诡异问题卡住的老手应该都能找到点有用的东西。1. 认识NEC协议满大街遥控器背后的“通用语言”1.1 NEC协议解决了什么问题红外遥控的本质很简单发射端用红外LED发出光脉冲接收端把光信号转回电信号然后从脉冲的宽窄、长短里读出0和1。但问题在于如果发射端只是简单地把数据位用“亮”和“灭”来表示接收端很难区分一段很长的低电平到底是“一个0”还是“两个0”也容易受到环境光、荧光灯等干扰源的误触发。NEC协议做的第一件事就是把每一位数据用固定节奏的“载波脉冲静默间隔”来表达而且每一位的起始都是相同的560µs载波脉冲后面的静默长度决定这一位是0还是1。这样接收端只需要精确测量“静默”时间就能可靠地判断每一位不需要依赖绝对电平持续时间的测量鲁棒性一下就上来了。第二件事是它定义了一套完整的帧结构引导码、地址码、地址反码、命令码、命令反码。引导码让接收端知道“下面开始发正式数据了”地址码用来区分不同设备命令码放按键功能反码用于自动校验。这套结构既解决了同步问题又解决了误码问题和多设备共存的寻址问题。可以说NEC协议把一次小巧的红外传输需要的所有要素都考虑全了。所以你会发现NEC协议虽然叫“协议”但它不是一套复杂的软件栈更像是一份约定好的“波形方言”。只要收发双方都按同一套时间规则说话就能稳定通信。1.2 为什么偏偏是38kHzNEC协议最常用的载波频率是38kHz也就是红外LED每秒闪烁38000次。你可能会问为什么不直接用直流亮灭来传数据因为环境里有大量红外干扰源阳光、白炽灯、节能灯都会发出红外成分。如果用纯直流电平接收端很难分辨哪些是遥控器的信号哪些是背景噪声。38kHz载波配合接收头里的带通滤波器就能把干扰基本滤掉。一体化红外接收头比如VS1838、TSOP38238这类内部集成了光敏二极管、放大电路、AGC电路和解调电路。它们只对38kHz左右的调制光敏感其他频率和直流背景光都会被抑制掉。这就像收音机调台载波就是那个“台”数据是台里播的内容。38kHz的载波周期大约是26.3µs典型占空比1/3左右也就是每周期里高电平约8.8µs低电平约17.5µs。选1/3占空比是功耗和发射距离的折中占空比太低平均光功率不够传不远太高LED持续大电流发热驱动电路压力也大。实际选LED限流电阻时可以按平均电流来算而不是按峰值电流这是很多人容易忽略的点。2. NEC协议的时序拆解把波形当作句子来读2.1 逻辑0和逻辑1两个不同长度的“音节”NEC协议里每一位数据都由一个低电平脉冲加一个高电平静默组成但“低”和“高”要看从哪端看。发射端输出的是载波调制信号有载波就是“发送”无载波就是“空闲”。一体化接收头输出的是数字电平且逻辑是反相的收到载波时输出低电平没收到载波时输出高电平。所以从接收头看有载波段是低电平静默段是高电平。后面讲时序我都默认从接收头输出的角度来说这也是大家用逻辑分析仪抓波形时实际看到的形态。逻辑0的波形是“低电平560µs 高电平560µs”总时长约1.12ms。逻辑1的波形是“低电平560µs 高电平1690µs”总时长约2.25ms。也就是说每一位的开头都有一个560µs的低电平同步头中间那个高电平的长度是区分0和1的关键短的是0长的是1。这个设计非常巧妙。接收端解码时只需要找“低电平脉冲”然后测它后面的高电平有多长。如果高电平在1.1ms左右判为0如果在2.2ms左右判为1。因为每一位都有固定的起始低脉冲即使数据连续发送接收端也能一个接一个地切分位不会错位。2.2 引导码、数据帧、重复码一段完整“句子”的结构一帧正常的NEC数据开头先是一个引导码9ms的低电平加4.5ms的高电平。9ms这个长度远远长于单个数据位接收端一看就知道“新的一帧来了”然后开始准备接收后续的32位数据。引导码之后依次是8位地址码、8位地址反码、8位命令码、8位命令反码。共32位每一位都按低位在前LSB first的顺序发送。地址反码就是地址码按位取反比如地址是0x00反码就是0xFF命令码是0x45反码就是0xBA。接收端可以把收到的码和反码做一个异或校验如果结果不是0xFF就认为这一帧有误码直接丢弃。这个机制的容错价值在实测中非常明显我后来写解码驱动时基本都靠它过滤垃圾数据。数据帧发完之后会有一个560µs的结束低脉冲表示这一帧说话完毕。还有一种情况就是按键一直按住不放。NEC协议不会用数据帧无限重发来表现“长按”而是每大约110ms发送一次重复码。重复码的结构比较简单9ms低电平 2.25ms高电平 560µs低电平。也就是说它的引导码部分与正常帧几乎一样只是那个4.5ms的高电平变成了2.25ms。接收端在已经收到过完整数据帧后如果再遇到9ms2.25ms的重复码就知道还是同一个键被按着不需要重新解析命令。2.3 地址码和反码设备如何避免串扰为什么NEC协议要把8位地址扩成16位地址码反码来发直接发8位不更节省时间吗这里藏着两层考虑。第一层是可靠性。前面说了反码可以让接收端做异或校验。一次电平传输受干扰、被环境光误触发都可能导致某一位翻转。如果不校验一个误码就可能让电视把“音量”当成“音量-”来执行。加了反码后命令码和命令反码必须严格互补否则直接丢弃大大降低误动作概率。第二层是设备区分。虽然没有遥控器的地址码是0x00电视可能是0x04DVD可能是0x08设备地址不同就能避免一个遥控器同时控制多台设备。当然实际中很多廉价遥控器根本不区分地址码全都用0x00或0xFF这时候地址反码就只是个校验位。我在实际开发中还发现一个有意思的现象很多变种协议会把第二个字节标准NEC中的地址反码直接用作地址高字节这样就有16位地址空间。遇到这种遥控器用标准NEC解码会得到一堆奇怪的地址对照不了键值表。所以做解码驱动时最好把“是否是严格反码”作为一个判断条件进入不同的解析分支兼容性会好很多。3. 实操从抓波形到完成解码3.1 硬件与测试环境准备要真正吃透NEC协议光看书不够必须自己抓一次波形。需要的硬件很简单一个遥控器、一个一体化红外接收头38kHz、一个逻辑分析仪几块钱的USB逻辑分析仪就够用、几根杜邦线。一体化接收头一般有三个引脚VCC、GND、OUT。OUT引脚是集电极开路或推挽输出通常需要接一个上拉电阻到VCC阻值4.7k到10k都可以。通电后在没有红外信号时OUT输出高电平一旦检测到38kHz载波OUT被拉低。接逻辑分析仪时把通道夹在OUT上地线接到GND采样率设置为2MHz或更高触发方式设置为下降沿触发然后按下遥控器的一个按键就能抓到完整的发射过程。我建议准备两个不同品牌的遥控器因为不同厂商对NEC时序有微小差异。有的引导码高电平可能是4.6ms而不是4.5ms有的数据位高电平是1.7ms而不是1.69ms。多抓几组数据心里就有底了。3.2 用逻辑分析仪抓下一段NEC波形连接好之后按一下遥控器上的“音量”键逻辑分析仪上会看到一段明显的波形开头是一个很宽的低电平脉冲约9ms接着是一个稍宽的高电平约4.5ms再往后是密密麻麻的一串窄脉冲最后以一个560µs低电平收尾。对照前面讲的时序你可以按住“音量”不放这时每隔约110ms会出现一个重复码。重复码的波形和数据帧很像但引导码后面的高电平只有2.25ms而且后面没有再跟32位数据直接就是560µs的结束位。这个区别在波形上肉眼很容易看出来。如果你的逻辑分析仪软件支持协议解析比如Saleae Logic的NEC协议解码插件它可以自动标出每一位是0还是1。不过我更喜欢关掉自动解析自己用光标量几个关键时间点。量完后你会发现NEC协议的实际参数和理想参数非常接近但也有一点小偏差这就是为什么解码时最好用时间窗口判断而不是固定等于某个值。3.3 单片机端的解码状态机抓完波形就可以在单片机上写解码程序了。我的常用思路是用一个定时器做输入捕获测量相邻两次边沿的时间差。这里给一个简化的状态机框架以STM32为例假设红外接收头的OUT引脚接到某个支持输入捕获的TIM通道上解码逻辑如下// 定时器捕获频率1MHz也就是1个计数1us // 每次捕获中断里读取两次边沿之间的间隔delta_us // 用一个变量记录当前接收状态 #define TOLERANCE_US 300 // 时间宽容度 #define GUIDE_LOW_MIN 8000 // 引导码低电平范围 #define GUIDE_LOW_MAX 11000 #define GUIDE_HIGH_MIN 4000 // 引导码高电平范围正常帧 #define GUIDE_HIGH_MAX 5000 #define REPEAT_HIGH_MIN 1800 // 重复码高电平范围 #define REPEAT_HIGH_MAX 2800 #define BIT_LOW_MIN 400 // 数据位低电平范围 #define BIT_LOW_MAX 700 #define BIT_ZERO_MIN 800 // 数据位高电平短为0长为1 #define BIT_ZERO_MAX 1300 #define BIT_ONE_MIN 1400 #define BIT_ONE_MAX 2100 static uint16_t bit_count; static uint32_t code; // 暂存32位数据 static uint8_t state; // 0等待引导码, 1接收数据, 2等待结束 void TIM_IRQHandler(void) { uint32_t delta get_capture_delta_us(); // 检测到低电平宽度在8~11ms范围内可能是引导码 if (state 0 delta GUIDE_LOW_MIN delta GUIDE_LOW_MAX) { state 1; // 等下一个高电平宽度判断是数据帧还是重复码 code 0; bit_count 0; return; } if (state 1) { // 第一个高电平宽度区分4.5ms数据帧和2.25ms重复码 if (bit_count 0) { if (delta GUIDE_HIGH_MIN delta GUIDE_HIGH_MAX) { bit_count 1; // 正式数据帧开始 } else if (delta REPEAT_HIGH_MIN delta REPEAT_HIGH_MAX) { state 0; // 重复码维持上一次按键值 repeat_event(); } return; } // 后续每一位先遇到低电平再遇到高电平高电平宽度决定0/1 if (delta BIT_LOW_MIN delta BIT_LOW_MAX) { return; // 数据位的起始低脉冲不用处理 } if (delta BIT_ZERO_MIN delta BIT_ZERO_MAX) { // 这一位是0直接累加 } else if (delta BIT_ONE_MIN delta BIT_ONE_MAX) { code | (1UL (bit_count - 1)); // LSB在前 } else { state 0; // 时间不在合理范围丢弃重来 return; } bit_count; if (bit_count 33) { // 32位数据 结束低脉冲 checksum_and_emit(code); state 0; } } }这段代码的核心思路就是前文说的每一位都由一个560µs左右的低电平开头紧接着的高电平宽度决定0或1。因为每位都从低电平开始所以用边沿捕获seq的delta值可以很自然地切分位边界。实际工程里建议把所有阈值都做成宏方便按抓到的真实波形微调。3.4 RK3576这类Linux平台怎么适配红外遥控单片机上的解码思路搞明白后Linux平台上适配红外遥控就顺理成章了。以RK3576开发板为例一般有两种方案。第一种是内核里已有红外接收驱动把红外接收头的OUT脚接到SoC支持IR输入的GPIO或专用红外接收控制器引脚。设备树里配置好引脚复用和中断然后在用户态用ir-keytable查看和修改键值映射表。现代内核大多用input子系统上报键值修改键值映射可以用ir-keytable -c清空默认表再导入自己的配置文件。第二种方案是板子上没有现成的IR控制器那就用一个GPIO模拟。把GPIO配置为中断输入中断里记录电平跳变的时间戳然后把时间序列交给内核的LIRC子系统或者用户态的lircd处理。用户态解析NEC协议后通过input子系统上报按键事件。调试时配合evtest工具可以看到每次按键上报的scancode和keycode。经常有人问“我按下了遥控器dmesg没报错evtest也没有任何事件怎么办”我一般先建议检查设备树里的GPIO号对不对再用gpioinfo或/sys/class/gpio看看引脚能不能正常读到电平跳变。如果引脚没反应八成是接线或者引脚复用的问题如果引脚有反应但没上报事件那就是解码层没识别到NEC帧回去抓波形最直接。4. 常见问题与排查技巧实录4.1 波形正常但解出的码全乱位序和反码校验这是我被问得最多的问题。逻辑分析仪抓出来的波形明明和手册上很像但解析出来的地址码、命令码就是不对。最常见的原因是位序搞反了。NEC协议是LSB first也就是每一位字节中最低位先发送。比如命令码0x45二进制是01000101在波形上先看到的应该是10100010这一串的顺序。如果用MSB方式解析出来的值自然完全不是那么回事。第二个常见原因是把地址反码也当成有效地址用了。前面说过标准NEC在地址码后面会跟一个地址反码很多新手直接把32位数据按两个字节取出来用结果看到0xFF、0x00之类的一脸懵。正确做法是先判断地址码和地址反码是否互为取反是则只取地址码和命令码如果不是则走扩展NEC或其他变体的解析分支。第三个原因是把重复码和数据帧的前半段混淆。重复码的引导码是9ms低2.25ms高数据帧是9ms低4.5ms高。如果代码里只判断了9ms低电平就开始收集数据等到2.25ms高电平这个位置就会把后续的560µs结束位当作第一位数据导致整个帧错位。这就是我开头说的那个RK3576项目的坑。4.2 遥控距离变近、接收失灵载波匹配与AGC如果你发现遥控器必须贴得很近才能触发第一个要怀疑的是载波频率匹配。一体化接收头有频率选择常见的是38kHz但也有36kHz、40kHz、56kHz的。如果遥控器用的发射频率是40kHz接收头是38kHz的灵敏度会明显下降距离大幅缩短。用示波器或逻辑分析仪看发射端的载波频率是最直接的确认方式。第二个嫌疑是接收头的AGC自动增益控制在搞鬼。一体化接收头内部有AGC电路用来抑制连续的红外干扰。如果环境光里含有较强红外成分接收头会自动降低增益这时候正常的遥控信号也可能被当成噪声滤掉。遇到这种情况先遮挡环境光试试或者把接收头前加一个滤光片。第三个嫌疑是供电问题。接收头对电源噪声比较敏感如果供电纹波大输出波形就会出现抖动影响解码。我遇到过在电机驱动的板子上红外解码不稳的情况最后在接收头VCC和GND之间加了一个10µF电容加0.1µF陶瓷电容问题就解决了。4.3 按键连发、一次按出多个键重复码误判正常NEC遥控器按下按键时先发一帧数据然后每约110ms发一次重复码直到按键抬起。如果你在按一次键时收到了多个不同键值多半是解码逻辑没有正确处理重复码导致上一帧数据的尾巴被当成了新一帧的引导码。我的建议是把“收到完整数据帧并校验通过”和“收到重复码”当成两个不同事件。完整数据帧到来时更新当前键值并上报重复码到来时只重复上报上一次键值不重新解析。如果重复码到来时还没有任何数据帧要直接丢弃。这样就能避免按键重复触发混乱。4.4 命名陷阱IR不一定指红外打印机能耗软故障排查参考排查红外问题的时候经常会在搜索资料时被“IR”这个词误导。比如佳能imageRUNNER系列打印机的驱动安装报错信息里会有“IR C3226”这样的型号字样很多人一看到IR就想到了红外。这个IR其实是imageRUNNER的缩写跟红外八竿子打不着。我在给客户调红外设备时就遇到过类似的事对方查了一下午红外协议结果问题只是打印机驱动安装路径不对。所以做红外项目时搜索关键词要尽量具体比如“NEC协议 时序”“38kHz 红外接收头”不要只用IR一个词。否则很容易被打印机、热像仪之类的其他含义带走。顺带提一句FLIR的ResearchIR这类热像仪分析软件也是“IR”的另一个含义我在调试红外发射管时偶尔会用热像仪看发射管的发热分布来判断驱动电流是否正常。这些场景和遥控用的红外协议完全是两码事但名字上都挂着IR容易混淆。4.5 通用功能码速查表NEC协议的命令码并没有强制统一不同厂商的设备完全可能把同一个键定义成不同码值。但公版遥控器和不少消费电子产品会沿用一套常见定义。以下是我手头一款公版NEC遥控器实测抓出来的码仅供参考不代表所有设备实际以你自己抓包结果为准。按键功能命令码十六进制地址码十六进制备注电源0x450x00最常见的电源键码之一菜单0x470x00不少电视、盒子沿用音量0x150x00部分遥控器用0x16音量-0x090x00部分遥控器用0x19频道0x180x00公版常用频道-0x080x00公版常用确认/OK0x140x00有的设备是0x40返回0x430x00有的设备是0x0D如果你在做万能遥控器或者学习型遥控器建议直接用逻辑分析仪把自己的遥控器按键全抓一遍生成一张键值表存成配置文件。比在网上盲目找码表靠谱得多。5. 我的一些经验和建议做红外遥控项目多了我给自己的几条规定第一新项目用到不熟悉的红外协议时先花半小时抓波形不凭记忆写解码代码。NEC协议再常见不同品牌遥控器的细微偏差也足够让你踩坑。第二解码代码里所有时间阈值都用范围判断不要用精确等于因为实际硬件的时钟偏差、接收头响应延迟都会让时序有几百微秒的变化。第三校验必须做反码校验是白给的可靠性不要省。另外一个容易被忽略的小技巧是接收头的输出引脚上拉电阻不是随便选的。上拉太小功耗高且输出低电平可能拉不彻底上拉太大边沿变缓影响时序测量。我习惯用4.7k到10k之间的值。如果MCU的GPIO内部有上拉且外部没有其他负载用内部上拉其实也够用。NEC协议看起来简单但真要在产品上跑稳定还是有不少细节需要打磨。希望这篇文章能帮你少走一些弯路。如果你在调试中遇到其他奇怪的时序问题别急着怀疑协议文档把波形抓出来一条一条量问题往往就自己浮出来了。