按下遥控器,电视亮起来。这个动作我从小到大做了上万次,直到某天我把逻辑分析仪的探头接到红外接收头的输出脚上,按下按键,看到屏幕上跳出的一串波形,才发现自己一直以为的“红外线”根本不是一束简单的光,而是一套规则极其清晰的编码方案。
这套方案里最出名、最普及的,就是红外协议中的NEC协议。说它是红外遥控领域的“普通话”一点都不夸张——大量电视、机顶盒、空调、风扇、音箱用的都是它或它的变体。这篇文章我从物理层的载波调制讲起,一路拆到帧格式、三种变体、内核驱动适配、手写解码器,最后聊一聊我实际调试时踩过的坑。
先提个醒:很多人搜“IR”会搜出来一堆不相干的东西,比如打印机的驱动问题、热成像分析软件之类的。它们和红外遥控的IR不是一回事,别被误导。本文所有内容只围绕红外遥控里的NEC协议展开,适合嵌入式开发者、电子爱好者、智能家居玩家,以及任何想搞懂遥控器工作原理的人。
1. 红外通信的物理基础:为什么遥控器偏爱38kHz的“抖动光”
1.1 你按下的不是“一束光”,而是一串加密的闪光
红外遥控用的是波长940nm左右的红外光,人眼看不见。发射端是一个红外发光二极管(IR LED),接收端是一个光敏二极管或一体化接收头。你按下按键时,主控芯片会控制这个LED按某种规律发光、熄灭,接收头感知到光的变化后,再把光信号变回电信号,交给后级解码。
问题来了:环境光里本身就含有大量红外成分。阳光里有,白炽灯有,甚至人体本身也在辐射红外线。如果只是简单地“亮了表示1,灭了表示0”,接收端根本分不清你按的是遥控器还是太阳。生活里做个类比:你在海边朝对面的人喊话,如果周围全是海浪声,对方听不清;你得想办法让自己的声音和噪声区分开。
红外的解决办法是:让LED以固定频率快速闪烁,比如每秒钟闪38000次,也就是38kHz。发送数据时,把要表达的信息“骑”在这种高频闪烁上。接收端只识别这个特定频率的光,其他频率一概当作噪声滤掉。这样阳光、灯光这些慢变化的光源就干扰不了它了。
1.2 38kHz载波背后的抗干扰设计
为什么偏偏是38kHz而不是几十赫兹或者几百赫兹?这里有两个关键原因。
第一,一体化接收头内部有带通滤波器,中心频率就是38kHz左右。带通滤波器的作用是只放过38kHz附近的光脉冲,其他频率全部衰减。这类接收头产量巨大、价格便宜,几毛钱一颗,全行业都在用,所以发射端自然就跟着统一到38kHz。现在你能买到的VS1838B、HS0038B、TSOP38238基本都是这个频率。
第二,38kHz刚好避开了环境光的干扰频段。日光灯镇流器会产生100Hz到几十kHz的噪声,但能量集中在低频段;阳光是宽谱连续光,经过接收头内部的自动增益控制(AGC)和带通滤波后,基本不会触发误动作。当然,如果阳光直射接收头,接收距离还是会明显缩短,这个后面讲坑的时候再说。
顺便说一句,不是所有遥控器都用38kHz。很多飞利浦设备用36kHz,部分日系设备用40kHz。接收头也有对应型号,比如TSOP38236、TSOP38240。用错频率不是完全收不到,但距离会急剧下降,因为信号落在滤波器通带边缘了。
1.3 一体化接收头:把调制信号还给逻辑电平
NEC协议里的“调制”和“编码”是两个层次的东西。编码是逻辑层的事,告诉接收方“这一位是0还是1”;调制是物理层的事,决定光怎么发出来。很多初学者搞混,看到时序图里的高电平就以为接收头输出高电平,实际正好相反。
典型的一体化接收头有三个引脚:VCC、GND、OUT。OUT在空闲时输出高电平;收到38kHz红外光时,内部解调电路把载波剥掉,输出低电平。也就是说,接收头的输出是发射信号的反相版本。发射端发引导码“9ms载波+4.5ms空闲”,你从接收头OUT脚量到的就是“9ms低电平+4.5ms高电平”。这一点非常容易看反,我见过太多人因此卡了几天。
接收头输出的信号直接接单片机GPIO、逻辑分析仪或者电平转换电路都行,因为它的输出已经是标准逻辑电平了,不需要再做放大整形。这也是为什么做红外遥控接收比做射频简单那么多——物理层被一颗芯片打包好了。
| 项目 | 典型值 | 说明 |
|---|---|---|
| 载波频率 | 38kHz | NEC协议最常用,也有36/40kHz |
| 发射波长 | 940nm | 近红外,人眼不可见 |
| 接收头空闲输出 | 高电平 | 收到载波后输出低电平 |
| 典型接收头 | VS1838B / HS0038B | 引脚兼容,带宽略有差异 |
2. NEC协议帧格式:一次按键,32比特,不多不少
2.1 一帧数据的标准结构
NEC协议一帧完整数据由四部分组成:引导码(Leader Code)、地址码、地址反码、命令码、命令反码,最后还有一个结束位。这里“一帧”对应的就是“你按一次按键”所发送的完整信息。
整个帧的结构可以用下面这张表概括:
| 组成 | 长度 | 内容 | 示例值 |
|---|---|---|---|
| 引导码 | 13.5ms | 9ms载波 + 4.5ms空闲 | 固定 |
| 地址码 | 8bit | 设备地址,通常表示厂商或设备类型 | 0x00 |
| 地址反码 | 8bit | 地址码按位取反 | 0xFF |
| 命令码 | 8bit | 按键功能码 | 0x45 |
| 命令反码 | 8bit | 命令码按位取反 | 0xBA |
| 结束位 | 560us | 一个560us的载波脉冲 | 固定 |
地址码在前面的叫标准NEC,地址码16位的叫扩展NEC,这个区别后面的章节单独讲。标准的8位地址加上8位反码,一共能表示256个不同设备,命令码8位能表示256种按键,对绝大多数消费电子来说完全够用。
2.2 引导码、结束位和每一位的“宽度”含义
NEC协议的时序参数非常规整,全部以560微秒为基础单位。560us这个数不是拍脑袋定的,它是38kHz载波的整数倍周期,便于发射端用定时器精确产生。
具体来说:
- 引导码:高电平持续9ms,然后低电平持续4.5ms。9ms约等于16个560us,是所有时序里最长的,解码时用它来对齐一帧的起始位置。
- 逻辑0:高电平560us,低电平560us,总共1.125ms。
- 逻辑1:高电平560us,低电平1.69ms,总共2.25ms。
- 结束位:一个560us的高电平脉冲,表示帧结束。
看出规律了吗?逻辑0和逻辑1的高电平宽度相同,都是560us,区别在于后面的低电平宽度。0的低电平是560us,1的低电平是1680us(即3个560us)。在接收头输出端看,就是低电平的持续时间不同。这种用脉冲位置来区分0和1的编码方式,术语叫脉冲位置调制(PPM)。
为什么要用位置区分而不是用脉冲宽度区分?一个很实际的好处是接收端不太容易受到LED老化、电池电压下降的影响。LED亮度变弱时,接收头输出的脉冲宽度变化不大,但幅度变化大;位置调制对幅度不敏感,抗干扰能力更强。
2.3 反码校验:为什么要发两次取反
地址码后面跟着地址反码,命令码后面跟着命令反码,四段加起来32bit。反码的作用就是校验:接收端把收到的地址码和地址反码逐位比对,如果每一位都取反相等,就认为地址部分有效;命令码同理。
比如地址码是0x00,二进制是00000000,反码就是11111111,也就是0xFF。命令码0x45(01000101)的反码是0xBA(10111010)。这种校验虽然简单原始,但它能查出绝大多数传输错误。红外遥控是单向通信,接收端不可能主动让发射端重发,所以发送方在一帧里就把校验信息带上,接收端错了就丢弃这一帧,等下一次重复码或者用户再次按键。
我发现一个很容易被忽略的细节:有些协议分析工具在界面上显示的“原始值”是反码已经换算完的十六进制,有些则直接显示反码本身。写代码做日志的时候一定要记录原始顺序,不然后面排查问题会非常痛苦。
2.4 常见的NEC协议功能码
“功能码”是命令码的具体值,它决定了一个按键对应什么操作。很多人搜“常见的nec协议功能码”其实就是想查这个表。这里有个前提必须搞清楚:NEC协议只定义了帧结构和传输规则,并没有规定0x45必须是电源键。功能码是每个厂商自己定的,但大量国产遥控器共用了一套源自某知名方案的映射,所以下面这张表在很多设备上是通用的:
| 功能码(十六进制) | 常见功能 | 备注 |
|---|---|---|
| 0x45 | 电源 / 待机 | 最常用的功能码 |
| 0x46 | 菜单 / 设置 | 有的设备是信号源 |
| 0x47 | 音量+ | 部分设备是频道+ |
| 0x44 | 音量- | 部分设备是频道- |
| 0x40 | 频道+ | 部分设备是返回 |
| 0x43 | 频道- | 部分设备是确认 |
| 0x07 | 静音 | 比较常见的静音码 |
| 0x0D | 输入/信号源 | 常见于电视遥控器 |
| 0x16 | 上方向 | 菜单导航用 |
| 0x15 | 下方向 | 菜单导航用 |
注意,同一颗键在不同品牌遥控器上功能码不同,这是正常的。真正规范的做法是先抓自己手头遥控器的波形,建立一张“功能码→按键名”的映射表,而不是背一张通用表。第5章我会演示怎么从波形里把功能码读出来。
3. 标准NEC、扩展NEC与重复码:三种波形一眼分辨
3.1 标准NEC和扩展NEC到底差在哪
网上资料经常把NEC协议分成标准和扩展两种,理解起来有点绕,我用最简单的说法来区分。
标准NEC的地址部分一共16bit,格式是:地址码8bit + 地址反码8bit。也就是说,真正有效的地址只有8bit,剩下的8bit纯粹用于校验。这种方式地址空间只有256个。
扩展NEC把地址部分改成了16bit有效地址,不再发送地址反码。格式变成:地址低8bit + 地址高8bit,然后直接接命令码和命令反码。16bit地址能表示65536个不同设备,适合产品线复杂的厂商,比如某些牌子需要在一个遥控器里区分电视、机顶盒、音响等多个设备。
命令部分两种格式完全相同:命令码8bit + 命令反码8bit,所以命令空间都是256个。从校验强度上说,标准NEC的地址部分有反码校验,更可靠;扩展NEC地址部分没有校验,靠命令反码来间接发现错误,错误概率略高,但在实际应用中差别不大。
3.2 重复码:长按时的“连发信号”
按住遥控器按键不松手,你会观察到什么现象?以标准NEC为例:第一次发送一帧完整数据,然后每隔110ms左右发送一个“重复码”,直到你松手。这个重复码和时间间隔是为了解决两个问题。
第一个问题是接收端的防抖。如果按键信号只发一次,接收端可能会因为干扰而丢掉那一帧,用户就得再按一次。长按重复机制让接收端在固定周期内必然收到一个信号,体验更可靠。
第二个问题是音量调整这种需要连续响应的场景。你按住音量+想从20加到60,如果每次只发一帧就不再发,那一次长按只能加一格音量,显然不合理。重复码让接收端能持续识别“按键仍然被按住”,从而实现快速连续调整。
重复码的波形很短:9ms载波引导 + 2.25ms空闲 + 560us载波,然后进入漫长的空闲等待下一个重复码。注意它和完整帧的引导码不一样——完整帧引导码的空闲部分是4.5ms,重复码只有2.25ms。解码时可以靠这个区分是“第一次按键”还是“重复按键”。
3.3 拿到波形后怎么快速判断是哪种变体
用手里的逻辑分析仪抓一段波形,怎么判断它是标准NEC、扩展NEC还是重复码?我通常按下面这个顺序看。
先看引导码:如果低电平(接收头输出端)是4.5ms,说明这是一帧完整数据;如果是2.25ms,说明这是重复码,不用继续看了。
再数引导码后面紧跟着的32bit数据。第9位到第16位这一组(从0开始数是bit8到bit15),如果它正好是第0到第7位的逐位取反,那一定是标准NEC;如果第8到第15位是另一个任意地址的高8位,不是取反关系,那就是扩展NEC。
| 特征 | 标准NEC | 扩展NEC | 重复码 |
|---|---|---|---|
| 引导码后空闲 | 4.5ms | 4.5ms | 2.25ms |
| 地址部分 | 8bit地址 + 8bit反码 | 16bit地址,无反码 | 无数据 |
| 数据总量 | 32bit | 32bit | 无 |
| 典型用途 | 廉价遥控器、多数电视 | 高端电视、多设备遥控 | 所有NEC系长按场景 |
实际抓包中还有一种特殊情况:某些遥控器的引导码会略有偏差,比如9ms变成9.2ms,4.5ms变成4.4ms。这种偏差在接收头的容忍范围内,解码器一般按“大于3ms就认为是引导码”这样的阈值来判断,不必要求恰好等于9ms。
4. 实战:在RK3576上适配红外遥控器
4.1 硬件连接与最小电路
最近不少开发板用户在做RK3576适配IR遥控器,这步其实比想象中简单。RK3576本身没有专用的红外接收外设,但它的GPIO可以配置为中断输入,配合内核的gpio-ir-recv驱动一样能实现标准的红外遥控接收。
硬件连接上,一体化红外接收头的三个引脚分别接:
- VCC接3.3V或5V,注意看接收头型号的供电范围。VS1838B一般支持2.7V到5.5V,直接用3.3V就行,和RK3576的GPIO电平匹配。
- GND接开发板地线。
- OUT接一个空闲GPIO,我习惯选带内部上拉的引脚,省得外部再挂上拉电阻。
如果用5V供电,OUT引脚输出的高电平就是5V,直接进3.3V的GPIO有风险,需要分压或者选3.3V供电的接收头。很多新手在这里烧过引脚。用3.3V供电是最省事的方案。
接收头最好尽量远离板上的高频干扰源,比如WiFi天线、DC-DC电感。红外接收头对电磁干扰不是非常敏感,但距离太近时,DC-DC的开关噪声可能让接收头输出毛刺,表现为“没按键却收到乱码”。
4.2 内核与设备树配置
RK3576跑Linux系统,适配红外遥控主要依赖内核里的RC(Remote Controller)子系统。需要确保内核开启了以下配置:
CONFIG_RC_CORE=y CONFIG_RC_DEC_NEC=y CONFIG_IR_GPIO_CIR=yCONFIG_RC_CORE是遥控子系统的总开关,CONFIG_RC_DEC_NEC是NEC协议解码器,CONFIG_IR_GPIO_CIR是GPIO红外接收驱动。这三个缺一不可。在SDK的内核配置里一般已经默认开启,如果你用的是裁剪比较狠的发行版内核,需要自己确认一下。
设备树里新增一个节点,大概是这样:
&pinctrl { ir_int: ir-int { rockchip,pins = <0 RK_PB1 RK_FUNC_GPIO &pcfg_pull_up>; }; }; &gpio0 { ir_receiver: ir-receiver { compatible = "gpio-ir-recv"; pinctrl-names = "default"; pinctrl-0 = <&ir_int>; gpios = <&gpio0 RK_PB1 GPIO_ACTIVE_LOW>; linux,rc-map-name = "rc-anytime"; status = "okay"; }; };compatible用的gpio-ir-recv,驱动会根据中断边沿把GPIO上的电平变化上报给RC子系统。GPIO_ACTIVE_LOW表示接收头空闲输出高电平、收到信号时拉低,对应的就是反相行为。pinctrl里配置了内部上拉,保证空闲时GPIO不会悬空。
linux,rc-map-name指定了默认按键映射表。rc-anytime是一张现成的NEC遥控器映射,如果你的遥控器功能码不在里面,之后可以用ir-keytable动态修改。
4.3 从中断到按键事件的内核解码流程
设备树配置好之后,理解一下内核里数据是怎么流动的,对排查问题帮助很大。整个链路可以概括为:
- 物理层:红外LED发光,接收头OUT引脚输出对应的高/低电平。
- 中断层:GPIO电平变化触发gpio-ir-recv驱动的中断回调。
- 事件层:驱动把边沿间隔转换成ir_raw_event,写入RC子系统的接收队列。
- 解码层:RC子系统调用已注册的协议解码器,比如nec_decode,对原始边沿脉冲序列进行匹配。
- 输入层:解码成功后,把scancode发给input子系统,根据键位映射表转换为Linux标准按键码,最终通过/dev/input/eventX上报给用户空间。
很多人在设备树改了节点之后发现没反应,先用evtest去看有没有input事件。如果有事件但报告的是KEY_RESERVED之类,说明映射表没对上;如果连事件都没有,就要往前查,用ir-keytable看是否收到了原始脉冲。
从调试效率上讲,建议按“原始脉冲 → 解码键值 → 按键事件”三层逐步排查。ir-keytable -t能同时看到扫描码,是定位问题最快的工具。
4.4 keymap映射与ir-keytable调试
系统起来后,先装ir-keytable工具,一般发行版包里叫v4l-utils或者ir-keytable。然后执行:
ir-keytable -p NEC -t这条命令把接收模式设为NEC协议,并进入测试模式。按一下遥控器按键,终端会打印类似下面的信息:
29.043294: scancode 0x00ff45ba (NEC)scancode就是这一帧的原始扫描码,0x00ff45ba里四组字节分别对应地址码、地址反码、命令码、命令反码。如果你的遥控器按键没有输出,检查接线和内核配置;如果输出了但按键在系统里没反应,那就是映射表的问题。
映射表文件在Linux上通常放在/lib/udev/rc_keymaps/,比如rockchip.toml。文件内容格式:
# table rockchip, type: NEC 0x00ff45ba KEY_POWER 0x00ff46b9 KEY_VOLUMEUP 0x00ff47b8 KEY_VOLUMEDOWN把scancode和对应的KEY_事件写上去,然后执行:
ir-keytable -c -w /lib/udev/rc_keymaps/rockchip.toml-c是清空旧表,-w是写入新表。写入成功后,用evtest打开/dev/input/eventX,选择对应的input设备,按按键应该能看到KEY_POWER这类标准事件。
一个很常见的坑是:ir-keytable -t显示出来的scancode可能带前后缀,不同版本的内核对scancode的编码方式不完全一样。如果你看到的是0x00ff45ba,映射表里就写0x00ff45ba,不要自己截断,保持一致即可。
5. 手写一个NEC解码器:逻辑分析仪下的详细拆解
5.1 用逻辑分析仪把遥控器的“话”录下来
内核的解码流程对很多人来说是个黑盒,想真正理解NEC协议,强烈建议自己抓一次波形。你只需要一个逻辑分析仪,把探头夹在接收头OUT引脚和GND上,然后按下遥控器按键。
我用的逻辑分析仪采样率设到1MHz就够,38kHz的载波虽然采样不到完整细节,但接收头输出的是解调后的电平信号,根本没有载波成分,所以不需要很高的采样率。协议层面的时间分辨率是560us,1MHz采样绰绰有余。
抓到的波形大概是这样的:平时一直处于高电平,按下按键后先是9ms的低电平(接收头输出端),然后4.5ms高电平,接着是一串低电平宽度不同的脉冲组合。这就是一帧完整NEC数据。对应到发射端的原始逻辑,接收头输出低=正在发载波。
5.2 从波形手工解析出一帧完整按键数据
我们拿一帧真实的遥控器数据来手工解析。假设抓到的时序如下:
- 9ms 低电平
- 4.5ms 高电平
- 560us 低电平,560us 高电平(这对应一个逻辑0)
- 560us 低电平,560us 高电平(又一个逻辑0)
- 560us 低电平,1.69ms 高电平(这是逻辑1)
- 后面类推...
记法其实很简单:每遇到一个“低电平560us”,看后面的高电平宽度。高电平560us记0,高电平1.69ms记1。数满32个bit,每8bit一组,转成十六进制。
比如某遥控器抓到的32bit是:
00000000 11111111 01000101 10111010转成十六进制就是0x00 0xFF 0x45 0xBA。第一字节0x00是地址码,第二字节0xFF是地址反码,第三字节0x45是命令码,第四字节0xBA是命令反码。0x45和0xBA互为取反,校验通过,所以这帧数据有效。查功能码表,0x45在很多方案里对应电源键,这就能解释为什么按下它电视机会关机。
5.3 单片机上的状态机解码思路
理解了协议后,在单片机上实现解码就顺理成章了。最常用的方法是:把GPIO配置为双边沿中断,每次中断读取定时器计数值,然后根据状态机判定当前收到的是引导码、逻辑0、逻辑1还是重复码。
我给出一个用C语言描述的核心逻辑:
#define T_LOW_SHORT 560 // 单位us #define T_HIGH_SHORT 560 #define T_HIGH_LONG 1690 #define T_LEADER_LOW 9000 #define T_LEADER_HIGH 4500 uint32_t frame_bits; int bit_count; enum { STATE_IDLE, STATE_LEADER, STATE_DATA, STATE_REPEAT } state; void ir_edge_isr(int level, uint32_t pulse_us) { switch (state) { case STATE_IDLE: if (level == 0 && pulse_us > 8000) { // 检测到引导码低电平段 state = STATE_LEADER; } break; case STATE_LEADER: if (level == 1 && pulse_us > 3500) { // 引导码后的高电平段 bit_count = 0; frame_bits = 0; state = STATE_DATA; } else if (level == 1 && pulse_us < 3000) { // 2.25ms高电平,说明是重复码 state = STATE_REPEAT; } else { state = STATE_IDLE; } break; case STATE_DATA: if (level == 1) { // 低电平固定是560us,高电平决定了0/1 frame_bits <<= 1; if (pulse_us > 1000) { frame_bits |= 1; // 高电平1.69ms => 1 } // 高电平560us => 0,不用额外处理 bit_count++; if (bit_count >= 32) { handle_frame(frame_bits); state = STATE_IDLE; } } break; case STATE_REPEAT: // 收到重复码,可以触发长按事件,也可以忽略 state = STATE_IDLE; break; } }状态机的关键点在于:接收头输出反相,所以“低电平”对应的是原始帧里的载波段。D层里的9ms低电平对应引导码的载波段,4.5ms高电平对应引导码后的空闲段。如果你把逻辑搞反,整个解码永远不会成功。
单片机上用定时器捕获模式会更容易,GPIO中断里只记录电平变化的时间戳,解码逻辑放在主循环里跑。这样可以避免在中断里做太多浮点运算和时间单位换算。
5.4 一个容易被忽略的细节:结束位和字节顺序
抓完整帧的时候,32bit数据收完之后还有一个560us的低电平脉冲,这就是结束位。有些资料把结束位算进数据位,有些不算。判断一个解码器写没写对,就看你有没有正确处理这个结束位,以及是否在收满32bit后立刻停止。如果你收完32bit还在等更多边沿,状态机可能会把结束位当成下一帧的引导码导致状态错乱。
字节顺序方面,NEC发送时是低位先发还是高位先发?市面上既有MSB先发的实现,也有LSB先发的实现。严格按NEC原始规范是LSB先发,但很多厂商用了MSB顺序,解码时如果不一致,你会发现命令码整体反转,比如0x45变成0xA2。遇到这种情况,别怀疑协议错了,先检查字节序。
6. 实际调红外时最容易踩的坑
6.1 按了没反应:先怀疑物理链路,别急着改代码
“遥控器按了没反应”是我遇到最多的求助。很多人的第一反应是解码配置错了,于是疯狂改设备树、换keymap,折腾半天发现根本不是软件问题。我建议遇到这种情况,按下面的顺序排查。
第一步,用手机摄像头看遥控器发射头。手机摄像头对红外光敏感,按下按键时屏幕上能看到白色或紫色的光点闪烁。看不到光点,基本可以确定是遥控器本身的问题:电池没电、电池接触片氧化、晶振虚焊、发射管压降异常。
第二步,确认接收头供电和电平。用万用表量接收头VCC和GND之间电压是否正常,再量OUT引脚空闲时是否高电平。如果OUT空闲时是低电平,大概率是接收头的型号方向接错,或者芯片已经损坏。
第三步,用ir-keytable -t看有没有原始扫描码。有扫描码说明物理链路和内核解码都正常,问题在keymap映射;没有扫描码才需要回头查设备树和GPIO配置。很多人跳过了前两步直接改代码,效率极低。
第四步,检查是不是协议不匹配。NEC协议虽然普及,但飞利浦的RC5、RC6,索尼的SIRC,以及一些日系厂商的自家协议也很常见。ir-keytable -t能显示出“无法识别”的提示,如果你发现按键按下去没有任何输出,很可能你手头的遥控器根本不是NEC协议。
6.2 重复码带来的“一次按键、两次触发”
长按处理不当会出现一个非常烦人的问题:明明只按了一次按键,系统却收到了两次甚至多次按键事件。原因是很多遥控器在按下瞬间发送完整帧之后,会在约110ms后补一个重复码。如果你的解码器把重复码也当成一次完整按键上报,就等于一次物理按键被解码成多次逻辑按键。
解决思路是在应用层做一个简单的抖动过滤:记录上一次按键的scancode和时间,如果下一次相同scancode在100ms内到达,就认为是重复码或抖动,丢弃处理。内核RC子系统的ir-keytable其实已经内置了repeat机制,但如果你是自己写解码器,一定要把重复码和完整帧区分开,不要一上来就把所有帧都当作新按键。
还有一个相关现象是“按住音量+跳两格”。这是重复码时间间隔较短造成的。如果应用层需要“长按连续响应”,应该把repeat帧转换成“持续按住”的状态,而不是每次都当作新的按键事件。
6.3 厂商魔改:NEC并不“标准”
NEC协议历史悠久,各厂商在实现时做“微调”的情况非常多。有些厂商把引导码宽度改了,比如9ms变成13.5ms;有些把数据位的时间放宽,比如低电平从560us变成600us;还有些用了完全不同的载波频率。这些魔改版本在协议上仍然能被称为“NEC系”,但如果你用标准NEC的参数去解码,可能会失败。
最典型的是空调遥控器。空调遥控器功能极其复杂——温度、模式、风速、摆风、定时——256个命令码根本不够用。很多空调厂商在NEC基础上扩展了帧格式:有的把命令码扩成16bit,有的把一帧扩成48bit甚至64bit,有的干脆用两帧组合表示一条完整指令。这也是为什么你用电视遥控器的解码库去解空调遥控器,经常解出一堆毫无意义的数据。
碰到这种遥控器,我的建议是不要死磕标准NEC解码,而是用逻辑分析仪把原始时序完整录下来,分析它的帧结构和数据位规律,然后针对性地写解码逻辑。很多开源红外库比如IRremoteESP8266就收录了大量空调厂商的编码格式,直接参考比自己从头摸索快得多。
6.4 接收头型号与电平极性
市面上常见的一体化接收头,比如VS1838B、HS0038B、TSOP38238,电气特性基本兼容,但细节差异会影响产品表现。
第一,带宽不同。VS1838B的通带比较宽,对38kHz附近的信号都敏感,好处是兼容非标遥控器,坏处是更容易被环境光和其他红外源干扰。TSOP系列通常带通特性更尖锐,抗干扰更强,但对频率偏差大的遥控器兼容性稍差。做产品选型时,如果使用环境里红外干扰多,优先考虑TSOP系列。
第二,输出极性虽然有标准,但有些廉价接收头模块会把输出反相,或者内部已经加了下拉电阻,导致空闲电平不确定。用之前一定先拿逻辑分析仪或者万用表量一下空闲电平和触发极性。
第三,供电电压影响接收距离。接收头在3.3V下工作正常,但距离可能比5V供电时缩短20%到30%。如果你的产品对遥控距离有硬性要求,供电和接收头选型要一起考虑。发射端的驱动电路影响更大——红外LED的峰值电流直接决定发射强度,用三极管或MOS管驱动、串联限流电阻把峰值电流抬高到100mA以上,距离能有明显改善。
我自己调红外接收时还有个习惯:在接收头OUT脚上并联一个100nF的电容,能滤掉一部分高频毛刺。虽然这会略微降低信号边沿的陡峭程度,但对解码成功率影响很小,对付恶劣电源环境很管用。
最后分享一个五年来一直用的调试套路:拿到一块新板子,我不会先写解码代码,而是先把逻辑分析仪挂上,按几个按键看看有没有波形、波形长什么样、是什么协议。就像接手一个新项目前先看输入输出接口,而不是一头扎进实现细节。红外这种东西,波形不会骗人,态度诚恳地面对波形,所有疑难杂症都会变得很好查。