在上一篇文章《从直流电机调速(PWM)到AD/DA 模数/数模转换》中,我们完成了51单片机学习的最后一个基础模块。作为51单片机系列学习的收尾,本文将深入探讨红外遥控技术——这是嵌入式系统中一种经典且实用的无线通信方式。通过本文,你将掌握红外遥控的工作原理、NEC编码协议、51单片机的外部中断与定时器配置,并最终实现两个完整的实战项目:红外遥控计数显示和红外遥控电机调速。这些项目将为你后续制作更复杂的综合练手项目打下坚实基础。
如果觉得有收获可以为我的文章点点小赞,这将激励我创作更好的内容!谢谢!
一、红外遥控
1.1 红外遥控简介
- 红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出
- 通信方式:单工,异步
- 红外LED波长:940nm
- 通信协议标准:NEC标准
1.2 硬件电路
1.3 基本发送与接收
- 空闲状态:红外LED不亮,接收头输出高电平
- 发送低电平:红外LED以38KHz频率闪烁发光,接收头输出低电平
- 发送高电平:红外LED不亮,接收头输出高电平
1.4 NEC编码
1.5 遥控器键码
1.6 51单片机的外部中断
1.7 外部中断寄存器
用来配置外部中断的Int0代码模块:
Int0.c文件↓
----------------------------
#include <REGX52.H> /** @brief 外部中断0初始化 @param 无 @retval 无 */ void Int0_Init(void) { IT0=1; //1为下降沿出发,0为低电平触发 IE0=0; EX0=1; EA=1; PX0=1; } /*外部中断0中断函数模板 void Int0_Routine(void) interrupt 0 { } */----------------------------
1.8 配置定时器0用以红外遥控
通过给Timer0增加扩展升级为Timer0_IR,增加1.开始计时,2.计时写入,3.计时读出三个和红外遥控配套的子函数,使定时器模块为之后红外通信的时间计算做基础:
Timer0_IR.c文件↓
----------------------------
#include <REGX52.H> /** @brief 定时器0初始化,1毫秒@11.0592Mhz @param 无 @retval 无 */ void Timer0_Init() { TMOD&=0xF0; //把TMOD的低四位清零,高四位保持不变 TMOD|=0x01; //把TMOD的最低位置1,高四位保持不变 TL0=0; //设置定时低八位初值 TH0=0; //设置定时高八位初值 TF0=0; //清除TF0标志 TR0=0; //定时器0不计数 } void Timer0_SetCounter(unsigned int Value)//准备计时,将输入值赋给TL0,TH0 { TL0=Value%256; TH0=Value/256; } unsigned int Timer0_GetCounter(void)//获取计时,返回TH0,TL0共16位 { unsigned int Result=0; Result=(TH0<<8)|TL0; return Result; } void Timer0_Run(unsigned char Flag) //开始计时,给1开始计时 { TR0=Flag; } /*定时器0函数中断模板(1s) void Timer0_Routine() interrupt 1 //定时器0中断函数 { static unsigned int T0Count=0; TL0=0x66; //设置定时初值 TH0=0xFC; //设置定时初值 T0Count++; if(T0Count>=1000) { T0Count=0; } } */----------------------------
1.9 红外遥控代码模块
(1)先写是否获取数据的Pool和是否获取Repeat指令的Pool的标志的子函数:
#include <REGX52.H> #include "Timer0_IR.h" #include "Int0.h" unsigned int IR_Time; unsigned char IR_State; unsigned char IR_Date[4]; unsigned char IR_pDate=0; unsigned char IR_DateFlag; unsigned char IR_RepeatFlag=0; unsigned char IR_Address; unsigned char IR_Command; void IR_Init(void)//红外遥控初始化 { Timer0_Init();//初始化红外定时器 Int0_Init();//打开红外定时器 } unsigned char IR_GetDateFlag(void)//返回是否获取数据的Pool { unsigned char PoolResult1=0; if(IR_DateFlag) { IR_DateFlag=0; PoolResult1=1; } return PoolResult1; } unsigned char IR_GetRepeatFlag(void)//返回是否获取Repeat指令的Pool { unsigned char PoolResult2=0; if(IR_RepeatFlag) { IR_RepeatFlag=0; PoolResult2=1; } return PoolResult2; }(2)将获取数据和指令的功能封装成子函数:
unsigned char IR_GetAddress(void)//最终获取的地址值 { return IR_Address; } unsigned char IR_GetCommand(void)//最终获取的指令值 { return IR_Command; }(3)写外部中断函数——有接收到红外信号就进中断,利用红外NEC协议编码:
记住!红外接收器出现电平下降沿时进入0号外部中断函数,这是在定时器寄存器那里配置的!
以下是完整的中断函数,里面的内容有点复杂,但我几乎每一句都有加了注释,可以停下来用心慢慢看:
//这个红外遥控外部中断被设置后,会在红外接收器出现电平下降沿时进入0号中断函数 void Int0_Routine(void) interrupt 0 { if(IR_State==0) //第一次出现下降沿,IR_State==0红外遥控开始读取指令标志(取消 空闲) { Timer0_SetCounter(0); //清理定时器计数,准备计时 Timer0_Run(1); //开始计时 IR_State=1; //打开接收指令函数 } //此时IR_State==1,接收指令函数被打开,如果再出现下降沿就会进入以下判断 else if(IR_State==1) //再一次出现下降沿,进行指令接收并分析是什么指令 { IR_Time=Timer0_GetCounter(); //获取两次下降沿的间隔时间 Timer0_SetCounter(0); //将定时器计数清零,重新计时,方便下次提取时间 if(IR_Time>12442-500 && IR_Time<12442+500)//接收到了开始指令 { IR_State=2; //接收到了开始指令,进行下一部分的接收数据函数 } else if(IR_Time>10369-500 && IR_Time<10369+500)//接收到了Repeat指令 { IR_RepeatFlag=1; //接收到Repeat信号,IR_RepeatFlag标志置一 Timer0_Run(0); //停止计时 IR_State=0; //返回状态0等待下一个下降沿 } else //其他干扰情况 { IR_State=1; //保持待接收 } } //此时如果接收到开始指令,则IR_State=2,下面进行接收数据函数 else if(IR_State==2) //再次出现下降沿,进行接收数据函数并验证数据,将接收完成标志置1 { IR_Time=Timer0_GetCounter(); //获取两次下降沿的间隔时间 Timer0_SetCounter(0); //将定时器计数清零,重新计时,方便下次提取时间 if(IR_Time>1032-500 && IR_Time<1032+500)//接收到一位0 { IR_Date[IR_pDate/8]&=~(0x01<<(IR_pDate%8)); //将0写入由小到大(与写1互斥)由4个char类型组成的共32bit 的数组 IR_pDate++; //计数加1,做到与写1互斥 } else if(IR_Time>2074-500 && IR_Time<2074+500) //接收到一位1 { IR_Date[IR_pDate/8]|=(0x01<<(IR_pDate%8)); //将1写入由小到大(与写0互斥)由4个char类型组成的共32bit 的数组 IR_pDate++; //计数加1,做到与写0互斥 } else //接收到错误数据 { IR_pDate=0; //清零计数 IR_State=1; //保持待接收,舍弃当前所有数据,待接收下一个指令 } if(IR_pDate>=32) //判断接收到32位bit,接收完成 { IR_pDate=0; //清零计数 if((IR_Date[0]==~IR_Date[1]) && (IR_Date[2]==~IR_Date[3]))//判断接收数据是否满足对应相反关系,完成数据校验 { IR_Address=IR_Date[0]; //转存数据 IR_Command=IR_Date[2]; //转存指令 IR_DateFlag=1; //接收完毕将接收完成标志置1,IR_DateFlag标志置一 } Timer0_Run(0); //接收完数据和指令,停止外部中断定时器 IR_State=0; //返回至初始状态 } } }综上,完整的IR代码模块为:
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 //VOL- #define IR_VOL_ADD 0x09 //VOL+ #define IR_0 0x16 #define IR_RPT 0x19 #define IR_U_SD 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(void);//红外遥控初始化 unsigned char IR_GetDateFlag(void);//返回是否获取数据的Pool unsigned char IR_GetRepeatFlag(void);//返回是否获取指令的Pool unsigned char IR_GetAddress(void);//最终获取的地址值 unsigned char IR_GetCommand(void);//最终获取的指令值 #endif----------------------------
IR.c文件↓
----------------------------
#include <REGX52.H> #include "Timer0_IR.h" #include "Int0.h" unsigned int IR_Time; unsigned char IR_State; unsigned char IR_Date[4]; unsigned char IR_pDate=0; unsigned char IR_DateFlag; unsigned char IR_RepeatFlag=0; unsigned char IR_Address; unsigned char IR_Command; void IR_Init(void)//红外遥控初始化 { Timer0_Init();//初始化红外定时器 Int0_Init();//打开外部中断 } unsigned char IR_GetDateFlag(void)//返回是否获取数据的Pool { unsigned char PoolResult1=0; if(IR_DateFlag) { IR_DateFlag=0; PoolResult1=1; } return PoolResult1; } unsigned char IR_GetRepeatFlag(void)//返回是否获取Repeat指令的Pool { unsigned char PoolResult2=0; if(IR_RepeatFlag) { IR_RepeatFlag=0; PoolResult2=1; } return PoolResult2; } unsigned char IR_GetAddress(void) //最终获取的地址值 { return IR_Address; } unsigned char IR_GetCommand(void) //最终获取的指令值 { return IR_Command; } //这个红外遥控外部中断被设置后,会在红外接收器出现电平下降沿时进入0号中断函数 void Int0_Routine(void) interrupt 0 { if(IR_State==0) //第一次出现下降沿,IR_State==0红外遥控开始读取指令标志(取消空闲) { Timer0_SetCounter(0); //清理定时器计数,准备计时 Timer0_Run(1); //开始计时 IR_State=1; //打开接收指令函数 } //此时IR_State==1,接收指令函数被打开,如果再出现下降沿就会进入以下判断 else if(IR_State==1) //再一次出现下降沿,进行指令接收并分析是什么指令 { IR_Time=Timer0_GetCounter(); //获取两次下降沿的间隔时间 Timer0_SetCounter(0); //将定时器计数清零,重新计时,方便下次提取时间 if(IR_Time>12442-500 && IR_Time<12442+500)//接收到了开始指令 { IR_State=2; //接收到了开始指令,进行下一部分的接收数据函数 } else if(IR_Time>10369-500 && IR_Time<10369+500)//接收到了Repeat指令 { IR_RepeatFlag=1; //接收到Repeat信号,IR_RepeatFlag标志置一 Timer0_Run(0); //停止计时 IR_State=0; //返回状态0等待下一个下降沿 } else //其他干扰情况 { IR_State=1; //保持待接收 } } //此时如果接收到开始指令,则IR_State=2,下面进行接收数据函数 else if(IR_State==2) //再次出现下降沿,进行接收数据函数并验证数据,将接收完成标志置1 { IR_Time=Timer0_GetCounter(); //获取两次下降沿的间隔时间 Timer0_SetCounter(0); //将定时器计数清零,重新计时,方便下次提取时间 if(IR_Time>1032-500 && IR_Time<1032+500)//接收到一位0 { IR_Date[IR_pDate/8]&=~(0x01<<(IR_pDate%8)); //将0写入由小到大(与写1互斥)由4个char类型组成的共32bit 的数组 IR_pDate++; //计数加1,做到与写1互斥 } else if(IR_Time>2074-500 && IR_Time<2074+500) //接收到一位1 { IR_Date[IR_pDate/8]|=(0x01<<(IR_pDate%8)); //将1写入由小到大(与写0互斥)由4个char类型组成的共32bit 的数组 IR_pDate++; //计数加1,做到与写0互斥 } else //接收到错误数据 { IR_pDate=0; //清零计数 IR_State=1; //保持待接收,舍弃当前所有数据,待接收下一个指令 } if(IR_pDate>=32) //判断接收到32位bit,接收完成 { IR_pDate=0; //清零计数 if((IR_Date[0]==~IR_Date[1]) && (IR_Date[2]==~IR_Date[3]))//判断接收数据是否满足对应相反关系,完成数据校验 { IR_Address=IR_Date[0]; //转存数据 IR_Command=IR_Date[2];//转存指令 IR_DateFlag=1; //接收完毕将接收完成标志置1,IR_DateFlag标志置一 } Timer0_Run(0); //接收完数据和指令,停止外部中断定时器 IR_State=0; //返回至初始状态 } } }----------------------------
1.10 实例一17.1.1红外遥控
main.c文件↓
----------------------------
#include <REGX52.H> #include "Delay.h" #include "LCD1602.h" #include "IR.h" unsigned char Num; unsigned char Address,Command; void main() { LCD_Init(); LCD_ShowString(1,1,"ADDR CMD NUM"); LCD_ShowString(2,1,"00 00 000"); IR_Init(); while(1) { if(IR_GetDateFlag() || IR_GetRepeatFlag())//判断接收标志为1或是重发标志为1,重发为1则会取得过去离重发标志 最近的一帧数据 { 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--; } if(Command==IR_VOL_ADD) //给遥控的VOL-添加让计数加一的功能 { Num++; } LCD_ShowNum(2,12,Num,3); } } }----------------------------
1.11 实例二17.2.1红外遥控电机调速
电机代码模块:
Motor.c文件↓
----------------------------
#include <REGX52.H> #include "Timer1.h" //引脚定义 sbit Motor=P1^0; unsigned char Counter=0,Compare=0; /** @brief 电机初始化 @param 无 @retval 无 */ void Motor_Init(void)//电机初始化 { Timer1_Init(); } /** @brief 电机设置速度 @param Speed 要设置的速度,范围0~100,100是全转 @retval 无 */ void Motor_SetSpeed(unsigned char Speed)//电机速度设置 //这个speed是速度比较值 { Compare=Speed; } void Timer1_Routine() interrupt 3 //电机——定时器1中断函数 { Counter++; // Counter%=100; //0到99的线性计数,计数值变化范围限制在0~99 if(Counter<Compare)//计数值小于比较值 { Motor=1; //1取反后给0,给低电平,电机启动 } else { Motor=0; //0取反后给1,给高电平,电机启动 } TL1=0xA4; //设置定时初值 TH1=0xFF; //设置定时初值,100us }----------------------------
main.c文件↓
----------------------------
#include <REGX52.H> #include "Delay.h" #include "Key.h" #include "Nixie.h" #include "IR.h" #include "Motor.h" unsigned char Command; unsigned char Motor_Speed_Gear=0; //这个Motor_Speed_Gear是速度快慢挡的第几挡 void main() { Motor_Init(); //电机初始化 IR_Init(); //红外遥控初始化 while(1) { if(IR_GetDateFlag()==1) //如果收到数据帧(仅单发,无连发功能) { Command=IR_GetCommand(); //获取遥控器命令码 if(Command==IR_0){Motor_Speed_Gear=0;}// if(Command==IR_1){Motor_Speed_Gear=1;}// if(Command==IR_2){Motor_Speed_Gear=2;}// if(Command==IR_3){Motor_Speed_Gear=3;}//根据遥控器命令码设置速度 if(Motor_Speed_Gear==0){Motor_SetSpeed(0);} if(Motor_Speed_Gear==1){Motor_SetSpeed(40);} if(Motor_Speed_Gear==2){Motor_SetSpeed(60);} if(Motor_Speed_Gear==3){Motor_SetSpeed(80);} } Nixie(1,Motor_Speed_Gear); //数码管显示速度 } }----------------------------
二、总结
本文系统性地介绍了基于51单片机的红外遥控技术实现,从基础原理到完整代码实现,涵盖了以下核心内容:
- 红外遥控基础:介绍了红外遥控的工作原理、通信方式(单工、异步)、硬件电路以及NEC编码协议标准。
- 51单片机硬件支持:详细讲解了外部中断(INT0)和定时器(Timer0)的配置方法,这是红外信号接收和时序解析的关键硬件基础。
- 代码模块化设计:将红外遥控功能封装为独立的IR模块,包含初始化、数据获取、指令解析等函数,提高了代码的可复用性和可维护性。
- 完整实现流程:通过状态机的方式实现了NEC协议的解码过程,包括引导码识别、数据位接收、校验和验证等关键步骤。
- 实践应用案例:提供了两个完整的应用实例:红外遥控计数显示和红外遥控电机调速,展示了红外遥控在实际项目中的应用方法。
技术要点总结:
- 硬件配置:正确配置外部中断触发方式(下降沿触发)和定时器工作模式是红外接收的基础。
- 时序精度:NEC协议对时序要求严格,需要精确测量脉冲宽度来区分逻辑0和逻辑1。
- 状态机设计:采用状态机(IR_State)管理解码过程,使代码逻辑清晰,易于调试和维护。
- 数据校验:通过地址码和命令码的反码校验机制,提高了数据传输的可靠性。
- 模块化编程:将红外遥控功能封装为独立模块,便于在不同项目中复用和移植。
学习建议:
- 建议读者先理解NEC协议的时间参数,再结合代码中的状态转换逻辑进行学习。
- 调试时可以使用示波器观察红外接收头的输出波形,验证解码逻辑的正确性。
- 在实际应用中,可以根据需要调整时序容差(代码中的±500微秒),以适应不同遥控器的特性。
- 本文提供的代码框架可以扩展支持其他红外协议,只需修改状态机中的时间判断条件即可。
都看到这了点点赞吧!