STM32红外遥控解码,NEC协议驱动

引言

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

本文将以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的分享

相关推荐
羽获飞2 小时前
从零开始学嵌入式之STM32——30.使用触发输入和从模式测量PWM信号的占空比
stm32·单片机·嵌入式硬件
技术民工之路2 小时前
Keil MDK 5.40:STM32 工程编译 + 调试完整教程
stm32·单片机·嵌入式硬件
小马学嵌入式~3 小时前
linux开发深度学习-时钟
linux·arm开发·嵌入式硬件·学习
LCG元3 小时前
STM32嵌入式开发:基于STM32F103的智能语音识别系统
stm32·嵌入式硬件·语音识别
项目題供诗4 小时前
51单片机入门-直流电机(十四)
单片机·嵌入式硬件·51单片机
安庆平.Я4 小时前
STM32——FreeRTOS - 任务创建和删除 ~ 静态方法
stm32·单片机·嵌入式硬件
悠哉悠哉愿意5 小时前
【单片机学习笔记】第十一届省赛复盘
笔记·单片机·嵌入式硬件·学习
学嵌入式的小杨同学5 小时前
STM32 进阶封神之路(二十七):MQTT 深度解析 —— 从协议原理到 OneNET 云平台接入(底层逻辑 + AT 指令开发)
stm32·单片机·嵌入式硬件·mcu·硬件架构·pcb·嵌入式实时数据库
DLGXY5 小时前
STM32(二十九)——读写、擦除FLASH
前端·stm32·嵌入式硬件