参考教程:[17-1] 红外遥控(外部中断)_哔哩哔哩_bilibili
一、红外遥控介绍
1、概述
(1)红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出。
(2)红外遥控的通信方式:单工、异步。
(2)红外LED波长:940nm。
(3)通信协议标准:NEC标准。
2、硬件电路
(1)注意,在开发板原理图中独立按键K3也与P32连接,而P32同时也用于接收外部的信号。
(2)在下面会讲到外部中断,由于K3与P32连接,这就导致按下按键K3后就会产生一个外部中断(K3断开时P32处于高电平,按下按键K3后P32处于低电平,相当于给了一个下降沿,如果按住K3不放,就是一直处于低电平)。
3、基本发送与接收
(1)空闲状态:红外LED不亮,接收头输出高电平。
(2)发送低电平:红外LED以38KHz频率闪烁发光(规定特定频率的目的是抗干扰,否则容易被自然界的红外光影响,这个部分是在底层就封装好了的,下面提到的发送高电平其实落实到底层都是指不断闪烁的光,只是没在时序图上表现出来而已),接收头输出低电平。
(3)发送高电平:红外LED不亮,接收头输出高电平。
4、NEC编码
(1)发送Data时低位在前,高位在后(Data共32位)。
(2)地址码是为了区分不同(品牌)的遥控器。
(3)设置反码是为了验证Data在传输过程中有没有出现错误。
(4)发送Data时,前560us低电平,后560us高电平,表示发送数据0;前560us低电平,后1690us高电平,表示发送数据1。
(5)如果按住按键不放,会不断发送Repeat部分,直至松开按键。
(6)Start是起始标志。
5、遥控器键码
二、STC89C52的外部中断
1、STC89C52的外部中断资源
(1)STC89C52有4个外部中断,外部中断有下降沿触发和低电平触发两种触发方式。
(2)红外遥控必须通过外部中断处理,且由于信号可能转瞬即逝,外部中断的优先级必须很高。
2、外部中断寄存器
将IT0配置为1,那么外部中断0采取的就是下降沿触发(否则是低电平触发,低电平触发的意思是只要一直收到低电平信号,那么中断就会反复执行,而选择下降沿触发的话中断只会执行一次),IT1同理。
三、红外遥控应用实验
1、红外遥控的基本实现实验
本例需要液晶屏模块的代码,剩余缺失代码会在下面给出,将它们添加到项目中,然后进行编译,根据主函数中的注释调试(温馨提示:IR.c文件中有大量注释用来帮助理解NEC协议)。
①Int0.h文件:
#ifndef __Int0_H__
#define __Int0_H__
void Int0_Init();
#endif
②Int0.c文件:
#include <REGX52.H>
void Int0_Init()
{
IT0 = 1; //下降沿触发
IE0 = 0;
EX0 = 1; //打开中断
EA = 1; //打开中断
PX0 = 1; //外部中断给予高优先级(见原理图)
}
/*外部中断函数模板
void Int0_Routine() interrupt 0
{
}
*/
③Timer0.h文件:
#ifndef __Timer0_H__
#define __Timer0_H__
void Timer0_Init();
void Timer0_SetCounter(unsigned int Value);
unsigned int Timer0_GetCounter();
void Timer0_Run(unsigned char Flag);
#endif
④Timer0.c文件:
#include <REGX52.H>
/**
* @brief 定时器0初始化,100us@11.0592MHz
* @param 无
* @retval 无
*/
void Timer0_Init()
{
TMOD = 0x01;
TF0 = 0; //中断标志位初始化为0,计数产生溢出时该位被置为1,向CPU发出中断请求,CPU响应中断后该位被置为0
TR0 = 0; //定时器T0不计时
TL0 = 0x00; //设置定时初值
TH0 = 0x00; //设置定时初值
}
void Timer0_SetCounter(unsigned int Value)
{
TH0 = Value/256; //设置定时初值
TL0 = Value%256; //设置定时初值
}
unsigned int Timer0_GetCounter()
{
return (TH0 << 8)| TL0; //返回计时值
}
void Timer0_Run(unsigned char Flag)
{
TR0 = Flag; //启动计时
}
/* 定时器0函数中断模板
void Timer0_Routine() interrupt 1 //CPU响应中断后执行的函数
{
static unsigned int T0Count = 0; //定义计数器
T0Count++;
if(T0Count >= 10000) //每10000个中断信号(1秒)执行一次下面的代码段
{
T0Count = 0;
}
//每次中断结束都要重置计数单元
TL0 = 0xA4; //设置定时初值
TH0 = 0xFF; //设置定时初值
}
*/
⑤IR.h文件:
#ifndef __IR_H__
#define __IR_H__
//遥控器命令码
#define IR_POWER 0x45
#define IR_MODE 0x46
#define IR_MUTE 0x47
#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_0 0x16
#define IR_RPT 0x19
#define IR_USD 0x0D
#define IR_1 0x0C
#define IR_2 0x18
#define IR_3 0x5E
#define IR_4 0x08
#define IR_5 0x1C
#define IR_6 0x5A
#define IR_7 0x42
#define IR_8 0x52
#define IR_9 0x4A
void IR_Init();
unsigned char IR_GetDataFlag();
unsigned char IR_GetRepeatFlag();
unsigned char IR_GetAddress();
unsigned char IR_GetCommand();
#endif
⑥IR.c文件:
#include <REGX52.H>
#include "Timer0.h"
#include "Int0.h"
unsigned char IR_State;
unsigned int IR_Time;
unsigned char IR_Data[4]; //Data有32位,使用4个sizeof为1的数组存储
unsigned char IR_pData; //用于辅助访问数组的变量
unsigned char IR_DataFlag;
unsigned char IR_RepeatFlag;
unsigned char IR_Address;
unsigned char IR_Command;
void IR_Init() //外部中断和定时器0初始化
{
Timer0_Init();
Int0_Init();
}
unsigned char IR_GetDataFlag() //通过调用该函数可以获知有没有接收到外部传来的命令
{
if(IR_DataFlag)
{
IR_DataFlag = 0; //有命令,将标志位置为0,因为同一个命令只接收一次,只响应一次
return 1;
}
return 0;
}
unsigned char IR_GetRepeatFlag() //通过调用该函数可以实现按住按键不松手一直执行某种行为
{
if(IR_RepeatFlag)
{
IR_RepeatFlag = 0;
//如果有Repeat信号,标志位先置为0,如果接下来还有Repeat信号,标志位会被重新置为1
return 1;
}
return 0;
}
unsigned char IR_GetAddress()
{
return IR_Address; //返回接收的地址码
}
unsigned char IR_GetCommand()
{
return IR_Command; //返回接收的命令
}
void Int0_Routine() interrupt 0
{
//注意,晶振不同,信号总时长的相关参数也会不同,下面的参数是按照11.0592MHz给出的(12MHz的参数在注释中给出)
//外部信号出现一次下降沿,执行一次外部中断函数
if(IR_State == 0) //从Start部分(或Repeat信号)第一个下降沿开始,IR_State初值为0,执行下面三句代码
{
Timer0_SetCounter(0); //定时器0初始计时数据为0
Timer0_Run(1); //启动定时器0计时
IR_State = 1; //下一个应该接收起始信号Start或者Repeat信号
}
else if(IR_State == 1) //Start部分(或Repeat信号)第二个下降沿又一次触发,第二次执行中断函数
{
IR_Time = Timer0_GetCounter(); //获取定时器0计时时间(Start部分前后两个下降沿的相隔时长)
Timer0_SetCounter(0); //定时器0计时清空,直接开始下一个计时
//分析刚刚的Start部分(或者Repeat部分)
if(IR_Time > 12442-500 && IR_Time < 12442+500) //9ms+4.5ms=13500us(起始信号总时长,±500是误差)
{
IR_State = 2; //起始部分结束,进入Data部分
}
else if(IR_Time > 10368-500 && IR_Time < 10368+500) //11250us是Repeat信号总时长
{
IR_RepeatFlag = 1; //重复标志置为1
Timer0_Run(0); //停止计时
IR_State = 0; //回到状态0,状态0必定走向状态1,状态1判断下一个信号是Start还是Repeat
}
else
{
IR_State = 1; //出错,重新回到初始状态接收新的起始信号
}
}
else if(IR_State == 2) //接收Data部分
{
IR_Time = Timer0_GetCounter();
//如果是第一个位,那么这个位信号的第一个下降沿就是Start部分结束的下降沿
//位信号的第二个下降沿触发,IR_Time记录两个下降沿的时间间隔
Timer0_SetCounter(0); //计时清零,马上开始记录下两个下降沿的时间间隔
//分析位信号的两个下降沿时间间隔,判断是0还是1(如果无误则继续接收下一个位信号)
if(IR_Time > 1032-500 && IR_Time < 1032+500) //560us+560us是数据0信号的时长
{
IR_Data[IR_pData/8] &= ~(0x01 << (IR_pData%8)); //将Data等分成4个1字节存进数组
IR_pData++;
}
else if(IR_Time > 2074-500 && IR_Time < 2074+500) //560us+1690us是数据1信号的时长
{
IR_Data[IR_pData/8] |= (0x01 << (IR_pData%8)); //将Data等分成4个1字节存进数组
IR_pData++;
}
else
{
IR_State = 1; //出错,重新回到初始状态接收新信号
IR_pData = 0; //防止影响下一个Data的接收
}
if(IR_pData>=32) //成功接收到Data的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;
//验证成功,接着判断下一个信号是Repeat还是Start;验证失败,判断下一个信号是否是Start
//无论如何都要回到状态0
}
}
}
⑦main.c文件:
#include <REGX52.H>
#include "LCD1602.h"
#include "IR.h"
unsigned char Address,Command,Num;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"ADDR CMD NUM");
LCD_ShowString(2,1,"00 00 000");
IR_Init();
while(1)
{
if(IR_GetDataFlag() || IR_GetRepeatFlag()) //如果收到数据帧或者收到连发帧(按住按键不放)
{
Address=IR_GetAddress(); //获取遥控器地址码
Command=IR_GetCommand(); //获取遥控器命令码
LCD_ShowHexNum(2,1,Address,2); //显示遥控器地址码
LCD_ShowHexNum(2,7,Command,2); //显示遥控器命令码
if(Command==IR_VOL_MINUS) //如果遥控器VOL-按键按下
{
Num--; //Num自减
}
if(Command==IR_VOL_ADD) //如果遥控器VOL+按键按下
{
Num++; //Num自增
}
LCD_ShowNum(2,12,Num,3); //显示Num
}
}
}
2、红外遥控电机调速实验
(1)由于定时器0用于计时,其计数单元经常被重置,因此定时器0已经不适合用于提供中断了,故引入定时器1。
(2)本项目需要的文件如下图所示,需要重写的会在下面给出。
(3)补充缺失的代码文件,然后进行编译,按照主函数的注释进行调试即可。
①Timer1.h文件:
#ifndef __TIMER1_H__
#define __TIMER1_H__
void Timer1_Init(void);
#endif
②Timer1.c文件:
#include <REGX52.H>
/**
* @brief 定时器1初始化,100us@11.0592MHz
* @param 无
* @retval 无
*/
void Timer1_Init(void)
{
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;
}
}
*/
③Motor.h文件:
#ifndef __MOTOR_H__
#define __MOTOR_H__
void Motor_Init(void);
void Motor_SetSpeed(unsigned char Speed);
#endif
④Motor.c文件:
#include <REGX52.H>
#include "Timer1.h"
//引脚定义
sbit Motor=P1^0;
unsigned char Counter,Compare;
/**
* @brief 电机初始化
* @param 无
* @retval 无
*/
void Motor_Init(void)
{
Timer1_Init();
}
/**
* @brief 电机设置速度
* @param Speed 要设置的速度,范围0~100
* @retval 无
*/
void Motor_SetSpeed(unsigned char Speed)
{
Compare=Speed;
}
//定时器1中断函数
void Timer1_Routine() interrupt 3
{
TL1 = 0xA4; //设置定时初值
TH1 = 0xFF; //设置定时初值
Counter++;
Counter%=100; //计数值变化范围限制在0~99
if(Counter<Compare) //计数值小于比较值
{
Motor=1; //输出1
}
else //计数值大于比较值
{
Motor=0; //输出0
}
}
⑤main.c文件:
#include <REGX52.H>
#include "Nixie.h"
#include "Motor.h"
#include "IR.h"
unsigned char Command,Speed;
void main()
{
Motor_Init();
IR_Init();
while(1)
{
if(IR_GetDataFlag()) //如果收到数据帧
{
Command=IR_GetCommand(); //获取遥控器命令码
if(Command==IR_0){Speed=0;} //根据遥控器命令码设置速度
if(Command==IR_1){Speed=1;}
if(Command==IR_2){Speed=2;}
if(Command==IR_3){Speed=3;}
if(Speed==0){Motor_SetSpeed(0);} //速度输出
if(Speed==1){Motor_SetSpeed(50);}
if(Speed==2){Motor_SetSpeed(75);}
if(Speed==3){Motor_SetSpeed(100);}
}
Nixie(1,Speed); //数码管显示当前速度
}
}