通常认为,
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 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/1f8cacd09ff8abc16bc3738e06729a09.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/2ac3b3afd5ad1fec561ceef91ca24945.jpeg)
图中箭头所指处即为变量在 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 所示的设置。
![](https://i-blog.csdnimg.cn/blog_migrate/e0b9d79207676102987bf3c2275fb1b8.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/e78c7eb5076ba36a5bde9d49c515fa44.jpeg)
两个 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
程序功能:读取单片机内部 ROM 内的数据并赋值给 A ,寻址范围 0 ~ 65535 。当数组用 code 定义时,如程序③中, tab2 数组用 code 定义,执行 p=tab2 后, R3 的值被赋值为 0xFF ,再执行 N2=*p 语句时,即跳转到自处,读取内部 ROM 地址内的数据。
由此可见,子函数“ C?CLDPTR ”的作用是,根据数据所在存储空间,用不同的寻址方式读取某地址下的数据。 R3 用于确定寻址方式, R3 的值与对应的寻址方式对应关系为:
1 、 R3 值等于 0x00 时,片内 RAM 间接寻址;此时数据用 dataidata 定义。
2 、 R3 值等于 0x01 时,片外 RAM (扩展 RAM )间接寻址;此时数据用 xdata 定义。
3 、 R3 值等于 0xFE 时,片外 RAM (扩展 RAM )低 246 字节间接寻址;此时数据用 pdata 定义
4 、 R3 值等于 0xFF 时,从存储存储器( ROM )进行变址寻址;此时数据用 code 定义。
3 、指针结构
R3 、 R2 、 R1 的值是 RAM 中 0x12 、 0x13 、 0x14 地址内的值,即变量 p 映射的 RAM 地址。而而 8 位单片机中,不管是何种寻址方式,最大寻址范围是 2 字节长度( 0 ~ 65535 ),为什么指针 *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 : 0x08 ~ 0x0F 单片机内部 RAM
tab2 : 0x03 ~ 0x1A 单片机内部 RAM ( idata )
tab3 : 0x08 ~ 0x0F 单片机扩展 RAM ( xdata )
tab4 : 0x00 ~ 0x08 单片机扩展 RAM 低 256 字节( pdata )
tab5 : 0x0003D ~ 0x0044 单片机程序存储区( code )
p : 0x10 ~ 0x12 单片机内部 RAM
注:扩展 RAM 可以在物理上可以分为片内或片外,如 STC15 系列增强型单片机的扩展 RAM 与单片机是封装在一起的,即片内扩展 RAM ;传统 8051 单片机没有片内扩展 RAM ,需连接外部 RAM 芯片,此为片外扩展 RAM 。
在 Memory Windows 窗口下,监视变量 p 映射的 RAM 地址: 0x10 ~ 0x12 的数值变化,如图 11 所示。
通过“ Step ”功能按钮执行住函数中的 5 调语句,可观察到 0x10 ~ 0x12 寄存器的数据变化:
执行 p=tab1 后, 0x10 、 0x11 、 0x12 : 0x00 、 0x00 、 0x08
执行 p=tab2 后, 0x10 、 0x11 、 0x12 : 0x00 、 0x00 、 0x13
执行 p=tab3 后, 0x10 、 0x11 、 0x12 : 0x01 、 0x00 、 0x08
执行 p=tab4 后, 0x10 、 0x11 、 0x12 : 0xFE 、 0x00 、 0x00
执行 p=tab5 后, 0x10 、 0x11 、 0x12 : 0xFF 、 0x00 、 0x3D
由此可知, 0x10 的赋值取决于 p 指向的物理存储区, 0x11 、 0x12 的值是数据存储区的地址。指针所映射的首地址,会根据指向的物理存储区被编译器赋不同的值: 0x00 , 0x01 , 0xFE , 0xFF 。这与程序③得到的结论一致,程序③中,寄存器 R3 、 R2 、 R1 对应值实际上就是指针所映射的 3 字寄存器数值。
结合程序③编译分析,当需要引用某物理地址内数据时,会调用“ C?CLDPTR ”函数,函数功能就是根据这些赋值确定使用何种寻址方式引用数据。而这一过程包括“ C?CLDPTR ”函数都是编译器自动完成的。
在汇编语言中, R1 寄存器可以用于间接寻址,如: MOV A , @R1 。不能写为 MOV A , @12H 。因此在程序③中,将变量 p 对应的 3 字节数据赋值给 R3 、 R2 、 R1 。
综上所述, 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;
}
思考:下列语句中:
它们执行的结果是一样的,那么哪种更好呢?对于初学者来说,显然是后者,因为后者更易于理解程序含义,而前者必须要理解指针在此处的作用;那么对于有经验的程序员呢?也是后者,因为程序执行效率上,后者也要大于前者,因为程序⑤在编译过程中,编译器始终会生成一个子函数用于确定寻址方式,再赋值;程序⑥则是直接确定了寻址方式执并行进行赋值。尽管执行效率的降低在接受范围内,但对于一个简单、明了的功能来说,用简单的方式实现要比复杂方式合理。
设计者在程序中使用指针的目的往往是让程序具有可移植性,但 8051 单片机的功能是有限的,它实现的功能相对固化,如时间显示、数据采集等等,这些功能确定后,几乎不会做出更改,基于此特点, 8051 单片机的代码代码量都不长。因此即便是不同构架的单片机程序互相移植,代码的修改并不复杂,移植过程中,也几乎都是针对不同构架单片机的 I/O 工作方式不同、指令周期不同做常规修改;或是关键字的修改。因此合理的设计单片机程序,尽可能的提高程序的效率、稳定性、可阅读性才是程序设计的核心主旨。指针在 8051 单片机中固然可以使用,但并不能说明指针的使用就一定是高效、准确、易于他人理解。
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 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/1f8cacd09ff8abc16bc3738e06729a09.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/2ac3b3afd5ad1fec561ceef91ca24945.jpeg)
图中箭头所指处即为变量在 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 所示的设置。
![](https://i-blog.csdnimg.cn/blog_migrate/e0b9d79207676102987bf3c2275fb1b8.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/e78c7eb5076ba36a5bde9d49c515fa44.jpeg)
两个 Address 填写的内容分别是: D:0x08 、 D:0x0A ,即 变 量“ j ”和变量“ p ”的首地址,输入后回车,便可监视 RAM中该 地址下的数据。设置好后,准备调试。
在 Debug Session 模式中,箭头所指处即为即将执行的语句,单击“ Step ”功能按钮(或按 F11 键),让程序运行,如图 3 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/8cd824ae0f48437e7fc42c08d692526a.jpeg)
第一次单击“ Step ”按钮后, Memory1 窗口内数据如图 4 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/818d78108b618b4c6b03f5b6a39d1caa.jpeg)
由调试结果可知, 08H 数据由 00H 变为 ABH , 09H 数据由 00H 变为 CDH ,出现这种变化是因为执行了语句 j=0xabcd;08H 为变量“ j ”高八位,存储“ AB ”, 09H 为变量“ j ”低八位,存储“ CD ”。
第二次单击“ Step ”按钮,执行语句: p=0xaa; 此时需观察 Memory2 窗口内数据,如图 5 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/e01cc92e3de17f1e663d6dae81266e5d.jpeg)
由调试结果可知, 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 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/2a9c6ae5339eb9002e5bcae06faf108a.jpeg)
由编译结果可知, 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 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/869e3d4401a282cea3520c7e5881e415.jpeg)
Value 为当前变量数值,程序为运行前, p 值为 0x00 ,单击 Step 按键功能后,执行 p=tab1;p 值变为 0x0A ,如图 8 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/a5d753cd54a5e64daa42aa2c35f594bd.jpeg)
0x0A 是什么值呢?将鼠标移至 tab1 数组位置,可显示出数组所在的物理地址, 0x0A 就是数组 tab1 的首地址,如图 9 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/5336a01bbfc240856a53aa702729688c.jpeg)
p=tab1 就是将 tab1 数组的首地址赋值给变量 p ,执行 p++ 即地址值加 1 ; *p 则是此物理地址内的具体数据,因此 for 循环中, N1=*p 是依次将 tab1 数组中的数据赋值给变量 N1 。由此可见,指针是作为一个变量,指向某一个地址。
那么指针到底是如何将某个地址内的数据“拿”出来的?下面通过 N1=*p 语句做演示说明, N1=*p 编译后的汇编代码如图 10 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/9dd9d61780e72bbea22436b50d425c57.jpeg)
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
程序功能:读取单片机内部 ROM 内的数据并赋值给 A ,寻址范围 0 ~ 65535 。当数组用 code 定义时,如程序③中, tab2 数组用 code 定义,执行 p=tab2 后, R3 的值被赋值为 0xFF ,再执行 N2=*p 语句时,即跳转到自处,读取内部 ROM 地址内的数据。
由此可见,子函数“ C?CLDPTR ”的作用是,根据数据所在存储空间,用不同的寻址方式读取某地址下的数据。 R3 用于确定寻址方式, R3 的值与对应的寻址方式对应关系为:
1 、 R3 值等于 0x00 时,片内 RAM 间接寻址;此时数据用 dataidata 定义。
2 、 R3 值等于 0x01 时,片外 RAM (扩展 RAM )间接寻址;此时数据用 xdata 定义。
3 、 R3 值等于 0xFE 时,片外 RAM (扩展 RAM )低 246 字节间接寻址;此时数据用 pdata 定义
4 、 R3 值等于 0xFF 时,从存储存储器( ROM )进行变址寻址;此时数据用 code 定义。
3 、指针结构
R3 、 R2 、 R1 的值是 RAM 中 0x12 、 0x13 、 0x14 地址内的值,即变量 p 映射的 RAM 地址。而而 8 位单片机中,不管是何种寻址方式,最大寻址范围是 2 字节长度( 0 ~ 65535 ),为什么指针 *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 : 0x08 ~ 0x0F 单片机内部 RAM
tab2 : 0x03 ~ 0x1A 单片机内部 RAM ( idata )
tab3 : 0x08 ~ 0x0F 单片机扩展 RAM ( xdata )
tab4 : 0x00 ~ 0x08 单片机扩展 RAM 低 256 字节( pdata )
tab5 : 0x0003D ~ 0x0044 单片机程序存储区( code )
p : 0x10 ~ 0x12 单片机内部 RAM
注:扩展 RAM 可以在物理上可以分为片内或片外,如 STC15 系列增强型单片机的扩展 RAM 与单片机是封装在一起的,即片内扩展 RAM ;传统 8051 单片机没有片内扩展 RAM ,需连接外部 RAM 芯片,此为片外扩展 RAM 。
在 Memory Windows 窗口下,监视变量 p 映射的 RAM 地址: 0x10 ~ 0x12 的数值变化,如图 11 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/3247977af845cf5f3d9900c22c66f082.jpeg)
通过“ Step ”功能按钮执行住函数中的 5 调语句,可观察到 0x10 ~ 0x12 寄存器的数据变化:
执行 p=tab1 后, 0x10 、 0x11 、 0x12 : 0x00 、 0x00 、 0x08
执行 p=tab2 后, 0x10 、 0x11 、 0x12 : 0x00 、 0x00 、 0x13
执行 p=tab3 后, 0x10 、 0x11 、 0x12 : 0x01 、 0x00 、 0x08
执行 p=tab4 后, 0x10 、 0x11 、 0x12 : 0xFE 、 0x00 、 0x00
执行 p=tab5 后, 0x10 、 0x11 、 0x12 : 0xFF 、 0x00 、 0x3D
由此可知, 0x10 的赋值取决于 p 指向的物理存储区, 0x11 、 0x12 的值是数据存储区的地址。指针所映射的首地址,会根据指向的物理存储区被编译器赋不同的值: 0x00 , 0x01 , 0xFE , 0xFF 。这与程序③得到的结论一致,程序③中,寄存器 R3 、 R2 、 R1 对应值实际上就是指针所映射的 3 字寄存器数值。
结合程序③编译分析,当需要引用某物理地址内数据时,会调用“ C?CLDPTR ”函数,函数功能就是根据这些赋值确定使用何种寻址方式引用数据。而这一过程包括“ C?CLDPTR ”函数都是编译器自动完成的。
在汇编语言中, R1 寄存器可以用于间接寻址,如: MOV A , @R1 。不能写为 MOV A , @12H 。因此在程序③中,将变量 p 对应的 3 字节数据赋值给 R3 、 R2 、 R1 。
综上所述, 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
数组内被赋值为:
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
。
第二个
for
循环从语句上可认为是与程序⑤功能相同,但实际上,不管是程序⑤还是程序⑥,都不能实现对
tab2
数组的赋值。但在程序⑥中,编译器会提示错误,如图
12
所示。
![](https://i-blog.csdnimg.cn/blog_migrate/18728748736dc68e7ec846339f8bcbde.jpeg)
因此,指针的使用不当,不仅会带来程序运行结果的不正确,同时也难以发现这些错误。
对比程序⑤和程序⑥中的两段程序:
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 单片机中固然可以使用,但并不能说明指针的使用就一定是高效、准确、易于他人理解。