Android & 单片机红外开发 返回首页

发表于 2018-07-03

效果展示

单片机接收显示发送的用户码和数据码

IRCode.gif

手机控制单片机 LED

IRLED.gif

红外简介

红外遥控技术是红外技术、红外通信技术和遥控技术的结合。红外遥控技术一般采用红外光波段内的近红外线,波长在 0.75 ~ 1.5μm 之间。由于红外线的波长较短,对障碍物的衍射能力较差,无法穿透墙壁,所以红外遥控术更适合应用在短距离直线控制的场合,也正是这样,放置在不同房间的家用电器可使用通用的遥控器而不会产生相互干扰。红外遥控所需传输的数据量较小,一般仅为几个至几十个字节的控制码,传输距离一般小于 10 米,广泛应用于电视机、机顶盒、DVD 播放器、功放等家用电器的遥控。通用红外遥控系统主要由发射和接收两大部分组成。发射部分包括单片机芯片或红外遥控发射专用芯片实现编码和调制,红外发射电路实现发射;接收部分包括一体化红外接收头电路实现接收和解调,单片机芯片实现解码。红外遥控发射专用芯片非常多,编码及调制频率也不完全一样。

红外发射原理简介

红外遥控二进制信号的编码。 红外遥控器发射的信号由一串「0」和「1」的二进制代码组成,不同的芯片对「0」和「1」的编码有所不同,通常有曼彻斯特 (Manchester) 编码和脉冲宽度编码 (PWM)。家用电器使用的红外遥控器绝大部分都是脉冲宽度编码,如下图所示。

PWMCode.png

红外遥控二进制信号的调制。 二进制信号的调制由发送单片机芯片或红外遥控发射专用芯片来完成,把编码后的二进制信号调制成频率为 38kHz 的间断脉冲串,相当于用二进制信号的编码乘以频率为38kHz 的脉冲信号得到的间断脉冲串,即是调制后用于红外发射二极管发送的信号。通用红外遥控器里面常用的红外遥控发射专用芯片载波频率为 38kHz,这是由发射端所使用的 455kHz 陶瓷晶振来决定的。在发射端对晶振进行整数分频,分频系数一般取 12 所以 455kHz ÷ 12 ≈ 37.9kHz ≈ 38kHz。

红外接收原理简介

红外遥控的接收与解调。 红外遥控接收采用一体化红外接收头,它将红外接收二极管、放大、解调、整形等电路集成在一起,具有体积小,抗干扰能力强等优点。红外接收头的封装主要有两种:一种采用铁皮屏蔽、一种是塑料封装。均有三只引脚,即电源正 (VDD) 电源负 (GND) 和数据输出 (VO 或 OUT)。红外接收头的引脚排列因型号不同而不尽相同,可参考厂家的使用说明。红外接收典型应用电路如下图所示。

IRReceiver.png

一体化红外接收头内部电路包括红外监测二极管、放大器、限幅器、带通滤波器、积分电路、比较器等。红外监测二极管监测到红信号,然后把信号送到放大器和限幅器,限幅器把脉冲幅度控制在一定的水平,而不论外发射器和接收器的距离远近。交流信号进入带通滤波器,带通滤波器可以通过 30kHz 到 60kHz 的副载波,通过解调电路和积分电路进入比较器,比较器输出高低电平,还原出发射的信号波形。红外接收头的信号输出端接单片机的外部中断 INT 引脚,单片机外部中断 INT 在红外脉冲下降沿时产生中断。

红外遥控的解码。 二进制信号的解码由接收单片机来完成,它把红外接收头送来的二进制编码波形通过解码,还原出发送端发送的数据。具体流程是单片机在中断期间启动定时器进行计数,直到下一个负脉冲到来将计数结果取出处理。根据其时长来判断信号「0」或「1」,从而还原发送的二进制信号。

NEC 编码协议

在红外开发中,可能最重要的就是发送二进制信号的编码协议了,各个厂家所使用的编码协议不同,所以遥控器也不能相互控制,而即使编码协议相同,使用的用户码不同,也不能被接收端接受。所以类似万能遥控器这种应用,第一个要解决的问题就是各类家用电器,各类厂家所使用的编码协议以及所使用的用户码等。但各类电器众多,这种应用是很难兼容全的,有的大厂家会将自家产品的编码及对应功能键的数据序列公布在网上,方便其他开发者开发,至于其他没有公布的,可能就要使用红外解码仪来破解它所使用的协议以及各功能键对应的数据码等。但这些都是题外话,回到主题,在日常家用电器中,NEC 编码是比较常见的一种编码协议,此次应用就是使用 NEC 编码协议来进行开发的。

通用红外遥控器发出的一串二进制代码按功能可以分为分「引导码、用户码 16 位、数据码 8 位、数据反码 8 位和结束位」,编码共占 32 位,如下图所示。

NEC.png

其中引导码由一个 9ms 的 38kHz 载波起始码和一个 4.5ms 的无载波低电平结果码组成。用户码由低 8 位和高 8 位组成 (用户码高八位和低八位可采用原码与反码的方式,可用于纠错,但也可直接是 16 位的原码方式),不同的遥控器有不同的用户码,避免不同设备产生干扰,用户码又称为地址码或系统码。数据码采用原码和反码方式重复发送,编码时用于对数据的纠错,遥控器发射编码时,低位在前,高位在后。结束位是 0.56ms 的 38kHz 载波。

而其中的「0」码由 0.56ms 的 38kHz 载波和 0.56ms 的无载波低电平组合而成,脉冲宽度为 1.125ms,「1」码由 0.56ms 的 38kHz 载波和 1.69ms 的无载波低电平组合而成,脉冲宽度为 2.25ms,如下图所示。

NECCode.png

Android 端红外发射

对于 Android 上的红外开发,其实「挺简单的」,因为接口就那么几个方法,重点还是编码协议,还是侧重于单片机接收端的开发。Android 上红外是在 4.4 后才开始支持的,其对应的管理类为 ConsumerIrManager,它对应有三个方法:

getCarrierFrequencies():用于查询获取红外发射器支持可用的载波频率范围

hasIrEmitter():用于检查该设备是否具有红外发射器

transmit(int carrierFrequency, int[] pattern):用于发射红外信号,第一个参数为发射的载波频率,第二个参数为红外信号的模式序列

本次应用设定用户码为 2333,采用 NEC 编码协议,其主要代码如下:

@RequiresApi(api = Build.VERSION_CODES.KITKAT)

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    @BindViews({R.id.btn_led1, R.id.btn_led2, R.id.btn_led3, R.id.btn_led4, R.id.btn_led5, R.id.btn_led6})
    List<Button> leds;

    private ConsumerIrManager consumerIrManager;

    // NEC 协议固定引导码 9000,4500 以及结束码 560,20000
    // 用户码 2333 ==> 0010 0011 0011 0011 =(逆序)=> 1100 0100 1100 1100

    // 数据码 01 ==> 0000 0001 =(逆序)=> 1000 0000
    private int[] pattern1 = {
            9000,4500,
            560,1690, 560,1690, 560,560, 560,560, 560,560, 560,1690, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,560, 560,1690, 560,1690, 560,560, 560,560,
            560,1690, 560,560, 560,560, 560,560, 560,560, 560,560, 560,560, 560,560,
            560,560, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690,
            560,20000
    };

    // 数据码 02 ==> 0000 0010 =(逆序)=> 0100 0000
    private int[] pattern2 = {
            9000,4500,
            560,1690, 560,1690, 560,560, 560,560, 560,560, 560,1690, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,560, 560,1690, 560,1690, 560,560, 560,560,
            560,560, 560,1690, 560,560, 560,560, 560,560, 560,560, 560,560, 560,560,
            560,1690, 560,560, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690,
            560,20000
    };

    // 数据码 03 ==> 0000 0011 =(逆序)=> 1100 0000
    private int[] pattern3 = {
            9000,4500,
            560,1690, 560,1690, 560,560, 560,560, 560,560, 560,1690, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,560, 560,1690, 560,1690, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,560, 560,560, 560,560, 560,560, 560,560,
            560,560, 560,560, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690,
            560,20000
    };

    // 数据码 04 ==> 0000 0100 =(逆序)=> 0010 0000
    private int[] pattern4 = {
            9000,4500,
            560,1690, 560,1690, 560,560, 560,560, 560,560, 560,1690, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,560, 560,1690, 560,1690, 560,560, 560,560,
            560,560, 560,560, 560,1690, 560,560, 560,560, 560,560, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690,
            560,20000
    };

    // 数据码 05 ==> 0000 0101 =(逆序)=> 1010 0000
    private int[] pattern5 = {
            9000,4500,
            560,1690, 560,1690, 560,560, 560,560, 560,560, 560,1690, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,560, 560,1690, 560,1690, 560,560, 560,560,
            560,1690, 560,560, 560,1690, 560,560, 560,560, 560,560, 560,560, 560,560,
            560,560, 560,1690, 560,560, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690,
            560,20000
    };

    // 数据码 06 ==> 0000 0110 =(逆序)=> 0110 0000
    private int[] pattern6 = {
            9000,4500,
            560,1690, 560,1690, 560,560, 560,560, 560,560, 560,1690, 560,560, 560,560,
            560,1690, 560,1690, 560,560, 560,560, 560,1690, 560,1690, 560,560, 560,560,
            560,560, 560,1690, 560,1690, 560,560, 560,560, 560,560, 560,560, 560,560,
            560,1690, 560,560, 560,560, 560,1690, 560,1690, 560,1690, 560,1690, 560,1690,
            560,20000
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        initInfrared();
        for (Button led: leds) {
            led.setOnClickListener(this);
        }
    }

    @SuppressLint("DefaultLocale")
    private void initInfrared() {
        consumerIrManager = (ConsumerIrManager) getSystemService(Context.CONSUMER_IR_SERVICE);
        assert consumerIrManager != null;
        if (!consumerIrManager.hasIrEmitter()) {
            Toast.makeText(this, "当前手机不支持红外遥控", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "红外设备就绪", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_led1:
                sendMessage(pattern1);
                break;
            case R.id.btn_led2:
                sendMessage(pattern2);
                break;
            case R.id.btn_led3:
                sendMessage(pattern3);
                break;
            case R.id.btn_led4:
                sendMessage(pattern4);
                break;
            case R.id.btn_led5:
                sendMessage(pattern5);
                break;
            case R.id.btn_led6:
                sendMessage(pattern6);
                break;
        }
    }

    private void sendMessage(int[] pattern) {
        consumerIrManager.transmit(38000, pattern);
        Toast.makeText(this, "发射完成", Toast.LENGTH_SHORT).show();
    }
}

对代码进行简单的分析,其中的 pattern 为红外信号序列的模式,其数值格式对应编码协议,根据协议来设定序列值。比如 NEC 编码协议,首先需要一个引导码表示信号的开始,先是一个 9ms 的 38kHz 载波起始码,再是一个 4.5ms 的无载波低电平结果码,对应 pattern 中,数值单位是 μs,所以是一个 9000 的高电平,再是一个 4500 的低电平。同理「0」码为一个 560 的高电平和一个 560 的低电平,「1」码为一个 560 的高电平和一个 1690 的低电平。而因为信号序列的发送,到接收端,其顺序刚好是相反的 (高电平变低电平,低电平变高电平),这里发送的信号也需要做逆序处理,以一个字节 (对应 8 位) 为一组。最后以 560 的高电平结束。

单片机端红外接收

至于单片机接收端,不同型号的单片机肯定有所不同,但其核心原理和逻辑还是相同的,只需要参考自己单片机的数据手册做相应变化即可,此次使用的是普中的单片机开发试验仪。根据手册可知,其红外接收器的输出端和中断是相连的,以下降沿方式触发。程序的流程图如下:

CFlowChart.png

主程序首先为初始化逻辑,然后就是不断循环从数据缓冲区中取出数据,并作相应的响应。而中断程序,则是接收信号,如果成功,就将其存入缓冲区中,以供主程序做出响应。为了直观展示效果 (因为这个单片机中的数码管和 LED 的端口是串联在一起的,所以不能同时展示数据值和 LED 的功能),实验中写了两份代码,一个用于展示接收到来自手机发射的用户码和数据码的值,另一个用于展示控制单片机 LED 的功能。下面以显示用户码和数据码的代码为例,其完整代码如下:

#include "reg52.h"

typedef unsigned char u8;
typedef unsigned int u16;

sbit IRIN = P3^2; // 红外输出口
u8 IrValue[6];  // 数据数组
u8 Time;        // 计时暂存
u8 DisplayData[8];  // 显示数据缓存

u8 code smgduan[17]={
0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0X76};
// 0、1、2、3、4、5、6、7、8、9、A、b、C、d、E、F、H的显示码

// 延时函数,i=1时,大约延时10us
void delay(u16 i) {
    while(i--);
}

// 数码管显示函数
void DigDisplay() {

    u8 i;
    for(i=0; i<8; i++) {
        // 位选,选择点亮的数码管
        switch(i) {
            case(0):
                P2 = 0; break; // 显示第 1 位
            case(1):
                P2 = 1; break; // 显示第 2 位
            case(2):
                P2 = 2; break; // 显示第 3 位
            case(3):
                P2 = 3; break; // 显示第 4 位
            case(4):
                P2 = 4; break; // 显示第 5 位
            case(5):
                P2 = 5; break; // 显示第 6 位
            case(6):
                P2 = 6; break; // 显示第 7 位
            case(7):
                P2 = 7; break; // 显示第 8 位
        }
        P0 = DisplayData[i]; // 发送数据
        delay(100); // 间隔一段时间扫描
        P0 = 0x00;  // 消隐
    }
}


// 初始化红外线接收
void IrInit() {

    IT0=1;  // 下降沿触发
    EX0=1;  // 打开中断0允许
    EA=1;   // 打开总中断

    IRIN=1; // 初始化端口
}


// 主函数
void main() {

    IrInit();
    while(1) {
        DisplayData[0] = smgduan[IrValue[0]/16];    // 用户码高位高半字节
        DisplayData[1] = smgduan[IrValue[0]%16];    // 用户码高位低半字节
        DisplayData[2] = smgduan[IrValue[1]/16];    // 用户码低位高半字节
        DisplayData[3] = smgduan[IrValue[1]%16];    // 用户码低位低半字节
        DisplayData[5] = smgduan[IrValue[2]/16];    // 数据码高半字节
        DisplayData[6] = smgduan[IrValue[2]%16];    // 数据码低半字节
        DisplayData[7] = smgduan[16];
        DigDisplay();
    }
}

// 读取红外数值的中断函数
void ReadIr() interrupt 0 {

    u8 j,k;
    u16 err;
    Time = 0;
    delay(700); // 7ms
    // 确认是否真的接收到正确的信号
    if(IRIN==0) {

        err = 1000; // 1000 * 10μs = 10ms,超过说明接收到错误的信号

        // 当两个条件都为真是循环,如果有一个条件为假的时候跳出循环,免得程序出错的时侯,程序死在这里
        // 等待前面 9ms 的低电平过去
        while((IRIN==0) && (err>0)) {
            delay(1);
            err--;
        }

        // 如果正确等到9ms低电平
        if(IRIN==1)	{
            err=500;
            // 等待 4.5ms 的起始高电平过去
            while((IRIN==1) && (err>0)) {
                delay(1);
                err--;
            }
            // 共有 4 组数据
            for(k=0; k<4; k++) {
                // 接收一组数据
                for(j=0; j<8; j++) {

                    err = 60;
                    // 等待信号前面的 560μs 低电平过去
                    while((IRIN==0) && (err>0)) {
                        delay(1);
                        err--;
                    }
                    err = 500;
                    // 计算高电平的时间长度
                    while((IRIN==1) && (err>0)) {
                        delay(10); // 0.1ms
                        Time++;
                        err--;
                        if(Time>30) {
                            return;
                        }
                    }
                    IrValue[k]>>=1; // k 表示第几组数据
                    // 如果高电平出现大于 565μs,那么是 1
                    if(Time>=8) {
                        IrValue[k]|=0x80;
                    }
                    Time=0; // 用完时间要重新赋值
                }
            }
        }
        // 利用反码判断数据码是否发生错误,如果错误则舍弃
        if(IrValue[2]!=~IrValue[3]) {
            return;
        }
    }
}

简单地解释一下,P2 口控制数码管的位选,P0 口控制数码管的段选,主程序从 IrValue 数据数组中获得相应位的数值并转换为段选码存入显示缓存数值的对应位。然后将各个数码管的数值显示出来,其循环不断的从第一个数码管刷新显示到第八个数码管,如此反复,因为刷新显示的时间很短,而人眼有视觉的滞留效应,所以看起来八个数码管是「静态显示的」。其次中断中,因为低电平的时长都是相同的,「0」码和「1」码不同的是高电平的时长 (发送端的载波信号,原本是高电平相同,低电平时长不同,但接收端刚好是反相的),所以就粗略地认为,高电平大于 565μs 的为「1」码,低于其值的为「0」码,当 32 位数据接收完后,利用数据码的反码来简单的判断数据码是否发生错误。

而对于 LED 的程序,只需修改主函数的逻辑即可,首先可以判断用户码是否符合预订设置的用户码 (项目中我没有判断用户码),其次再根据取得的数据码,控制 LED 的亮灭等。对于三个项目的代码,已经上传到 GitHub,有需要的可以在文末链接中自取。

写在最后

其实弄懂了红外的原理后,会发现 (除去底层硬件自己实现) 还是挺简单的,应用层的编写无非就是需要掌握通信的编码协议。拓展思考后,发现给单片机加个 WIFI 模块,或者买个树莓派啥的,完全可以实现在外面用手机控制家里的电器 (使用红外控制的),比如空调,这就简单的实现了所谓的「智能家居」?

扩展阅读,参考文档

GitHub 项目地址

脉冲宽度编码

NEC Infrared Transmission Protocol

ConsumerIrManager


如果你觉得本文对你有帮助,不妨请我喝瓶葡萄味芬达


显示评论