1. 项目缘起:从一次“失控”的空调说起
去年夏天,我手头一个基于STM32的智能家居中控项目遇到了一个不大不小的麻烦。项目需要集成红外遥控学习功能,用来控制客厅里的一台老式美的空调。我信心满满地接好了红外接收头,用逻辑分析仪抓取了原装遥控器的波形,然后开始对着NEC协议的数据手册写解码程序。本以为是个标准操作,结果却让人哭笑不得:中控发出的指令,十次里有八次空调没反应,剩下两次要么是开了制冷却调到30度,要么是打开了扫风模式。项目眼看要延期,我开始从头排查,这才发现红外NEC协议解码这个看似简单的任务,里面藏着不少从数据手册上看不到的“坑”。
红外遥控,几乎是每个嵌入式开发者都会接触到的技术。它成本低廉、技术成熟,广泛应用于家电、玩具等领域。而NEC协议,则是红外遥控领域事实上的“普通话”,绝大多数消费电子产品的红外遥控都兼容或基于此协议。对于STM32这类单片机来说,实现NEC协议解码是检验其定时器、外部中断和GPIO应用能力的经典课题。但很多人(包括当时的我)容易把它想得太简单,以为就是按照协议规定的时序去判断0和1,结果在实际环境中,却要面对信号畸变、环境光干扰、不同厂商的协议变种等一系列问题。
今天,我就结合那次踩坑和后续多个项目的实战经验,为你彻底拆解如何在STM32上稳定、可靠地实现红外NEC协议解码。我们不止于读懂协议,更要搞定从硬件连接到软件滤波,从基础解码到处理“非标”信号的完整方案。无论你是正在做课程设计的学生,还是需要为产品添加红外控制功能的工程师,这篇内容都能让你避开我走过的弯路。
2. NEC协议核心:不止于0和1的时序逻辑
在动手写代码之前,我们必须吃透NEC协议的本质。很多教程只告诉你“引导码是9ms低电平+4.5ms高电平”,但这远远不够。理解其设计哲学,才能写出健壮的代码。
2.1 协议帧结构:为何如此设计?
一帧完整的NEC协议数据,通常由以下几部分组成:
- 引导码(Start Code):一个9ms的载波脉冲(低电平),紧接着一个4.5ms的空闲(高电平)。这个超长的、独特的信号就像广播里的报时音,用于唤醒接收端,并告诉它:“注意,下面要开始传数据了!” 它的长度远大于数据位,这为接收端的自动增益控制和噪声过滤提供了时间窗口。
- 用户码(Customer Code):8位地址码,用于区分不同厂家的设备。例如,索尼的电视和松下的空调就有不同的用户码,防止互相干扰。通常还会发送这8位的反码(逻辑取反)作为校验,所以实际传输16位。接收端会同时检查原码和反码是否匹配,这是第一道简单的容错机制。
- 数据码(Data Code):8位命令码,即具体的按键指令,如“音量加”、“开机”。同样,其后也跟随8位命令反码。
- 结束位(Stop Bit):一个560μs的脉冲后跟随一段空闲。有时,在连按按键时,它会被简化为一个单独的560μs脉冲,后面直接跟下一帧的引导码(称为“重复码”)。
这个“原码+反码”的双重传输机制,是NEC协议在无线环境下的一个巧妙设计。它用极小的开销(多传8位)实现了单比特错误的检测,在早期单片机处理能力有限、无线环境干扰大的背景下,大大提升了可靠性。
2.2 数据位的编码:脉宽调制的奥秘
NEC协议采用脉冲位置调制(PPM)的一种变体,具体来说是脉冲宽度编码。
- 逻辑‘0’:由一个560μs的载波脉冲(低电平)和560μs的空闲(高电平)组成,总周期为1.125ms。
- 逻辑‘1’:由一个560μs的载波脉冲(低电平)和1.69ms的空闲(高电平)组成,总周期为2.25ms。
这里有一个至关重要的细节:无论是‘0’还是‘1’,其起始部分都是一个560μs的脉冲。这个脉冲是载波信号(通常为38kHz)调制产生的。区别仅在于脉冲之后的高电平持续时间不同。这意味着,接收端硬件(一体化红外接收头)在收到信号时,会先解调掉38kHz的载波,输出一个反向的电平信号。我们单片机检测的,是这个解调后的波形。所以,我们在示波器或逻辑分析仪上看到的“低电平”,实际上对应着红外发射管正在发射光脉冲。
理解这一点,就能明白为什么我们的解码程序核心是测量两个下降沿之间的时间间隔。因为每个数据位都以一个下降沿(脉冲开始)为起点,我们测量从这个下降到下一个下降沿的时间,如果接近1.125ms,则是‘0’;如果接近2.25ms,则是‘1’。
2.3 示波器下的真实世界:协议变种与噪声
如果你用示波器去实测一个美的空调遥控器,可能会发现它的引导码不是严格的9ms+4.5ms,可能是8.8ms+4.4ms。数据位的脉宽也可能有微小的偏差。这就是“协议变种”。NEC协议是一个事实标准,不同厂商在具体参数上会有细微调整,但只要编码规则一致,通常都能兼容。
更麻烦的是环境噪声。日光灯、白炽灯、甚至阳光中都含有红外成分,会被接收头拾取,产生随机的低电平毛刺。这些毛刺如果被误判为数据位的起始下降沿,就会导致解码完全错误。这就是我项目初期失败的主要原因——没有做有效的软件滤波。
3. 硬件连接与信号调理:给STM32一双“好耳朵”
稳定的解码始于干净的信号。红外接收头(如VS1838B、HS0038)的选择和电路设计是第一步。
3.1 一体化红外接收头:不只是接三根线
常用的一体化接收头有三个引脚:VCC(3.3V/5V)、GND、OUT(信号输出)。它内部已经集成了红外接收管、前置放大器、带通滤波器(中心频率38kHz)和解调电路。你只需要给它供电,它就能输出解调后的数字信号。
注意:务必在VCC和GND之间就近放置一个10uF~100uF的电解电容和一个0.1uF的瓷片电容,用于电源去耦。红外接收头在工作时电流会有瞬间变化,不稳定的电源会导致输出信号抖动,产生误触发。这是我早期用面包板调试时踩过的坑,加上电容后解码稳定性立竿见影。
接收头的OUT引脚需要连接到STM32的GPIO。这里有一个关键决策点:使用外部中断模式还是定时器输入捕获模式?
- 外部中断模式:将GPIO配置为下降沿触发的外部中断。每个下降沿(脉冲开始)进入中断,在中断服务函数中记录时间戳(通过SysTick或通用定时器),并计算时间间隔来判断是引导码、数据‘0’还是数据‘1’。这种方法直观,对初学者友好,但中断频繁(一帧数据多达68个边沿),在系统繁忙时可能丢失中断。
- 定时器输入捕获模式:将一个定时器的输入捕获通道配置为捕获该GPIO的下降沿。硬件会自动在边沿发生时记录定时器计数器的值,并产生中断。你只需要在中断中读取两次捕获值的差值,即可得到高电平或低电平的持续时间。这种方法更精确,对CPU占用率更低,是更专业的选择。
对于追求稳定性和低功耗的产品,我强烈推荐使用定时器输入捕获模式。下面我们以STM32F103C8T6的TIM2_CH1(PA0引脚)为例进行配置。
3.2 定时器输入捕获配置详解
我们目标是测量高电平的持续时间。NEC协议中,无论是引导码还是数据位,有用的信息都编码在高电平的宽度里。
- GPIO初始化:将PA0配置为浮空输入(或上拉输入),因为接收头输出默认高电平。
- 定时器初始化:以TIM2为例。
- 时钟源:内部时钟(APB1总线,通常72MHz)。
- 预分频器(PSC):设置为71。这样,定时器时钟 = 72MHz / (71+1) = 1MHz,即计数器每1微秒计数一次。这个精度足够测量毫秒和微秒级的红外信号。
- 自动重装载值(ARR):设置为最大值0xFFFF(65535)。因为我们只关心两次捕获之间的差值,ARR设大一些避免溢出(65.535ms内必须处理完一次捕获,对NEC协议绰绰有余)。
- 捕获/比较通道1:配置为输入捕获模式,捕获下降沿。开启捕获中断。
- 中断服务函数逻辑:这是解码的核心。
- 第一次下降沿(引导码开始):记录捕获值
IC1Value1,然后将捕获边沿改为上升沿。 - 第一次上升沿(引导码低电平结束):记录捕获值
IC1Value2。计算差值IC1Value2 - IC1Value1,这个值应该在9000左右(对应9ms),允许一定误差(如±500)。如果符合,说明检测到引导码,准备接收数据。然后将捕获边沿改回下降沿,并清空数据缓冲区。 - 后续的下降沿(数据位开始):对于数据位,我们关心的是前一个上升沿到当前下降沿之间的时间(即高电平持续时间)。所以需要在每次上升沿时记录时间戳
LastRiseTime。在下降沿中断中,用当前捕获值减去LastRiseTime,得到高电平持续时间HighLevelTime。- 如果
HighLevelTime在1000~1350微秒之间(理论1125μs),则判定为逻辑‘0’。 - 如果
HighLevelTime在1600~2000微秒之间(理论1690μs),则判定为逻辑‘1’。 - 将判断出的位存入缓冲区。
- 如果
- 重复上述过程,直到收齐32位数据(用户码16位+数据码16位)或超时。
- 第一次下降沿(引导码开始):记录捕获值
这种利用输入捕获自动测量脉宽的方式,比在外部中断里手动开定时器要精准和可靠得多。
4. 软件解码实战:状态机与鲁棒性设计
直接写一堆if-else来判断各种状态会让代码难以维护且容易出错。更好的方法是使用有限状态机(FSM)。对于NEC解码,我们可以定义以下几个状态:
typedef enum { IR_IDLE, // 空闲状态,等待引导码 IR_LEADER_CODE, // 已收到引导码,等待用户码/数据码 IR_REPEAT_CODE, // 处理重复码(连按) IR_ERROR // 错误状态 } IR_DecodeState_t;在定时器输入捕获的中断服务函数(或由它触发的回调函数)中,我们根据当前状态和测量到的时间值进行状态转移和数据收集。
4.1 解码状态机流程
IR_IDLE状态:
- 当捕获到下降沿,并测得低电平持续时间在8ms~10ms范围内,且紧随其后的高电平持续时间在4ms~5ms范围内,则判定为有效的引导码。
- 状态转移到
IR_LEADER_CODE,复位位计数器bit_cnt = 0,清空数据缓冲区ir_data = 0。
IR_LEADER_CODE状态:
- 在此状态下,我们开始按位接收数据。每次进入下降沿中断,就根据前一个高电平的持续时间判断位值。
- 将判断出的位值(0或1)移位存入
ir_data。例如,ir_data = (ir_data << 1) | bit_value。 - 位计数器
bit_cnt加1。 - 当
bit_cnt == 32时,表示一帧数据接收完成。接下来进行数据校验:- 将收到的32位数据拆分为:
address(高8位)、~address(次8位)、command(次次8位)、~command(低8位)。 - 检查
address与~address是否互为反码,command与~command是否互为反码。如果两者都通过,则认为解码成功,将address和command存入结果变量,并设置一个标志位(如ir_ok = 1)通知主循环。如果校验失败,则转入IR_ERROR状态。
- 将收到的32位数据拆分为:
- 无论成功与否,解码完成后状态都回到
IR_IDLE,等待下一帧。
重复码的处理:
- 当按键被长时间按住时,遥控器不会发送完整帧,而是先发一帧完整数据,之后每隔约110ms发送一个特殊的“重复码”。重复码由9ms低电平和2.25ms高电平,再加一个560μs的脉冲组成。
- 在
IR_IDLE状态,如果捕获到一个下降沿,且测得的前一个高电平持续时间在2.0ms~2.5ms之间(对应重复码的2.25ms高电平),则可以判定为重复码。 - 此时,状态转移到
IR_REPEAT_CODE,我们可以直接复用上一次成功解码的command,并设置ir_repeat = 1标志,通知主循环“同一个键被持续按下”。处理完后状态回到IR_IDLE。
IR_ERROR状态:
- 发生任何不符合预期的时序(如位间隔超时、校验失败),都进入此状态。
- 在此状态可以记录错误类型,或简单地复位所有变量,然后无条件跳转回
IR_IDLE状态。
使用状态机后,代码逻辑清晰,易于调试和扩展。主循环只需要轮询ir_ok和ir_repeat标志即可。
4.2 超时处理与软件滤波:对抗干扰的盾牌
这是保证解码鲁棒性的关键,也是很多简单示例代码缺失的部分。
位超时(Bit Timeout):
- 在
IR_LEADER_CODE状态,每收到一个位,都应该启动一个超时计时器(可以用SysTick或另一个定时器)。 - 超时时间应略大于一个逻辑‘1’的周期(2.5ms比较安全)。
- 如果在超时时间内没有收到下一个下降沿,则认为本帧数据传输出错(可能被噪声打断),应强制跳转到
IR_ERROR状态。这能有效防止因丢失一个边沿而导致程序永远卡在等待状态。
- 在
帧间静默(Inter-frame Gap):
- 在成功解码一帧或处理完重复码后,可以故意让解码器进入一个短暂的“沉默期”(例如10-20ms),在此期间忽略所有外部中断或捕获事件。因为红外接收头在信号结束后可能还会输出一些杂波,这个沉默期可以过滤掉它们,防止误触发。
数字滤波(Digital Filtering):
- 对于GPIO输入,STM32的硬件有可配置的消抖滤波功能,但对于高速的红外信号(微秒级),通常不适用。
- 更有效的软件滤波是“多次采样判决”。例如,在判断下降沿时,可以在中断中连续快速读取几次GPIO电平,如果都是低电平,才认为是真正的下降沿。这可以滤除极窄的噪声毛刺。但要注意,这会增加中断处理时间,需要权衡。
动态阈值调整:
- 不要使用绝对固定的时间阈值(如1125μs)来判断‘0’和‘1’。可以在一帧数据开始时,用测量到的引导码高低电平时间作为基准进行比例计算。例如,测得引导码高电平为
T_leader_high,那么数据位高电平的判断阈值可以设为(T_leader_high / 4.5) * 1.125和(T_leader_high / 4.5) * 1.69附近的一个范围。这样能更好地适应不同遥控器的微小差异。
- 不要使用绝对固定的时间阈值(如1125μs)来判断‘0’和‘1’。可以在一帧数据开始时,用测量到的引导码高低电平时间作为基准进行比例计算。例如,测得引导码高电平为
5. 调试与验证:让问题无处遁形
写好了代码,怎么知道它能不能用?除了直接对着设备测试,我们还需要更科学的调试手段。
5.1 逻辑分析仪:解码过程的“X光机”
逻辑分析仪是调试数字通信协议的利器。将探头连接到红外接收头的OUT引脚和STM32的某个调试GPIO(用于在代码中打点)。
- 抓取原始波形:按下遥控器,抓取完整的波形。验证引导码、数据位的波形是否符合预期。测量具体的时间参数,用于校准你代码中的判断阈值。
- 验证解码逻辑:在STM32代码中,每当成功解码一帧,就让一个调试GPIO引脚翻转一次。在逻辑分析仪上同时观察这个引脚和红外信号。你可以清晰地看到,每次红外信号结束后,是否立即有一次翻转,从而确认解码是否成功触发。你甚至可以把这个调试引脚当成一个简单的“解码成功”指示灯。
5.2 串口打印:内部状态的“监视器”
通过串口将解码过程中的关键信息打印出来,是最直接的调试方法。
- 在收到引导码时,打印“Leader Code Detected”。
- 每收到一个位,打印其值(0/1)和测量到的高电平时间。
- 在完成一帧接收时,打印出原始的32位数据,以及解析出的地址码和命令码。
- 在遇到错误时,打印错误类型(如“Timeout Error”, “Check Sum Error”)。
这样,当解码失败时,你可以通过串口日志一步步回溯,看是在哪个环节出了错,是时间测量不准,还是状态机逻辑有漏洞。
5.3 示波器实测:深入信号的微观世界
当遇到特别棘手的干扰问题时,示波器比逻辑分析仪更能看清信号的细节。你可以观察到:
- 接收头输出的信号基线是否平稳?有没有上下漂移?
- 低电平的底部有没有毛刺或振铃?
- 环境光干扰(如快速开关日光灯)在信号线上产生了什么样的噪声?
根据观察到的现象,回头调整你的硬件(如加强电源滤波、在接收头OUT引脚对地加一个小电容吸收尖峰)或软件(如调整滤波算法、超时时间)。
6. 进阶与扩展:从解码到应用
当你能够稳定解码NEC信号后,就可以在此基础上构建更复杂的功能。
6.1 构建红外信号数据库(码库)
一个实用的红外学习型遥控器,需要存储不同设备的编码。你可以设计一个简单的数据结构来管理:
typedef struct { uint16_t vendor_id; // 厂商ID(用户码) uint8_t func_code; // 功能码(命令码) char func_name[20]; // 功能描述,如“Power”, “Vol+” // 如果需要学习重复码间隔,也可以存储 } IR_Command_t;通过“学习模式”,将遥控器对准你的设备按键,程序解码后把address和command存入Flash或EEPROM。在“发射模式”下,根据存储的码值,控制一个红外发射管(需连接三极管驱动)按照NEC协议格式发送出去。注意,发射时需要用一个定时器(如PWM模式)产生38kHz的载波来调制信号。
6.2 处理非标准NEC协议
正如前文所述,很多设备(如我遇到的那台美的空调)使用的是NEC协议的变种。它们的差异可能在于:
- 引导码长度不同。
- 逻辑‘0’和‘1’的脉宽比例不同(但‘1’总是‘0’的两倍左右)。
- 数据格式不同:可能只有16位(8位地址+8位命令),没有反码;或者地址码是16位。
应对策略就是让你的解码程序可配置化。不要将时间阈值写成死代码,而是定义成变量:
typedef struct { uint16_t leader_low_min; uint16_t leader_low_max; uint16_t leader_high_min; uint16_t leader_high_max; uint16_t bit0_high_min; uint16_t bit0_high_max; uint16_t bit1_high_min; uint16_t bit1_high_max; uint8_t data_bits; // 总位数,32或16 uint8_t with_inverse; // 是否包含反码 } IR_ProtocolConfig_t;为不同的设备预定义不同的配置结构体。在解码前,先尝试用几种常见的配置去匹配引导码,匹配成功后再用该配置进行后续数据位的解码。这就实现了一个简单的“自动协议识别”功能。
6.3 低功耗设计考量
如果设备是电池供电,需要时刻监听红外信号,那么功耗就很重要。一体化接收头本身在工作时就有一定电流(约0.5-1mA)。一种常见的优化是周期性唤醒:让STM32和接收头大部分时间处于睡眠模式,每隔几十毫秒唤醒一次,快速检查一下是否有红外信号(可以通过检查GPIO电平简单判断),如果没有立即再次休眠。虽然可能丢失信号头部的几个毫秒,但NEC协议的引导码很长,有很大概率能捕捉到。这需要结合STM32的低功耗模式(Stop模式)来设计。
从一次失败的空调控制开始,到最终实现一个稳定可靠、能适应多种设备的红外解码模块,这个过程让我对嵌入式开发中的“细节魔鬼”有了更深的认识。红外NEC解码就像一门基础内功,它考验的是你对硬件定时器的掌握、对中断服务的理解、对状态机编程的熟练度,以及最重要的——在充满噪声的真实世界中设计鲁棒性系统的能力。代码不仅仅要在实验室里跑通,更要在客厅的日光灯下、在午后的阳光下稳定工作。当你按下遥控器,设备应声而动的那一刻,你会觉得所有这些关于时序、滤波、状态转移的思考,都是值得的。