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

0-红外线科普

红外线(infrared ray)又称红外辐射,介于可见光和微波之间、波长范围为0.76-1000微米的红外波段的电磁波。 它是频率比红光低的不可见光。红外线的英文名是Infrared,其中的infra-意为意为“低于,在…下”。

频率范围:0.3THz(300GHz)~400THz。由下图可知,其频率介于无线电波和可见光之间。

 

图0 电磁波谱

1. 红外模块原理

0-预备知识和硬件认识

红外(Infrared,IR)遥控是一种无线、非接触控制技术,常用于遥控器、无线键盘、鼠标等设备之间的通信。IR协议的工作原理是,发送方通过红外线发送一个特定的编码,接收方通过识别该编码来执行相应的操作。

红外遥控系统分为发射和接收两部分,发射部分的发射元件为红外发光二极管,它发出的是红外线而不是可见光;接收电路的红外接收管是一种光敏二极管。

图1 红外发射-接收系统

常见的红外接收管特性和形状如下:

图2 红外接收管

附:vs1838参数如下:

  • 工作电压:2.7-5.5V
  • 接收距离:18-20M
  • 引脚定义:上图中,从左到右依次是IR、GND、VCC。

 

常见的红外发射管如下:

图3 红外发射管

在使用的时候,控制引脚连接红外发射管正极,红外发射管负极连接一个合适的电阻(我选用220Ω,200~500Ω应该都可以),电阻再接地。

正规的用法如下图

图4 红外发射管、接收管常见应用原理图

1-1. 红外模块发射原理

红外模块的发射原理就是利用单片机的PWM功能,产生38kHz的方波,然后以特定的数据格式发送出去:

图5 红外信号调制

原始信号就是我们要发送的一个数据“0”位或者一位数据“1”位,而所谓 38K 载波就是频率为 38K 的方波信号,调制后信号就是最终我们发射出去的波形。

我们使用原始信号来控制 38K 载波,当信号是数据“0”的时候,38K 载波毫无保留的全部发送出去,当信号是数据“1”的时候,不发送任何载波信号(空闲状态)。

ps: 这张图是有歧义的:歧义的地方如下

  •  图中仅仅是一种版本,另一种版本是为1的时候发送载波,为0的时候空闲。
  • 在实际编程的过程中,并没有所谓的【调制】过程。利用能产生PWM波的引脚连接红外发射管就行了。

 

1-2. 最常见的红外通信协议——NEC 协议

IR协议是指红外线通信协议的总称,而NEC协议是IR协议中的一种具体实现(其他协议还有 RC5、RC6、美的空调的R05D等等)。

数据编码:NEC红外线协议使用脉冲宽度编码(Pulse Width Encoding)来表示二进制数据。每个数据位由一系列的脉冲组成,逻辑0和逻辑1分别由不同的脉冲宽度表示。

帧结构:NEC 协议的数据格式包括了引导码、用户码、用户码(或者用户码反码)、按键键码和键码反码,最后一个停止位。

图6 NEC协议帧结构

也就是说,每次信息都是按照引导码 (9ms载波脉冲+4.5ms 空闲信号)地址码、地址反码、控制码和控制反码的格式进行传输,因此,单次信息传输的时间是固定不变的

图7 NEC协议数码0、1表示

另外,NEC协议中的表示引导码、0、1的定义如下:

  • 引导码:9ms 的载波+4.5ms 的空闲, 总时长13.5ms
  • 比特值“0”:560us/562.5us 的载波+560us/562.5us 的空闲, 总时长1120us/1125us
  • 比特值“1”:560us 的载波+1.68ms 的空闲。总时长2250us

 

另外,当红外遥控器上的按键被一直按下时,红外遥控器只会发送一次完整的信息,其后会每隔 110ms 发送一次重复码(连发码)

重复码的数据格式比较简单,同样是由 9ms的引导码开始,紧接着是2.25ms的空闲信号,随后是560us的载波脉冲。

图8 NEC协议重复码

1-3:扩展——除NEC之外的IR编码方式:R05D协议

参考资料:R05d电控功能说明书 - 百度文库 (baidu.com)

与NEC相同的地方:

  • 载波频率为38KHZ

与NEC不同的地方:

  • 数据格式:引导码+48位数据(=6字节)+分隔码+48位数据(=6字节)(LAA’BB’CC’ S LAA’BB’CC’),
    由查阅相关文档我们知道开机的编码为:B=10111111 C=00011000(自动模式、自动风速、26度);其中A=10110010(一般A都是不变的固定的)
    总结:NEC为4字节数据,而R05D则有12字节。
  • MSB/LSB先行方式不同:R05D采用MSB在先,LSB在后;也就是高位先发,NEC采用低位(LSB)先发送
  • 引导码电平时间不同:R05D协议引导码定义如下:


  • 多出了分隔符,协议定义电平如下:
  • 数据码0、1的电平时间不同,较NEC协议较短:

 

1-4、 红外模块接收原理

HS0038/vs1838等红外接收头,内部集成了监测,放大,滤波,解调等一系列电路处理输出基带信号。

HS0038B/vs1838红外接收头在收到有载波的信号的时候,会输出一个低电平,空闲的时候会输出高电平。这样通过红外接收头的每对低-高电平对应的时长,便可以进行解码——得到一位位0和1,进而得出数据。

图8 红外接收模块接收图

 

2-STM32 接收红外信号——记录电平时间

2-1 红外接收、发送知识

由上述原理图可知,当IE为高电平时发送红外光,为低电平时不发送红外光。

在NEC协议中,信息传输是基于38K载波,也就是说红外线是以载波的方式传递。

发送波形如下图所示:

NEC协议规定:

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

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

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

 

在红外接收端,如果接收到红外38K载波,则IR输出为低电平,如果不是载波包括固定低电平和固定高电平则输出高电平。在IR端接收的信号如下所示:

           

      

2-2、软件实现自学习

设计原理:

1、 利用定时器的输入捕获功能,接收波形,记录高低电平持续时间

2、从下降沿接收引导信号开始,每触发一次就改变一次触发方式,从而使每个电平变化都能捕获到。

源码实现如下:

定时器捕获初始化设置(CubeMax自动配置生成):

void MX_TIM4_Init(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_IC_InitTypeDef sConfigIC = {0};

  htim4.Instance = TIM4;
  htim4.Init.Prescaler = 71;
  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim4.Init.Period = 10000;
  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_IC_Init(&htim4) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
  sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
  sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
  sConfigIC.ICFilter = 0;
  if (HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_4) != HAL_OK)
  {
    Error_Handler();
  }

}

定时器捕获中断回调处理:

复制代码
复制代码
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_4)
    {
        if(TIM4->CCER & (TIM_CCER_CC4P))   //下降沿触发
        {
            TIM4->CCER &= ~(TIM_CCER_CC4P); //切换
            gu8BitVal = 1;
        }
        else                               //上升沿触发
        {
            TIM4->CCER |= TIM_CCER_CC4P;    //切换
            gu8BitVal = 0;
        }


        if(gsInfrared.State == NONE_STATE)
        {
            gsInfrared.State = RECV_STATE;
        }
        else if(gsInfrared.State == RECV_STATE)
        {
            NowTimCnt = HAL_TIM_ReadCapturedValue(&htim4, TIM_CHANNEL_4);
            gsInfrared.KeepTime[gsInfrared.SampleCount] = Round(NowTimCnt);
            gsInfrared.BitValue[gsInfrared.SampleCount ++] = gu8BitVal;
        }

        TIM4->CNT = 0;
    }
}
复制代码
复制代码

3、设置的定时器溢出时间为10ms(  htim4.Init.Period = 10000;),如果10毫秒内不再接收电平变化则默认接收结束,设置结束标志。

复制代码
复制代码
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim == &htim4)
    {
        if(gsInfrared.State == RECV_STATE)
        {
            gsInfrared.State = END_STATE;
        }
    }
}
复制代码
复制代码

至此,实现了红外遥控的学习功能,获得的记录数据为记录长度和电平信号数组与电平信号维持的时间数组。

用美的遥控器进行测试,与R05D协议规定时间基本相同:

引导码:

分割码:

另外如果按数据格式:引导码+48位数据(=6字节)+分隔码+引导码+48位数据(=6字节)(LAA’BB’CC’ S LAA’BB’CC’)

那么分割码之前应该有时间段数量为(1+48)*2 = 98, 因为数组起始为0,因此分隔码前数组范围为0~97;所以分隔码标号就是98、99,验证了上面显示的。

分隔码之后也是98个时间段,因此总共有98+98+2 = 198 个时间段,但是实验结果总共有199个,可推理出这应该是一个 STOP  信号。

图 最后一个数据(数组第199个数据,表示停止位)

3-STM32 实现发送红外信号

3-1 STM32的PWM输出模式复习

PWM模式1:
  向上计数时,当CNT < CCRx 时,输出有效电平,否则为无效电平; 向下计数时,当CNT > CCRx 时输出为无效电平,否则为有效电平。

PWM模式2:
  向上计数时,一当CNT < CCRx 时,输出无效电平,否则为有效电平; 在向下计数时,一旦 CNT > CCRx 时输出为有效电平,否则为无效电平。

设置定时器输出38KPWM信号,在记录电平为0是输出记录时间的38K载波信号,如果为1则不输出载波,实现如下:

PWM生成相关的定时器设置(CubeMx自动配置生成):

void MX_TIM5_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};

  htim5.Instance = TIM5;
  htim5.Init.Prescaler = 0;
  htim5.Init.CounterMode = TIM_COUNTERMODE_UP;//向上计数
  htim5.Init.Period = 1896;//PWM的频率为  72Mhz/(1896-1) ≈ 38KHz   
    //上面也可以 72Mhz / 5(预分频) / 378(计数次数) ≈ 38KHz
 
  htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_PWM_Init(&htim5) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;//有效电平为高电平,
    //如果TIM的比较值设置为0,那么一直输出低电平;
    //如果设置为周期的一半,则输出50%的PWM波
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  if (HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
  {
    Error_Handler();
  }
  HAL_TIM_MspPostInit(&htim5);

}

 

发送实现,注意记录为0时发载波,记录为1时不发载波:

void InfraredSend(void)
{
    uint16_t Count = 0;
    
    while(Count < gsInfrared.SampleCount &&  gsInfrared.State == END_STATE)
    {
        if(gsInfrared.BitValue[Count] == 0)
        {
            TIM5->CCR2 = 948;//设置38KHz,占空比为50%的PWM波
            delay_us(gsInfrared.KeepTime[Count]);
            TIM5->CCR2 = 0;
        }
        else
        {
            TIM5->CCR2 = 0;
            delay_us(gsInfrared.KeepTime[Count]);
            TIM5->CCR2 = 0;
        }

        Count ++;
    }

    delay_us(20000);
}

往PWM比较寄存器设置948即为设置38KHz,占空比为50%的PWM波,也可在初始化时固定948,在此函数内启停定时器即可;

至此,自学习功能的全部思路已实现,通过对各个不同类型的红外遥控进行功能测试,均成功。

4-STM32 红外信号NEC协议解码

4-1 在中断内解码(一边接收红外一边解码)

可用的中断有两种:EXTI中断和TIM输入捕获中断。

编程思路:均配置为下降沿触发(且触发方式不变)。根据两个下降沿之间的时间差来判断。

 

4-2 接收完所有数据再解码

PS:查看很多资料发现很多红外解码未判断高电平时间(或者高电平),个人感觉不是很好,应该是不仅低电平时间得符合,高电平时间也应该符合。

自己写了一个小函数验证了一下,这个函数只是验证,未经仔细推敲,还可优化,仅供参考这一思想。

误差设计:±200us(拍脑袋值)

void InFraredDataDeal(void)
{
    uint32_t DataBuff = 0;
    uint16_t Count = 0;

    if(gsInfrared.State == END_STATE)
    {
        gsInfraredData.State = 0;

        do
        {
            switch(gsInfraredData.State)
            {
            case 0:   //引导码识别
            {

                if(gsInfrared.KeepTime[0] >= 8800 && gsInfrared.KeepTime[0] <= 9200 && gsInfrared.BitValue[0] == 0)
                {
                    if(gsInfrared.KeepTime[1] >= 4300 && gsInfrared.KeepTime[1] <= 4700 && gsInfrared.BitValue[1] == 1)
                    {
                        if(gsInfrared.KeepTime[2] >= 360 && gsInfrared.KeepTime[2] <= 760 && gsInfrared.BitValue[2] == 0)
                        {
                            Count = 3;
                            gsInfraredData.State = 1;
                        }
                    }
                    else if(gsInfrared.KeepTime[1] >= 2300 && gsInfrared.KeepTime[1] <= 2700 && gsInfrared.BitValue[1] == 1)
                    {
                        if(gsInfrared.KeepTime[2] >= 360 && gsInfrared.KeepTime[2] <= 760 && gsInfrared.BitValue[2] == 0)
                        {
                            gsInfraredData.ReDataCount ++;
                            gsInfraredData.State = 3;
                        }
                    }
                    else
                    {
                        gsInfraredData.State = 3;
                    }

                }
                else
                {
                    gsInfraredData.State = 3;
                }


            }
            break;


            case 1:   //数据解析
            {

                if(gsInfrared.KeepTime[Count + 1] >= 360 && gsInfrared.KeepTime[Count + 1] <= 760 && gsInfrared.BitValue[Count + 1] == 0)
                {

                    if(gsInfrared.BitValue[Count] == 1)
                    {
                        if(gsInfrared.KeepTime[Count] >= 1480 && gsInfrared.KeepTime[Count] <= 1880)
                        {
                            DataBuff <<= 1;
                            DataBuff |= 1;
                        }
                        else if(gsInfrared.KeepTime[Count] >= 360 && gsInfrared.KeepTime[Count] <= 760 && gsInfrared.BitValue[Count] == 1)
                        {
                            DataBuff <<= 1;
                            DataBuff |= 0;
                        }
                        else
                        {
                            gsInfraredData.State = 3;
                        }
                    }
                }

                if(Count < gsInfrared.SampleCount)
                {
                    Count += 2;
                }
                else
                {
                    gsInfraredData.State = 2;
                }

            }
            break;

            case 2:   //成功解析
            {
                gsInfraredData.Data = DataBuff;
                gsInfraredData.State = 3;

            }
            break;

            default:
            {
                gsInfraredData.State = 3;   //解析结束

            }
            break;
            }

        }
        while(gsInfraredData.State != 3);

        gsInfrared.State = NONE_STATE;
        gsInfrared.SampleCount = 0;
    }

}

NEC协议高位(MSB)在前,所以左移,经测试帧格式为:引导码+用户码+用户码反码+命令码+命令码反码,能成功解析数据!其他协议解析需要具体情况具体分析。

 

 

X-参考文章:

1.【Arduino教程】如何使用红外控制空调(Bilibili作者:坑坑是这样炼成的):https://www.bilibili.com/read/cv14721363/

推荐:STM32的红外编程有一定难度。先用Arduino学习,可以更好地学习红外IR协议。

这篇文章以实现控制格力空调为最终目的,讲解了如何接收、发送、解编码红外信号的全过程。

2. STM32之红外遥控信号自学习实现 - 树荫下的阳光 - 博客园

推荐:本文的STM32编程部分来源于这篇文章。

另外,对比参考文章1,就知道上述收发程序的逻辑与Arduino中IRremote库的Rcv/SendRaw例程很类似。

3. CSDN作者:人面兽心Edison【STM32】R05D电控红外协议的美的空调遥控器:https://blog.csdn.net/weixin_42204837/article/details/109263771?spm=1001.2014.3001.5501

推荐:详细地阐述了美的空调 R05D 协议的编码机制,并给出了按照这种协议红外发射的程序。

4. STM32F407实现NEC协议红外线解码: https://blog.csdn.net/xiaolong1126626497/article/details/131458658?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-12-131458658-blog-131857097.235^v43^control&spm=1001.2101.3001.4242.7&utm_relevant_index=15

推荐:实现了NEC协议的红外接收译码,作者采用了STM32中的寄存器编程(很酷!),程序很简洁明了,值得借鉴。

5. STM32 ——(红外遥控)(CSDN作者 ):https://blog.csdn.net/qq_56030168/article/details/121189036

推荐:本文主要内容来自于参考文章3.

6. 红外遥控系统通信(NEC)(知乎作者:David Panda):https://zhuanlan.zhihu.com/p/559261264?utm_id=0
推荐:作者采用C51和状态机的方式,实现红外解码。
 
 
关于红外,其他有趣的知识和实验:
posted @ 2024-02-10 23:29  FBshark  阅读(1034)  评论(0编辑  收藏  举报