章将实现MIPS32指令集架构中定义的加载存储指令,分两步:首先实现除ll、sc指令外的一般加载存储指令,其次实现比较特殊的加载存储指令ll、sc。
9.1加载存储指令说明
MIPS32指令集架构中定义的加载存储指令共有14条,如下:
--8条加载指令:lb、lbu、lh、lhu、ll、lw、lwl、lwr
--6条存储指令:sb、sc、sh、sw、swl、swr
9.1.1加载指令lb、lbu、lh、lhu、lw说明
这5条加载指令可以根据指令中26~31bit的指令码加以区分,另外,加载指令的第0~15bit是offset、第21~15bit是base,加载地址的计算方法如下,先将16位的offset符号扩展至32位,然后与地址为base的通用寄存器的值相加,即可得到加载地址。
加载地址 = signed_extended(offset) + GPR[base]
9.1.2存储指令sb、sh、sw说明
从图9-2可知,这3条存储指令可以根据指令中26~31bit的指令码加以区分,另外,存储指令的第0~15bit是offset、第21~25bit是base,存储地址的计算方法如下,先将16位的offset符号扩展至32位,然后与地址为base的通用寄存器的值相加,即可得到存储地址。
存储地址 = signed_extended(offset) + GPR[base]
9.1.3加载存储指令用法示例
OPenMIPS处理器是按照字节寻址,并且是大端模式,在这种模式下,数据的高位保存在存储器的低地址中,而数据的低位保存在存储器的高地址中。比如:使用指令sb在0x50处存储0x81,存储器的实际存储效果如图所示。
9.1.4加载指令lwl、lwr说明
9.1.5存储指令swl、swr说明
2加载存储指令实现思路
节介绍除了ll、sc之外的加载存储指令的实现思路,ll、sc指令的实现思路将在9.7节专题介绍
1.加载指令实现思路
加载指令在译码阶段进行译码,得到运算类型alusel_o、aluop_o,以及要写的目的寄存器信息。这些信息传递到执行阶段,然后又传递到访存阶段,访存阶段依据这些信息,设置对数据存储器RAM的访问信号。从RAM读取回来的数据需要按照加载指令的类型、加载地址进行对齐调整,调整后的结果作为最终要写入的目的寄存器的数据。
2.存储指令实现思路
存储指令在译码阶段进行译码,得到运算类型alusel_o、aluop_o,以及要存储的数据,这些信息传递到执行阶段,然后又传递到访存阶段,访存阶段依据这些信息,设置对数据存储器RAM的访问信号,将数据写入RAM。
需要特别注意的是:本章假设可以在一个时钟周期内完成对外部数据存储器RAM的读写操作,在后续章节实现实践版OpenMIPS处理器的时候会考虑复杂情况。
9.2.1数据流图的修改
为了实现除ll、sc之外的加载存储指令,修改数据流图。主要是在访存阶段增加了对数据存储器RAM的访问,同时,由于要写入目的寄存器的数据可能是执行阶段的结果,也可能是在访存阶段从数据存储器RAM加载得到的数据,所以在访存阶段增加了一个多路选择器,进行选择。
9.2.2系统结构的修改
为了实现除ll、sc之外的加载存储指令,需要对系统结构进行修改,增加部分模块的接口。
主要修改内容如下。
(1)译码阶段的ID模块增加了输出信号inst_o,其值就是处于译码阶段的指令,该信号会传递到执行阶段,在执行阶段的EX模块会利用该信号的值计算加载、存储地址mem_addr_o.
(2)执行阶段的EX模块将运算子类型aluop_o、加载存储地址mem_addr_o、读取的第二个操作数reg2_o等信息,通过EX/MEM模块传递到访存阶段的MEM模块。
(3)访存阶段的MEM模块依据加载、存储指令的类型,确定对数据存储器RAM的访问信息,通过mem_ce_o接口送出数据存储器使能信号,mem_addr_o接口送出访问地址,mem_we_o接口指出是加载还是存储操作、mem_sel_o接口送出字节选择信号,如果是存储指令,那么还通过mem_data_o接口输出要存储的数据,如果是加载指令,那么会从mem_data_i接口获得读取到的数据,然后MEM模块依据具体的加载指令类型、加载地址,对获取的数据进行对齐调整,最终得到要写入目的寄存器的数据。
9.3修改OpenMIPS以实现加载存储指令
9.3.1修改译码阶段
1.修改ID模块
ID模块要增加接口inst_o.在ID模块还要增加对加载存储指令的分析,这些指令的指令码都是不同的,所以可以直接依据指令码确定是哪一种指令
译码工作主要是确定要写的目的寄存器、要读取的寄存器和要执行的运算三个方面。以下对几个有代表性的指令的译码过程进行说明。
(1)lb指令
--要写的目的寄存器:加载指令lb需要将加载结果吸入目的寄存器,所以设置wreg_o为WriteEnable,同时可知,要写的目的寄存器地址是指令中的第16~20bit,所以设置wd_o为inst[20:16]。
--要读取的寄存器:计算加载目标地址需要使用地址为base的寄存器值,所以设置reg1_read_o为1,表示通过RegFile模块的读端口1读取寄存器的值,默认读取的寄存器地址reg1_addr_o是指令的第21~25bit,正是lb指令中的base。所以最终译码阶段的输出reg1_o就是地址为base的寄存器的值。
--要执行的运算:设置alusel_o为EXE_RES_LOAD_STORE,表示运算类型是加载存储,设置aluop_o为EXE_LB_OP,表示运算子类型是字节加载lb.
lbu、lh、lhu、lw指令与lb指令的译码过程类似,知识aluop_o的值不同。
(2)lwl指令
--要写的目的寄存器:加载指令lwl需要将加载结果吸入目的寄存器,所以设置wreg_o为WriteEnable,同时可知,要写的目的寄存器地址是指令中的第16~20bit,所以设置wd_o为inst[20:16]。
--要读取的寄存器:计算加载目标地址需要使用地址为base的寄存器值,所以设置reg1_read_o为1,表示通过RegFile模块的读端口1读取寄存器的值,默认读取的寄存器地址reg1_addr_o是指令的第21~25bit,正是lwl指令中的base。所以最终译码阶段的输出reg1_o就是地址为base的寄存器的值。此外,由于lwl指令只是部分地修改目的寄存器,因此,设置reg2_read_o也为1表示通过RegFile模块的读端口2读取寄存器的值,默认读取的寄存器地址reg2_addr_o是指令的第16~20bit,正是lwl指令中的rt。所以最终译码阶段的输出reg2_o就是地址为rt的寄存器的值。
--要执行的运算:设置alusel_o为EXE_RES_LOAD_STORE,表示运算类型是加载存储,设置aluop_o为EXE_LWL_OP,表示运算子类型是字节加载lwl.
lwr指令与lwl指令的译码过程类似,只是aluop_o的值不同。
(3)sb指令
--要写的目的寄存器:加载指令sb需要将加载结果吸入目的寄存器,所以设置wreg_o为WriteEnable,同时可知,要写的目的寄存器地址是指令中的第16~20bit,所以设置wd_o为inst[20:16]。
--要读取的寄存器:计算加载目标地址需要使用地址为base的寄存器值,所以设置reg1_read_o为1,表示通过RegFile模块的读端口1读取寄存器的值,默认读取的寄存器地址reg1_addr_o是指令的第21~25bit,正是sb指令中的base。所以最终译码阶段的输出reg1_o就是地址为base的寄存器的值。要存储的值是通用寄存器的值,因此,设置reg2_read_o也为1表示通过RegFile模块的读端口2读取寄存器的值,默认读取的寄存器地址reg2_addr_o是指令的第16~20bit,正是sb指令中的rt。所以最终译码阶段的输出reg2_o就是地址为rt的寄存器的值。
--要执行的运算:设置alusel_o为EXE_RES_LOAD_STORE,表示运算类型是加载存储,设置aluop_o为EXE_SB_OP,表示运算子类型是字节加载sb.
sh、sw、swr、swl指令与sb指令的译码过程类似,只是aluop_o的值不同。
2.修改ID/EX模块
ID/EX模块需要增加部分接口,用于将ID模块新增加的输出信号inst_o传递到执行阶段的EX模块。
9.3.2修改执行阶段
1.修改EX模块
在执行阶段的EX模块会计算加载存储的目的地址,参考图9-19可知,EX模块会增加部分接口,如表9-3所示。
2.修改EX/MEM模块
参考图9-19可知,EX/MEM模块会增加部分接口,用于EX模块新增的输出传递到访存阶段
9.3.3修改访存阶段
访存阶段主要是修改MEM模块,参考图9-19可知,需要为其添加对数据存储器RAM的访问接口。
此处对mem_sel_o做进一步说明,分加载、存储两种操作分别说明。
(1)对于加载操作,MIPS32指令集架构中定义的加载指令可以加载字节、半字、字,但是数据总线的宽度是32位,占4个字节。如果执行加载字节指令lb、lbu,那么就要知道通过数据总线输入的4个字节中,哪个字节是要读取的数据:如果执行加载半字指令lh、lhu,那么就要知道哪个半字是要读取的数据,mem_sel_o的作用就是指出哪一部分是有效数据。mem_sel_o的宽度为4,分别对应数据总线的4个字节,比如:使用加载指令lb读取数据存储器地址0x1出的字节,那么可以设置mem_sel_o为4'b0100,意思就是,希望外部存储器在输出数据时,将地址0x1处的字节放在32位数据总线的次高字节,也就是第16~23bit的位置,当数据送到处理器时,处理器就取出其中第16~23bit对应的字节,作为数据存储器地址0x1处的值。
(2)对于存储操作,MIPS32指令集架构中定义的存储指令可以存储字节、半字、字,但数据总线的宽度是32位,占4个字节,如果执行字节存储指令sb、半字存储指令sh,那么外部数据存储器就要知道用过数据总线传递过来的4个字节中,哪个字节、哪个半字是要存储的数据,mem_sel_o作用就是指出哪一部分是要存储的有效数据。比如:使用存储指令sh向地址0x2处存储0x8281,那么可以设置mem_data_o为0x82818281、设置mem_sel_o为4'b0011,这样外部存储器就知道要存储的数据是0x82818281的最低两个字节,正是0x8281.
下面对其中几个典型指令的访存过程进行解释
(1)因为要访问数据存储器,所以设置mem_ce_o为ChipEnable
(2)因为是加载操作,所以设置mem_we_o为WriteDisable
(3)给出要访问的数据存储器地址mem_addr_o, 其值就是执行阶段计算出来的地址mem_data_i.
(4)依据mem_addr_i的最后两位,确定mem_sel_o的值,并据此从数据存储器的输入mem_data_i中获得要读取的字节,进行符号扩展。比如:如果mem_addr_i的最后两位是01,那么设置mem_sel_o为4'b0100,表示希望输出存储器给出的数据的第16~23bit就是要读取的字节,也就是mem_data_i[23:16],将其最高位进行符号扩展,得到最终结果wdata_o,作为要写入目标寄存器的数据。
2.lwl指令的访存过程
(1)因为要访问数据存储器,所以设置mem_ce_o为ChipEnable
(2)因为是加载操作,所以设置mem_we_o为WriteDisable
(3)给出要访问的数据存储器地址mem_addr_o, 其值就是执行阶段计算出来的地址mem_data_i.,但最后两位要设置为0,因为lwl指令要从RAM中读出一个字,所以需要将地址对齐,同时设置mem_sel_0为4'b1111。
(4)依据mem_addr_i的最后两位,将从数据存储器读取的数据mem_data_i与目的寄存器的原始值reg2_i进行组合,得到最终要写入目的寄存器的值wdata_o,
3.sb指令的访存过程
(1)因为要访问数据存储器,所以设置mem_ce_o为ChipEnable.
(2)因为是存储操作,所以设置mem_we_o为WriteEnable。
(3)给出要访问的数据存储器地址mem_addr_o,其值就是执行阶段计算出来的地址mem_addr_i。
(4)sb指令要写入的数据是寄存器的最低字节,将该字节复制到mem_data_o的其余部分,然后依据mem_data_i的最后两位,确定mem_sel_o的值。
4.swl指令的访存过程。
9.4修改最小SOPC
为了验证上一节添加的加载存储指令是否实现正确,需要修改在第四章中设计的最小SOPC,为其添加数据存储器RAM。
9.4.1添加数据存储器
9.6链接加载指令ll、条件存储指令sc说明
ll、sc指令是MIPS32指令MIPS32指令集架构中比较特殊的加载存储指令,用来实现信号量机制。在多线程系统中,需要RMW(Read-Mpdify-Write)操作序列保证对某个资源的独占性,RMW操作序列的含义是,读取内存某个地址的数据,读取饿得数据经过修改,然后再保存回内存原地址,这个过程不能有任何干扰,因此需要建立一个临界区域(Critical Region), 临界区域中完成的操作通常称为原子操作,原子操作不能被打扰。操作系统建立邻接区域的方式通常是信号量机制,如下。
wait(semaphore);
原子操作
signal(semaphore);
semaphore是一个信号量,为1表示信号量使用中,为0表示信号量空闲。进行原子操作前,使用wait函数查询semaphore的值,如果为1,则等待,否则,将其置为1,开始执行原子操作。操作结束后,signal函数将semaphore置为0,这样其他线程就可以执行原子操作了。
需要注意的是,wait函数的执行也是一个原子操作,是一种“先检测后设置”的操作(tesr-and -set operation),这种操作一般不希望被外部设备中断,也不希望被其他线程打断,很多处理器都有专门的指令用来实心"先检测后设置"操作。这也是一种信号量机制。
MIPS32架构采用特殊的方式实现信号量机制,对于原子操作,MIPS32架构并不保证它一定是原子性的,也就是允许检测和设置在没有原子性保证的情况下运行,但只在它确实是原子的运行了的时候才能让“设置”生效。MIPS32架构采用链接加载指令ll、条件存储指令sc来实现这种信号量机制。
ll指令同一般的加载指令一样,从内存中加载一个字,但是,有点不同 ,ll指令还会将处理器内部的一个链接状态位LLbit置为1,表明发生了一个链接加载操作,并将链接的地址保存到一个特殊寄存器LLAddr中。
ll指令执行完毕后,会进行一定的操作(如:修改加载得到的数据),然后执行sc指令,这可以认为是一个RMW序列。有如下两种情况干肉这个RMW序列,受到干扰后,处理器会设置链接状态位LLbit为0
--在ll、sv指令之间产生异常,从而进入异常处理例程,或者发生线程切换,熬制RMW徐磊受到干扰。
--多处理器的系统中,另一个CPU改写了RMW序列要操作的内存空间。
对于OpenMIPS而言,只有第1种情况。
执行sc指令时,会对从ll指令开始的RMW序列进行检查,判断是否受到干扰,实际就是判断LLbit是否wie1,如果没有受到任何干扰,LLbit保持为1,那么操作是原子的,sc指令会对ll指令加载数据的地址进行写回操作,并设置一个通用寄存器的值为1,表示成功,反之不进行写回操作,并设置一个通用寄存器的值为0,表示失败。
9.7ll、sc指令实现思路
9.7.2数据流图的修改
9.7.3系统结构的修改
9.8修改OpenMIPS以实现ll、sc指令
9.8.2修改译码阶段的ID模块
9.8.3修改访存阶段