三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

红外通信原理与实战:从38kHz载波到NEC协议解码

红外通信原理与实战:从38kHz载波到NEC协议解码

1. 项目概述:从遥控器到微控制器,红外通信的桥梁

每次按下电视遥控器,或者用手机的红外功能控制空调,背后都是一套看似简单实则精妙的通信系统在运作。红外接收器,这个封装在黑色环氧树脂里、前端带有一个透明或淡紫色滤光片的小元件,就是这套系统的“耳朵”。它负责捕捉空气中那束肉眼看不见的红外光脉冲,并将其转换为我们单片机能够理解的数字信号。对于嵌入式开发者,尤其是刚接触STM32或51单片机的朋友来说,理解并应用红外接收器,是打通“感知”物理世界信号、实现非接触控制的关键一步。这不仅仅是复制一段解码库那么简单,从理解38kHz载波调制解调的原理,到处理实际电路中可能遇到的干扰,再到针对不同单片机平台优化代码,每一步都藏着从理论到实践的细节。今天,我们就来彻底拆解这个项目,无论你是想用STM32做一个万能遥控器,还是用51单片机实现一个简单的红外遥控小车,都能从这里找到可落地的方案和避坑指南。

2. 红外通信系统核心原理深度拆解

红外通信并非直接发送我们想要的“0”和“1”数据。为了抵抗环境光(尤其是日光灯)的干扰,提高传输的可靠性和距离,它采用了一套名为载波调制的机制。理解这一点,是正确使用红外接收器的前提。

2.1 载波调制:为什么是38kHz?

发送端(红外发射管)并不会持续发光来表示“1”,熄灭来表示“0”。这样做极易被环境光淹没。实际的做法是:当需要发送信号时,发射管会以固定的频率快速闪烁(亮灭),这个频率就是载波频率,最常见的是38kHz(周期约26.3微秒)。而我们要传输的数据,是通过控制这一串载波脉冲的有无(即“包络”)来表达的。

为什么偏偏是38kHz?这背后是工程上的权衡。频率太低(如几kHz),容易受到白炽灯等光源的谐波干扰;频率太高(如几百kHz),则对发射管和接收头的响应速度要求高,功耗增大,且传输距离会缩短。38kHz是一个在抗干扰能力、器件成本、功耗和传输距离之间取得良好平衡的点。红外接收头内部集成了PIN光电二极管、前置放大器、带通滤波器和解调电路。这个带通滤波器的中心频率就设计在38kHz,它只允许以38kHz为中心、一定带宽内的信号通过,从而极大地抑制了环境光的直流分量和其他频率的噪声。

2.2 信号编码:NEC协议实战解析

载波解决了“如何可靠地发送”的问题,而编码协议则规定了“数据如何组织”。NEC协议是消费电子领域应用最广泛的红外协议之一,以其简单可靠著称,非常适合作为学习样板。

一个完整的NEC码帧由以下几部分组成:

  1. 引导码:一个9ms的38kHz载波脉冲,跟着一个4.5ms的低电平。这是帧的起始标志,接收头通过检测到这个独特的长脉冲/长间隔组合,来判定一帧数据的开始,并自动进行增益控制。
  2. 用户码:16位,用于区分不同的设备制造商,防止你的电视遥控器误操作了邻居的空调。
  3. 用户反码:16位,是用户码的按位取反,用于校验用户码的正确性。
  4. 数据码:8位,即具体的按键值。
  5. 数据反码:8位,是数据码的按位取反,用于校验数据码。

“0”和“1”的表示:在NEC协议中,每一位数据的周期是固定的1.125ms。其中:

  • 逻辑“0”:一个560微秒的载波脉冲,后跟一个560微秒的低电平。
  • 逻辑“1”:一个560微秒的载波脉冲,后跟一个1.69ms的低电平。

可以看到,“0”和“1”的区别在于低电平的持续时间不同。接收头在解调后,输出的就是这种高低电平的波形。我们的单片机需要通过测量两个下降沿之间的时间(即低电平的宽度),来判断接收到的是“0”还是“1”。

注意:很多红外接收头(如VS1838、HS0038)的输出是反相的。即当收到38kHz载波时,输出低电平;无载波时,输出高电平。因此我们实际在单片机引脚上捕获到的“引导码”可能是一个9ms的低电平跟着一个4.5ms的高电平。在编写解码程序时,务必要确认你所用接收头的输出极性,逻辑分析仪是确认这一点的最佳工具。

2.3 红外接收头关键参数选型

市面上常见的红外接收头有三只引脚:VCC(3.3V/5V)、GND和OUT(信号输出)。选型时需关注:

  • 载波频率:必须与遥控器发射频率匹配,通常是38kHz,也有33kHz、40kHz等。
  • 供电电压:常见有5V和3.3V兼容型。为STM32供电时,优先选择支持3.3V的型号。
  • 接收角度:视角越宽,对准要求越低,但可能更容易受到侧面干扰。
  • 封装:常用的是直插和贴片。直插(如HS0038B)适合面包板实验;贴片(如VS1838B)节省空间。

实操心得:对于学习和小型项目,推荐使用VS1838BHS0038B,它们非常常见,资料多,且通常兼容3.3V-5V供电。购买时注意区分“带铁壳”和“不带铁壳”的版本,铁壳有一定的电磁屏蔽作用,抗干扰能力稍强。

3. 硬件电路设计与连接要点

红外接收器的硬件连接极其简单,但简单的连接背后,有几个细节决定了系统的稳定性。

3.1 最小系统连接电路

无论是STM32还是51单片机,基础连接方式一致:

  1. VCC引脚:连接到单片机的电源正极(5V或3.3V)。务必确认接收头的工作电压范围。
  2. GND引脚:连接到单片机系统的电源地。这是最重要的回路,必须保证共地
  3. OUT引脚:连接到单片机的任何一个GPIO引脚(配置为上拉输入或浮空输入)。对于需要高精度解码的情况,强烈建议连接到具有外部中断输入捕获功能的引脚上。

3.2 电源去耦与抗干扰设计

这是新手最容易忽略,也最可能引发诡异问题的地方。红外接收头内部是高度集成的模拟-数字混合电路,对电源噪声非常敏感。

  • 必须添加去耦电容:在接收头的VCC和GND引脚之间,尽可能靠近元件本体,并联一个10μF的电解电容和一个0.1μF(104)的陶瓷电容。电解电容负责滤除低频噪声,陶瓷电容负责滤除高频噪声。这个组合能极大提升接收稳定性,避免因电源纹波导致的误触发。
  • 信号线布线:如果电路板面积允许,OUT信号线应尽量短。如果必须走长线,可以考虑在靠近单片机输入端的位置,对地加一个几十皮法的小电容(如22pF)来滤除高频毛刺,但需注意这可能略微影响上升沿速度。
  • 避免靠近噪声源:尽量让红外接收头远离单片机、电机驱动、开关电源等高频噪声源。接收头前端的透光窗口应对准设备外壳的开孔,并确保无遮挡。

一个真实的踩坑案例:我曾在一个基于STM32的四轴飞行器遥控器上使用红外接收,最初未加去耦电容,在电机启动时,红外解码会完全失灵。在添加了10μF+0.1μF的去耦电容后,问题立刻解决。这充分说明了电源纯净度对模拟前端器件的重要性。

4. 在51单片机上的软件解码实现

51单片机(如STC89C52)资源有限,通常没有专门的输入捕获单元,解码需要依靠外部中断定时器来配合完成。这是一种“资源节俭型”但极其锻炼对协议时序理解能力的方案。

4.1 基于外部中断和定时器的解码流程

核心思路:利用外部中断检测信号线的下降沿(代表红外脉冲的开始),然后开启定时器,在下一个下降沿到来时中断,通过读取定时器的计数值来计算低电平的持续时间,从而判断是引导码、逻辑“0”还是逻辑“1”。

步骤详解:

  1. 初始化

    • 将连接红外OUT的引脚(如P3.2/INT0)配置为下降沿触发的外部中断模式。
    • 初始化一个定时器(如Timer0),设置为16位自动重装模式,定时时间建议在50-100微秒量级,用于精确计时。定时器中断优先级应低于外部中断。
    • 定义一个状态机变量ir_state,用于记录当前解码状态(如等待引导码、接收用户码、接收数据码等)。
    • 定义一个数组ir_buffer[4]用于存储接收到的4个字节(用户码高8位、低8位、数据码、数据反码)。
    • 定义一个位索引变量bit_cnt和一个字节索引变量byte_cnt
  2. 外部中断服务函数: 这是解码的核心。每次下降沿都会进入此函数。

    void EXTI0_IRQHandler(void) interrupt 0 // 51单片机INT0中断号 { static unsigned int time_cnt; // 用于存储定时器计数值 EXTI_ClearFlag(); // 清除中断标志(具体函数名依编译器而定) // 第一次进入,可能是引导码开始,记录时间并开启定时器 if(ir_state == IR_IDLE) { Timer_Start(); // 启动定时器 ir_state = IR_LEADER; } // 第二次及以后进入,计算两次下降沿之间的时间 else { time_cnt = Timer_GetValue(); // 获取定时器自上次中断以来的计数值 Timer_Reset(); // 重置定时器,为下次测量准备 // 根据当前状态和时间值进行判断 switch(ir_state) { case IR_LEADER: // 引导码低电平约9ms,高电平约4.5ms // 我们测量到的是“9ms低电平+4.5ms高电平”的总时间,约13.5ms if(time_cnt > 12000 && time_cnt < 15000) { // 换算成定时器计数,需根据实际定时器频率计算 ir_state = IR_DATA; byte_cnt = 0; bit_cnt = 0; ir_buffer[0] = ir_buffer[1] = ir_buffer[2] = ir_buffer[3] = 0; } else { ir_state = IR_IDLE; // 不是合法的引导码,重置状态 } break; case IR_DATA: // 判断是逻辑0(约1.125ms)还是逻辑1(约2.25ms) // 注意:这里测量的是“560us低电平+560us/1690us高电平”的总时间 if(time_cnt > 800 && time_cnt < 1200) { // 逻辑0的范围 // 将0移入缓冲区当前字节 ir_buffer[byte_cnt] >>= 1; // 先右移,低位在前 } else if(time_cnt > 1800 && time_cnt < 2500) { // 逻辑1的范围 ir_buffer[byte_cnt] >>= 1; ir_buffer[byte_cnt] |= 0x80; // 最高位置1 } else { // 时间不在范围内,解码错误 ir_state = IR_IDLE; break; } bit_cnt++; if(bit_cnt >= 8) { bit_cnt = 0; byte_cnt++; if(byte_cnt >= 4) { // 4个字节接收完毕 ir_state = IR_COMPLETE; Timer_Stop(); // 停止定时器 // 进行校验:用户反码和数据反码 if( (ir_buffer[2] == (unsigned char)(~ir_buffer[3])) ) { // 数据码校验成功,可以处理按键了 g_ir_key_value = ir_buffer[2]; // 将数据码存入全局变量 } ir_state = IR_IDLE; // 准备接收下一帧 } } break; default: ir_state = IR_IDLE; break; } } }

注意事项

  • 定时器精度:定时器的计时单位决定了时间判断的精度。如果系统主频为12MHz,定时器每计一个数代表1微秒,那么判断逻辑就非常直观。如果使用其他频率,需要仔细计算。
  • 中断嵌套与优先级:确保红外解码中断(特别是外部中断)有足够高的优先级,避免被其他长时间的中断(如串口发送)阻塞,导致丢失下降沿。
  • 防抖动处理:红外信号可能受到干扰,可以在中断入口简单延时几个微秒再读取引脚状态,进行二次判断,但会牺牲一些响应速度。

5. 在STM32上的高效解码方案

STM32拥有更强大的外设,我们可以采用更高效、更精准的解码方式,将CPU从繁重的时序测量中解放出来。这里主要介绍两种主流方案。

5.1 方案一:输入捕获模式(推荐)

利用STM32定时器的输入捕获功能,可以直接硬件测量脉冲宽度,精度高且不占用过多CPU。

配置步骤(以STM32F1的TIM2_CH1为例):

  1. GPIO配置:将连接红外OUT的引脚(如PA0)配置为复用功能上拉,并映射到TIM2_CH1。
  2. 定时器配置
    • 初始化TIM2,时钟源为内部时钟,预分频器(PSC)设置使得计数器每微秒计数一次(若主频72MHz,则PSC=71)。
    • 设置自动重装载值(ARR)为最大值(如0xFFFF),因为我们只需要测量相对时间。
    • 配置通道1为输入捕获模式,捕获边沿为下降沿(因为接收头输出反相,下降沿对应红外载波开始,即脉冲起点)。
    • 开启捕获/比较1中断(CC1IE)和更新中断(UIE,用于处理超时)。
  3. 中断服务函数逻辑
    • 在捕获中断中,读取捕获比较寄存器(CCR1)的值,这个值就是下降沿发生时计数器的值。
    • 用当前捕获值减去上一次的捕获值,就得到了两次下降沿之间的时间差(即脉冲周期)。
    • 根据这个时间差,判断是引导码、逻辑“0”还是逻辑“1”,其判断逻辑与51单片机方案类似,但时间值更精确。
    • 在更新中断中,可以设置一个超时机制。如果很长时间没有捕获到新的下降沿(例如超过20ms),则认为一帧数据接收结束或出错,重置解码状态机。

优势:几乎不占用CPU,测量精度由硬件定时器保证,抗干扰能力强,代码结构清晰。

5.2 方案二:外部中断配合DMA到内存(高级用法)

这是一种更“炫技”也更高效的方案,适合需要同时处理多路红外或系统负载很重的场景。

  1. 配置一个基本定时器(如TIM6)产生固定频率的中断(例如100kHz)。
  2. 配置红外OUT引脚的外部中断,触发方式为双边沿(上升沿和下降沿)。
  3. 配置一个DMA通道,目标地址是一个内存中的数组,传输方向为外设到存储器。
  4. 在外部中断服务函数中,不进行任何复杂计算,仅仅做一件事:读取当前定时器(如TIM6)的计数器值,并将这个值通过DMA(或直接写入)存储到数组中。同时记录是上升沿还是下降沿。
  5. 在一帧数据接收完成后(通过超时判断),主循环或一个低优先级任务去分析这个“时间戳数组”。通过计算相邻时间戳的差值,就能得到所有高电平和低电平的精确宽度,从而完整还原出波形。

优势:将高优先级的中断处理时间压缩到极短(仅记录时间戳),将所有复杂的解码逻辑移到后台任务,极大减轻了中断对系统实时性的影响。可以完美还原整个波形,甚至能分析非标准协议。

实操心得:对于大多数应用,输入捕获方案是首选,它在性能和实现复杂度上取得了最佳平衡。使用STM32CubeMX进行配置可以事半功倍。务必注意,输入捕获测量的是两个同向边沿之间的时间。对于NEC协议,我们需要测量低电平宽度,因此应该捕获下降沿(对应脉冲开始),然后计算连续两个下降沿之间的时间,这个时间包含了前一个脉冲的高电平和当前脉冲的低电平。我们需要在状态机里区分是引导码还是数据位。

6. 软件解码的优化与抗干扰策略

无论采用51还是STM32,纯软件解码都会面临干扰问题。以下策略能显著提升解码成功率。

6.1 状态机设计要点

一个健壮的解码程序必须基于状态机。典型状态包括:

  • IR_IDLE:空闲状态,等待引导码。
  • IR_LEADER_H/IR_LEADER_L:分别检测引导码的高电平和低电平部分。分开检测可以提高对引导码的容错能力。
  • IR_DATA:正在接收数据位。在此状态下,需要持续检查位接收是否超时。
  • IR_COMPLETE:一帧数据接收完成,进行校验。
  • IR_ERROR:发生错误(如时序超范围、校验失败),等待一段时间后自动回到IR_IDLE

6.2 时序容错处理

遥控器晶振有误差,传输路径有延迟,测量也有误差。因此,判断时间不能使用绝对相等(==),而应该使用范围判断(><)。

// 不推荐 if(pulse_width == 9000) { /* 引导码 */ } // 推荐 #define LEADER_H_MIN 8500 #define LEADER_H_MAX 9500 if(pulse_width > LEADER_H_MIN && pulse_width < LEADER_H_MAX) { /* 引导码 */ }

容限范围需要根据实际测试调整,通常可以在标准值的±15%左右。

6.3 重复码与长按处理

当持续按住遥控器按键时,NEC协议在发送完一帧完整数据后,会每隔约110ms发送一个特殊的重复码(一个9ms低电平、2.25ms高电平、560us低电平的脉冲)。解码程序需要识别这种重复码,并重复执行上一次按键的动作,而不是将其当作新按键或错误帧。这需要在状态机中增加一个IR_REPEAT状态。

6.4 使用硬件PWM模拟发射(进阶)

掌握了接收,也可以尝试发射。用STM32的定时器PWM功能,可以非常方便地模拟出38kHz载波。将PWM频率设置为38kHz,占空比设置为1/3(约33%)以获得较好的发射效率。需要发射信号时,用另一个GPIO或定时器控制PWM输出的使能,按照NEC协议的时序(引导码、数据位)来开关载波即可。

一个实用技巧:在调试发射电路时,可以用另一个红外接收头对着发射管,用单片机解码收到的信号,来验证自己发射的波形是否正确。这是最直接的闭环测试方法。

7. 常见问题排查与实战调试记录

红外解码调试中,十有八九会碰到问题。下面是一个典型的问题排查清单。

现象可能原因排查方法与解决方案
完全无反应1. 电源接反或电压不对。
2. 接收头OUT引脚未接上拉电阻(部分单片机内部无上拉)。
3. 接收头损坏。
4. 遥控器没电或损坏。
1. 用万用表测量VCC和GND间电压。
2. 在OUT引脚与VCC间加一个4.7k-10k的上拉电阻。
3. 更换接收头。
4. 用手机摄像头(普通CMOS传感器即可)对准遥控器发射管,按按键,在手机屏幕上应能看到发射管闪烁白光,证明遥控器在工作。
解码不稳定,时灵时不灵1.电源噪声(最常见)。
2. 环境光干扰强烈(如直射太阳光、荧光灯)。
3. 单片机中断被其他高优先级任务阻塞。
4. 时序判断范围太窄。
1.务必在接收头VCC和GND引脚就近添加10μF+0.1μF去耦电容
2. 避免强光直射,或为接收头加装遮光罩。
3. 检查系统中断优先级,确保红外中断(特别是外部中断)能及时响应。
4. 适当放宽引导码和数据位的时序判断范围。
解码数据错误1. 接收头输出极性理解错误(反相问题)。
2. 定时器计时不准确。
3. 逻辑“0”和“1”的判断阈值设置不当。
1. 用逻辑分析仪或示波器观察OUT引脚实际波形,确认下降沿对应的是脉冲开始还是结束。
2. 校准系统时钟和定时器分频设置。
3. 用逻辑分析仪捕获一帧正确波形,测量每个位的实际时间,以此调整代码中的阈值。
只能解码一次,之后失灵解码状态机在某个错误状态下“卡死”,没有正确回到IR_IDLE状态。在状态机中增加超时机制。在任何状态下,如果超过一定时间(如30ms)没有收到新的边沿中断,就强制将状态机重置为IR_IDLE
通信距离很短1. 发射管驱动电流不足。
2. 接收头灵敏度下降或供电不足。
3. 发射与接收频率有偏差。
1. 检查发射电路,确保发射管有足够的瞬时电流(通常需要100mA以上),可使用三极管驱动。
2. 确保接收头供电电压达标,更换接收头试试。
3. 用示波器测量发射载波频率,调整单片机PWM输出。

终极调试工具——逻辑分析仪:一个几十块钱的8通道逻辑分析仪(配合PulseView或Saleae软件)是调试红外通信的神器。它能直观地显示OUT引脚上的波形,精确测量高低电平的时间,让你清楚地看到引导码、数据位和重复码。很多解码问题在逻辑分析仪面前都会无所遁形。没有它之前,调试像是盲人摸象;有了它之后,一切尽在掌握。

红外接收器的应用,就像在单片机和现实世界之间架起了一座简单的无线桥梁。从理解38kHz载波调制抗干扰的原理,到动手焊接上去耦电容稳定电源,再到用状态机编写健壮的解码程序,最后用逻辑分析仪验证波形,这个过程完整地体现了一个嵌入式开发者从理论到实践、从模块到系统的问题解决路径。无论是资源紧张的51单片机,还是功能强大的STM32,核心思想都是相通的:理解协议、稳定硬件、精细编码。当你按下遥控器,单片机准确地识别出你的指令并执行相应动作时,那种成就感正是驱动我们不断深入嵌入式世界的乐趣之一。

← 返回列表