前阵子帮朋友调一块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 为什么偏偏是38kHz
NEC协议最常用的载波频率是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。接收端在已经收到过完整数据帧后,如果再遇到9ms+2.25ms的重复码,就知道还是同一个键被按着,不需要重新解析命令。
2.3 地址码和反码:设备如何避免串扰
为什么NEC协议要把8位地址扩成16位(地址码+反码)来发?直接发8位不更节省时间吗?这里藏着两层考虑。
第一层是可靠性。前面说了,反码可以让接收端做异或校验。一次电平传输受干扰、被环境光误触发,都可能导致某一位翻转。如果不校验,一个误码就可能让电视把“音量+”当成“音量-”来执行。加了反码后,命令码和命令反码必须严格互补,否则直接丢弃,大大降低误动作概率。
第二层是设备区分。虽然没有遥控器的地址码是0x00,电视可能是0x04,DVD可能是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遥控器实测抓出来的码,仅供参考,不代表所有设备,实际以你自己抓包结果为准。
| 按键功能 | 命令码(十六进制) | 地址码(十六进制) | 备注 |
|---|---|---|---|
| 电源 | 0x45 | 0x00 | 最常见的电源键码之一 |
| 菜单 | 0x47 | 0x00 | 不少电视、盒子沿用 |
| 音量+ | 0x15 | 0x00 | 部分遥控器用0x16 |
| 音量- | 0x09 | 0x00 | 部分遥控器用0x19 |
| 频道+ | 0x18 | 0x00 | 公版常用 |
| 频道- | 0x08 | 0x00 | 公版常用 |
| 确认/OK | 0x14 | 0x00 | 有的设备是0x40 |
| 返回 | 0x43 | 0x00 | 有的设备是0x0D |
如果你在做万能遥控器或者学习型遥控器,建议直接用逻辑分析仪把自己的遥控器按键全抓一遍,生成一张键值表,存成配置文件。比在网上盲目找码表靠谱得多。
5. 我的一些经验和建议
做红外遥控项目多了,我给自己的几条规定:第一,新项目用到不熟悉的红外协议时,先花半小时抓波形,不凭记忆写解码代码。NEC协议再常见,不同品牌遥控器的细微偏差也足够让你踩坑。第二,解码代码里所有时间阈值都用范围判断,不要用精确等于,因为实际硬件的时钟偏差、接收头响应延迟都会让时序有几百微秒的变化。第三,校验必须做,反码校验是白给的可靠性,不要省。
另外一个容易被忽略的小技巧是:接收头的输出引脚上拉电阻不是随便选的。上拉太小,功耗高,且输出低电平可能拉不彻底;上拉太大,边沿变缓,影响时序测量。我习惯用4.7k到10k之间的值。如果MCU的GPIO内部有上拉且外部没有其他负载,用内部上拉其实也够用。
NEC协议看起来简单,但真要在产品上跑稳定,还是有不少细节需要打磨。希望这篇文章能帮你少走一些弯路。如果你在调试中遇到其他奇怪的时序问题,别急着怀疑协议文档,把波形抓出来,一条一条量,问题往往就自己浮出来了。