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

日记详情

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

STM32红外遥控解码实战:从NEC协议原理到状态机实现

STM32红外遥控解码实战:从NEC协议原理到状态机实现

1. 项目概述:从“接收”到“理解”的跨越

红外遥控接收模块,听起来像是电子爱好者入门时的一个小玩意儿,不就是接收个信号嘛。但当你真正把它和STM32这样的微控制器结合起来,事情就变得有意思了。这不仅仅是把一个接收头接到IO口上那么简单,它背后是一整套从物理信号到逻辑指令的解码过程。我见过不少新手,买来模块,接上电源和地,把数据线随便连到一个引脚,然后就开始对着代码发愁——为什么收到的数据全是乱码?为什么按键反应时灵时不灵?这其中的门道,恰恰是嵌入式开发中“软硬结合”思维的绝佳体现。

这个项目的核心,是利用STM32来解码红外遥控器发出的信号,将那一连串肉眼不可见的红外光脉冲,翻译成我们程序可以识别的键值。市面上绝大多数消费电子产品的遥控器,比如电视、空调、机顶盒,都采用NEC编码协议,因为它简单、可靠、成本低。所以,我们的目标很明确:用STM32搭建一个红外接收电路,并编写程序,实现对NEC编码红外信号的稳定解码。这不仅能让你控制一个LED或者继电器,更是你理解时序通信、中断处理、状态机编程的敲门砖。无论你是刚接触STM32的学生,还是想为智能家居项目添加遥控功能开发者,这个从硬件连接到软件解码的全过程,都值得你亲手做一遍。

2. 红外遥控与NEC编码协议原理解析

2.1 红外通信的基本模型

红外遥控的本质是一种无线数字通信,它利用红外发光二极管(LED)发出特定波长的红外光(通常是940nm)来传输数据。接收端则是一个红外接收头,比如常见的HS0038、VS1838。这个接收头可不是简单的光敏二极管,它内部集成了光电二极管、前置放大器、带通滤波器和解调电路。带通滤波器中心频率通常是38kHz,这个设计非常关键,目的是为了滤除环境光(如日光灯、太阳光)中其他频率的红外干扰,只允许被38kHz载波调制的信号通过。

发送端(遥控器)在发送一个二进制位时,并不是直接亮灭红外LED,而是先让LED以38kHz的频率闪烁(即载波),再用要发送的数据去控制这个载波的包络(即调制)。对于接收头来说,它只输出解调后的包络信号。也就是说,它把38kHz的载波“过滤”掉了,给我们STM32 GPIO引脚检测的,是一个干净的数字电平信号:有载波时输出低电平,无载波时输出高电平。理解这一点至关重要,否则你会困惑于为什么接收头信号空闲时为高电平,而有信号时反而出现一堆低电平脉冲。

2.2 NEC编码协议的数据帧结构

NEC协议以其独特的编码方式著称,一帧数据包含引导码、用户码、用户反码、数据码和数据反码。

引导码:它是一个9ms的低电平脉冲,紧接着一个4.5ms的高电平。这个长长的起始信号就像田径比赛的发令枪,用来告诉接收方:“注意,数据要来了!”同时,它强大的特征也使得接收电路能轻易地从噪声中识别出有效信号的开始。

用户码和数据码:引导码之后是8位的用户码(Address)和8位的用户反码。用户码用于区分不同厂家的设备,防止你家的电视被邻居的空调遥控器误操作。用户反码是用户码按位取反,用于校验。紧接着是8位的数据码(Command)和8位的数据反码,数据码就对应具体的按键值。这种“原码+反码”的简单校验机制,在一定程度上提高了抗干扰能力。

位的表示逻辑:NEC协议采用脉冲位置调制。每一位数据本身用560us的低电平脉冲开始(位起始信号),随后用高电平的持续时间来区分“0”和“1”。

  • 逻辑“0”:560us低电平后,跟随560us的高电平。
  • 逻辑“1”:560us低电平后,跟随1690us(约3倍于逻辑0的时间)的高电平。

你可以这样记忆:每一位都以一个“小低脉冲”开头,然后看后面的“高电平尾巴”长短,短的是0,长的是1。一帧完整的数据总共是32位(用户码16位 + 数据码16位)。

重复码:当你长时间按住一个按键不放时,遥控器不会一直发送完整的数据帧,那样太耗电。取而代之的是,它每110ms发送一次“重复码”。重复码非常简单:一个9ms的低电平加一个2.25ms的高电平,再接一个560us的低电平。接收程序必须能识别这种重复码,以实现长按连续触发功能。

注意:不同来源的资料对高低电平时间的描述可能有几十微秒的偏差,这是因为晶振精度和制造公差导致的。在实际解码时,我们需要设定一个合理的范围(比如±150us)来进行判断,而不是追求绝对精确的值。

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

3.1 红外接收模块的选型与接口

市面上最常见的红外接收头是三个引脚的:VCC(电源正极)、GND(电源负极)、OUT(信号输出)。例如HS0038B,它的工作电压通常是2.7V到5.5V,与STM32的3.3V电平完全兼容。如果你使用5V供电的模块,其输出信号的高电平可能也是5V,直接接入3.3V的STM32 GPIO可能存在风险,虽然很多情况下也能工作,但为稳妥起见,最好加一个简单的电平转换电路(如两个电阻分压)或选择3.3V兼容的型号。

连接非常简单:

  • VCC 接 STM32开发板的 3.3V 或 5V 电源(确保模块兼容)。
  • GND 接开发板的 GND。
  • OUT 接 STM32的任何一个具有外部中断或输入捕获功能的GPIO引脚,例如 PA0、PA1等。

实操心得:尽量将接收头远离STM32的晶振、数字开关电源等高频噪声源,并用杜邦线连接时,给VCC和GND之间并联一个10uF和一個0.1uF的电容,靠近接收头放置,可以极大地提高抗电源干扰的能力。很多“时灵时不灵”的问题,就是电源纹波导致的。

3.2 STM32 GPIO工作模式配置

对于红外接收,我们通常将连接OUT脚的GPIO配置为浮空输入模式。为什么不使用上拉输入?因为红外接收头内部输出级通常是开集电极或开漏结构,在无信号时(空闲状态)会输出高电平。如果STM32端再启用内部上拉,相当于两个上拉电阻并联,不会造成问题,但也没有必要。浮空输入即可。

最关键的是要开启这个GPIO的外部中断功能。因为红外信号是毫秒甚至微秒级的,如果用轮询的方式去检测引脚电平变化,很可能错过关键边沿,导致解码失败。通过外部中断,我们可以在信号每一次跳变(下降沿或上升沿)时立即得到通知,从而精确测量脉冲宽度。

以STM32F1系列和HAL库为例,配置PA0为下降沿触发中断的代码如下所示。我们选择下降沿触发,是因为NEC协议中每一个位的开始(560us低脉冲)和引导码的开始(9ms低脉冲)都是下降沿,抓住下降沿就能卡住每个时间段的起点。

// GPIO和中断初始化片段 GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断 GPIO_InitStruct.Pull = GPIO_NOPULL; // 浮空输入 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); // 设置中断优先级 HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 使能中断线

4. 软件解码策略:状态机与定时器精粹

解码的核心思想是:在外部中断中记录时间点,在主循环或定时器中断中分析时间间隔,进而判断是引导码、数据位0、数据位1还是重复码。我强烈推荐使用“状态机+定时器”的方法,这是最清晰、最稳定的一种实现方式。

4.1 利用基本定时器测量脉冲宽度

我们不能在外部中断服务函数里进行复杂的判断和延时,那样会阻塞其他中断。正确的做法是,在中断里只做最必要的事:记录当前时间戳,并改变状态标志。

我们需要一个定时器来提供高精度的时间基准。用一个基本定时器(如TIM6/TIM7)配置为1MHz的计数频率(每微秒计数一次),这样方便计算。在中断服务函数里,我们读取定时器计数器的值,与上一次中断时的值相减,就得到了两次中断之间的时间间隔(脉冲宽度)。

// 全局变量 volatile uint32_t g_ir_last_fall_time = 0; // 上次下降沿时间戳 volatile uint32_t g_ir_pulse_width = 0; // 测量的脉冲宽度(高电平持续时间) volatile uint8_t g_ir_edge_flag = 0; // 边沿捕获完成标志 // 外部中断服务函数(下降沿触发) void EXTI0_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) { uint32_t current_time = __HAL_TIM_GET_COUNTER(&htim6); // 假设htim6是1MHz的定时器 g_ir_pulse_width = current_time - g_ir_last_fall_time; // 计算高电平持续时间 g_ir_last_fall_time = current_time; g_ir_edge_flag = 1; // 设置标志,通知主循环处理 __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); } }

注意,我们测量的是两次下降沿之间的时间,也就是高电平的持续时间。因为NEC协议用高电平持续时间来区分0和1。

4.2 解码状态机的设计与实现

主循环或一个低优先级的定时器中断中,我们不断检查g_ir_edge_flag标志。一旦置位,就根据当前解码状态和测量到的g_ir_pulse_width进行状态转移。

一个典型的状态机可以设计为以下几个状态:

  1. 空闲态:等待引导码的低电平部分(第一个下降沿已触发,等待测量第一个高电平宽度)。
  2. 引导码确认态:如果测得的高电平宽度在4ms-5ms之间(对应4.5ms),则认为引导码成功接收,进入接收数据状态,并准备接收32位数据。
  3. 接收数据态:在这个状态下,每来一个下降沿(即一个位的开始),就根据上一次测得的高电平宽度判断前一个位是0还是1,并将其存入缓冲区。接收完32位后,进行校验(用户反码、数据反码),校验通过则解码成功。
  4. 等待重复码态:解码成功后,进入此状态。如果下一个测得的高电平宽度在2ms左右(对应2.25ms),则认为是重复码,触发重复按键事件。
// 简化的状态机处理片段(在主循环中) if(g_ir_edge_flag) { g_ir_edge_flag = 0; switch(ir_state) { case IR_IDLE: if(g_ir_pulse_width > 8500 && g_ir_pulse_width < 9500) { // 测量到约9ms的低电平后的第一个高电平,可能是引导码 // 实际上,第一个下降沿我们只记录了时间,现在测量的是引导码高电平 // 但更严谨的做法是状态机从下降沿触发开始,这里为简化理解 ir_state = IR_LEADER_CODE; bit_count = 0; ir_data = 0; } break; case IR_LEADER_CODE: if(g_ir_pulse_width > 4000 && g_ir_pulse_width < 5000) { // 确认是4.5ms高电平,引导码成功 ir_state = IR_RECEIVING_DATA; } else { ir_state = IR_IDLE; // 不是引导码,复位 } break; case IR_RECEIVING_DATA: // 判断位值 if(g_ir_pulse_width > 1400 && g_ir_pulse_width < 1800) { // 逻辑1 ir_data |= (1UL << bit_count); } else if(g_ir_pulse_width > 400 && g_ir_pulse_width < 800) { // 逻辑0 // ir_data对应位默认为0,无需操作 } else { // 时间不对,接收错误 ir_state = IR_IDLE; break; } bit_count++; if(bit_count >= 32) { // 接收完32位,进行校验 if( ((~(ir_data>>16)) & 0xFF) == ((ir_data>>24) & 0xFF) && ((~(ir_data>>0)) & 0xFF) == ((ir_data>>8) & 0xFF) ) { // 校验成功 user_code = (ir_data >> 16) & 0xFF; key_code = (ir_data >> 0) & 0xFF; ir_state = IR_SUCCESS; } else { ir_state = IR_IDLE; } } break; case IR_SUCCESS: // 成功后可进入等待重复码状态 if(g_ir_pulse_width > 2000 && g_ir_pulse_width < 2500) { // 重复码,触发重复按键动作 handle_repeat_key(); } ir_state = IR_IDLE; // 处理完回到空闲 break; } }

注意事项:上述代码是一个高度简化的示例,用于说明原理。在实际项目中,你需要考虑更多细节,比如在IR_RECEIVING_DATA状态,你其实是在每次进入中断时(新的下降沿)处理上一个位。更严谨的实现需要两个状态变量:一个记录当前是处于“位低电平期”还是“位高电平期”,另一个记录当前已接收的位数。但“下降沿触发+测量高电平宽度”是NEC解码最核心的思路。

5. 进阶优化与抗干扰处理

5.1 使用输入捕获模式实现高精度解码

上述“外部中断+基本定时器”的方法已经能工作,但使用定时器的输入捕获功能是更专业、更节省CPU资源的选择。输入捕获可以在引脚电平变化时,自动将定时器计数器的值锁存到捕获/比较寄存器中,并产生中断。这样,我们就不需要配置外部中断了,所有时间测量都在定时器中断内完成,精度更高,代码更集中。

以STM32的通用定时器(如TIM2)为例,配置一个通道为输入捕获模式,捕获下降沿。在捕获中断中,读取捕获寄存器值,与上一次的值相减得到时间间隔。同时,可以切换捕获边沿(例如,第一次捕获下降沿后,立刻改为捕获上升沿,再下次又改回下降沿),从而同时测量高电平和低电平的宽度,使得解码逻辑更加健壮。

5.2 软件滤波与错误处理机制

红外通信容易受到环境光、日光灯、其他红外源(如另一个遥控器)的干扰。为了提高稳定性,必须加入软件滤波。

  1. 宽度校验:不仅判断高电平宽度,也判断低电平宽度。每个位的起始低电平应该是560us左右,如果偏差太大(比如小于200us或大于900us),则判定为干扰,丢弃当前帧,状态机复位。
  2. 帧头强化校验:引导码的9ms低电平和4.5ms高电平是一个非常独特的组合。可以要求连续两个这样的组合才认为是有效开始,这能过滤掉很多突发干扰。
  3. 数据一致性检查:除了协议规定的反码校验,还可以加入“连发码一致性”检查。如果连续收到多个相同的键值(非重复码),则认为数据可靠。
  4. 超时复位:在接收数据状态,如果超过一定时间(比如25ms,远大于一帧时间)没有收到新的下降沿,则认为帧传输中断,状态机强制复位到空闲态。这是防止状态机“卡死”的重要手段。
// 在定时器中断或主循环中增加超时检查 if(ir_state != IR_IDLE) { uint32_t time_now = HAL_GetTick(); // 获取系统滴答时钟 if(time_now - last_edge_time > 25) { // 超过25ms无新边沿 ir_state = IR_IDLE; // 超时复位 // 可以在这里增加错误计数,用于调试 } } // 每次成功进入边沿中断时,更新 last_edge_time = time_now;

6. 调试技巧与常见问题排查

红外解码的调试,肉眼不可见,需要借助工具和技巧。

6.1 利用串口打印与逻辑分析仪

串口打印:在解码状态机的关键节点(如进入引导码、接收到一位数据、解码成功、校验失败)通过串口打印信息。这是最基础的调试方法。你可以把每次测量到的高电平宽度打印出来,与标准值对比。

逻辑分析仪:这是调试红外信号的“神器”。将逻辑分析仪的探头连接到接收头的OUT引脚和地线。设置合适的采样率(比如2MHz以上),触发方式设为下降沿。按下遥控器,你就能在软件界面上清晰地看到完整的波形:引导码、数据位、重复码。可以直接用测量工具测量脉冲宽度,与你的程序判断逻辑进行一一对照。逻辑分析仪能让你直观地看到问题所在:是信号本身畸变,还是程序判断阈值设置不合理。

6.2 常见问题速查表

问题现象可能原因排查思路与解决方案
完全无反应,打印无输出1. 硬件连接错误(电源、地、信号线接反或接触不良)
2. GPIO模式配置错误(未配置为输入)
3. 中断未使能或优先级配置问题
4. 接收头损坏
1. 用万用表检查VCC、GND电压,检查接线。
2. 用另一块开发板或Arduino简单测试接收头好坏(输出接LED看是否闪烁)。
3. 检查CubeMX配置或代码中的GPIO和中断初始化。
能进入中断,但解码出的键值混乱或固定1. 时间判断阈值设置不合理(太严格或太宽松)
2. 状态机逻辑有漏洞,特别是在位边界处理上
3. 定时器时钟频率设置错误,导致时间测量不准
1. 用逻辑分析仪抓取波形,实测脉冲宽度,据此调整代码中的判断阈值范围。
2. 仔细梳理状态机,确保每个状态转换的条件和动作都正确。在接收数据态,打印出每一位判断的结果和当前数据缓冲区的值。
3. 检查定时器初始化代码,确认计数频率是否为预期的1MHz(或其它值)。
按键反应迟钝,或长按无效1. 重复码处理逻辑缺失或错误
2. 主循环处理解码任务太慢,或中断被高优先级任务阻塞
3. 软件滤波过于严格,丢弃了有效数据
1. 确认代码中实现了对重复码(约9ms低+2.25ms高)的识别和处理。
2. 确保解码处理部分(状态机)的执行频率足够高。如果放在主循环,确保没有其他耗时操作阻塞。可以考虑在低优先级定时器中断中处理。
3. 适当放宽宽度判断的容差范围。
近距离正常,远距离失灵1. 接收头接收角度或灵敏度问题
2. 环境光干扰太强(特别是日光灯)
3. 电源噪声
1. 确保遥控器对准接收头,且中间无遮挡。有些接收头指向性较强。
2. 为接收头加装遮光罩(如热缩管),避免侧向光干扰。
3. 在接收头VCC和GND引脚就近并联电容(10uF电解电容和0.1uF陶瓷电容),并检查电源质量。
不同遥控器解码结果不一致1. 不同品牌遥控器的用户码不同
2. 个别遥控器发射的脉冲宽度有较大偏差
1. 这是正常现象。你的程序应该输出完整的32位数据,然后分别提取用户码和键码。不要假设用户码是某个固定值。
2. 适当放宽协议中所有时间参数的判断范围,增强兼容性。

6.3 一个关键的实操心得:理解“空闲高电平”

这是最容易让初学者困惑的地方。逻辑分析仪显示,接收头在无信号时输出高电平,有信号时出现低电平脉冲。这与我们直觉的“有信号=1,无信号=0”相反。一定要牢记:接收头输出的是解调后的反相信号。38kHz载波存在时,它输出低电平;无载波时,输出高电平。NEC协议的数据是用载波的有无来构成的,所以反映在接收头输出上,就是低电平代表“有信号段”,高电平代表“无信号段”。我们解码时关注的是高电平的持续时间,这个时间对应的是载波消失的时间长度,协议正是用这个长度来编码0和1的。

把这个物理层逻辑想通了,再看波形图和代码,就会豁然开朗。红外解码项目虽小,但它串联了硬件接口、中断、定时器、状态机、协议分析、抗干扰等嵌入式开发的核心概念。把它做稳定、做可靠的过程,就是一个嵌入式工程师基本功锤炼的过程。当你成功解码出第一个键值,并稳定地控制一个设备时,你获得的不仅仅是功能实现,更是一种对底层硬件与时间精准操控的信心。

← 返回列表