STM32红外接收分析

title: STM32红外接收分析
date: 2020-06-18 00:45:12
tags:
categories: STM32学习记录


红外遥控原理分析

对于红外遥控,一般都不会陌生,我们身边就有很多采用红外遥控的设备,例如绝大多数的电视,空调,都使用的是红外遥控原理的遥控器,配上红外接收管接收遥控信号。

红外线(Infrared)是频率介于微波可见光之间的电磁波,波长在1mm到760纳米(nm)之间,频率比红光低的不可见光。

红外发射端一般使用红外LED,接收端使用红外接收二极管,利用光电效应,经过滤波和功率放大,形成能够承载信号的数字量。红外线发射管在LED封装行业中主要有三个常用的波段,如下850NM、875NM、940NM。850NM波长的主要用于红外线监控设备,875NM主要用于医疗设备,940NM波段的主要用于红外线控制设备。一般的红外遥控器使用的发射管是940nm波长,当然,我们中学做实验在气垫导轨上用到的光电门,也是这种红外光,这个波长的红外光人眼虽然看不见,但是相机的CMOS传感器是可以捕捉到的,具体验证方法,用手机开启相机,对着空调或者电视遥控器的发射管,按下遥控器,就可以看见屏幕里粉色的灯珠。

再来谈一谈红外遥控的方式是怎么传递信息的。大多数用于遥控的红外信号,采用NEC编码的方式。将用于驱动红外LED的电平信号,经过调制,形成某个高频信号的幅度、相位、频率等参量变化的过程,用一个信号去装载另一个信号。比如我们的红外遥控信号要发送的时候,先经过38K调制,如图所示:

红外遥控器通信原理_红外遥控器协议_红外通信对比无线通信的优势

经过这样的调制信号,我们就可以用它来装载我们需要的信号。比如对于NEC编码,规定发射信号有以下4种:

  1. 起始码:用于判断起始信号
  2. 逻辑0
  3. 逻辑1
  4. 重复码:如果遥控器有连按功能,那么发完一次完整的信号后,按键松开前都会循环发送重复码

我们由于红外传输的是一种数字信号,所以我们可以使用脉宽调制(JPWM)的方式,用不同的电平时间区分这4种信息。具体的不同就是这样:

  1. 起始码:9ms低电平 + 4.5ms高电平
  2. 逻辑0: 560us + 560us
  3. 逻辑1: 560us + 1680us
  4. 重复码 : 9ms低电平 + 2.5ms高电平

如果按照NEC编码的功能来区分,以正常的时序,就有以下几种码:

  1. 起始码
  2. 用户地址码(8bit)+用户地址反码(8bit)
  3. 数据码(8bit)+数据反码(8bit)
  4. 如果连按,之后会一直发送重复码

器件准备

  1. STM32核心板或开发板,有板载红外接收管最好
  2. 一个红外遥控器(可以用空调或者电视遥控器代替)
  3. 一个红外接收管,最好是带有放大电路的,但是不要使用带有解调芯片的。
  4. 如果可以,最好是有一个逻辑分析仪或者示波器,可以提前对未知的红外遥控设备进行直观的解码

准备的材料

程序编写

程序编写分为2个部分,分别是接收程序和发送程序,为了学习标准的NEC编码,我们使用标准遥控器,先通过编写接收程序,来体验红外编码的时序,有一个直观的理解之后,再尝试编写发送代码。

红外接收有3个引脚,5V,GND,以及信号引脚,先将接收模块连接逻辑分析仪,注意不要忘了共地,打开逻辑分析仪,再软件中设置上升沿和下降沿捕获,然后按下遥控器(可以是空调遥控器,我这里用专用红外遥控器的数字1键演示),然后逻辑分析仪就会捕获到一次信号,如下图:

逻辑电平图

这样,我们就得到了红外遥控器上按下数字1的红外信号,根据上面的原理分析,这个信号就很容易理解了:首先是一个起始码,有9ms低电平,4.5ms高电平,紧接着是8位地址码,这里是0(0000 0000),相应的8位地址反码就是255(1111 1111),紧接着是8位数据码,这里是162(1010 0010),相应8位数据反码93(0101 1101),然后经过一段时间高电平,接收到了重复码,有9ms低电平和2.5ms高电平,如果按住不放,之后就一直循环接收到重复码。

是不是一目了然?其实对我来说,这是迫不得已,因为我买的遥控器,资料非常有限,只给了一个电气参数的PDF,编码表也没有,例程更没有,无奈我只能动用逻辑分析仪,弄清楚了每个按键的编码值,顺便分析了一下电平时间和时序,算是做一个铺垫吧。

我要在小车上用红外接收,需要用到的按键,有下面一些:
在这里插入图片描述

接收程序思路分析

NEC编码解码的实质,其实是对电平时间的精确读取,我不禁想到了超声波模块。对了!一切都是那么的熟悉,定时器的捕获模式,刚好满足这种需求。

  1. 设置低电平捕获,一旦接收到信号,进入捕获中断,改为高电平捕获,捕捉到高电平时,读取计数值,再根据时基,就得到了低电平的时间,获取高电平时间的方法也是一样
  2. 然后根据时间的范围,设置一些标志位,来判断数据起始位,然后获取地址码,地址反码,数据码,数据反码
  3. 然后校验,将地址码与地址反码的取反进行比较,再与设定的地址码进行比较,如果都相等则表示地址数据无误,并且控制设备无误,继续下一步
  4. 将数据码与数据反码的取反进行比较,相等则表示数据无误,再赋值给需要接收键值的变量。

由于我是应用在小车上的,本来是设计成蓝牙遥控,这样就可以拓展出红外遥控了,C8T6一共有4个定时器的资源,TIM1用于PWM模式,驱动两个电机,TIM2和TIM4用于读取2个AB相编码器,TIM3用于超声波模块的输入捕获,那么要想红外遥控,只能废掉超声波避障了。

timer.c&timer.h

根据红外接收的工作特性,应该接收到一次低电平,程序进入中断,我们开始判断是否是起始位,因此,初始化程序应该配置为Input Capture输入捕获模式,捕获极性为低电平捕获

#include "timer.h"
#include "delay.h"


TIM_HandleTypeDef htim3;      //定时器3句柄

void Remote_Init(void)
{  
    TIM_IC_InitTypeDef TIM3_CH3_IC_Initure;	//定时器初始化句柄
    
    htim3.Instance=TIM3;                          //选择定时器:定时器3
    htim3.Init.Prescaler=(72-1);                  //预分频器:72分频,1M的计数频率,1um周期
    htim3.Init.CounterMode=TIM_COUNTERMODE_UP;    //计数方向:向上计数
    htim3.Init.Period=10000;                      //自动装载值:最大计时10ms
    htim3.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频:不分频
    HAL_TIM_IC_Init(&htim3);						//进行初始化
    
    //初始化TIM1输入捕获参数
    TIM3_CH3_IC_Initure.ICPolarity=TIM_ICPOLARITY_RISING;    //捕获极性:上升沿捕获
    TIM3_CH3_IC_Initure.ICSelection=TIM_ICSELECTION_DIRECTTI;//交错映射:直接映射到TI3上
    TIM3_CH3_IC_Initure.ICPrescaler=TIM_ICPSC_DIV1;          //输入捕获分频:不分频
    TIM3_CH3_IC_Initure.ICFilter=0x03;                       //硬件滤波:设置8个定时器时钟周期滤波
    HAL_TIM_IC_ConfigChannel(&htim3,&TIM3_CH3_IC_Initure,TIM_CHANNEL_3);//通道选择:配置TIM4通道3
    HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_3);   //开始捕获TIM3的通道3
    __HAL_TIM_ENABLE_IT(&htim3,TIM_IT_UPDATE);   //使能更新中断
}

//Input Capture 输入捕获MSP函数
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef GPIO_Initure;
    __HAL_RCC_TIM3_CLK_ENABLE();            //使能TIM3时钟
    __HAL_RCC_GPIOB_CLK_ENABLE();			//开启GPIOB时钟
	
    GPIO_Initure.Pin=GPIO_PIN_0;            //PB0
    GPIO_Initure.Mode=GPIO_MODE_AF_INPUT;  	//复用输入
    GPIO_Initure.Pull=GPIO_PULLUP;          //上拉
    GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速
    HAL_GPIO_Init(GPIOB,&GPIO_Initure);

    HAL_NVIC_SetPriority(TIM3_IRQn,1,3); 	//设置中断优先级,抢占优先级1,子优先级3
    HAL_NVIC_EnableIRQ(TIM3_IRQn);       	//开启ITM4中断
}

接下来,我们实际上有两种中断,一种是捕获成功进入捕获中断,另一种未捕获,达到计数重装值,进入更新溢出中断,方便起见,在中断服务函数中写HAL库的中断处理总函数(HAL库就是香),在函数内部会自动进入各自的回调函数:

void TIM3_IRQHandler(void)
{
	HAL_TIM_IRQHandler(&htim3);//HAL库定时器处理函数
}

由于在处理过程中需要有各种标志的判断,定义一个储存状态的8位变量会方便许多:

//[7]:收到了引导码标志
//[6]:得到了一个按键的所有信息
//[5]:保留	
//[4]:标记上升沿是否已经被捕获								   
//[3:0]:溢出计时器
u8 	RmtSta=0;	//状态标志变量

然后,我们继续分析,起始码中的9ms低电平其实没有必要判断,作为一个触发的信号就好,因为我们完全可以通过后面的4.5ms高电平,就得到一个起始码,然后逻辑电平0和1的区别,也在于高电平的时间长短,甚至重复码的特征也是2.5ms的高电平,那么代码的主要思路就很确定了:上升沿捕获中断中设置计时起点,在下降沿捕获中断期间判断计数值,得到高电平持续的时间,就可以在下降沿中断期间做出判断,是起始码还是重复码,是逻辑1还是逻辑0。

因此有必要设置一个存储计数值的变量,表示高电平时间,本质是读取CCR4寄存器,所以是16位:

u16 Dval;		//上升沿期间,计数器增加的值

再定义一个储存所有有效逻辑电平的变量,8+8+8+8一共32位:

u32 RmtRec=0;	//红外接收到的数据	 

如果需要记录连按,那么就需要一个计数变量,一般情况不是必须的:

u8  RmtCnt=0;	//按键按下的次数

接下来就是中断回调函数:

//定时器更新(溢出)中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM3)
	{
 		if(RmtSta&0x80)//检测第[7]位,如果收到引导码
		{
			RmtSta&=~0X10;						//清零第[4]位,清除上升沿捕获标志
			if((RmtSta&0X0F)==0X00)RmtSta|=1<<6;//标记已经完成一次按键的键值信息采集
			if((RmtSta&0X0F)<14)RmtSta++;
			else
			{
				RmtSta&=~(0x80);//清空起始标识
				RmtSta&=~(0x0F);//清空溢出计数器
			}						 	   	
		}	
	}
}

//定时器输入捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)//捕获中断发生时执行
{
	if(htim->Instance==TIM3)
	{
		if(DATA_PIN)//读取引脚状态,如果引脚高电平,说明是上升沿捕获
		{
			TIM_RESET_CAPTUREPOLARITY(&htim3,TIM_CHANNEL_3);   					//清除捕获极性
			TIM_SET_CAPTUREPOLARITY(&htim3,TIM_CHANNEL_3,TIM_ICPOLARITY_FALLING);//配置捕获极性,设置为下降沿捕获
			__HAL_TIM_SET_COUNTER(&htim3,0);		//清空定时器计数值	  
		  	RmtSta|=0X10;							//设置第[4]位,标记上升沿已经被捕获
		}else //下降沿捕获
		{
			Dval=HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_3);//读取输入捕获的计数值
			TIM_RESET_CAPTUREPOLARITY(&htim3,TIM_CHANNEL_3);   //清除捕获极性
			TIM_SET_CAPTUREPOLARITY(&htim3,TIM_CHANNEL_3,TIM_ICPOLARITY_RISING);//配置捕获极性,设置为上升沿捕获
			if(RmtSta&0X10)							//完成一次上升沿捕获 
			{
 				if(RmtSta&0X80)//接收到了引导码
				{
					
					if(Dval>300&&Dval<800)			//收到逻辑0,560us
					{
						RmtRec<<=1;
						RmtRec|=0;   
					}else if(Dval>1400&&Dval<1800)	//收到逻辑1,1680us
					{
						RmtRec<<=1;
						RmtRec|=1;
					}else if(Dval>2200&&Dval<2600)	//收到重复码,2.5ms
					{
						RmtCnt++; 					//按键次数增加1次
						RmtSta&=~(0x0F);			//清空溢出计数器		
					}
 				}else if(Dval>4200&&Dval<4700)		//收到起始码,4.5ms
				{
					RmtSta|=0x80;					//设置第[7]位,表示成功接收到了引导码
					RmtCnt=0;						//清除按键次数计数器
				}						 
			}
			RmtSta&=~(0x10);						//清除上升沿捕获标志
		}
	}
}

所有的操作我都打上了详细的注释,下面我按照正常接收一帧有效数据来过一遍这个时序,单片机如何读到数据,存入RmtRec变量中的:

  1. 首先起始码,9ms低电平,由于是下降沿,不会触发中断,然后4.5ms高电平,触发中断,判断为上升沿捕获,标记上升沿捕获,然后重新设置下降沿捕获,计数值清零,这时便开始记录高电平的持续时间。

    if(DATA_PIN)//读取引脚状态,如果引脚高电平,说明是上升沿捕获
    {
    	TIM_RESET_CAPTUREPOLARITY(&htim3,TIM_CHANNEL_3);   					//清除捕获极性
    	TIM_SET_CAPTUREPOLARITY(&htim3,TIM_CHANNEL_3,TIM_ICPOLARITY_FALLING);//配置捕获极性,设置为下降沿捕获
    	__HAL_TIM_SET_COUNTER(&htim3,0);		//清空定时器计数值	  
    	RmtSta|=0X10;							//设置第[4]位,标记上升沿已经被捕获
    }
    
  2. 一旦捕获到下降沿,便重新设置上升沿捕获,然后读取CCR4寄存器,存入Dval变量,由于设置的是72分频,计数值就是1um的数量,判断这个变量的所在区间,就可以得到收到的是什么。如果是起始码,就把相应的起始码标志位置1。如果判断标志位已经收到起始码,就判断逻辑码重复码。如果是逻辑码,就将Rec变量左移,如果是1,就进行与操作,如果是0,可以不操作,因为右移自动补0。如果是重复码,就把重复计数加1。

    if(RmtSta&0X10)							//完成一次上升沿捕获 
    {
     	if(RmtSta&0X80)//如果已经收到引导码
    	{
    		
    		if(Dval>300&&Dval<800)			//收到逻辑0,560us
    		{
    			RmtRec<<=1;
    			RmtRec|=0;   
    		}
    		else if(Dval>1400&&Dval<1800)	//收到逻辑1,1680us
    		{
    			RmtRec<<=1;
    			RmtRec|=1;
    		}
    		else if(Dval>2200&&Dval<2600)	//收到重复码,2.5ms
    		{
    			RmtCnt++; 					//按键次数增加1次
    			RmtSta&=~(0x0F);			//清空溢出计数器		
    		}
     	}
    	else if(Dval>4200&&Dval<4700)		//收到引导码,4.5ms
    	{
    		RmtSta|=0x80;					//设置第[7]位,表示成功接收到了引导码
    		RmtCnt=0;						//清除按键次数计数器
    	}						 
    }
    
  3. 如果超时,从逻辑分析仪的数据来看,只有一种可能,就是每次数据之间的上升沿时间会超过我设定的10ms自动重装时间,也就是说,会有下降沿中断无法达到,于是触发了溢出中断回调函数,这个中断就可以作为接收结束的标志,在里面进行标志位的复位操作即可。Sta变量的0-3位,作为计数位使用,可以计数0-15次,对于我的红外遥控器如果是连按的话,通常在15个重装时间150ms内会有重复码,如果15次还没有低电平出现,就视为松开了按键,需要清空起始标志和计数位,等待下一次按下按键。

    //定时器更新(溢出)中断回调函数
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
    	if(htim->Instance==TIM3)
    	{
     		if(RmtSta&0x80)//检测第[7]位,如果收到引导码
    		{
    			RmtSta&=~0X10;						//清零第[5]位,清除上升沿捕获标志
    			if((RmtSta&0X0F)==0X00)RmtSta|=1<<6;//标记已经完成一次按键的键值信息采集
    			if((RmtSta&0X0F)<14)RmtSta++;
    			else
    			{
    				RmtSta&=~(0x80);//清空起始标识
    				RmtSta&=~(0x0F);//清空溢出计数器
    			}						 	   	
    		}	
    	}
    }
    
  4. 接下来,一共32位数据就存入了Rec变量中,由于存入数据时采用左移的方式,那么右移32-8=24位,与0xFF得到地址码,右移32-16=16位,得到地址反码,右移8位,得到数据码,直接与0xFF就是数据反码。然后,验证遥控用户码是否等于宏定义的地址码,并且取出从第24位开始的8位数据码,return返回,判断这个值,做相应的控制操作,就完成了。

    u8 Remote_Scan(void)
    {        
    	u8 sta=0;       
    	u8 temp1,temp2;
    	if(RmtSta&(1<<6))//得到一个按键的所有信息了
    	{ 
    	    temp1=RmtRec>>24;			//得到地址码
    	    temp2=(RmtRec>>16)&0xff;	//得到地址反码 
     	    if((temp1==(u8)~temp2)&&temp1==REMOTE_ID)//检验遥控地址码,遥控器发送的地址码要和这里宏定义的相同才匹配 
    	    { 
    	        temp1=RmtRec>>8;
    	        temp2=RmtRec; 	
    	        if(temp1==(u8)~temp2)sta=temp1;//键值正确	 
    		}   
    		if((sta==0)||((RmtSta&0X80)==0))//按键数据错误/遥控已经没有按下了
    		{
    		 	RmtSta&=~(1<<6);//清除接收到有效按键标识
    			RmtCnt=0;		//清除按键次数计数器
    		}
    	}
        return sta;
    }
    
    void read_yaokong(void)
    {
    	u8 key;
    	key = Remote_Scan();
    	switch (key)
    	{
    		case 24: Flag_Qian=1,Flag_Hou=0,Flag_Left=0,Flag_Right=0; break;	//前进
    		case 74: Flag_Qian=0,Flag_Hou=1,Flag_Left=0,Flag_Right=0; break;	//后退
    		case 16: Flag_Qian=0,Flag_Hou=0,Flag_Left=1,Flag_Right=0; break;	//左转
    		case 90: Flag_Qian=1,Flag_Hou=0,Flag_Left=0,Flag_Right=1; break;	//右转
    		case 56: Flag_Qian=0,Flag_Hou=0,Flag_Left=0,Flag_Right=0; break;	//停止
    		case 162: mode_flag = 0; break;	//遥控模式
    		case 98: mode_flag = 1; break;	//CCD循迹模式
    		case 226: mode_flag = 2; break;	//超声波避障模式
    	}
    }
    

然后补上头文件即可完成全部的代码。

#ifndef __TIMER_H
#define __TIMER_H
#include "sys.h"


#define DATA_PIN   PBin(0)		//红外数据输入脚

#define REMOTE_ID 0      		   

extern u8 RmtCnt;	        //按键按下的次数

void Remote_Init(void);     //红外传感器接收头引脚初始化
u8 Remote_Scan(void);
void read_yaokong(void);
#endif

显然从逻辑分析仪看,我的红外遥控器发送的地址码是0x00,所以直接宏定义为0即可。

猜你喜欢

转载自blog.csdn.net/Carbon6/article/details/106880942