参考资料:https://www.bilibili.com/video/BV1Mb411e7re?p=37

一、红外遥控原理

1. 红外遥控简介

  • 红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出

  • 通信方式:单工,异步

  • 红外LED波长:940nm

  • 通信协议标准:NEC标准

红外接收头:

单片机学习(十)红外遥控与外部中断_红外

2. 红外LED和接收头的硬件电路

LED硬件电路

LED硬件电路有以下两种:

单片机学习(十)红外遥控与外部中断_外部中断_02单片机学习(十)红外遥控与外部中断_红外_03

其中第一种的信号形式是这样的:

单片机学习(十)红外遥控与外部中断_初值_04

即LED或者是完全不发光,或者是以38KHz的频率进行闪烁发出红外光

而第二种则需要软件控制IN的输入。

接收头硬件电路

单片机学习(十)红外遥控与外部中断_外部中断_05单片机学习(十)红外遥控与外部中断_红外_06

接收头会对接收到的红外光进行一定的过滤工作,然后输出到P32引脚上。

3. 基本发送与接收

红外LED的三种发送状态下接收头的输出

  • 空闲状态:红外LED不亮,接收头输出高电平
  • 发送低电平:红外LED以38KHz频率闪烁发光,接收头输出低电平
  • 发送高电平:红外LED不亮,接收头输出高电平

例如:

单片机学习(十)红外遥控与外部中断_初值_07

4. NEC编码

红外接收头接收到的信号调制输出的结果应符合红外NEC协议

单片机学习(十)红外遥控与外部中断_下降沿_08

输出信号的格式

\[Address+\overline{Address}+Command+\overline{Command} \]

每个部分1个字节(8位),而第一和第二,第三和第四个部分互为反码是为了进行数据的校验

信号的三种情况

接收到的信号分为以下三种情况

  1. 信息头,图中的红色信号,提示将要发送信号
  2. 信息体,图中的蓝色信号,真正需要传输的内容
  3. 重复信号,图中的绿色信号,代表前面发送的内容重复发送

编码任务

我们的任务就是编写接收信号的驱动程序,在代码层读取接收到的信号,将其转化为AddressCommand保存起来(将信号转为数字)

我们可以观察将得到的信号转化为对应的编码的一组例子:

单片机学习(十)红外遥控与外部中断_下降沿_09

可以发现点击不同的按键,接收头得到的键码是不一样的。

5. 遥控器的键码

单片机学习(十)红外遥控与外部中断_#define_10

我们可以参考这个图上的编码来对遥控器的输入进行对应的响应。

二、外部中断

1. 51单片机的外部中断资源

  • STC89C52有4个外部中断

  • STC89C52的外部中断有两种触发方式:

    • 下降沿触发
    • 低电平触发
  • 中断号:
    单片机学习(十)红外遥控与外部中断_初值_11

  • 外部中断引脚
    单片机学习(十)红外遥控与外部中断_外部中断_12
    这里只有两个引脚:P32P33,所以实际上是只有两个外部中断

我们使用下降沿触发中断,然后获取到前后两个下降沿相距的时间,然后通过这个时间推断出是信号头,还是信号体(0,1)以及repeat

2. 中断通路

单片机学习(十)红外遥控与外部中断_#define_13三、编码实现

1. 外部中断的中断通路配置

按照上面的配置图,我们可以得到以下的配置:

void Int0_Init() {
    // 修改IT0可以控制是下降沿触发(1)还是低电平触发(0)
    IT0 = 1;
    IE0 = 0;
    EX0 = 1;
    EA = 1;
    PX0 = 1;
}

然后我们可以编写一小段程序测试通路是否配置完成:

unsigned char Num;

void main() {
    LCD_Init();
    // 初始化,配置外部中断通路
    Int0_Init();
    while (1) {
        LCD_ShowNum(1, 1, Num, 3);
    }
}

// 外部中断
void Int0_Routine() interrupt 0 {
    Num++;
}

因为我们的独立按钮3也是连接在P32引脚上的,因此当我们点击按钮3时也会触发外部中断,故我们可以看到当点击按钮3时,会发生Num++

单片机学习(十)红外遥控与外部中断_下降沿_14单片机学习(十)红外遥控与外部中断_初值_15

这说明外部中断配置成功,我们把外部中断初始化也抽象出一个模块Int0

// Int0.c
void Int0_Init() {
    // 修改IT0可以控制是下降沿触发(1)还是低电平触发(0)
    IT0 = 1;
    IE0 = 0;
    EX0 = 1;
    EA = 1;
    PX0 = 1;
}

/*
// 外部中断
void Int0_Routine() interrupt 0 {
    
}
*/

2. 定时器模块

此时我们需要将Timer0作为我们的计时器,与之前不同的是我们此时不是用它来发起中断了,而是专门用它来记录时间

// Timer0.c
void Timer0_Init(void)
{
    TMOD &= 0xF0;  //设置定时器模式
    TMOD |= 0x01;  //设置定时器模式
    TL0 = 0;       //设置定时初值
    TH0 = 0;       //设置定时初值
    TF0 = 0;       //清除TF0标志
    TR0 = 0;       //定时器0不计时
}
// 设置计数器的值
void Timer0_SetCouter(unsigned int value) {
    TH0 = value / 256;
    TL0 = value % 256;
}
// 获取当前计数器的值
unsigned int Timer0_GetCounter() {
    return (TH0 << 8) | TL0;
}
// 设置是否开始计时,0为停止,1为开始
void Timer0_Run(unsigned char flag) {
	TR0 = flag;
}

因为我的开发板上的晶振是11.0592MHz的,因此每隔12/(11.0592*10^6)s,即1.085μs计数器即会加一,我们到时需要使用这个时间来计算各个信号时间需要经过的计数器记录数

3. 编写接收器模块

接收器模块需要依赖Timer0模块和Int0模块,首先是Init函数:

void IR_Init() {
    Timer0_Init();
    Int0_Init();
}

我们首先定义一些全局变量用于接收信息或标志信息:

// 接收时间的变量
unsigned int IR_Time;
// 状态变量
unsigned char IR_State;

// 存储4个字节的数组
unsigned char IR_Data[4];
// 数组指针(下标)
unsigned char IR_pData;

// 标志数据是否读取完成
unsigned char IR_DataFlag;
// 标志数据是否读取到Repeat
unsigned char IR_RepeatFlag;
// 存储Address信息
unsigned char IR_Address;
// 存储Command信息
unsigned char IR_Command;

然后接收信息的过程使用一个有限状态机来完成,这个状态机一共有三个状态:

  1. 开始状态,接收到第一个下降沿开始计时,转为状态2
  2. 接收信息头状态,当再接收到一个信号下降沿时计算经过的时间:
    • 经过时间为9ms+4.5ms=13500μs,判断为开始信号,转为状态3【接收信息体状态】
    • 经过时间为9ms+2.25ms=11250μs,判断为repeat信号,转为状态1【开始状态】
    • 否则一直维持状态2【接收信息头状态】
  3. 接收信息体状态,每接收到一个下降沿根据经过的时间判断是0还是1,并将这些数据存储起来,当接收完4*8=32个bit之后转变为状态1【开始状态】,若接收到的数据非01(数据异常),则返回状态2【接收信息头状态】

状态转换图如下图所示:

单片机学习(十)红外遥控与外部中断_下降沿_16

因为我们使用的开发板上是11.0592MHz的晶振,故每隔1.085μs计数器会加一,我们需要事先计算出开始信号,repeat信号,信息体中0和1的信号时间:

  • 信号头:9ms+4.5ms=13500μs,对应计时数:13500/1.085≈12442
  • repeat信号:9ms+2.25ms=11250μs,对应计时数:11250/1.085≈10368
  • 信息体:
    • 逻辑0:560μs+560μs=1120μs,对应计时数:1120/1.085≈1032
    • 逻辑1:560μs+1690μs=2250μs,对应计时数:2250/1.085≈2074

由此我们可以编写代码:

// 用于判断是否在输入是否在compare附近,offset是偏移量
u8 IR_AroundNum(u16 num, u16 compare, u16 offset) {
    return num >= (compare - offset) && num <= (compare + offset);
}
// 外部中断进入的程序
void Int0_Routine() interrupt 0 {
    // 1.开始状态
    if (IR_State == 0) {
        Timer0_SetCounter(0);
        Timer0_Run(1);
        IR_State = 1;
    } 
    // 2.接收信息头状态
    else if (IR_State == 1) {
        IR_Time = Timer0_GetCounter();
        Timer0_SetCounter(0);
        // 接收到开始信号9ms+4.5ms=13500μs
        if (IR_AroundNum(IR_Time, 12442, 500)) {
            P2 = 0;
            IR_State = 2;
        }
        // repeat:9ms+2.25ms=11250μs
        else if (IR_AroundNum(IR_Time, 10368, 500)) {
            IR_RepeatFlag = 1;
            // 以一帧为单位接收,接收完则返回0状态
            IR_State = 0;
        } 
        else {
            IR_State = 1;
        }
    } 
    // 3.接收信息体状态
    else if (IR_State == 2) {
        IR_Time = Timer0_GetCounter();
        Timer0_SetCounter(0);
        // 逻辑0:560μs+560μs=1120μs
        if (IR_AroundNum(IR_Time, 1032, 500)) {
            // 置零
            IR_Data[IR_pData / 8] &= ~(0x01 << (IR_pData % 8));
            IR_pData++;
        }
        // 逻辑1:560μs+1690μs=2250μs
        else if (IR_AroundNum(IR_Time, 2074, 500)) {
            // 置一
            IR_Data[IR_pData / 8] |= (0x01 << (IR_pData % 8));
            IR_pData++;
        }
        // 接收到异常信号转为接收信息头状态
        else {
            IR_pData = 0;
            IR_State = 1;
        }
        // 判断是否接收完32bit
        if(IR_pData>=32) {
            IR_pData = 0;
            // 数据校验
            if((IR_Data[0]==~IR_Data[1]) && (IR_Data[2]==~IR_Data[3])) {
                // 转存到IR_Address和IR_Command变量中
                IR_Address = IR_Data[0];
                IR_Command = IR_Data[2];
                IR_DataFlag = 1;
            }
            Timer0_Run(0);
            IR_State = 0;
        }
    }
}

代码中使用if-else条件判断来实现有限状态机,其实是一种耦合度比较高的实现方式,但是由于c语言难以实现面向对象,就只好暂时使用这样的实现了。

然后这个模块再暴露出去一些获取标志变量接收到的数据的函数:

u8 IR_GetDataFlag() {
    if(IR_DataFlag) {
        IR_DataFlag = 0;
        return 1;
    }
    return 0;
}

u8 IR_GetRepeat() {
    if(IR_RepeatFlag) {
        IR_RepeatFlag = 0;
        return 1;
    }
    return 0;
}

u8 IR_GetAddress() {
    return IR_Address;
}

u8 IR_GetCommand() {
    return IR_Command;
}

然后在main.c中编写程序测试一下功能:

unsigned char Num;
unsigned char Address;
unsigned char Command;

void main() {
    LCD_Init();
    IR_Init();
    
    LCD_ShowString(1,1,"ADDR");
    LCD_ShowString(1,7,"CMD");
    LCD_ShowString(1,12,"NUM");

    while (1) {
        if(IR_GetDataFlag() || IR_GetRepeat()) {
            Address = IR_GetAddress();
            Command = IR_GetCommand();
            LCD_ShowHexNum(2,1,Address,2);
            LCD_ShowHexNum(2,7,Command,2);
            // 0x15是VOL-
            if(Command == 0x15) {
                Num--;
            }
            // 0x09是VOL+
            else if(Command == 0x09) {
                Num++;
            }
            LCD_ShowHexNum(2,12,Num,3);
        }
    }
}

运行的结果是我们点击一个按键,在LCD1602中就会显示出对应的键码Address值,此外当我们点击VOL+VOL-时展示的Num也会发生变化:

单片机学习(十)红外遥控与外部中断_下降沿_17

为了使用方便,我们再在IR.h中使用宏定义定义一些按键:

#define IR_POWER        0x45
#define IR_MODE         0x46
#define IR_START_STOP   0x44
#define IR_PREVIOUS     0x40
#define IR_NEXT         0x43
#define IR_EQ           0x07
#define IR_VOL_MINUS    0x15
#define IR_VOL_ADD      0x09
#define IR_VOL_ADD      0x09
#define IR_RPT          0x19
#define IR_USD          0x0D
#define IR_NUM_0        0x16
#define IR_NUM_1        0x0C
#define IR_NUM_2        0x18
#define IR_NUM_3        0x5E
#define IR_NUM_4        0x08
#define IR_NUM_5        0x1C
#define IR_NUM_6        0x5A
#define IR_NUM_7        0x42
#define IR_NUM_8        0x52
#define IR_NUM_9        0x4A

这样我们即可在外部轻松调用接收器模块获取信息了。

4. 使用红外遥控控制电机运转

鉴于定时器0被红外接收模块用于计时了,因此若我们希望使用定时器实现电机的PWM调速,就只能使用定时器1了,模仿定时器0,我们很容易写出对应的模块代码:

// Timer1.c
void Timer1Init(void)//100微秒@11.0592MHz
{
    TMOD &= 0x0F;//设置定时器模式
    TMOD |= 0x10;//设置定时器模式
    TL1 = 0xA4;  //设置定时初值
    TH1 = 0xFF;  //设置定时初值
    TF1 = 0;     //清除TF1标志
    TR1 = 1;     //定时器1开始计时
    ET1 = 1;
    EA = 1;
    PT1 = 0;
}

/*定时器中断函数模板
void Timer1_Routine() interrupt 3
{
	static unsigned int T1Count;
	TL1 = 0xA4;  //设置定时初值
	TH1 = 0xFF;  //设置定时初值
	T1Count++;
	if(T1Count>=1000)
	{
		T1Count=0;
		
	}
}
*/

然后我们需要编写电机模块:

sbit Motor = P1 ^ 0;
unsigned char Counter = 0, Compare = 0;

void Motor_Init() {
    Timer1Init();
}
// 修改Compare进行调速
void Motor_SetPower(unsigned char power) {
    if (power < 0) {
        power = 0;
    } 
    else if (power > 100) {
        power = 100;
    }
    Compare = power;
}
// 使用风力等级进行调速,一共0~3四级
void Motor_SetPowerByLevelNum(unsigned char level) {
    if(level == 0) {
        Motor_SetPower(0);
    }
    else if(level == 1) {
        Motor_SetPower(60);
    }
    else if(level == 2) {
        Motor_SetPower(80);
    }
    else if(level == 3) {
        Motor_SetPower(100);
    }
}
// PWM调速
void Timer1_Routine() interrupt 3 {
    TL1 = 0xA4;//设置定时初值
    TH1 = 0xFF;//设置定时初值
    Counter++;
    Counter %= 100;
    if (Counter < Compare) {
        Motor = 1;
    } else {
        Motor = 0;
    }
}

main.c:

unsigned char Command;
unsigned char FanState;
unsigned char FanPower;

// 电机两个状态,关闭状态和开启状态
#define POWER_OFF 0
#define POWER_ON 1

void main() {
    LCD_Init();
    IR_Init();
    Motor_Init();
    
	// 初始是关闭状态
    LCD_ShowString(1, 1, "Fan State:");
    LCD_ShowString(1, 11, "OFF");
    FanState = POWER_OFF;
    FanPower = 0;// 初始风力等级为0

    while (1) {
        if (IR_GetDataFlag()) {
            Command = IR_GetCommand();
            // 关闭状态
            if (FanState == POWER_OFF) {
                // 点击POWER键后电机转为开启状态
                if (Command == IR_POWER) {
                    FanState = POWER_ON;
                    // 默认开启时风力为1级
                    FanPower = 1;
                    LCD_ShowString(1, 11, "ON ");
                    LCD_ShowString(2, 1, "Power:");
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
            } 
            // 开启状态
            else if (FanState == POWER_ON) {
                // 点击POWER键后电机转为关闭状态
                if (Command == IR_POWER) {
                    FanState = POWER_OFF;
                    FanPower = 0;
                    LCD_ShowString(1, 11, "OFF");
                    LCD_ShowString(2, 1, "       ");
                } 
                // 点击按钮1转为1挡
                else if (Command == IR_NUM_1) {
                    FanPower = 1;
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
                // 点击按钮2转为2挡
                else if (Command == IR_NUM_2) {
                    FanPower = 2;
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
                // 点击按钮3转为3挡
                else if (Command == IR_NUM_3) {
                    FanPower = 3;
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
            }
            // 设置电机档位
            Motor_SetPowerByLevelNum(FanPower);
        }
    }
}

运行效果:

单片机学习(十)红外遥控与外部中断_初值_18