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种:
我们由于红外传输的是一种数字信号,所以我们可以使用脉宽调制(JPWM)的方式,用不同的电平时间区分这4种信息。具体的不同就是这样:
如果按照NEC编码的功能来区分,以正常的时序,就有以下几种码:
程序编写分为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编码解码的实质,其实是对电平时间的精确读取,我不禁想到了超声波模块。对了!一切都是那么的熟悉,定时器的捕获模式,刚好满足这种需求。
由于我是应用在小车上的,本来是设计成蓝牙遥控,这样就可以拓展出红外遥控了,C8T6一共有4个定时器的资源,TIM1用于PWM模式,驱动两个电机,TIM2和TIM4用于读取2个AB相编码器,TIM3用于超声波模块的输入捕获,那么要想红外遥控,只能废掉超声波避障了。
根据红外接收的工作特性,应该接收到一次低电平,程序进入中断,我们开始判断是否是起始位,因此,初始化程序应该配置为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变量中的:
首先起始码,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]位,标记上升沿已经被捕获
}
一旦捕获到下降沿,便重新设置上升沿捕获,然后读取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; //清除按键次数计数器
}
}
如果超时,从逻辑分析仪的数据来看,只有一种可能,就是每次数据之间的上升沿时间会超过我设定的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);//清空溢出计数器
}
}
}
}
接下来,一共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即可。