题目参看:
1、基本功能描述
- 通过单片机控制 8 个 LED 指示灯按照特定的顺序(工作模式)亮灭;
- 指示灯的流转间隔可通过按键调整,亮度可由电位器 RB2 进行控制;
- 各工作模式的流转间隔时间需在 E2PROM 中保存,并可在硬件重新上电后,自动载入。
2、设计说明
- 关闭蜂鸣器、继电器等与本试题程序设计无关的外设资源;
- 设备上电后默认数码管、LED 指示灯均为熄灭状态;
- 流转间隔可调整范围为 400ms-1200ms;
- 设备固定按照模式 1、模式 2、模式 3、模式 4 的次序循环往复运行。
3、LED 指示灯工作模式
- 模式 1:按照 L1、L2…L8 的顺序,从左到右单循环点亮。
- 模式 2:按照 L8、L7…L1 的顺序,从右到左单循环点亮。
- 模式 3:
- 模式 4:
4、亮度等级控制 检测电位器RB2的输出电压,控制8个LED指示灯的亮度,要求在0V-5V的可调区间内,实现 4 个均匀分布的 LED 指示灯亮度等级。
5、按键功能
- 按键 S7 定义为“启动/停止”按键,按下后启动或停止 LED 的流转。
- 按键 S6 定义为“设置”按键,按键按下后数码管进入“流转间隔” 设置界面,如下图所示:
通过按键 S6 可切换选择“运行模式”和“流转间隔”两个显示单元,当前被选择的显示单元以 0.8 秒为间隔亮灭 。
- 按键 S5 定义为“加”按键,在设置界面下,按下该键,若当前选择的是运行模式,则运行模式编号加 1,若当前选择的是流转间隔,则流转间隔增加 100ms。
- 按键 S4 定义为“减”按键,在设置界面下,按下该键,若当前选择的是运行模式,则运行模式编号减 1,若当前选择的是流转间隔,则流转间隔减少 100ms。
- 按键功能说明: a) 按键 S4、S5 的“加”、“减”功能只在“设置状态”下有效,数值的调整应注意边界属性。 b)在非“设置状态”下,按下 S4 按键可显示指示灯当前的亮度等级,4 个亮度等级从暗到亮,依次用数字 1、2、3、4 表示;松开S4 按键,数码管显示关闭,亮度等级的显示格式如下图所示:
分析题目:
- 用到哪几个底层? 数码管,按键,E2PROM(I2C),AD;
- 特殊功能:Led四个闪烁模式(流水灯的思想),亮度等级控制(这个应该是比较难的地方),按键功能和数码管闪烁功能和第八届的类似;
- 注意明确E2PRPOM的功能,理解原理。
功能实现:
一. 流水灯的四个工作模式
注意看清题目要求:设备固定按照模式 1、模式 2、模式 3、模式 4 的次序循环往复运行(emm,我刚开始就理解错了题意),模式1和模式2类似,模式3和模式4类似,那这里就只举模式1和模式3的例子吧:
void LedMode1()
{
static u8 shift1 = 0x01;//L1-L8
P2 = (P2 & 0x1F) | 0x80;
P0 = ~shift1;
PWMSTA = ~shift1;//不要直接操作P0口!!!!
shift1 <<= 1;
if(shift1 == 0x00)
{
runmode = 2;
shift1 = 0x01;
}
P2 &= 0x1F;
}
void LedMode3()
{
static u8 index = 1;
P2 = (P2 & 0x1F) | 0x80;
if(index == 1)
{
P0 = 0x7E;
PWMSTA = 0x7E;
index+=1;
}
else if(index == 2)
{
P0 = 0xBD;
PWMSTA = 0xBD;
index += 1;
}
else if(index == 3)
{
P0 = 0xDB;
PWMSTA = 0xDB;
index += 1;
}
else if(index == 4)
{
P0 = 0xE7;
PWMSTA = 0xE7;
index = 1;
runmode = 4;
}
P2 &= 0x1F;
}
做几点说明:
- 关于shift这个中间变量:为什么要定义一个中间变量呢?我们知道51上的LED是低电平点亮的,要实现流水灯的效果,我们需要让8个LED循环点亮,这就需要通过移位来实现,但是移0肯定是不行的,(以右移为例)移位后高位是补0的,那么我们实现的效果最后就是全亮的状态,所以(移1)设置一个中间变量,P0口的状态只要再取反就可以了;
- 关于PWMSTA这个变量:是用来存储P0口的状态的,调LED亮度的时候会用到,是一个全局变量,LED模式这部分比较简单,主要还是在后面。 二. 按键功能: 关于“启动/停止”功能,这个功能很好实现,只要设置一个标志量,然后在按键按下的时候改变标志量的值就可以了;
u8 flagstart = 0;//0-停止LED流转,1-开始LED流转
流转间隔设置,以及数码管闪烁,这个功能也比较好实现,数码管闪烁的思想和第八届的电子钟是一样的思想,需要注意的一点是:看清题目要求,题目中说 “各工作模式的流转间隔时间需在 E2PROM 中保存,并可在硬件重新上电后,自动载入”,说明是4个不同的模式分别对应四个不同的流转间隔,那么我们需要定义一个数组,用来存储4个模式下的流转间隔,并实现分别设置时间,看代码:
u16 runtime[4] = {400, 400, 400, 400};//四个模式下的闪烁间隔时间(注意类型错误)
void KeyAction(u8 keycode)
{
u8 buff[4];//定义一个数组用来存储缩小10倍后的流转间隔(因为E2PROM里最多写入u8类型的数据)
u8 i;
if(keycode == '1')
{
if(flagstart == 0)
{
flagstart = 1;
}
else if(flagstart == 1)
{
flagstart = 0;
}
}
else if(keycode == '2')
{
if(flagset == 0)
{
flagset = 1;
SetMode = 0;
}
else if(flagset == 1)
{
SetMode += 1;
if(SetMode > 1)
{
flagset = 0;
runmode = 1;
}
}
}
else if(keycode == '3')
{
AddTime();
for(i = 0; i<4; i++)
{
buff[i] = runtime[i] / 10;
}
E2Write(buff, 0x00, 4);//将数组中的值写入E2PROM
//(注意这个写法),写入地址从0x00开始,因为是4个字节的数据,所以字节数写4
}
else if(keycode == '4')
{
SubTime();
for(i = 0; i<4; i++)
{
buff[i] = runtime[i] / 10;
}
E2Write(buff, 0x00, 4);
}
}
三. 加减时间功能:(以加为例,减法类似)
u8 runmode = 1;//运行模式编号
u16 runtime[4] = {400, 400, 400, 400};//四个模式下的闪烁间隔时间
u8 SetMode = 0;//0-闪烁LED的模式,1-闪烁流转间隔
void AddTime()
{
u8 i;
if(SetMode == 0)
{
runmode+=1;
if(runmode >4)
{
runmode = 1;
}
}
else
{
for(i = 0; i<4; i++)
{
if(runmode == i+1)
{
runtime[i] += 100;
if(runtime[i] > 1200)
{
runtime[i] = 400;
}
}
}
}
}
模式1对应runtime[0],模式2对应runtime[1],模式3对应runtime[2],模式4对应runtime[3]
四. 设置模式(这里我写的不是很简便,有很多重复部分,读者可以自己优化一下)
numBlinkSta这个标志量是在中断里每隔0.8秒改变一次状态的,这样就实现了数码管每隔0.8秒亮灭了,第八届的题里我有详细写;
u8 numBlinkSta = 1;//0-数码管灭,1-数码管亮
void SetTime()
{
LedBuff[4] = 0xFF;//关闭第5个数码管
if(SetMode == 0)//设置LED的模式
{
if(numBlinkSta)
{
LedBuff[7] = 0xBF;//第8个数码管显示"-"
LedBuff[5] = 0xBF;//第6个数码管显示"-"
LedBuff[6] = LedChar[runmode];//第7个数码管显示运行模式
}
else
{
LedBuff[7] = 0xFF;
LedBuff[5] = 0xFF;
LedBuff[6] = 0xFF;
}
LedBuff[0] = LedChar[runtime[runmode-1] % 10];
LedBuff[1] = LedChar[(runtime[runmode-1]/10) % 10];
LedBuff[2] = LedChar[(runtime[runmode-1]/100) % 10];
if(runtime[runmode-1] < 1000)
{
LedBuff[3] = 0xFF;
}
else
{
LedBuff[3] = LedChar[(runtime[runmode-1]/1000) % 10];
}
}
else if(SetMode == 1)//设置流转间隔
{
if(numBlinkSta)
{
LedBuff[0] = LedChar[runtime[runmode-1] % 10];
LedBuff[1] = LedChar[(runtime[runmode-1]/10) % 10];
LedBuff[2] = LedChar[(runtime[runmode-1]/100) % 10];
if(runtime[runmode-1] < 1000)
{
LedBuff[3] = 0xFF;
}
else
{
LedBuff[3] = LedChar[(runtime[runmode-1]/1000) % 10];
}
}
else
{
LedBuff[0] = 0xFF;
LedBuff[1] = 0xFF;
LedBuff[2] = 0xFF;
LedBuff[3] = 0xFF;
}
LedBuff[7] = 0xBF;
LedBuff[5] = 0xBF;
LedBuff[6] = LedChar[runmode];
}
}
五. 重头戏!!!关于调整亮度!!!
用到的也是PWM波的思想,先明确一点:我们是如何实现亮度调节的呢?说白了,就是在极短的一段时间里让小灯灭一段时间,然后再亮一段时间,这样在视觉上看起来灯的亮度就是发生变化的,看代码,有详细注释:
u8 brightness = 0;//亮度等级
void ConfigTimer1()//软件生成的10us定时器(每10us进一次中断)
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x10; //设置定时器模式
TL1 = 0xF7; //设置定时初值
TH1 = 0xFF; //设置定时初值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
ET1 = 1;//注意这里软件生成的函数里是没有这条语句的,要自己加上!!!!
}
void InterruptTimer1()interrupt 3
{
static u16 PWMCnt = 0;//存储进入中断的次数
TL1 = 0xF7; //设置定时初值
TH1 = 0xFF; //设置定时初值
PWMCnt++;
PWMCnt %= 101;//666的操作,实现满100清零,设置总时间为1ms,即PWM波的频率为1k(在1ms的时间里让小灯灭hightime的时间,其余时间是亮的状态)
//这里就体现了之前的PWM波的思想了,只不过之前的那个是更高级的操作,就是频率可调
//这里我们是直接限定了频率
P2 = (P2 & 0x1F) | 0x80;
if(PWMCnt >= hightime)//如果进入中断扫描的时间大于等于高电平持续的时间
{
P0 = PWMSTA;//点亮对应状态下的led
}
else
{
P0 = 0xFF;//否则关闭所有的小灯
}
P2 &= 0x1F;
}
u8 AdjustDC()//判断高电平的时间,实现调节rb2,灯的亮度发生改变(高电平时间需要在主函数里不断获取,每隔200ms获取一次即可)
{
u16 val;
val = GetADCValue(3);
if(val<60)
{
hightime = 90;
brightness = 1;
}
else if(val <130)//这里必须大于等于130(否则不会显示这个等级的亮度)
{
hightime = 70;
brightness = 2;
}
else if(val <195)//这里必须大于190,emm这个数据是后期调的时候发现的,不太懂为啥
{
hightime = 30;
brightness = 3;
}
else
{
hightime = 5;
brightness = 4;
}
return hightime;
}
emmm,读者可以好好看看注释,还有一点我觉得特别重要的地方!!!就是什么时候读取E2PROM的问题:读取E2PROM里面的数据必须要放在总中断打开之前,主函数里读取一次就可以了,为什么呢?因为我们是一次读取四个字节的数据的,而我们的定时器1是一个10us定时器,时间太快了,会影响到E2PROM读取数据,比如说我们正在读第一个字节的数据,这时候中断来了,我们需要进入中断,那么就会扰乱E2PROM读取数据;
另外还要注意!!!刚下载程序的时候,要先向E2PROM里面写入数据,不能先读取,也就是说下第一遍程序的时候要先把E2Read这个函数屏蔽,看一下代码:
u8 runtimebuff[4];
E2Read(runtimebuff, 0x00, 4);//一个字节是1个u8,E2Prom的地址范围是0x00到0xFF(数组的名字就是地址,不需要再取地址符)
for(i = 0;i <4; i++)
{
runtime[i] = runtimebuff[i] * 10;
}
EA = 1;
CloseOther();
ConfigTimer0(2);
ConfigTimer1();
六. 按下S4显示亮度等级
这个功能和第八届省赛试题里,按下按键显示温度差不多,我们在AdjustDC这个函数里面改变的亮度等级,只要在按下S4的时候显示出来就可以了,需要注意的是一直按着S4才会显示,一旦松开就不显示了,所以我们需要一直扫描S4的状态,那么我们就在KeyScan这个按键扫描函数里处理不就可以了吗,看代码:
void KeyScan()
{
static u8 keybuff[4] = {0xFF, 0xFF, 0xFF, 0xFF};
u8 i;
keybuff[0] = (keybuff[0] << 1) | KEY_IN_1;
keybuff[1] = (keybuff[1] << 1) | KEY_IN_2;
keybuff[2] = (keybuff[2] << 1) | KEY_IN_3;
keybuff[3] = (keybuff[3] << 1) | KEY_IN_4;
for(i = 0; i<4; i++)
{
if(keybuff[i] == 0x00)
{
KeySta[i] = 0;
if(keybuff[3] == 0x00)
{
if(flagset == 0)
{
ShowNumber(brightness);//注意shownumber函数调用时会自动关闭除显示数字之外的数码管,所以要把他放前面
LedBuff[1] = 0xBF;
}
}
}
else if(keybuff[i] == 0xFF)
{
KeySta[i] = 1;
}
}
}
具体功能就是这样实现的,注意一下细节,打好底层,基本上就可以写出来了,我会再写一遍程序,遇到问题的话还会再更新~
第一遍遇到的问题:
1. nice的操作:到8归零,两种方法:index &= 7(是2的整数次方才能用这个方法)或者!!!!!!index %= 8;(通用!!!!!)
2. 每调试一个地方就要下载验证一下,再次强调一下E2PROM的操作:E2Read(runtimebuff, 0x00, 4);//一个字节是1个u8,E2Prom的地址范围是0x00到0xFF(数组的名字就是地址,不需要再取地址符)
3. 要先往E2PROM里面写入你想写入的数据,然后再读,否则你从E2PROM里面读到的是E2PROM里原有的值!!!!!
4. 另外!!!要特别注意数据溢出的问题!!!!!
5. 关于软件生成的10us定时器,有几点需要注意的问题:
6. 进入中断时,不要忘记设置定时初值!!!!!!!
第二遍遇到的问题:
- 尽量不要使用ShowNumber这个函数,直接操作LedBuff[]比较好 ,我第二遍做的时候想把SetTime这个函数写得简单一点,然后就用到了ShowNumber这个函数,但是出现了问题,一个是数码管显示不稳定,还有一个就是亮度显示部分也出现了问题;
- 对寄存器执行操作时,哪里用到就在哪里使用,用完就关闭:
- 还有就是 if 和 else if 的问题: