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
- 基于51单片机利用ADC0808芯片实现A/D转换。
- 51 单片机汇编语言:利用 RET 指令实现多路分支
- 利用51系列单片机定时器功能实现测量脉冲宽度
- 利用MCS-51系列单片机和0038模块实现红外线解码
- 利用 51 单片机实现 0.0 ~ 10 秒表
- Arduino + Cubieboard + 继电器 + 光敏电阻 实现天亮自动播放音乐
- 51单片机——利用DS12C887实现时钟
- 利用单片机的GPIO外接分立电阻权编码网络实现AD转换(原创)
- 51/52单片机:利用外部中断实现4位(多位)数码管动态扫描+1/-1--------计数器
- 五、光敏电阻实现手机的自动屏幕亮度效果
- 利用meta实现网页自动刷新和自动跳转
- 利用AOP实现SqlSugar自动事务
- android中实现利用距离传感器实现自动锁屏
- 利用JS实现表单的自动提交
- 利用Ant实现项目自动构建测试备份并发布到项目web
- 利用SUS实现自动补丁管理
- 利用jenkins实现CI/CD的自动、持续构建及测试软件
- 利用Selenium Webdriver 2.0 实现从Web自动保存文件到本地
- Linux基础之-利用shell脚本实现自动监控系统服务
- Vim中利用OmniCppComplete实现C++代码自动补全