单片机C语言指针意义浅析—Keil-C51

来源:互联网 发布:人工智能2电影 编辑:程序博客网 时间:2024/04/19 00:22
通常认为,C语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上,甚至认为指针是C语言的灵魂。这里说通常,是广义上的,因为随着编程语言的发展,指针也饱受争议,并不是所有人都承认指针的“强大”和“优点”。在单片机领域,指针同样有着应用,本章节针对Keil C-51环境下的指针意义做简要分析。



1     指针与变量
指针是一个变量,它与其他变量一样,都是RAM中的一个区域,且都可以被赋值,如程序①所示。
#include "REG52.H"        
unsigned int j;
unsigned char *p;
void main()
{
         while(1)
         {
                   j=0xabcd;
                   p=0xaa;
         }
}
Debug Session模式下,将鼠标指针移到到变量“j”“p”位置,可以显示变量的物理地址,如图1-1、1-2所示。 






图中箭头所指处即为变量在RAM中的“首地址”,为什么是“首地址”呢?变量根据类型可分为8位(单字节)、16位(双字节),程序中变量“j”是无符号整型,所占物理空间应为2字节,而在8位单片机中,RAM的一个存储单元大小是8位,即1字节,因此需2个存储单元才满足变量“j”长度。所以实际上变量“j”的物理地址为“08H”“09H”。同理,“pD:0x0A)”即变量“p”的首地址为“0AH”。
下面通过单步执行程序来观察RAM内的数据变化,打开两个Memory Windows窗口,在Keil软件下方显示为Memory1Memory2,在两个窗口中,分别做如图2-1、2-2所示的设置。






两个Address填写的内容分别是:D:0x08D:0x0A,即量“j”和变量“p”的首地址,输入后回车,便可监视RAM中该地址下的数据。设置好后,准备调试。

Debug Session模式中,箭头所指处即为即将执行的语句,单击“Step”功能按钮(或按F11键),让程序运行,如图3所示。

第一次单击“Step”按钮后,Memory1窗口内数据如图4所示。

由调试结果可知,08H数据由00H变为ABH09H数据由00H变为CDH,出现这种变化是因为执行了语句j=0xabcd;08H为变量“j”高八位,存储“AB”,09H为变量“j”低八位,存储“CD”。
第二次单击“Step”按钮,执行语句:p=0xaa;此时需观察Memory2窗口内数据,如图5所示。

由调试结果可知,0CH处值由00变为“AAH”,程序相吻合。这里需要注意,在Keil C-51编译环境下,指针变量,不管长度是单字节或是双字节,指针变量所占字节数为3字节。故此处“AAH”不是存储在0AH而存储在0CH0A+2)地址中。
综上所述,指针实际上是变量,都是映射到RAM中的一段存储空间,区别是,指针占用3字节,而其他变量可根据需要设定其所占RAM1字节(char)、2字节(int)、4字节(long)。

2       指针作用
指针的作用是什么呢?先来看下面的程序:
程序②
#include "REG52.H"         
unsigned chartab1[8]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
unsigned char codetab2[8]={0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80}; 
unsigned char N1,N2;
void main()
{
            N1=tab1[0];
            N2=tab2[0];
}
显然,程序执行的结果是N1=0x01N2=0x10。这里都是讲数组内的数据赋值给变量,但存在区别,tab1数组使用的是单片机RAM空间,而tab2数组使用的是单片机程序存储区(ROM)空间。尽管使用C语言为变量赋值时语句相同,但编译结果并不相同,此程序编译后的结果如图6所示。
 


由编译结果可知,N1=tab1[0]语句实际上是直接寻址,而N2=tab2[0]是寄存器变址寻址。不管是何种寻址方式,都是将一个物理地址内的数据取出来使用:tab1数组中,tab[0]对应的RAM地址是0x0Atab[1]对应的RAM地址是0x0B……以此类推;tab2数组中,tab[0]对应的ROM地址是0x00A5tab[1]对应的ROM地址是0x00A6……以此类推。不管这些数组或变量所在的RAMROM地址如何,用户最终需要的是数组或变量的数据,而指针,就是通过变量或数组的物理地址访问数据,也就是说,通过指针,同样可以访问数组或变量数据。现将程序②做出调整,得到程序③如下:
#include "REG52.H"         
unsigned chartab1[8]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
unsigned char code tab2[8]={0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80};
unsigned char N1,N2;
unsigned char  *p;
void main()
{        
         unsignedchar i;
         p=tab1;
         for(i=0;i<8;i++,p++)
         N1=*p;
         
         p=tab2;
         for(i=0;i<8;i++,p++) 
         N2=*p;
}
程序执行结果:tab1数组内的8个数值依次被赋值给N1tab2数组内的8个数值依次被赋值给N2
程序③执行Debug Session功能后,打Watch Windows窗口,在Watch1窗口下添加需要监视的变量,此处为“p”和“N1”,如图7所示。

Value为当前变量数值,程序为运行前,p值为0x00,单击Step按键功能后,执行p=tab1;p值变为0x0A,如图8所示。

0x0A是什么值呢?将鼠标移至tab1数组位置,可显示出数组所在的物理地址,0x0A就是数组tab1的首地址,如图9所示。

p=tab1就是将tab1数组的首地址赋值给变量p,执行p++即地址值加1*p则是此物理地址内的具体数据,因此for循环中,N1=*p是依次将tab1数组中的数据赋值给变量N1。由此可见,指针是作为一个变量,指向某一个地址。
那么指针到底是如何将某个地址内的数据“拿”出来的?下面通过N1=*p语句做演示说明,N1=*p编译后的汇编代码如图10所示。

C0x00A0C0x00A9的汇编代码即是C程序中的N1=*p。程序先将变量p的值赋值给R3R2R1三个通用寄存器,程序为:
MOV   R3,p(0x12)
MOV   R2,0x13
MOV   R1,0x14
然后调用了一个子函数:LCALL  C?CLDPTR(C:00E4),而C程序中,未定义或使用任何子函数,那么这个子函数是哪里来的?作用是什么?根据标号C:00E4可找到该子函数,程序代码如下:

C:0x00E4   BB0106   CJNE     R3,#0x01,C:00ED
C:0x00E7   8982     MOV      DPL(0x82),R1
C:0x00E9   8A83     MOV      DPH(0x83),R2
C:0x00EB   E0       MOVX     A,@DPTR
C:0x00EC   22       RET      
C:0x00ED   5002     JNC      C:00F1
C:0x00EF   E7       MOV      A,@R1
C:0x00F0   22       RET      
C:0x00F1   BBFE02   CJNE     R3,#0xFE,C:00F6
C:0x00F4   E3       MOVX     A,@R1
C:0x00F5   22       RET      
C:0x00F6    8982    MOV      DPL(0x82),R1
C:0x00F8   8A83     MOV      DPH(0x83),R2
C:0x00FA   E4       CLR      A
C:0x00FB   93       MOVC     A,@A+DPTR
C:0x00FC   22       RET      

此程序功能是:先用R3寄存器的值与0x01比较,当R3的值大于0x01时,再和0xFE做比较,比较的结果有如下情况:
1R3的值等于0x01时,执行如下程序:
C:0x00E7   8982     MOV      DPL(0x82),R1
C:0x00E9   8A83     MOV      DPH(0x83),R2
C:0x00EB   E0       MOVX     A,@DPTR
C:0x00EC   22       RET      
程序功能:读取扩展RAM内的数据并赋值给A,寻址范围065535。当数组用xdata定义时,会跳转到此处。
2R3的值小于0x01即等于0x00时,执行如下程序:
C:0x00EF   E7       MOV      A,@R1
C:0x00F0   22       RET  
程序功能:读取单片机内部256字节RAM内的数据并赋值给A,寻址范围0255。当数组用dataidata定义时,会跳转到此处。如执行N1=*p语句时,即跳转到自处,读取内部RAM地址内的数据。    
3R3的值不等于0x000x01时,通过JNC指令跳转到C:0x00F1处,开始与0xFE做比较。R3的值等于0xFE时,执行如下程序:
C:0x00F4   E3       MOVX     A,@R1
C:0x00F5   22       RET  
程序功能:读取单片机片外RAM内的数据并赋值给A,寻址范围0255。当数组用pdata定义时,会跳转到此处。通常8051单片机不使用pdata定义变量或数组。
4R3的值不等于0xFE时,即R3的值等于0xFF时,跳转到C:0x00F6处执行如下程序:
C:0x00F6   8982     MOV      DPL(0x82),R1
C:0x00F8   8A83     MOV      DPH(0x83),R2
C:0x00FA   E4       CLR      A
C:0x00FB   93       MOVC     A,@A+DPTR
C:0x00FC   22       RET
程序功能:读取单片机内部ROM内的数据并赋值给A,寻址范围065535。当数组用code定义时,如程序③中,tab2数组用code定义,执行p=tab2后,R3的值被赋值为0xFF,再执行N2=*p语句时,即跳转到自处,读取内部ROM地址内的数据。  
由此可见,子函数“C?CLDPTR”的作用是,根据数据所在存储空间,用不同的寻址方式读取某地址下的数据。R3用于确定寻址方式,R3的值与对应的寻址方式对应关系为:
1R3值等于0x00时,片内RAM间接寻址;此时数据用dataidata定义。
2R3值等于0x01时,片外RAM(扩展RAM)间接寻址;此时数据用xdata定义。
3R3值等于0xFE时,片外RAM(扩展RAM)低246字节间接寻址;此时数据用pdata定义
4R3值等于0xFF时,从存储存储器(ROM)进行变址寻址;此时数据用code定义。

3、指针结构
R3R2R1的值是RAM0x120x130x14地址内的值,即变量p映射的RAM地址。而而8位单片机中,不管是何种寻址方式,最大寻址范围是2字节长度(065535),为什么指针*p却占用了3字节RAM空间呢?下面通过程序④说明。
程序④:
#include "REG52.H"         
unsigned char tab1[8];
unsigned char idata tab2[8];
unsigned char xdata tab3[8];   
unsigned char pdata tab4[8];
unsigned char codetab5[8]={0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80}; 
unsigned char  *p;
void main()
{        
         p=tab1;
         p=tab2;
         p=tab3;
         p=tab4;
         p=tab5;
}
Debug Session模式下可知,程序中数组与变量所映射的物理地址为及物理存储区分别为:
tab1 :        0x080x0F                        单片机内部RAM
tab2     0x030x1A                       单片机内部RAMidata
tab3     0x080x0F                        单片机扩展RAMxdata
tab4     0x000x08                        单片机扩展RAM256字节(pdata
tab5     0x0003D0x0044            单片机程序存储区(code
p            0x100x12                        单片机内部RAM
注:扩展RAM可以在物理上可以分为片内或片外,如STC15系列增强型单片机的扩展RAM与单片机是封装在一起的,即片内扩展RAM;传统8051单片机没有片内扩展RAM,需连接外部RAM芯片,此为片外扩展RAM
Memory Windows窗口下,监视变量p映射的RAM地址:0x100x12的数值变化,如图11所示。

通过“Step”功能按钮执行住函数中的5调语句,可观察到0x100x12寄存器的数据变化:

执行p=tab1后,0x100x110x120x000x000x08
执行p=tab2后,0x100x110x120x000x000x13
执行p=tab3后,0x100x110x120x010x000x08
执行p=tab4后,0x100x110x120xFE0x000x00
执行p=tab5后,0x100x110x120xFF0x000x3D
由此可知,0x10的赋值取决于p指向的物理存储区,0x110x12的值是数据存储区的地址。指针所映射的首地址,会根据指向的物理存储区被编译器赋不同的值:0x000x010xFE0xFF。这与程序③得到的结论一致,程序③中,寄存器R3R2R1对应值实际上就是指针所映射的3字寄存器数值。
结合程序③编译分析,当需要引用某物理地址内数据时,会调用“C?CLDPTR”函数,函数功能就是根据这些赋值确定使用何种寻址方式引用数据。而这一过程包括“C?CLDPTR”函数都是编译器自动完成的。
在汇编语言中,R1寄存器可以用于间接寻址,如:MOV  A@R1。不能写为MOV A@12H。因此在程序③中,将变量p对应的3字节数据赋值给R3R2R1
综上所述,Keil C-51编译环境下,指针是一个占3字节的特殊变量,编译器编译程序时,自动生成判断寻址方式的子函数,并根据根据目标数据所在的物理存储区不同,为指针首字节赋值,根据赋值的不同,进行不同方式的寻址;指针的后2字节,用于存放引用的地址。

调试训练:
下面的程序编译器会怎样编译?与程序③有何不同?请根据程序③和程序④的分析方式分析程序⑤的执行结果。
程序⑤
#include "REG52.H"         
unsigned char tab1[8];
unsigned char codetab2[8]={0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff}; 
unsigned char  *p;
void main()
{        
         unsignedchar i;
         p=tab1;
         for(i=0;i<8;i++,p++)
         *p=i;
         
         p=tab2;
         for(i=0;i<8;i++,p++) 
         *p=i;
}

思考:下列语句中:
p=tab2;
for(i=0;i<8;i++,p++)
*p=i;
执行完for循环后,tab2数组内的值会改变吗?为什么?
4、指针意义
在汇编编程中,由于单片机数据存放的物理存储区不同,导致有不同的寻址方式,用户进行必须根据这一规律设计程序。而在C语言中,不管目标数据所在的物理存储区如何,指针都可指向该地址,并自动编译寻址方式。
但指针并不是万能的,如程序⑤中:
p=tab2;
for(i=0;i<8;i++,p++)
*p=i;
这些语句编译时并不会报错,但却不能实现功能,因为tab2数组是定义在程序存储器(ROM)的常量数组,ROM内的数据更改是不能通过这种方式实现的。因此,当用户不明确单片机的物理存储区特性时,使用指针会容易出错。先将程序⑤中的主函数语句做如下修改,得到程序⑥:
#include"REG52.H"         
unsignedchar tab1[8];
unsignedchar code tab2[8]={0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff};
unsignedchar  *p;
voidmain()
{        
         unsigned char i;
         for(i=0;i<8;i++,p++)
         tab1[ i]=i;
         
         for(i=0;i<8;i++,p++)
         tab2[ i]=i;
}
单独看第一个for循环,可实现与程序⑤一样的效果,即tab1数组内被赋值为:01234567
第二个for循环从语句上可认为是与程序⑤功能相同,但实际上,不管是程序⑤还是程序⑥,都不能实现对tab2数组的赋值。但在程序⑥中,编译器会提示错误,如图12所示。
因此,指针的使用不当,不仅会带来程序运行结果的不正确,同时也难以发现这些错误。
对比程序⑤和程序⑥中的两段程序:

p=tab1;                                                      for(i=0;i<8;i++,p++)                        
for(i=0;i<8;i++,p++)                                   tab1[ i]=i;
*p=i;

它们执行的结果是一样的,那么哪种更好呢?对于初学者来说,显然是后者,因为后者更易于理解程序含义,而前者必须要理解指针在此处的作用;那么对于有经验的程序员呢?也是后者,因为程序执行效率上,后者也要大于前者,因为程序⑤在编译过程中,编译器始终会生成一个子函数用于确定寻址方式,再赋值;程序⑥则是直接确定了寻址方式执并行进行赋值。尽管执行效率的降低在接受范围内,但对于一个简单、明了的功能来说,用简单的方式实现要比复杂方式合理。
设计者在程序中使用指针的目的往往是让程序具有可移植性,但8051单片机的功能是有限的,它实现的功能相对固化,如时间显示、数据采集等等,这些功能确定后,几乎不会做出更改,基于此特点,8051单片机的代码代码量都不长。因此即便是不同构架的单片机程序互相移植,代码的修改并不复杂,移植过程中,也几乎都是针对不同构架单片机的I/O工作方式不同、指令周期不同做常规修改;或是关键字的修改。因此合理的设计单片机程序,尽可能的提高程序的效率、稳定性、可阅读性才是程序设计的核心主旨。指针在8051单片机中固然可以使用,但并不能说明指针的使用就一定是高效、准确、易于他人理解。
原创粉丝点击