不同数据类型间的相互转换
在C语言中,不同数据类型之间是可以混合运算的。当表达式中的数据类型不一致时,首先转换为同一类型,然后再进行计算。C语言有两种方式实现类型转换。一是自动类型转换,另外一种是强制类型转换。
转换的主要原则:短字节的数据向长字节数据转换。 比如
unsigned char a; unsigned int b; unsigned int c; c = a * b;
如果: a = 10; b = 200; 那么C的结果就是2000。
那么当 a = 100 , b = 700 ,C会是70000吗? unsigned int 的范围是0 ~65535,但是70000超过了65535,其结果就会溢出 ,最终C的结果是(70000 - 65536)= 4464 这个结果具体和你的编译器有关,不同的编译器结果可能不一样。1111 1111 1111 1111 这个二进制表达的数组是65535也就是16位数据能表达的最大数值,70000的二进制是0001 0001 0001 0111 0000,因为它被限制只能储存16位的数据因此高4位被砍掉了,C显示的就是低16的数据 0001 0001 0111 0000 = 4464
要想C正常获得70000这个结果,需要把C定义成一个unsigned long型
比如 unsigned char a; unsigned int b; unsigned long c; c = a * b;
发现结果仍是4464
C语言不同类型运算的时候数组会转换为同一类型运算,但是每一步运算都会进行识别判断,但不会进行一个总的分析判断。上述 a * b 的时候是按照unsigned int运算的,那么运算的结果也是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。
但是 c = (unsigned long)(a*b) 的结果依然是4464注意区分,圆括号的运算优先级是最高的。
在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个,秒表的精度要求达到小数点后2位。
首先计算秒表的精度:99i+i = 1s i=10ms,精度是10ms
显然如果精度达到小数点后3位 999i +i = 1s i=1ms 精度是1ms
因此6个数码管前4个显示秒表的整数部分,后两个显示秒表的小数部分。先上代码(该代码来自教材)
# include<reg52.h>
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
sbit KEY1 = P2^4;
sbit KEY2 = P2^5;
sbit KEY3 = P2^6;
sbit KEY4 = P2^7;
code unsigned char LedChar[] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, //数码管0-F的值
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char KeySta[4] = { //按键当前态
1,1,1,1
};
unsigned char LedBuff[6] = { //数码管显示缓冲区
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
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; //开总中断
ENLED = 0; //使能数码管
ADDR3 = 1;
P2 = 0xFE; //1111 1110 P2.0置0,选择第4行按键作为独立按键
ConfigTimer0(2); // 配置T0定时2ms
while(1)
{
if(StopwatchRefresh) //需要刷新秒表示数时调用显示函数
{
StopwatchRefresh = 0;
StopwatchDisplay();
}
KeyDriver();
}
}
/*配置并启动,ms-T0定时时间 */
void ConfigTimer0(unsigned int ms)
{
unsigned long tmp; // 定义临时变量
tmp = 11059200 / 12; //定时器计数频率,即每秒的机器周期数
tmp = (tmp * ms) / 1000 ; //计算所需的计数值
tmp = 65536 - tmp ; //计算定时器的初值(重载值)
tmp = tmp + 18; //补偿响应延时造成的误差
T0RH = (unsigned char)(tmp >> 8) ; //定时器重载值拆分成为高低字节
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0位模式1
TH0 = T0RH;
TL0 = T0RL;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0定时器
}
/*秒表计数显示函数 */
void StopwatchDisplay()
{
signed char i;
unsigned char buf[4]; //数据转换的缓冲区
//小数部分转换到低2位
//LedBuff[0] = LedChar[DecimalPart % 10];
//LedBuff[1] = LedChar[DecimalPart / 10];
LedBuff[0] = LedChar[DecimalPart%10];
LedBuff[1] = LedChar[DecimalPart/10];
//整数部分转换到高4位
buf[0] = IntegerPart % 10;
buf[1] = (IntegerPart / 10) %10;
buf[2] = (IntegerPart / 100) %10;
buf[3] = (IntegerPart / 1000) %10;
for(i = 3; i >= 1; i--) // 整数部分高位的0转换为空字符
{
if( buf[i] == 0)
LedBuff[i+2] = 0xFF;
else
break;
}
for(; i >= 0; i--) //有效数字位转换为显示字符
{
LedBuff[i+2] = LedChar[buf[i]];
}
LedBuff[2] &= 0x7F; //点亮小数点
}
/*秒表启停函数 */
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}; //循环检测4个按键
for(i = 0; i < 4; i++)
{
if(backup[i] != KeySta[i]) //按键动作检测
{
if( backup[i] != 0)
{
if( i == 1) //ESC键复位秒表
StopwatchReset();
else if ( i == 2) //回车键启停秒表
StopwatchAction();
}
backup[i] = KeySta[i]; //刷新前一次的备份值
}
}
}
/* 按键扫描函数,需在定时中断中调用 */
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)
{ //连续8次扫描为0,即16ms内都是按下状态,可以认为按键已稳定的按下
KeySta[i] = 0;
}
else if (keybuf[i] == 0xFF) //连续8次扫描为1,即16ms内都是弹起状态,可以认为按键已稳定的弹起
KeySta[i] = 1;
}
}
/* 按键扫描函数,需在定时中断中调用 */
void LedScan()
{
static unsigned char i = 0;
P0 = 0xFF; //显示消隐
P1 = (P1 & 0xF8) | i; //F8=1111 1000 位选索引值赋值P1口低3位
P0 = LedBuff[i]; //缓冲区中索引位置的数据送到P0口
if(i < 5) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}
/* 秒表计数函数,每隔10ms调用一次进行秒表计数累加 */
void StopwatchCount()
{
if(StopwatchRunning) //当处于运行状态时递增计数值
{
DecimalPart++; //小数部分+1
if (DecimalPart >= 100) //小数部分计到100时进位到整数部分
{
DecimalPart = 0;
IntegerPart++; //整数部分+1
if(IntegerPart >= 10000) //整数部分计到10000时归零
{
IntegerPart = 0;
}
}
StopwatchRefresh = 1; //设置秒表计数刷新标志
}
}
/* T0中断服务函数,完成数码管、按键扫描与秒表计数 */
void InterruptTimer0() interrupt 1
{
static unsigned char tmr10ms = 0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
LedScan(); //数码管扫描显示
KeyScan(); //按键扫描
tmr10ms++;
if(tmr10ms >= 5)
{
tmr10ms = 0;
StopwatchCount(); //调用秒表计数函数
}
}
精度10ms的秒表计时_哔哩哔哩_bilibili
看一下结果视频:可以看到秒表确实正常工作了,有清零,暂停两种基本功能。该秒表的最大量程是9999.99秒。
然后分析下该程序的一些可能有疑问的部分。
1:
void LedScan()
{
static unsigned char i = 0;
P0 = 0xFF; //显示消隐
P1 = (P1 & 0xF8) | i; //F8=1111 1000 位选索引值赋值P1口低3位
P0 = LedBuff[i]; //缓冲区中索引位置的数据送到P0口
if(i < 5) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}
下图是上图的等效语句,上图显然精炼点。
P0 = 0xFF; //刷新数码管前P0口8位全部置1使LED都不工作。
switch(i)
{
case 0: ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; i++; P0 = LedBuff[0]; break; //数码管1刷新
case 1: ADDR2 = 0; ADDR1 = 0; ADDR0 = 1; i++; P0 = LedBuff[1]; break; //数码管2刷新
case 2: ADDR2 = 0; ADDR1 = 1; ADDR0 = 0; i++; P0 = LedBuff[2]; break; //数码管3刷新
case 3: ADDR2 = 0; ADDR1 = 1; ADDR0 = 1; i++; P0 = LedBuff[3]; break; //数码管4刷新
case 4: ADDR2 = 1; ADDR1 = 0; ADDR0 = 0; i++; P0 = LedBuff[4]; break; //数码管5刷新
case 5: ADDR2 = 1; ADDR1 = 0; ADDR0 = 1; i = 0; P0 = LedBuff[5]; break; //数码管6刷新
default: break;
}
2:
这个小数点的值算法可以参考我之前的博文初学51单片机定时器数码管及C语言实践_51单片机定时器控制数码管程序-CSDN博客
这个点是数码管的dp位,它是最高位,因为是低电平使能,因此要加上这个点只要
0111 1111 = 0x7F 与原先的数相与,就可以把原先数值的最高位置0。而本案的数码管真值,表示的都是不带点的数,因此最高位都是1。该与运算就把点加上去了。
3:
该程序里的18是怎么来的?tmp是中断初值设置,因此这个18是18个机器周期的意思,加18会使tmp值变大,相应中断时间变短了,说明该程序在某处浪费了时间。
首先这个误差可以通过软件debug出来,也可以通过累计计时比如计时30分钟和正确的秒表计时比较后确定总的误差是多少,然后通过计算转换到每个中断需要补偿的时间。
那边必然产生一个问题,误差到底是由哪些语句产生的?
首先我们得知道数码管显示语句在哪里,通读程序可知他是由中断函数里的LedScan()函数提供,我们之前说秒表的精度是10ms,但是实际上它每隔2ms就会刷新一下数码管。假设我们有子弹时间的话,一开始同一数码管的视觉上时间显示间隔可以这么理解。当然这个速度很快
事实上当开始执行的时候,
数码管0-5间隔2ms依次显示下去的,一开始的显示表示。
通过两图结合程序对比可知,事实上当过去10ms的时候,即第5次进入中断,小数部分低位数码管显示数值已经刷新了,即0变成1了,但是第5次中断数码管显示的是第5个数码管的数值
P0 = LedBuff[4];,此时第5个的数码管的数值是0,但高位0不显示。而需要再次显示
P0 =LedBuff[0] 还需要两个中断,也就是说秒表在计时的时候除却其他人为操作造成的误差,由于程序结构照成的误差就有1个精度单位,本案为10ms。注:这是显示误差不是计时误差。这个误差是必然发生的,要想降低这个误差就需要更改中断间隔,提高秒表的精度。
所以除却该误差,如果要想数码管显示准确无误,它必须满足这几个条件:
1:中断的时间间隔是非常精确的2ms,而这个精度由单片机和晶振提供的,就是该单片机的机器周期,本案是12/11059200=1.085us也就是微秒级的。这个对于我们的普通秒表来说应该是够了
2:进入中断后马上执行显示语句,并且显示语句本身以及显示语句之前都没有时间损耗(显然不可能)
3:电路响应足够快,快到对于秒表精度来说忽略不计。
第二个条件
查看中断函数与数码管显示函数
void InterruptTimer0() interrupt 1
{
static unsigned char tmr10ms = 0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
LedScan(); //数码管扫描显示
KeyScan(); //按键扫描
tmr10ms++;
if(tmr10ms >= 5)
{
tmr10ms = 0;
StopwatchCount(); //调用秒表计数函数
}
}
void LedScan()
{
static unsigned char i = 0;
P0 = 0xFF; //显示消隐
P1 = (P1 & 0xF8) | i; //F8=1111 1000 位选索引值赋值P1口低3位
P0 = LedBuff[i]; //缓冲区中索引位置的数据送到P0口
if(i < 5) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}
显然数码管显示语句P0 = LedBuff[i];前有好几个语句这些都有可能照成误差,开始分析一下。
一开始进入中断,TF0 = 0(中断函数自动清0),那代表着第二个中断时间开始计时了,这时定时器的初值是0x00;
TH0 = T0RH;
TL0 = T0RL; 执行完这句定时器0初值重载,那么前面的三句都是时间上的误差。执行了该句以后,马上执行LedScan(); 该函数
static unsigned char i = 0;
P0 = 0xFF; //显示消隐
P1 = (P1 & 0xF8) | i; //F8=1111 1000 位选索引值赋值P1口低3位
P0 = LedBuff[i]; 执行完该句数码管才显示结束因此前面的加上它本身共4句都是误差来源。
事实上对于该程序的时间误差,统筹的看会比较好一点。对于时间流逝流程如下图,(这个过程不太好描述,笔者无法准确表述,各位自己感悟一下,秒表的显示逻辑是这样的,我们肉眼看到也是如此)
debug一下,把程序停在该句 P0 = LedBuff[i]
第一次运行是0.00421875
第二次运行是0.00623806
第三次运行是0.00825738
......
第11次运行是0.02441406
计算第11次的值减去第1次的值,除以10.求得该时间间隔是0.002019531
我们希望的时间是2ms即0.002s,所以多余的时间就是误差,这个误差我们需要消除
即0.000019531*11059200/12=17.9997个机器周期取18,这就是程序里补偿时间18的来由。
debug过程debug误差过程_哔哩哔哩_bilibili
事实上对于中断函数里的程序,笔者看了一会觉得逻辑是这样的才对,
一开始写大概率这种逻辑的,但是从debug的过程中可以看到,第5次中断的时候会进入if函数,那么时间间隔就会发生变化了,当然这依然可以时间补偿,就是求时间的时候需要注意一下,而且这会导致每5次中断,其中一个数码管的刷新时间变得长一点,虽然不影响显示结果,因此该函数放在LedSCan()后面,而且因为几乎必然发生的显示延迟,对于进位结果下个中断使能,好像也不是很难接受的结果。
按照刷新频率来说,第一个0ms-100ms为例,小数部分最低位的数码管我们希望的值是0-9共显示10次数字,但是数码管的刷新完一次要12ms, 则100/12= 8 余4,也就是说在头一个100ms里它只完成扫描了8次,第9次只扫描了前4个数码管。因此必然有个数没显示,经过笔者计算第一个100ms里无法显示的数是6,它是从5直接跳到7,当然下一个100ms未显示的数未必是6了。
最低位数码管从5跳到7.这个7显示正确吗,精度有问题吗?事实上7的精度是最高的,5的误差是最大的,这边可以理解为显示延迟已经不支持它显示6了,数码管是第6次刷新,但是时间已经累计到7了并且该处ledbuff[0]值已进被程序赋值为7了,因此跳过了6直接显示7。可以预见的是如果一直计时在未来的某个100ms里可能会出现跳过两次数字显示,毕竟发现一次跳过显示只需要72ms,但不会出现三次跳过的情况。
3:只要电路的响应频率远大于2ms,应该都没问题。
笔者自己梳理的程序逻辑导图:
总结:今天又进步了一小步。