1. 项目概述
最近在整理工作室的电子元件时,翻出了一个闲置已久的HX1838红外接收模块和配套遥控器。这个模块价格非常亲民,整套下来也就三四块钱,但功能却相当实用。作为嵌入式开发者,我决定用STM32F103C6T6最小系统板来实现NEC协议的红外解码功能,这不仅是个很好的学习案例,也能为后续的智能家居项目打下基础。
红外遥控在消费电子领域应用广泛,从电视、空调到各种智能设备都能看到它的身影。NEC协议因其简单可靠的特性,成为了最常用的红外通信协议之一。通过这个项目,我们不仅能掌握红外通信的基本原理,还能学习如何用STM32的外部中断和定时器来实现精确的时序测量。
2. 硬件准备与连接
2.1 所需材料清单
要完成这个项目,我们需要准备以下硬件:
- STM32F103C6T6最小系统板(或其他STM32系列开发板)
- HX1838红外接收模块
- NEC协议红外遥控器
- USB转TTL串口模块(用于调试输出)
- 杜邦线若干
- 逻辑分析仪(可选,用于调试)
2.2 硬件连接详解
HX1838红外接收模块有三个引脚:
- VCC:接3.3V或5V电源
- GND:接地
- OUT:信号输出,接STM32的PA0引脚
这里选择PA0引脚是因为它支持外部中断功能,方便我们捕获红外信号的电平变化。实际连接时,建议使用短杜邦线并尽量远离干扰源,因为红外信号本身比较微弱,长导线容易引入噪声。
注意:HX1838的输出信号是反向的,即无信号时输出高电平,收到红外信号时输出低电平。这一点在编程时需要特别注意。
2.3 电源考虑
虽然HX1838可以工作在3.3V或5V,但实测发现5V供电时接收距离更远。如果使用3.3V系统,可以考虑在VCC和3.3V之间加一个100Ω电阻,既保证工作电压又不会影响信号质量。
3. NEC协议深度解析
3.1 协议帧结构
NEC协议的一帧数据由以下几部分组成:
- 引导码:9ms的低电平 + 4.5ms的高电平
- 地址码:8位设备地址
- 地址反码:8位(地址码按位取反)
- 命令码:8位按键功能码
- 命令反码:8位(命令码按位取反)
数据采用脉冲位置调制(PPM)方式:
- 逻辑"0":560µs低电平 + 560µs高电平
- 逻辑"1":560µs低电平 + 1.69ms高电平
3.2 时序细节与容错处理
在实际应用中,由于元器件差异和环境干扰,脉冲宽度可能会有±10%的偏差。因此我们在代码中设置了合理的容错范围:
- 引导码低电平:8-10ms
- 引导码高电平:4-5ms
- 数据位低电平:500-600µs
- 逻辑"0"高电平:500-600µs
- 逻辑"1"高电平:1.6-1.8ms
这种宽松的判断条件可以提高解码的成功率,特别是在有一定干扰的环境中。
3.3 重复码机制
当遥控器按键被长时间按住时,NEC协议会发送重复码来节省能量。重复码的格式为:
- 9ms低电平
- 2.25ms高电平
- 560µs低电平(结束脉冲)
重复码没有数据位,只是告诉接收端之前的按键仍然被按住。在我们的基础实现中暂时不处理重复码,但保留了扩展接口。
4. STM32CubeMX配置
4.1 时钟配置
系统时钟配置为72MHz,使用外部8MHz晶振(HSE)通过PLL倍频实现。高精度的时钟源对脉冲宽度测量至关重要。
4.2 GPIO配置
PA0配置为GPIO_EXTI0,模式设置为GPIO_MODE_IT_RISING_FALLING(双边沿触发),上拉电阻使能(GPIO_PULLUP)。这样配置可以捕获红外信号的所有跳变沿。
4.3 定时器配置
TIM2配置为:
- 预分频器(Prescaler):71(72MHz/(71+1)=1MHz)
- 计数周期(Counter Period):0xFFFF(16位最大值)
- 计数模式:向上计数
这样配置后,TIM2的计数频率为1MHz,即每个计数代表1µs,非常适合测量红外信号的微秒级脉冲宽度。
4.4 串口配置
USART1配置为异步模式,波特率115200,8位数据位,无校验位,1位停止位。用于输出调试信息和解码结果。
5. 软件设计实现
5.1 状态机设计
红外解码本质上是一个时序解析过程,非常适合用状态机实现。我们定义了四个状态:
c复制typedef enum {
IR_STATE_IDLE, // 空闲状态,等待引导码
IR_STATE_START, // 已收到引导码低电平,等待高电平
IR_STATE_DATA, // 正在接收数据位
IR_STATE_COMPLETE // 接收完成
} IR_State_t;
状态转移由外部中断触发,根据当前状态和脉冲宽度决定下一个状态。
5.2 脉冲宽度测量
在外部中断服务函数中,我们使用TIM2来测量脉冲宽度:
c复制uint32_t IR_GetTick(void) {
return __HAL_TIM_GET_COUNTER(&htim2);
}
// 在中断中计算脉冲持续时间
uint32_t current_time = IR_GetTick();
uint32_t duration;
if (current_time >= last_time) {
duration = current_time - last_time;
} else {
// 处理定时器溢出
duration = (0xFFFF - last_time) + current_time;
}
last_time = current_time;
这种方法即使定时器溢出也能正确计算时间间隔,保证了长时间测量的准确性。
5.3 数据解析实现
在IR_STATE_DATA状态下,我们根据高电平持续时间判断数据位是0还是1:
c复制if (duration > 1000 && duration < 1800) { // 1.69ms -> 1
data_buffer = (data_buffer << 1) | 1;
bit_count++;
} else if (duration > 400 && duration < 800) { // 560µs -> 0
data_buffer = (data_buffer << 1) | 0;
bit_count++;
} else {
// 异常脉冲,重置状态机
state = IR_STATE_IDLE;
}
接收完32位数据后,进行反码校验:
c复制uint8_t addr = (data_buffer >> 24) & 0xFF;
uint8_t addr_inv = (data_buffer >> 16) & 0xFF;
uint8_t cmd = (data_buffer >> 8) & 0xFF;
uint8_t cmd_inv = data_buffer & 0xFF;
if ((addr == (uint8_t)(~addr_inv)) && (cmd == (uint8_t)(~cmd_inv))) {
// 校验通过,保存结果
result.raw_data = data_buffer;
result.address = addr;
result.command = cmd;
result.valid = 1;
state = IR_STATE_COMPLETE;
} else {
// 校验失败,丢弃数据
state = IR_STATE_IDLE;
}
6. 调试与优化
6.1 常见问题排查
-
无法解码任何信号
- 检查红外接收头的VCC和GND连接是否正确
- 确认OUT引脚确实连接到了PA0
- 用示波器或逻辑分析仪检查接收头是否有信号输出
-
解码不稳定,时好时坏
- 尝试缩短连接线长度
- 在VCC和GND之间加一个100nF去耦电容
- 调整脉冲宽度的判断阈值
-
只能解码第一次按键
- 确保在数据处理完成后调用了IR_ClearResult()重置状态机
- 检查中断是否被意外禁用
6.2 性能优化技巧
-
中断优化
- 保持中断服务函数尽可能简短
- 将耗时操作(如数据校验)移到主循环中处理
- 必要时使用DMA来减轻CPU负担
-
电源滤波
- 在红外接收头的VCC和GND之间并联一个100nF陶瓷电容和一个10µF电解电容
- 使用线性稳压器而非开关电源为接收头供电
-
软件滤波
- 对连续几次解码结果进行比较,只有一致时才认为有效
- 添加去抖动逻辑,防止单次按键被误判为多次
7. 实际应用扩展
7.1 按键映射实现
通过建立命令码到功能的映射表,可以实现不同按键控制不同功能:
c复制typedef struct {
uint8_t cmd;
void (*handler)(void);
} KeyMap_t;
KeyMap_t key_map[] = {
{0x45, &VolumeUp},
{0x46, &VolumeDown},
// 其他按键映射...
};
void IR_ProcessCommand(uint8_t cmd) {
for (int i = 0; i < sizeof(key_map)/sizeof(KeyMap_t); i++) {
if (key_map[i].cmd == cmd) {
key_map[i].handler();
break;
}
}
}
7.2 重复码处理
要支持重复码检测,可以在状态机中添加新状态:
c复制typedef enum {
// 原有状态...
IR_STATE_REPEAT
} IR_State_t;
// 在中断处理中添加对重复码的判断
if (duration > 2000 && duration < 2500) { // 2.25ms重复码高电平
state = IR_STATE_REPEAT;
result.repeat = 1;
}
7.3 多协议支持
通过抽象协议接口,可以轻松扩展支持其他红外协议:
c复制typedef struct {
void (*init)(void);
uint8_t (*decode)(IR_Result_t *result);
} IR_Protocol_t;
IR_Protocol_t protocols[] = {
{NEC_Init, NEC_Decode},
{RC5_Init, RC5_Decode},
// 其他协议...
};
8. 项目总结与心得
通过这个项目,我们完整实现了STM32对NEC红外协议的解码。几个关键点值得总结:
-
定时器配置:1MHz的计数频率(1µs分辨率)是测量红外信号的关键,预分频值需要根据系统时钟精心计算。
-
状态机设计:清晰的状态划分和转移条件使代码更易理解和维护。在实际调试中,我发现最初设计的状态机缺少对异常情况的处理,后来增加了超时重置机制才使解码更稳定。
-
中断处理:保持中断服务函数简短高效非常重要。最初我在中断中直接进行复杂的数据处理,导致有时会丢失后续中断。后来改为只在中断中记录时间,在主循环中处理数据,稳定性大幅提升。
-
实际测试:不同品牌遥控器的信号可能存在细微差异,建议收集多个遥控器进行测试,适当调整时序容错范围。
这个红外解码框架已经成功应用在我最近的几个项目中,包括一个基于STM32的万能遥控器和智能家居控制器。读者可以根据实际需求进一步扩展功能,比如添加学习功能、支持更多协议等。完整的工程代码可以从提供的链接下载,建议先阅读代码中的详细注释,再根据需要进行修改。
