什么是体系结构?

 

所谓“体系结构”,也可以称为“系统结构”,是指程序员在为特定处理器编制程序时所“看到”从而可以在程序中使用的资源及其相互间的关系。

体系结构最为重要的就是处理器所提供的指令系统和寄存器组。指令系统分为CISC(Complex Instruction Set Computer,复杂指令集计算机)和RISC(Reduced Instruction Set Computer,精简指令集计算机)。其中,嵌入式系统中的CPU往往是RISC结构的,至于原因,在后面总结完CISC和RISC之后会给出。ARM核就是RISC结构,由于其在嵌入式系统占的比重比较大,所以ARM几乎成为RISC的代名词了。寄存器组与采用的指令系统是密切相关的,从这一点上考虑,体系结构中最为重要的应该就是指令系统了。

在体系结构中,还有存储器结构。现在有两种:冯·诺依曼结构和哈佛结构。传统的计算机采用冯·诺依曼结构,也称为普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。主要特点是:程序和数据共用一个存储空间;程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置;采用单一的地址及数据总线;程序指令和数据的宽度相同。这样,处理器在执行指令时,必须从存储器中取出指令解码,再取操作数执行运算,即使单条指令也要耗费几个甚至几十个周期,那么,在高速运算的时候,在传输通道上会出现瓶颈效应。目前使用冯·诺依曼结构的MPU和MCU有很多,如Intel 8086、ARM公司的ARM7、MIPS公司的MIPS处理器等。Harvard结构是一种将程序指令存储和数据存储分开的存储器结构,Harvard结构是一种并行体系结构,主要特点是:程序和数据存储在不同的存储空间中,即程序存储器和数据存储器是两个相互独立的存储器,每个存储器独立编址、独立访问。与两个存储器相对应的是系统中的4套总线:程序的数据总线和地址总线,数据的数据总线和地址总线。这种分离的程序总线和数据总线可允许在一个机器周期内同时获取指令字和操作数,从而提高了执行速度,使数据的吞吐量提高了1倍。又由于程序和数据存储器在两个分开的物理空间中,因而取指和执行能完全重叠。目前使用Harvard结构的如所有的DSP处理器、Motolora公司的MC68系列、Zilog公司的Z8系列、ATMEL公司的AVR系列、ARM公司的ARM9等。

体系结构与实现的关系是什么?

一个处理器的体系结构就是它的逻辑抽象,至于这个抽象的处理器具体如何实现,则称为(硬件)组成,或者就成为“实现”。同一个体系结构可以有不同的实现。计算机专业的基础课程《计算机体系结构与组成原理》就是从理论研究逻辑抽象及其实现方法。这两个方面是不同层次上的概念,原则上是相互独立的。但是实际上是相互影响的。这里有一个比较重要的概念就是微程序。它对指令系统的设计有比较密切的关系和影响。

CISC与微程序

 

微程序是指令系统实现的一种方法,另外,相同的指令系统也可以通过“硬链接”来实现。不过,CISC体系结构的处理器一般采用微程序来实现复杂指令集,当然也可以采用其他实现方法,而RISC则不需要。由于微程序在CISC中比较重要,下面CISC均指微程序实现的CISC。

为了支持复杂指令集,CISC通常采用一个复杂的数据通路和一个微程序控制器。微程序控制器由一个微程序存储器、一个微程序计数器和地址选择逻辑构成。基本工作原理如下:在微程序存储器中的每个字表示一个控制字,并且包含了一个时钟周期内所有数据通路控制信号的值。这就意味着控制字中的每一位表示一个数据通路控制线的值。而每个处理器指令是由一系列的控制字组成。当从内存中取出这样的一条指令时,首先把它放在指令寄存器中,然后地址选择逻辑再根据它来确定微程序存储器中相应的控制字顺序起始位置。当把该起始位置放到微程序计数器中后,就从微程序存储器中找到相应的控制字,并利用它在数据通路中把数据从一个寄存器传送到另一个寄存器。由于微程序计数器中的地址并发递增来指向下一个控制字,因此对于序列中的每个控制器都会重复上述步骤。最后,执行完最后一个控制字,该条指令执行结束,就从内存中取出一条新的指令,继续如上执行。

从宏观上理解,采用微程序技术的存储器中,实际上有两种不同层次的指令。一种是面向程序员(软件)的、高层的“指令”,另一种是面向硬件实现的、低层的“微指令”。这样,由电子元件和线路完成一套基本的功能,就可以通过微程序对这些功能进行不同的组合和编排,以实现高层指令所要求的复杂功能。而高层的“指令系统”,则相当于由函数库向用户提供的API。所以,微程序的采用实际上是对软件设计中“子程序调用”这个概念的推广,将其适用范围向下推进了一个层次,从而对所要求的操作进一步化整为零,分而治之(Divide and Conquer),使指令系统的实现得到简化。

从另一个角度来看,微程序的采用鼓励助长了在指令系统中采用复杂指令的倾向。人们曾经普遍认为:既然微程序的采用使复杂指令的实现成为可能和并行,处理器就应该为一些典型的复杂操作和典型的高级语言成分提供相应的指令,这样执行效率应该比较高。结果自然使人们得到一个印象,即指令系统的设计应该向高级语言看齐和靠拢,其“最高境界”就是与某种高级语言相同。在这种倾向下,各种处理器的指令系统越来越大,一些指令也越来越复杂。但是,随着技术的发展,人们对此产生怀疑,这是有着坚实的实践基础的。主要有以下几个方面:

1、为了提高运算速度,在微处理器中采用了“预取指令”等“流水线操作技术”。流水线技术是提高总体运算速度的有效手段,但是对CISC来说,控制字的数量和时钟周期的数目对于每一条指令都可以是不同的,所以很难实现指令流水操作。

2、即使不考虑流水线,在实现中也往往因为要顾及一些特殊的复杂指令的实现,而只好让简单指令作出一些牺牲。问题在于这样做从总体和全局看是否划算,这就取决于指令的使用频度。然而调查研究表明,20%的比较简单的指令被反复使用,使用量约占整个程序的80%;而80%左右的指令则很少使用,使用量仅占20%左右。这就是指令的2/8规律。

3、微处理器的集成规模是受半导体技术以及生产成本限制的,而微程序存储器又是微处理器内部“占地”面积最大的部分,复杂指令又是其中的“大户”。如果能消减微程序存储器的大小,或者在同样大小的芯片中集成其他的功能部件,或者可以减少芯片的尺寸和耗电,或者二者兼得,对嵌入式系统有着特殊的意义。因为用于嵌入式系统的处理器/控制器,常常要求将这些外围模块集成在同一芯片中。而要消减微程序存储器的大小,从指令系统中砍掉复杂指令当然是首选,最好是不用。

4、VLSI制造工艺要求CPU控制逻辑的规整性

进入20世纪80年代后,VLSI技术的发展非常迅速,往往每3到4年集成度就提高一个数量级。VLSI工艺要求规整性,而CISC处理器中,为了实现大量复杂的指令,控制逻辑极不规整,给VLSI工艺造成很大困难。

5、现代的微处理器都带有较大的高速缓存,运行中访问内存时命中高速缓存的概率可以达到较高的程度。命中时,高速缓存的访问速度与微程序存储器相似。这样使得许多简单指令没有必要用微程序来实现,而复杂的指令,用微程序来实现和用简单指令组成的子程序实现已经没有多大区别。

基于以上原因,RISC的概念和技术应运而生了。

RISC的特点

 

RISC是在继承CISC的成功技术并且克服其缺点的基础上产生并发展起来的,大部分RISC具有以下特点。

1、RISC体系结构中的指令系统都比较小,即不同指令的数量较少,并且只提供简单指令。所谓“精简指令集”,一方面是说指令集的大小,另一方面是说每条指令的复杂程度,两个条件缺一不可。

2、每条运算指令的操作数都必须先预存于寄存器中。也就是说,一般运算指令在执行过程中是不访问内存的,所有的操作数不是立即数就是来自某个寄存器,而执行结果也只能存入寄存器。为此,需要配备专门的访内指令。主要是两条:LOAD和STORE,都是以微处理器为中心进行的命名。Load就是从内存load数据到某寄存器,Store就是store寄存器中的数据到内存中。这样所有指令的执行长度就变得整齐划一,而寻址方式的实现也就变得既简单又整齐。

3、指令的格式也整齐划一,典型的RISC体系结构中,每条指令长度为32位,包括一个操作码位段和三个操作数位段。这样,由于每条指令都是32位,其执行长度固定,采用流水线以后就可以基本上达到每个时钟脉冲执行一条指令的目标,因此,“每个时钟脉冲一条指令”成了RISC拥护者的旗号和目标。

4、由于一般指令的操作数都必须事先存放在寄存器中,计算过程中的中间结果自然也就不应该存放到内存中,也应该存放到寄存器中。这样就需要较多的通用寄存器,而传统的CISC寄存器的数量一般不超过16个,不适合RISC体系结构的要求。所以RISC体系结构一般都有比较多的寄存器组,通常是32个。

5、RISC的子程序调用过程,或“子程序链接”,与CISC的不同。在CISC体系结构中,call指令将返回地址存入内存中的堆栈,因而需要访问内存。而RISC的原则之一就是尽量少访问内存,所以返回地址是放在寄存器中而不是堆栈中的。这样,一般专门拿一个寄存器作为“(子程序)链接寄存器”(R14,映射为LR)。这样如果调用的是底层的“叶”子程序,便不需要为子程序链接访问内存,而如果是中层的子程序,则可按照一定的调用约定将此寄存器的内容溅出到堆栈中。此外,调用参数的传递也不通过堆栈,而是通过寄存器。对于有大量子程序调用,特别是有大量很小的“叶”子程序调用的软件,这样的链接方法可以节省很多访问内存的开销,有利于提高效率。

6、中断的过程也可以看成是特殊的子程序链接。对于RISC体系结构,对中断是的寄存器作出约定,那些寄存器的内容是必须保存,因而在中断处理程序中可以自由使用。如果中断处理程序需要更多的寄存器,那么就先行一步溅出这些寄存器的内容。

与CISC架构相比较,尽管RISC架构有诸多的优点,但是决不能认为可以取代CISC架构。事实上,RISC和CISC架构各有优势,而且界限并不那么明显。现代的CPU往往采用CISC的外围,内部加入了RISC的特性,如超长指令集CPU就是融合了RISC和CISC的优势,成为未来CPU的发展方向之一。

嵌入式系统的处理器为什么大都是RISC结构的?

 

1、在同样的集成规模下,RISC的CPU核在芯片上占用面积要小得多。这样就可以将一些如外设接口等等的外围模块集成在同一块芯片上。

2、有利于减少芯片的尺寸和功耗(有利于散热)。

3、结构简单,开发成本低。

4、对于实时应用,RISC指令具有均匀划一并且较小的执行长度,因此有利于中断延迟的可预测性,并且有利于缩短中断延迟。

ARM的四层含义

 

1、ARM是一种RISC MPU/MCU的体系结构,如同x86架构是一种CISC体系结构一样。另外,还有MIPS架构、PowerPC架构等等。

2、ARM是Advanced RISC Machine Limited公司的简称。

3、ARM是Advanced RISC Machine Limited公司的产品,该产品以IP Core(Intellectual Property Core,知识产权核)的形式提供的。

4、ARM还用以泛指许多半导体厂商买了这种设计后生产出来的“ARM处理器”系列的芯片及其衍生产品。

半导体厂商固然可以光购买ARM公司的设计而直接生产ARM处理器芯片,但是更好的方法是以ARM处理器为核心,在同一块芯片上配上自己开发的外围模块,形成面向特定应用和市场的专用芯片,甚至“片上系统(System on a Chip,SoC)”。这样,作为专用处理器/控制器芯片的生产商既可以减少开发中的风险,又可以大大缩短开发周期,降低成本。所以,“ARM处理器”一般是作为“内核”存在于一些专用处理器/控制器的内部,因而又常常叫做“ARM核”。特别地,如果一个处理器核不带浮点运算功能,有时候就对此特别加以强调,称之为“整形核”。

ARM体系结构版本及其变种的含义

 

ARM体系结构的版本的字符串由下面几个部分组成:

1、字符串ARMv

2、ARM指令集版本号

3、表示变种的字符。由于在ARM体系版本4以后,M变种成为系统的标准功能,字符M通常不需要列出。

4、使用字符x表示排除某种功能。

例如:ARMv4T表示ARM版本4,支持Thumb。

变种字母含义:

1、T――支持Thumb

2、M――Multiplier,支持长乘法指令

3、E――Extended,支持增强型DSP指令

4、J――Java加速器Jazelle

5、SIMD――支持ARM媒体功能扩展

还有几个字母含义如下:

1、D――Debug,提供调试支持

2、I――芯片上带有内置的ICE,从而支持程序内的断点和数据空间的“观察点”设置。

ARM的存储器结构

 

以ARM体系结构的第4版为界限,之前采用的是冯·诺依曼结构,此版本及其之后的版本都是采用Harvard结构。不过这种结构是一种Modified Harvard,就是将CPU访问内存的通道分成指令和数据两个相互独立的通道。CPU虽然可以同时从“内存”取指令和读写数据,但实际上只是分别访问两个高速缓存。而这两个高速缓存最后还是通过共同的总线访问内存。

 

ARM指令系统

系统的指令系统介绍这里就不罗列了,没有多大意义。这里只是总结一些小的知识点和经验,便于对ARM指令系统有更为深入的理解。

 

1、ARM处于用户态模式时,可见的通用寄存器是16个,即R0-R15。外加一个CPSR(Current Program Status Register,当前程序状态寄存器),总共是17个。其中3个有特殊用途:R15――程序计数器PC,R14――程序链接寄存器LR,R13――堆栈指针SP。

 

2、ARM有7中运行状态:usr、fiq、irq、sve、abt、und、sys。除用户状态外的6中均为系统状态(特权模式)。当CPU从用户状态进入系统状态时,或者发生系统状态间的切换时,都需要将CPSR的内容保存起来,以备将来恢复原来的运行状态,所以每个系统状态都有个“保存程序状态寄存器(Saved Program Status Register)”SPSR。CPSR和SPSR都是32位的,实际上只用了其中的一部分。

 

3、ARM体系结构中,每一条指令都可以条件执行。例如:

 

cmp r0, #0

addeq r0, r2, r5

addne r0, r0, r0, lsl #1

 

等价于:

 

if (r0 = = 0) {

r0 = r2 + r5;

}

else {

r0 = r0 * 3;

}

 

可见,利用这种独特的条件执行可以得到很简洁的汇编代码,要不然就得在汇编代码中插入条件转移指令。不过,当if或else下面的条件执行部分较大时,插入条件转移指令更合适。有人做过分析,当一个条件执行部分的大小超过三条指令时,就还是以插入条件转移指令为好;反之则以条件执行指令为好。

 

在条件转移的设计中,同时要注意程序的可读性。虽然程序都能得到正确的结果,但是安排合理的程序更具有可读性,也更加易于维护。宁肯牺牲一部分时间来完善,也不要在事后代码维护的时候才意识到。

 

4、关于堆栈寻址

堆栈是一块连续的内存,也可以说是存储区,不过因为作为特定的数据结构,它对数据存储顺序是有要求的,即先进后出(或者说是后进先出)。堆栈寻址时,使用SP指向一块存储区域,指针所指向的单元就是堆栈的栈顶。存储器堆栈可以分为两种:

 

一种是向上生长,就是向着高地址方向生长,称为递增堆栈。

一种是向下生长,就是向着低地址方向生长,称为递减堆栈。

 

另外,堆栈指针指向最后压入的堆栈的有效数据项,称为满堆栈;堆栈指针指向下一个要放入的空位置,称为空堆栈。这样,就有四种组合:满递增、空递增、满递减、空递减。

(D代表Descending,A代表Ascending,F代表Full,E代表Empty)

 

写程序通过分析结果来理解堆栈寻址是一种最好的方法,形象直观。现在根据汇编实验分析结果对上述堆栈寻址作出总结。

 

入栈规律:

(1)满堆栈操作先调整SP,然后存入数据。

(2)空堆栈操作先存入数据,然后调整SP。

(3)递增堆栈调整SP时,执行SP=SP+4

(4)递减堆栈调整SP时,执行SP=SP-4

 

出栈规律正好与入栈相反,也就是入栈的逆操作。

(1)空堆栈操作先调整SP,然后存入数据。

(2)满堆栈操作先存入数据,然后调整SP。

(3)递减堆栈调整SP时,执行SP=SP+4

(4)递增堆栈调整SP时,执行SP=SP-4

 

明确了这四个规律,就很容易分析各种堆栈寻址方式对应的堆栈分布情况了。

stmfd sp!, {r4-r11}

 

假设初始SP为0x0400,那么执行完毕后内存0x03E0-0x03FF保存寄存器R4-R11的内容。

stmed sp!, {r4-r11}

 

假设初始SP为0x0400,那么执行完毕后内存0x03E4-0x0403保存寄存器R4-R11的内容。

实际应用中,只选用一种方式使用就可以了。最常用最典型的就是后缀为“FD”时的结构,这是人们熟悉的堆栈结构。

stmfd sp!, {r4-r11, lr} /*入栈*/

ldmfd sp!, {r4-r11, lr} /*出栈*/

 

5、关于转移指令

ARM的转移指令是独特的,最简单最基本的转移指令是b,表示“branch”,例如:

b reset

 

这里“标签”是一段程序的入口,一般是一个函数,或者是汇编程序的一个标签。在基本的操作码b后面加上条件后缀EQ、NE、GT、LT等等,就成了条件转移指令。由于指令的长度只有32位,编码在指令中的就只能是一个相对于PC当前值的位移,而不可能是个32位的绝对地址。所以,这是一条“相对转移”指令。如果要做绝对转移,那么就得采用别的手段,例如,可以把转移的目标地址放到寄存器R4中,那么将它传递到PC中就可以完成转移。

mov pc, r4

 

这就变成绝对转移了,但是,b指令不允许以寄存器的内容作为目标地址。

 

事实上,子程序调用的返回指令mov pc,lr也是绝对转移。

 

ARM处理器中有一条执行指令的流水线,不管是相对转移还是绝对转移,当CPU执行到引起转移时,即引起pc突变的指令时,其后面的几条指令已经被取入了流水线,甚至已经对指令解码了。程序计数器pc的突变迫使流水线舍弃这些已经在流水线中的指令,使流水线短暂断流,然后从新的地址取指令,并又逐步“灌满”流水线。在这个过程中,CPU可能会有一个短暂(例如几个时钟周期)的“无所事事”的空隙。在典型的RISC结构中,一般都把转移前的最后一条指令改放到转移指令后面,或者把转移目标处的第一条指令搬过来放到转移指令后面,称为指令调度。这样,把本来会浪费掉的几个时钟周期利用起来,效率当然提高了,但是对于代码的阅读、理解以及调试,都有不利的影响。所以常常受到来自CISC阵营的批评和攻击。ARM体系结构的设计者并没有紧跟RISC的潮流,仍然采用传统的方法,宁可浪费一点效率也要保证程序的简洁,所以不采用指令调度,而只是丢弃已经进入流水线的指令。毕竟,大多数情况下,因此而降低的效率只占很小的比例。

 

相对转移指令b有个变形bl,意为“转移并连接(Branch and Link)”,专门用于子程序调用。执行这条指令时CPU将pc的当前值(指向下一条指令)保存在寄存器lr中,即R14中,同时转向目标地址。这是,要从子程序返回时,只要把lr的内容写入pc就行了。例如:

 

bl uHALir_ReadMode

uHALir_ReadMode:

mov pc, lr

 

这里考虑为什么不像传统的做法那样自动把返回地址保存在堆栈中?原因前面提到过了,RISC的设计原则之一就是尽量少访问内存,而改用寄存器代替,这样可以有效的提高效率。堆栈是在内存中,把返回的地址放到堆栈意味着访问内存,而寄存器间的访问比访问内存操作要快得多。这样,先通过较快的方法进入到子程序,如果还需要进一步调用更深层的子程序,则可以到那时再把lr的内容保存(“溅出”)堆栈中,如果不需要访问深层子程序,则可以省去为返回地址而读/写内存的操作。程序在运行时会形成一颗“子程序调用树”。统计表明,对叶节点,即底层子程序的调用常常占很高的比例,这是因为对底层子程序的调用往往是在循环中进行的,而且,底层子程序本身往往是很小的,为调用本身很小的底层子程序访问内存两次,所占的比例就不小了。