通常认为,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”。同理,“p(D:0x0A)”即变量“p”的首地址为“0AH”。
下面通过单步执行程序来观察RAM内的数据变化,打开两个Memory Windows窗口,在Keil软件下方显示为Memory1和Memory2,在两个窗口中,分别做如图2-1、2-2所示的设置。
两个Address填写的内容分别是:D:0x08、D:0x0A,即变量“j”和变量“p”的首地址,输入后回车,便可监视RAM中该地址下的数据。设置好后,准备调试。
在Debug Session模式中,箭头所指处即为即将执行的语句,单击“Step”功能按钮(或按F11键),让程序运行,如图3所示。
第一次单击“Step”按钮后,Memory1窗口内数据如图4所示。
由调试结果可知,08H数据由00H变为ABH,09H数据由00H变为CDH,出现这种变化是因为执行了语句j=0xabcd;08H为变量“j”高八位,存储“AB”,09H为变量“j”低八位,存储“CD”。
第二次单击“Step”按钮,执行语句:p=0xaa;此时需观察Memory2窗口内数据,如图5所示。
由调试结果可知,0CH处值由00变为“AAH”,程序相吻合。这里需要注意,在Keil C-51编译环境下,指针变量,不管长度是单字节或是双字节,指针变量所占字节数为3字节。故此处“AAH”不是存储在0AH而存储在0CH(0A+2)地址中。
综上所述,指针实际上是变量,都是映射到RAM中的一段存储空间,区别是,指针占用3字节,而其他变量可根据需要设定其所占RAM是1字节(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=0x01,N2=0x10。这里都是讲数组内的数据赋值给变量,但存在区别,tab1数组使用的是单片机RAM空间,而tab2数组使用的是单片机程序存储区(ROM)空间。尽管使用C语言为变量赋值时语句相同,但编译结果并不相同,此程序编译后的结果如图6所示。
由编译结果可知,N1=tab1[0]语句实际上是直接寻址,而N2=tab2[0]是寄存器变址寻址。不管是何种寻址方式,都是将一个物理地址内的数据取出来使用:tab1数组中,tab[0]对应的RAM地址是0x0A,tab[1]对应的RAM地址是0x0B……以此类推;tab2数组中,tab[0]对应的ROM地址是0x00A5,tab[1]对应的ROM地址是0x00A6……以此类推。不管这些数组或变量所在的RAM或ROM地址如何,用户最终需要的是数组或变量的数据,而指针,就是通过变量或数组的物理地址访问数据,也就是说,通过指针,同样可以访问数组或变量数据。现将程序②做出调整,得到程序③如下:
#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个数值依次被赋值给N1;tab2数组内的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所示。
C:0x00A0至C:0x00A9的汇编代码即是C程序中的N1=*p。程序先将变量p的值赋值给R3、R2、R1三个通用寄存器,程序为:
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做比较,比较的结果有如下情况:
(1)R3的值等于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,寻址范围0~65535。当数组用xdata定义时,会跳转到此处。
(2)R3的值小于0x01即等于0x00时,执行如下程序:
C:0x00EF E7 MOV A,@R1
C:0x00F0 22 RET
程序功能:读取单片机内部256字节RAM内的数据并赋值给A,寻址范围0~255。当数组用data或idata定义时,会跳转到此处。如执行N1=*p语句时,即跳转到自处,读取内部RAM地址内的数据。
(3)R3的值不等于0x00或0x01时,通过JNC指令跳转到C:0x00F1处,开始与0xFE做比较。R3的值等于0xFE时,执行如下程序:
C:0x00F4 E3 MOVX A,@R1
C:0x00F5 22 RET
程序功能:读取单片机片外RAM内的数据并赋值给A,寻址范围0~255。当数组用pdata定义时,会跳转到此处。通常8051单片机不使用pdata定义变量或数组。
(4)R3的值不等于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
关键字:单片机 C语言 指针 Keil-C51