1. 项目概述:从“看不见的光”到“听得懂的信号”
在电子制作和嵌入式开发领域,红外通信是一种既古老又无处不在的技术。说它古老,是因为从早期的电视遥控器到现在的智能家居设备,它已经默默服务了几十年;说它无处不在,是因为你几乎每天都会用到它——空调遥控、电视换台、甚至一些老式的数据传输。这个项目的核心,就是拆解这个“隐形信使”的工作原理,并把它实实在在地用在你手边的STM32或经典的51单片机上,让它们也能“看见”并理解红外光传递的指令。
简单来说,红外接收器就是一个专门“听”特定波长红外光的“耳朵”。它不像摄像头那样追求成像,而是专注于解码红外光闪烁的特定节奏(即编码),将这个光信号转换成单片机能够处理的电信号。对于开发者而言,掌握它意味着你能轻松地为你的项目添加非接触式遥控功能,或者实现简单的设备间无线通信,成本极低,可靠性却很高。
无论你是刚接触单片机的新手,想做一个属于自己的万能遥控器;还是有一定经验的开发者,需要在产品中集成红外控制功能,理解红外接收器的原理和在MCU上的应用,都是一项非常实用且基础的核心技能。接下来,我会结合多年的调试经验,带你从物理原理走到代码实现,把每个环节的“坑”和“技巧”都摊开来讲明白。
2. 红外通信原理深度拆解:不只是“闪一闪”
很多人对红外通信的理解停留在“遥控器按一下,接收头输出个信号”的层面。这就像只知道汽车能跑,却不懂发动机和变速箱如何配合。要玩转红外,必须深入其信号编码的机理。
2.1 核心物理基础:调制与解调
红外发光二极管(IR LED)发出的光是波长在850nm到940nm附近的不可见红外光。如果直接让LED以代表数据的频率明灭,会遇到两个致命问题:环境光干扰和驱动能力限制。
- 环境光干扰:太阳光、白炽灯都含有丰富的红外成分,它们就像巨大的噪音,会彻底淹没微弱的信号光。
- 驱动能力:LED需要较大的电流才能发出足够强的光,但单片机I/O口的驱动电流有限(通常20mA以下)。
解决方案就是“调制(Modulation)”。我们用一个频率固定(通常是38kHz,也有36kHz、40kHz等)的载波信号,去“驮着”我们的数据信号。具体操作是:当需要发送逻辑“1”或“0”时,并不是让LED常亮或常灭,而是让LED以38kHz的频率高速闪烁或熄灭。这个38kHz的载波频率是接收头预先设定好的(由接收头内部晶振决定),它就像一个约定的“暗号”。
接收端,也就是红外接收头(如VS1838B、HS0038),内部集成了光电二极管、前置放大器、带通滤波器和解调电路。它的任务非常专一:
- 感光:内部光电二极管感知红外光强度变化。
- 滤波:内部的带通滤波器只允许中心频率(如38kHz)附近的信号通过,将太阳光、灯光等低频或高频的干扰噪声极大程度地滤除。这是抗干扰的关键。
- 解调:将38kHz的载波“剥离”掉,还原出最原始的、代表数据“1”和“0”的电平信号,并反向输出(即接收到载波时输出低电平,无载波时输出高电平)。
注意:接收头输出的信号是反向的。这是因为内部设计通常为“接收到有效载波信号时,输出管脚被拉低”。这一点在分析波形和编写解码程序时至关重要,很多新手在这里栽跟头。
2.2 主流编码协议解析:NEC与RC-5
调制解决了传输问题,编码则解决了数据组织问题。最常见的两种协议是NEC和RC-5。
NEC协议:在消费电子领域占统治地位,绝大多数国产设备遥控器都使用它或其变种。
- 帧结构:一次完整的传输包括:
引导码+用户码(16位)+用户反码(16位)+数据码(8位)+数据反码(8位)+结束位。 - 逻辑定义:
- 引导码:一个9ms的低电平脉冲,后跟一个4.5ms的高电平。用于唤醒接收头,并标识一帧数据的开始。
- 逻辑‘0’:560us的低电平,后跟560us的高电平。
- 逻辑‘1’:560us的低电平,后跟1.69ms的高电平。
- 重复码:如果按键持续按下,则发送一个简化的重复帧(9ms低电平+2.25ms高电平+560us低电平),之后每110ms发送一次,直到按键松开。
- 特点:通过发送“反码”进行简单的校验,结构简单,但传输效率相对较低。
RC-5协议:由飞利浦制定,在一些音响、高端电视设备中常见。
- 帧结构:固定14位,包括:
2位起始位(总是为‘1’)+1位翻转位(用于区分连续按键)+5位系统地址码+6位指令码。 - 逻辑定义:采用双相相位编码(Manchester编码)。每一位数据都在中点发生跳变。逻辑‘0’定义为“前半位为高电平,后半位为低电平”;逻辑‘1’定义为“前半位为低电平,后半位为高电平”。
- 特点:没有引导码,任何时刻都可以开始接收。抗干扰能力更强,编解码逻辑稍复杂。
为了更直观地对比,我们可以看下面的表格:
| 特性 | NEC协议 | RC-5协议 |
|---|---|---|
| 载波频率 | 38kHz | 36kHz |
| 编码方式 | 脉冲位置调制(PPM) | 双相相位调制(Manchester) |
| 数据帧长度 | 32位数据 + 引导码 | 固定14位 |
| 逻辑‘0’ | 560us低 + 560us高 | 前半高,后半低(中点下降沿) |
| 逻辑‘1’ | 560us低 + 1.69ms高 | 前半低,后半高(中点上升沿) |
| 引导码 | 有(9ms低 + 4.5ms高) | 无 |
| 错误校验 | 8位数据+8位反码 | 无(依靠编码本身) |
| 典型应用 | 电视、空调、机顶盒等 | 飞利浦音响、电视 |
2.3 红外接收头选型与电路设计要点
市面上常见的接收头有三脚(VCC, GND, OUT)和贴片两种封装。选型时要注意:
- 中心频率:必须与你的发射端匹配。38kHz最通用。
- 供电电压:常见为3.3V或5V兼容,确认你的单片机系统电压。
- 接收角度与距离:角度越大,对准越容易,但可能更容易受到侧面干扰。距离通常室内无障碍可达7-10米,足够家用。
电路连接极其简单,但细节决定成败:
VCC ----> 单片机电源(3.3V/5V) GND ----> 单片机地 OUT ----> 单片机GPIO引脚(配置为上拉输入或外部中断引脚)- 必须加电源去耦电容:在接收头的VCC和GND引脚之间,就近并联一个10μF的电解电容和一个0.1μF的瓷片电容。红外接收头内部有高增益放大器,对电源噪声极其敏感,不加电容会导致误触发甚至无法工作。
- 上拉电阻:如果单片机GPIO内部无上拉电阻,需要在OUT引脚和VCC之间接一个4.7kΩ ~ 10kΩ的上拉电阻,确保空闲时输出为高电平。
- 远离干扰源:尽量远离电机、继电器、开关电源等噪声源,布线时信号线不要与功率线平行走线。
3. 在51单片机上的应用与软件解码实战
51单片机(如STC89C52)资源有限,没有专门的外设处理这种复杂时序,通常采用外部中断+定时器的软件解码方式。这是理解红外解码原理的最佳路径。
3.1 系统初始化与引脚配置
首先,将红外接收头的OUT引脚连接到51单片机的INT0(P3.2)或INT1(P3.3)引脚,以利用其外部中断功能。
#include <reg52.h> sbit IR_IN = P3^2; // 红外接收头输出引脚接在P3.2 (INT0) void Timer0_Init(void); // 定时器0初始化,用于精确计时 void ExtInt0_Init(void); // 外部中断0初始化 void DelayMs(unsigned int ms); // 毫秒延时函数(需根据晶振调整) void main() { EA = 1; // 开启总中断 Timer0_Init(); ExtInt0_Init(); // ... 其他初始化 while(1) { // 主循环,解码成功后的处理逻辑放在这里 if(ir_flag) { // ir_flag是一个全局标志位,在中断中置位 ir_flag = 0; // 根据解码得到的用户码和数据码执行相应操作 if(ir_user_code == 0x00FF && ir_data_code == 0x15) { // 例如,接收到音量+键 P1 = ~P1; // 用LED演示 } } } } void Timer0_Init(void) { TMOD &= 0xF0; // 清除T0控制位 TMOD |= 0x01; // 设置T0为模式1(16位定时器) // 假设晶振为11.0592MHz,定时器每计一个数耗时1.085us // 我们不预装初值,在中断中动态处理时间计算 ET0 = 1; // 开启T0中断 TR0 = 1; // 启动T0 } void ExtInt0_Init(void) { IT0 = 1; // 设置外部中断0为下降沿触发(因为接收头输出反向,下降沿对应引导码/数据起始) EX0 = 1; // 开启外部中断0 }3.2 核心解码逻辑实现
解码的核心思想是:在外部中断(接收到下降沿)中,启动定时器精确测量两个下降沿之间的高电平持续时间,根据这个时间来判断是引导码、逻辑‘1’、逻辑‘0’还是重复码。
unsigned int ir_time = 0; // 用于记录高电平时间 bit ir_flag = 0; // 解码成功标志 unsigned char ir_data[4]; // 存储解码出的4字节数据(用户码高8、低8、数据码、数据反码) unsigned char ir_cnt = 0; // 数据位计数器 void ExtInt0_ISR(void) interrupt 0 { static unsigned int last_fall_time = 0; // 上一次下降沿的时刻 unsigned int current_time, interval; // 关闭外部中断,防止在解码过程中被新的下降沿打断 EX0 = 0; // 获取当前定时器计数值(即从开始到现在的时间) current_time = (TH0 << 8) | TL0; // 合并定时器高8位和低8位 TH0 = 0; // 重置定时器,为下一次计时做准备 TL0 = 0; // 计算距离上一次下降沿的时间间隔(单位:定时器计数) interval = current_time - last_fall_time; last_fall_time = current_time; // 更新本次下降沿时间 // 判断时间间隔属于哪种情况 if(interval > 8000 && interval < 10000) { // 引导码 9ms 低电平后的下降沿?不,这里测量的是高电平 // 注意:接收头输出反向。引导码的波形是:9ms低(接收头输出高),4.5ms高(接收头输出低)。 // 我们是在下降沿触发中断,所以第一个下降沿是9ms低电平结束的时刻,此时ir_time记录的是9ms低电平时间。 // 我们需要等待下一个下降沿(4.5ms高电平结束)来判断引导码。 // 更常见的做法是:在中断开始先判断当前引脚电平。 // 这里为简化,我们换一种思路:在中断中不直接判断,而是记录时间,在主循环或状态机中分析。 // 但典型教材代码常在中断中完成所有判断,我们按典型写法调整: } // 更经典的中断服务函数写法是: if(IR_IN == 0) { // 如果当前引脚是低电平(说明刚进入的是下降沿后的低电平段) ir_time = (TH0 << 8) | TL0; // 记录当前定时器值,这个值代表了**刚刚结束的高电平的持续时间** TH0 = 0; TL0 = 0; } else { // 如果当前引脚是高电平(说明刚进入的是上升沿后的高电平段) unsigned int time_val = (TH0 << 8) | TL0; // 这个值代表了**刚刚结束的低电平的持续时间** TH0 = 0; TL0 = 0; // 分析低电平时间(即接收头输出高电平的时间) if((time_val > 7800) && (time_val < 8800)) { // 约8.5ms,对应引导码的9ms低电平(有一定误差范围) ir_cnt = 0; // 开始接收数据,位计数器清零 } else if((time_val > 950) && (time_val < 1100)) { // 约1.02ms,对应逻辑‘0’或‘1’的560us低电平 // 此时需要结合之前记录的高电平时间(ir_time)来判断是0还是1 if((ir_time > 950) && (ir_time < 1100)) { // 高电平约560us ir_data[ir_cnt/8] &= ~(0x01 << (ir_cnt%8)); // 逻辑‘0’ ir_cnt++; } else if((ir_time > 1650) && (ir_time < 1800)) { // 高电平约1.69ms ir_data[ir_cnt/8] |= (0x01 << (ir_cnt%8)); // 逻辑‘1’ ir_cnt++; } if(ir_cnt >= 32) { // 接收完32位数据 ir_flag = 1; // 解码成功 EX0 = 1; // 重新开启外部中断 return; } } else if((time_val > 3800) && (time_val < 4500)) { // 约4.2ms,可能是重复码 // 处理重复码... } } EX0 = 1; // 重新开启外部中断 }实操心得:在51上做红外解码,定时器的精度和中断响应速度是关键。建议使用12MHz或11.0592MHz的晶振,避免使用内部RC振荡器,因为误差太大。中断服务函数(ISR)要尽可能短小高效,只做最必要的计时和判断,复杂的校验和处理逻辑放到主循环中根据
ir_flag标志去执行。另外,所有的时间判断都要留出足够的误差余量(±20%),因为不同品牌的遥控器、接收头以及环境温度都会造成时序偏差。
3.3 51单片机解码的优化与常见问题
问题1:解码不稳定,时灵时不灵。
- 排查:首先用示波器观察接收头OUT引脚波形。如果没有示波器,可以编写一个简单程序,将接收头输出直接连接到一个LED上,观察按遥控器时LED的闪烁情况。正常应看到密集的闪烁(引导码)和疏密不同的闪烁(数据)。
- 解决:
- 检查电源:确保接收头VCC电压稳定,务必加上之前提到的10μF + 0.1μF去耦电容。
- 调整时间容差:适当放宽代码中的时间判断范围。例如,逻辑‘0’的高电平判断从(950, 1100)调整为(800, 1200)。
- 规避中断冲突:确保在红外解码的关键时间段(约几十毫秒),没有其他更高级别或长时间的中断(如串口中断)发生,以免丢失时序。
问题2:只能解码一次,之后再也解不出来。
- 排查:很可能是中断或定时器没有正确重置。在解码完成或出错后,是否重置了
ir_cnt,是否重新开启了外部中断(EX0=1)? - 解决:在解码成功(
ir_flag=1)或判断为错误帧后,必须将状态变量(ir_cnt,last_fall_time等)清零,并确保EX0=1。
优化技巧:使用状态机重构解码程序。将解码过程分为IDLE(空闲)、LEADER(等待引导码)、DATA(接收数据)、REPEAT(处理重复码)等状态,使程序逻辑更清晰,易于维护和调试。
4. 在STM32上的应用与硬件解码进阶
STM32的性能远超51,除了可以用更精准的软件解码外,其强大的外设(特别是定时器和中断控制器)为我们提供了更优雅、更节省CPU资源的解决方案——利用定时器的输入捕获功能进行硬件解码。
4.1 利用定时器输入捕获实现精准解码
以STM32F103C8T6的TIM2_CH1(PA0)为例,配置为输入捕获模式,可以自动记录每个上升沿或下降沿发生时定时器的计数值,从而精确计算出脉冲宽度。
CubeMX配置要点:
- 将连接红外接收头的GPIO(如PA0)配置为
TIM2_CH1。 - 配置TIM2:
- Clock Source: Internal Clock
- Prescaler (PSC): 71 (假设系统时钟72MHz,分频后得到1MHz计数频率,即1个计数=1μs)
- Counter Period (ARR): 65535 (16位定时器最大值)
- CH1 Polarity: Rising Edge / Falling Edge (通常先设置为上升沿捕获,在中断中切换)
- 开启TIM2的捕获/比较中断(CC1IE)。
代码实现核心:
// 在中断回调函数中处理 uint32_t this_capture, pulse_width; static uint32_t last_capture = 0; static uint8_t state = 0; // 解码状态机状态 static uint32_t raw_data[32]; // 存储每一位的高低电平时间 static uint8_t bit_index = 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim->Instance == TIM2) { this_capture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC1OF)) { // 处理溢出 __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC1OF); } // 计算脉冲宽度(单位:微秒) if(this_capture > last_capture) { pulse_width = this_capture - last_capture; } else { pulse_width = (0xFFFF - last_capture) + this_capture; // 处理定时器溢出 } last_capture = this_capture; // 根据当前捕获的边沿和计算出的脉冲宽度进行状态机解析 // 状态0:等待引导码低电平(下降沿触发,测量到的高电平时间应>8ms) // 状态1:等待引导码高电平结束(上升沿触发,测量到的低电平时间应>4ms) // 状态2:接收数据位(交替检测下降沿和上升沿,根据高电平时间判断0/1) // 具体状态机代码较长,逻辑与51单片机类似,但得益于硬件计时,精度和可靠性极高。 // 切换捕获边沿,为测量下一个脉冲做准备 if(__HAL_TIM_GET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1) == TIM_INPUTCHANNELPOLARITY_RISING) { __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); } else { __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); } __HAL_TIM_SET_COUNTER(htim, 0); // 可选,清空计数器重新开始 HAL_TIM_IC_Start_IT(htim, TIM_CHANNEL_1); // 重新启动捕获 } }4.2 使用标准外设库(如HAL库)与第三方库
为了提升开发效率,我们可以利用成熟的第三方库,如“IRremote”库的STM32移植版。这类库通常已经实现了NEC、RC-5、Sony等多种协议的解码与发送,提供了友好的API。
使用步骤通常如下:
- 将库文件(
irremote.c,irremote.h等)添加到工程。 - 根据库要求,配置一个定时器用于微秒级延时(
delay_us),配置一个GPIO用于接收,另一个GPIO用于发送(如果需要)。 - 初始化库:
IRrecvInit(RECV_PIN)。 - 在主循环中调用
IRrecvDecode(&results),如果返回true,则可以从results.value中获取解码后的键值。
优势:代码成熟稳定,支持协议多,开发速度快。劣势:可能对硬件资源有特定要求,需要理解其配置方式,有时为了适配自己的板子需要修改底层驱动。
4.3 STM32方案对比与选型建议
| 解码方式 | 实现难度 | CPU占用 | 精度/稳定性 | 适用场景 |
|---|---|---|---|---|
| 软件延时查询 | 简单 | 高(阻塞) | 低,受系统负载影响 | 对实时性要求极低,仅学习原理 |
| 外部中断+定时器 | 中等 | 中(中断驱动) | 较高 | 51单片机,STM32简单应用 |
| 定时器输入捕获 | 复杂 | 低(硬件辅助) | 高 | 高可靠性、多任务系统首选 |
| 第三方库 | 简单 | 取决于库实现 | 高 | 快速原型开发,需要支持多种协议 |
个人建议:对于STM32项目,如果只是简单功能,用外部中断+定时器(类似51的方法)完全够用。但如果系统复杂,有多个任务运行,强烈推荐使用定时器输入捕获方式,它将最耗时的计时工作交给硬件,极大地解放了CPU。而使用第三方库则是产品开发中快速集成的优选。
5. 调试技巧与高级应用拓展
5.1 没有示波器如何调试?
示波器是分析红外信号的利器,但并非人人都有。我们可以用一些“土办法”:
- LED指示法:将接收头的OUT引脚通过一个三极管或直接(如果电流小)驱动一个LED。正常工作时,按下遥控器,LED会快速闪烁。如果LED常亮或常灭,说明电路或电源有问题。
- 串口打印脉冲宽度:在中断或输入捕获回调中,将测量到的高电平、低电平时间通过串口打印出来。通过对比打印出的时间序列(单位微秒),与NEC等协议的标准时间对比,就能判断解码是否正确,误差有多大。这是最有效的软件调试方法。
- 逻辑分析仪:一个便宜的逻辑分析仪(几十元)比示波器更适合分析数字时序。它可以长时间记录GPIO的电平变化,并以波形形式显示,能清晰看到引导码、数据位的结构,是调试数字通信协议的“神器”。
5.2 实现红外发射(控制其他设备)
掌握了接收,发射就水到渠成。你需要一个红外发射管(IR LED)和一个限流电阻(通常100Ω左右)。
原理:用单片机的GPIO口,通过三极管或MOS管驱动IR LED。发送数据时,需要按照协议(如NEC)的时序,用38kHz的载波调制后,控制LED的亮灭。
在STM32上的高效实现:
- 方法一:PWM + 定时器:用一个定时器(如TIM3)产生38kHz的PWM波作为载波,输出到IR LED的驱动电路。用另一个定时器或软件控制PWM输出的启停,以生成符合协议的数据波形。这是最专业的方式。
- 方法二:延时模拟:在需要发送38kHz载波的时间段内,用
for循环配合__nop()或微秒延时函数,高速翻转GPIO来模拟38kHz方波。这种方法简单但占用CPU,精度差。
// 模拟发送一个38kHz的560us脉冲(逻辑‘0’的低电平部分) void IR_Send_0_Pulse(void) { uint32_t i; for(i = 0; i < 21; i++) { // 560us / (1/38kHz ≈ 26.3us) ≈ 21.3个周期 IR_LED_ON(); // 打开驱动电路 Delay_us(13); // 载波半周期约13us IR_LED_OFF(); // 关闭驱动电路 Delay_us(13); } }5.3 常见问题排查速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 完全无反应 | 1. 电源接反或电压不对 2. 接收头损坏 3. 单片机引脚配置错误 | 1. 检查VCC/GND,电压用万用表测量。 2. 更换接收头。 3. 确认引脚配置为输入模式,并启用上拉。 |
| 反应不灵敏,要对准很近 | 1. 发射端电池电量不足 2. 接收头接收角度小或性能差 3. 环境强光干扰(如太阳直射) | 1. 更换遥控器电池。 2. 尝试不同型号的接收头,选择接收角度大的。 3. 避免强光环境,或为接收头加装遮光罩。 |
| 解码错误,键值混乱 | 1. 时序判断范围太窄 2. 中断被其他高优先级任务打断 3. 电源噪声大 | 1. 扩大代码中的时间容差范围(±25%)。 2. 调整中断优先级,确保红外中断响应及时。 3. 检查并加强电源滤波,接收头VCC引脚并联电容。 |
| 连续按键处理异常 | 重复码逻辑未正确处理 | 查阅协议文档,正确识别和处理重复码(如NEC协议的9ms低+2.25ms高+560us低脉冲)。 |
| 自己发的码自己收不到 | 1. 发射管驱动电流不足 2. 发射与接收载波频率不匹配 3. 硬件电路自干扰 | 1. 减小限流电阻,增加驱动电流(但不要超过LED极限)。 2. 确保发射的载波频率与接收头中心频率一致(通常38kHz)。 3. 发射时短暂关闭接收中断,或从物理上拉开发射管与接收头的距离。 |
红外通信是一个细节见真章的领域。从原理上吃透调制、编码,在实践上重视电源、滤波和时序容错,你就能稳定可靠地驾驭这项技术。无论是用51单片机做学习实验,还是用STM32开发商业产品,这套从基础到进阶的知识体系都能让你得心应手。最后记住,当你遇到奇怪的解码问题时,第一件事就是用串口打印出时间脉冲,或者用逻辑分析仪抓取波形,数据不会说谎,它能帮你快速定位问题根源。