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

通常认为, 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
程序功能:读取单片机内部 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;
}

思考:下列语句中:
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 所示。
因此,指针的使用不当,不仅会带来程序运行结果的不正确,同时也难以发现这些错误。
对比程序⑤和程序⑥中的两段程序:

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 单片机中固然可以使用,但并不能说明指针的使用就一定是高效、准确、易于他人理解。
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值