【嵌入式】GPIO外部中断+定时器 实现红外NEC协议解码

2023-12-22 17:40:21

目录

一 背景说明

二 原理分析

三 软件实现

四 补充说明


一 背景说明

? ? ? ? 项目中需要使用红外进行简单控制,选用比较通用的红外NEC协议实现。

? ? ? ? 红外(Infrared,IR)遥控是一种无线、非接触控制技术,常用于遥控器、无线键盘、鼠标等设备之间的通信。IR协议的工作原理是,发送方通过红外线发送一个特定的编码,接收方通过识别该编码来执行相应的操作。
????????IR协议是指红外线通信协议的总称,而NEC协议是IR协议中的一种具体实现。红外遥控系统分为发射和接收两部分,发射部分的发射元件为红外发光二极管,它发出的是红外线而不是可见光;接收电路的红外接收管是一种光敏二极管。

? ? ? ? 【1】发射部分就是红外遥控器,淘宝上几块钱一个,支持NEC协议即可,也可以自己定制协议。

? ? ? ? 【2】接收部分是红外接收头,我这边选用的是亿光的 IRM-H638T/TR2

二 原理分析

? ? ? ? 要实现红外NEC协议的解码,先了解一下协议内容。

????????【1】电平形式:

????????NEC协议采用PPM(Pulse Position Modulation,脉冲位置调制)的形式进行编码,数据的每一位(Bit)脉冲长度为560us,由38KHz的载波脉冲 (carrier burst) 进行调制,推荐的载波占空比为 1/3至 1/4。有载波脉冲的地方,其宽度都为 560us,而载波脉冲的间隔时间是不同的。

?????????逻辑“1”的载波脉冲+载波脉冲间隔时间为2.25ms;逻辑“0”的载波脉冲+载波脉冲间隔时间为逻辑“1”的一半,即1.125ms

? ? ? ? 【2】协议内容:

????????每次信息都是按照下面的格式进行传输,因此,单次信息传输的时间是固定不变的:

????????引导码 (9ms载波脉冲+4.5ms 空闲信号) + 地址码 + 地址反码 + 控制码 + 控制反码

? ? ? ? 【3】示波器分析:

? ? ? ? 接示波器或者逻辑分析仪可以看到,点击红外遥控器的按键,接收端能够收到一组符合逻辑的码,如下所示,就是收到一组 0x00FFE21D 的NEC码:

三 软件实现

? ? ? ? 接收解码的思路是:GPIO外部接收中断收到一个下降沿,则定时器开始计时,并将计时的值放入数组中。如果计时时长在设计范围内,则说明接收到了引导码,后面依次将两次下降沿的时间计入数组。这样每一组NEC码,均可以由一个长度为33的数组来表示,其中,数组第一个值就是引导码时长,后面每8个值可以组成一组码

? ? ? ? 根据上述思路,需要用到 GPIO外部接收中断 + 定时器 (P.S. 正好之前用过这个组合来模拟串口,这边有需要的可以借鉴 【嵌入式】NXP/LPC使用GPIO+定时器模拟UART串口接收_gpio模拟uart-CSDN博客。实现如下:

【1】GPIO以及外部中断初始化(接口:RED_Init):

//红外引脚定义
#define RED_PORT        (GPIOB)
#define RED_PIN         (GPIO_Pin_11)
#define RED_PORT_SRC    (GPIO_PortSourceGPIOB)
#define RED_PIN_SRC     (GPIO_PinSource11)

/**************************************************************************
* 函数名称: RED_Init
* 功能描述: 红外接收初始化
**************************************************************************/
void RED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
	EXTI_InitTypeDef EXTI_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); 

	GPIO_InitStructure.GPIO_Pin  = RED_PIN;         // 红外接收
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   // 上拉输入模式
	GPIO_Init(RED_PORT,&GPIO_InitStructure);
    SYSCFG_EXTILineConfig(RED_PORT_SRC, RED_PIN_SRC);  // 选择GPIO管脚用作外部中断线路
    EXTI_ClearITPendingBit(EXTI_Line11);
    
	// 配置外部中断
	EXTI_InitStructure.EXTI_Line = EXTI_Line11;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStructure); 

	// 配置NVIC结构体
	NVIC_InitStructure.NVIC_IRQChannel = EXTI4_15_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPriority = 2;   // 抢占优先级为0
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;   // 使能
	NVIC_Init(&NVIC_InitStructure);
}

【2】定时器初始化(接口:Timer2_Config)?& 100us定时器中断(接口:TIM2_IRQHandler):

//Timer2 100us定时器
#define TIMER2_ARR  (100-1)
#define TIMER2_PSC  (96-1)

/**************************************************************************
* 函数名称: Timer2_Config/TIM2_IRQHandler
* 功能描述: 定时器2配置/定时器2中断
* 返 回 值: 100us
* 其它说明: 用于红外读取。定时时间公式:Tout = ((arr+1) * (psc+1)) / Tclk
**************************************************************************/
void Timer2_Config(void)
{
    TIM_TimeBaseInitTypeDef TIM_StructInit;
    NVIC_InitTypeDef NVIC_InitStructure;
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能定时器时钟

    //定时器基础配置
    TIM_StructInit.TIM_Period = TIMER2_ARR;             //自动重装值
    TIM_StructInit.TIM_Prescaler = TIMER2_PSC;          //预分频系数
    TIM_StructInit.TIM_ClockDivision = TIM_CKD_DIV1;    //时钟分频
    TIM_StructInit.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
    TIM_StructInit.TIM_RepetitionCounter = 0;           //不重复计数
    TIM_TimeBaseInit(TIM2, &TIM_StructInit);
    
    //NVIC中断配置
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPriority = 2;     //数字越小优先级越高
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    TIM_ClearFlag(TIM2, TIM_FLAG_Update);
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);          //使能更新中断
    TIM_Cmd(TIM2, ENABLE);
}

u16 timer2_cnt;

void TIM2_IRQHandler(void)
{
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    timer2_cnt++;
}

【3】GPIO接收中断(接口:EXTI4_15_IRQHandler):

u8 red_read_buf[4];     //形式:00-FF-控制码-控制反码

u8 red_time_buf[33];    //用于记录两个下降沿之间的时间
u8 red_id;              //红外接收数组下标
bool red_start_flag;    //红外开始接收标志位
bool red_done_flag;     //红外接收完成标志位

extern u16 timer2_cnt;

void EXTI4_15_IRQHandler(void)
{
    if(red_start_flag)
    {
        if((timer2_cnt < 150) && (timer2_cnt >= 50))   //接收到引导码
            red_id = 0;

        red_time_buf[red_id] = timer2_cnt;  //获取计数时间
        timer2_cnt = 0;
        red_id ++;

        if(33 == red_id)    //接收完成
        {
            red_id = 0;
            timer2_cnt = 0;
            red_start_flag = FALSE;
            red_done_flag = TRUE;
        }
    }
    else    // 下降沿第一次触发
    {
        red_id = 0;
        timer2_cnt = 0;
        red_start_flag = TRUE;
    }

    EXTI_ClearITPendingBit(EXTI_Line11);  // 清除中断标志
}

【4】至此,可以得到一个长度为33的数组,该数组中的值即为两次下降沿之间的时间(以100us为单位)。接下来就是分析该数组,得到红外NEC码(接口:redBufConv):

/**************************************************************************
* 函数名称: redBufConv
* 功能描述: 将时间数组转化为红外接收数组
**************************************************************************/
void redBufConv(void)
{
    u8 i, j;
    u8 id = 1;
    u8 t_value;

    if(TRUE == red_done_flag)       //若一次接收完成,执行一次转化
    {
        for(i = 0;i < 4;i ++)
        {
            for(j = 0;j < 8;j ++)
            {
                if((red_time_buf[id] >= 8) && (red_time_buf[id] < 15))          //表示0
                    t_value = 0;
                else if((red_time_buf[id] >= 18) && (red_time_buf[id] < 25))    //表示1
                    t_value = 1;
                red_read_buf[i] <<= 1;
                red_read_buf[i] |= t_value;
                id ++;
            }
        }
        red_done_flag = FALSE;
    }
}

【5】在主循环中调用该转换接口,就可以得到数组 red_read_buf ,该数组的四个元素分别对应NEC红外接收码的 地址码/地址反码/控制码/控制反码。当然最后还要根据获取到的码,转化成不同的命令,结合软件逻辑进行处理,这边不再赘述。

四 补充说明

????????【1】:上面的实现只是实现红外接收的其中一种方法,网上还有一种比较常见的方法是利用下降沿触发,在中断中进行延迟,判断高电平持续时间以此来判断信号类别。这种方法的优点是逻辑比较清晰,可读性强。但是我在一开始的应用中,由于延时时间不准等原因,没有收到正确的NEC码。个人感觉这不是一种很好的方法,因为在中断中进行延时会导致主函数得不到及时的处理。

????????【2】:在调试时,不要在中断中加入过多无关语句,例如打印语句,这会导致结果出错。

文章来源:https://blog.csdn.net/sinat_33408502/article/details/135152619
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。