【自学嵌入式:51单片机】红外遥控控制电机调速

注:LCD1602用的是我这篇博客的版本:https://www.cnblogs.com/qinruiqian/p/19020925
江科大这版51最后一节课用红外遥控控制电机调速,因为普中的开发板没做电机供电和USB接口的隔离,如果连着电脑去用红外线遥控,非常不灵敏,如果接5V1A手机充电器,马上正常工作!

红外遥控

image

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

  • 通信方式:单工,异步
  • 红外LED波长:940nm
  • 通信协议标准:NEC标准

硬件电路

image
image

调制过程:低电平导通三极管两个,用38kHz闪烁,滤掉自然界自然光中的红外光,它传递信息很快,没法用if else判断,将out引脚接在外部中断引脚上,通过外部中断进行

基本发送与接收

image

NEC通信协议

image

红外遥控器,在传送数据时,传送哪些数,这些数哪个先发,哪个后发灯,这些规则统称为IR协议,IR协议最常用的就是NEC协议
NEC通信协议包括引导码,用户码,用户反码,键码和键码反码,取反码是为了验证传输的对不对
image
最后一个是停止位,停止位起到隔离作用,一般不用判断

引导码是要发9ms的38k载波信号和4.5ms的空闲才称为是引导码
image

红外接收器接收规则:有载波输出低电平0,无载波即空闲时输出高电平1,本文用到的红外接收器是HS0-38
image

用户码、用户反码、键码和键码反码每个部分一个字节
用户码代表这个遥控器的大类是什么,用户反码就是用户码取反,键码是遥控器上每个键都有一个唯一值用于区分,键码反码就是键码取反

红外遥控器的逻辑0和1的定义:
image

对应的红外接收端HS0038转换后的信号:
image

假设传输0000 0000,过程如下:
image

Repeat信号是为了按下连续的按键准备的
image
9ms低电平和2.25ms高点哦组成

51单片机外部中断

image
外部中断是INT0 和 INT1引脚

外部中断寄存器

image

红外遥控器键码

image

代码实现LCD1602显示红外遥控器的键码值并且音量加减控制NUM+-

Timer0.h

#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0_Init(void); //初始化定时器0
void Timer0_SetCounter(unsigned int Value); //定时器0设置计数器
unsigned int Timer0_GetCounter(void); //获取定时器0的计数器
void Timer0_Run(unsigned char Flag); //启动定时器0,Flag表示是否启动

#endif

Timer0.c

#include <REGX52.H>

//定时器0中断初始化
void Timer0_Init(void)
{
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;		//设置定时器模式
	TL0 = 0;		//设置定时初值
	TH0 = 0;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 0;		//定时器0暂时不计时
}

//定时器0设置计数器
void Timer0_SetCounter(unsigned int Value)
{
	TH0 = Value/256;
	TL0 = Value%256;
}

//获取定时器0的计数器
unsigned int Timer0_GetCounter(void)
{
	return (TH0<<8) | TL0; //把TH0和TL0拼成一个16位无符号整数
}

//启动定时器0,Flag表示是否启动
void Timer0_Run(unsigned char Flag)
{
	TR0 = Flag;
}

INT0.h

#ifndef __INT0_H__
#define __INT0_H__

void initINT0(); //初始化外部中断

#endif

INT0.c

#include <REGX52.H>

//初始化外部中断
void initINT0()
{
	IT0 = 1; //1-下降沿触发;0-低电平触发
	IE0 = 0; //中断标志位清零
	EX0 = 1; //中断打开
	EA = 1; //允许中断
	PX0 = 1; //给外部中断高优先级
}

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_PRT 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

extern unsigned int IR_Time; //计时变量
void IR_Init(void); //红外传输初始化
unsigned char IR_GetDataFlag(void); //获取数据传输标志位
unsigned char IR_GetRepeatFlag(void); //获取是否重发标志
unsigned char IR_GetAddress(void); //获取地址码
unsigned char IR_GetCommand(void); //获取指令

#endif

IR.c

#include <REGX52.H>
#include "INT0.h"
#include "Timer0.h"

unsigned int IR_Time; //计时变量
unsigned char IR_State; //状态0-默认,1-启动信号,2-解码数据
unsigned char IR_Data[4]; //用长整型移位超过16会出问题,所以用4个8位二进制的数组
unsigned char IR_pData; //当前收到了第几位的指针,相当于栈顶指针
unsigned char IR_DataFlag; //数据传输结束了为1,否则为0
unsigned char IR_RepeatFlag; //是否重发标志
unsigned char IR_Address; //地址码
unsigned char IR_Command; //IR的命令

//红外传输初始化
void IR_Init(void)
{
	Timer0_Init();
	initINT0(); //下降沿触发,检测引导信号
}

//获取数据传输标志位
unsigned char IR_GetDataFlag(void)
{
	//传输结束,清零,返回1,代表收到
	if(IR_DataFlag)
	{
		IR_DataFlag = 0;
		return 1;
	}
	//否则返回0
	return 0;
}

//获取是否重发标志
unsigned char IR_GetRepeatFlag(void)
{
	//传输结束,清零,返回1,代表收到
	if(IR_RepeatFlag)
	{
		IR_RepeatFlag = 0;
		return 1;
	}
	//否则返回0
	return 0;
}

//获取地址码
unsigned char IR_GetAddress(void)
{
	return IR_Address;
}

//获取指令
unsigned char IR_GetCommand(void)
{
	return IR_Command;
}

//外部中断函数,连着红外模块,如果信号过来自动中断
void Int0_Rountine(void) interrupt 0
{
	switch(IR_State)
	{
		case 0: //默认起始状态
			//P2=0;//用LED灯全亮测试是否有信号过来
			Timer0_SetCounter(0); //把计数器清零
			Timer0_Run(1); //开始启动,需要解码
			IR_State = 1;
			break;
		case 1: //状态为1,识别起始信号
			IR_Time = Timer0_GetCounter(); //获取中断时间
			Timer0_SetCounter(0); //计时器清零
			//比较时间,看是启动信号还是什么
			//允许误差500微秒
			//启动信号
			//引导码9ms低电平+4.5ms空闲
			//我的开发板是11.0592晶振,一个机器周期是1.085微秒
			//为了精确计算,引导码是13500微秒
			//要除1.082得到一个更精确的值
			//13500 / 1.085 = 12442
			//然后给500微秒的范围进行判断
			//500 / 1.085 = 461
			//但是还是接不到信号,于是我试了试1000微秒,也就是1ms
			//1000 / 1.085 = 922
			if(IR_Time > 12442 - 922 && IR_Time < 12442 + 922)
			{
				//P2_1=0;//用LED灯测试是否是起始信号
				IR_State = 2; //状态2解码数据
			}
			//重发Repeat信号,9ms+2.25ms
			//还是11250 / 1.085 = 10369
			//范围没重复,这个最大值11291,那个最大值11500多,不重复
			else if(IR_Time >10369 - 922 && IR_Time < 10369 + 922)
			{
				IR_RepeatFlag = 1; //置重发信号为1
				Timer0_Run(0); //计数器停止
				IR_State = 0; //回到初始状态
			}
			//上面都不是,可能是解码错误
			else
			{
				IR_State = 1; //继续搜寻起始信号
			}
			break;
		case 2: //状态2,解码数据
			//继续读取计数器的数,然后清零计数器
			IR_Time = Timer0_GetCounter();
			Timer0_SetCounter(0);
			//逻辑0,560us低电平。560us高电平
			//1120us / 1.085 = 1032us
			//逻辑1,560us低电平,1690us高电平
			//2250us / 1.085 = 2074
			//1032 + 922 = 1954 < 2074 - 922 = 1152,不重叠
			//收到逻辑0
			if(IR_Time > 1032 - 922 && IR_Time < 1032 + 922)
			{
				//8个二进制位是一个字节,IR_pData / 8取的是当前是第几个字节
				//IR_pData % 8是指左移多少位
				//0x01确定0的位置
				//按位取反再做与运算
				//因为传的是逻辑0,要把0准确写进去,所以要取反然后取与
				IR_Data[IR_pData / 8] &= ~(0x01 << (IR_pData % 8));
				IR_pData++;
			}
			//收到逻辑1
			else if(IR_Time > 2074 - 922 && IR_Time < 2074 + 922)
			{
				//传逻辑1,不用取反,直接或就行
				IR_Data[IR_pData / 8] |= (0x01 << (IR_pData % 8));
				IR_pData++;
			}
			//收到错误
			else
			{
				//重新搜寻起始信号,栈顶指针清零
				IR_pData = 0;
				IR_State = 1;
			}
			//如果接收4个字节完成
			if(IR_pData >= 32)
			{
				IR_pData = 0;
				//验证取反是否正确
				if((IR_Data[0] == ~IR_Data[1]) && (IR_Data[2] == ~ IR_Data[3]))
				{
					IR_Address = IR_Data[0]; //地址码转存过来
					IR_Command = IR_Data[2]; //命令码
					IR_DataFlag = 1; //传输结束
				}
				//然后切换回搜寻起始信号的空闲状态并停止计时器
				Timer0_Run(0);
				IR_State = 0; //数据发送完成
				/*
				当IR_State=1(等待读取状态)时,外部中断触发后会直接进入 “判断引导码 / 重复码” 的逻辑,
				但此时定时器已停止(Timer0_Run(0)),且状态机未回到初始状态,无法重新启动定时器计时。
				后续按键时,红外模块发送的引导码下降沿无法正确触发 “空闲状态(0)” 的初始化操作(定时器清零 + 启动),
				导致无法识别新的键码。
				*/
			}
			break;
			
	}
}

main.c

#include <REGX52.H>
#include "LCD1602.h"
#include "IR.h"

unsigned char Num; //记录音量键加减的数值
unsigned char Address; //地址码
unsigned char Command; //命令

void main()
{
	//initINT0();
	LCD1602_Init();
	LCD1602_ShowString(0,0,"ADDR CMD NUM");
	LCD1602_ShowString(1,0,"00   00  000");
	IR_Init();
	while(1)
	{
		//如果收到32位数据
		//按住不放发送重发标志,所以要检测一下重发
		if(IR_GetDataFlag() || IR_GetRepeatFlag())
		{
			Address = IR_GetAddress();
			Command = IR_GetCommand();
			LCD1602_ShowHexNum(1,0,Address,2);
			LCD1602_ShowHexNum(1,5,Command,2);
			if(Command == IR_VOL_MINUS) //红外遥控器的音量--模块
			{
				Num--;
			}
			if(Command == IR_VOL_ADD) //红外遥控器的音量++模块
			{
				Num++;
			}
			LCD1602_ShowNum(1, 9, Num, 3);
		}
	}
}

实现红外遥控电机调速(红外不灵敏的可以看过来)

我一开始以为江科大的代码,用红外遥控不好使,结果是连着电脑端做实验,电脑端USB口有信号干扰,我用一个5V1A的手机充电器连接开发板,马上这个红外调速就变得非常灵敏,一般来说,那个无线四项步进电机是三极管驱动的,那个VCC应该是单独引出一个电源,这样能保护单片机,但是普中的板子并没有,而且电机转的时候,自身也有信号干扰,总而言之,接个手机充电器就好用了,代码太多了,直接开源了:https://gitee.com/qin-ruiqian/51-infrared-ray-dcm

posted @ 2025-08-06 21:16  秦瑞迁  阅读(259)  评论(0)    收藏  举报