vesperW · 6 天前

基于 MCU 实现红外接收,原理及源码描述

一.模块来源

红外接收模块网上很多,类似如下:

image.png

二.规格参数

1.CR2025 环保纽扣电池,容量 160mah

2.发射距离:8m 以上(具体和周围环境、接收端的灵敏度等因素有关)

3.有效角度:60 度

4.面贴材料:0.125mmPET,有效寿命 2 万次。

5.品质稳定,性价比高

6.静态电流 3-5uA,动态电流 3-5mA。

三.移植过程

我们的目标是将例程移植至 CW32F030C8T6 开发板上【能够实现红外信号接收的功能】。首先要获取资料,查看数据手册应如何实现读取数据,再移植至我们的工程。

3.1 查看资料

在光谱中波长自 760nm 至 400um 的电磁波称为红外线,它是一种不可见光。红外线通信的例子我们每个人应该都很熟悉,目前常用的家电设备几乎都可以通过红外遥控的方式进行遥控,比如电视机、空调、投影仪等,都可以见到红外遥控的影子。这种技术应用广泛,相应的应用器件都十分廉价,因此红外遥控是我们日常设备控制的理想方式。

红外线的通讯原理

红外光是以特定的频率脉冲形式发射,接收端收到到信号后,按照约定的协议进行解码,完成数据传输。在消费类电子产品里,脉冲频率普遍采用 30KHz 到 60KHz 这个频段,NEC 协议的频率就是 38KHZ。这个以特定的频率发射其实就可以理解为点灯,不要被复杂的词汇难住了,就是控制灯的闪烁频率(亮灭),和刚学单片机完成闪烁灯一样的意思,只不过是灯换了一种类型,都是灯。

接收端的原理: 接收端的芯片对这个红外光比较敏感,可以根据有没有光输出高低电平,如果发送端的闪烁频率是有规律的,接收端收到后输出的高电平和低电平也是有规律对应的,这样发送端和接收端只要约定好,那就可以做数据传输了。

红外线传输协议可以说是所有无线传输协议里成本最低,最方便的传输协议了,但是也有缺点,距离不够长,速度不够快;当然,每个传输协议应用的环境不一样,定位不一样,好坏没法比较,具体要看自己的实际场景选择合适的通信方式。

NEC 协议介绍

NEC 协议是众多红外线协议中的一种(这里说的协议就是他们数据帧格式定义不一样,数据传输原理都是一样的),我们购买的外能遥控器、淘宝买的 mini 遥控器、电视机、投影仪几乎都是 NEC 协议。像格力空调、美的空调这些设备使用的就是其他协议格式,不是 NEC 协议,但是只要学会一种协议解析方式,明白了红外线传输原理,其他遥控器协议都可以解出来。

NEC 协议一次完整的传输包含: 引导码、8 位地址码、8 位地址反码、8 位命令码、8 位命令反码。这里我们主要讲解如何接收红外发送端发送的 NEC 协议内容。

Image

引导码:由 9ms 的低电平+4.5ms 的高电平组成。

Image

4 个字节的数据: 地址码+地址反码+命令码+命令反码。这里的反码可以用来校验数据是否传输正确,有没有丢包。

重点: NEC 协议传输数据位的时候,0 和 1 的区分是依靠收到的高、低电平的持续时间来进行区分的。这是解码关键。

数据发送 0 码:0.56ms 低电平+ 0.56ms 的高电平。

Image

数据发送 1 码:0.56ms 低电平+1.68ms 的高电平

Image

所以,收到一个数据位的完整时间表示方法是这样的:

收到数据位0:   0.56ms低电平+ 0.56ms的高电平    
收到数据位1:  0.56ms低电平+1.68ms的高电平

还有一个重复码,它是由一个 9ms 的低电平和一个 2.5ms 的高电平组成。当一个红外信号连续发送时,可以通过发送重复码的方式快速发送。

Image

3.2 引脚选择

当红外线接收头感应到有红外光就输出低电平,没有感应到红外光就输出高电平。因此我们配置红外引脚为外部中断下降沿触发方式,当红外引脚有下降沿时,我们马上进入中断处理并接收红外信号。

Image

模块接线图

3.3 移植至工程

引脚配置如下:

//红外引脚初始化
void infrared_goio_config(void)
{
    IR_RCC_GPIO_ENABLE();      // 使能GPIO时钟

    GPIO_InitTypeDef GPIO_InitStruct;                   // GPIO初始化结构体

    GPIO_InitStruct.Pins  = IR_PIN;                     // GPIO引脚
    GPIO_InitStruct.Mode  = GPIO_MODE_INPUT_PULLUP;     // 上拉输入
    GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;            // 速度高
    GPIO_InitStruct.IT    = GPIO_IT_FALLING;            // 下降沿触发中断
    GPIO_Init(IR_PORT, &GPIO_InitStruct);               // 初始化

    // 清除PA0中断标志
    GPIOA_INTFLAG_CLR(EXTI_BV);
    // 使能NVIC
    NVIC_EnableIRQ(EXTI_IRQ);
}

红外信号的数据,全部是以时间长度来确定数据是 0 还是 1,而最小的单位要求有 560us,已经达到了 us 级的测量。

我们在 空白工程中已经为大家准备好了 us 延时,就在 board 文件中。

获取高低电平时间

获取低电平时间的实现代码如下:

//获取红外低电平时间
//以微秒us作为时间参考
void get_infrared_low_time( uint32_t *low_time )
{
    uint32_t time_val = 0;

    while( GPIO_ReadPin(IR_PORT, IR_PIN) == 0 )
    {
        if( time_val>= 500 )
        {
            *low_time = time_val;
            return;
        }
        delay_us(20);
        time_val++;
    }
    *low_time = time_val;
}

当引脚为低电平时,将进入 while 循环,直到不为低电平时就结束循环。在循环之中不断的让时间变量 time_val 累加, 每加一次需要经过 20us。当 time_val 变量累加时间大于 500 * 20 = 10000us = 10ms 时,判断为超时,强行结束该函数,防止阻碍系统运行。

获取高电平时间的代码同理:

//获取红外高电平时间
//以微秒us作为时间参考
void get_infrared_high_time(uint32_t *high_time)
{
    uint32_t time_val = 0;
    while( GPIO_ReadPin(IR_PORT, IR_PIN) == 1 )
    {
        if( time_val >= 250 )
        {
            *high_time = time_val;
            return;
        }
        delay_us(20);
        time_val++;
    }
    *high_time = time_val;
}

引导码与重复码判断

引导码是由一个 9ms 的低电平和一个 4.5ms 的高电平组成。每当接收到一个红外信号时,第一个数据就是引导码。我们通过判断红外信号的第一个数据是否是引导码,来决定是否要进行后面的数据接收处理。

重复码是由一个 9ms 的低电平和一个 2.5ms 的高电平组成。当我们的红外遥控一直按住按键时,就会发出重复码,我们可以检测重复码,来确定是否要连续触发重复动作,比如长按开机,长按加速等等。

/******************************************************************
 * 函 数 名 称:guide_and_repeat_code_judgment
 * 函 数 说 明:引导 和 重复 码 判断
 * 函 数 形 参:无
 * 函 数 返 回:1:不是引导码   2:重复码  0:引导码
 * 作       者:LC
 * 备       注:以20微秒us作为时间参考
                引导码:由一个 9ms 的低电平和一个 4.5ms 的高电平组成
                重复码:由一个 9ms 的低电平和一个 2.5ms 的高电平组成
******************************************************************/
uint8_t guide_and_repeat_code_judgment(void)
{
    uint32_t out_time=0;
    get_infrared_low_time(&out_time);
    //time>10ms             time <8ms
    if((out_time > 500) || (out_time < 400))
    {
        return 1;
    }
    get_infrared_high_time(&out_time);
    // x>5ms  或者 x<2ms
    if((out_time > 250) || (out_time < 100))
    {
        return 1;
    }

    //如果是重复码  2ms < time < 3ms
    if((out_time > 100) && (out_time < 150))
    {
        return 2;
    }

    return 0;
}

完整红外数据接收

具体接收流程:【判断是否接收到引导码】->【接收数据】->【判断数据是否正确】。

//接收红外数据
void receiving_infrared_data(void)
{
    uint16_t group_num = 0,data_num = 0;
    uint32_t time=0;
    uint8_t bit_data = 0;
    uint8_t ir_value[5] = {0};

    uint8_t guide_and_repeat_code = 0;

    //等待引导码
    guide_and_repeat_code = guide_and_repeat_code_judgment();
    //如果不是引导码则结束解析
    if(  guide_and_repeat_code == 1 ) return;

    //共有4组数据
    //地址码+地址反码+命令码+命令反码
    for(group_num = 0; group_num < 4; group_num++ )
        {
        //接收一组8位的数据
        for( data_num = 0; data_num < 8; data_num++ )
        {
            //接收低电平
            get_infrared_low_time(&time);
            //如果不在0.56ms内的低电平,数据错误
            if((time > 60) || (time < 20))
            {
                return ;
            }

            time = 0;
            //接收高电平
            get_infrared_high_time(&time);
            //如果是在1200us<t<2000us范围内则判断为1
            if((time >=60) && (time < 100))
            {
                bit_data = 1;
            }
            //如果是在200us<t<1000us范围内则判断为0
            else if((time >=10) && (time < 50))
            {
                bit_data = 0;
            }

            //groupNum表示第几组数据
            ir_value[ group_num ] <<= 1;

            //接收的第1个数为高电平;在第二个for循环中,数据会向右移8次
            ir_value[ group_num ] |= bit_data;

            //用完时间要重新赋值
            time=0;
        }
    }
    //判断数据是否正确,正确则保存数据
    infrared_data_true_judgment(ir_value);
}

判断数据是否正确,可以通过将正常数据取反,与反码比较。如果不一致说明数据不对。

typedef struct INFRARED_DATA{

    uint8_t AddressCode;            //地址码
    uint8_t AddressInverseCode;     //地址反码
    uint8_t CommandCode;            //命令码
    uint8_t CommandInverseCode;     //命令反码

}_INFRARED_DATA_STRUCT_;
_INFRARED_DATA_STRUCT_ InfraredData;


//红外数据是否正确判断
uint8_t infrared_data_true_judgment(uint8_t *value)
{
    //判断地址码是否正确
    if( value[0] != (uint8_t)(~value[1]) )  return 0;
    //判断命令码是否正确
    if( value[2] != (uint8_t)(~value[3]) )  return 1;

    //串口输出查看接收到的数据
    printf("%x %x %x %x\r\n",value[0],value[1],value[2],value[3]);
    //保存正确数据
    InfraredData.AddressCode        = value[0];
    InfraredData.AddressInverseCode = value[1];
    InfraredData.CommandCode        = value[2];
    InfraredData.CommandInverseCode = value[3];
}

//获取红外发送过来的命令
uint8_t get_infrared_command(void)
{
    return InfraredData.CommandCode;
}
//清除红外发送过来的数据
void clear_infrared_command(void)
{
    InfraredData.CommandCode = 0x00;
}

最后,记得在外部中断服务函数中,调用红外接收函数。


void EXTI_HANDLER(void)
{
        if(IR_PORT->ISR_f.EXTI_PIN)   // 中断标志位
        {
                if(GPIO_ReadPin(IR_PORT, IR_PIN) == GPIO_Pin_RESET)  // 如果是低电平
                {
                        //接收一次红外数据
                        receiving_infrared_data();
                }
                GPIOA_INTFLAG_CLR(EXTI_BV); // 清除标志位
        }
}

移植步骤中的导入.c 和.h 文件与【CW32 模块使用】DHT11 温湿度传感器相同,只是将.c 和.h 文件更改为 bsp_ir_receiver.c 与 bsp_ir_receiver.h。这里不再过多讲述,移植完成后面修改相关代码。

以下为完成红外接收代码:

bsp_ir_receiver.c

/*
 * Change Logs:
 * Date           Author       Notes
 * 2024-06-24     LCKFB-LP    first version
 */

#include "bsp_ir_receiver.h"
#include "stdio.h"
#include "board.h"



typedef struct INFRARED_DATA{

    uint8_t AddressCode;            //地址码
    uint8_t AddressInverseCode;     //地址反码
    uint8_t CommandCode;            //命令码
    uint8_t CommandInverseCode;     //命令反码

}_INFRARED_DATA_STRUCT_;


_INFRARED_DATA_STRUCT_ InfraredData;

//红外引脚初始化
void infrared_goio_config(void)
{
        IR_RCC_GPIO_ENABLE();      // 使能GPIO时钟


        GPIO_InitTypeDef GPIO_InitStruct;                // GPIO初始化结构体

        GPIO_InitStruct.Pins  = IR_PIN;                  // GPIO引脚
        GPIO_InitStruct.Mode  = GPIO_MODE_INPUT_PULLUP;  // 上拉输入
        GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;         // 速度高
        GPIO_InitStruct.IT    = GPIO_IT_FALLING;         // 下降沿触发中断
        GPIO_Init(IR_PORT, &GPIO_InitStruct);            // 初始化

        // 清除PA0中断标志
        GPIOA_INTFLAG_CLR(EXTI_BV);
        // 使能NVIC
        NVIC_EnableIRQ(EXTI_IRQ);
}




//获取红外低电平时间
//以微秒us作为时间参考
void get_infrared_low_time( uint32_t *low_time )
{
    uint32_t time_val = 0;

    while( GPIO_ReadPin(IR_PORT, IR_PIN) == 0 )
    {
        if( time_val>= 500 )
        {
            *low_time = time_val;
            return;
        }
        delay_us(20);
        time_val++;
    }
    *low_time = time_val;
}

//获取红外高电平时间
//以微秒us作为时间参考
void get_infrared_high_time(uint32_t *high_time)
{
    uint32_t time_val = 0;
    while( GPIO_ReadPin(IR_PORT, IR_PIN) == 1 )
    {
        if( time_val >= 250 )
        {
            *high_time = time_val;
            return;
        }
        delay_us(20);
        time_val++;
    }
    *high_time = time_val;
}

/******************************************************************
 * 函 数 名 称:guide_and_repeat_code_judgment
 * 函 数 说 明:引导 和 重复 码 判断
 * 函 数 形 参:无
 * 函 数 返 回:1:不是引导码   2:重复码  0:引导码
 * 作       者:LC
 * 备       注:以20微秒us作为时间参考
                引导码:由一个 9ms 的低电平和一个 4.5ms 的高电平组成
                重复码:由一个 9ms 的低电平和一个 2.5ms 的高电平组成
******************************************************************/
uint8_t guide_and_repeat_code_judgment(void)
{
    uint32_t out_time=0;
    get_infrared_low_time(&out_time);
    //time>10ms             time <8ms
    if((out_time > 500) || (out_time < 400))
    {
        return 1;
    }
    get_infrared_high_time(&out_time);
    // x>5ms  或者 x<2ms
    if((out_time > 250) || (out_time < 100))
    {
        return 1;
    }

    //如果是重复码  2ms < time < 3ms
    if((out_time > 100) && (out_time < 150))
    {
        return 2;
    }

    return 0;
}

//红外数据是否正确判断
uint8_t infrared_data_true_judgment(uint8_t *value)
{
    //判断地址码是否正确
    if( value[0] != (uint8_t)(~value[1]) )  return 0;
    //判断命令码是否正确
    if( value[2] != (uint8_t)(~value[3]) )  return 1;


    printf("%x %x %x %x\r\n",value[0],value[1],value[2],value[3]);
    //保存正确数据
    InfraredData.AddressCode        = value[0];
    InfraredData.AddressInverseCode = value[1];
    InfraredData.CommandCode        = value[2];
    InfraredData.CommandInverseCode = value[3];
}

//接收红外数据
void receiving_infrared_data(void)
{
    uint16_t group_num = 0,data_num = 0;
    uint32_t time=0;
    uint8_t bit_data = 0;
    uint8_t ir_value[5] = {0};

    uint8_t guide_and_repeat_code = 0;

    //等待引导码
    guide_and_repeat_code = guide_and_repeat_code_judgment();
    //如果不是引导码则结束解析
    if(  guide_and_repeat_code == 1 )
    {
        printf("err\r\n");
        return;
    }

    //共有4组数据
    //地址码+地址反码+命令码+命令反码
    for(group_num = 0; group_num < 4; group_num++ )
        {
        //接收一组8位的数据
        for( data_num = 0; data_num < 8; data_num++ )
        {
            //接收低电平
            get_infrared_low_time(&time);
            //如果不在0.56ms内的低电平,数据错误
            if((time > 60) || (time < 20))
            {
                return ;
            }

            time = 0;
            //接收高电平
            get_infrared_high_time(&time);
            //如果是在1200us<t<2000us范围内则判断为1
            if((time >=60) && (time < 100))
            {
                bit_data = 1;
            }
            //如果是在200us<t<1000us范围内则判断为0
            else if((time >=10) && (time < 50))
            {
                bit_data = 0;
            }

            //groupNum表示第几组数据
            ir_value[ group_num ] <<= 1;

            //接收的第1个数为高电平;在第二个for循环中,数据会向右移8次
            ir_value[ group_num ] |= bit_data;

            //用完时间要重新赋值
            time=0;
        }
    }
    //判断数据是否正确,正确则保存数据
    infrared_data_true_judgment(ir_value);
}

//获取红外发送过来的命令
uint8_t get_infrared_command(void)
{
    return InfraredData.CommandCode;
}
//清除红外发送过来的数据
void clear_infrared_command(void)
{
    InfraredData.CommandCode = 0x00;
}


void EXTI_HANDLER(void)
{
        if(IR_PORT->ISR_f.EXTI_PIN)   // 中断标志位
        {
                if(GPIO_ReadPin(IR_PORT, IR_PIN) == GPIO_Pin_RESET)  // 如果是低电平
                {
                        //接收一次红外数据
                        receiving_infrared_data();
                }
                GPIOA_INTFLAG_CLR(EXTI_BV); // 清除标志位
        }
}

bsp_ir_receiver.h

/*
 * Change Logs:
 * Date           Author       Notes
 * 2024-06-24     LCKFB-LP    first version
 */

#ifndef _BSP_IR_RECEIVER_H__
#define _BSP_IR_RECEIVER_H__

#include "board.h"

#define IR_RCC_GPIO_ENABLE()     __RCC_GPIOA_CLK_ENABLE()
#define IR_PORT                  CW_GPIOA
#define IR_PIN                   GPIO_PIN_2

#define EXTI_PIN                 PIN2
#define EXTI_BV                  bv2
#define EXTI_IRQ                 GPIOA_IRQn
#define EXTI_HANDLER             GPIOA_IRQHandler

void infrared_goio_config(void);
uint8_t get_infrared_command(void);
void clear_infrared_command(void);
#endif

四.移植至工程

在自己工程中的 main 主函数中,编写如下。

/*
 * Change Logs:
 * Date           Author       Notes
 * 2024-06-24     LCKFB-LP    first version
 */
#include "board.h"
#include "stdio.h"
#include "bsp_uart.h"
#include "bsp_ir_receiver.h"

int32_t main(void)
{
    board_init();        // 开发板初始化

    uart1_init(115200);        // 串口1波特率115200


    //红外接收初始化
    infrared_goio_config();

    printf("Start!!!\r\n");

    while(1)
    {
            //如果按下遥控的【1】键
            if( get_infrared_command() == 0xA2 )
            {
                    clear_infrared_command();
                    printf("按下【1】按键! \r\n");
            }

    }
}

移植现象:

Image

模块移植成功案例代码:

链接:https://pan.baidu.com/s/1Yln6MD82bPkgS2x-YMnfCQ?pwd=LCKF
提取码:LCKF

END

来源:strongerHuang

推荐阅读

欢迎大家点赞留言,更多 Arm 技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。

推荐阅读
关注数
2896
内容数
309
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息