背景知识
在蓝牙遥控器未普及前,基本上所有的遥控类设备都会用到红外遥控器,红外遥控器的工作原理,在之前的一篇文章中已经提及,有兴趣的话,可以翻看《【换取手持数字示波器】红外触摸框及各类红外设备的工作原理》这篇文章。而在蓝牙遥控普及后,受限于待机功耗要求,大部分蓝牙遥控器的电源按键,其实还是走的红外遥控。
而红外遥控协议编码中,使用最多的编码为NEC编码,了解此种编码,有助于在后续的调试中快速理解供应商规格信息,遇到问题后快速定位和解决问题。
红外遥控编码波形测量方法
红外遥控编码波形的测量,可以直接量红外接收头端的波形,量出后便可根据波形分析对应的编码格式。这部分,如果有逻辑分析仪的话,可以直接使用逻辑分析仪的协议解码功能解出编码,帮助分析问题。
由于没有示波器,仅在网上找到了有问题的波形图(不知为何,网上能找到的示波器量到的波形图都是一样的问题),具体是什么问题,在后面的讲解后给出。
图1
NEC编码格式
总体规则
从宏观的规则来看,NEC编码可以分为按键码和重复码两种类型。按键码,顾名思义,就是按键按下时发出来的编码,此编码包括当前遥控器当前按键的所有信息。而重复码,可以这么理解,就是在发出了按键码后,为了减少数据量所制定的一种编码,此编码不含任何按键信息。
其大致的波形如下:
图2
按键码解析
从宏观上来看,按键码所包含的信息尤为重要,他的正确与否直接决定了重复码波形出来时,接收端的响应按键的功能。因此,按键码中增加了不少错误检查信息。具体格式如下:
启动码 + 数据码
而数据码又可以细分为
地址码 + 地址码位反 + 按键码 + 按键码位反
因此完整的按键码格式为:
启动码 + 地址码 + 地址码位反 + 按键码 + 按键码位反
注:所有的波形都是低位在前,高位在后
各码的作用:
启动码:此编码直接通知从机端准备接收遥控编码
地址码:可以通俗的理解为厂商定义的遥控器型号编码,或者厂商码,虽然没有哪家这么说明。既然是型号编码(厂商码),那就意味着,同一个遥控器上所有按键的地址码都是同一个。
地址码位反:可以理解为校验码,当这一位与地址码位反的值一致,则说明地址码有效
按键码:这个码对应遥控器上具体的按键,也就是说,遥控器上每个按键的按键码都会不一样。
按键码位反:功能同地址码位反
启动码格式
启动码为图一中红色部分,其具体格式为,9ms低电平 + 4.5ms高电平,具体波形如下:
图3
数据码格式
数据码中,所有数据按位看,可以拆分为逻辑0和逻辑1。
逻辑0的波形为,560us低电平 + 560us高电平,具体波形如下:
图4
逻辑1的波形为,560us低电平 + 1690us高电平,具体波形如下:
图5
而地址码,地址码位反,按键码,按键码位反各占1个字节。
图1问题点分析
有了以上知识,我们已经可以分析图1对应的编码了,从图1中解析出来的二进制数据为0111 0000 0111 0000 0100 1000 1011 0111,而根据前面的知识,我们可以解析出来的十六进制数据为0xED120E0E,而根据数据码中各码的分布规则,我们可以拿到以下信息:
地址码:0x0E
地址码取反:0x0E
按键码:0x12
按键码取反:0xED
到这里,相信大家能看出问题来了,地址码取反不应该是0xF1嘛,为何在这里是0x0E。实际上,标准的NEC码遥控器,虽然地址码写的是2字节,但是实际上两个字节数据是位反相等的。有兴趣确认的,可以找遥控器产商要到他们家NEC公版遥控器规格书了解,或者在网上找各个知名主机厂释放出来的遥控器规格书。
重复码解析
前面已经提到,重复码不包含地址码和按键码信息,因此重复码就变得尤其简单,甚至可以理解为,重复码波形类似于启动码,只是高低电平的持续时间和波形多少差异罢了。实际上,重复码确实长这样,重复码的波形为9ms低电平 + 2.25ms高电平,如下图:
图6
各码值间隔时间
其实讲到这,还有一个知识点并未提及,按键码和重复码有了,那按键码和重复码之间的间隔是多少,重复码与重复码之间的间隔是多少并未提及。在NEC标准中,码值与码值之间的间隔大致为109ms左右,即图2中的110ms。
NEC解码实现
这里先列一段TI在十三年前公开源码中所带的代码,具体代码如下:
#define MAX_TIMEOUT 10000 #define MIN_ZERO 600 #define MIN_ONE 1000 #define MAX_ONE 2000 #define MAX_TIMINGS 33 typedef struct { unsigned char nAddress; unsigned char nCommand; } TIR; TIR ReadIR_NEC(void) { unsigned int n; unsigned int nBit; unsigned int nData; unsigned int nCount; unsigned char pData[4]; unsigned int pIRTiming[MAX_TIMINGS]; TIR necData; necData.nAddress = 0; necData.nCommand = 0; for (n = 0; n < MAX_TIMINGS; n++) { pIRTiming[n] = 0; nCount = 0; while ((nCount < MAX_TIMEOUT) && !(P1IN_bit.P2)) nCount++; if (nCount >= MAX_TIMEOUT) break; while ((nCount < MAX_TIMEOUT) && (P1IN_bit.P2)) nCount++; if (nCount >= MAX_TIMEOUT) break; pIRTiming[n] = nCount; } if (n != MAX_TIMINGS) return necData; // a timeout has occurred -> not a valid NEC code for (n = 0; n < 4; n++) { nData = 0; for (nBit = 0; nBit < 8; nBit++) { nCount = pIRTiming[1 + n * 8 + nBit]; if (nCount < MIN_ZERO) break; // not a valid zero -> not a valid NEC code if (nCount < MIN_ONE) continue; // a valid zero -> continue with next bit if (nCount > MAX_ONE) break; // not a valid one -> not a valid NEC code nData |= 1 << (7 - nBit); } if (nBit != 8) return necData; // bit timing not within margins -> not a valid NEC code pData[n] = nData; } if (pData[0] != (~(pData[1]) & 0xFF)) return necData; // test NEC address complement and return if not correct if (pData[2] != (~(pData[3]) & 0xFF)) return necData; // test NEC command complement and return if not correct necData.nAddress = pData[0]; necData.nCommand = pData[2]; return necData; } void DispatchIR_NEC(TIR necData) { if (!necData.nAddress && !necData.nCommand) return; if (necData.nAddress != NEC_ADDRESS) return; switch (necData.nCommand) { case NEC_CMD_VOL_UP: Buttons(volumeUp); break; case NEC_CMD_VOL_DN: Buttons(volumeDown); break; case NEC_CMD_MUTE: Buttons(muteMode); break; default:; } } void ProcessIR(void) { TIR necData = ReadIR_NEC(); DispatchIR_NEC(necData); } #define DEBOUNCE 100 #pragma vector = PORT1_VECTOR __interrupt void P1_Interrupt(void) { volatile unsigned int i; unsigned char P1IN_Temp = P1IN; /* IR hook start */ if (P1IFG & 0x04) { ProcessIR(); P1IFG = 0x00; return; } /* IR hook end */ }
对于这段代码,我吐槽N多,
1. 中断里面直接干活是什么操作?
2. 中断里面硬延时好几毫秒是什么操作?
3. 中断里面计时直接用变量计数又是什么操作?
4. 红外干扰为啥完全不考虑?
......
针对这些问题,我在实际项目中彻底改写了实现,由于公司代码保密的要求,这部分代码不对外开放。这里仅提供思路:
1. 中断处理挪至主函数实现,不在中断中处理
2. 在未收NEC编码时,使能GPIO下降沿触发。
3. 在收到gpio下降沿中断时,切换为边沿触发并启用计时器
4. 在后续触发中,记录计时器信息并根据计时器信息决定是否丢弃某些数据甚至并重置接收