一、 实验目标:
设计完成一个连续取指令并进行指令译码的电路,从而掌握设计简单数据通路的基本方法。
二、实验内容
本实验完成:1)首先完成一个译码器;2)接着实现一个寄存器文件;3)最后添加指令存储器和地址部件等将这些部件组合成一个数据通路原型。
三、实验环境
硬件:桌面PC
软件:Linux Chisel开发环境
四、实验步骤及说明
1)设计译码电路:
输入位32bit的一个机器字,按照课本MIPS 指令格式,完成add、sub、lw、sw指令译码,其他指令一律译码成nop指令。输入信号名为Instr_word,对上述四条指令义译码输出信号名为add_op、sub_op、lw_op和sw_op,其余指令一律译码为nop,输出信号均为1bit。
给出Chisel设计代码和仿真测试波形,观察输入Instr_word为add R1,R2,R3; sub R0,R5,R6,lw R5,100(R2), sw R5,104(R2)、JAL 100时,对应的输出波形。
(1)指令对象Instructions.scala
指令定义与匹配:使用 BitPat 定义了MIPS指令的匹配模式,这些模式用于在指令解码时识别不同的操作。例如,instr_add 匹配加法指令的位模式。
// 指令对象,定义了MIPS指令及其对应的操作 object Instructions{ // 定义开关状态 val on = 1.U(1.W) val off = 0.U(1.W) // 定义指令的匹配模式 def instr_add = BitPat("b000000???????????????00000100000") def instr_sub = BitPat("b000000???????????????00000100010") def instr_lw = BitPat("b100011??????????????????????????") def instr_sw = BitPat("b101011??????????????????????????") |
操作输出的定义: 定义了不同指令对应的操作输出状态,使用 List 形式表示。例如,add_op 表示加法指令的操作输出,其中的 on 和 off 表示该操作被激活或不被激活。
// 定义每种指令对应的操作输出 val nop_op = List(off,off,off,off,on) val add_op = List(on,off,off,off,off) val sub_op = List(off,on,off,off,off) val lw_op = List(off,off,on,off,off) val sw_op = List(off,off,off,on,off) |
映射关系: 使用 Array 将指令的匹配模式与其对应的操作输出进行了映射。这种设计允许在指令解码时,可以通过匹配指令模式来快速找到对应的操作输出。
// 将指令与其对应的操作输出进行映射 val map = Array( instr_add -> add_op, instr_sub -> sub_op, instr_lw -> lw_op, instr_sw -> sw_op, ) |
Instructions.scala整体代码:
object Instructions{ // 指令对象,定义了MIPS指令及其对应的操作 // 定义开关状态 val on = 1.U(1.W) val off = 0.U(1.W) // 定义指令的匹配模式 def instr_add = BitPat("b000000???????????????00000100000") def instr_sub = BitPat("b000000???????????????00000100010") def instr_lw = BitPat("b100011??????????????????????????") def instr_sw = BitPat("b101011??????????????????????????") // 定义每种指令对应的操作输出 val nop_op = List(off,off,off,off,on) val add_op = List(on,off,off,off,off) val sub_op = List(off,on,off,off,off) val lw_op = List(off,off,on,off,off) val sw_op = List(off,off,off,on,off) // 将指令与其对应的操作输出进行映射 val map = Array( instr_add -> add_op, instr_sub -> sub_op, instr_lw -> lw_op, instr_sw -> sw_op, ) } |
(2)译码器Decoder.scala:
输入输出定义:创建 DecoderSignals 类,定义了输入指令和五个输出信号,分别对应不同的操作。输入为32位的指令,输出为1位的控制信号。
// 定义输入输出信号的Bundle class DecoderSignals extends Bundle{ val Instr_word = Input(UInt(32.W))// 输入指令,32位 // 输出指令 val add_op = Output(UInt(1.W)) val sub_op = Output(UInt(1.W)) val lw_op = Output(UInt(1.W)) val sw_op = Output(UInt(1.W)) val nop_op = Output(UInt(1.W)) } |
Decoder模块:在 Decoder 类中,定义了输入输出接口。使用 ListLookup 函数进行指令解码。该函数根据输入的指令 io.Instr_word 在 map 中查找对应的操作输出信号,默认返回 nop_op 信号。
信号赋值:将 ListLookup 返回的操作信号分别赋值给 io.add_op、io.sub_op、io.lw_op、io.sw_op 和 io.nop_op。这允许后续的电路逻辑根据这些控制信号执行相应的操作。
class Decoder extends Module { val io = IO(new DecoderSignals()) // 使用 ListLookup 进行指令解码,找出对应的操作信号 val decodersignals = ListLookup(io.Instr_word,nop_op,map) // 将解码得到的信号分配给输出 io.add_op := decodersignals(0) io.sub_op := decodersignals(1) io.lw_op := decodersignals(2) io.sw_op := decodersignals(3) io.nop_op := decodersignals(4) } |
Verilog代码生成:exp3_Decoder 对象用于生成Verilog代码,使用 ChiselStage 将 Decoder 模块转换为Verilog代码并保存到指定的目录中。
// 生成Verilog代码的主对象 object exp3_Decoder extends App { (new chisel3.stage.ChiselStage).emitVerilog(new Decoder(), Array("--target-dir","generated")) } |
Decader.scala整体代码:
// 定义输入输出信号的Bundle class DecoderSignals extends Bundle{ val Instr_word = Input(UInt(32.W))// 输入指令,32位 // 输出指令 val add_op = Output(UInt(1.W)) val sub_op = Output(UInt(1.W)) val lw_op = Output(UInt(1.W)) val sw_op = Output(UInt(1.W)) val nop_op = Output(UInt(1.W)) } // 定义解码器 class Decoder extends Module { val io = IO(new DecoderSignals()) // 使用 ListLookup 进行指令解码,找出对应的操作信号 val decodersignals = ListLookup(io.Instr_word,nop_op,map) // 将解码得到的信号分配给输出 io.add_op := decodersignals(0) io.sub_op := decodersignals(1) io.lw_op := decodersignals(2) io.sw_op := decodersignals(3) io.nop_op := decodersignals(4) } // 生成Verilog代码的主对象 object exp3_Decoder extends App { (new chisel3.stage.ChiselStage).emitVerilog(new Decoder(), Array("--target-dir","generated")) } |
(3)译码器测试DecoderTest.scala:
测试框架设置:该测试类继承自 AnyFlatSpec 和 ChiselScalatestTester,使用 Chisel 的测试框架来验证 Decoder 模块的功能。
测试用例描述:使用 behavior of "Decoder" 定义测试的主题,接着使用 it should "pass" 描述具体的测试内容。
class DecoderTest extends AnyFlatSpec with ChiselScalatestTester{ behavior of "Decoder" it should "pass" in{ test(new Decoder).withAnnotations(Seq(WriteVcdAnnotation)){ c=> |
时钟控制:在每次设置新指令后,调用 c.clock.step(1) 来模拟时钟的推进,确保电路能够根据新指令更新状态。
指令测试:每个 poke 调用用于将特定的指令加载到解码器的输入 Instr_word 中。每个指令之后,通过 expect 验证各个操作信号的输出是否符合预期:
NOP 指令: 确认无操作的输出为1。
c.clock.step(1) // 测试无操作指令(NOP) c.io.Instr_word.poke("b00000000_00000000_00000000_00000000".U) c.clock.step(1) |
ADD 指令: 确认加法操作为1,其他操作为0。
// 测试加法指令,设置指令为 add R1, R2, R3 c.io.Instr_word.poke("b000000_00010_00011_00001_00000_100000".U) c.io.add_op.expect(1.U) c.io.lw_op.expect(0.U) c.io.sub_op.expect(0.U) c.io.sw_op.expect(0.U) c.io.nop_op.expect(0.U) c.clock.step(1) |
SUB 指令: 确认减法操作为1,其他操作为0。
// 测试减法指令,设置指令为 sub R0, R5, R6 c.io.Instr_word.poke("b000000_00101_00110_00000_00000_100010".U) c.io.add_op.expect(0.U) c.io.lw_op.expect(0.U) c.io.sub_op.expect(1.U) c.io.sw_op.expect(0.U) c.io.nop_op.expect(0.U) c.clock.step(1) |
LW 指令: 确认加载操作为1,其他操作为0。
// 测试加载指令,设置指令为 lw R5, 100(R2) c.io.Instr_word.poke("b100011_00010_00101_00000_00001_100100".U) c.io.add_op.expect(0.U) c.io.lw_op.expect(1.U) c.io.sub_op.expect(0.U) c.io.sw_op.expect(0.U) c.io.nop_op.expect(0.U) c.clock.step(1) |
SW 指令: 确认存储操作为1,其他操作为0。
// 测试存储指令,设置指令为 sw R5, 104(R2) c.io.Instr_word.poke("b101011_00010_00101_00000_00001_101000".U) c.io.add_op.expect(0.U) c.io.lw_op.expect(0.U) c.io.sub_op.expect(0.U) c.io.sw_op.expect(1.U) c.io.nop_op.expect(0.U) c.clock.step(1) |
DecaderTest.scala整体代码:
class DecoderTest extends AnyFlatSpec with ChiselScalatestTester{ behavior of "Decoder" it should "pass" in{ test(new Decoder).withAnnotations(Seq(WriteVcdAnnotation)){ c=> c.clock.step(1) // 测试无操作指令(NOP) c.io.Instr_word.poke("b00000000_00000000_00000000_00000000".U) c.clock.step(1) // 测试加法指令,设置指令为 add R1, R2, R3 c.io.Instr_word.poke("b000000_00010_00011_00001_00000_100000".U) c.io.add_op.expect(1.U) c.io.lw_op.expect(0.U) c.io.sub_op.expect(0.U) c.io.sw_op.expect(0.U) c.io.nop_op.expect(0.U) c.clock.step(1) // 测试减法指令,设置指令为 sub R0, R5, R6 c.io.Instr_word.poke("b000000_00101_00110_00000_00000_100010".U) c.io.add_op.expect(0.U) c.io.lw_op.expect(0.U) c.io.sub_op.expect(1.U) c.io.sw_op.expect(0.U) c.io.nop_op.expect(0.U) c.clock.step(1) // 测试加载指令,设置指令为 lw R5, 100(R2) c.io.Instr_word.poke("b100011_00010_00101_00000_00001_100100".U) c.io.add_op.expect(0.U) c.io.lw_op.expect(1.U) c.io.sub_op.expect(0.U) c.io.sw_op.expect(0.U) c.io.nop_op.expect(0.U) c.clock.step(1) // 测试存储指令,设置指令为 sw R5, 104(R2) c.io.Instr_word.poke("b101011_00010_00101_00000_00001_101000".U) c.io.add_op.expect(0.U) c.io.lw_op.expect(0.U) c.io.sub_op.expect(0.U) c.io.sw_op.expect(1.U) c.io.nop_op.expect(0.U) c.clock.step(1) // 测试无操作指令,设置指令为 NOP c.io.Instr_word.poke("b000011_00000_00000_00000_00000_000000".U) c.io.add_op.expect(0.U) c.io.lw_op.expect(0.U) c.io.sub_op.expect(0.U) c.io.sw_op.expect(0.U) c.io.nop_op.expect(1.U) c.clock.step(1) println("success!\n") } } } |
2)设计寄存器文件:
共32个32bit寄存器,允许两读一写,且0号寄存器固定读出位0。五个输入信号为RS1、RS2、WB_data、Reg_WB、RF_WrEn,寄存器输出RS1_out和RS2_out;寄存器内部保存的初始数值等同于寄存器编号。
给出Chisel设计代码和仿真测试波形,观察RS1=5,RS2=8,WB_data=0x1234,Reg_WB=1,RF_WrEn=1的输出波形和受影响寄存器的值。
(1)寄存器文件RegisterFile.scala
模块定义:定义一个名为RegisterFile的模块。实现一个包含32个32位寄存器的寄存器文件,支持两读一写的操作。
class RegisterFile extends Module |
输入输出接口定义:
输入信号包括:
RS1:选择第一个要读取的寄存器,5位宽,能够选择0到31的寄存器。
RS2:选择第二个要读取的寄存器,5位宽,能够选择0到31的寄存器。
WB_data:要写入寄存器的数据,32位宽。
Reg_WB:写使能信号,布尔类型,用于控制是否执行写入操作。
输出信号:
RS1_out:第一个寄存器的输出数据,32位宽。
RS2_out:第二个寄存器的输出数据,32位宽。
val io = IO(new Bundle { // RS1,RS2输入信号,用于选择要读取的寄存器 val RS1 = Input(UInt(5.W)) val RS2 = Input(UInt(5.W)) // RS1,RS2输出数据 val RS1_out = Output(UInt(32.W)) val RS2_out = Output(UInt(32.W)) // 写入数据信号,用于写入寄存器 val WB_data = Input(UInt(32.W)) // 选择写入数据的寄存器 val Reg_WB = Input(Bool()) }) |
寄存器数组初始化:使用 RegInit 创建一个寄存器数组 registers,包含32个32位的寄存器,初始值设置为它们的索引(即寄存器编号)。
// 寄存器堆,32个32位寄存器,初始值等于寄存器编号 val registers = RegInit(VecInit((0 until 32).map(_.U(32.W)))) |
读取寄存器:
对于 RS1_out:使用 Mux 判断如果 RS1 为0,输出0;否则,输出对应寄存器的值。
对于 RS2_out:逻辑类似,确保 RS2 为0时输出0,其他情况下输出对应寄存器的值。
// RS1,RS2输出数据,0号寄存器固定读出位0 io.RS1_out := Mux(io.RS1 === 0.U, 0.U, registers(io.RS1)) io.RS2_out := Mux(io.RS2 === 0.U, 0.U, registers(io.RS2)) |
写入寄存器:使用 when 语句检查写使能信号 RF_WrEn 和 Reg_WB。如果 RF_WrEn 为真且 Reg_WB 不为0,则将 WB_data 写入指定的寄存器。特别注意,0号寄存器的写入被禁止,因此在写入时判断 Reg_WB =/= 0.U。
// 写入数据到寄存器 registers(io.Reg_WB) := io.WB_data // 根据写信号选择要写入的寄存器 when (io.Reg_WB) { registers(io.RS1) := Mux(io.RS1 === 0.U, 0.U, io.WB_data) registers(io.RS2) := Mux(io.RS2 === 0.U, 0.U, io.WB_data) } |
该模块的整体功能设计是:该模块实现了一个简单而有效的寄存器文件,允许从任意寄存器读取数据,并在满足条件时向特定寄存器写入数据。需要注意的是要确保读出0号寄存器时输出始终为0。仅在写使能信号为真时执行,并避免向0号寄存器写入。
RegisterFile.scala整体代码:
class RegisterFile extends Module { val io = IO(new Bundle { // RS1,RS2输入信号,用于选择要读取的寄存器 val RS1 = Input(UInt(5.W)) val RS2 = Input(UInt(5.W)) // RS1,RS2输出数据 val RS1_out = Output(UInt(32.W)) val RS2_out = Output(UInt(32.W)) // 写入数据信号,用于写入寄存器 val WB_data = Input(UInt(32.W)) // 选择写入数据的寄存器 val Reg_WB = Input(Bool()) }) // 寄存器堆,32个32位寄存器,初始值等于寄存器编号 val registers = RegInit(VecInit((0 until 32).map(_.U(32.W)))) // 写入数据到寄存器 registers(io.Reg_WB) := io.WB_data // RS1,RS2输出数据,0号寄存器固定读出位0 io.RS1_out := Mux(io.RS1 === 0.U, 0.U, registers(io.RS1)) io.RS2_out := Mux(io.RS2 === 0.U, 0.U, registers(io.RS2)) // 根据写信号选择要写入的寄存器 when (io.Reg_WB) { registers(io.RS1) := Mux(io.RS1 === 0.U, 0.U, io.WB_data) registers(io.RS2) := Mux(io.RS2 === 0.U, 0.U, io.WB_data) } } object RegisterFile extends App { (new chisel3.stage.ChiselStage).emitVerilog(new RegisterFile(), Array("--target-dir","generated")) } |
(2)寄存器文件测试RegisterFileTest.scala:
测试类:RegisterFileTest 继承自 AnyFlatSpec 和 ChiselScalatestTester,用于编写测试。
class RegisterFileTest extends AnyFlatSpec with ChiselScalatestTester |
测试模块行为:使用 behavior of "RegisterFile Module" 定义被测试的模块的名称。it should "observe the effect on registers" 定义测试的目的,即观察寄存器的效果。
behavior of "RegisterFile Module" it should "observe the effect on registers" in |
测试过程:使用 test 方法创建一个 RegisterFile 实例,并启用 VCD 波形输出(通过 WriteVcdAnnotation)。在测试块内部,使用 poke 方法设置输入信号。
时钟步进:c.clock.step(1)让时钟向前迈进一步,这将触发 RegisterFile 模块的行为,执行写操作。
test (new RegisterFile).withAnnotations(Seq(WriteVcdAnnotation)) { c => // 设置输入信号 c.io.RS1.poke(5.U) // RS1 = 5 c.io.RS2.poke(8.U) // RS2 = 8 c.io.WB_data.poke(0x1234.U) // WB_data = 0x1234 // 时钟向前一步, 写入 c.io.Reg_WB.poke(true) // 写使能 c.clock.step(1) } |
RegisterFileTest.scala整体代码:
class RegisterFileTest extends AnyFlatSpec with ChiselScalatestTester { behavior of "RegisterFile Module" it should "observe the effect on registers" in { test (new RegisterFile).withAnnotations(Seq(WriteVcdAnnotation)) { c => // 设置输入信号 c.io.RS1.poke(5.U) // RS1 = 5 c.io.RS2.poke(8.U) // RS2 = 8 c.io.WB_data.poke(0x1234.U) // WB_data = 0x1234 // 时钟向前一步, 写入 c.io.Reg_WB.poke(true) // 写使能 c.clock.step(1) } } } |
3)设计指令存储器:
实现一个32个字的指令存储器,从0地址分别存储4条指令add R1,R2,R3; sub R0,R5,R6,lw R5,100(R2), sw R5,104(R2)。然后组合指令存储器、寄存器文件、译码电路,并结合PC更新电路(PC初值为0),最终让电路能逐条指令取出、译码(不需要完成指令执行)。给出Chisel设计代码和仿真测试波形,观察四条指令的执行过程波形,记录并解释其含义。
(1)MemAndPC模块(指令存储与指令计数)
模块定义:定义一个名为MemAndPC的模块。实现一个基本的内存读写模块,支持指令的写入和读取。
class MemAndPC extends Module |
输入输出接口定义:
输入信号:
rdEna:读取使能信号,布尔类型,控制是否执行读取操作。
wrEna:写入使能信号,布尔类型,控制是否执行写入操作。
wrAddr:写入地址,10位宽,用于指定要写入的内存地址。
wrData:要写入的数据,32位宽。
输出信号:rdData:读取的数据,32位宽。
val io = IO(new Bundle { // 读 val rdEna = Input(Bool()) val rdData = Output(UInt(32.W)) // 写 val wrEna = Input(Bool()) val wrAddr = Input(UInt(10.W)) val wrData = Input(UInt(32.W)) }) |
(2)寄存器和内存初始化:
程序计数器(PC):pcReg初始化为0的32位寄存器,用作当前读取的内存地址。
内存:使用 SyncReadMem 定义一个大小为128字节的同步读取内存,每个单元为8位(1字节)。
val pcReg = RegInit(0.U(32.W)) val mem = SyncReadMem(128,UInt(8.W)) |
写入内存:当 wrEna 为真时,执行写入操作。写入数据时,将32位的 wrData 分成4个8位的部分,依次写入到指定地址 wrAddr 及其后续地址(wrAddr + 1、wrAddr + 2 和 wrAddr + 3)。
// 写内存 when (io.wrEna) { // 分 4 个单元写 mem.write(io.wrAddr, io.wrData(7, 0)) mem.write(io.wrAddr + 1.U, io.wrData(15, 8)) mem.write(io.wrAddr + 2.U, io.wrData(23, 16)) mem.write(io.wrAddr + 3.U, io.wrData(31, 24)) } |
读取内存:当 rdEna 为真时,执行读取操作。读取4个连续的内存单元,分别从 pcReg、pcReg + 1、pcReg + 2 和 pcReg + 3 读取数据。使用连接运算符 ## 将四个读取的数据拼接成一个32位的 rdData 输出。读取后,pcReg 增加4,指向下一组指令。如果 rdEna 为假,rdData 输出为0(默认输出)。
// 读内存 when (io.rdEna) { // 分 4 个单元读 val rdData0 = mem.read(pcReg) val rdData1 = mem.read(pcReg+1.U) val rdData2 = mem.read(pcReg+2.U) val rdData3 = mem.read(pcReg+3.U) io.rdData := rdData3 ## rdData2 ## rdData1 ## rdData0 // ## 表连接 pcReg := pcReg + 4.U }.otherwise{ io.rdData := 0.U } |
该模块的整体功能设计是:
- 该模块实现了一个简单的指令存储器,能够支持对指令的写入和读取,并且自动更新程序计数器以便于顺序读取。
- 通过分块读取和写入,支持32位数据在内存中的分布。
- 程序计数器PC的增加使得模块可以连续读取指令,适合顺序执行的指令流。
整体代码:
class MemAndPC extends Module { val io = IO(new Bundle { // 读 val rdEna = Input(Bool()) val rdData = Output(UInt(32.W)) // 写 val wrEna = Input(Bool()) val wrAddr = Input(UInt(10.W)) val wrData = Input(UInt(32.W)) }) val pcReg = RegInit(0.U(32.W)) val mem = SyncReadMem(128,UInt(8.W)) // 写内存 when (io.wrEna) { // 分 4 个单元写 mem.write(io.wrAddr, io.wrData(7, 0)) mem.write(io.wrAddr + 1.U, io.wrData(15, 8)) mem.write(io.wrAddr + 2.U, io.wrData(23, 16)) mem.write(io.wrAddr + 3.U, io.wrData(31, 24)) } // 读内存 when (io.rdEna) { // 分 4 个单元读 val rdData0 = mem.read(pcReg) val rdData1 = mem.read(pcReg+1.U) val rdData2 = mem.read(pcReg+2.U) val rdData3 = mem.read(pcReg+3.U) io.rdData := rdData3 ## rdData2 ## rdData1 ## rdData0 // ## 表连接 pcReg := pcReg + 4.U }.otherwise{ io.rdData := 0.U } } |
4)Junction模块(综合电路)
模块定义:定义一个名为Junction的模块。该模块整合了指令解码、寄存器文件和指令内存,形成一个简单的处理器结构。
class Junction extends Module |
输入输出接口定义:
输出信号:
Instr_word:输出当前指令的32位数据。
add_op、sub_op、lw_op、sw_op、nop_op:指令操作信号,布尔类型,用于指示当前指令的类型。
RS1_out、RS2_out:寄存器文件中读取的两个寄存器的输出数据。
输入信号:
rdEna:读取使能信号,控制是否执行写入操作。
wrAddr:写入地址,10位宽,用于指定要写入的内存地址。
wrData:要写入的数据,32位宽。
wrEna:写入使能信号,控制是否执行写入操作。
val io = IO(new Bundle { // Decoder val Instr_word = Output(UInt(32.W)) val add_op = Output(Bool()) val sub_op = Output(Bool()) val lw_op = Output(Bool()) val sw_op = Output(Bool()) val nop_op = Output(Bool()) // RegisterFile val RS1_out = Output(UInt(32.W)) val RS2_out = Output(UInt(32.W)) // Instruction val rdEna = Input(Bool()) val wrAddr = Input(UInt(10.W)) val wrData = Input(UInt(32.W)) val wrEna = Input(Bool()) }) |
模块实例化:
- 解码器:decoder实例化一个解码器模块,负责解码指令并生成操作信号。
- 寄存器文件:registerFile实例化寄存器文件模块,负责存储和读取寄存器的数据。
- 指令内存:instructionMemory实例化指令存储模块,负责存储和读取指令。
// 实例化 val decoder = Module(new Decoder) val registerFile = Module(new RegisterFile) val instructionMemory = Module(new Instruction) |
连接信号:
- 指令操作信号连接:从解码器输出的操作信号连接到模块输出。
- 控制信号连接:将读取和写入信号连接到指令内存模块,以控制其操作。
- 输入数据连接:从指令内存读取的数据用于选择寄存器文件中的寄存器(RS1 和 RS2),通过指令的特定位提取寄存器编号。
- 指令连接:将指令内存的读取数据直接传递给解码器进行处理。
// 连接指令信号 io.add_op := decoder.io.add_op io.sub_op := decoder.io.sub_op io.lw_op := decoder.io.lw_op io.sw_op := decoder.io.sw_op io.nop_op := decoder.io.nop_op // 连接控制信号 instructionMemory.io.rdEna := io.rdEna instructionMemory.io.wrEna := io.wrEna instructionMemory.io.wrAddr := io.wrAddr instructionMemory.io.wrData := io.wrData // 连接输入数据 registerFile.io.RS1 := instructionMemory.io.rdData(25,21) registerFile.io.RS2 := instructionMemory.io.rdData(20, 16) decoder.io.Instr_word := instructionMemory.io.rdData io.Instr_word := instructionMemory.io.rdData |
初始化寄存器文件控制信号:初始化 Reg_WB 和 WB_data 为默认值,表示在当前模块内不进行寄存器的写入操作。
// 初始化寄存器文件的控制信号 registerFile.io.Reg_WB := false.B registerFile.io.WB_data := 0.U |
输出信号连接:将寄存器文件的输出信号直接连接到模块的输出,以便外部模块访问。
// 连接模块输出信号 io.RS1_out := registerFile.io.RS1_out io.RS2_out := registerFile.io.RS2_out |
该模块的整体功能设计是:
- 模块整合了指令存储、寄存器访问和指令解码的基本功能,为一个简单的处理器提供了核心组件。
- 模块之间通过输入输出信号进行连接,形成数据流,简化了指令的处理流程。
- 通过将不同的功能模块化,便于后续扩展和维护。
整体代码:
class Junction extends Module { val io = IO(new Bundle { // Decoder val Instr_word = Output(UInt(32.W)) val add_op = Output(Bool()) val sub_op = Output(Bool()) val lw_op = Output(Bool()) val sw_op = Output(Bool()) val nop_op = Output(Bool()) // RegisterFile val RS1_out = Output(UInt(32.W)) val RS2_out = Output(UInt(32.W)) // Instruction val rdEna = Input(Bool()) val wrAddr = Input(UInt(10.W)) val wrData = Input(UInt(32.W)) val wrEna = Input(Bool()) }) // 实例化 val decoder = Module(new Decoder) val registerFile = Module(new RegisterFile) val instructionMemory = Module(new Memory) // 连接指令信号 io.add_op := decoder.io.add_op io.sub_op := decoder.io.sub_op io.lw_op := decoder.io.lw_op io.sw_op := decoder.io.sw_op io.nop_op := decoder.io.nop_op // 连接控制信号 instructionMemory.io.rdEna := io.rdEna instructionMemory.io.wrEna := io.wrEna instructionMemory.io.wrAddr := io.wrAddr instructionMemory.io.wrData := io.wrData // 连接输入数据 registerFile.io.RS1 := instructionMemory.io.rdData(25,21) registerFile.io.RS2 := instructionMemory.io.rdData(20, 16) decoder.io.Instr_word := instructionMemory.io.rdData io.Instr_word := instructionMemory.io.rdData // 初始化寄存器文件的控制信号 registerFile.io.Reg_WB := false.B registerFile.io.WB_data := 0.U // 连接模块输出信号 io.RS1_out := registerFile.io.RS1_out io.RS2_out := registerFile.io.RS2_out } |
5)仿真测试波形:
测试类:JunctionTest 类继承自 AnyFlatSpec,并实现 ChiselScalatestTester,用于编写测试案例。
class JunctionTest extends AnyFlatSpec with ChiselScalatestTester |
测试行为描述:使用 behavior of "Junction" 描述要测试的模块,指定模块名为 "Junction"。it should "pass" 定义了一个测试用例,表示期望该测试能够成功通过。
behavior of "Junction" it should "pass" in { |
测试逻辑:使用 test(new Junction) 来实例化 Junction 模块,并使用 withAnnotations(Seq(WriteVcdAnnotation)) 记录波形。
test(new Junction).withAnnotations(Seq(WriteVcdAnnotation)) { c => |
指令定义:定义五条 MIPS 指令,以 32 位无符号整数形式表示(即二进制格式)。
ADD: add R1, R2, R3(十六进制为 00430820)
SUB: sub R0, R5, R6(十六进制为 00A60022)
LW: lw R5, 100(R2)(十六进制为 8CA20032)
SW: sw R5, 104(R2)(十六进制为 ACA20034)
JAL: jal 100(十六进制为 0C220032)
val ADD = "b00000000010000110000100000100000".U(32.W) // add R1, R2, R3 val SUB = "b00000000101001100000000000100010".U(32.W) // sub R0, R5, R6 val LW = "b10001100101000100000000000110010".U(32.W) // lw R5, 100(R2) val SW = "b10101100101000100000000000110100".U(32.W) // sw R5, 104(R2) val JAL = "b00001100001000100000000000110010".U(32.W) // jal 100 |
时钟设置: c.clock.setTimeout(0) 初始化时钟,设置超时为 0,表示不限制时钟步骤。
写入指令:对每条指令进行写入操作:
通过 poke 方法设置 wrEna 为 true 以使能写入。
使用 wrAddr 指定写入地址(从 0 到 16,每条指令占 4 字节)。
使用 wrData 将指令数据写入内存。
每次写入后,调用 c.clock.step(1) 使时钟前进一步。
// ADD 指令 c.io.wrEna.poke(true) c.io.wrAddr.poke(0.U) c.io.wrData.poke(ADD) c.clock.step(1) |
结束写入:在写完所有指令后,将 wrEna 设为 false,表示结束写入。使能读操作,通过 poke 将 rdEna 设置为 true,准备读取存储器中的指令。
结束测试:c.clock.step(10) 让时钟继续前进,保持一定时间,以便可以观察到输出波形。
// 结束 c.io.wrEna.poke(false) c.io.rdEna.poke(true) c.clock.step(10) |
仿真波形观察:观察写入指令到存储器的过程,每条指令的地址和数据应在时序上相应。观察add R1,R2,R3; sub R0,R5,R6,lw R5,100(R2), sw R5,104(R2)四条指令的执行过程波形,以及观察寄存器输出,以验证寄存器文件是否能正确根据指令提取寄存器的值。
整体测试代码:
class JunctionTest extends AnyFlatSpec with ChiselScalatestTester { behavior of "Junction" it should "pass" in { test(new Junction).withAnnotations(Seq(WriteVcdAnnotation)) { c => val ADD = "b00000000010000110000100000100000".U(32.W) // add R1, R2, R3 的十六进制为 00430820 val SUB = "b00000000101001100000000000100010".U(32.W) // sub R0, R5, R6 的十六进制为 00A60022 val LW = "b10001100101000100000000000110010".U(32.W) // lw R5, 100(R2) 的十六进制为 8CA20032 val SW = "b10101100101000100000000000110100".U(32.W) // sw R5, 104(R2) 的十六进制为 ACA20034 val JAL = "b00001100001000100000000000110010".U(32.W) // jal 100 的十六进制为 0C220032 c.clock.setTimeout(0) // 初始化时钟 c.io.wrEna.poke(true) // ADD 指令 c.io.wrAddr.poke(0.U) c.io.wrData.poke(ADD) c.clock.step(1) c.io.wrEna.poke(true) // SUB 指令 c.io.wrAddr.poke(4.U) c.io.wrData.poke(SUB) c.clock.step(1) c.io.wrEna.poke(true) // LW 指令 c.io.wrAddr.poke(8.U) c.io.wrData.poke(LW) c.clock.step(1) c.io.wrEna.poke(true) // SW 指令 c.io.wrAddr.poke(12.U) c.io.wrData.poke(SW) c.clock.step(1) c.io.wrEna.poke(true) // JAL 指令 c.io.wrAddr.poke(16.U) c.io.wrData.poke(JAL) c.clock.step(1) c.io.wrEna.poke(false) // 结束 c.io.rdEna.poke(true) c.clock.step(10) } } } |