STM32F4_红外遥控

目录

1. 红外遥控简介

2. NEC协议

3. 硬件设计

4. 实验程序详解

4.1 main.c

4.2 Remote.c

4.3 Remote.h


1. 红外遥控简介

        红外遥控是一种无线、非接触的控制技术。具有抗干扰能力强,信息传输可靠,功耗低,成本低,易实现等优点。被广泛的应用于家用电器,越来越多的被使用到计算机系统中。

        红外遥控不像无限电遥控那样,可以穿过障碍物去控制被控的对象。所以,在设计红外遥控时,不需要每套(发射器和接收器)有不同的遥控频率或编码(防止遥控隔墙控制邻居的家用电器),因此,同类的红外遥控设备,可以使用相同的遥控频率和编码,不会出现遥控信号 “串门” 的情况。并且红外光为不可见光,对环境影响很小。红外光的波长远小于无线电波的波长,所以红外遥控不会影响家中其他电器。

红外遥控的编码目前广泛使用的是NEC Protocol 的 PWM 脉冲宽度调制和 philips RC-5 Protocol 的 PPM 脉冲位置调制

STM32F4的开发板使用的是 NEC 协议

2. NEC协议

基本发送和接收:

        空闲状态红外LED不亮,接收头输出高电平

        发送低电平红外LED以 38 KHz频率闪烁发光,接收头输出低电平

        发送高电平红外LED不亮,接收头输出高电平

:38KHz是最底层的频率,在NEC协议编码过程中不会体现;

NEC协议产生的波形(也可以说是我们遥控器按下时,发出的波形),如下图

        在遥控器没有按下时,处于空闲状态默认为高电平

        下图中红色的波形,表示Start起始信号Start信号由9ms的下降沿和4.5ms的上升沿组成。

        下图中蓝色的波形,表示遥控按键发送的数据DataData信号由560us的下降沿和560us的上升沿或者560us的下降沿和1690us的上升沿组成。其中560us的下降沿和560us的上升沿表示逻辑0560us的下降沿和1690us的上升沿表示逻辑1

        Data一般是32位的

        Data的格式是地址码Address+地址反码+Command命令+命令反码。其中每部分都是8位,总共32位。

        其中地址码Address是遥控器的标识符(防止不同品牌的遥控器互相用);反码是用来验证的,判断有没有发错。命令码就是界码,表示遥控器按下哪个按键。

        下图中绿色的波形表示按下不放时(类似于电视遥控换台,换台键按住不放,则电视频道一直++)。Repeat信号由9ms下降沿和2.25ms上升沿组成。

STM32F4_红外遥控_第1张图片

NEC协议的特点:

        1. 8位地址和8位指令长度

        2. 地址和命令2次传输(确保可靠性)

        3. PWM脉冲宽度调制,以发射红外载波的占空比代表 “0” 和 “1” 

        4. 载波频率为38KHz

        5. 位时间为1.125ms或2.25ms

遥控接收头在收到脉冲的时候为低电平,在没有脉冲的时候为高电平。

前面已经提及了NEC的数据格式,这里总结一下:

        起始码、地址码、地址反码、控制码、控制反码。起始码由一个 9ms 的低电平和一个 4.5ms 的高电平组成,地址码、地址反码、控制码、控制反码均是 8 位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可用于校验)。

NEC规定的连发码

        上图中绿色部分Repeat由9ms低电平+2.5ms高电平+0.56ms低电平+97.94高电平(这里的0.56ms低电平+97.94高电平对应于上图总的连发码110ms)组成。如果一帧数据发送完毕以后,按键仍然没有放开,则发送重复码,也就是连发码。

我们可以通过统计连发码的次数来标记按键按下的长短/次数。

3. 硬件设计

STM32F4_红外遥控_第2张图片

红外接收头连接在MCU引脚的PA8上。

注意REMOTE_IN 和 DCMI_XCLK共用了PA8,所以他们不可以同时使用。

4. 实验程序详解

实验现象

        本实验采用定时器的输入捕获功能实现红外解码。

        首先开机在LCD上显示一些信息之后,等待红外触发,如果接收正确的红外信号,解码。并且在LCD上显示键值和所代表的意义,以及按键的次数(主要是按键不放导致Repeat)

4.1 main.c

#include "stm32f4xx.h"                 
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "MyI2C.h"
#include "AT24C02.h"
#include "Remote.h"

//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
	LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
	led_set(sta);
}
int main(void)
{
	u8 key;
	u8 t=0;
	u8 *str=0;
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
	delay_init(168);
	uart_init(115200);
	
	LED_Init();
	LCD_Init();
	Remote_Init();
	
	POINT_COLOR=RED;
	LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
	LCD_ShowString(30,70,200,16,16,"REMOTE Test");
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2023/06/21");
	LCD_ShowString(30,130,200,16,16,"KEYVAL:");
	LCD_ShowString(30,150,200,16,16,"KEYCNT:");
	LCD_ShowString(30,170,200,16,16,"SYMBOL:");
	
	while(1)
	{
		key=Remote_Scan(); //获得按键返回值,Remote_Scan()错误的情况下返回0,其余情况返回键值
		if(key)
		{
			LCD_ShowNum(30+7*8,130,key,3,16);  			//显示键值
			LCD_ShowNum(30+7*8,150,RemoteCNT,3,16);     //显示按键次数
            //RemoteCNT记录按键按下的次数,如果按键一直按下,那么发完一个命令以后,DownTimerCount>2200&&DownTimerCount<2600
            //在绿色区域内重复,所以对应的 RemoteCNT++;
			switch(key)
			{
				case 0:str="ERROR";break;
				case 162:str="POWER";break;	    
				case 98:str="UP";break;	    
				case 2:str="PLAY";break;		 
				case 226:str="ALIENTEK";break;		  
				case 194:str="RIGHT";break;	   
				case 34:str="LEFT";break;		  
				case 224:str="VOL-";break;		  
				case 168:str="DOWN";break;		   
				case 144:str="VOL+";break;		    
				case 104:str="1";break;		  
				case 152:str="2";break;	   
				case 176:str="3";break;	    
				case 48:str="4";break;		    
				case 24:str="5";break;		    
				case 122:str="6";break;		  
				case 16:str="7";break;			   					
				case 56:str="8";break;	 
				case 90:str="9";break;
				case 66:str="0";break;
				case 82:str="DELETE";break;
			}
			LCD_Fill(86,170,116+8*8,170+16,WHITE);  //清除范围内的数据,x 86~116+8*8  ; y 170~170+16
			LCD_ShowString(30+7*8,170,200,16,16,str);
		}
		else
		{
			delay_ms(10);
		}
		t++;
		if(t==20)
		{
			t=0;
			LED0=!LED0;
		}
	}
}



4.2 Remote.c

#include "stm32f4xx.h"              
#include "Remote.h"
#include "usart.h"

//红外遥控初始化
//设置IO引脚及TIM1_CH1的输入捕获 
//红外接收头引脚接到PA8,PA8引脚复用TIM1_CH1,定时器1通道1
void Remote_Init(void)
{
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);  //使能TIM1时钟
	
	//引脚初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF; //引脚复用为定时器1
	GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8;  //PA8
	GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource8,GPIO_AF_TIM1);  //PA8复用为TIM1
	
	//定时器1初始化
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;  //向上计数模式
	TIM_TimeBaseInitStructure.TIM_Period=10000;   //设定计数器自动重装载值,10ms溢出,计数器从0开始计数,1ms计数1000,那么10ms计数值到10000,也就是自动重装载值
	TIM_TimeBaseInitStructure.TIM_Prescaler=167;  //预分频值,167+1/168=1M的计数频率,1us+1
	TIM_TimeBaseInit(TIM1,&TIM_TimeBaseInitStructure);
	
	//初始化定时器输入捕获
	TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICInitStructure.TIM_Channel=TIM_Channel_1; //定时器1
	TIM_ICInitStructure.TIM_ICFilter=0x03;  //是否滤波
	TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising; //上升沿捕获
	TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1; //配置输入不分频
	TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;  //映射到TI1上
	TIM_ICInit(TIM1,&TIM_ICInitStructure);
	
	TIM_ITConfig(TIM1,TIM_IT_Update|TIM_IT_CC1,ENABLE);  //使能更新和捕获
	TIM_Cmd(TIM1,ENABLE);  //开启定时器1中断
	
	NVIC_InitTypeDef NVIC_InitStructure;  //TIM1是高级定时器,有两个中断服务函数
	NVIC_InitStructure.NVIC_IRQChannel=TIM1_UP_TIM10_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=2;   //子优先级2
	NVIC_Init(&NVIC_InitStructure);
	
	NVIC_InitStructure.NVIC_IRQChannel=TIM1_CC_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=3;
	NVIC_Init(&NVIC_InitStructure);
	//开启定时器1输入捕获中断和更新中断。捕获到上升沿,产生捕获中断;当定时器计数溢出时,产生更新中断;
}

//在写利用定时器TIM1的通道1输入捕获遥控器的信号时,首先需要了解NEC协议到底是怎么传输的,NEC协议和IIC、SPI是完全不同的
//这里我说的不同是针对于最显著的特点不同:IIC、SPI时序均是通过时序的上升沿、下降沿来发送数据的,只需要特定时间设置上升沿、下降沿就可以写时序图对应的程序,更精确地说高低电平就可以表示逻辑上的1和0
//NEC协议却不是这样,NEC协议的逻辑1和0是通过一段时间的下降沿 + 一段时间的上升沿来描述的,不单单是如此,NEC协议的起始信号和数据信号以及重复信号Repeat都是通过一段时间的上升沿 + 一段时间的下降沿来描述的
//所以首先需要明确如何判断这段信号表示逻辑0还是逻辑1;也就是如何判断上升沿和下降分别持续的时间,进而判断到底代表逻辑1还是逻辑0
//*******************************************************************************
//定时器的输入捕获功能是可以得到时序高低电平的持续时间的,这个我们在之前已经学习过
//这里就是通过定时器的输入捕获功能获取高低电平的持续时间,通过电平持续的时间来判断到底是逻辑1还是逻辑0;NEC协议最根本的思路就是如此
//*******************************************************************************
//通过观察NEC协议的编码说明图可以发现:逻辑1和逻辑0的低电平持续时间都是560us;而高电平的持续时间是不同的,因此可以输入捕获计算高电平的持续时间,进而判断遥控发出的信号数据代表逻辑1还是逻辑0


//遥控器接收的状态
//[7]:收到了引导码标志,引导码其实就是NEC协议上的起始码
//[6]:得到了一个按键的所有信息
//[5]:保留
//[4]:标记上升沿是否已经被捕获								   
//[3:0]:溢出计时器
u8 RemoteReceiveState=0; //定义的变量RemoteReceiveState实际上是一个8位寄存器,其各个位如上所示,该变量代表遥控器接收的状态
u16 DownTimerCount;  //下降沿时计数器的值;     因为要计算高电平的持续时间,所以下降沿一定是在上升沿之后(当然,是在提前已经捕获,就意味着上一个一定是上升沿)
//上升沿计数器开始计时,下降沿得到计数器的值,就意味着得到了高电平的持续时间
u32 RemoteReceiveData=0;  //红外接收到的数据
u8 RemoteCNT=0;     //按键按下的次数

//定时器TIM1溢出中断
void TIM1_UP_TIM10_IRQHandler(void)
{
	//首先,进入该中断还是先判断中断状态位,然后清除中断状态位,为下一次判断状态位做准备
	//接着,明确溢出中断的思路:
	//之所以进入溢出中断,是因为初始化中断时,设置的自动重装载值是10000,设置的预分频值是167,可以计算出时钟频率为1M,那么溢出的周期就是10ms
	//进入中断就意味着计数值++,来到了10000,也可以说整个时序持续了10us,进入了溢出中断
	//这里首先需要知道:定时器溢出的周期是10ms,输入捕获设置的是上升沿捕获,一旦捕获上升沿就会进入输入捕获中断服务函数,判断是上升沿就会清空计数器
	//清空计数器之后,计数器自然不会++,来到10000。
	//说这么多想表达的意思就是:只要进入溢出中断,那么就意味着10ms内都没有输入捕获来清空计数器的值,也就是输入信号没有发生变化,说明10ms没有接收红外信号了,因此可以判断为接收结束
	if(TIM_GetITStatus(TIM1,TIM_IT_Update)==SET) //判断溢出中断标志位
	{
		if(RemoteReceiveState&0X80)  //表示收到了引导码,也就是收到了起始码
        //这里需要明白,起始码之后的时序(不算上连发码)加在一起也没有10ms,是不会进入溢出中断的
		//一旦进入了溢出中断,表示一个时序一定是进入了连发阶段,数据时序+连发阶段时间是远远大于10ms的
		//也就表示已经有数据被接收到了,现在正在执行该数据的连发
		{
			RemoteReceiveState&=~0X10; //0001 0000根据优先级先取反,1110 1111按位与表示将RemoteReceiveState的第4位置0
			//取消上升沿已经被捕获的标志,为下一次捕获上升沿做准备
			if((RemoteReceiveState&0X0F)==0X00) //低四位全为0,表示刚刚溢出,也就是计数周期刚好到10ms的临界值,这个时候一个按键的所有信息肯定已经被接收完了
			{
				RemoteReceiveState|=1<<6; //标记已经得到了一个按键的所有信息
			}
			if((RemoteReceiveState&0X0F)<14) //取出溢出计数器的值,离开循环的条件是寄存器低4位为13
			{
				RemoteReceiveState++;
			}
			else //寄存器低4位值大于13,发一个数据近似于10ms(一定是不到10ms的),连发13次数据一定是超过了连发码的时序周期,所以清空相关参数,为下次做准备
			{
				//收到的连发码已经结束,清空相关参数
				RemoteReceiveState&=~(1<<7);  //清空引导标识
				RemoteReceiveState&=0XF0;    //清空计数器
			}
		}
	}
	TIM_ClearITPendingBit(TIM1,TIM_IT_Update); //清除中断标志位,为下次中断判断做准备
}
//定时器TIM1输入捕获中断服务函数
void TIM1_CC_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM1,TIM_IT_CC1)==SET)    //获取中断输入捕获的状态位
	{
		//进入中断服务函数的思路是:获得高电平的时间,进行比较,究竟是代表逻辑1还是代表逻辑0
		//获得的思路是:初始化设置上升沿捕获,上升沿捕获进入中断服务函数,也就是进入该函数,立刻设置下降沿捕获,一方面捕获本次高电平,另一方面为下次捕获下降沿做准备
		//之所以准备捕获下降沿是因为:上升沿设置计时,下降沿的时候得到计数器的值,也就意味着得到了整个高电平的持续时间,期间的状态位设置均通过RemoteReceiveState设置相关位即可
		if(Remode_DATA)  //红外输入引脚为高电平,上升沿捕获
		{
			TIM_OC1PolarityConfig(TIM1,TIM_ICPolarity_Falling); //调用配置TIM1通道1极性函数,在收到上升沿捕获的同时,立刻设置下降沿捕获,为下次捕获下降沿做准备,同时也是为了捕获本次的高电平
			TIM_SetCounter(TIM1,0);  //定时器清0,表示计数器开始计数
			RemoteReceiveState|=0x10;//设置RemoteReceiveState的第4位为1,表示上升沿已经被捕获 
		}
		else //否则红外输入引脚为低电平,下升沿捕获
		{
			DownTimerCount=TIM_GetCapture1(TIM1); //获得TIM1计数器的值给到变量,表示下降沿时计数器的值,也就意味着得到了高电平的时间
			TIM_OC1PolarityConfig(TIM1,TIM_ICPolarity_Rising); //设置上升沿捕获,为下次捕获上升沿做准备
			//接下来就需要对得到的计数器值进行逻辑判断
			if(RemoteReceiveState&0x10) //得到计数器的第四位,也就是高电平是否已经被捕获,这里是进行判断的逻辑,就是捕获下降沿得到计数器值的同时,判断上一个时序是不是上升沿
				//如果上一个时序不是上升沿,那么两个下降沿之间的计数器值也就不能代表高电平的持续时间
			//如果上一个时序是上升沿,那么得到的计数器值DownTimerCount才是有效的
			{
				if(RemoteReceiveState&0x80)//得到最高位,表示收到了引导码,已经完成了NEC的Start步骤
				{
					//接下来根据高电平的时间判断究竟是逻辑1还是逻辑0
					//该判断语句需要结合NEC协议的时序图上的上升沿和下降沿的时间
					if(DownTimerCount>300&&DownTimerCount<800) //逻辑0的高电平持续时间为560us,表示逻辑0
					{
						//该程序左移的操作建立在C语言补码的形式之上
						RemoteReceiveData=RemoteReceiveData<<1;//NEC协议发送数据是低位在前,高位在后;
						//左移一位,右侧低位补0
						RemoteReceiveData=RemoteReceiveData|0;//将数据的右侧最低位置0
                        //上述两句程序的意思是倘若检测出为低电平0,则将RemoteReceiveData左移一位,将最低位置0
                        //因为是低位在前,所以如此循环,就能保证第一次检测到的一直左移到最高位;
					}
					else if(DownTimerCount>1400&&DownTimerCount<1800) //逻辑1的高电平持续时间为1690us,表示逻辑1
					{
						RemoteReceiveData=RemoteReceiveData<<1;
						RemoteReceiveData=RemoteReceiveData|1; //同理还是左移一位,右侧补0,补0的那一位置1
					}
					else if(DownTimerCount>2200&&DownTimerCount<2600) //绿色重复的高电平时间2.25ms=2250us
					{
						RemoteCNT++; //进入重复区间,进来一次,按键++
						RemoteReceiveState&=0xF0;  //清空计数器,因为进入该Repeat区间也就象征着NEC协议结束了,清空计数器,为下位数据传输做准备
					}
				}
				else if(DownTimerCount>4200&&DownTimerCount<4700) //表示来到起始信号的高电平持续时间 4.5ms=4500us
					{
						RemoteReceiveState|=1<<7;//进入NEC协议的起始阶段,意味着收到了引导码,所以需要将状态位设置为1
						//理解这个代码首先需要知道C语言中操作符的优先级
						//在C语言中,取反操作符>左移/右移>与>或  
						//代码中先计算1<<7位,再进行或操作运算
						RemoteCNT=0; //起始阶段一定没有按键按下,将上一次传输时的按键值清0
					}
			}
			RemoteReceiveState&=~(1<<4); //该代码的意义是:设置状态位4=0;
			//因为上一个高电平已经被处理了,紧接着设置高电平还没有被捕获,为下一次捕获的状态位判断做准备
		}
	}
	TIM_ClearITPendingBit(TIM1,TIM_IT_CC1);  //清除中断捕获状态标志位,为下一次判断中断标志位做准备
}
//处理红外键盘
//返回值:
//	 0,没有任何按键按下
//其他,按下的按键键值.
u8 Remote_Scan(void)
{
	u8 sta=0;  //记录键值
	u8 t1,t2;
	if(RemoteReceiveState&(1<<6)) //判断状态位,如果已经得到一个按键的所有信息了
	{
		//数据格式是:地址码+地址反码+命令码+命令反码
		//RemoteReceiveData是红外接收到的数据,32位
		t1=RemoteReceiveData>>24;   //得到地址码 高8位
		t2=(RemoteReceiveData>>16)&0xFF; //得到地址反码 17~24位,右移16位得到高16位,和0xFF按位与,表示取出低8位
		if((t1==(u8)~t2)&&t1==REMOTE_ID) //校验遥控识别码及地址
		{
			//地址码校验没问题后,取出命令码和命令反码,进行校验;
			//其中命令码就是键值对应的信息
			t1=RemoteReceiveData>>8; //t1定义的是8位,RemoteReceiveData>>8表示的是第9位~32位,t1是8位的变量,所以赋值的时候只能得到低8位,也就是第9~16位,也就得到了命令码
			t2=RemoteReceiveData;  //直接赋值,得到的就是低8位,命令反码
			if(t1==(u8)~t2)
			{
				sta=t1; //如果校验没问题,就将命令码t1赋值给sta,以便于返回该值,得到键值
			}
		}
		if((sta==0)||((RemoteReceiveState&0X80)==0)) //  按键数据错误/遥控已经没有按下了
		{
			RemoteReceiveState&=~(1<<6);  //清除接收到有效按键标识
			RemoteCNT=0;				  //清除按键次数计数器
		}
	}
	return sta;
}


4.3 Remote.h

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

#define Remode_DATA PAin(8)	 //红外数据输入脚

#define REMOTE_ID 0           //红外遥控识别码(ID),每款遥控器的该值基本都不一样,但也有一样的.
							  //我们选用的遥控器识别码为0
extern u8 RemoteCNT;  //按键按下的次数

void Remote_Init(void);
void TIM1_UP_TIM10_IRQHandler(void);
void TIM1_CC_IRQHandler(void);
u8 Remote_Scan(void);

#endif

该实验代码的每一步都进行了详细的解释。如果对代码还有疑问或者哪里解释的不对,欢迎评论改正!!!

你可能感兴趣的:(STM32,stm32,单片机,嵌入式硬件)