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

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

火山上的企鹅

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

暂无评论

相关推荐

在Stm32CubeIDE环境下使用DAP-Link仿真

目录 一、文章背景 二、准备工作 三、调试过程 四、编写脚本自动执行OpenOCD服务 一、文章背景 最近师弟需要调STM32,由于他已经习惯了Eclipse的开发环境,所以给他推荐了Stm32CubeIDE

STM32的12位ADC过采样实现16位分辨率

1.什么是过采样过采样技术是一种以牺牲采样速度来提高ADC分辨率的技术。部分STM32单片机是支持硬件过采样的,如STM32G0系列。通过过采样,可以将12位的ADC提升到16位,非常实用。根据过采样技