引言

先上两张照片,这个模块并不贵,带上遥控器估计三四块钱吧,买了也挺久(大约一年了?),最近终于有时间了,咱们一起来把他驱动起来。

本文将以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 状态机定义

typedef enum {
    IR_STATE_IDLE,          // 空闲,等待引导码低电平
    IR_STATE_START,         // 已收到引导码低电平,等待引导码高电平
    IR_STATE_DATA,          // 接收32位数据
    IR_STATE_COMPLETE       // 接收完成
} IR_State_t;

3.2 脉冲宽度测量

在外部中断中记录每次电平变化的时间,计算两次中断之间的差值得到上一个电平的持续时间。使用TIM2的计数器(1µs分辨率),处理溢出情况。

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 状态机核心逻辑

中断处理函数根据当前状态和电平变化(上升沿/下降沿)进行状态转移和数据解析。

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 数据读取与状态复位

主循环中检测到有效数据后,打印结果并清除标志,重置状态机。

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的分享

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐