第3章 QMEM剖析
OR1200中有很多模块,笔者决定首先分析QMEM,理由如下。
(1)第2章建立的最小系统中,指令都存储在QMEM中。
(2)在建立最小系统过程中,or1200_qmem_top.v是唯一一个修改了源代码的文件,该文件就是QMEM模块的顶层文件,我们需要明白为什么要作这个修改。
(3)从图1.6中可以发现QMEM处于IMMU与ICache、DMMU与DCache之间的“交通要道”,地位十分重要,便于通过QMEM的分析理解Wishbone总线。
(4)QMEM很简单。
本章首先描述了QMEM的作用,由于QMEM与CPU、IMMU、ICache、DMMU、DCache都是通过Wishbone总线连接的,所以会对Wishbone总线规范进行介绍。在此基础上分析QMEM的内部RAM实现。在OR1200运行过程中,会在三种情况下使用到QMEM:取指令、加载(Load)数据、存储(Store)数据。本章将结合取指令的情况分析QMEM在其中的工作过程,后两种情况留在 “加载存储类指令剖析”时再分析。取指令又分两种情况:复位后取第一条指令、正常运行过程中的指令读取。在本章的最后两小节,将分别对这两种情况进行分析。
3.1 QMEM的作用
QMEM是Quick Memory的简称,顾名思义,就是快速存储器,实际上这是一个片上RAM,可以实现在一个时钟周期内读取其中存储内容(指令、数据),用户可以把一些常用的代码存放在其中,比如:Context切换过程、异常处理句柄、堆栈等。有学者将QMEM类比为OR1200的一级缓存(L1 Cache),这种说法是不对的,缓存是不可寻址的,但是QMEM是可以寻址的,而且QMEM没有缓存中存在的替换问题,所以QMEM的作用不是缓存,就是一个可寻址的片上存储器。它在OR1200结构中的位置如图3.1所示。
取指令的过程:CPU给出指令的有效地址(EA:Effective Address),IMMU将有效地址翻译为物理地址(PA:Physical Address),然后送到QMEM中。QMEM判断该地址是否位于QMEM的地址范围内,如果是,就直接取出指令送到CPU;如果不是位于QMEM的地址范围内,则将该地址发送给ICache。ICache查看该地址是否被缓存,如果是就直接读出指令送往QMEM,后者直接转发给CPU;如果ICache没有命中,则通过指令WB_BIU模块访问外部存储器获取指令。
加载/存储数据的过程:CPU给出读写数据的有效地址,DMMU将有效地址翻译为物理地址,然后送到QMEM中。QMEM判断该地址是否位于QMEM的地址范围内,如果是,就直接读写指定地址;如果不是位于QMEM的地址范围内,则将该地址发送给Dcache。DCache查看该地址是否被缓存,如果是就直接将该数据送往QMEM,后者直接转发给CPU,或者直接写数据;如果DCache没有命中,则会通过SB、数据WB_BIU模块访问外部存储器读写数据。
图3.1 QMEM在OR1200中的位置
所以QMEM的作用如图3.2所示,需要注意的是QMEM中只有一个RAM,指令、数据共用这一个RAM。
图3.2 QMEM的作用示意图
在OR1200的配置文件or1200_defines.v中,关于QMEM的配置选项如下。
`define OR1200_QMEM_IMPLEMENTED `define OR1200_QMEM_IADDR 32'h0080_0000 `define OR1200_QMEM_IMASK 32'hfff0_0000 // QMEM中指令存储地址是 //0x00800000-0x008fffff,所以共1MB `define OR1200_QMEM_DADDR 32'h0080_0000 `define OR1200_QMEM_DMASK 32'hfff0_0000 QMEM中数据存储地址与指令存储 //地址可以不一样,默认是一样的 `define OR1200_QMEM_BSEL //默认是没有定义,但通过加载存储 //类指令的分析可知必须有该定义 // QMEM interface acknowledge // To enable qmem_ack port, define this macro. //`define OR1200_QMEM_ACK 不需要这个定义
其中OR1200_QMEM_IADDR、OR1200_QMEM_IMASK两个参数定义了QMEM中指令的地址范围,OR1200_QMEM_DADDR、OR1200_QMEM_DMASK两个参数定义了QMEM中数据的地址范围,当IMMU、DMMU将指令地址、数据地址送入QMEM后,在QMEM中会进行如下运算。
or1200_qmem_top.v `ifdef OR1200_QMEM_IADDR assign iaddr_qmem_hit = (qmemimmu_adr_i & `OR1200_QMEM_IMASK) == `OR1200_QMEM_IADDR; `else assign iaddr_qmem_hit = 1'b0; `endif `ifdef OR1200_QMEM_DADDR assign daddr_qmem_hit = (qmemdmmu_adr_i & `OR1200_QMEM_DMASK) == `OR1200_QMEM_DADDR; `else assign daddr_qmem_hit = 1'b0; `endif
以IMMU送入的指令地址为例,将输入地址qmemimmu_adr_i与OR1200_QMEM_IMASK相“与”,然后与OR1200_QMEM_IADDR比较,如果采用默认值,即:
iaddr_qmem_hit = (qmemimmu_adr_i & 32'hfff0_0000) == 32'h0080_0000;
此时只要输入地址在0x00800000-0x008fffff(1M大小的空间范围内),都会使得iaddr_qmem_hit为1,表示地址在QMEM范围内,就会从QMEM中读取对应地址指令,反之iaddr_qmem_hit为0。当DMMU将数据地址送入QMEM后,也会进行同样的运算。所以当采用默认配置时QMEM中指令与数据拥有相同的地址空间,地址范围是0x00800000 至0x008fffff,大小都是1M。
在第2章建立最小系统时,将上面的代码改为:
`ifdef OR1200_QMEM_IADDR assign iaddr_qmem_hit = 1'b1; `else assign iaddr_qmem_hit = 1'b0; `endif `ifdef OR1200_QMEM_DADDR assign daddr_qmem_hit = 1'b1; `else assign daddr_qmem_hit = 1'b0; `endif
代码修改后使得无论输入的指令地址、数据地址的值是多少,iaddr_qmem_hit、daddr_qmem_hit始终为1,表示要访问的指令、数据在QMEM中,这样就会从QMEM内部的RAM中取得指令、数据,所以在最小系统中将代码放在QMEM中,处理器OR1200能够正确执行。
3.2 Wishbone总线快速了解
在继续介绍QMEM之前,读者需要了解Wishbone总线规范,所以本节对Wishbone总线规范进行“足够我们需要”的介绍。
PC机一般都提供PCI插槽,各种板卡(包括显卡、语音卡、网卡,甚至用户自制的板卡)只要满足PCI接口标准,就可以直接插在PC机的PCI插槽使用,十分方便。同样的道理,目前有很多IP核的研发者或公司,为了方便不同研发者或公司的IP核能够直接连接,就要求这些IP核遵守共同的接口标准。在片上系统(SoC)中,处理器核与其他IP核通过共享总线互通互联,这些IP核必须遵守相同的总线规范。总线规范定义了IP核之间的通用接口。目前常见的片上总线规范有ARM公司的AMBA、IBM公司的CoreConnect、Altera公司的Avalon,以及本节介绍的Wishbone。笔者不对这些规范进行比较,只介绍Wishbone规范。
Wishbone总线规范是Silicore公司最先提出的,由于其开放性,现在已有不少用户群,特别是一些免费的IP核,大多数都采用Wishbone规范。Wishbone除了开放、免费,还有简单、灵活、轻量、支持用户自定义标签的特点。目前已有B4版本的规范,OR1200中遵循的是Wishbone B2与B3版本的规范,用户可以自行配置是采用B2还是B3规范。
Wishbone有多种互联方式:点对点、数据流、共享总线和交叉互联等。OR1200内部使用的都是点对点连接方式。在点对点连接方式中,有一个主设备,一个从设备,连接关系如图3.3所示,注意图中输出信号使用“_O”结束,输入信号使用“_I”结束,同时所有的信号都是高电平有效。
图3.3 Wishbone总线规范点对点互联方式
图3.3中主从设备接口的含义如下。
(1)CLK_I、RST_I:分别是时钟信号、复位信号,由外部输入。
(2)DAT_O/DAT_I:主设备和从设备的之间的数据信号,数据可以由主设备传送给从设备,也可以由从设备传送给主设备。一对主设备和从设备之间最多存在两条数据总线,一条用于主设备向从设备传输数据,另一条用于从设备向主设备传输数据。
(3)ADR_O/ADR_I:地址信号,主设备输出地址到从设备。
(4)WE_O/WE_I:写使能信号,主设备输出到从设备,代表当前周期中进行的操作是写操作还是读操作,1代表写,0代表读。
(5)SEL_O/SEL_I:数据总线选择信号,标识当前操作中数据总线上哪些比特是有效的,以总线粒度为单位。SEL_O/SEL_I的宽度为数据总线宽度除以数据总线粒度,比如一个具有32位宽、粒度为1字节的数据总线的选择信号应定义为SEL_O(3:0)/ SEL_I(3:0),SEL(4’b1001)代表当前操作中数据总线的最高和最低字节有效。
(6)CYC_O/CYC_I:总线周期信号,CYC_O/CYC_I有效代表一个主设备请求总线使用权或者正在占有总线,但是不一定正在进行总线操作(是否正在进行总线操作取决于选通信号STB_O/STB_I是否有效)。只有该信号有效,Wishbone主设备和从设备之间的其他信号才有意义。CYC_O/CYC_I信号在一次总线操作过程中必须持续有效,比如一次块读操作可能需要多个时钟周期,CYC_O/CYC_I信号必须在多个时钟周期中持续有效。
(7)STB_O/STB_I:选通信号。选通有效代表主设备发起一次总线操作。只有选通信号有效时(此时CYC_O/CYC_I也必须为高),ADR_O/ADR_I、DAT_O/DAT_I、SEL_O/SEL_I才有意义。
(8)ACK_O/ACK_I:实际还可以有ERR_O/ERR_I、RTY_O/RTY_I,都表示主从设备间的操作结束方式信号。ACK表示成功,ERR表示错误,RTY表示重试。操作总是在某一总线周期内完成的,因此操作结束方式也称为总线周期结束方式。成功是操作的正常结束方式,错误表示操作失败,造成失败的原因可能是地址或者数据校验错误,写操作或者读操作不支持等。重试表示从设备当前忙,不能及时处理该操作,可以稍后重新发起。接收到操作失败或者重试后,主设备如何响应取决于主设备的设计者。
(9)TAGN_O/TAGN_I:标签信号,用户可以利用标签信号传递自定义的信息。
一个总线周期由多个不同的时钟周期构成,完成单次读/写操作、块读/写操作、读改写操作,总线周期也相应分为单次读/写周期、块读/写周期、读改写周期。本节重点介绍单次读/写周期。
一般情况下,一次操作由主设备和从设备控制信号的一次握手,以及同时进行的地址和数据总线的一次传输构成。当主设备将CYC_O置高,一个总线周期开始,此后当STB_O为高时,一次总线操作开始。CYC_O和STB_O可以同时从低电平变为高电平,表示发起总线周期的同时开始一次总线操作,因此在只有一个主设备时可以将两者合并为一个信号。在OR1200中就直接将CYC_O、STB_O合并成CYCSTB_O。
主从设备之间信号虽然很多,但单次读/写操作实际上十分简单。单次读操作的Wishbone总线信号如图3.4所示,此处是从主设备的角度观察信号变化。
在时钟上升沿0,主设备将地址信号ADR_O、适当的SEL_O放到总线上,将WE_O置低表示读操作,将CYC_O、STB_O置高表示一次总线操作开始。
在时钟上升沿1到达之前,从设备检测到主设备发起的操作,将适当的数据放到主设备的输入信号DAT_I,同时将主设备的ACK_I置高作为对主设备STB_O的响应。从设备可以在ACK_I有效之前插入任意数量的等待状态。
图3.4 Wishbone总线单次读操作时主设备的信号(不考虑TAGN_O/TAGN_I)
在时钟上升沿1,主设备发现ACK_I信号为高,将DAT_I采样,并将STB_O和CYC_O置低表示操作完成。从设备发现STB_O置低后,将主设备的输入信号ACK_I也置低。单次读操作就完成了。
单次写操作的Wishbone总线信号如图3.5所示,此处还是从主设备的角度观察信号变化。
图3.5 Wishbone总线单次写操作时主设备的信号(不考虑TAGN_O/TAGN_I)
在时钟上升沿0,主设备将地址信号ADR_O、数据信号DAT_O放到总线上,将WE_O置高表示写操作,将适当的SEL_O放到总线上指示从设备DAT_O中哪些字节是有效的,将CYC_O、STB_O置高表示一次总线操作开始。
在时钟上升沿1到达之前,从设备检测到主设备发起的操作,将锁存DAT_O的数据,同时将主设备的ACK_I置高作为对主设备STB_O的响应。从设备可以在ACK_I有效之前插入任意数量的等待状态。
在时钟上升沿1,主设备发现ACK_I信号为高,将STB_O和CYC_O置低表示操作完成。从设备发现STB_O置低后,将主设备的输入信号ACK_I也置低。单次写操作就完成了。
以上介绍了Wishbone总线规范的单次读/写操作的操作周期,在OR1200中将STB_O与CYC_O合并成了一个信号CYCSTB_O,所以对于单次读/写操作我们可以简单地认为CYCSTB_O为1时,总线操作开始,依据WE_O的值决定是读还是写,当ACK_I为1时表示总线操作结束。
块读/写周期实际就是连续的多个单次读/写周期,在块读/写周期中CYC_O、STB_O持续有效,本章3.6节的理想取指模型中使用的就是块读/写周期。
在第1章介绍OR1200内各个模块的时候提到模块WB_BIU,这是一个Wishbone总线接口单元,使得OR1200可以访问具备Wishbone总线接口的外设、存储器等。实际在OR1200内部也大量使用到了Wishbone规范,比如:CPU模块与IMMU、IMMU与QMEM、QMEM与ICache、ICache与WB_BIU之间都使用到了Wishbone总线规范,图3.6中加粗的连线表示模块之间使用的是Wishbone总线规范,使用的都是点对点互联标准。
图3.6 Wishbone总线规范在OR1200内部大量使用
3.3 QMEM的Wishbone总线接口
文件or1200_top.v是OR1200的顶层文件,其中定义了各个模块之间的连接关系,本书光盘中的or1200_top.vsd就是参照该文件绘制的,读者借助or1200_top.vsd可以清楚地了解到QMEM模块是如何连接到其余模块的。QMEM对外连接主要体现在如下4个方面。
(1)QMEM与CPU、IMMU的连接。
(2)QMEM与ICache的连接。
(3)QMEM与CPU、DMMU的连接。
(4)QMEM与DCache的连接。
3.3.1 QMEM与CPU、IMMU的连接
指令存放在存储器(对最小系统而言就是QMEM)中,OR1200的CPU模块是通过指令Wishbone总线与存储器相连的,从本书光盘的or1200_cpu.vsd、or1200_top.vsd中可以更直观地了解到CPU模块的指令Wishbone总线接口分别位于GENPC、IF两个模块。CPU、IMMU和QMEM的连接关系如图3.7所示。图中没有Wishbone规范中的WE_O信号,因为指令存储器是只读的,不可写,所以没有WE_O信号接口。
图3.7 OR1200中CPU、IMMU和QMEM模块的连接关系
在我们的最小系统中没有配置IMMU(IMMU模块实际存在,但没有发挥作用,在IMMU中直接将部分CPU的信号与QMEM的信号相连),所以可以简单地认为QMEM的信号直接连接到CPU模块,IMMU模块对应的顶层文件or1200_immu_top.v中有如下代码。
or1200_defines.v
`ifdef OR1200_NO_IMMU //在我们的最小系统中没有配置IMMU
……
assign qmemimmu_adr_o = icpu_adr_i;
assign icpu_tag_o = qmemimmu_tag_i;
assign qmemimmu_cycstb_o = icpu_cycstb_i & ~page_cross;
//page_cross表示前后两次指令的地址是否位于不同
//的内存页,OR1200中默认内存页是8KB,当位于不
//同的页时page_cross为1,反之为0,笔者认为当
//没有配置使用IMMU时,page_cross其实没有作用,
//可以去掉,所以此处可以直接改为
//assign qmemimmu_cycstb_o = icpu_cycstb_i
assign icpu_rty_o = qmemimmu_rty_i;
assign icpu_err_o = qmemimmu_err_i;
……
`else
……
所以在最小系统中QMEM与CPU的连接关系可以认为如图3.8所示,QMEM与CPU内部的GENPC、IF模块是直接相连的。
图3.8 在最小系统中IMMU的作用很小
3.3.2 QMEM与ICache的连接
如果要读取指令的地址不在QMEM地址范围内,那么QMEM会将该地址转发给Icache。ICache如果未命中,则会通过WB_BIU访问外部存储器。此处QMEM与ICache、ICache与WB_BIU、WB_BIU与外部存储器之间均是采用Wishbone总线连接,如图3.9所示。
图3.9 QMEM、ICache、WB_BIU、外部存储器之间都是使用Wishbone总线规范互联
在QMEM中有如下赋值语句,其中qmem_iack为1表示要读取的指令地址在QMEM地址范围内,且已取得,反之表示指令地址不在QMEM地址范围内。iaddr_qmem_hit的含义之前已解释过,为0表示指令地址不在QMEM地址范围内。
or1200_qmem_top.v //QMEM与CPU、IMMU之间信号 assign qmemicpu_dat_o = qmem_iack ? qmem_do : icqmem_dat_i; assign qmemicpu_ack_o = qmem_iack ? 1'b1 : icqmem_ack_i; assign qmemimmu_rty_o = qmem_iack ? 1'b0 : icqmem_rty_i; assign qmemimmu_err_o = qmem_iack ? 1'b0 : icqmem_err_i; assign qmemimmu_tag_o = qmem_iack ? 4'h0 : icqmem_tag_i; // QMEM与ICache之间 assign icqmem_adr_o = iaddr_qmem_hit ? 32'h0000_0000 : qmemimmu_adr_i; assign icqmem_cycstb_o = iaddr_qmem_hit ? 1'b0 : qmemimmu_cycstb_i; assign icqmem_ci_o = iaddr_qmem_hit ? 1'b0 : qmemimmu_ci_i; assign icqmem_sel_o = iaddr_qmem_hit ? 4'h0 : qmemicpu_sel_i; assign icqmem_tag_o = iaddr_qmem_hit ? 4'h0 : qmemicpu_tag_i;
如果iaddr_qmem_hit、qmem_iack为0,上述代码相当于如下内容。
//QMEM与CPU、IMMU之间信号 assign qmemicpu_dat_o = icqmem_dat_i; assign qmemicpu_ack_o = icqmem_ack_i; assign qmemimmu_rty_o = icqmem_rty_i; assign qmemimmu_err_o = icqmem_err_i; assign qmemimmu_tag_o = icqmem_tag_i; // QMEM与ICache之间 assign icqmem_adr_o = qmemimmu_adr_i; assign icqmem_cycstb_o = qmemimmu_cycstb_i; assign icqmem_ci_o = qmemimmu_ci_i; assign icqmem_sel_o = qmemicpu_sel_i; assign icqmem_tag_o = qmemicpu_tag_i;
结合图3.8、图3.9可以明白,上述代码表明当指令地址不在QMEM地址范围内时.QMEM只是将CPU、IMMU的Wishbone总线接口与ICache的Wishbone总线接口直接连接起来。
3.3.3 QMEM与CPU、DMMU的连接
OR1200的CPU模块是通过数据Wishbone总线与存储器相连的,借助于本书光盘的or1200_cpu.vsd、or1200_top.vsd可以直观地了解到CPU模块的数据Wishbone总线接口全部位于LSU模块。CPU、DMMU和QMEM的连接关系如图3.10所示。此处提供了WE_O、DAT_O信号接口,因为数据存储器是可读可写的。
在我们的最小系统中也没有配置DMMU,所以可以简单地认为QMEM的信号直接连接到CPU模块,DMMU模块对应的顶层文件or1200_dmmu_top.v中有如下代码。
or1200_dmmu_top.v `ifdef OR1200_NO_DMMU //在我们的最小系统中没有配置DMMU assign qmemdmmu_adr_o = dcpu_adr_i; assign dcpu_tag_o = qmemdmmu_tag_i; assign qmemdmmu_cycstb_o = dcpu_cycstb_i; assign dcpu_err_o = qmemdmmu_err_i; `else ……
图3.10 OR1200中CPU、DMMU、QMEM的连接关系
所以在最小系统中QMEM与CPU的连接关系可以认为如图3.11所示,QMEM与CPU内部的LSU模块是直接相连的。
图3.11 在最小系统中DMMU的作用很小
3.3.4 QMEM与DCache的连接
如果要访问数据的地址不在QMEM地址范围内,QMEM会将该地址转发给Dcache。DCache如果未命中,则会通过SB、WB_BIU访问外部存储器。此处QMEM与DCache、DCache与SB、SB与WB_BIU、WB_BIU与外部存储器之间均是采用Wishbone总线连接,如图3.12所示。
图3.12 QMEM、DCache、SB、WB_BIU、外部存储器之间都通过Wishbne总线规范连接
在QMEM中有如下赋值语句,daddr_qmem_hit的含义之前已解释过,为0表示要访问的数据地址不在QMEM地址范围内,反之表示要访问的数据地址在QMEM地址范围内。
or1200_qmem_top.v //QMEM与CPU、DMMU之间的信号 assign qmemdcpu_dat_o = daddr_qmem_hit ? qmem_do : dcqmem_dat_i; assign qmemdcpu_ack_o = daddr_qmem_hit ? qmem_dack : dcqmem_ack_i; assign qmemdcpu_rty_o = daddr_qmem_hit ? ~qmem_dack : dcqmem_rty_i; assign qmemdmmu_err_o = daddr_qmem_hit ? 1'b0 : dcqmem_err_i; assign qmemdmmu_tag_o = daddr_qmem_hit ? 4'h0 : dcqmem_tag_i; //QMEM与DCache之间的信号 assign dcqmem_adr_o = daddr_qmem_hit ? 32'h0000_0000 : qmemdmmu_adr_i; assign dcqmem_cycstb_o = daddr_qmem_hit ? 1'b0 : qmemdmmu_cycstb_i; assign dcqmem_ci_o = daddr_qmem_hit ? 1'b0 : qmemdmmu_ci_i; assign dcqmem_we_o = daddr_qmem_hit ? 1'b0 : qmemdcpu_we_i; assign dcqmem_sel_o = daddr_qmem_hit ? 4'h0 : qmemdcpu_sel_i; assign dcqmem_tag_o = daddr_qmem_hit ? 4'h0 : qmemdcpu_tag_i; assign dcqmem_dat_o = daddr_qmem_hit ? 32'h0000_0000 : qmemdcpu_dat_i;
如果daddr_qmem_hit为0,上述代码相当于如下内容。
//QMEM与CPU、DMMU之间的信号 assign qmemdcpu_dat_o = dcqmem_dat_i; assign qmemdcpu_ack_o = dcqmem_ack_i; assign qmemdcpu_rty_o = dcqmem_rty_i; assign qmemdmmu_err_o = dcqmem_err_i; assign qmemdmmu_tag_o = dcqmem_tag_i; //QMEM与DCache之间的信号 assign dcqmem_adr_o = qmemdmmu_adr_i; assign dcqmem_cycstb_o = qmemdmmu_cycstb_i; assign dcqmem_ci_o = qmemdmmu_ci_i; assign dcqmem_we_o = qmemdcpu_we_i; assign dcqmem_sel_o = qmemdcpu_sel_i; assign dcqmem_tag_o = qmemdcpu_tag_i; assign dcqmem_dat_o = qmemdcpu_dat_i;
参考图3.11、3.12 可以明白,上述代码表示当要访问的数据地址不在QMEM地址范围内时QMEM只是将CPU、DMMU的Wishbone总线接口与DCache的Wishbone总线接口直接连接起来。
3.4 QMEM的内部RAM
从图3.2可知在QMEM中有一个RAM,指令与数据都存储在该RAM中。QMEM中关于该RAM的例化语句如下。
or1200_qmem_top.v
or1200_spram_2048x32 or1200_qmem_ram(
.clk(clk), .rst(rst),
`ifdef OR1200_BIST
.mbist_si_i(mbist_si_i), .mbist_so_o(mbist_so_o),.mbist_ctrl_i(mbist_ctrl_i),
`endif
`ifdef OR1200_QMEM_BSEL
.sel(qmem_sel),
`endif
`ifdef OR1200_QMEM_ACK
.ack(qmem_ack), //实际的RAM代码中并没有ack信号,所以这里是不需要的
`endif
.addr(qmem_addr[12:2]), .ce(qmem_en), .we(qmem_we),.oe(1'b1),
.di(qmem_di),.doq(qmem_do)
);
打开文件or1200_spram_2048x32.v,在其中定义具体的RAM,由于考虑了各种芯片的实际情况,所以这个文件很长,但与我们有关的很少,在or1200_defines.v中默认是Generic Memory,表示通用存储器,不考虑实际厂家,所以or1200_spram_2048x32.v文件中只有如下部分是在OR1200最小系统中使用的。
or1200_spram_2048x32.v parameter aw = 11; //地址线的宽度为11,数据线的宽度为32,所以共有8KB parameter dw = 32; reg [dw-1:0]mem [(1<<aw)-1:0]; //使用数组定义RAM,在综合的时候会将其综合为RAM单元 reg [aw-1:0] addr_reg; //使用mem.data初始化RAM,也就是初始化QMEM initial $readmemh ( "mem.data", mem ); assign doq = (oe) ? mem[addr_reg] : {dw{1'b0}}; //doq是读出的数据 always @(posedge clk or `OR1200_RST_EVENT rst) if (rst == `OR1200_RST_VALUE) addr_reg <= {aw{1'b0}}; //地址寄存 else if (ce) addr_reg <= addr; always @(posedge clk) if (ce && we) mem[addr] <= di; //写RAM
上述代码就是使用数组得到一个RAM,具体芯片的综合工具会将这个数组综合为存储单元。QMEM中的RAM例化涉及如下信号:qmem_en、qmem_we、qmem_di、qmem_addr、qmem_do和qmem_sel。在QMEM中上述信号定义如下。
or1200_qmem_top.v assign qmemicpu_dat_o = qmem_iack ? qmem_do : icqmem_dat_i; assign qmemdcpu_dat_o = daddr_qmem_hit ? qmem_do : dcqmem_dat_i; assign qmem_en = iaddr_qmem_hit & qmemimmu_cycstb_i | daddr_qmem_hit & qmemdmmu_cycstb_i; assign qmem_we = qmemdmmu_cycstb_i & daddr_qmem_hit & qmemdcpu_we_i; assign qmem_di = qmemdcpu_dat_i; assign qmem_addr = (qmemdmmu_cycstb_i & daddr_qmem_hit) ? qmemdmmu_adr_i : qmemimmu_adr_i; `ifdef OR1200_QMEM_BSEL assign qmem_sel = (qmemdmmu_cycstb_i & daddr_qmem_hit) ? qmemdcpu_sel_i : qmemicpu_sel_i; `endif
(1)qmem_en:连接到RAM的ce,作为片选信号,当CPU取指令(qmemimmu_cycstb_i为1),且指令地址在QMEM地址范围内(iaddr_qmem_hit为1)时,或者CPU加载/存储数据(qmemdmmu_cycstb_i为1),且数据地址在QMEM地址范围内(daddr_qmem_hit为1)时,都会使得qmem_en为1。
(2)qmem_we:连接到RAM的we,作为写操作信号,当CPU加载/存储数据(qmemdmmu_cycstb_i为1),且数据地址在QMEM地址范围内(daddr_qmem_hit为1),且是写操作(qmemdcpu_we_i为1)时,会使得qmem_we为1。
(3)qmem_di:连接到RAM的di,作为RAM的输入数据,因为指令存储器是只读的,所以CPU的指令Wishbone总线没有WE_O、DAT_O接口,只有CPU的数据Wishbone总线有WE_O、DAT_O接口,因此qmem_di直接连接到CPU数据Wishbone总线的DAT_O接口(参考图3.11可知qmemdcpu_dat_i连接到CPU的dcpu_dat_o)。
(4)qmem_addr:连接到RAM的addr,作为地址信号,CPU是可以按字节寻址的,而QMEM中的RAM是按字寻址的,且地址宽度是11,所以只取QMEM输入地址的2~12位,比如读取0x4处的数据,实际送往RAM的地址是0x1,如图3.13所示。此外由于只有一个RAM,但是指令总线、数据总线都可能要访问该RAM,当同时访问时就有一个优先级的问题,这里采用的数据总线的优先级高于指令总线的优先级。当加载/存储数据(qmemdmmu_cycstb_i为1),且数据地址在QMEM地址范围内(daddr_qmem_hit为1)时,送到RAM的地址就是数据Wishbone总线送入的地址qmemdmmu_adr_i,反之是指令Wishbone总线送入的地址qmemimmu_adr_i。
(5)qmem_do:连接到RAM的doq,作为RAM的输出数据,在一定条件下赋值给qmemicpu_dat_o、qmemdcpu_dat_o。
(6)qmem_sel:在RAM中没有该信号,但实际上该信号在加载存储类指令执行时是有作用的,所以OR1200的rel3版本的QMEM内部RAM设计是有问题的,具体讨论及改正方法将在加载存储类指令分析时给出。
图3.13 QMEM中的RAM是按照字寻址的
CPU会在三种情况下用到QMEM:取指令、加载(Load)数据和存储(Store)数据。关于后两者将在加载存储类指令分析时进行介绍,本章只结合取指令过程分析QMEM的工作过程,取指令也分两种情况:复位后取第一条指令、正常运行过程中的指令读取。
3.5 复位后取第一条指令的过程分析
假如宇宙始于一次大爆炸,那么我们的OR1200就始于加电的那一刻,但此刻还是一片混沌(内部还没有初始化),所以需要一个复位信号,复位之后,OR1200的世界就进入一个确定性的世界了,会依次取得指令并执行,本节分析取得第一条指令的过程。
3.5.1 复位信号有效阶段
复位信号有效阶段指的是OR1200的输入信号rst为1的时期。
通过图3.8可知CPU中的GENPC模块产生指令地址(icpu_adr_o),以及指令Wishbone总线周期的开始信号(icpu_cycstb_o),从QMEM取得的指令(icpu_dat_i)送入IF模块。所以我们的关注点就是GENPC模块、IF模块和QMEM模块。
1.GENPC模块的信号变化
在GENPC模块中有如下代码,确定指令Wishbone总线的总线周期开始信号icpu_cycstb_o、地址信号icpu_adr_o的值。
or1200_genpc.v assign icpu_adr_o = !no_more_dslot & !except_start & !spr_pc_we & (icpu_rty_i| genpc_refetch) ? icpu_adr_i : {pc[31:2], 1'b0, ex_branch_taken| spr_pc_we}; assign icpu_cycstb_o = ~(genpc_freeze | (|pre_branch_op && !icpu_rty_i));
上式中很多信号都是外部输入到GENPC的,通过or1200_cpu.vsd可以很清楚地知道信号的来源(再次强调多使用光盘中的or1200_top.vsd、or1200_cpu.vsd,会给我们带来很大的方便),以no_more_dslot为例,该信号来自CTRL模块的输出no_more_dslot,在CTRL模块中有如下代码。
or1200_ctrl.v assign no_more_dslot = (|ex_branch_op & !id_void & ex_branch_taken) | (ex_branch_op == `OR1200_BRANCHOP_RFE); always @(posedge clk or `OR1200_RST_EVENT rst) if (rst == `OR1200_RST_VALUE) ex_branch_op <= `OR1200_BRANCHOP_NOP;//or1200_defines.v中定义 //OR1200_BRANCHOP_NOP为0 ……
所以no_more_dslot在复位的时候为0。使用同样的步骤可以发现在复位的时候except_start、spr_pc_wr、genp_freeze、pre_branch_op都为0,所以复位的时候GENPC的icpu_cycstb_o信号为1。但是变量icpu_rty_i不同,通过分析可以得出图3.14,从中可知icpu_rty_i的值最终取决于WB_BIU模块中的icbiu_ack_o、icbiu_err_o。
图3.14 GENPC模块的icpu_rty_i信号与WB_BIU模块有关
在WB_BIU模块中,icbiu_ack_o、icbiu_err_o的值如下。
or1200_wb_biu.v assign biu_ack_o= (wb_fsm_state_cur == wb_fsm_trans) & wb_ack & wb_stb_o & (wb_ack_cnt ~^ biu_ack_cnt); assign biu_err_o = (wb_fsm_state_cur == wb_fsm_trans) & wb_err_i & wb_stb_o & (wb_err_cnt ~^ biu_err_cnt) always @(posedge wb_clk_i or `OR1200_RST_EVENT wb_rst_i) begin if (wb_rst_i == `OR1200_RST_VALUE) wb_fsm_state_cur <= wb_fsm_idle; //复位的时候wb_fsm_state_cur为wb_fsm_idle,所以上面的 //biu_ack_o、biu_err_o都为0 else wb_fsm_state_cur <= wb_fsm_state_nxt; end
复位的时候biu_ack_o、biu_err_o都为0,所以GENPC的icpu_rty_i为1,导致GENPC模块的输出icpu_adr_o等于icpu_adr_i。
or1200_genpc.v assign icpu_adr_o = !no_more_dslot & !except_start & !spr_pc_we & (icpu_rty_i | genpc_refetch) ? //这里icpu_rty_i为1 icpu_adr_i : {pc[31:2], 1'b0, ex_branch_taken|spr_pc_we};
参考图3.8可知GENPC的icpu_adr_i来自IMMU模块,是IMMU的输出icpu_adr_o,IMMU中有如下代码。
or1200_immu_top.v wire[31:0] icpu_adr_boot = boot_adr; // parameter boot_adr = `OR1200_ BOOT_ADR,即0x100 always @(`OR1200_RST_EVENT rst or posedge clk) if (rst == `OR1200_RST_VALUE) begin pcreg_default <= `OR1200_BOOT_PCREG_DEFAULT; //复位的时候pcreg_default为0x39 pcreg_select <= 1'b1; end else if (icpu_adr_select) begin icpu_adr_default <= icpu_adr_boot; icpu_adr_select <= 1'b0; end else begin icpu_adr_default <= icpu_adr_i; end always @(icpu_adr_boot or icpu_adr_default or icpu_adr_select) if (icpu_adr_select) icpu_adr_o = icpu_adr_boot ; //icpu_adr_select在复位的时候为1,所 //以icpu_adr_o在复位的时候 //值为OR1200_BOOT_ADR,即0x100 else icpu_adr_o = icpu_adr_default ;
从上述代码可知在复位的时候IMMU的输出信号icpu_adr_o为0x100,所以复位的时候GENPC的输入icpu_adr_i就是0x100。此时GENPC的输出icpu_adr_o等于icpu_adr_i,所以GENCP的输出icpu_adr_o是0x100。
通过上面的分析可知在复位的时候GENPC的输出icpu_adr_o为0x100、icpu_cycstb_o为1,对照Wishbone总线的规范可知,也就是主设备的输出CYCSTB_O有效,表示总线操作开始,主设备的输出ADR_O为0x100。
此外在GENPC模块内部也有一些信号变化,其代码如下。
or1200_genpc.v always @(posedge clk or `OR1200_RST_EVENT rst) if (rst == `OR1200_RST_VALUE) begin pcreg_default <= `OR1200_BOOT_PCREG_DEFAULT; //复位的时候pcreg_default为0x39 pcreg_select <= 1'b1;// select async. value due to reset state end else if (pcreg_select) begin pcreg_default <= pcreg_boot[31:2]; pcreg_select <= 1'b0; // select FF value end else if (spr_pc_we) begin pcreg_default <= spr_dat_i[31:2]; end else if (no_more_dslot | except_start | !genpc_freeze & !icpu_rty_i & !genpc_refetch) begin pcreg_default <= pc[31:2]; end assign pcreg_boot = `OR1200_BOOT_ADR; //就是0x100 always @(pcreg_boot or pcreg_default or pcreg_select) if (pcreg_select) //复位的时候pcreg_select为 //1,所以pcreg为pcreg_boot //[31:2],也就是0x40 pcreg = pcreg_boot[31:2]; else pcreg = pcreg_default ; always @(pcreg or ex_branch_addrtarget or flag or branch_op or except_type or except_start or operand_b or epcr or spr_pc_we or spr_dat_i or except_prefix) begin casez ({spr_pc_we, except_start, branch_op}) // synopsys parallel_case {2'b00, `OR1200_BRANCHOP_NOP}: begin pc = {pcreg + 30'd1, 2'b0}; //复位的时候pc为{0x40+1,00}, //也就是0x104 ex_branch_taken = 1'b0; end ……
ModelSim仿真波形如图3.15所示,可验证复位信号有效阶段GENPC的信号确如上面的分析。
图3.15 ModelSim仿真显示GENPC模块在复位信号有效阶段的信号值
2.IF模块的信号变化
QMEM的输出数据送到IF的icpu_dat_i,但该数据不一定就是下一步需要执行的指令,在IF模块中变量if_insn表示下一步需要执行的指令,其定义如下。
or1200_if.v assign if_insn = no_more_dslot | rfe | if_bypass ? {`OR1200_OR32_NOP, 26'h041_0000} : saved ? insn_saved : icpu_ack_i ? icpu_dat_i : {`OR1200_OR32_NOP, 26'h061_0000};
在复位的时候no_more_dslot、rfe、if_bypass和saved都为0,读者可以自行分析原因,icpu_ack_i来自QMEM的输出qmemicpu_ack_o,通过分析,在复位阶段icpu_ack_i的值最终由WB_BIU模块中的icbiu_ack_o确定,如图3.16所示。
图3.16 GENPC模块的icpu_ack_i信号与WB_BIU有关
在复位的时候WB_BIU模块icbiu_ack_o为0,所以IF的输入icpu_ack_i为0。从而if_insn为{`OR1200_OR32_NOP, 26’h061_0000},这里OR1200_OR32_NOP的值就是6’b000101,所以if_insn为0x14610000。
综合对GENPC、IF模块的分析,可以得出在复位信号有效阶段,CPU、QMEM之间的指令Wishbone总线的信号如图3.17所示,此处还是从主设备的角度观察信号,即观察CPU侧的信号情况。从图中可以了解到总线操作周期开始了(CYCSTB_O为1),地址也送出去了,但是还没有收到从设备的响应(ACK_I为0)。
图3.17 复位信号有效阶段CPU、QMEM之间Wishbone总线的信号
3.QMEM模块的信号变化
对QMEM模块重点分析其内部RAM的接口信号,如3.4节的分析,主要是qmem_en、qmem_we和qmem_addr。
or1200_qmem_top.v assign qmem_en = iaddr_qmem_hit & qmemimmu_cycstb_i | daddr_qmem_hit & qmemdmmu_cycstb_i; //在最小系统中iaddr_qmem_hit始终为1,同时从上面的 //分析可知qmemimmu_cycstb_i为1,所以qmem_en为1 assign qmem_we = qmemdmmu_cycstb_i & daddr_qmem_hit & qmemdcpu_we_i; assign qmem_addr=(qmemdmmu_cycstb_i&daddr_qmem_hit)?qmemdmmu_adr_i:qmemimmu_adr_i;
qmem_we、qmem_addr的值取决于qmemdmmu_cycstb_i的值,从图3.11中可知该信号连接到CPU内部的LSU模块的dcpu_cycstb_o,在LSU中有如下代码。
or1200_lsu.v assign dcpu_cycstb_o = du_stall | lsu_unstall | except_align ? 1'b0 : |ex_lsu_op; //这里du_stall是与Debug有关的信号,一般情况下为0,lsu_unstall与 //except_align在复位的时候都为0,ex_lsu_op也为0,所以dcpu_cycstb_o为0 always @(posedge clk or `OR1200_RST_EVENT rst) begin if (rst == `OR1200_RST_VALUE) ex_lsu_op <= `OR1200_LSUOP_NOP; //复位的时候ex_lsu_op为0 ……
从上面的代码可知在复位的时候dcpu_cycstb_o为0,所以QMEM中RAM的qmem_we为0,qmem_addr为qmemimmu_adr_i,即指令Wishbone总线输入的地址,此时是0x100。
3.5.2 复位信号无效后的第一个时钟周期上升沿
1.GENPC模块的信号变化
复位信号无效指的是OR1200的输入rst为0的时期。
参考3.5.1节中的内容,此时pcreg_select变为0。
2.IF模块的信号变化
IF模块中有变化的信号不影响系统执行,暂时不考虑。
3.QMEM模块的信号变化
在QMEM中的变量state会变为OR1200_QMEMFSM_FETCH,同时qmem_icak为1。
or1200_qmem_top.v always @(`OR1200_RST_EVENT rst or posedge clk) if (rst == `OR1200_RST_VALUE) begin state <= `OR1200_QMEMFSM_IDLE; qmem_dack <= 1'b0; qmem_iack <= 1'b0; end else case (state) // synopsys parallel_case `OR1200_QMEMFSM_IDLE: begin …… else if (qmemimmu_cycstb_i & iaddr_qmem_hit & qmem_ack) begin //qmem_ack始终为1 state <= `OR1200_QMEMFSM_FETCH; qmem_iack <= 1'b1; qmem_dack <= 1'b0; end end ……
在上述分析知道QMEM内部RAM的片选信号ce为1,地址信号addr为qmem_addr[12:2],也就是0x40,写信号we为0,所以此时会寄存地址信号,addr_reg变为0x40。
or1200_spram_2048x32.v
always @(posedge clk or `OR1200_RST_EVENT rst)
if (rst == `OR1200_RST_VALUE)
addr_reg <= {aw{1'b0}};
else if (ce)
addr_reg <= addr; //addr_reg寄存地址信号,变为0x40
3.5.3 复位信号无效后的第一个时钟周期的组合逻辑阶段
1.QMEM模块的信号变化
QMEM的内部RAM会给出第一条指令,位于mem[0x40]处。
or1200_spram_2048x32.v
assign doq = (oe) ? mem[addr_reg] : {dw{1'b0}}; //此时的addr_reg为0x40
由QMEM内部RAM的例化语句可知doq连接到QMEM的qmem_do,并且此时qmem_iack为1,所以qmemicpu_dat_o为qmem_do,也就是第一条指令的值,同时qmemicpu_ack_o为1。
or1200_qmem_top.v assign qmemicpu_dat_o = qmem_iack? qmem_do : icqmem_dat_i; assign qmemicpu_ack_o = qmem_iack ? 1'b1 : icqmem_ack_i; assign qmemimmu_rty_o = qmem_iack ? 1'b0 : icqmem_rty_i; assign qmemimmu_err_o = qmem_iack ? 1'b0 : icqmem_err_i; assign qmemimmu_tag_o = qmem_iack ? 4'h0 : icqmem_tag_i;
参考图3.8可知qmemicpu_ack_o输出到IF模块的接口icpu_ack_i,qmemicpu_dat_o输出到IF模块的接口icpu_dat_i。
2.IF模块的信号变化
IF模块的icpu_ack_i为1,会将QMEM读出的第一条指令icpu_dat_i赋值给变量if_insn,此时从Wishbone总线的角度观察,icpu_ack_i为1 表示从设备响应了主设备,图3.18 是ModelSim仿真显示此时指令Wishbone总线的信号。
or1200_if.v assign if_insn = no_more_dslot | rfe | if_bypass ? {`OR1200_OR32_NOP, 26'h041_0000}:saved?insn_saved:icpu_ack_i?icpu_dat_i: {`OR1200_OR32_NOP, 26'h061_0000}; //此时icpu_ack_i为1,icpu_dat_i就是QMEM的输出
图3.18 复位信号无效后的第一个时钟周期CPU、QMEM之间Wishbone总线信号
3.GENPC模块的信号变化
参考3.5.1节中的内容,此时pcreg变为0x40,pc还是0x104,icpu_rty_o为0,不考虑ex_branch_taken、spr_pc_we,认为两者都是0,所以icpu_adr_o变为pc的值,代码如下。
or1200_genpc.v assign icpu_adr_o = !no_more_dslot & !except_start & !spr_pc_we & (icpu_rty_i genpc_refetch) ? icpu_adr_i : {pc[31:2], 1'b0, ex_branch_taken|spr_pc_we};
icpu_adr_o会变为0x104,也就是第二条指令的地址。图3.19给出了通过ModelSim仿真得到的复位信号无效后第一个时钟周期的信号变化。通过分析可知在这第一个时钟周期完成了第一条指令的获取,同时也做好了获取下一条指令的准备。
图3.19 ModelSim仿真显示在复位信号无效后的第一个时钟周期取到第一条指令
3.6 第二条及后续指令的读取过程分析
3.6.1 复位信号无效后的第二个时钟周期的上升沿
1.GENPC模块的信号变化
参考3.5.1 节中的代码,此时icpu_cycstb_o保持为1(复杂情况我们暂不讨论),pcreg_default变为pc,也就是0x104,pcreg也变为0x104,pc加4,变为0x108,是第三条指令的地址。从这里我们可以归纳得出:GENPC模块的变量pc,始终保存着下一条指令的地址,而pcreg保存着刚刚取到的指令地址,该指令还没有进入流水线的译码阶段。
2.IF模块的信号变化
变化的信号不影响系统的执行,暂时不考虑。
3.QMEM模块的信号变化
在QMEM中,变量state会保持为OR1200_QMEMFSM_FETCH,同时qmem_iack保持为1。
or1200_qmem_top.v always @(`OR1200_RST_EVENT rst or posedge clk) if (rst == `OR1200_RST_VALUE) begin state <= `OR1200_QMEMFSM_IDLE; qmem_dack <= 1'b0; qmem_iack <= 1'b0; end else case (state) // synopsys parallel_case `OR1200_QMEMFSM_FETCH: begin …… else if (qmemimmu_cycstb_i & iaddr_qmem_hit & qmem_ack) begin //qmem_ack始终为1 state <= `OR1200_QMEMFSM_FETCH; qmem_iack <= 1'b1; qmem_dack <= 1'b0; end end ……
将地址信号addr寄存到变量addr_reg,此时addr为0x104。
or1200_spram_2048x32.v
always @(posedge clk or `OR1200_RST_EVENT rst)
if (rst == `OR1200_RST_VALUE)
addr_reg <= {aw{1'b0}};
else if (ce)
addr_reg <= addr//此时addr_reg为0x104
3.6.2 复位信号无效后的第二个时钟周期的组合逻辑阶段
同3.5.3节。
再往后取第三条、第四条等后续指令都是一样的过程。这就是一种理想的取指情况:即icpu_adr_o值依次增加,也就依次读出QMEM中的指令,这也是Wishbone总线规范中块读周期的过程,如图3.20所示。
图3.20 理想的取指过程
但实际情况是复杂的,比如:在上面的分析中认为GENPC的icpu_cycstb_o始终为1,重复列出该信号定义如下。
or1200_genpc.v assign icpu_cycstb_o = ~(genpc_freeze | (|pre_branch_op && !icpu_rty_i));
当genpc_freeze为1,或者pre_branch_op不为0时,都会导致icpu_cycstb_o为0,此时指令Wishbone总线就处于无效状态,也就不会读出指令。在分析转移指令的时候就会发生这种情况。
再比如:在上面分析的时候认为icpu_adr_o值依次增加,在每个时钟加4,但实际上icpu_adr_o的值是一个复杂的变量,受很多因素的影响,重复列出该信号如下。
or1200_genpc.v assign icpu_adr_o = !no_more_dslot & !except_start & !spr_pc_we & (icpu_rty_i | genpc_refetch) ? icpu_adr_i : {pc[31:2], 1'b0, ex_branch_taken|spr_pc_we};
很多因素我们都没有考虑,是不是在这里考虑所有的因素呢?这样看起来似乎会更加完善。笔者不这么认为,OR1200是一个系统,一个整体,各个模块之间的联系很紧密,相互影响,比如:在3.5.1节分析icpu_rty_i值的时候就要通过多个模块才能确定其值。此处如果考虑所有的情况,比如:列出什么时候icpu_adr_o加4,什么时候不加4,什么时候保持不变,这样做的工作量实在是很庞大,笔者没有这个能力保证所有的读者都听得懂,所以此处只给出了一种理想情况,也是OR1200大部分时候的运行情况,在后面当分析到某些指令、某些场景的时候,会具体分析这些指令、场景是如何影响这个理想的取指过程的。比如:当我们分析系统调用指令l.sys的时候,就会介绍l.sys对这个取指过程的影响,分析转移指令l.bf的时候就会介绍l.bf对这个取指过程的影响,分析特殊寄存器读指令l.mfspr的时候就会介绍l.mfspr对这个取指过程的影响。通过这样一步一步的探索,由简到繁,由不求甚解到全面了解,最后揭示得出结论,而不是直接给出结论,再分析得出这个结论的理由。笔者认为前者更符合我们认识事物的规律。