基于天空星HC32F4A0开发板的NEC红外遥控接收实战:从协议解析到驱动实现

基于天空星HC32F4A0开发板的NEC红外遥控接收实战:从协议解析到驱动实现 基于天空星HC32F4A0开发板的NEC红外遥控接收实战从协议解析到驱动实现最近在做一个智能家居的小项目需要用到红外遥控功能。翻出家里的旧遥控器发现大部分都是NEC协议的正好手头有天空星的HC32F4A0开发板就想着自己实现一个红外接收功能。今天就把这个实战过程分享给大家从协议原理到代码实现一步步带你搞定红外遥控接收。1. 红外遥控基础先搞懂NEC协议1.1 红外通信的基本原理咱们先来聊聊红外通信是怎么回事。红外线是波长在760nm到400um之间的电磁波人眼是看不见的。你家里的电视、空调、投影仪遥控器用的都是红外通信技术。红外通信的原理其实挺简单的发送端用特定的频率通常是38KHz来闪烁红外LED接收端有个专门的红外接收头能感应到这个频率的红外光。你可以把它想象成用闪光灯发摩尔斯电码只不过这个闪光灯闪得特别快38KHz而且人眼看不见。注意NEC协议用的就是38KHz这个频率这也是市面上最常见的红外遥控频率。1.2 NEC协议的数据格式NEC协议是红外遥控中最常用的协议之一很多电视、机顶盒、投影仪遥控器都用它。一个完整的NEC数据包包含以下几个部分组成部分说明时长引导码9ms低电平 4.5ms高电平13.5ms地址码8位设备地址8个数据位地址反码地址码的按位取反8个数据位命令码8位操作命令8个数据位命令反码命令码的按位取反8个数据位这里有个关键点NEC协议用高低电平的持续时间来区分0和1数据位0560us低电平 560us高电平总共1.12ms数据位1560us低电平 1.68ms高电平总共2.24ms你看0和1的低电平时间都是560us区别在于高电平的持续时间。这就是我们解码时要判断的依据。还有一个重复码当长按遥控器按键时发送的不是完整数据包而是9ms低电平 2.5ms高电平的重复码用来表示继续执行上一个命令。2. 硬件连接与引脚配置2.1 红外接收模块我用的是常见的红外接收头某宝上几毛钱一个。这种接收头有三个引脚VCC接3.3V或5V看模块规格GND接地OUT信号输出接MCU的GPIO提示红外接收头在没有接收到红外信号时输出高电平接收到38KHz红外信号时输出低电平。所以我们要配置为下降沿触发中断。2.2 天空星HC32F4A0引脚配置在天空星开发板上我选择了PA2引脚作为红外接收引脚。下面是具体的配置代码// 引脚定义 #define IR_PORT GPIO_PORT_A #define IR_PIN GPIO_PIN_02 #define EXTI_IRQN INT005_IRQn #define EXTI_CH EXTINT_CH02 #define EXTI_SRC_IRQ INT_SRC_PORT_EIRQ2 // 红外引脚初始化 void infrared_gpio_config(void) { /* 关闭存储器保护 */ LL_PERIPH_WE(LL_PERIPH_ALL); stc_gpio_init_t stcGpioInit; /* GPIO初始化 */ (void)GPIO_StructInit(stcGpioInit); stcGpioInit.u16PinDir PIN_DIR_IN; // 输入模式 stcGpioInit.u16ExtInt PIN_EXTINT_ON; // 中断使能 stcGpioInit.u16PullUp PIN_PU_OFF; // 上拉关闭 (void)GPIO_Init(IR_PORT, IR_PIN, stcGpioInit); stc_extint_init_t stcExtIntInit; /* 外部中断配置 */ (void)EXTINT_StructInit(stcExtIntInit); stcExtIntInit.u32Filter EXTINT_FILTER_ON; // 外部中断滤波使能 stcExtIntInit.u32FilterClock EXTINT_FCLK_DIV8; // 8分频 stcExtIntInit.u32Edge EXTINT_TRIG_FALLING; // 下降沿触发 (void)EXTINT_Init(EXTI_CH, stcExtIntInit); stc_irq_signin_config_t stcIrqSignConfig; /* IRQ配置 */ stcIrqSignConfig.enIntSrc EXTI_SRC_IRQ; // 外部中断源 stcIrqSignConfig.enIRQn EXTI_IRQN; // 中断号 stcIrqSignConfig.pfnCallback EXTI_IRQHANDLER; // 中断处理函数 (void)INTC_IrqSignIn(stcIrqSignConfig); /* NVIC初始化 */ NVIC_ClearPendingIRQ(stcIrqSignConfig.enIRQn); NVIC_SetPriority(stcIrqSignConfig.enIRQn, DDL_IRQ_PRIO_01); // 优先级 NVIC_EnableIRQ(stcIrqSignConfig.enIRQn); flag 0; }这里有几个关键配置我解释一下下降沿触发红外接收头平时输出高电平收到信号时变低所以用下降沿触发滤波使能红外信号容易受干扰开启滤波能提高稳定性8分频适当降低采样频率避免误触发3. 时序测量如何判断0和13.1 测量高低电平时间红外解码的核心就是测量高低电平的持续时间。NEC协议的最小时间单位是560us我们需要us级的测量精度。好在天空星的开发板模板里已经提供了us延时函数。先来看测量低电平时间的函数// 获取红外低电平时间 // 以微秒us作为时间参考 void get_infrared_low_time(uint32_t *low_time) { uint32_t time_val 0; while(GPIO_ReadInputDataBit(IR_PORT, IR_PIN) 0) { if(time_val 500) // 超时判断500 * 20us 10ms { *low_time time_val; return; } delay_us(20); // 每次延时20us time_val; } *low_time time_val; }这个函数的逻辑很简单当引脚为低电平时进入循环每次循环延时20us计数器加1当引脚变为高电平时退出循环最终的时间 计数器值 × 20us比如计数器值为28那么低电平时间就是28 × 20 560us正好是NEC协议的标准低电平时间。测量高电平时间的函数原理相同// 获取红外高电平时间 void get_infrared_high_time(uint32_t *high_time) { uint32_t time_val 0; while(GPIO_ReadInputDataBit(IR_PORT, IR_PIN) 1) { if(time_val 250) // 超时判断250 * 20us 5ms { *high_time time_val; return; } delay_us(20); time_val; } *high_time time_val; }3.2 引导码和重复码的判断每次接收红外数据第一个要判断的就是引导码。引导码的特点是9ms低电平 4.5ms高电平。重复码则是9ms低电平 2.5ms高电平。/****************************************************************** * 函数名称guide_and_repeat_code_judgment * 函数说明引导和重复码判断 * 函数形参无 * 函数返回1不是引导码 2重复码 0引导码 * 作 者LC * 备 注以20微秒us作为时间参考 引导码由一个9ms的低电平和一个4.5ms的高电平组成 重复码由一个9ms的低电平和一个2.5ms的高电平组成 ******************************************************************/ uint8_t guide_and_repeat_code_judgment(void) { uint32_t out_time 0; // 测量低电平时间 get_infrared_low_time(out_time); // time10ms 或者 time8ms 都不是引导码 if((out_time 500) || (out_time 400)) // 400-500对应8ms-10ms { return 1; } // 测量高电平时间 get_infrared_high_time(out_time); // x5ms 或者 x2ms 都不是引导码 if((out_time 250) || (out_time 100)) // 100-250对应2ms-5ms { return 1; } // 如果是重复码 2ms time 3ms if((out_time 100) (out_time 150)) // 100-150对应2ms-3ms { return 2; } return 0; // 是引导码 }这里的时间判断我解释一下低电平时间应该在8ms-10ms之间9ms ± 1ms容差高电平时间如果在2ms-3ms之间是重复码高电平时间如果在4ms-5ms之间是引导码4. 完整数据接收与解码4.1 数据接收流程完整的红外数据接收流程是这样的等待并判断引导码接收4组数据地址码、地址反码、命令码、命令反码每组数据8位共32位验证数据正确性// 接收红外数据 void receiving_infrared_data(void) { uint16_t group_num 0, data_num 0; uint32_t time 0; uint8_t bit_data 0; uint8_t ir_value[4] {0}; // 存储4个字节的数据 uint8_t guide_and_repeat_code 0; // 等待引导码 guide_and_repeat_code guide_and_repeat_code_judgment(); // 如果不是引导码则结束解码 if(guide_and_repeat_code 1) { printf(err\r\n); return; } // 共有4组数据地址码地址反码命令码命令反码 for(group_num 0; group_num 4; group_num) { // 接收一组8位的数据 for(data_num 0; data_num 8; data_num) { // 接收低电平 get_infrared_low_time(time); // 如果不在0.56ms内的低电平数据错误 if((time 60) || (time 20)) // 20-60对应400us-1200us { return; } time 0; // 接收高电平 get_infrared_high_time(time); // 判断是0还是1 // 如果是1高电平时间在1.2ms-2.0ms范围内 if((time 60) (time 100)) // 60-100对应1.2ms-2.0ms { bit_data 1; } // 如果是0高电平时间在0.2ms-1.0ms范围内 else if((time 10) (time 50)) // 10-50对应0.2ms-1.0ms { bit_data 0; } // 将数据左移新数据放在最低位 ir_value[group_num] 1; // 设置最低位 ir_value[group_num] | bit_data; // 用完时间变量要重新赋值 time 0; } } // 判断数据是否正确正确则保存数据 infrared_data_true_judgment(ir_value); flag 1; // 设置数据接收完成标志 }4.2 数据验证与存储NEC协议很贴心每个数据都带了一个反码用于校验。我们可以通过比较原码和反码来验证数据是否正确。// 定义红外数据结构 typedef struct INFRARED_DATA { uint8_t AddressCode; // 地址码 uint8_t AddressInverseCode; // 地址反码 uint8_t CommandCode; // 命令码 uint8_t CommandInverseCode; // 命令反码 } _INFRARED_DATA_STRUCT_; _INFRARED_DATA_STRUCT_ InfraredData; uint8_t flag 0; // 数据接收完成标志 // 红外数据是否正确判断 uint8_t infrared_data_true_judgment(uint8_t *value) { // 判断地址码是否正确 if(value[0] ! (uint8_t)(~value[1])) return 0; // 判断命令码是否正确 if(value[2] ! (uint8_t)(~value[3])) return 1; // 保存正确数据 InfraredData.AddressCode value[0]; InfraredData.AddressInverseCode value[1]; InfraredData.CommandCode value[2]; InfraredData.CommandInverseCode value[3]; return 0; } // 获取红外发送过来的命令 uint8_t get_infrared_command(void) { return InfraredData.CommandCode; } // 显示数据 void show_infrared_data(void) { if(flag ! 1) return; printf(%x %x %x %x\r\n, InfraredData.AddressCode, InfraredData.AddressInverseCode, InfraredData.CommandCode, InfraredData.CommandInverseCode); } // 清除红外发送过来的数据 void clear_infrared_command(void) { InfraredData.AddressCode 0; InfraredData.AddressInverseCode 0; InfraredData.CommandCode 0; InfraredData.CommandInverseCode 0; flag 0; }5. 中断处理与主程序5.1 中断服务函数红外接收需要实时性所以我们在中断中处理数据接收void EXTI_IRQHANDLER(void) { if(EXTINT_GetExtIntStatus(EXTI_CH) SET) // 中断标志位为1 { if(GPIO_ReadInputPins(IR_PORT, IR_PIN) RESET) // 如果是低电平 { receiving_infrared_data(); // 接收一次红外数据 } EXTINT_ClearExtIntStatus(EXTI_CH); // 清中断标志位 } }这里有个细节我们在下降沿触发中断但实际开始解码是在引脚为低电平时。这是因为红外信号开始是9ms的低电平我们等引脚变低后再开始解码。5.2 主程序示例最后在主程序中初始化并测试红外接收#include board.h #include bsp_uart.h #include stdio.h #include bsp_ir_receiver.h int main(void) { board_init(); uart1_init(115200U); // 红外接收初始化 infrared_gpio_config(); printf(Start!!!\r\n); while(1) { if(flag 1) // 有新的红外数据 { show_infrared_data(); // 显示接收到的数据 // 如果按下遥控器的1键假设命令码是0xA2 if(get_infrared_command() 0xA2) { printf(按下1按键\r\n); } // 清除数据准备接收下一次 clear_infrared_command(); } delay_ms(5); } }6. 实际调试中的几个坑点在实际调试这个红外接收功能时我踩过几个坑这里分享给大家时序精度问题最开始我用的是ms级延时结果完全解不出数据。后来发现NEC协议的时间精度要求很高必须用us级测量。天空星开发板的delay_us()函数帮了大忙。干扰问题日光灯、手机屏幕等都会发出红外光可能干扰接收。解决办法是开启外部中断滤波代码中已经配置在接收头前加个深色滤光片软件上增加数据校验重复码处理长按按键时会发送重复码如果只处理引导码长按就失效了。我的代码中已经包含了重复码的判断你可以根据实际需求处理重复码比如长按加速。不同遥控器的差异虽然都是NEC协议但不同厂家的遥控器可能有细微差别。如果发现某个遥控器不工作可以先用逻辑分析仪抓一下波形看看时间参数是否需要调整。这个红外接收代码我在天空星HC32F4A0开发板上实测通过能稳定接收大部分NEC协议的遥控器信号。你可以直接拿去用也可以根据自己的需求修改。比如增加更多按键的判断或者把接收到的命令通过串口发送给其他设备。