一、前景提要

(备注:这个代码是c语言编到keil的,51核的芯片,下面的所有波形图都是用逻辑分析器测量的)

        市面上的红外接收管大体有两种,一种是两脚的,它类似于三极管,接收到红外信号后内部的PN结会趋近导通,反之不通,红外信号越强导通能力越强。
        另一种是三脚的,3脚的红外接收管内部有芯片,会对接收信号做处理,这样即使接收到的是载波(就是发射管那边发的是高频PWM)也会输出持续的低电平,反之高电平。
        三脚的接收管会滤掉一些杂波之类的、略贵几毛钱,适合新手学习用,低成本批量产品很少用三脚的。

        我这边用的是3脚的红外接收管,一个接地、一个接VCC、一个信号输出脚,VCC端并电容(我用的0.1uF)滤波,信号输出脚串了个100R到单片机。接收管的输出脚在没接收到信号的时候输出的是高电平

        这篇文章刚写的时候是2020年7月,那个暑假我刚开始系统性学单片机,文章里所贴的代码是当时入门时写的,确实是新手中的新手,还请见谅。这篇文章我在25年9月又重新修缮了一下,希望大家看完能有所收获。

二、NEC接收部分

在红外发送器发送信号时,接收头会接收到信号,首先,接收到的该信号有一个头码(引导码)

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

(↑9ms低电平)


(↑4.5ms高电平)

然后波形就会收到32位的数据码了具体分布如下:


其中,1、引导码;2、用户码;3、用户反码;4、数据码;5、数据反码;6、结束码

前面讲的9ms低电平和4.5ms高电平就是引导码的阶段,在2~5阶段中,每个阶段收到8位数据,一共是32位数据,可以使用4个8位寄存器保存,因为2和3的值是相对应取反的,4和5的值是相对应取反的,所以4个寄存器可以拿来互相比较用以校验数据准确性。我的程序是用32位寄存器保存的。

好的现在我们看一下0和1这两个数据的特征。


↑这是0的值,协议里是560us的低电平,然后再560us的高电平,周期是1.20ms;

↑这是1的值,协议里是560us的低电平,然后再1680us的高电平,周期是2.24ms;

最后就是0.56ms的低电平结束码了。这就是接收到的第一串数据。贴的这串数据是01FE48B7H。


上面那一串应该就能理解了吧~,然后继续,如果信号发送器发送的按键信息“长按”了,之后接受到的就是重复码了:


重复码的规则是这样的,从第一次接受到信号开始计时,到每一次接收重复码,间隔时间是110ms:

↑也就是这三个间隔都为110ms。

然后,重复码的具体信号是这样的:

首先接收到的是9ms的低电平,然后是2.25ms的高电平,然后是560us的低电平。

ok波形讲完了,那么代码中要注意到的有哪些呢?

贴代码:

unsigned char sum,sum0,sum1,b1,b2,b3,c1,c2,c3,d,j;
unsigned long number,number_out;
void main()
{
/************************************系统初始化****************************************/
    CLKSWR = 0x51;                        //选择内部高频RC为系统时钟,内部高频RC 2分频,Fosc=16MHz
    CLKDIV = 0x01;                        //Fosc 1分频得到Fcpu,Fcpu=16MHz 
/**********************************相关配置初始化**************************************/
    P1M6 = 0xC1;
    P1M5 = 0xC1;
    P1M4 = 0xC1;
    P1M7 = 0x61;                        //P17非斯密特带上拉输入;
    
/**********************************TIM0配置初始化**************************************/
    TCON1 = 0x00;                        //Tx0定时器时钟为Fosc
    TMOD = 0x00;                        //16位重装载定时器/计数器

    //Tim0计算时间     = (65536 - 0xFFBD) * (1 / (Fosc /Timer分频系数))
    //                = 67 / (16000000 / 12)
    //                = 50us

    //定时1ms
    //反推初值     = 65536 - ((50/10000000) / (1/(Fosc / Timer分频系数)))
    //               = 65536 - ((50/10000000) / (1/(16000000 / 12)))
    //            = 65536 - 67
    //            = 0xFFBD
    
    TH0 = 0xFF;
    TL0 = 0xBD;                            //T0定时时间50us
    IE |= 0x02;                            //打开T0中断
    TCON = 0x10;                        //使能T0
    
    EA = 1;                                //打开总中断
    
    sum = sum0 = sum1 = b1 = b2 = b3 = c1 = c2 = c3 = d = j = 0;    //标志位初始化
    
    while(1)
    {
        if(b2 == 0)    //判断头码是否正确,正确b2置1
        {
            if(P1_7 == 1)
            {
                c1 = 0;
                if((160 <= sum0)&&(sum0 <= 200))    b1 = 1,sum1 = 0;    //头码9ms低电平
                sum0 = 0;
            }
            if(P1_7 == 0)
            {
                if(c1 == 0)    sum0 = 0,c1 = 1;
                if((70 <= sum1)&&(sum1 <= 110)&&(b1 == 1))    b1 = 0,b2 = 1,sum1 = 0;    //头码4.5ms高电平
                if((34 <= sum1)&&(sum1 <= 54)&&(b1 == 1))    b1 = 0,b2 = 0,b3 = 1,sum1 = 0;
            }
        }
        if(b2 == 1)
        {
            if(P1_7 == 0)
            {
                if(j <= 31)
                {
                    if((18 <= sum)&&(sum <= 27))    d = 0,c3 = 1,j++;
                    else if ((40 <= sum)&&(sum <= 50))    d = 1,c3 = 1,j++;
                }
            }
            if(c3 == 1)
            {
                if(d == 0)    number |= 0x00;
                else if(d == 1)    number |= 0x01;
                if(j <= 31)    number = number<<1;
                c3 = 0;
                c2 = 0;
                sum = 0;
            }
            if(j == 32)        //串码判断结束
            {
                b1 = b2 = 0;
                c1 = c2 = c3 = 0;
                sum = sum0 = sum1 = 0;
                j = 0;
                
                switch(number)
                {
                    case 0x01FE48B7: number_out = 1;break;    //判断按键按下
                    case 0X01FE58A7: number_out = 2;break;
                    case 0x01FE7887: number_out = 3;break;
                    default:number_out = 0;break;
                }
                number = 0;
            }
        }
        else if((b3 == 1)&&(P1_7 == 1))    //检测到按键重复的编码
        {
            c1 = 0;
            if((10 <= sum0)&&(sum0 <= 17))    P1_4 = 1;    //上次按键长按了,灯3亮
            sum0 = 0;
        }

        if(number_out == 1)    P1_6 = 1;    //按键1被按下,灯1亮
        if(number_out == 2)    P1_5 = 1;    //按键2被按下,灯2亮
        if(number_out == 3)    P1_4 = P1_5 = P1_6 = 0;    //按键3被按下,三个灯灭
        
    }
}
void TIMER0_Rpt(void) interrupt 1    //T0中断,每50us一次
{
    if(P1_7 == 1)    sum1++;
    else    sum0++;
    if(b2 == 1) sum++;
}

代码不是很精简,用的标志位也很多,是因为这个代码我没有用while,需要大家注意的是,我们必须在接收的信号为高/低时去判断上次低/高电平的持续时间,如果持续时间不符合要求,要把sumx置0,其他也没什么要注意的了。

三、NEC发送部分


大家可以明显看到发送的信号中:原本应该是持续高电平的地方变成了“脉冲”。

先说一下,红外发送器在未发送的时候单片机给的是低电平,并且发送出去的信号与上面接收进来的信号高低电平是相反的,因为我这边IO口是过了一个三极管推挽给红外发射管的。就是说如果发送出去是低电平,接收管输出的是高电平。那么每个阶段的间隔时间就应当按照第二章接收部分一样来设定才行。

但是有一点是必须要注意的:当发送器发送高电平时,实际上发送的是38khz的载波,也就是周期约为26.31us(我程序里是26.25us),占空比为1/3(或1/4,有时候可以2/3)的波:

红框框的一整个才是一个“0”的信号,大家可以看到,发送载波的时候一个周期(26.25us)内高电平的时间约为8us。

但是我的定时器周期为26.25us,我要如何在26.25us中让信号出8us的高电平,然后再变为低电平呢?
用中断,详细可以看一下程序的定时器周期部分:

#define    ALLOCATE_EXTERN
#include "HC89F303B.h"

void Delay_us(unsigned int fui_i);

unsigned int sum,sum0,sum1,sum3;
unsigned char b1,b2,b3,c1,c2,c3,c4,c5,c6,c7,c8,d,e,j;
unsigned long number,number_out;
void main()
{
/************************************系统初始化****************************************/
    CLKSWR = 0x51;                        //选择内部高频RC为系统时钟,内部高频RC 2分频,Fosc=16MHz
    CLKDIV = 0x01;                        //Fosc 1分频得到Fcpu,Fcpu=16MHz 
/**********************************相关配置初始化**************************************/
    P1M6 = 0xC1;
    P1M5 = 0xC1;
    P1M4 = 0xC1;
    
    P2M1 = 0xC1;        //P21输出
    
    P1M7 = 0x61;                        //P17非斯密特带上拉输入;
    
    P3M5 = 0x61;        //按键1    低电平触发
    P2M4 = 0x61;        //按键2
    P0M2 = 0x61;        //按键3
    
/**********************************TIM0配置初始化**************************************/
    TCON1 = 0x00;                        //Tx0定时器时钟为Fosc
    TMOD = 0x00;                        //16位重装载定时器/计数器

    //Tim0计算时间     = (65536 - 0xFFBD) * (1 / (Fosc /Timer分频系数))
    //                = 35 / (16000000 / 12)
    //                = 26.25us

    //定时1ms
    //反推初值     = 65536 - ((26.25/1000000) / (1/(Fosc / Timer分频系数)))
    //               = 65536 - ((26.25/1000000) / (1/(16000000 / 12)))
    //            = 65536 - 35
    //            = 0xFFDD
    
    TH0 = 0xFF;
    TL0 = 0xDD;                            //T0定时时间26.25us
    IE |= 0x02;                            //打开T0中断
    TCON = 0x10;                        //使能T0
    
    EA = 1;                                //打开总中断
    
    sum = sum0 = sum1 = b1 = b2 = b3 = c1 = c2 = c3 = c4 = d = j = 0;    //标志位初始化
    number = number_out = 0;
    while(1)
    {
        if(P3_5 == 0) j = 1;
        else if(P2_4 == 0) j = 2;
        else if(P0_2 == 0) j = 3;
        else j = 0;
        switch(j)
        {
            case 1:    number = 0x01FE48B7;break;
            case 2:    number = 0X01FE58A7;break;
            case 3:    number = 0x01FE7887;break;
            default:    number = 0;break;
        }
        while(number != 0)
        {
            if(c5 == 1)    c4 = 2;//执行过一次后判断
            number_out = number;
            if(c4 == 0)
            {
                if(b1 == 0)
                {
                    if(c2 == 0)    sum1 = 0,c2 = 1;
                    if(c3 == 0)    e = 1,sum0 = 0;
                    else e = 0;
                    if(sum1 >= 342)    c3 = 1;//9ms之后
                    if(sum0 >= 171)    b1 = 1,c3 = 0,c2 = 0,sum1 = sum0 = 0,e = 1,j = 31;
                }
                else
                {
                    if(b2 == 0)
                    {
                        if((sum1 >= 21)&&(c3 == 0))    c3 = 1,sum1 = 0,e = 0;    //高电平之后
                        else if(c3 == 1)
                        {
                            if(j != 0)    d = (number >> j) & 0X00000001;
                            else d = number & 0X00000001;
                            b2 = 1;
                            sum0 = 0;
                        }else ;
                    }
                    else if(b2 == 1)
                    {
                        e = 0;
                        if(sum0 >= (21+42*d))
                        {
                            b2 = 0;
                            if(j == 0)    c4 = 1,sum0 = sum1 = 0;
                            j--;
                            sum1 = 0;
                            sum0 = 0;
                            c3 = 0;
                            e = 1;
                        }
                    }
                }
            }
            else if(c4 == 1)
            {
                e = 1;
                if(sum1 >= 21)    //一串码结束,给标志位计时。
                {
                    e = 0;
                    number = 0;
                    c5 = 1;
                    c2 = 0;
                    c3 = 0;
                }
            }
            else if(c4 == 2)    //长按的函数
            {
                if(c2 == 0)    e = 0,c2 = 1;
                if(sum0 >= (1600 + c6*2663))        //时间判定到
                {
                    e = 1;
                    c3 = 1;        
                    sum1 = 0;                        
                }
                if(c3 == 1)                //开始重复码
                {
                    if(c7 == 0)    sum1 = 0,c7 = 1;
                    if(c8 == 0)    e = 1,sum0 = 0;
                    else e = 0;
                    if(sum1 >= 342)    c8 = 1;//9ms之后
                    if(sum0 >= 86)    c8 = 0,c7 = 0,c3 = 2,sum1 = 0,e = 1;    //一段时间的低电平之后
                }
                if((c3 == 2)&&(sum1 >= 21)) number = 0,e = 0,c6 = 1;
            }
        }
    }
}


void TIMER0_Rpt(void) interrupt 1    //T0中断,每26.25us一次
{
    if(e == 1)//高电平
    {
        sum1++;    
        P2_1 = 1;
        Delay_us(9);        //高电平时间在7.750us(周期26.250us)~8us(周期26.5us)之间 占空比约为1/3
        P2_1 = 0;
    }
    else sum0++,P2_1 = 0;
    
    if(c5 == 1) sum3++;
    if((P3_5 == 0)||(P2_4 == 0)||(P0_2 == 0))    sum++;
    if((P3_5 != 0)&&(P2_4 != 0)&&(P0_2 != 0))    //当按键全部不按下时,复位所有标志位
    {
        e = 0;
        b1 = b2 = b3 = c1 = c2 = c3 = c4 = c5 = c6 = c7 = c8 = d = 0;
        number = 0;
        j = 31;
    }
} 

void Delay_us(unsigned int fui_i)    //依cpu频率而定,fui_1 = 1时,延时约为1~2us
{
    while(fui_i--);    
}


也是为了避免while……所以用了很多标志位(整个程序里就一个似乎必要的while),大家看看就好,不要学,很憨的,用while会简单很多。

其实到这里就已经结束了,但是我还有几句话想说.....

刚写这个程序的时候,我问身边的老工程师,他们说这个代码只能用二十几us的定时器写,包括之后一年我又写过一个RFID卡检波的类似程序,也是用的几十us的定时器的。

实际上这个方法在现在已经不适用了,现在便宜的单片机已经有输入捕获功能了。所以当你在2025年或之后看到这篇文章,请你如果有时间,一定要研究一下你所用的单片机的输入捕获功能,它可以在每次进入捕获中断的时候直接获取到上次捕获与这次捕获的间隔时间(单位是运行周期)。然后就可以让这种检波程序的成功率实现质的飞跃。

我前几周已经在STC8,一颗5毛钱、22.1Mhz主频的单片机上实现对于100Khz、100:1分辨率的高精度检波了,这真的让我不由得感叹科技的进步。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐