第3章存 储 系 统3.1哈佛结构与冯·诺依曼结构了解计算机发展史的人都知道,冯·诺依曼设计的存储程序计算机结构对计算机的发展意义深远,其典型特点是数据和指令无区别地存放于可按地址寻访的存储器中,即存储器本身并不关心存放的是指令还是数据,而是直接把内容存放于指定的位置。哈佛结构在冯·诺依曼结构的基础上进行了一些改进,最主要的就是把数据和指令分别存放于不同的存储器,称为数据存储器和程序存储器。程序指令存储和数据存储分开,数据的存取和指令的读取可以同时进行,有效提高了指令的执行速度。 图3.151单片机内部总线结构51单片机内部总线图如图3.1所示,从图中可以发现,处于结构图左上方的RAM(内部数据存储器)和右上方的EPROM/ROM(内部指令存储器)处于两个独立的存取体内,具有不同的地址总线,但共用数据线,因此其寻址过程是独立的,这样可以提升两个存储体的读写并行性。 基于Intel 8086系列的PC存储系统采用的是冯·诺依曼结构,而基于51核的单片机存储系统采用的是哈佛结构。51单片机在进行数据读写时必须指明读写的存储体,因此51单片机中的数据读写代码有别于PC中的数据读写代码。 3.251单片机存储系统 51单片机采用了哈佛结构,存储器分为数据存储器和程序存储器。同时,由于成本和芯片集成度等多方面因素,集成于芯片上的数据存储器和程序存储器的容量有限,在一些需要更大存储器空间的特殊应用中需在芯片外配置数据存储器和程序存储器。因此,51单片机可能访问的存储器包括片内数据存储器、片外数据存储器、片内程序存储器和片外程序存储器。 由于51单片机片上集成了一些接口,接口的设置和控制需要通过访问一系列特殊功能的寄存器完成,而51单片机也为这些寄存器配置了一个独立的地址空间。因此,51单片机可能访问的地址空间除了与存储器相关的地址空间外,还包括一个特殊功能寄存器地址空间。51单片机的完整存储系统结构如图3.2所示。 图3.251存储系统结构图 从图3.2中可以看出,片内数据存储器、片外数据存储器、片内程序存储器以及片外程序存储器都是从地址0000H开始编址的。内核启动时,由加载到EA上的电平决定是使用片内程序存储器还是片外程序存储器。EA接高电平(EA=1)时,内核使用片内存储器,当访问地址超过片内程序存储器范围时,访问外部程序存储器;当EA接低电平(EA=0)时,内核使用片外存储器,此时片内程序存储器将不被使用。因此,内核访问程序存储器时无须区分访问的是片内还是片外的程序存储器。 由此可以认为,51单片机的存储器有4个独立的存储地址空间: 程序存储器地址空间、内部数据存储器地址空间、外部数据存储器地址空间和特殊功能的寄存器地址空间。这些地址空间的编址是独立的,访问不同的地址空间时需要使用不同的指令。 由于内部数据存储器的低128字节采用直接寻址,其访问速度较快;高128字节采用间接寻址,其访问速度相对较慢,因此在编写代码时,这两部分地址的访问代码也有所区别。 综上所述,根据访问特性的不同,51单片机可访问的地址空间可简单分为5个不同的地址段: 程序存储器地址空间(不分片内还是片外)、内部数据存储器低128字节地址空间、内部数据存储器高128字节地址空间、外部数据存储器地址空间和特殊功能寄存器地址空间。在Keil开发环境中,在定义“变量”时需要加上不同的关键字以指定存放的地址空间。 3.3C51变量定义 C语言中,定义变量的实质是告诉编译器在存储器内开辟一段(1,2,4,8字节)区域,以及数据在该区域中的存放方式(n位无符号数方式、n位有符号数方式、单精度浮点方式、双精度浮点方式),记录其位置与变量名之间的对应关系,以实现通过变量名访问对应的内存空间。 由于X86系统采用的是冯·诺依曼结构,所有指令和数据均以同等方式存放于主存中,在8086系统中定义变量无须告诉编译器存放于哪个存储器(地址空间),因此在8086系统中学习C语言时,定义变量的方式为“变量类型+变量名”。其中“变量类型”告诉编译器数据在内存中占用的位数和存放方式。如语句“char a”在内存中开辟了一个8位的空间,以补码整数方式存放数据;语句“float b”在内存中开辟了一个32位的空间,以单精度浮点数方式存放数据。 当然,8086系统中定义变量也有一个例外,那就是寄存器变量。定义变量时可以将操作频繁的变量存放在CPU内部的寄存器中,以提升程序执行的速度,其定义方式为“register+变量类型+变量名”。在定义变量时,前面加上的register就是告诉编译器将变量放置在寄存器中。 在51单片机中,可供操作的“变量”存放的位置可能存在于5种不同的地址空间。因此,在定义变量时需要告诉编译器将“变量”存放在哪个地址空间。这里之所以将变量加引号是因为一些“变量”并非真正意义的变量,可能是不可修改的。 3.4内/外部数据存储器空间的访问 真正意义的变量可存放的地址空间只有内部数据存储器和外部数据存储器。而内部存储器又因访问速度的不同分为低128字节空间和高128字节空间。对于一些使用频繁的变量,应优先选用内部数据存储器的低128字节空间,其次选择内部数据存储器的高128字节空间,最后选择外部数据存储器。 下面将列出指定变量存放空间的关键字。 (1) data。 定义变量时,加上关键字data,编译器便会将该变量存放在内部数据存储器的低128字节空间中,如下例所示。data char a; char data b;以上两种方式的效果是一样的,即变量类型指示字段和变量存放位置指示字段出现的顺序可以交换。 在51单片机中,该类变量访问时速度最快。但是由于该空间内的低32字节用于存放4个寄存器组,部分空间用于存放位变量,堆栈也需要存放于该空间,因此该空间可用于放置普通变量的空间并不多,建议只将一些使用频率较高的变量放置到该空间。 (2) idata。 使用idata指定的变量存放位置可以为低128字节空间和高128字节空间,由编译器根据内存安排情况自行选择。对于一些使用频率不太高的变量,可以使用该关键字。该变量定义示例如下。idata char a; char idata b;(3) pdata和xdata。 当系统布设有外部数据存储器时(在一些应用中,当内部数据存储器无法满足需求时,可以在芯片外布设外部数据存储器),可以使用pdata和xdata将变量指定到外部数据存储器。两种指定方式有一定区别,在具体使用时可以查阅相关资料。 (4) bit和bdata。 在一些工程运用中,为了存储某个事物的状态,需定义相应的变量。一些事物可能只有两种状态,如某个灯的开关状态、蜂鸣器是否在响、某个设备是否在工作等,人们称用于存放这种状态的变量为开关变量或位变量。如果用一个char或unsigned char变量存放开关变量,则会比较浪费空间。 51单片机中,专门针对开关变量在内部数据存储器的低128字节区域中设置了可进行位寻址的位寻址区,其地址范围为20~2FH,一共有128位,占用16字节。在该区域内定义的位变量只占用1位的空间,这将大大节省内存开销。位变量的定义示例如下。bit a;需要注意的是,位变量的定义无须指定其存放位置,这是因为51单片机中的位变量只能存放在可位寻址区。 如果希望将非位变量存放在可位寻址区,则在定义变量时应加上bdata关键字,其变量定义示例如下。bdata char a; char bdata b;需要注意的是,可位寻址的区域大小只有16字节,因此bdata关键字需慎用。 3.5程序存储器空间的访问 在一些工程应用中,往往需要存放一些大型的、在系统运行期间无须改变的数组。例如在需要显示汉字的场合,可能需要存放常用汉字的字形码。在这种情况下,将其存放于内部数据存储器几乎是不可能的,而且也没有必要。对于这种在系统运行期间无须修改的数组或变量,可以将其存放在容量较大的程序存储器内(内部数据存储器至多256字节,而程序存储器多达4K字节甚至更多)。 若需将变量或数组存放于程序存储器,则需使用关键字code,示例如下。code int maxNumber=256; code unsigned char fontCode\[1024\]={0x00,0xff,...};代码中的“...”部分表示省略未列出的内容。需要注意的是,用该方式指定的“变量”实际是常量,在程序中不能对其进行赋值,只能在定义时赋初值,否则会出错。 3.6特殊功能寄存器及特殊功能的位3.6.1特殊功能寄存器特殊功能寄存器(sfr)是51单片机中各功能部件对应的寄存器,用于存放相应功能部件的控制命令、状态或数据。向某一特殊功能寄存器写入数据可以对某一功能部件发送命令或数据;从某一特殊功能寄存器读取数据则可以获取某一功能部件的状态或从某一功能部件接收数据。 例如,向P1写数据可以实现P1.0~P1.7引脚输出的电平置为设定状态;从P1读取数据可以了解P1.0~P1.7各引脚上接入的高电平或低电平状态。 每个特殊功能的寄存器都有唯一的读/写地址与之对应,在对特殊功能寄存器访问前,需完成特殊功能寄存器的定义。为便于交流,每个特殊功能寄存器都有一个通用的特殊功能寄存器名称,通常用其通用名代替具体的功能寄存器。当对通用数据输入/输出口0(通用名为P0,地址为0x80)进行读写操作时,可以说成“对P0进行读写操作”。编程时也可以根据需要自行定义特殊功能寄存器的名称,但需参考该通用寄存器名,以免引起误解,降低程序可读性。51核中各特殊功能寄存器的功能及地址对照关系如表3.1所示。续表表3.1特殊功能寄存器地址对照通用名功能地址P0通用数据输入/输出口00x80P1通用数据输入/输出口10x90P2通用数据输入/输出口20xA0P3通用数据输入/输出口30xB0TMOD定时/计数器工作方式控制寄存器0x89TCON定时/计数器控制寄存器0x88TL0定时/计数器0计数值低字节0x8ATH0定时/计数器0计数值高字节0x8CTL1定时/计数器1计数值低字节0x8BTH1定时/计数器1计数值高字节0x8DPCON电源、串行通信波特率控制寄存器0x87SCON串行通信控制寄存器0x98SBUF串行通信输入/输出数据缓冲寄存器0x99IE中断允许/禁止控制寄存器0xA8IP中断优先级控制寄存器0xB8A(ACC)累加器0xE0BB寄存器0xF0PSW程序状态字0xD0DPL数据指针低字节0x82DPH数据指针高字节0x83SP堆栈指针0x81在Keil中,如果要对某一特殊功能的寄存器进行读写操作,则必须先用“特殊功能寄存器定义语句”为其指定操作的名字,即特殊功能寄存器名,只有这样才能通过寄存器名完成操作。特殊功能寄存器定义语句格式如下。sfr 特殊功能寄存器名=特殊功能寄存器地址;例如,若对51的通用数据输入/输出口1进行操作,则先要为其指定一个用于操作的特殊功能寄存器名字。由于其对应的特殊功能寄存器操作地址为0x90,因此若想将其对应的特殊功能寄存器的名称指定为P1,则应使用以下语句实现特殊功能寄存器的定义。sfr P1=0x90;定义特殊功能寄存器名称与地址的关系后,就可以通过所定义的寄存器名称操作对应的寄存器了。例如,若想将P1口的所有引脚(P1.0~P1.7)置为高电平,即可执行如下语句。P1=0xFF;当然,在定义特殊功能寄存器时也可以不使用通用的名称。例如,在某一应用中将P1口的8个引脚用于控制8个LED灯,为了便于与其功能相联系,可以将该特殊功能寄存器取名为LED,如以下代码所示。sfr LED=0x90;之后就可以使用定义的LED完成对8个LED灯的控制。 当然,为特殊功能寄存器指定名称时也不能太随意,例如将地址0x90的名称指定为P2。虽然程序本身不会有问题,但这将在阅读代码时引起混乱。因此在为特殊功能寄存器指定其本身通用名之外的自定义名字时,请使用表3.1以外的关键字。 3.6.2特殊功能的位 通过定义特殊功能寄存器可以一次性操作该寄存器包含的所有位,但是当只需要操作其中的某一个位且不希望改变其他位时就不太方便了(某些特殊功能寄存器在使用中经常需要只修改个别位)。对此,51核在特殊功能寄存器地址空间内分出了一些可位寻址的空间。一般情况下,如果某个特殊功能寄存器的二进制地址末3位为0(十六进制地址末位为0或8),则该寄存器内的8个位均可进行位寻址。如P1口的地址为0x90,则其可进行位寻址。而每一位的地址就是其所在寄存器地址加上该位在寄存器中的顺序号。如P1.0的位地址为0x90+0=0x90;P1.3的位地址为0x90+3=0x93。表3.2列出了51核中可位寻址的特殊功能寄存器地址及其位地址范围。表3.2SFR中位地址分布通用名位地址范围寄存器地址P00x80~0x870x80P10x90~0x970x90P20xA0~0xA70xA0P30xB0~0xB70xB0TCON0x88~0x8F0x88SCON0x98~0x9F0x98IE0xA8~0xAF0xA8IP0xB8~0xBF0xB8A(ACC)0xE0~0xE70xE0B0xF0~0xF70xF0PSW0xD0~0xD70xD0同特殊功能的寄存器一样,特殊功能的位(sbit)在访问前也需要进行定义,其定义方式有两种: 通过特殊功能寄存器名定义和通过位地址定义。 格式如下。sbit 特殊功能的位名=特殊功能寄存器名^顺序号;//方式一 sbit 特殊功能的位名=特殊功能的位地址//方式二如要操作P1.3引脚,并指定其特殊功能位名为P13(注意: 根据C语言语法要求,不能用“P1.3”作为自定义关键字名,因为关键字中出现了“.”),则可以通过如下语句完成定义。sbit P13=P1^3;//方式一该定义方式要求特殊功能的寄存器P1必须已经定义好了。如果不想先定义P1,则可以直接通过特殊功能位的地址完成定义,具有相同功能的代码如下。sbit P13=0x93;//方式二定义特殊功能的位后,就可以对其进行操作了。例如若将引脚P1.3变为高电平,则执行如下语句即可。P13=1;与特殊功能寄存器名称一样,特殊功能位的名称也可以自定义。同样,在使用自定义特殊功能位名时也不要和已有的通用特殊功能位混用,如EA、ET0等。例如,如果在应用中可以将P1.3用于控制LED灯D4的点亮与熄灭,则可以用D4作为特殊功能位的名称,如以下代码所示。sbit D4=0x93;这样的话,可以利用语句“D4=1;”或“D4=0;”实现对LED灯D4的控制。使用该方式编译代码既便于阅读,也便于代码的移植代码移植主要指将某一功能代码从一个硬件或软件平台中搬移到一个硬件或软件平台所需要做的工作,移植的前提条件是在不同平台中的运行效果一致。。 3.6.3寄存器相关头文件 C语言头文件的作用是定义一些常用的结构体、申明常用的函数等。在使用C51编程时,可以通过sfr和sbit的方式定义特殊功能寄存器和特殊功能的位,但前提条件是必须准确地记得每一个特殊功能寄存器和特殊功能位的地址,一旦地址出错,系统将无法正常运行,甚至出现很难察觉的错误。 通常情况下,芯片厂商会提供其生产的每种芯片的头文件,头文件中根据通用的寄存器名称定义芯片的特殊功能寄存器。使用芯片时,只需将厂商提供的头文件复制到工程的适当位置,并包含使用特殊功能寄存器的源文件即可。 同时,Keil环境还为一些常用的芯片提供了对应的头文件,因此可以在C文件开始处使用include语句将相关的h文件包含进来,这样既能减少编程人员定义特殊功能寄存器的工作量,也能减少出错的概率。由于将多个不同的名称指向同一地址是被允许的,因此为提高程序的可读性,编程人员只需使用sfr或sbit对个别特殊功能寄存器或位重新指定名称即可。 Keil针对的51核的头文件为REG51.H,在51基础上升级的52核对应的头文件为REG52.H。STC公司为STC89C52RC芯片定制的头文件为STC89C5xRC.h。如果只用到芯片的基本功能,则一般在C文件中包含REG51.H或REG52.H即可。当需要用到芯片的扩展功能,如第三定时器或片上的EEPROM时,就必须使用专门针对芯片定制的头文件。 本章小结 本章从哈佛结构与冯·诺依曼结构的区别入手,讲述了51核单片机的存储系统使用的组织结构和C语言访问相关存储器的方法,主要包括内部数据存储器、外部数据存储器、程序存储器、特殊功能的寄存器以及对应的变量存储位置关键字data、idata、pdata、xdata、bit、bdata、code、sfr、sbit等。最后简单介绍了寄存器定义的相关头文件。希望读者在后期编码中可以根据需要灵活使用这些关键字,并根据需要包含相应的头文件。 练习 3.1简述哈佛结构与冯·诺依曼结构的关系。 3.2指出PC中的变量定义与51单片机中的变量定义的异同。 3.3关键字data、idata、pdata、xdata、bit、bdata、code、sfr、sbit的功能分别是什么?如何使用? 3.4定义一个查询列表table[],用于存放十六进制符号,如table[0]='0',table[10]='A'(请注意数据类型和存储类型)。