引言


先上两张照片,这个模块并不贵,带上遥控器估计三四块钱吧,买了也挺久(大约一年了?),最近终于有时间了,咱们一起来把他驱动起来。
本文将以STM32F103C6T6最小系统板为例,介绍如何驱动HX1838红外接收头,解码NEC协议红外遥控器信号。重点讲解接收过程中的状态机设计、脉冲宽度测量以及NEC协议解析流程,并提供关键代码实现,帮助后来者理解红外解码的原理和实现。
1. NEC红外协议基础
NEC协议是红外遥控最常用的协议之一,其数据格式简单可靠,被大量消费电子设备采用。一个完整的NEC数据帧包含以下部分:
-
引导码:9ms的低电平 + 4.5ms的高电平
-
地址码:8位(通常表示设备地址)
-
地址反码:8位(地址码的按位取反,用于校验)
-
命令码:8位(按键功能码)
-
命令反码:8位(命令码的按位取反,用于校验)
数据位的编码采用脉冲宽度调制:
-
逻辑"0":560µs低电平 + 560µs高电平
-
逻辑"1":560µs低电平 + 1.69ms高电平
发送顺序为LSB优先(最低位先发),即数据位从低位到高位依次传输。
如果遥控器按键一直按住不放,则只发送一次完整数据帧,之后每隔约110ms发送一个重复码:9ms低电平 + 2.25ms高电平(无数据位)。重复码的识别可根据需要实现,本文暂不处理。
这里我放上几张我使用逻辑分析仪抓到的时序图,方便大家理解。有一说一没想到我十几块买的逻辑分析仪还能解析NEC协议,通过这几张图我们就能很清晰的看到协议内容及其构成。




2. 硬件连接与CubeMX配置
2.1 模块介绍
-
红外接收头 HX1838:一体化红外接收头,输出TTL电平,无信号时为高电平,收到红外脉冲时输出反向电平(低电平有效)。引脚定义:VCC(3.3V/5V)、GND、OUT。
-
STM32F103C6T6:使用PA0引脚作为外部中断输入,连接HX1838的OUT端。
2.2 CubeMX关键配置
-
时钟:系统时钟72MHz(HSE+PLL)
-
PA0 :GPIO_EXTI0,模式
GPIO_MODE_IT_RISING_FALLING(双边沿触发),上拉GPIO_PULLUP -
USART1:PA9(TX)、PA10(RX),异步模式,波特率115200,用于调试输出
-
TIM2:预分频器71,自动重载值0xFFFF,计数频率1MHz(1µs),用于测量脉冲宽度
-
中断 :使能EXTI0中断,设置优先级


3. 驱动核心设计
红外解码的关键是精确测量高低电平的持续时间,并依据NEC协议的时序规则进行状态转移。我们采用状态机实现。
3.1 状态机定义
cpp
typedef enum {
IR_STATE_IDLE, // 空闲,等待引导码低电平
IR_STATE_START, // 已收到引导码低电平,等待引导码高电平
IR_STATE_DATA, // 接收32位数据
IR_STATE_COMPLETE // 接收完成
} IR_State_t;
3.2 脉冲宽度测量
在外部中断中记录每次电平变化的时间,计算两次中断之间的差值得到上一个电平的持续时间。使用TIM2的计数器(1µs分辨率),处理溢出情况。
cpp
static uint32_t IR_GetTick(void) {
return __HAL_TIM_GET_COUNTER(&htim2);
}
// 在中断中计算duration
if (current_time >= last_time)
duration = current_time - last_time;
else
duration = (0xFFFF - last_time) + current_time; // 处理溢出
3.3 状态机核心逻辑
中断处理函数根据当前状态和电平变化(上升沿/下降沿)进行状态转移和数据解析。
cpp
void IR_HandleEXTI(void) {
uint32_t current_time = IR_GetTick();
uint32_t duration;
uint8_t pin_state = HAL_GPIO_ReadPin(IR_PORT, IR_PIN);
// 计算持续时间(处理溢出)
// ...
// 判断电平变化类型
if (last_pin_state == GPIO_PIN_RESET && pin_state == GPIO_PIN_SET) {
// 上升沿:低电平结束,测量低电平持续时间
switch (state) {
case IR_STATE_IDLE:
if (duration > 8000 && duration < 10000) // 9ms引导码低电平
state = IR_STATE_START;
break;
case IR_STATE_DATA:
// 数据位低电平应为560µs,检查异常
if (duration < 300 || duration > 800)
state = IR_STATE_IDLE;
break;
// ...
}
} else if (last_pin_state == GPIO_PIN_SET && pin_state == GPIO_PIN_RESET) {
// 下降沿:高电平结束,测量高电平持续时间
switch (state) {
case IR_STATE_START:
if (duration > 4000 && duration < 5000) // 4.5ms引导码高电平
state = IR_STATE_DATA;
else
state = IR_STATE_IDLE;
break;
case IR_STATE_DATA:
// 根据高电平持续时间判断数据位0或1
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; // 异常脉冲
break;
}
if (bit_count == 32) {
// 解析数据
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;
}
break;
// ...
}
}
last_pin_state = pin_state; // 保存状态供下次使用
}
3.4 数据读取与状态复位
主循环中检测到有效数据后,打印结果并清除标志,重置状态机。
cpp
void IR_ClearResult(void) {
result.valid = 0;
state = IR_STATE_IDLE;
bit_count = 0;
data_buffer = 0;
}
4. 测试与结果
将红外接收头连接至PA0,串口连接至PC,运行程序。按下遥控器按键,串口输出解码结果,例如:
NEC解码结果:
原始数据: 0x00FF45BA
地址码: 0x00 (0)
地址反码: 0xFF (255)
命令码: 0x45 (69)
命令反码: 0xBA (186)
校验: 通过
每个按键对应唯一的命令码,可根据命令码实现自定义功能。
5. 常见问题与调试
-
无法解码:检查红外接收头输出引脚是否连接正确,确认定时器计数频率为1MHz,脉冲宽度阈值可根据实际波形微调。
-
只能解码一次 :确保
IR_ClearResult()重置了状态机为IR_STATE_IDLE。 -
干扰误触发:可在中断中添加软件去抖动或调整阈值范围。
6. 总结
本文介绍了基于STM32F103和HX1838红外接收头的NEC协议解码驱动,通过状态机测量脉冲宽度,完整解析红外遥控信号。关键点包括:
-
双边沿外部中断测量高、低电平持续时间
-
根据NEC协议时序进行状态转移和数据位判断
-
处理定时器溢出,保证测量精度
该驱动可轻松移植到其他STM32系列,为嵌入式红外遥控应用提供可靠基础。完整代码已通过测试,读者可根据实际需求扩展重复码识别、按键映射等功能。
7. 现象展示

8.代码参考
通过网盘分享的文件:IR_NEC_Demo_STM32F103.zip
链接: https://pan.baidu.com/s/1ZTxQSrUt3kyMu9SOadnauw?pwd=nthm 提取码: nthm
--来自百度网盘超级会员v8的分享