数字秒表实例
不同数据类型之间的转换
C语言有两种方法实现类型转换,一是自动类型转换,另外一种是强制类型转换。
当不同数据类型之间混合运算的时候,不同类型的数据首先会转换为同一类型,转换的主要原则是:短字节的数据向长字节数据转换。比如:
unsigned char a; unsigned int b; unsigned int c; c = a *b;
注意:考虑数据溢出
强制类型转换的方法:在一个变量前边加上一个数据类型名,并且这个类型名用小括号括起来,就表示把这个变量强制转换成括号里的类型。如 c = (unsigned long)a * b;
在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。
定时时间精准性调整
产生时间误差的原因:
单片机系统里,硬件进入中断需要一定的时间,大概是几个机器周期,还要进行原始数据保护,就是把进中断之前程序运行的一些变量先保存起来,专业术语叫作中断压栈,进入中断后,重新给定时器TH和TL赋值,也需要几个机器周期,这样下来就会消耗一定的时间。
解决办法:
方法1:使用软件debug进行补偿。
在前边讲过使用debug来观察程序运行时间,可以把2次进入中断的时间间隔观察出来,看看和实际定时的时间相差了几个机器周期,然后在进行定时器初值赋值的时候,进行一个调整。我们用的是11.0592M的晶振,发现差了几个机器周期,就把定时器初值加上几个机器周期,这样就相当于进行了一个补偿。
方法2:使用累计误差计算出来。
有的时候除了程序本身存在的误差外,硬件精度也可能会影响到时钟的精度,比如晶振,会随着温度变化出现温漂现象,就是实际值和标称值要差一点。还可以采取累计误差的方法来提高精度。比如可以让时钟运行半个小时或者一个小时,看看最终时间差了几秒,然后算算一共进了多少次定时器中断,把这差的几秒平均分配到每次的定时器中断中,就可以实现时钟的调整。
字节修改位的技巧
“&”和“|”运算:
对于二进制位操作来说,不管该位原来的值是0还是1,它跟0进行&运算,得到的结果都是0,而跟1进行&运算,将保持原来的值不变;不管该位原来的值是0还是1,它跟1进行|运算,得到的结果都是1,而跟0进行|运算,将保持原来的值不变。
TMOD = TMOD & 0xF0; TMOD = TMOD | 0x01;
数码管扫描函数算法改进
原代码:
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;
}
改进:
P0 = 0xFF;
P1 = (P1 & 0xF8) | i;
P0 = LedBuff[i];
if (i < 5)
i++;
else
i = 0;
秒表程序
此程序是一个“真正的”并且“实用的”秒表程序,第一,它有足够的分辨率,保留到小数点后两位,即每10ms计一次数,第二,它也足够精确,因为补偿了定时器中断延时造成的误差,如果你愿意,它完全可以被用来测量你的百米成绩。
#include <REG52.H>
sbit KEY1 = P3 ^ 1;
sbit KEY2 = P3 ^ 0;
sbit KEY3 = P3 ^ 2;
sbit KEY4 = P3 ^ 3;
unsigned char code LedChar[] = {
// 数码管显示字符转换表
0x3f,
0x06,
0x5b,
0x4f,
0x66,
0x6d,
0x7d,
0x07,
0x7f,
0x6f,
0x77,
0x7c,
0x39,
0x5e,
0x79,
0x71,
};
unsigned char LedBuff[8] = {
// 数码管显示缓冲区
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
};
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 KeyDriver();
void main()
{
EA = 1;
P3 = 0x0f; // 拉高独立按键所在IO口电平
ConfigTimer0(2); // 配置T0定时2ms
while (1) {
if (StopwatchRefresh) { // 需要刷新秒表示数时
StopwatchRefresh = 0;
StopwatchDisplay();
}
KeyDriver();
StopwatchDisplay();
}
}
void ConfigTimer0(unsigned int ms)
{
// 配置并启动T0
unsigned long tmp;
tmp = 12000000 / 12; // 定时器频率
tmp = (tmp * ms) / 1000; // 计算所需的计数值
tmp = 65536 - tmp;
tmp = tmp + 18; // 18是补偿中断响应造成的误差
T0RH = (unsigned char)(tmp >> 8); // 拆出高字节
T0RL = (unsigned char)tmp;
TMOD = 0x01; // 配置为模式1
TH0 = T0RH;
TL0 = T0RL;
ET0 = 1;
TR0 = 1;
}
void StopwatchDisplay()
{
signed char i;
unsigned char buf[6]; // 数据转换的缓冲区
// 小数部分转换到低2位
LedBuff[0] = LedChar[DecimalPart % 10];
LedBuff[1] = LedChar[DecimalPart / 10];
// 整数转换为高6位
buf[0] = IntegerPart % 10;
buf[1] = IntegerPart / 10 % 10;
buf[2] = IntegerPart / 100 % 10;
buf[3] = IntegerPart / 1000 % 10;
buf[4] = IntegerPart / 10000 % 10;
buf[5] = IntegerPart / 100000 % 10;
for (i = 5; i >= 1; i--) {
// 整数部分高位的0转换为空字符
if (buf[i] == 0)
LedBuff[i + 2] = 0x00;
else
break;
}
for (; i >= 0; i--) {
LedBuff[i + 2] = LedChar[buf[i]];
}
LedBuff[2] |= 0x80; // 点亮从左往右数第三个小数点
}
void StopwatchAction()
{
if (StopwatchRunning)
// 已启动则停止
StopwatchRunning = 0;
else
// 未启动则启动
StopwatchRunning = 1;
}
void StopwatchReset()
{
StopwatchRunning = 0;
DecimalPart = 0;
IntegerPart = 0;
StopwatchRefresh = 1;
}
void KeyDriver()
{
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) {
if (i == 1)
StopwatchReset();
else if (i == 2)
StopwatchAction();
}
backup[i] = KeySta[i];
}
}
}
void KeyScan()
{
unsigned char i;
static unsigned char keybuf[4] = {
1,
1,
1,
1,
};
// 读取key值
keybuf[0] = keybuf[0] & KEY1;
keybuf[1] = keybuf[1] & KEY2;
keybuf[2] = keybuf[2] & KEY3;
keybuf[3] = keybuf[3] & KEY4;
for (i = 0; i < 4; i++) {
if (keybuf[i] == 0) {
KeySta[i] = 0;
} else if (keybuf[i] == 1) {
KeySta[i] = 1;
}
}
}
void LedScan()
{
static unsigned char i = 0;
P0 = 0x00; // 动态数码管
P2 = 0xff; // P2^2,3,4 -> 38译码器
P2 = (P2 & 0xf8) | i;
P2 <<= 2;
P0 = LedBuff[i];
if (i < 5)
i++;
else
i = 0;
}
// 秒表计数函数,每隔10ms调用一次进行秒表计数累加
void StopwatchCount()
{
if (StopwatchRunning) {
DecimalPart++;
if (DecimalPart >= 100) {
DecimalPart = 0;
IntegerPart++;
if (IntegerPart >= 100000) {
IntegerPart = 0;
}
}
StopwatchRefresh = 1;
}
}
// T0中断,完成数码管、按键扫描与秒针计数
void InterruptTimer0() interrupt 1
{
static unsigned char tmr10ms = 0;
TH0 = T0RH;
TL0 = T0RL;
LedScan(); // 数码管扫描
KeyScan(); // 按键扫描
// 定时10ms进行一次秒表计数
tmr10ms++;
if (tmr10ms >= 5) {
tmr10ms = 0;
StopwatchCount();
}
}
PWM知识与实例
PWM是Pulse Width Modulation的缩写,它的中文名字是脉冲宽度调制,一种说法是它利用微处理器的数字输出来对模拟电路进行控制的一种有效的技术,其实就是使用数字信号达到一个模拟信号的效果。
这是一个周期是10ms、频率是100Hz的波形,但是每个周期内,高低电平脉冲宽度各不相同,这就是PWM的本质。占空比是指高电平的时间占整个周期的比例。比如第一部分波形的占空比是40%,第二部分波形占空比是60%,第三部分波形占空比是80%。
#include <REG52.H>
sbit PWMOUT = P2 ^ 0;
unsigned char HighRH = 0;
unsigned char HighRL = 0;
unsigned char LowRH = 0;
unsigned char LowRL = 0;
void ConfigPWM(unsigned int fr, unsigned char dc);
void ClosePWM();
void main()
{
unsigned int i;
EA = 1;
while (1) {
ConfigPWM(100, 10);
for (i = 0; i < 40000; i++)
;
ClosePWM();
ConfigPWM(100, 90);
for (i = 0; i < 40000; i++)
;
}
}
void ConfigPWM(unsigned int fr, unsigned char dc)
{
unsigned int high, low;
unsigned long tmp;
tmp = (12000000 / 12) / fr;
high = (tmp * dc) / 100;
low = tmp - high;
high = 65536 - high + 12;
low = 65536 - low + 12;
HighRH = (unsigned char)(high >> 8);
HighRL = (unsigned char)high;
LowRH = (unsigned char)(low >> 8);
LowRL = (unsigned char)low;
TMOD = 0x01;
TH0 = HighRH;
TL0 = HighRL;
ET0 = 1;
TR0 = 1;
PWMOUT = 1;
}
void ClosePWM()
{
TR0 = 0;
ET0 = 0;
PWMOUT = 1;
}
void InterrupTimer0() interrupt 1
{
if (PWMOUT == 1) {
TH0 = LowRH;
TL0 = LowRL;
PWMOUT = 0;
} else {
TH0 = HighRH;
TL0 = HighRL;
PWMOUT = 1;
}
}
51单片机RAM区域的划分
51单片机的RAM分为两个部分,一块是片内RAM,一块是片外RAM。
标准51的片内RAM地址从0x00H~0x7F共128个字节,而现在用的51系列的单片机都是带扩展片内RAM的,即RAM是从0x00~0xFF共256个字节。片外RAM最大可以扩展到0x0000~0xFFFF共64K字节。这里有一点大家要明白,片内RAM和片外RAM的地址不是连起来的,片内是从0x00开始,片外也是从0x0000开始的。还有一点,片内和片外这两个名词来自于早期的51单片机,分别指在芯片内部和芯片外部,但现在几乎所有的51单片机芯片内部都是集成了片外RAM的,而真正的芯片外扩展则很少用到了,虽然它还叫片外RAM,但实际上它现在也是在单片机芯片内部的。
- data:片内RAM从0x00~0x7F。
- idata:片内RAM从0x00~0xFF。
- pdata:片外RAM从0x00~0xFF。
- xdata:片外RAM从0x0000~0xFFFF。
data是idata的一部分,pdata是xdata的一部分。为什么还这样去区分呢?因为RAM分块的访问方式主要和汇编指令有关,因此这块内容大家了解一下即可,只需要记住如何访问速度更快就行了。
STC89C52共有512字节的RAM,分为256字节的片内RAM和256字节的片外RAM。一般情况下使用data区域,data不够用了,就用xdata,如果希望程序执行效率尽量高一点,就使用pdata关键字来定义。其他型号有更大的RAM的51系列单片机,如果要使用更大的RAM,就必须得用xdata来访问了。