红外遥控学习,万能遥控解决方案
Posted 火山上的企鹅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了红外遥控学习,万能遥控解决方案相关的知识,希望对你有一定的参考价值。
红外遥控学习,万能遥控解决方案
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
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 测试过程
左边使用 Keil 的 Debug调试,捕获一帧红外遥控信号;
右边使用 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); //发送开机指令
验证的话,直接看要操控的目标,如电视机是否生效了,如果不行,在验证发出的波形,是否满足要求。
以上是关于红外遥控学习,万能遥控解决方案的主要内容,如果未能解决你的问题,请参考以下文章