红外遥控学习,万能遥控解决方案

1. 原理

目前电视机、空调等家电大部分还是采用的红外遥控的,有时项目需要把遥控嵌入到自己的设备中,或者又是物联网需要控制家电,此时就需要智能学习和发送了,红外遥控电路图如下:

在这里插入图片描述
左侧为红外发送电路,右侧有红外接收电路。

● 发送端:

以NEC协议为例(实际测试中遵循NEC协议的不多),信息传输是基于38K载波,也就是说红外线是以载波的方式传递。

发送协议数据“0” = 发送载波560us + 不发送载波560us

发送协议数据“1” = 发送载波560us+ 不发送载波1680us

发送的波形如下图所示,下图中为 0 0 0 0 1 0 1
在这里插入图片描述
● 接收端:

在红外接收端,如果接收到红外38K载波,则IR输出为低电平,如果不是载波包括固定低电平和固定高电平则输出高电平。在IR端接收的信号如下所示:
在这里插入图片描述
NEC协议规定:

发送协议数据“0” = 发送载波560us + 不发送载波560us

发送协议数据“1” = 发送载波560us+ 不发送载波1680us

发送引导码 = 发送载波9000us + 不发送载波4500us

● 总结:

1、接收端收到38K载波脉冲为低电平,没有则为高电平;所以在设计发送的时候,发送低电平应该开启38KHZ的PWM,发送高电平则关闭PWM(默认为高电平)

2、数据“0” 和数据“1” 是由接收到的一个高电平和一个低电平组合而来,一般来说高电平时间等于低电平时间为数据0,其它则为1

参考: STM32之红外遥控信号自学习实现

2. 思路

思路简单,简述为: 捕获——>保存——>发射

先捕获一个红外遥控按键的全部信息,不管内容是什么,再保存到flash中,就是多个高低电平的持续时间,然后再一一发射出去。这样不管是什么协议都能成功。

3. 红外遥控接收

3.1 初始化定时器

具体见代码和详细的注释:

// ir_inputCapture_send.c
/*******************************************************************
 * @brief   定时器16输入捕获初始化/红外遥控初始化、设置IO以及TIM16_CH1的输入捕获、10ms溢出
********************************************************************/
void TIM16_Remote_Init(void)
{  
    TIM_IC_InitTypeDef TIM16_CH1Config;  
    
    TIM16_Handler.Instance = TIM16;                          			//通用定时器16
    TIM16_Handler.Init.Prescaler=(48-1);                	 			//预分频器,1M的计数频率,1us加1.
    TIM16_Handler.Init.CounterMode=TIM_COUNTERMODE_UP;      			//向上计数器
    TIM16_Handler.Init.Period = (60000-1);                      		//自动装载值,16位最大65536,此处设置60ms的计时
    TIM16_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;			//时钟分频因子
    HAL_TIM_IC_Init(&TIM16_Handler);
    
    //初始化TIM1输入捕获参数
    TIM16_CH1Config.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;    	//注意下降沿捕获  
    TIM16_CH1Config.ICSelection=TIM_ICSELECTION_DIRECTTI;				//映射到TI4上
    TIM16_CH1Config.ICPrescaler=TIM_ICPSC_DIV1;          				//配置输入分频,不分频
    TIM16_CH1Config.ICFilter=0x03;  //  0                				//IC4F=0003 8个定时器时钟周期滤波
    HAL_TIM_IC_ConfigChannel(&TIM16_Handler, &TIM16_CH1Config, TIM_CHANNEL_1);//配置TIM4通道4
    __HAL_TIM_ENABLE_IT(&TIM16_Handler,TIM_IT_UPDATE);   				//使能更新中断    
    HAL_TIM_IC_Stop_IT(&TIM16_Handler,TIM_CHANNEL_1);
}
// stm32f0xx_hal_msp.c.c
//定时器16底层驱动,时钟使能,引脚配置
//此函数会被上述的HAL_TIM_IC_Init()调用
//htim:定时器句柄
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef   GPIO_InitStruct;
    if(htim->Instance==TIM16)
    {
        /************TIM16_CH1************/
        GPIO_InitTypeDef GPIO_Initure;
        __HAL_RCC_TIM16_CLK_ENABLE();            			//使能TIM16时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();			 			//开启GPIOA时钟
        
        GPIO_Initure.Pin =          GPIO_PIN_6;            	//PB9
		GPIO_Initure.Mode =         GPIO_MODE_AF_PP;  		//复用输入
        GPIO_Initure.Pull =         GPIO_PULLUP;           	//上拉
        GPIO_Initure.Speed =        GPIO_SPEED_FREQ_HIGH; 	//高速
        GPIO_Initure.Alternate =    GPIO_AF5_TIM16;
        HAL_GPIO_Init(GPIOA,&GPIO_Initure);
        HAL_NVIC_SetPriority(TIM16_IRQn,2,0); 				//设置中断优先级,抢占优先级2,子优先级0
        HAL_NVIC_EnableIRQ(TIM16_IRQn);       				//开启ITM16中断
    }
}

3.2 定时器输入捕获

红外编码是由多个字节编码组成的,根据持续的高低电平时间不同,而形成的编码信号,此时可以通过定时器捕获来实现。

定时器输入捕获中断回调函数如下,红外信号基本都是低电平起,可先用下降沿捕获:

// ir_inputCapture_send.c
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)//捕获中断发生时执行
{
	if(htim->Instance==TIM16)     
	{
		if(READ_IR_GPIO())  														        //上升沿捕获         
		{
            Dval = HAL_TIM_ReadCapturedValue(&TIM16_Handler, TIM_CHANNEL_1);                //读取CCR1也可以清CC4IF标志位
			TIM_RESET_CAPTUREPOLARITY(&TIM16_Handler, TIM_CHANNEL_1);   					//一定要先清除原来的设置!!
			TIM_SET_CAPTUREPOLARITY(&TIM16_Handler, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING); //CC1P=1 设置为下降沿捕获
            
            if(0 == irRun.receive.cnt)    { 
                __HAL_TIM_SET_COUNTER(&TIM16_Handler,0);  						//清空定时器值   	  
                return ;
            } 
            
            if(irRun.receive.cnt<(MAX_IR_LEARN_DATA_SIZE - 1))  {  				// 保存在数组中的 0 2 4 6 8 中
                irRun.buf[irRun.receive.cnt++] = Dval;
            } 
            else {                                  
                RcvRemoteFinish();
            }
            __HAL_TIM_SET_COUNTER(&TIM16_Handler,0);  	//清空定时器值   	  
		}
        else //下降沿捕获 
        {
			Dval = HAL_TIM_ReadCapturedValue(&TIM16_Handler, TIM_CHANNEL_1);
			TIM_RESET_CAPTUREPOLARITY(&TIM16_Handler,TIM_CHANNEL_1);       
			TIM_SET_CAPTUREPOLARITY(&TIM16_Handler,TIM_CHANNEL_1,TIM_ICPOLARITY_RISING);	//配置TIM16通道1上升沿捕获
            
            //当前这一次,不计数,下一次上升沿才计数
            if(0 == irRun.receive.cnt)    {  
                __HAL_TIM_SET_COUNTER(&TIM16_Handler,0);  		    			//清空定时器值   
                return;
            } 

            if(irRun.receive.cnt < (MAX_IR_LEARN_DATA_SIZE-1))   				//1 3 5 7 9
            {
                irRun.buf[irRun.receive.cnt++] = Dval;
                if(irRun.receive.cnt>=20) {
                    irRun.receive.dataComplete = 1;						     
                }
            }
            else 
            {
                RcvRemoteFinish();
            }
            __HAL_TIM_SET_COUNTER(&TIM16_Handler,0);  		    		//清空定时器 放最后
		}
	}
}
// ir_inputCapture_send.c
//60m定时器计时后溢出中断的回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM16)  						//定时器更新(溢出)中断回调函数  10ms溢出
	{  
 		if(irRun.receive.dataComplete)				//至少已经收到了20个字节,才算接收到一帧信息
		{	
            RcvRemoteFinish();
		}	
        else {
            irRun.receive.cnt = 0;					//重新来过,此帧数据无效
        }
	}
}
// ir_inputCapture_send.c
/*  当前红外遥控一帧结束。
	触发条件:① 接收到的红外字节个数已经超过最大允许的长度  (HAL_TIM_IC_CaptureCallback)
			 ② 如上述的定时器计时后溢出中断的回调函数   (HAL_TIM_PeriodElapsedCallback)
*/
static void RcvRemoteFinish(void)
{
    //1. 先失能 避免重洗掉前面保存的一帧完整数据
    TIM16_Remote_Disable();

    //2. 重新设置下降沿捕获,以防上升沿捕获的
    TIM_SET_CAPTUREPOLARITY(&TIM16_Handler, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING); //CC1P=1 设置为下降沿捕获

    //3-1. 清零数据存在的标志
    irRun.receive.dataComplete = 0; //先清零

    //4-2. (多等待了60ms),不管如何都算完成了
    irRun.receive.complete = 1;    // irRun.receive.complete 为一帧接收的完整标志
                
    irRun.receive.len = irRun.receive.cnt - 1;   //一帧接收的最大长度,irRun.receive.cnt 多了1个    
    
	//清零接收长度 缓冲数据
    irRun.receive.cnt = 0;    
}

至此为止 数据保存在 irRun.buf 中, 个数为 irRun.receive.len

3.3 获取数据

具体见代码和详细的注释:

//  获取一帧数据在上层调用
//	返回值: 0, 没有任何按键按下
//   	    1 ,按下的按键键值.  
// ir_inputCapture_send.c
u8 GetRemoteRcvBuf(uint16_t *buf, uint32_t *len, uint8_t isRecord)
{        
    if(irRun.receive.complete && !irRun.receive.dataComplete) { //完成一帧的标记, 且现在没有正在捕获的信号
        TIM_SET_CAPTUREPOLARITY(&TIM16_Handler, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING); //CC1P=1 设置为下降沿捕获
        
        //最多拷贝MAX_IR_LEARN_DATA_SIZE个, 第二次保护,前有一次保护措施   
        if(irRun.receive.len >= MAX_IR_LEARN_DATA_SIZE-1) 
           irRun.receive.len = MAX_IR_LEARN_DATA_SIZE-1; 
        if(irRun.receive.len >= GetFuncLen() && GetFuncLen()>20 && isRecord) {
            irRun.receive.len = GetFuncLen();
            if(irRun.receive.len >= MAX_IR_COMPARE)
               irRun.receive.len = MAX_IR_COMPARE;
        }

        for(uint8_t i=0; i < irRun.receive.len; i++) {  //i<98
            buf[i] = irRun.buf[i+1];                    //[97+1]
        }

        *len = irRun.receive.len;
        //可以中断接收另外的数据呢
        irRun.receive.cnt = 0;  
        irRun.receive.complete = 0;
        irRun.receive.len = 0;

        return 1;
    } 
    return 0;
}

上层应用调用:

// app.c
ack = GetRemoteRcvBuf(&userSave.irFunctionBuf[0], &userSave.irFunctionLen, 0);

得到的红外编码保存在 userSave.irFunctionBuf[] 中,数据长度在 userSave.irFunctionLen 中。当然验证后有效,就可以保存在Flash中了,掉电后不丢失。

红外接收到此结束,接下来验证一番:

3.4 红外接收测试

测试代码,直接在main.c中调用:

#ifdef SYS_USE_DEBUG
u16 test_cnt = 0;
void All_Remote_Test()
{
    u8 ack = 0;
    TIM16_Remote_Enable(); //使能红外遥控捕获
    while(1) {
        ack = GetRemoteRcvBuf(&userSave.irFunctionBuf[0], &userSave.irFunctionLen, 0);
        if(ack == 1) {
            ack = 0;
            LED_ON();
            test_cnt++;
            TIM16_Remote_Enable(); //使能红外遥控捕获
        }
        LED_OFF();
    }
}
#endif

3.5 测试过程

左边使用 KeilDebug调试,捕获一帧红外遥控信号;

右边使用 Kingst 数字信号逻辑分析仪,接收到的此帧数据;

遥控器采用三星的电视机遥控器(不是典型NEC协议),对比如下,左边任意一个字节的数字(持续时间us)都能与右边完美对应上,证明了以上程序的可行性。
在这里插入图片描述

4. 发送程序

原理弄懂,红外发送程序相对来说就简单很多

4.1 初始化定时器和定时器的通道

定时器不分频的情况下,需要产生38KHZ的频率的方波,Period 需要设置成: 48000/38=1263.158

具体见代码和详细的注释:

//ir_inputCapture_send.c
//定时器不分频,需要产生38KHZ的频率的方波,Period 需要设置成: 48000/38=1263.158
void TIM3_PWM_Init(void)
{  
    TIM_MasterConfigTypeDef MasterConfig;
    
    TIM3_Handler.Instance = TIM3;          						//定时器3
    TIM3_Handler.Init.Prescaler = 0;        					//定时器分频
    TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP;			//向上计数模式
    TIM3_Handler.Init.Period = 1264;          					///48000/38=1263.158
    TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
    TIM3_Handler.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    HAL_TIM_PWM_Init(&TIM3_Handler);       						//初始化PWM
    
    MasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    MasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIMEx_MasterConfigSynchronization(&TIM3_Handler, &MasterConfig);
    
    TIM3_CH4Handler.OCMode = TIM_OCMODE_PWM1;         			//模式选择PWM1
    TIM3_CH4Handler.Pulse = 632;                      			//设置比较值,此值用来确定占空比,默认比较值为自动重装载值的一半,即占空比为50%
    TIM3_CH4Handler.OCPolarity = TIM_OCPOLARITY_HIGH; 			//输出比较极性为低 
    TIM3_CH4Handler.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CH4Handler,TIM_CHANNEL_4);	//配置TIM3通道4
	
    HAL_TIM_PWM_MspInit(&TIM3_Handler);
    HAL_TIM_PWM_Stop(&TIM3_Handler,TIM_CHANNEL_4);//关闭PWM通道4
    }

4.1发送函数

发送函数记住核心即可:低电平的时候开启PWM,高电平关闭。

/*******************************************************************
 * @brief   红外PWM控制的发送,在轮询中操作 低电平的时候开启PWM,高电平关闭
 * @retval  None
********************************************************************/
void IrPwmMultiSend(u16 *buf, u32 len)
{
    //1、关闭红外接收中断
    TIM16_Remote_Disable();   //停止捕获TIM16的通道1
    //先停止
    HAL_TIM_PWM_Stop(&TIM3_Handler,TIM_CHANNEL_4);
    delay_ms(10);

    for(u8 i=0; i<len; i++) {
        //偶数 [0] [2] [4] [6] 低电平的时候开启PWM
        __HAL_TIM_SET_COUNTER(&TIM3_Handler,0);
        if(i%2 == 0) { 
            HAL_TIM_PWM_Start(&TIM3_Handler,TIM_CHANNEL_4);
        }
        else {
            HAL_TIM_PWM_Stop(&TIM3_Handler,TIM_CHANNEL_4);
        }
        delay_us(buf[i]);
    }
    
    HAL_TIM_PWM_Stop(&TIM3_Handler,TIM_CHANNEL_4);
    TIM16_Remote_Enable();   //开始捕获TIM16的通道1
}

上层调用:

//app.c
IrPwmMultiSend(&userSave.irPowerBuf[0], userSave.irPowerLen);  //发送开机指令

验证的话,直接看要操控的目标,如电视机是否生效了,如果不行,在验证发出的波形,是否满足要求。

版权声明:本文为CSDN博主「火山上的企鹅」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_16504163/article/details/114222019

生成海报
点赞 0

火山上的企鹅

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

OV7670摄像头模块资料

OV7670摄像头模块资料 一、实物图和原理图 二、模块简介 OV7670 是 OV( OmniVision)公司生产的 CMOS VGA 图像传感器。该传感器体积小、工作电压低,提供单片 VGA

基于STM32的指纹密码锁

设计简介: 本设计是基于单片机的指纹密码锁,主要实现以下功能: 矩阵按键输入密码,并通过按键显示*号可通过按键或手机开门密码可通过按键进行开门可通过蓝牙模块连接手机进行开门可通过指纹进

STM32串口发送接收数据

1.串口通信 我用的32是stm32f10x最小系统没有UART4和UART5 USART : 通用同步异步收发器 UART : 通用异步收发器 nRTS : 请求发送 nCTS : 请求接收 区别:USART指单片机的一个IO端