设计一个简单的单周期CPU
1.1 设计单周期 CPU 时的总体思路
设计输入是指令系统规范,设计输出是一个数字逻辑电路,这个数字逻辑电路能够实现指令系统规范所定义的各项功能。
1.1.1 指令系统规范
指令系统规范是指令系统的规范文件,规范性文档是很严谨的,包含大量的细节,一般没有废话,因此建议:反复多看几遍,把正文、注解中的每个字、每个上下标、每个符号都看到。
1.1.2 CPU一般性设计方法
从宏观来看,设计一个CPU就是设计它的数据通路+控制逻辑
基本方法:对指令系统中定义的指令进行功能的分解,得到一系列操作和操作对象,他们对应各自的数据通路。因为指令间存在一些相同或相近的操作和操作对象,我们可以只涉及一套相应的数据通路公用,遇到无法共享的,就各自一套,用多路选择器从中选择出所需的结果。
1.2 单周期 CPU 的数据通路设计
1.2.1 ADDU 指令
pc:
PC 的输入目前看来有两个:一个是复位值 0xBFC00000,
一个是复位撤销之后每执行完一条指令更新为当前 PC+4(4代表寻址4个字节,即一个指令的宽度)。
虚实地址转换:
任何时候,CPU上运行的程序中出现的地址都是虚地址,而CPU本身访问内存,I/O所用的都是物理地址
指令 RAM:
RAM 进一步分拆为指令 RAM 和数据 RAM 两块物理上独立的 RAM 以简化设计
异步读的指令RAM:
“同步读 RAM”即第 1拍发读请求和读地址,第 2拍 RAM才会输出读数据。
“异步读 RAM”的读时序行为类似于寄存器堆的读,当拍给地址当拍出数据;其写时序行为和同步读 RAM 的一样。
尽管指令 RAM暂时实现为异步读的行为,但是我们还是要为其保留一个读使能输入端口。这是为了后面实现流水线 CPU 将 RAM 更换为同步 RAM 时接口的统一。
指令定义分析
ADDU指令的定义:
上面的表格定义了该指令的编码格式:ADDU指令码的 第31..26 位(op 域)必须是 0b000000(前导字符“0b”表示后续是二进制数),
第 5..0 位(func 域)必须是 0b100001,
第 10..6 位(sa 域)必须是 0b00000。
一旦指令码的这三个域满足这三个值,那么这条指令就是 ADDU指令。
此时,指令码的 25..21位的数值表示的是 rs寄存器号,
20..16 位的数值表示的是 rt寄存器号,
15..11 位的数值表示的是 rd 寄存器号。
通用寄存器堆
在做 CPU 顶层设计时,将通用寄存器堆作为一个子模块看待在做 CPU 顶层设计时,将通用寄存器堆作为一个子模块看待。
加法器
将加法器作为一个子模块看待:
我们将通用寄存器堆读端口1的输入rdata(rs寄存器的值)连接到加法器的src1,将通用寄存器堆读端口2的输入rdata2(rt寄存器的值)连接到加法器的src2,将加法器result的输入连接到通用寄存器堆写数据端口wdata
1.2.2 ADDIU指令
ADDIU 和 ADDU 的指令编码定义是可区分的,将利用这一信息来产生多路选择器的控制信号。
ADDU 指令的求和结果写到第 rd 号通用寄存器中,而 ADDIU 指令的求和结果写入到第 rt 号通用寄存器中。
1.2.3 SUBU 指令
因为补码减法运算有以下属性:
对加法器的源操作数 2 输入和进位输入分别添加“二选一”,使补码加法器既能做加法也能做减法
源操作 2 输入在处理加法时是 src2,在处理减法时是 src2 按位取反;
进位输入在处理加法时是 0,在处理减法时是 1。
1.2.4 LW指令
在执行方面的功能:
(1)将基址寄存器rs的值和指令码中的立即数offset相加,得到虚地址
(2)将虚地址vaddr通过虚实地址映射得到物理地址paddr
(3)根据paddr,从内存中读出数据
数据 RAM
由于 LW 指令每次要访问一个 32 位宽的字,我们将数据RAM的宽度定为32比特,此时数据RAM 的地址输入就是
访存物理地址除以4取下整的值,数据RAM的数据输出即为LW指令执行的结果.
1.2.6 BEQ(相等跳转) 和 BNE(不相等跳转) 指令
分支指令具有如下三个功能要点:
(1)判断分支条件,决定是否跳转:
(实现独立的分支判断比较逻辑)
(2)计算跳转目标;
(3)如果跳转,则修改取指 PC 为跳转目标,否则 PC 加 4。
转移延迟槽指令位置是紧跟在转移指令之后,位于这个位置的指令有一个特点是,无论其对应的转移指令跳转与否,这条指令一定执行。
PC 更新:
由于转移延迟槽指令的存在,转移指令判断出需要跳转的结果后,不能立即将生成nextPC
的“二选一”的选择输入立刻置为 1,而是要将这个是否跳转的结果用触发器存下来,
等到执行该转移指令的延迟槽指令时,再根据存下来的跳转结果置生成 nextPC
“二选一”的选择输入
1.2.7 JAL(Jump And Link)指令:
特点:
(1)不用进行分值条件判断,一定跳转;
(2)跳转目标地址通过转移延迟槽指令的PC与指令吗中立即数计算得到,采用拼接的计算方式
(3)通用寄存器和PC均要修改
高级语言中的函数调用会引入 call 和 return 两个跳转。call 跳转到被调用函数的入口,return(只能动态确定回跳目标) 回跳到调用点后面的那条指令。
在MIPS用带“link操作”的指令完成call操作,link操作会将该跳转延迟槽指令中PC+4写入通用寄存器,让进行return功能时直接取值即可
1.2.8 JR指令
特点:
(1)无需判断,必然跳转
(2)跳转的目标地址来自于通用寄存器堆的第rs项
1.2.9 SLT 和 SLTU 指令
复用“加法器”来实现 SLT 和 SLTU 的运算逻辑。
先通过复用“加法器”进行 GR[rs]-GR[rt]的运算,
然后根据源操作数的正负、和(Sum)的正负和进位(Cout)的正负,就可以得到SLT和SLTU的结果了。
这些结果和原有的“加法器”结果通过一个“二选一”得到运算类指令的执行结果,
然后输入到产生最终写通用寄存器值的那个“二选一”的输入上。
这样,SLT 和 SLTU 所需的数据通路就设计好了。
1.2.10 SLL、SRL和SRA指令(逻辑左移、逻辑右移和算术右移)
源操作数均有两个:通用寄存器堆的第rt项,指令码中sa域的数值;
结果均写到通用寄存器对的rd项
移位器:
“移位器”有两个数据输入: 32 位的被移位数值 src 和 5 位的移位量 sa,
还有一个控制输入用于确定移位操作的类型 op,以及最终输出的 32 位移位结果 res。
内部实现:
最直接方法:
shft_src << shft_amt”、“shft_src >> sfht_amt”和“$signed(shft_src) >>> $signed(shft_amt)”分别描述逻辑左移、逻辑右移和算术右移的逻辑,
然后通过三选一根据shft_op得出结果shft_res
介绍一种面向面积优化的电路设计,其基本思想是将被移位的数据逆序排列后,左移操作就被转换为右移操作。具体的实现示意如下:(若主频要求不高或移位器逻辑不位于整个 CPU 的关键路径上,显然这种实现更好。)
assign shft_src = op_srl ? {
src[ 0], src[ 1], src[ 2], src[ 3],
src[ 4], src[ 5], src[ 6], src[ 7],
src[ 8], src[ 9], src[10], src[11],
src[12], src[13], src[14], src[15],
src[16], src[17], src[18], src[19],
src[20], src[21], src[22], src[23],
src[24], src[25], src[26], src[27],
src[28], src[29], src[30], src[31]
}: src[31:0];
assign shft_res = shft_src[31:0] >> shft_amt[4:0];
assign sra_mask = ~(32’hffffffff >> shft_amt[4:0]);
assign srl_res = shft_res;
assign sra_res = ({32{src[31]}} & sra_mask) | shft_res;
assign sll_res = {
shft_res[ 0], shft_res[ 1], shft_res[ 2], shft_res[ 3],
shft_res[ 4], shft_res[ 5], shft_res[ 6], shft_res[ 7],
shft_res[ 8], shft_res[ 9], shft_res[10], shft_res[11],
shft_res[12], shft_res[13], shft_res[14], shft_res[15],
shft_res[16], shft_res[17], shft_res[18], shft_res[19],
shft_res[20], shft_res[21], shft_res[22], shft_res[23],
shft_res[24], shft_res[25], shft_res[26], shft_res[27],
shft_res[28], shft_res[29], shft_res[30], shft_res[31]
};
1.2.11 LUI、AND、OR、XOR 和 NOR 指令
除了LUI外的指令均是按位逻辑运算指令
所有指令操作模式:均是将通用寄存器堆第rs项的值和通用寄存器堆第rt项的值按位进行相应的逻辑运算后
结果写入寄存器的第rd项。
LUI 的具体运算,其实就是低 16 位立即数与 16 比特 0 拼接起来
1.3 单周期 CPU 的控制信号生成
从 PC 开始沿着数据通路梳理所有的控制信号:
(1)PC 的输入生成逻辑 GenNextPC
中包含一个“四选一”,其四个输入依次是:
in0 对应顺序取指的 PC(即 PC+4),
in1 对应 PC 加上保存下来的分支指令的 offset,
in2 对应 PC 高位拼接上保存下来的直接跳转指令的 instr_index,
in3 对应通用寄存器堆读端口 1 的数据读出。
将该“四选一”的选择信号` sel_nextpc` 设计为 #“零-独热码”#
即信号宽 4 个比特,每个比特对应一个数据输入,任何合法的选择信号中至多有一个比特置为 1。
(2)指令 RAM 的片选信号inst_ram_en
,高电平有效。对于实现单周期CPU时每周期都能执行完一条的指令,只要CPU的复位撤销,该信号恒为1
(3)指令 RAM 的写使能信号inst_ram_wen
,高电平有效。
(4)ALU 的源操作数输入alu_src1
的生成逻辑 GenALUSrc1
中包含一个“三选一”,其三个输入依次是:
in0 对应通用寄存器堆读端口 1 的数据读出,
in1 对应 PC,
in2 对应指令码的 sa 域零扩展至 32 位。
该“三选一”的选择信号 sel_alu_src1 设计为“零-独热码”(3比特)
(5)ALU 的源操作数输入alu_src2
的生成逻辑GenALUSrc2
中包含一个“三选一”,其三个输入依次是:
in0 对应通用寄存器堆读端口 2 的数据读出,
in1 对应指令码的 imm 域符号扩展至 32 位,
in2 对应常值 32’d8。
该“三选一”的选择信号 sel_alu_src2 设计为“零-独热码”(3比特)。
(6)ALU 内部的多路选择器的选择信号经由 alu_op
再次译码产生出来。alu_op
设计为“零-独热码”,每一位对应 ALU 所支持的一种操作,共 12 位
(7)数据 RAM 的片选信号 data_ram_en
,高电平有效。
(8)数据 RAM 的写使能信号 data_ram_wen
,高电平有效。
(9)通用寄存器堆的写使能信号rf_we
。
(10)通用寄存器堆写地址生成逻辑 GenRFDst
中包含一个“三选一”。其三个输入依次是:
in0 对应指令的 rd域,
in1 对应指令的 rt域,
in2 对应常值 32’d31。
该“三选一”的选择信号 sel_rf_dst 设计为“零-独热码”(3)比特。
(11)通用寄存器堆写数据生成逻辑 GenRFRes
中包含一个“二选一”。其两个输入依次是:
in0 对应 ALU 计算结果 alu_res,
in1 对应从 RAM 读出的 load 操作返回值 ld_res。
该“二选一”的 1比特选择信号 sel_rf_res
为 0 表示选择 alu_res,为 1 表示选择 ld_res。
1.4 复位的处理
复位信号,其有效期间应该将 CPU 中所有软件可感知的状态都初始化到一个唯一确定的状态。
效果:一个 CPU 上无论重启多少次,它每次运行同一个程序的行为都是一致的。
CPU 的电路不用在复位信号有效期间将这些存储中的值置为某个确定的值,它们的复位将由软件完成。
初学者最容易犯的错误——在复位信号有效期间就对外发起第一条指令的取指请求。