单片机数字秒表程序
扫描二维码
随时随地手机看文章
不同数据类型间的相互转换
在 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。
定时时间精准性调整
在 6.5.2 章节有一个数码管秒表显示程序,那个程序是 1 秒数码管加 1,但是细心的同学做了实验后,经过长时间运行会发现,和我们实际的时间有了较大误差了,那如何去调整这种误差呢?要解决问题,先找到问题是什么原因造成的。
先对我们前面讲过的中断内容做一个较深层次的补充。还是讲解中断的那个场景,当我们在看电视的时候,突然发生了水开的中断,我们必须去提水的时候,第一,我们从电视跟前跑到厨房需要一定的时间,第二,因为我们看的电视是智能数字电视,因此在去提水之前我们可以使用遥控器将我们的电视进行暂停操作,方便回来后继续从刚才的剧情往下进行。
那么暂停电视,跑到厨房提水,这一点点时间是很短的,在实际生活中可以忽略不计,但是在单片机秒表程序中,误差是会累计的,每 1 秒钟都差了几个微妙,时间一久,造成的累计误差就不可小觑了。
单片机系统里,硬件进入中断需要一定的时间,大概是几个机器周期,还要进行原始数据保护,就是把进中断之前程序运行的一些变量先保存起来,专业术语叫做中断压栈,进入中断后,重新给定时器 TH 和 TL 赋值,也需要几个机器周期,这样下来就会消耗一定的时间,我们得把这些时间补偿回来。
方法一,使用软件 debug 进行补偿。
我们在前边讲过使用 debug 来观察程序运行时间,那我们可以把我们 2 次进入中断的时间间隔观察出来,看看和我们实际定时的时间相差了几个机器周期,然后在进行定时器初值赋值的时候,进行一个调整。我们用的是 11.0592M 的晶振,发现差了几个机器周期,就把定时器初值加上几个机器周期,这样就相当于进行了一个补偿。
方法二,使用累计误差计算出来。
有的时候,除了程序本身存在的误差外,硬件精度也可能会影响到时钟的精度,比如晶振,会随着温度变化出现温漂现象,就是实际值和标称值要差一点。那么我们还可以采取累计误差的方法来提高精度。比如我们可以让时钟运行半个小时或者一个小时,看看最终时间差了几秒,然后算算一共进了多少次定时器中断,把这差的几秒平均分配到每次的定时器中断中,就可以实现时钟的调整。
大家要明白,这个世界上本就没有绝对的精确,我们只能在一定程度上提高精确度,但是永远都不会使误差为零,如果在这个基础上还感觉精度不够的话,不要着急,后边我们会专门讲时钟芯片的,通常时钟芯片计时的精度比单片机的精度要高一些。
字节操作修改位的技巧
这里再介绍个编程小技巧,在编程时,有的情况下需要改变一个字节中的某一位或者几位,但是又不想改变其它位原有的值,该如何操作呢?
比如我们学定时器的时候遇到一个寄存器 TCON,这个寄存器是可以进行位操作的,可以直接写 TR0=1;TR0 是 TCON 的一个位,因为这个寄存器是允许位操作,这样写是没有任何问题的。还有一个寄存器 TMOD,这个寄存器是不支持位操作的,那如果我们要使用 T0的模式 1,我们希望达到的效果是 TMOD 的低 4 位是 0b0001,但如果我们直接写成 TMOD =0x01 的话,实际上已经同时操作到了高 4 位,即属于 T1 的部分,设置成了 0b0000,如果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 位清零,变成了 0bxxxx0000;然后再进行第二步与 0x01 进行|运算,那么高 7 位均不变,最低位变成 1 了,这样就完成了只将低 4 位的值修改位 0b0001,而高 4 位保持原值不变的任务,即只设置了 T0 而不影响 T1。熟练掌握并灵活运用这个方法,会给你以后的编程带来便利。
另外,在 C 语言中,a &= b;等价于 a = a&b;同理,a |= b;等价于 a = a|b;那么刚才的一段代码就可以写成 TMOD &= 0xF0;TMOD |= 0x01 这样的简写形式。这种写法可以一定程度上简化代码,是 C 语言常用的一种编程风格。
数码管扫描函数算法改进
在学习数码管动态扫描的时候,为了方便大家理解,我们程序写的细致一些,给大家引入了 switch 的用法,随着编程能力与领悟能力的增强,对于 74HC138 这种非常有规律的数字器件,我们在编程上也可以改进一下逻辑算法,让程序变的更简洁。这种逻辑算法,通常不是靠学一下可以全部掌握的,而是通过不断的编写程序以及研究他人程序的过程中一点点积累起来的,从今天开始,大家就要开始积累吧。
前边动态扫描刷新函数我们是这么写的:
P0 = 0xFF;
switch (i){
case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0]; break;
case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=LedBuff[1]; break;
case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=LedBuff[2]; break;
case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=LedBuff[3]; break;
case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=LedBuff[4]; break;
case 5: ADDR2=1; ADDR1=0; ADDR0=1; i=0; P0=LedBuff[5]; break;
default: break;
}
我们来分析每一个 case 分支,它们的结构是相同的,即改变 ADDR2~0、改变索引 i、取数据写入 P0,只要把 case 后的常量与 ADDR2