目录
一 背景说明
二 原理分析
三 软件实现
四 补充说明
项目中需要使用红外进行简单控制,选用比较通用的红外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】:在调试时,不要在中断中加入过多无关语句,例如打印语句,这会导致结果出错。