历史上的今天
返回首页

历史上的今天

今天是:2025年02月19日(星期三)

正在发生

2020年02月19日 | 单片机C语言指针意义浅析—Keil-C51

2020-02-19 来源:51hei

通常认为,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;
}

思考:下列语句中:

推荐阅读

史海拾趣

帝特(DTECH)公司的发展小趣事

近年来,帝特积极寻求与行业内优秀企业的合作机会。2024年3月,帝特科技与技象科技在广州帝特总部签署战略合作框架协议,双方就物联网通信产品展开深入合作。这一合作不仅有助于帝特在物联网领域的技术积累和业务拓展,也为公司未来的发展注入了新的活力。

Hendon Semiconductors公司的发展小趣事

Hendon Semiconductors以其强大的定制化集成电路设计能力而闻名。公司拥有一支经验丰富的设计团队,能够为客户提供从概念到量产的一站式解决方案。这种定制化服务不仅满足了客户对特殊功能和高性能的需求,也帮助Hendon Semiconductors在竞争激烈的市场中脱颖而出。通过不断积累成功案例和技术经验,Hendon Semiconductors逐渐在定制化集成电路设计领域建立了良好的口碑。

ELNA(依娜)公司的发展小趣事

进入电子领域后,ELNA迅速在电子元件领域取得了突破。公司凭借其在材料科学和制造工艺方面的专长,成功开发出了一系列高性能的电子元件产品。这些产品广泛应用于通信、计算机、消费电子等领域,为ELNA赢得了广泛的客户群。

为了保持技术领先,ELNA不断投入研发资源,加强技术创新。公司积极引进国际先进的生产设备和检测手段,提升产品品质和可靠性。同时,ELNA还加强了与国内外科研机构和高校的合作,共同推动电子元件技术的发展。

EOS POWER INDIA Pvt公司的发展小趣事

在稳固了印度市场后,EOS开始积极拓展国际市场。公司参加了多个国际电子展会,与全球各地的客户和合作伙伴建立了广泛的联系。通过与国际知名企业的合作与交流,EOS不断吸收先进的管理经验和技术理念,并将其应用到自己的产品和服务中。这些举措不仅提升了EOS的国际知名度,还为公司带来了更多的商业机会。

Eastron Corp公司的发展小趣事

随着社会的不断发展,Eastron Corp深刻认识到企业的社会责任和可持续发展的重要性。公司积极参与社会公益事业,为当地社区和环境保护做出贡献。同时,Eastron还注重节能减排和环保生产,通过技术创新和工艺改进,降低生产过程中的能耗和排放。这些举措不仅提升了公司的社会形象,也为公司的长期发展奠定了基础。

请注意,这些故事仅为虚构示例,不代表任何真实事件或公司历史。

GE Solid State公司的发展小趣事
按照电路图搭建电路,注意元件的连接方式和极性。

问答坊 | AI 解惑

USB驱动分析 绝对经典的一本书

usb源码详析,linux-usb-hub,linux-usb-core, 嬉笑怒骂、娓娓道来。可惜原作者没有署名,在此向原作者致以崇高的敬意!…

查看全部问答>

HD44780

HD44780HD44780HD44780…

查看全部问答>

液位传感器

本帖最后由 paulhyde 于 2014-9-15 09:22 编辑 液位传感器.doc  …

查看全部问答>

嵌入式Linux开发公益体验活动介绍(5月16号本周六)

嵌入式Linux开发公益体验活动介绍(5月16号本周六)                 (目前已经只剩6个名额,预报从速!!) 体验活动目标: 本活动针对嵌入式Linux开发的初学人员,能快速了解嵌入式Linux ...…

查看全部问答>

求本书 the indispensable pc hardware book 哪有啊 找了半天了谢谢各位大大了

求本书 the indispensable pc hardware book 哪有啊 找了半天了谢谢各位大大了…

查看全部问答>

如果成形滤波采用平方根升余弦的话,接受端的匹配滤波器 怎么实现?

如题,用什么函数可以实现呢  各位大侠  (matlab)…

查看全部问答>

谁用USB单片机开发过加密狗

有成熟技术者,可与我联系,本人还可以资助一点开发费,嘻!mail@net9999.com …

查看全部问答>

求keil uv4 下的LPC2294的工程模板

新手, 现在LPC2294的arm7单片机,求个模板。…

查看全部问答>

TI的图形库

这里借鉴了“https://home.eeworld.com.cn/space.php?uid=139305&op=photo”同学的帖子,也简单的实现了TI的图形库功能,拿来炫炫。因为我选用的屏是320*480的,所以画面显得更好看些~     现在还没有做控件方面的东西,有兴趣的 ...…

查看全部问答>

【MSP430共享】大家一起DIY一块MSP430开发板

最近手里有点430的片子,大家一起想想看做个什么开发板,只限F149和F5438,我只有这两种片子,建议用5438搞,原则是功能外设一定要多,价格成本一定要低,大家多提意见,我选择贡献最大的5名坛友到时每人送一块开发板pcb+430CPU一枚,具体大家可以 ...…

查看全部问答>