本章内容主要通过一些相关例程,来提高大家的编程技巧,并且帮助大家进行一些算法上的积累。同学们在做这部分内容的时候,还是那句话,一定要能够达到不看教程,独立把程序做出来的效果,那样才能基本上掌握相关知识点和内容。 10.1 数字秒表实验10.1.1 不同数据间的类型转换在C语言中,不同数据类型之间是可以混合运算的。当表达式中的数据类型不一致时,首先转换为同一种类型,然后再进行计算。C语言有两种方法实现类型转换,一是自动类型转换,另外一种是强制类型转换。这块内容是比较繁杂的,因此我们根据我们常用的编程应用来讲部分相关内容。 当不同数据类型之间混合运算的时候,不同类型的数据首先会转换为同一类型,转换的主要原则是:短字节的数据向长字节数据转换。 比如:unsigned char a ; unsigned int b; unsigned int c; c = a *b; 在运算的过程中,程序会自动全部按照unsigned int型来计算。比如a=10,b=200,c的结果就是2000。那当a=100,b=700,那c是70000吗?新手最容易犯这种错误,大家要注意每个变量的数据类型,c的数据类型是unsigned int型,取值范围是0~65535,70000超过65535溢出了,所以最终c的结果是(70000 - 65536) = 4464。 那要想让c正常获得70000这个结果,需要把c定义成一个unsigned long型。我们如果写成:unsigned char a=100; unsigned int b=700; unsigned long c=0; c = a *b;如果有做过实验的同学,会发现这个c的结果还是4464,这个是个什么情况呢? 大家注意,C语言不同类型运算的时候数值会转换同一类型运算,但是每一步运算都会进行识别判断,不会进行一个总的分析判断。比如我们这个程序,a和b相乘的时候,是按照unsigned int类型运算的,运算的结果也是unsigned int类型的4464,只是最终把unsigned int类型4464赋值给了一个unsigned long型的变量而已。我们在运算的时候如何避免这类问题的产生呢?可以采用强制类型转换的方法。 在一个变量前边加上一个变量类型,并且这个变量类型用小括号括起来,表示把这个变量强制转换成括号里的变量类型。如 c = (unsigned long)a * b;由于强制类型转换运算优先级高于*,所以这个地方的运算是先把a转换成一个unsigned long型的变量,而后与b相乘,根据C语言的规则b会自动转换成一个unsigned long型的变量,而后运算完毕结果也是一个unsigned long型的,最终赋值给了c。 当不同类型变量相互赋值时,短字节的数据向长字节的变量赋值时,值不变,比如unsigned char a=100; unsigned int b=700; b = a;那么最终b的值就是100了。但是如果我们的程序是unsigned char a=100; unsigned int b=700; a=b;那么a的值仅仅是取了b的低8位,我们首先要把700变成一个16位的二进制数据,然后取它的低8位出来,也就是188,这就是长字节给短字节赋值的结果。 在51单片机里边,有一种特殊情况,就是bit类型的变量,这个bit类型的强制类型转换,是不符合上边讲的这个原则的,比如bit a = 0; unsigned char b; a = (bit)b;这个地方要特别注意,使用bit做强制类型转换,不是取b的最低位,而是他会判断b这个变量是0还是非0的值,如果b是0,那么a的结果就是0,如果b是任意非0的其他数字,那么a的结果都是1。 1.1.2 定时时间精准性调整我们的6.5.2章节有一个数码管秒表显示程序,那个程序是1秒数码管加1,但是细心的同学做了实验后,经过长时间运行会发现,和我们的实际的时间有了较大误差了,那如何去调整这种误差呢?要解决问题,先找到问题是什么原因造成的。 先补充介绍一下我们我们前边讲的中断的内容。其实单片机做的很智能的,当我们在看电视的时候,突然发生了水开的中断,我们必须去提水的时候,第一,我们从电视跟前跑到厨房需要一定的时间,第二,因为我们看的电视是智能数字电视,因此在去提水之前我们可以使用遥控器将我们的电视进行暂停操作,方便回来后继续从刚才的剧情往下进行。那暂停电视,跑到厨房提水,这一点点时间是很短的,在实际生活中可以忽略不计,但是在单片机秒表系统,误差会累计的,每1秒钟都差了几个微妙,时间一久,造成的累计误差就不可小觑了。 单片机系统里,硬件进入中断需要一定的时间,大概是几个机器周期,还有要进行原始数据保护,就是把进中断之前程序运行的一些变量先保存起来,这个专业词汇叫做中断压栈,进入中断后,重新给定时器TH和TL赋值,也需要几个机器周期,这样下来就会消耗一定的时间,我们得把这些时间补偿回来。 方法一,使用软件debug进行补偿。 我们前边教程讲过使用debug来观察程序运行时间,那我们可以把我们2次进入中断的时间间隔观察出来,看看和我们实际定时的时间相差了几个机器周期,然后在进行定时器初值赋值的时候,进行一个调整。我们用的是11.0592M的晶振,发现差了几个机器周期,就把定时器初值加上几个机器周期,这样相当于进行了一个补偿。 方法二,使用累计误差计算出来。 有的时候,除了程序本身存在的误差外,硬件精度也可能会影响到时钟的精度,比如晶振,会随着温度变化出现温漂现象,就是精度和标称值要差一点。那么我们还可以采取累计误差的方法来提高精度。比如我们可以让时钟运行半个小时或者一个小时,看看最终时间差了几秒,然后算算一共进了多少次定时器中断,然后把这差的几秒平均分配到每次的定时器中断中,就可以实现时钟的调整。 大家要明白,这个世界上本就没有绝对的精度,我们只能提高精度,但是不可能消除误差的,如果在这个基础上还感觉精度不够的话,不要着急,后边我们会专门讲时钟芯片的,通常时钟芯片计时的精度比单片机的精度要高一些。 10.1.3 使用字节操作修改位的技巧这里介绍个编程小技巧,在我们编程序的时候,有的情况下,想要操作一个字节中的某一位或者几位的时候,但是又不想改变其他位原有的值,该如何操作呢? 比如我们学定时器的时候遇到一个寄存器TCON,这个寄存器是可以进行位操作的,比如我们可以直接写TR0 =1;TR0是TCON的一个位,因为这个寄存器是允许位操作,这样写是没有任何问题的。还有一个寄存器TMOD,这个寄存器是不支持位操作的,那如果我们要使用T0的模式1,我们希望达到的效果是TMOD的低4位是0001,如果我们直接写成TMOD = 0x01的话,实际上已经同时操作到了高4位,即属于T1的部分,设置成了0000,如果T1定时器没有用到的话,那我们随便怎么样都行,但是如果程序中既用到了T0,又用到了T1,那我们设置T0的同时已经干扰到了T1的模式配置,这我们不希望看到的结果。 在这种情况下,就可以用我们前边学过的"&"和"|"运算了。对于二进制位操作来说,不管该位原来的值是0还是1,它跟"0"进行"&&"运算,得到的结果都是0,而跟"1"进行"&&"运算,将保持原来的值不变;不管该位原来的值是0还是1,它跟"1"进行"||"运算,得到的结果都是1,而跟"0"进行"||"运算,将保持原来的值不变。 利用上述这个规律,我们就可以着手解决刚才的问题了。如果我们现在要设置TMOD的定时器0工作在模式1下,又不干扰定时器1的配置,我们可以进行这样的操作:TMOD = TMOD & 0xF0; TMOD = TMOD | 0x01;第一步与0xF0后,TMOD的高4位不变,低4位清零,变成了xxxx0000;然后再进行第二步和0x01进行或运算,那高7位均不变,最低位变成1了,这样就完成了只将低4位的值修改位0001,而高4位保持原不变的任务,即只设置了T0而不影响T1。熟练掌握并灵活运用这个方法,会给你以后的编程带来便利。 另外,在C语言中,a &= b;等价于a = a&b;同理,a |= b;等价于a = a|b;那么前边那一段代码就可以写成TMOD &= 0xF0;TMOD |= 0x01这样的简写形式。这种写法可以一定程度上简化代码,是C语言的一种编程风格。 10.1.4 数码管刷新函数算法改进在学习数码管动态刷新的时候,为了方便大家理解,我们程序写的细致一些,给大家引入了switch的用法,随着我们编程能力的增强,对于74HC138这种非常有规律的数字器件,我们在编程上也可以改进一下逻辑算法,让程序变的更简洁。这种逻辑算法,通常不是靠学一下可以全部掌握的,而是通过不断的编写程序以及研究别人的程序一点点积累起来的,从今天开始,大家就要开始积累。 前边动态刷新函数我们是这么写的: switch(j) { case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]]; break; case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]]; break; case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]]; break; case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]]; break; case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]]; break; case 5: ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]]; break; default: break; } 首先我们进行第一步改进,写成: switch(j) { case 0: ADDR0=0; ADDR1=0; ADDR2=0; break; case 1: ADDR0=1; ADDR1=0; ADDR2=0; break; case 2: ADDR0=0; ADDR1=1; ADDR2=0; break; case 3: ADDR0=1; ADDR1=1; ADDR2=0; break; case 4: ADDR0=0; ADDR1=0; ADDR2=1; break; case 5: ADDR0=1; ADDR1=0; ADDR2=1; break; default: break; } P0=LedChar[LedNumber[j++]]; if(6==j) j=0; 这种写法已经比上边那种写法简单多了,我们还要继续简化。我们来看,ADDR0是P1的第0位,ADDR1是P1的第1位,ADDR2是P1的第2位,我们可以看出来,程序中的case 0到case 5的过程中,P1的这低3位的值分别是000,001,010,011,100,101。转换成十进制,也就是从0到5。那我们程序就可以进一步改进,写成以下函数形式: void LedScan() //LED显示扫描函数 { static unsigned char index = 0; P0 = 0xFF; //关闭所有段选位,显示消隐 P1 = (P1 & 0xF8) | index; //位选索引值赋值到P1口低3位 P0 = LedNumber[index]; //相应显示缓冲区的值赋值到P0口 if (index < 5) //位选索引0-5循环,因有6个数码管 index++; else index = 0; } 大家看看,P1 = (P1 & 0xF8) | index;这行代码就利用了上面讲到的"&"和"|"运算来将index的低3位直接赋值到P1口的低3位上,这样写是不是要简洁的多,也巧妙的多,同样可以完美实现动态刷新的功能。 10.1.5 秒表程序做了一个秒表程序给同学们做参考,程序中涉及到的知识点我们几乎都讲过了,涉及到了定时器、数码管、中断、按键等多个知识点。此程序是多知识点同时应用到一个程序中的小综合,因此需要大家完全消化掉。这种小综合也是将来做大项目程序的一个基础,因此还是老规矩,大家边抄边理解,理解透彻后独立写出来就算此关通过。 #include <reg52.h>
sbit KEY1 = P2^4;
sbit KEY2 = P2^5;
sbit KEY3 = P2^6;
sbit KEY4 = P2^7;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
unsigned char code LedChar[] = { //数码管显示字符转换表
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[6] = { //数码管显示缓冲区
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char KeySta[4] = { //按键状态缓冲区
1, 1, 1, 1
};
bit StopwatchRunning = 0; //秒表运行标志
bit StopwatchRefresh = 1; //秒表计数刷新标志
unsigned char DecimalPart = 0; //秒表的小数部分
unsigned int IntegerPart = 0; //秒表的整数部分
unsigned char T0RH = 0; //T0重载值的高字节
unsigned char T0RL = 0; //T0重载值的低字节
void ConfigTimer0(unsigned int ms);
void StopwatchDisplay();
void KeyAction();
void main ()
{
P2 = 0xFE; //选择第4行按键以进行扫描
P0 = 0xFF; //P0口初始化
ADDR3 = 1; //选择数码管
ENLED = 0; //LED总使能
EA = 1; //开总中断
ConfigTimer0(2); //配置T0定时2ms
while(1)
{
KeyAction();
StopwatchDisplay();
}
}
void ConfigTimer0(unsigned int ms) //T0配置函数
{
unsigned long tmp;
tmp = 11059200 / 12; //定时器计数频率
tmp = (tmp * ms) / 1000; //计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp + 19; //修正中断响应延时造成的误差,运行30分钟修正值
T0RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0为模式1
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
}
void StopwatchDisplay() //秒表计数显示函数
{
unsigned char i;
unsigned char buff[6];
if (StopwatchRefresh)
{
StopwatchRefresh = 0;
i = DecimalPart % 10; //小数部分转换到低2位
buff[0] = LedChar[ I];
i = DecimalPart / 10;
buff[1] = LedChar[ I];
buff[2] = IntegerPart % 10; //整数部分转换到高4位
buff[3] = (IntegerPart / 10) % 10;
buff[4] = (IntegerPart / 100) % 10;
buff[5] = (IntegerPart / 1000) % 10;
for (i=5; i>=3; i--) //高位的0转换为空字符
{
if (buff[ I] == 0)
buff[ I] = 0xFF;
else
break;
}
for ( ; i>=2; i--) //有效数字位转换显示字符
{
buff[ I] = LedChar[buff[ I]];
}
buff[2] &= 0x7F; //点亮小数点
for (i=0; i<=5; i++) //一次性拷贝到显示缓冲区,消除可能存在的显示抖动
{
LedBuff[ I] = buff[ I];
}
}
}
void StopwatchAction() //秒表启停函数
{
if (StopwatchRunning) //已启动则停止
StopwatchRunning = 0;
else //未启动则启动
StopwatchRunning = 1;
}
void StopwatchReset() //秒表复位函数
{
StopwatchRunning = 0; //停止秒表
DecimalPart = 0; //清零计数值
IntegerPart = 0;
StopwatchRefresh = 1; //置刷新标志
}
void KeyAction() //按键动作函数
{
unsigned char i;
static unsigned char backup[4] = {1,1,1,1};
for (i=0; i<4; i++)
{
if (backup[ I] != KeySta[ I])
{
if (backup[ I] != 0) //按键按下时执行动作
{
switch (i)
{
case 1: StopwatchReset(); break; //Esc键复位秒表
case 2: StopwatchAction(); break; //回车键启停秒表
default: break;
}
}
backup[ I] = KeySta[ I];
}
}
}
void LedScan() //LED显示扫描函数
{
static unsigned char index = 0;
P0 = 0xFF; //关闭所有段选位,显示消隐
P1 = (P1 & 0xF8) | index; //位选索引值赋值到P1口低3位
P0 = LedBuff[index]; //相应显示缓冲区的值赋值到P0口
if (index < 5) //位选索引0-5循环,因有6个数码管
index++;
else
index = 0;
}
void KeyScan() //按键扫描函数
{
unsigned char i;
static unsigned char keybuf[4] = { //按键扫描缓冲区,保存一段时间内的扫描值
0xFF, 0xFF, 0xFF, 0xFF
};
//按键值移入缓冲区
keybuf[0] = (keybuf[0] << 1) | KEY1;
keybuf[1] = (keybuf[1] << 1) | KEY2;
keybuf[2] = (keybuf[2] << 1) | KEY3;
keybuf[3] = (keybuf[3] << 1) | KEY4;
//消抖后更新按键状态
for (i=0; i<4; i++)
{
if (keybuf[ I] == 0x00)
{
KeySta[ I] = 0;
}
else if (keybuf[ I] == 0xFF)
{
KeySta[ I] = 1;
}
}
}
void StopwatchCount() //秒表计数函数
{
if (StopwatchRunning)
{
DecimalPart++; //小数部分+1
if (DecimalPart >= 100) //小数部分计到100时进位到整数部分
{
DecimalPart = 0;
IntegerPart++;
if (IntegerPart >= 10000) //整数部分计到10000时归零
{
IntegerPart = 0;
}
}
StopwatchRefresh = 1;
}
}
void InterruptTimer0() interrupt 1 //T0中断服务函数
{
static unsigned char tmr10ms = 0;
TH0 = T0RH; //将重载值赋值到计数器
TL0 = T0RL;
KeyScan(); //按键扫描
LedScan(); //数码管扫描显示
//定时10ms进行一次秒表计数
tmr10ms++;
if (tmr10ms >= 5)
{
tmr10ms = 0;
StopwatchCount();
}
}10.2 PWM的学习PWM在我们今后的单片机应用中非常非常多,应用的方向也很多,它的原理很简单,但是往往应用于不同场合上意义不完全一样,这里我先把基本概念和基本原理给大家介绍一下,后边遇到用的时候起码知道是个什么东西。 PWM是Pulse Width Modulation的缩写,它的中文名字是脉冲宽度调制,一种说法是它利用微处理器的数字输出来对模拟电路进行控制的一种有效的技术,其实就是使用数字信号达到一个模拟信号的效果。这是个什么概念呢?我们一步步来介绍。 首先从它的名字来看,脉冲宽度调制,就是改变脉冲宽度来实现不同的效果。我们先来看三组不同的脉冲信号,如图10-1所示。 图10-1 PWM波形 这是一个周期是10ms,即频率是100Hz的波形,但是每个周期内,高低电平脉冲各不相同,这就是PWM的本质。在这里大家要记住一个概念,叫做“占空比”。占空比是指高电平的时间占整个周期的比例。比如第一部分波形的占空比是40%,第二部分波形占空比是60%,第三部分波形占空比是80%,这就是PWM的解释。 那为何它会对模拟电路进行控制呢?大家想一想,我们数字电路里,只有0和1两种状态,比如我们教程第二课学会的点亮LED小灯那个程序,当我们写一个LED = 0;小灯就会长亮,当我们写一个LED = 1;小灯就会灭掉。当我们让小灯亮和灭间隔运行的时候,小灯是闪烁。如果我们把这个间隔不断的减小,减小到我们的肉眼分辨不出来,也就是100Hz以上的频率,这个时候小灯表现出来的现象就是既保持亮的状态,但是亮度没有LED = 0;的时候亮度高。那我们不断改变时间参数,让LED = 0;的时间大于或者小于LED = 1;的时间,会发现亮度都不一样,这就是模拟电路的感觉了,不再是纯粹的0和1,还有亮度不断变化。大家会发现,如果我们是100Hz的信号,如图10-1所示,假如高电平熄灭小灯,低电平点亮小灯的话,第一部分波形熄灭4ms,点亮6ms,亮度最高,第二部分熄灭6ms,点亮4ms,亮度次之,第三部分熄灭8ms,点亮2ms,亮度最低。我们用程序验证一下。 #include <reg52.h> sbit PWMOUT = P0^0; sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned char HReloadH = 0; //高电平重载值的高字节 unsigned char HReloadL = 0; //高电平重载值的低字节 unsigned char LReloadH = 0; //低电平重载值的高字节 unsigned char LReloadL = 0; //低电平重载值的低字节 void ConfigPWM(unsigned int fr, unsigned char dc); void ClosePWM(); void main () { unsigned int i; P0 = 0xFF; //P0口初始化 ADDR0 = 0; //选择独立LED ADDR1 = 1; ADDR2 = 1; ADDR3 = 1; ENLED = 0; //LED总使能 EA = 1; //开总中断 while(1) { ConfigPWM(100, 10); //频率100Hz,占空比10% for (i=0; i<40000; i++); ClosePWM(); ConfigPWM(100, 40); //频率100Hz,占空比40% for (i=0; i<40000; i++); ClosePWM(); ConfigPWM(100, 90); //频率100Hz,占空比90% for (i=0; i<40000; i++); ClosePWM(); for (i=0; i<40000; i++); } } void ConfigPWM(unsigned int fr, unsigned char dc) //PWM配置函数,fr-频率,dc-占空比 { unsigned int high, low; unsigned long tmp; tmp = (11059200 / 12) / fr; //计算一个周期所需的计数值 high = (tmp * dc) / 100; //计算高电平所需的计数值 low = tmp - high; //计算低电平所需的计数值 high = 65536 - high + 13; //计算高电平的定时器重载值并修正 low = 65536 - low + 13; //计算低电平的定时器重载值并修正 HReloadH = (unsigned char)(high >> 8); //高电平重载值拆分为高低字节 HReloadL = (unsigned char)high; LReloadH = (unsigned char)(low >> 8); //低电平重载值拆分为高低字节 LReloadL = (unsigned char)low; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = HReloadH; //加载T0重载值 TL0 = HReloadL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 PWMOUT = 1; //输出高电平 } void ClosePWM() //关闭PWM { TR0 = 0; //停止定时器 ET0 = 0; PWMOUT = 1; //输出高电平 } void InterruptTimer0() interrupt 1 //T0中断服务函数 { if (PWMOUT == 1) //当前输出为高电平时,装载低电平值并输出低电平 { TH0 = LReloadH; TL0 = LReloadL; PWMOUT = 0; } else //当前输出为低电平时,装载高电平值并输出高电平 { TH0 = HReloadH; TL0 = HReloadL; PWMOUT = 1; } } 大家下载了这个程序,会发现小灯从最亮到灭一共4个亮度等级。如果我们让亮度等级更多,并且让亮度等级连续起来,会产生一个小灯渐变的效果,和人呼吸有点类似,所以我们习惯上称之为呼吸灯,程序代码如下,这个程序用了2个定时器2个中断,这是我们第一次这样用,大家可以学习一下。我们来试试这个程序,试完了大家一定要自己关闭教程把程序写出来,切记。 #include <reg52.h> sbit PWMOUT = P0^0; sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned long PeriodCnt = 0; //PWM周期计数值 unsigned char HReloadH = 0; //高电平重载值的高字节 unsigned char HReloadL = 0; //高电平重载值的低字节 unsigned char LReloadH = 0; //低电平重载值的高字节 unsigned char LReloadL = 0; //低电平重载值的低字节 unsigned char T1RH = 0; //T1重载值的高字节 unsigned char T1RL = 0; //T1重载值的低字节 void ConfigTimer1(unsigned int ms); void ConfigPWM(unsigned int fr, unsigned char dc); void main () { P0 = 0xFF; //P0口初始化 ADDR0 = 0; //选择独立LED ADDR1 = 1; ADDR2 = 1; ADDR3 = 1; ENLED = 0; //LED总使能 EA = 1; //开总中断 ConfigPWM(100, 10); //配置并启动PWM ConfigTimer1(50); //T1定时调整占空比 while(1); } void ConfigTimer1(unsigned int ms) //T1配置函数 { unsigned long tmp; tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 11; //修正中断响应延时造成的误差 T1RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 T1RL = (unsigned char)tmp; TMOD &= 0x0F; //清零T1的控制位 TMOD |= 0x10; //配置T1为模式1 TH1 = T1RH; //加载T1重载值 TL1 = T1RL; ET1 = 1; //使能T1中断 TR1 = 1; //启动T1 } void ConfigPWM(unsigned int fr, unsigned char dc) //PWM配置函数,fr-频率,dc-占空比 { unsigned int high, low; PeriodCnt = (11059200 / 12) / fr; //计算一个周期所需的计数值 high = (PeriodCnt * dc) / 100; //计算高电平所需的计数值 low = PeriodCnt - high; //计算低电平所需的计数值 high = 65536 - high + 13; //计算高电平的定时器重载值并修正 low = 65536 - low + 13; //计算低电平的定时器重载值并修正 HReloadH = (unsigned char)(high >> 8); //高电平重载值拆分为高低字节 HReloadL = (unsigned char)high; LReloadH = (unsigned char)(low >> 8); //低电平重载值拆分为高低字节 LReloadL = (unsigned char)low; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = HReloadH; //加载T0重载值 TL0 = HReloadL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 PWMOUT = 1; //输出高电平 } void AdjustDutyCycle(unsigned char dc) //占空比调整函数,频率不变只调整占空比 { unsigned int high, low; high = (PeriodCnt * dc) / 100; //计算高电平所需的计数值 low = PeriodCnt - high; //计算低电平所需的计数值 high = 65536 - high + 13; //计算高电平的定时器重载值并修正 low = 65536 - low + 13; //计算低电平的定时器重载值并修正 HReloadH = (unsigned char)(high >> 8); //高电平重载值拆分为高低字节 HReloadL = (unsigned char)high; LReloadH = (unsigned char)(low >> 8); //低电平重载值拆分为高低字节 LReloadL = (unsigned char)low; } void InterruptTimer0() interrupt 1 //T0中断服务函数,产生PWM { if (PWMOUT == 1) //当前输出为高电平时,装载低电平值并输出低电平 { TH0 = LReloadH; TL0 = LReloadL; PWMOUT = 0; } else //当前输出为低电平时,装载高电平值并输出高电平 { TH0 = HReloadH; TL0 = HReloadL; PWMOUT = 1; } } void InterruptTimer1() interrupt 3 //T1中断服务函数,定时动态调整占空比 { static bit br = 0; static unsigned char index = 0; unsigned char code table[13] = { //占空比调整表 5, 18, 30, 41, 51, 60, 68, 75, 81, 86, 90, 93, 95 }; TH1 = T1RH; //重新加载T1重载值 TL1 = T1RL; AdjustDutyCycle(table[index]); //调整PWM的占空比 if (br == 0) //逐步增大占空比 { index++; if (index >= 12) { br = 1; } } else //逐步减小占空比 { index--; if (index == 0) { br = 0; } } } 呼吸灯写出来后,其他各种效果的灯光闪烁都应该可以做出来,大家看到的KTV里边那绚丽的灯光闪烁,其实就是采用的PWM技术控制的。 10.3 交通灯实验同学们在学习技术的时候,一定要多动脑筋,遇到问题后,三思而后问。有些时候你考虑的和真理就差一点点了,没有坚持下去,别人告诉你后你才恍然大悟。这样得到的结论,可以让你学到知识,但是却培养不了你的逻辑思维能力。不是不能问,而是要在认真思考的基础上提问。 有同学有疑问,板子上只有8个流水灯,那如果我要做很多个流水灯一起花样显示怎么办呢?那我们在讲课的时候其实都提到过了,板子上是有8个流水灯,还有6个数码管,还有1个点阵LED,一个数码管相当于8个小灯,一个点阵LED相当于64个小灯,那如果全部算上的话,我们板子上实际共接了8+6*8+64=120个小灯,你如果单独只接小灯,花样灯就做出来了。 还有同学问,板子上流水灯和数码管可以一起工作吗?如何一起工作呢?我们刚说了,一个数码管是8个小灯,但是大家反过来想一想,8个流水灯是不是相当于一个数码管吗。那板子上6个数码管我们可以让他们同时亮,7个数码管就不会了吗?当然了,思考的习惯是要慢慢培养的,想不到的同学继续努力,每天前进一小步,坚持一段时间后回头看看,就会发现你学会了很多。 发一个交通灯的程序给大家做学习参考。因为板子资源有限,所以我把左边LED8和LED9一起亮作为绿灯,把LED5和LED6一起亮作为黄灯,把LED2和LED3一起亮作为红灯,用数码管做倒计时,做了一个简易的交通灯程序给大家做参考学习,让LED和数码管同时参与工作。 #include <reg52.h> sbit KEY1 = P2^4; sbit KEY2 = P2^5; sbit KEY3 = P2^6; sbit KEY4 = P2^7; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned char code LedChar[] = { //数码管显示字符转换表 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; unsigned char LedBuff[7] = { //数码管+独立LED显示缓冲区 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; bit flag1s = 1; //1秒定时标志 unsigned char T0RH = 0; //T0重载值的高字节 unsigned char T0RL = 0; //T0重载值的低字节 void ConfigTimer0(unsigned int ms); void TrafficLight(); void main () { P2 = 0xFE; //选择第4行按键以进行扫描 P0 = 0xFF; //P0口初始化 ADDR3 = 1; //选择数码管 ENLED = 0; //LED总使能 EA = 1; //开总中断 ConfigTimer0(1); //配置T0定时1ms while(1) { if (flag1s) { //每秒执行一次 flag1s = 0; TrafficLight(); } } } void ConfigTimer0(unsigned int ms) //T0配置函数 { unsigned long tmp; tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 17; //修正中断响应延时造成的误差 T0RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = T0RH; //加载T0重载值 TL0 = T0RL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 } void TrafficLight() { static unsigned char color = 2; //交通灯颜色索引,0-绿色,1-黄色,2-红色 static unsigned char timer = 0; //交通灯倒计时定时器 if (timer == 0) //倒计时到0时,切换交通灯 { switch (color) { //左端两个LED代表绿灯,中间两个LED代表黄灯,右端两个LED代表红灯, case 0: color=1; LedBuff[6]=0xE7; timer=2; break; //切换到黄色,亮3秒 case 1: color=2; LedBuff[6]=0xFC; timer=29; break; //切换到红色,亮30秒 case 2: color=0; LedBuff[6]=0x3F; timer=39; break; //切换到绿色,亮40秒 default: break; } } else //倒计时未到0时,递减其计数值 { timer--; } LedBuff[0] = LedChar[timer%10]; //倒计时数值个位显示 LedBuff[1] = LedChar[timer/10]; //倒计时数值十位显示 } void LedScan() //LED显示扫描函数 { static unsigned char index = 0; //LED位选索引 P0 = 0xFF; //关闭所有段选位,显示消隐 P1 = (P1 & 0xF8) | index; //位选索引值赋值到P1口低3位 P0 = LedBuff[index]; //相应显示缓冲区的值赋值到P0口 if (index < 6) //位选索引0-6循环,因有6个数码管+一组独立LED index++; else index = 0; } void InterruptTimer0() interrupt 1 //T0中断服务函数 { static unsigned int tmr1s = 0; //1秒定时器 TH0 = T0RH; //定时器重新加载重载值 TL0 = T0RL; LedScan(); //LED扫描显示 tmr1s++; //1秒定时的处理 if (tmr1s >= 1000) { tmr1s = 0; flag1s = 1; } } 10.4 长短按键的应用10.4.1 51单片机RAM区域划分前边介绍单片机资源的时候,我们提到过我们的STC89C52RC共有512字节的RAM,就是用来保存数据的,如我们定义的变量都是直接存在RAM里边。51单片机的这512字节的RAM数据是分块的,因此我们在访问的时候,也要注意一些问题。 51单片机的RAM分为两个部分,一块是片内RAM,一块是片外RAM。标准的51的片内RAM地址从0x00H~0x7F共128个字节,而现在我们用的51系列的单片机都是带扩展片内RAM的,RAM是从0x00~0xFF共256个字节。片外RAM最大可以扩展到0x0000~0xFFFF共64K字节。这里有一点大家要明白,片内RAM和片外RAM的地址不是连起来的,片内是从0x00开始,片外也是从0x0000开始的。以下是几个Keil C51语言中的关键字,代表了RAM不同区域的划分,大家先记一下。 data:片内RAM从0x00~0x7F idata:片内RAM从0x00~0xFF pdata:片外RAM从0x0000~0x00FF xdata:片外RAM从0x0000~0xFFFF 大家可以看出来,data是idata的一部分,pdata是xdata的一部分。为什么还这样去区分呢?因为RAM分块的访问方式主要和汇编语言有关,因此这块内容大家了解一下即可,只需要记住如何访问速度更快即可。 我们定义一个变量a,可以这样:unsigned char data a=0,而我们前边定义变量时都没有加data这个关键字,是因为我们在Keil默认设置下,data是可以省略的,即什么都不加的时候变量就是定义到data区域中的。data区域RAM的访问在汇编语言中用的是直接寻址,访问运行速度是最快的。如果你定义成idata,不仅仅可以访问data区域,还可以访问0x80H~0xFF的范围,但加了idata关键字后,访问的时候是利用了51单片机的通用寄存器进行间接寻址,速度较data速度慢一些,而且我们平时大多数情况下不太希望访问到0x80H~0xFF,因为这块通常用于中断和函数调用的堆栈,所以在绝大多数情况下,我们使用内部RAM的时候,只用data就可以了。 对于外部RAM来说,使用pdata定义的变量存到了外部RAM的0x00~0xFF的地址范围里,这块地址的访问和idata类似,都是用8位的通用寄存器进行间接寻址,而如果你定义成xdata,可以访问的范围更广泛,从0到64k的地址都可以访问到,但是它需要使用2个8位的寄存器DPTRH和DPTRL来进行间接寻址,速度是最慢的。 我们的STC89C52RC共有512字节的RAM,256字节的片内RAM和256字节的片外RAM。一般情况下,我们是使用data区域,data不够用了,我们就用xdata,如果希望程序执行效率点,可以使用pdata关键字来定义。其他型号的,有更大的RAM的51系列单片机,如果要使用更大的RAM,就必须得用xdata来访问了。 10.4.2 长短按键在我们的单片机系统中,如果我们按下一次按键加1,那我们第八章学到的技术就可以完成,但是我们想连续加很多数字的时候,要一次次按下这个按键确实不方便,我们希望我们按住按键的时候,数字会持续增加,这就是这节课的长短按键实例。 当按下一个按键持续时间低于1秒的时候,运行一次按键动作,当按下按键持续时间超过1秒后,每经过200ms则自动再执行一次按键动作,形成一个长按键效果。这个程序做的是一个定时炸弹的效果,打开开关后,数码管显示数字0,按下向上的按键数字加1,按下向下的按键数字减1,长按向上按键1秒后,数字会持续增加,长按向下按键1秒后,数字会持续减小。设定好数字后,按下回车按键,时间就会进行倒计时,当倒计时到0的时候,用蜂鸣器和板子上的8个LED小灯做炸弹效果,蜂鸣器持续响,LED小灯全亮。 #include <reg52.h> sbit BUZZ = P1^6; //蜂鸣器控制引脚 sbit KEY_IN_1 = P2^4; //矩阵按键的扫描输入引脚1 sbit KEY_IN_2 = P2^5; //矩阵按键的扫描输入引脚2 sbit KEY_IN_3 = P2^6; //矩阵按键的扫描输入引脚3 sbit KEY_IN_4 = P2^7; //矩阵按键的扫描输入引脚4 sbit KEY_OUT_1 = P2^3; //矩阵按键的扫描输出引脚1 sbit KEY_OUT_2 = P2^2; //矩阵按键的扫描输出引脚2 sbit KEY_OUT_3 = P2^1; //矩阵按键的扫描输出引脚3 sbit KEY_OUT_4 = P2^0; //矩阵按键的扫描输出引脚4 sbit ADDR3 = P1^3; //LED选择地址线3 sbit ENLED = P1^4; //LED总使能引脚 unsigned char code LedChar[] = { //数码管显示字符转换表 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; unsigned char LedBuff[7] = { //数码管+独立LED显示缓冲区 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到PC标准键盘键码的映射表 { '1', '2', '3', 0x26 }, //数字键1、数字键2、数字键3、向上键 { '4', '5', '6', 0x25 }, //数字键4、数字键5、数字键6、向左键 { '7', '8', '9', 0x28 }, //数字键7、数字键8、数字键9、向下键 { '0', 0x1B, 0x0D, 0x27 } //数字键0、ESC键、 回车键、 向右键 }; unsigned char KeySta[4][4] = { //全部矩阵按键的当前状态 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; unsigned long pdata KeyDownTime[4][4] = { //每个按键按下的持续时间,单位ms {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0} }; bit enBuzz = 0; //蜂鸣器使能标志 unsigned char T0RH = 0; //T0重载值的高字节 unsigned char T0RL = 0; //T0重载值的低字节 bit flag1s = 0; //1秒定时标志 bit flagStart = 0; //倒计时启动标志 unsigned int CountDown = 0; //倒计时计数器 void ConfigTimer0(unsigned int ms); void DisplayNumber(unsigned int dat); void KeyDrive(); void main(void) { P0 = 0xFF; //P0口初始化 ADDR3 = 1; //选择数码管 ENLED = 0; //LED总使能 EA = 1; //开总中断 ConfigTimer0(1); //配置T0定时1ms DisplayNumber(CountDown); while(1) { KeyDrive(); if (flagStart && flag1s) //倒计时启动且1秒定时到达时,处理倒计时 { flag1s = 0; if (CountDown > 0) //倒计时未到0时,计数器递减 { CountDown--; DisplayNumber(CountDown); if (CountDown == 0) //减到0时,执行声光报警 { enBuzz = 1; //启动蜂鸣器发声 LedBuff[6] = 0x00; //点亮独立LED } } } } } void ConfigTimer0(unsigned int ms) //T0配置函数 { unsigned long tmp; tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 31; //修正中断响应延时造成的误差 T0RH = (unsigned char)(tmp >> 8); //定时器重载值拆分为高低字节 T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = T0RH; //加载T0重载值 TL0 = T0RL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 } void DisplayNumber(unsigned int dat) //将一个无符号整型数转到数码管显示缓冲区以供显示 { signed char i; unsigned char buf[6]; for (i=0; i<6; i++) //拆分为十进制的位 { buf[ i] = dat % 10; dat /= 10; } for (i=5; i>=1; i--) //高位的0不予显示 { if (buf [ i] == 0) LedBuff[ i] = 0xFF; else break; } for ( ; i>=0; i--) //有效数据位转换为显示字符 { LedBuff[ i] = LedChar[buf[ i]]; } } void KeyAction(unsigned char keycode) //按键动作函数,根据键码执行相应动作 { if ((keycode>='0') && (keycode<='9')) //本程序中对0-9的数字按键不做响应 {} else if (keycode == 0x26) //向上键,倒计时设定值递增 { if (CountDown < 9999) //最大计时9999秒 { CountDown++; DisplayNumber(CountDown); } } else if (keycode == 0x28) //向下键,倒计时设定值递减 { if (CountDown > 1) //最小计时1秒 { CountDown--; DisplayNumber(CountDown); } } else if (keycode == 0x0D) //回车键,启动倒计时 { flagStart = 1; //启动倒计时 } else if (keycode == 0x1B) //Esc键,取消倒计时 { enBuzz = 0; //关闭蜂鸣器 LedBuff[6] = 0xFF; //关闭独立LED flagStart = 0; //停止倒计时 CountDown = 0; //倒计时数归零 DisplayNumber(CountDown); } } void KeyDrive() //按键动作驱动函数 { unsigned char i, j; static unsigned char backup[4][4] = { //按键值备份,保存前一次的值 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; static unsigned long pdata TimeThr[4][4] = { //保持按下时启动快速输入的时间阈值 {1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000} }; for (i=0; i<4; i++) //循环扫描4*4的矩阵按键 { for (j=0; j<4; j++) { if (backup[ i][j] != KeySta[ i][j]) //检测按键动作 { if (backup[ i][j] != 0) //按键按下时执行动作 { KeyAction(KeyCodeMap [ i][j]); //调用按键动作函数 } backup[ i][j] = KeySta[ i][j]; } if (KeyDownTime[ i][j] > 0) //检测执行快速输入 { if (KeyDownTime[ i][j] >= TimeThr[ i][j]) //按下时间达到阈值时执行一次动作 { KeyAction(KeyCodeMap[ i][j]); //调用按键动作函数 TimeThr[ i][j] += 200; //间隔200ms执行下一次动作 } } else //按键弹起时复位阈值时间 { TimeThr[ i][j] = 1000; //启动快速输入的条件为持续按下超过1000ms } } } } void LedScan() //LED显示扫描函数 { static unsigned char index = 0; P0 = 0xFF; //关闭所有段选位,显示消隐 P1 = (P1 & 0xF8) | index; //位选索引值赋值到P1口低3位 P0 = LedBuff[index]; //相应显示缓冲区的值赋值到P0口 if (index < 6) //位选索引0-6循环,因有6个数码管+一组独立LED index++; else index = 0; } void KeyScan() //按键扫描函数 { unsigned char i; static unsigned char keyout = 0; //矩阵按键扫描输出计数器 static unsigned char keybuf[4][4] = { //按键扫描缓冲区,保存一段时间内的扫描值 {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF} }; //将一行的4个按键值移入缓冲区 keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1; keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2; keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3; keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4; //消抖后更新按键状态 for (i=0; i<4; i++) //每行4个按键,所以循环4次 { if ((keybuf[keyout][ i] & 0x0F) == 0x00) { //连续4次扫描值为0,即16ms(4*4ms)内都只检测到按下状态时,可认为按键已按下 KeySta[keyout][ i] = 0; KeyDownTime[keyout][ i] += 4; //按下持续时间累加 } else if ((keybuf[keyout] [ i] & 0x0F) == 0x0F) { //连续4次扫描值为1,即16ms(4*4ms)内都只检测到弹起状态时,可认为按键已弹起 KeySta[keyout][ i] = 1; KeyDownTime[keyout][ i] = 0; //按下的持续时间清零 } } //执行下一次的扫描输出 keyout++; keyout &= 0x03; switch (keyout) { case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break; case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break; case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break; case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break; default: break; } } void InterruptTimer0() interrupt 1 //T0中断服务函数 { static unsigned int tmr1s = 0; //1秒定时器 TH0 = T0RH; //定时器重新加载重载值 TL0 = T0RL; if (enBuzz) //蜂鸣器发声处理 BUZZ = ~BUZZ; //驱动蜂鸣器发声 else BUZZ = 1; //关闭蜂鸣器 KeyScan(); //按键扫描 LedScan(); //LED扫描显示 if (flagStart) //倒计时启动时处理1秒定时 { tmr1s++; if (tmr1s >= 1000) { tmr1s = 0; flag1s = 1; } } else //倒计时为启动时1秒定时器始终归零 { tmr1s = 0; } } 长按键功能实现的重点有两个:第一,是在原来的矩阵按键扫描函数KeyScan内,当检测到按键按下后,持续的对一个时间变量进行累加,其目的是用这个时间变量来记录按键按下的时间;第二,是在按键驱动函数KeyDrive里,除了原来的检测到按键按下这个动作时执行按键动作函数KeyAction外,还监测表示按键按下时间的变量,根据它的值来完成长按时的连续快速按键动作功能。 1.5 作业1、将第一个例程进行倒计时处理,从9999.99开始进行倒计时,并且只显示有效位。 2、理解PWM的实质,在点阵上实现不同亮度的小灯的花样排列。 3、实现数码管计时和流水灯同时运行的效果。 4、学会长短按键的用法,独立把本章程序全部写出来。 |