本人太懒了,有很多次想写博客,单只是想想罢了,要去备战考研了。等过了考研之后,我一定要多学东西,目前学的东西太少了。。。。看不起自己啊!废话不多说,由于有这个课程设计,所以一并写在这里吧!扬帆,启航!
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