单片机学习(十)红外遥控与外部中断
目录
一、红外遥控原理
输出信号的格式
信号的三种情况
编码任务
LED硬件电路
接收头硬件电路
1. 红外遥控简介
2. 红外LED和接收头的硬件电路
3. 基本发送与接收
4. NEC编码
5. 遥控器的键码
二、外部中断
1. 51单片机的外部中断资源
2. 中断通路
三、编码实现
1. 外部中断的中断通路配置
2. 定时器模块
3. 编写接收器模块
4. 使用红外遥控控制电机运转
参考资料:https://www.bilibili.com/video/BV1Mb411e7re?p=37
一、红外遥控原理
1. 红外遥控简介
红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出
通信方式:单工,异步
红外LED波长:940nm
通信协议标准:NEC标准
红外接收头:
2. 红外LED和接收头的硬件电路
LED硬件电路
LED硬件电路有以下两种:
其中第一种的信号形式是这样的:
即LED或者是完全不发光,或者是以38KHz
的频率进行闪烁发出红外光
而第二种则需要软件控制IN
的输入。
接收头硬件电路
接收头会对接收到的红外光进行一定的过滤工作,然后输出到P32
引脚上。
3. 基本发送与接收
红外LED的三种发送状态下接收头的输出:
空闲状态:红外LED不亮,接收头输出高电平
发送低电平:红外LED以38KHz频率闪烁发光,接收头输出低电平
发送高电平:红外LED不亮,接收头输出高电平
例如:
4. NEC编码
红外接收头接收到的信号调制输出的结果应符合红外NEC协议:
输出信号的格式
Address+Address¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯+Command+Command¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
每个部分1个字节(8位),而第一和第二,第三和第四个部分互为反码是为了进行数据的校验。
信号的三种情况
接收到的信号分为以下三种情况:
信息头,图中的红色信号,提示将要发送信号
信息体,图中的蓝色信号,真正需要传输的内容
重复信号,图中的绿色信号,代表前面发送的内容重复发送
编码任务
我们的任务就是编写接收信号的驱动程序,在代码层读取接收到的信号,将其转化为Address
和Command
保存起来(将信号转为数字)。
我们可以观察将得到的信号转化为对应的编码的一组例子:
可以发现点击不同的按键,接收头得到的键码是不一样的。
5. 遥控器的键码
我们可以参考这个图上的编码来对遥控器的输入进行对应的响应。
二、外部中断
1. 51单片机的外部中断资源
STC89C52有4个外部中断
STC89C52的外部中断有两种触发方式:
下降沿触发
低电平触发
中断号:
外部中断引脚
这里只有两个引脚:P32
和P33
,所以实际上是只有两个外部中断
我们使用下降沿触发中断,然后获取到前后两个下降沿相距的时间,然后通过这个时间推断出是信号头,还是信号体(0,1)以及repeat
。
2. 中断通路
三、编码实现
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++
:
这说明外部中断配置成功,我们把外部中断初始化也抽象出一个模块Int0
:
// Int0.cvoid Int0_Init() { // 修改IT0可以控制是下降沿触发(1)还是低电平触发(0) IT0 = 1; IE0 = 0; EX0 = 1; EA = 1; PX0 = 1; }/* // 外部中断 void Int0_Routine() interrupt 0 { } */
2. 定时器模块
此时我们需要将Timer0
作为我们的计时器,与之前不同的是我们此时不是用它来发起中断了,而是专门用它来记录时间:
// Timer0.cvoid 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;// 标志数据是否读取到Repeatunsigned char IR_RepeatFlag;// 存储Address信息unsigned char IR_Address;// 存储Command信息unsigned char IR_Command;
然后接收信息的过程使用一个有限状态机来完成,这个状态机一共有三个状态:
开始状态,接收到第一个下降沿开始计时,转为状态2
接收信息头状态,当再接收到一个信号下降沿时计算经过的时间:
经过时间为
9ms+4.5ms=13500μs
,判断为开始信号,转为状态3【接收信息体状态】经过时间为
9ms+2.25ms=11250μs
,判断为repeat
信号,转为状态1【开始状态】否则一直维持状态2【接收信息头状态】
接收信息体状态,每接收到一个下降沿根据经过的时间判断是
0
还是1
,并将这些数据存储起来,当接收完4*8=32
个bit之后转变为状态1【开始状态】,若接收到的数据非0
或1
(数据异常),则返回状态2【接收信息头状态】
状态转换图如下图所示:
因为我们使用的开发板上是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
也会发生变化:
为了使用方便,我们再在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.cvoid 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 1void 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); } } }
运行效果: