51单片机利用光敏电阻实现光照自动控制系统

本人太懒了,有很多次想写博客,单只是想想罢了,要去备战考研了。等过了考研之后,我一定要多学东西,目前学的东西太少了。。。。看不起自己啊!废话不多说,由于有这个课程设计,所以一并写在这里吧!扬帆,启航!
51单片机利用光敏电阻实现光照自动控制系统,这个设计其实不难,难的是其中的各种状态逻辑,先看设计要求:
1、设计题目
单片机光照控制系统的设计。
2、设计要求
(1)基本要求
①单片机外接光电传感器或光敏电阻;(采用光敏电阻进行ADC采样输入)
②采集传感器输出的信号并进行显示;(LCD1602进行显示)
③光照度小于给定值时点亮其他的LED灯进行补光;(每0.5S监测周围亮度,调整补光)
④补光的级数为5级。(5个LED)
⑤校准输出,按照流明的单位进行显示。(这有点难度,懵逼(&))
OK!看一下设计的原理图:
在这里插入图片描述
文末有完整的AD版解决方案!!!!(原理图和PCB)
下面我们来简单说一下思路:
1.上电复位后,检测EEPROM是否有自定义的光照校准系数,若有:
(1)直接运行ADC采集,经过AD值到流明的处理,进行流明单位的输出,以后每间隔0.5S进行一次转换(采用定时器进行精确进行),输出到LCD1602上进行显示。
(2)自动检测亮度:将得到的流明与预设的常规亮度进行比较:若比其小,则控制LED进行补光,每次递增一颗灯进行补光;若比其大,则停止补光,继续输出流明。
2.若没有:则按下按键KEY1检测调整系数:可以按下KEY2和KEY3进行LED灯的补光,采集亮度不同时候的ADC值(最大亮度和最小亮度),计算校准系数,并显示在LCD1602上,再次按下KEY1进行写入到EEPROM的0x01地址。之后运行自动检测亮度程序。当然KEY1作为功能键,有以下功能:
(1)在自动检测亮度时,长按KEY1会进入“光照校准系数”的设定存储程序的设置最暗亮度界面,进行此时的AD采集;
(2)在“光照校准系数”的设定存储程序的设置最暗亮度界面按下KEY1后,保存采集的AD,并进入设置最亮亮度界面;
(3)在设置最亮亮度界面中按下KEY1,保存采集的AD,并进入光照校准系数的计算程序;
底层的I2C驱动就不说了,看看个个硬件如何协调工作
下面详细分析一下:
上电复位后

void run()
{
	//初始化液晶显示
	init_LCD();
	//启动按键扫描:每隔10ms扫描
	configTimer0(10);
	
	//1.上电复位后,检测EEPROM是否有光照校准系数,
	if(isExistCali())
	{
		//若有:
		//先读校准系数
		cali=getCali();
		
	
	}else{
	//若没有:进入系数校准状态
	calibrationSetting();
		
	}	
	//开始自动补光:以后每间隔500ms进行一次转换(采用定时器进行精确进行),输出到LCD1602上进行显示。
  configTimer1(5);

}

main函数:

void main()
{

	
	run();
	

	while(1)
	{
		keyHandler();
	}

	
}

Tips: 大部分流程有注释,没啥说的,上电复位后要用定时器T0来启动按键扫描,驱动整个按键状态机的运行,在程序整个运行期间不停止!!!注意配置定时器T1每隔500ms进行亮度检测输出。keyHandler用于处理按下键后的阻塞操作

24c01和校准系数的读写

//24C01的物理地址见原理图:1010000
#define E2P_ADRESS_READ  0xA1
#define E2P_ADRESS_WRITE 0xA0


//数据有效和校准系数在24c01的地址
#define Cali_Exsist_Adress  0x00
#define CaliPara_Adress 		0x01



 /**
  * @brief 判断EEP是否存有校准系数 
  * @param  void
  * @retval void
  */
bit isExistCali(void);


 /**
  * @brief 	设置校准系数是否有效
  * @param  effect:0代表无效,1代表有效
  * @retval void
  */
void setCaliEffect(unsigned char effect);


 /**
  * @brief  读取校准系数
  * @param  void
  * @retval 校准系数
  */
unsigned char getCali(void);


 /**
  * @brief  写入校准系数
  * @param  cali:校准系数
  * @retval void
  */
void setCali(unsigned char cali);

Tips: 没啥说的,,,

按键状态机

#define LONGTIME 100//长按和短按的区分:20*(8~10ms)

//定义按键的状态枚举值:
typedef enum{  
    KEY_S1,   //空闲
    KEY_S2,  //软件消抖中
    KEY_S3,  //短按状态
		KEY_S4,	//长按状态
    KEY_S5,	 //释放按键
}key_states;  

//按键的状态,以及对应状态所对应的回调
typedef struct{
	unsigned char keycount;//检测的按键数量
	unsigned char key;//代表有动作的一个键(1~3,0代表无按键)	
	key_states key_state;  //按键初始状态为空闲
	unsigned char (*onNoStatus)(void* listener);//空闲的回调
	unsigned char (*onShortPress)(void* listener);//短按的回调函数
	unsigned char (*onLongPress)(void* listener);//长按的回调函数
	unsigned char (*onReleaseKey)(void* listener);//按键释放回调
	
}KeyListener;



 /**
  * @brief  按键状态机,需要每隔8~10ms调用一次
	* @param  key_listener:3个按键的状态
  * @retval void
  */
void key_scan(KeyListener* key_listener);

void key_scan(KeyListener* key_listener)
{   
  	unsigned char i;//记录3个按键的一个
    static unsigned char press=0;  //持续按键的计数值:用于区分短按还是长按
		
	  for(i=1;i<=key_listener->keycount;i++)//依次检测三个按键
			{	
				
			 switch(key_listener->key_state)  
				{  
					case KEY_S1: //空闲状态 	key_listener->key记录当前检测的按键
						key_listener->key=i;
							if((P1 & (0x10<<key_listener->key))!= (0x10<<key_listener->key))  //当前键按下:进入消抖状态
							{
									
									key_listener->key_state = KEY_S2;  
							}else{//没有检测到按键:空闲状态  
									key_listener->key_state = KEY_S1; 									
									key_listener->onNoStatus(key_listener);					
							}								
							break;  
		
					case KEY_S2:  //消抖状态(去干扰),key_listener->key记录着上一次的按键:直接对这个按键进行判断,优化时间
							if((P1 & (0x10<<(key_listener->key)))!= (0x10<<(key_listener->key))){  //确认有键按下:进入按键按下长短区分的状态
									key_listener->key_state = KEY_S3;  	
								
							}else { //按键检测的误操作:可能是干扰,则忽略干扰
									key_listener->key_state = KEY_S1;
									key_listener->key=0;//重置记录的动作按键						
							}								
							break;  
		
					case KEY_S3:  //按键按下长短区分的状态并进行回调处理
							if((P1 & (0x10<<(key_listener->key)))!= (0x10<<(key_listener->key))){  //持续检测到按键未释放:计数值递增
									key_listener->key_state = KEY_S3;  
									press++;  
									if(press>LONGTIME){ //超过短按的时间限制:此次为长按 
									
										key_listener->key_state = KEY_S4;											
										
										
									}  
							}else {// 没有超过长按的时间限制:此次为短按,进行短按的回调处理,进入释放状态									
								key_listener->onShortPress(key_listener);
								key_listener->key_state = KEY_S5;								
							}
							break;  
					case KEY_S4: 
						key_listener->onLongPress(key_listener);//回调处理,同时检测按键长按的释放
						key_listener->key_state = KEY_S5;			
					
					break;
					
					case KEY_S5:  
							if((P1 & (0x10<<(key_listener->key)))== (0x10<<(key_listener->key))){  //没有按键:进入空闲状态并清空此次按键计数值
									key_listener->onReleaseKey(key_listener);
									//在释放回调处理后,重置记录的动作按键(消耗了一次按键)
								  key_listener->key_state = KEY_S1;  
									key_listener->key=0;
									press = 0; 
							}else{
							key_listener->key_state = KEY_S5; 
							}  
							return; //(按键的完整生命周期结束)  
			}
   //若有键有动作,立即跳出剩余按键检测以缩短扫描时间实现优化(即只对一个键进行检测,无法判断两个键)
				if(key_listener->key_state!=KEY_S1)
				{
							break;
				}

			}
   

	}

Tips: 注意其中的回调函数是在T0的中断中进行的,状态每10ms刷新1次,所以在这里的回调函数里不能有耗时和等待按键的操作,只能用于简单的IO开关等无阻塞的操作!!!!阻塞的操作放在keyHandler中处理
对应状态的回调处理如下:

/*************在此处进行按键回调的具体处理(!!!!此处不能再有检测按键和耗时的操作,只适用于简单无阻塞的操作!!!!!!)**************************/
static unsigned char onNoStatus(KeyListener* listener)//空闲的回调
{
	//TODO:
	return listener->key;

}
static unsigned char onShortPress(KeyListener* listener)//短按的回调函数
{
	//TODO:
	//KEY1作为复杂的多功能键,其处理方法在具体环境中,故不再此写出
	//KEY2(补光)和KEY3(减光)作为简单单一功能键,所以在中断中处理
		if(listener->key==2){
			LedMgr.LED_AUTO(LIGHT);
				
			
		}else if(listener->key==3){
			LedMgr.LED_AUTO(DARK);
				
		}	

		return listener->key;

}
static unsigned char onLongPress(KeyListener* listener)//长按的回调函数
{
	//TODO:
		if(listener->key==2){
			LedMgr.LED_ON(LED_5);
				
			
		}else if(listener->key==3){
			LedMgr.LED_OFF();
				
		}
		return listener->key;

}
static unsigned char onReleaseKey(KeyListener* listener)//按键释放回调
{
	//TODO:
		return listener->key;

}

/********************按键回调的一般处理,任何场景使用****************************/

unsigned char keyHandler()
{
	switch(key_listener.key)
	{
	
			//KEY1作为多功能按键:其中长按可以在单片机运行的任何时间都转到校准系数设置界面		
		case	1:
			if(key_listener.key_state==KEY_S4)//长按处理
			{
				
				calibrationSetting();
						
			}
		
	
	}
	return 0;
	
	
}

Tips: 任何界面长按KEY1进入校准系数设置界面

/*******************************/

	unsigned char cali=0;//校准系数
	unsigned char ad0=0;//采集到的第一次和第三次以后的AD
	unsigned char ad1=0;//第二次采集的AD
  unsigned int lm=0;//每次ad0通过校准系数计算而来的流明值
	location mLocation={0,0};//显示在LCD1602的位置(0,0)

 /**
    * @brief  进入校准系数设置,清除EEP中的数据,同时停止自动补光
    * @param  void
    * @retval void
    */	
void calibrationSetting(void)
{
		//定时器1暂停计时和中断
		timer1Pause();
		//清除EEP中的数据
		setCaliEffect(0);
		Delay(5);//等待EEP写入
		setCali(0);
		Delay(5);//等待EEP写入
		//LCD1602显示提示(设置“暗”的AD值)
		LCDShowStrs(mLocation,darkStr);
		//等待KEY1按下以确定
		while((key_listener.key!=1)||(key_listener.key_state!=KEY_S3));
		
		//直接运行AD转换,
		ad0=getADCValue();
		
		//LCD1602显示提示(设置“亮”的AD值)
		LCDShowStrs(mLocation,lightStr);
			
		//等待KEY1按下以确定(前后按下需要时间进行区分,可用延时)
		Delay(1000);
		while((key_listener.key!=1)||(key_listener.key_state!=KEY_S3));
			
		//直接运行AD转换,
		ad1=getADCValue();
		
	 //进行校准系数的计算
		cali=calPara(ad0,ad1);
		
		//保存校准系数
		setCali(cali);
		setCaliEffect(1);
		//显示开始提示
		LCDShowStrs(mLocation,runStr);
    timer1Resume();//定时器1恢复计时和中断

}

Tips: 注意,进入设置前要把定时器T1关闭,以停止自动补光,退出后记得打开T1

LED灯的管理和自动补光

//定义5个LED的IO引脚
sbit LED1=P1^4;
sbit LED2=P1^3;
sbit LED3=P1^2;
sbit LED4=P1^1;
sbit LED5=P1^0;
//定义5个LED的总开关
sbit LED_Switch=P2^4;

//枚举LED的所有情况:0个灯亮,1个...
typedef enum{
	LED_0=0,
	LED_1,
	LED_2,
	LED_3,
	LED_4,
	LED_5
}LED_Num;

//定义开关状态(低电平三极管导通)
#define ON 	0
#define OFF 1

//定义LED的调整方向
#define DARK 	0
#define LIGHT 1


//5个LED的管理器:管理和设置LED的状态
typedef struct{
	
	LED_Num ledNum;
	void (*LED_OFF)();//关闭
	void (*LED_ON)(LED_Num num);//打开指定数目的LED
	void (*LED_AUTO)(unsigned char dir);//LED自动补光

}LED_Manager;

extern LED_Manager LedMgr;//声明一个LED管理器

 /**
  * @brief  直接关闭9012三极管,使所有的LED一起熄灭,并复位IO
  * @param  void
  * @retval void
  */
void LED_OFF();


 /**
  * @brief  设置要点亮的LED数目(LED0~LED5)
	* @param  num:枚举数目(0-5个灯)
  * @retval void
  */
void LED_ON(LED_Num num);

 /**
  * @brief  LED的自动补光(每次调整只按1颗灯进行递增)
	* @param  dir: LIGHT: 补光,DARK:关闭
  * @retval void
  */
void LED_AUTO(unsigned char dir);

Tips: 定义了一个管理器方便统一的管理LED的状态…

 /**
  * @brief  根据预设的正常流明自动校准当前亮度,预设值可以更改
  * @param  void
  * @retval void
  */
void autoCalibrate(void)
{
		//得到系数后直接运行ADC转换
  	ad0=getADCValue();
		//进行流明单位的输出,
		lm=AD2LM(ad0,cali);
		numToStr(lm,LMStr);
		LCDShowStrs(mLocation,LMStr);
	if(lm<=MIN_LM)//比预设的正常流明小,进行自动补光
	{
		LedMgr.LED_AUTO(LIGHT);
	}


}

Tips: 自动补光也就这么回事嘛:实时监测当前亮度,比预设值小,就点亮一个LED,再检测,再点亮…

LCD1602要显示字符串和数字的处理

/********************LCD1602要显示的字符串*************************/
unsigned char code darkStr[]=		"Now_Dark:Set";
unsigned char code lightStr[]= 	"Now_Light:Set";
unsigned char code runStr[]= 		"Run....";
unsigned char LMStr[14]=				"NOW__LM:";//LMStr[8]开始设置值,记得末尾加'\0'
 /**
  * @brief  把数字加入到字符串
  * @param  lm:流明值,str:被插入的字符串(str[8]开始添加)
  * @retval void
  */
void numToStr(unsigned int lm,unsigned char* str)
{
	str[8]=lm/10000+0x30;//第5位
	str[9]=lm%10000/1000+0x30;//第4位
	str[10]=lm%10000%1000/100+0x30;//第3位
	str[11]=lm%10000%1000%100/10+0x30;//第2位
	str[12]=lm%10000%1000%100%10+0x30;//第1位
	str[13]='\0';
}

Tips: 显然,这没啥说的,记得字符串末尾加’\0’结束字符串。

ADC和校准系数的计算,流明的转换

#define ADCHANNEL 0		//模拟输入通道0
#define PCF_READ_ADRESS 	0x91	//PCF8591的读地址
#define PCF_WRITE_ADRESS 	0x90	//PCF8591的写地址


 /**
  * @brief  读取一次当前的ADC转换值 
  * @param  void
  * @retval void
  */
unsigned char getADCValue();
 /**
  * @brief  通过不同亮度下的2个AD值计算校准系数
  * @param  ad0:“暗”的AD,ad1:“亮”AD
  * @retval 计算得到的校准系数
  */
unsigned char calPara(unsigned char ad0,unsigned char ad1);


 /**
  * @brief  AD值通过校准系数来转换为流明
  * @param  ad:当前采集到的AD,cali:校准系数
  * @retval 转换得到的流明
  */
unsigned int AD2LM(unsigned char ad,unsigned char cali);

Tips: 把地址和通道定义成宏,方便以后硬件改变容易修改程序

Tips: 好了,差不多了,再总结一下流程:
上电复位–>>启动T0来运行按键扫描–>>检测EEP是否有校准系数–>>没有就进入设置界面进行设置–>>最后启动T1进行亮度检测和自动补光
proteus仿真图,Keil项目和AD项目在这里
提取码:65z7

猜你喜欢

转载自blog.csdn.net/qq_43572058/article/details/105606144