第1章 OpenRISC介绍
1.1 开源软核处理器
随着FPGA技术的发展,以及EDA软件工具的进步,如今的FPGA应用范围越来越广,甚至出现了SOPC(System-On-a-Programmable-Chip,可编程片上系统)的概念,用可编程逻辑技术把整个系统放到一块可编程逻辑芯片上,其中可以包含处理器、DSP、RAM、ROM、总线控制器、UART控制器、以太网控制器、USB控制器、JTAG接口和FPGA编程接口等各种功能模块,且每个功能模块都可以设计成为IP(Intellectual Property)核。IP核指的是完成某种功能的设计模块。借助于IP核及一些标准的总线接口,硬件设计开发者可以像搭积木那样搭建硬件平台。
IP核有3 种:软核、固核和硬核。软核指的是在寄存器级或者门级对电路功能用HDL(Hardware Description Language)进行描述,表现为VHDL或Verilog HDL代码。软核与生产工艺无关,不涉及物理实现,为后续设计留有很大的空间,增大了IP的灵活性和适应性。用户可以对软核的功能加以裁剪以符合特定的应用,也可以对软核的参数进行设置,包括总线宽度、存储器容量、使能或禁止某些功能块等。固核是完成了综合的功能块,通常以网表的形式提交给用户使用。硬核指的是以版图的形式实现的设计模块,它基于一定的设计工艺,不同的用户可以根据自己的需要选用特定生产工艺下的硬核。软核使用灵活,但其可预测性差,延时不一定能达到要求;硬核可靠性高,能确保性能,如速度、功耗等,可以很快投入使用。
本书关注的并不是如何使用各种IP核搭建SOPC系统,而是由于采用了可编程逻辑器件设计各种硬件模块,由于使用了硬件编程语言,使任何一个人都可以通过阅读源代码掌握各种功能模块的内部结构,比如可以掌握处理器内部的结构、原理,同样的,不需要高昂的代价、漫长的周期就可以在FPGA上设计自己的处理器,就像开源的操作系统将操作系统拉下神坛,开源处理器也揭开了处理器的神秘面纱,使得人人皆可谈论,正所谓:“旧时王谢堂前燕,飞入寻常百姓家。”
使用HDL描述的处理器称之为软核处理器。各大FPGA厂商都有自己的软核处理器IP,Altera公司的NiosII,XILINX公司的MicroBlaze,但是这些IP核都不开放源代码。在此之外还有许多开源的软核处理器。本节列举部分开源软核处理器,内容如下。
1.OR1200
OR1200来自于OpenRISC项目,OpenRISC项目的目标是建立一个免费、开源的计算平台。这个计算平台提供一个带DSP功能的RISC处理器架构,提供一些基于该架构的RISC处理器,以及免费、开源的开发工具、库、操作系统和应用程序。OpenRISC包括OpenRISC 1000和OpenRISC 2000两个子项目。OR1200是OpenRISC 1000项目的一个主要实现,最初发布于2001年,一直在持续改进,目前的稳定版本是Rel3,采用Verilog HDL编写源代码。OR1200是一个32位的RISC处理器,采用Harvard结构(即分开的指令与数据缓存)、五级整数流水线(本书分析认为只有三级整数流水线),并且支持MMU,具备基本DSP功能。
2.LEON系列
LEON系列至今已发布到了LEON4,这是一种SPARC V8架构的处理器。最初的LEON1与LEON2由欧洲航天局发布,LEON3由Gaisler Research公司设计发布。2008年Aeroflex收购了Gaisler Research公司,并于2010年1月发布了LEON4,不过LEON4至今还没有公布源代码。LEON系列使用VHDL编写代码,其LEON2、LEON3系列都提供了一个容错版(Fault Tolerance),分别是LEON2-FT、LEON3-FT,这主要是因为LEON系列原计划是使用在航天器上,在航天器上的电子器件面临一个特殊问题,那就是太空中的各种高能粒子具有很高的动能,当这些粒子穿过航天器的电子器件时可能会影响半导体电路的逻辑状态,甚至对半导体材料造成永久损害。单个高能粒子对电子器件功能产生的影响称之为单粒子效应。其中,导致存储内容在0和1之间发生变化的现象,称之为SEU(Single Event Upset:单粒子翻转)。在容错版中通过一些逻辑结构纠正SEU带来的影响。LEON2采用五级流水线,LEON3采用七级流水线,同时增加了对多处理器的支持。LEON4又引入了静态分支预测、二级缓存。
3.OpenSparc系列
OpenSparc开始于2006年3月,Sun发布了OpenSparc T1的源代码,这是一个64bit的处理器,采用了SPARC V9架构,含有8个处理器核,之间通过crossbar通信,每个核支持4个硬件线程,所以共支持32个线程。此外,每个处理器核支持8KB数据一级缓存、16KB指令一级缓存,8个处理器核共享3MB大小的二级缓存,含有4个DDR-II SDRAM控制器。2007年11月Sun发布了OpenSparc T2,采用的还是8核,但每个核支持8个线程,所以共支持64个线程,采用的是8级整数流水线,12级浮点流水线。
4.S1 Core
S1 Core是Simple RISC公司发布的,该项目基于OpenSparc,所以它也是采用SPARC V9的架构,其目的是简化OpenSparc,后者下载到FPGA上会占用很大的资源,简化的地方主要在于:只有1个64位的处理器核,而不是8个。虽然做了简化,但根据1-CORE Technologies提供的数据显示,在XILINX的Virtex-5上S1 Core需要使用37000~60000个LUTs,还是比较占用资源。
5.LatticeMicro32
LatticeMicro32是Lattice公司发布的32位RISC软核处理器,该处理器虽然由Lattice发布,但也可以使用在其他厂商的FPGA上,如Altera的FPGA,这就比NiosII有优势,NiosII只能使用在Altera的FPGA上。LatticeMicro32采用Harvard结构、32个通用寄存器、32个外部中断、可选的指令与数据缓存和Wishbone总线接口。提供了3种配置模式使得用户可以在占用资源与速度之间取舍:(1)基本模式:没有乘法器、没有缓存、多周期转换器;(2)标准模式:有乘法器、8KB指令缓存、没有数据缓存、流水线转换器;(3)完全模式:有乘法器、8KB指令缓存、8KB数据缓存、流水线转换器。
软核处理器还有其他很多种,在此不一一介绍,本书将对OR1200进行深入的剖析,至于为何选择OR1200作为剖析目标,笔者在前言中已明确,即一通百通、融会贯通、取长补短、创新提高。
在1-CORE Technologies公司的网站上对主要的软核处理器进行了一个比较,如表1.1所示。值得注意的是表1.1中所列出的软核处理器都是RISC处理器。RISC是精简指令集计算机(Reduced Instruction Set Computer),与之相对的是CISC,即复杂指令集计算机(Complex Instruction Set Computer)。在计算机的发展早期,人们使用汇编语言编程,偏好强大好用的指令集,处理器的设计人员于是将指令集设计得更强大、更灵活,并且那个时期的存储器昂贵且速度慢,因此指令使用了变长编码,以节约存储空间,由于一条指令就能完成很多的功能,对内存的访问也减少了,这样也减少了缓慢的存储器访问对程序性能的影响。典型的CISC指令集就是Intel的x86指令集。上世纪70年代中期,人们发现在CISC指令集的各种指令中,其使用频率相差悬殊,大约有20%的指令会被反复使用,占整个程序代码的80%。而余下的80%的指令却不经常使用,在程序设计中只占20%,显然,这种结构是不太合理的。于是人们提出将指令集和处理器进行重新设计,减少那些使用不多的指令,只保留常用的简单指令,这样处理器就不需要浪费太多的晶体管去做那些很复杂又很少使用的功能,于是产生了RISC。1979年美国加州大学伯克利分校提出了RISC的概念,RISC并不只是简单地减少指令,更主要的目的是研究如何使计算机的结构更加简单合理以提高运算速度。其特点是指令长度固定、指令格式种类少、寻址方式种类少、大量使用寄存器。由于在RISC中使用的指令大多数是简单指令且都能在一个时钟周期内完成,因而处理器的频率得以大幅提升,同时易于设计流水线。RISC是计算机历史上的一个里程碑,以致有人把RISC定义为:1985年之后发布的所有处理器。
表1.1 主要的软核处理器比较
1.2 OpenRISC 1000架构
前文提及OpenRISC项目,其包括OpenRISC 1000、OpenRISC 2000两个子项目。OpenRISC 1000项目产生了OpenRISC 1000架构,OR1200就是符合OpenRISC 1000架构的处理器。本小节将简单介绍OpenRISC 1000架构。
图1.1 OpenRISC项目
OpenRISC 1000架构是一个开放的架构,其目标是使用在中、高性能的网络和嵌入式计算环境,因此强调通过多种措施改善性能,同时力求简单、低功耗。OpenRISC 1000架构的特点如下。
● 免费、开源的架构
● 32位或64位线性地址空间
● 简单、长度统一的指令,有基本指令集及扩展指令
基本指令集(ORBIS32/64,OpenRISC Basic Instruction Set):指令长32位,数据宽度为32或64位
向量/DSP扩展指令(ORVDX64,OpenRISC Vector/DSP eXtension):指令长32位,数据宽度为8、16、32或64位
浮点扩展指令(ORFPX32/64,OpenRISC Floating-Point eXtension):指令长32位,数据宽度为32或64位
● 简单的内存寻址方式
● 大多数指令中有两个源寄存器(或者一个寄存器、一个常数),一个目的寄存器
● 32个通用寄存器,也可以有多组影子寄存器
● 支持分支延迟
● 可以采用Harvard结构,即分开的指令、数据缓存,分开的指令、数据MMU,也可以不分开,即采用Stanford结构
● 部分功能既可以通过硬件实现也可以使用软件辅助实现
● 对异常(Exception)情况进行了简单区分
● 支持快速上下文切换
OR1200处理器中实现了OpenRISC 1000架构的部分,而不是全部,作为一本剖析OR1200的书,笔者本着够用就行的原则不对OpenRISC 1000着过多的笔墨,只对OR1200做详细介绍。
1.3 OR1200——OpenRISC 1000架构的一个实现
OR1200是一个采用Harvard结构的32位RISC处理器,其基于OpenRISC 1000架构,具有五级流水线(需要探讨)、支持MMU、Cache及基本的DSP功能。OR1200总体结构如图1.2所示。
图1.2 OR1200总体结构
OR1200具有32个通用寄存器,没有影子寄存器,所以不支持上下文快速切换。默认配置中有8KB的直接映射数据缓存、8KB直接映射指令缓存,缓存的块大小为16字节。默认配置中数据MMU含有64项的直接映射数据快表(DTLB:Data Translation Lookaside Buffers),指令MMU含有64项的直接映射指令快表(ITLB:Instruction Translation Lookaside Buffers)。OR1200采用Wishbone B3版本的总线接口,支持电源管理、计时器、调试和外部中断。
需要注意的是,虽然在OR1200的代码中包括FPU,但这里的FPU实际并不是OR1200的一部分,而是一个独立的项目,所以本书不对FPU的实现代码进行分析。
下面简单介绍OR1200的寻址模式、位和字节次序、寄存器集、指令集、异常模型,图1.2中的MMU、Cache、可编程中断控制(PIC)、计时器单元(TT)、电源管理(PM)、Wishbone总线单元将在本书的后续章节分别独立分析。
1.3.1 寻址模式
OR1200的寻址模式有寄存器寻址、立即数寻址、寄存器相对寻址和PC相对寻址4种。其中寄存器相对寻址、PC相对寻址内容如下。
(1)寄存器相对寻址
这种寻址模式主要是加载/存储指令使用,它将一个16 位的立即数做符号扩展,然后与指定通用寄存器的值相加,从而得到有效地址,如图1.3所示。
图1.3 寄存器相对寻址
(2)PC相对寻址
这种寻址模式主要是转移指令使用,在转移指令中有一个26位的立即数,将其符号扩展,然后与程序计数寄存器(PC:Program Counter)的值相加,从而得到有效地址,如图1.4所示。
图1.4 PC相对寻址
1.3.2 位和字节次序
数据在存储器中是按照字节存放的,OR1200也是按照字节访问存储器中的指令或数据,但是如果需要读出一个字,也就是4 字节,比如读出的是mem[n]、mem[n+1]、mem[n+2]和mem[n+3]这4 字节,那最终交给处理器的是{mem[n],mem[n+1],mem[n+2],mem[n+3]},还是{mem[n+3],mem[n+2],mem[n+1],mem[n]},就有所区分了,前者称为大端模式(Big-Endian),也称为MSB(Most Significant Byte),后者称之为小端模式(Little-Endian),也称之为LSB(Least Significant Byte)。在大端模式下,数据的高位保存在存储器的低地址中,而数据的低位保存在存储器的高地址中,图1.5给出0x12345678在两种模式下的存储情况。OR1200采用的是大端模式(Big-Endian)。
图1.5 大、小端模式下存储0x12345678的区别
1.3.3 寄存器集
RISC有一个特点:大量使用寄存器。这是因为寄存器的存取可以在一个时钟周期内完成,同时也简化了寻址方式。OR1200的指令中除加载/存储指令外,都是使用寄存器或立即数作为操作数的。OR1200中的寄存器分为两类,通用寄存器(GPR:General Purpose Register)和特殊寄存器(SPR:Special Purpose Register)。
通用寄存器有32个,分别以r0、r1……r31表示,都是32位,其中r0一般用做常量0。
特殊寄存器被分为11组,如表1.2所示。
表1.2 OR1200中的特殊寄存器组
可见大部分特殊寄存器都是与图1.2中特定的硬件模块有关,所以在后续章节分析相关模块的时候再对该模块中相应的特殊寄存器进行介绍。这里只需要知道一点,在第0组寄存器中有一个SR(Supervisor Register)寄存器,SR中有一个SM位,该位决定了OR1200所处的两种模式:当SR[SM]为0时,OR1200处于用户模式;当SR[SM]为1时,OR1200处于特权模式。在用户模式下OR1200可以访问的特殊寄存器受限,在特权模式下可以访问所有的特殊寄存器。
1.3.4 指令集
OpenRISC 1000架构指令集中的指令分为3种:基本指令集、向量/DSP扩展指令和浮点扩展指令,其指令助记符分别以“l.”、“lv.”和“lf.”开始。OR1200主要实现了OpenRISC 1000 架构中基本指令集(ORBIS32/64)中的一部分指令,而不是全部。共有76 条指令,本书将其分为6 类,分别是:数据处理类指令(含算术、逻辑、移位、比较、数据传送等)、特殊寄存器访问类指令、转移类指令、异常处理类指令、乘法除法类指令和加载存储类指令。
1.数据处理类指令
数据处理类指令包含算术、逻辑、移位、比较、数据传送等指令,算术指令,如:加法指令l.add、减法指令l.sub;逻辑指令,如:寄存器相与指令l.and、寄存器相或指令l.or;移位指令,如:带立即数循环右移指令l.rori、逻辑左移指令l.sll;比较指令,如:比较寄存器是否相等指令l.sfeq;数据传送指令,如:l.movhi,表示将16位立即数左移16位,然后存储到目标寄存器。本书将在第4章对数据处理类指令进行详细介绍,同时分析其执行过程。
2.特殊寄存器访问类指令
特殊寄存器访问类指令有两个:特殊寄存器读指令l.mfspr、特殊寄存器写指令l.mtspr。本书将在第5章对特殊寄存器访问类指令进行详细介绍,同时分析其执行过程。
3.转移类指令
转移类指令包括条件转移、无条件转移和绝对转移、相对转移等指令。本书将在第6章对转移类指令进行详细介绍,同时分析其执行过程。
4.异常处理类指令
异常处理类指令包括系统调用l.sys、自陷l.trap和异常返回l.rfe等指令。本书将在第7章对异常处理类指令进行详细介绍,同时分析其执行过程。
5.乘法、除法类指令
乘法、除法类指令包括乘法指令、乘累加指令和除法指令等。本书将在第8章对乘法、除法类指令进行详细介绍,同时分析其执行过程。
6.加载存储类指令
加载存储类指令包括加载指令、存储指令。加载指令如:加载一个字到寄存器的指令l.lwz;存储指令如:存储一个字到指定地址的指令l.sw。本书将在第9 章对加载存储类指令进行详细介绍,同时分析其执行过程。
1.3.5 异常模型
在指令的执行过程中由于外部信号、错误或指令本身的一些原因会导致处理器进入异常处理过程。异常处理过程偏离了原程序的执行流程,OR1200将跳转到一个指定的地址,从那里读取指令,开始异常处理,这个指定的地址称为异常处理例程入口地址。OR1200定义的异常类型及其处理例程入口地址如表1.3所示。异常处理过程涉及几个特殊寄存器的使用,以及延迟槽的知识。本书将在第7章详细介绍OR1200的异常处理过程。
表1.3 异常类型及对应处理例程的入口地址
1.4 OR1200代码组织
使用SVN从地址http://opencores.org/ocsvn/openrisc/openrisc可以得到最新的OR1200代码。本书以OR1200的Rel3这个版本为例进行分析,在Rel3版本中有一个浮点处理单元FPU,但这不是OR1200 的一部分,是另外一个开源项目,所以本书不分析FPU。下载后进入branches/or1200_rel3/rtl/verilog这个目录,可以找到OR1200的所有Verilog源代码文件。其顶层文件是or1200_top.v,利用这个文件我们得出OR1200的各个模块的连接关系,如图1.6所示,其中各模块的说明如表1.4所示。这里将顶层模块称为OR1200_TOP。在本书的光盘中有一个详细的连接关系图,文件名为or1200_top.vsd,请使用VISIO打开。图1.6 就是依据or1200_top.vsd绘制的简化版本,但在其中也明确地展现了OR1200中各个模块的连接关系,比如从图1.6中可以发现MMU与Cache之间要经过QMEM,还可以清晰地观察到OR1200采用的是Harvard结构。
图1.6 OR1200_TOP内部各模块连接关系
表1.4 OR1200_TOP内部各模块说明
上述模块中的IMMU、DMMU、ICache、DCache、QMEM、SB、PIC、PM、DU和TT都是可选项,本书在第2章搭建的最小系统就只有CPU、QMEM,如图1.7所示,这样可以排除其他模块的影响,使得我们将注意力完全放在对CPU模块的分析上。
图1.7 OR1200可以运行的最小系统
CPU是OR1200 中的核心模块,完成运算和控制的功能,其内部包括多种模块,图1.8展示了其内部的各种模块,但是没有画出各模块之间的连接关系,只因连接关系复杂,如果简单地将模块连接起来,会引起读者的误解。
图1.8 OR1200中CPU模块的内部功能模块
笔者参考CPU模块的顶层文件or1200_cpu.v详细地绘制了CPU模块内部各个功能模块之间的连接,存放在本书的光盘中,文件名为or1200_cpu.vsd,也是一个VISIO文件,在本书的阅读过程中,建议读者经常查阅光盘中的or1200_cpu.vsd与or1200_top.vsd两张图,这样做的优点有两个:一是本书在分析指令处理的过程中会经常提到某个信号输出到某个模块,如果不借助于这两张图,那么需要从代码中找模块连接关系、信号的走向,十分不便,有了这两张图,就可以迅速知道某个模块的输出信号流向了哪里;第二个原因是图形记忆是比较好的记忆方法,借助于图形我们可以更好地理解、记忆各个模块的作用。
从图1.8中我们可以发现CPU中的核心模块是CTRL,实际上通过后面对具体指令处理过程的分析可知所有的指令都需要在该模块中译码、分析,然后输出控制信号给其他模块进行计算,流水线也是在该模块中实现的。
CPU模块内部各功能模块的作用、涉及的主要Verilog文件如表1.5所示。
表1.5 CPU模块内部各功能模块说明
1.5 本书的分析方法
关于本书的分析方法,在此给予说明。之前有学者著《CPU源代码分析与芯片设计及Linux移植》一书,作者在那本书中对OR1200的源代码进行过一个模块一个模块的分析,给出注释,本书不打算采用这种方法。笔者借鉴了软件的分析方法,在当前很多的软件设计中采用的是事件驱动的方法,按钮被点击了,就会对应一个按钮点击事件,可以查看对应的事件处理函数,这样把函数与具体的事件联系起来,阅读代码会更加容易,对于处理器分析而言这里的事件可以解读为指令,来了一条指令如何处理,通过对指令处理过程的分析,达到对处理器内部部分模块工作原理的掌握,但是分析完所有指令的处理过程,我们就实现了对处理器内部全部模块工作原理的完全掌握了。这种分析方法笔者称之为指令驱动的分析方法。
指令驱动的分析方法要求我们对每一条指令都进行分析,而对于单个指令的分析,本书将紧密结合流水线的工作过程,采用流水线驱动的分析方法。因为每一条指令都要经过流水线的各个阶段,故而通过分析指令在流水线每个阶段需要完成的工作,也就明白了指令的实现过程。
综上所述,本书的分析方法概括地讲就是:宏观上采用指令驱动的分析方法,微观上采用流水线驱动的分析方法。
基于上述思路本书完成了对OR1200处理器的分析,在第2章建立了我们的实验环境,主要是一个OR1200可以运行的最小系统及编译链接的工具。在第3章对最小系统中的QMEM模块进行了分析,并给出了理想的取指过程。
在第4~9章中我们将按照指令的类别分别进行分析,分析的步骤基本一样:首先写一段简单的测试代码,测试代码包含我们即将分析的指令类别中的全部或部分指令,然后使用OR1KSim模拟器查看执行情况,接下来对这一类指令中有代表性的若干条指令按照流水线的三个阶段进行分析,主要分析指令执行过程中在流水线各个阶段引起的信号变化,以及通过这些变化的信号是如何逐步实现指令目标的。通过对第4~9章的学习读者可以明白OR1200中全部指令的执行过程,进而理解OR1200中CPU模块的工作原理、工作过程。
在剩下的章节中本书分别介绍了OR1200中除CPU模块外的其余模块,包括:MMU、Cache、Store Buffer、Wishbone总线接口单元、可编程中断控制器(PIC)、计时器单元(TT)和电源管理(PM)等模块。这些模块有的简单有的复杂,对于简单模块本书就直接分析源代码,如:PM模块;对于复杂模块,本书采用的是情景分析法,分析使用该模块的各种情景及各种情景下模块的工作过程,从而达到了解该模块原理的目的。需要说明一点,由于调试模块(DU)涉及大量JTAG的知识,本书专注于对处理器工作原理的讲解,所以对DU模块就略过不讲。