BUAA-CO-P3 基于logisim的单周期MIPS架构CPU

声明

P3对alu、grf的设计非常不合理,直接将各个可能的操作数接入alu,而且默认grf要读的寄存器是rs、rt。但因为足够应付需求(懒得重构)就保留了这种错误方法,但在后面的设计中都注意规避了。

前置理解

内存

1. 哈佛结构(本CPU应用的) 内存 = 数据内存(DM) + 指令内存(IM)

  • IM: CPU只会从IM读取指令,永远不会写入指令 使用ROM
  • DM: 可读(lw)可写(sw) 使用RAM

2. 冯·诺伊曼结构 指令和数据存放在同一个物理内存RAM中

指令处理

  1. 用RTL描述每条指令
    • :数据传送方向;
    • R[a] : 寄存器 a;
    • M[a] : 主存中地址为a的单元;
    • PC :程序计数器
    • f(data) : 表示对数据data进行f操作
  2. 加指令:
    • R/I/J
    • 看清指令的RTL描述,把数据流搞清楚
  3. 测试指令直接使用$3这样的寄存器,更易读

Debug

Wrong instruction behavior: in MIPS code 0x3c09cba1, we got ‘@00003030: $9 <= 0xcba10000’ when we expected ‘@0000303c: $11 <= 0x00030000’. The wrong behavior was next to the correct execution of MIPS code 0x3c083bc2.

部分 含义
MIPS code 0x3c09cba1 实际出错的机器码(十六进制)
‘@00003030: $9 <= 0xcba10000’ PC = 00003030时,0xcba10000被写入$9
next to … 出错前的最后一条正确执行的指令(帮助你定位上下文)
  1. 机器码 指令:转化为32位机器码,翻译为指令,例如:

    opcode rs rt immediate
    001101 00110 00110 0110011001101100
     ori $6, $6, 0x666c
    
  2. PC:起始地址为00003000

简化操作的logisim部件

  • odd/even partry:odd/even输出1
  • Bit Finder:找到最高位/最低位1/0的位置
  • negator:取负
  • Bit Adder:不要傻傻地自己去写 (TT…

易错

  1. 输出的regWrite等信号是否为终值?
    • 即:到底有没有写入?

模块设计

IM (指令存储器)

  • 核心任务:根据PC中的地址,从ROM中读出指令输出
  • 整体设计
    • 地址转换逻辑
      • 地址偏移:pc - 0x00003000
      • 将PC的字节地址转换为ROM的字地址
  • 模块规格
    信号名 方向 位宽 描述
    PC I 32 指令的地址
    Instruction O 32 指令机器码

GRF(通用寄存器组)

  • 核心任务:为ALU提供快速的数据源,并接收计算结果

  • 整体设计

    • 读数据:指令最多需要同时读取两个不同地址的数据 - 两个读端口
    • 写数据
    • 0 号寄存器的值始终保持为 0。其他寄存器初始值(复位后)均为 0,无需专门设置

  • 模块规格:

    信号名 方向 位宽 描述
    reset I 1 异步复位信号
    clk I 1 时钟信号
    RA1 I 5 读地址1
    RA2 I 5 读地址2
    WA I 5 写地址
    WD I 32 写数据
    RegWrite I 1 写使能信号
    RD1 O 32 读数据1
    RD2 O 32 读数据2

    注:这里的“地址”指的是GRF中的第n个reg

ALU

  • 整体设计
    • 能够实现多种运算功能,并根据op选择对应功能
      op 运算
      0000 无符号rs + rt
      0001 无符号rs - rt
      0010 rs & imm
      0011 rs | imm
      0100 16位立即数加载至高位
      0101 rs + imm
  • 模块规格
    信号名 方向 位宽 描述
    op I 4 选择运算功能
    rs I 32 rs value
    rt I 32 rt value
    immEXT I 32 扩展后的立即数
    result O 32 运算结果
    equal O 1 判断是否相等

DM(数据存储器)

  • 核心任务 根据 CPU 提供的地址,进行读或写操作。它连接了 CPU 的核心与外部数据存储。
  • 整体设计
    • 使用 RAM 实现,容量为 3072 × 32bit,应具有异步复位功能,复位值0x00000000

    • 起始地址0x00000000地址范围0x00000000 ~ 0x00002FFF

    • RAM 应使用双端口模式,即设置 RAM 的 Data Interface 属性为 Separate load and store ports

  • 模块规格
    信号名 方向 位宽 描述
    Address I 32 读/写地址
    WriteData I 32 要写入的数据
    MemWrite I 1 写使能信号
    MemRead I 1 读使能信号
    clk I 1 时钟信号
    reset I 1 异步复位信号
    MemData O 32 读出的数据

EXT(扩展单元)

  • 核心任务 把16位立即数扩展到32位
  • 整体设计 能够对立即数进行零扩展/符号扩展
  • 模块规格
    信号名 方向 位宽 描述
    immIn I 16 待扩展的立即数
    op I 1 扩展方式选择信号:
    1 - 符号扩展
    0 - 零扩展
    immOut O 32 扩展后的立即数

Controller

  • 核心任务:根据当前正在执行的指令输出对应控制信号

  • 整体设计 解码逻辑 = 和逻辑 + 或逻辑

    • 和逻辑:识别

      • 将输入的机器码识别为相应指令
    • 或逻辑:生成

      • 根据输入指令生成对应的控制信号
      • 控制信号表;
        控制信号\指令 add sub ori lw sw beq lui addi j
        RegWrite 1 1 1 1 0 0 1 1 0
        ALUop 0000 0001 0011 0101 0101 0000 0100 0101 0000
        MemWrite 0 0 0 0 1 0 0 0 0
        MemRead 0 0 0 1 0 0 0 0 0
        EXTop 0 0 0 1 1 1 0 1 0
        RegDst 00 00 01 01 00 00 01 01 00
        WDsel 0 0 0 1 0 0 0 0 0
        NPCop 00 00 00 00 00 01 00 00 10
  • 模块规格

    信号名 方向 位宽 描述
    opcode I 6 指令[31:26]位
    funct I 6 指令[5:0]位
    RegWrite O 1 GRF写使能信号
    ALUop O 4 ALU运算模式选择信号
    MemWrite O 1 DM写使能信号
    MemRead O 1 DM读使能信号
    EXTop O 1 EXT扩展模式选择信号
    RegDst O 2 GRF写回地址选择信号:
    01:rt
    00:rd
    10:rs
    WDsel O 2 GRF写入数据选择信号:
    0:ALU运算结果
    1:DM读出的数据(lw)
    NPCop O 2 PC更新模式选择信号:
    00:顺序执行
    01:beq分支跳转
    10:j跳转

NPC

  • 核心任务 计算下一周期PC应更新成的值

  • 整体设计

    • 顺序执行: NPC = PC + 4
      • 分支跳转:NPC = (PC + 4) + (SignExtend(offset))
      • j跳转:NPC = PC31…28||instr_index||00
  • 模块规格

    信号名 方向 位宽 描述
    immExt I 32 拓展后的立即数(offset)
    instr_index I 26 instr_index
    equal I 1 判断reg rs、rt所存值是否相同
    NPCop I 2 00:NPC = PC + 4
    01:分支跳转
    10:j
    PC I 32 当前指令地址
    NPC O 32 下一条指令地址

测试

核心思路:让 CPU 在执行指令后,将关键的结果存储到DM的特定位置。测试结束后,比较 Logisim 和 Mars 的 DM 内容是否完全一致

测试方法

环境设置

  • 选择Compact,Data at Address 0

编写MIPS汇编测试程序

  1. 初始化
    • 初始化寄存器:初始化为不同的非零值
    • 初始化指针:设置$t0作为指向DM的指针,初始值为0x0000
  2. 书写程序
    • 执行一条指令,保存一个结果,移动一次指针
      • 对于直接产生结果的指令(如add、ori)
        • 直接将结果保存到内存
      • 对于不直接产生结果的指令(如beq、j)
        • 构造场景,使得跳转成功与否导致存入内存的值不同
    • 例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      .text
      # .globl main 告诉汇编器 main 是一个全局标签,程序的入口
      .globl main

      main:
      # --- 1. 初始化阶段 ---
      # 初始化寄存器为非零值
      addi $s0, $zero, 10
      addi $s1, $zero, 20
      addi $s2, $zero, 10
      # ... 初始化更多寄存器 ...

      # 初始化内存指针 $t0
      addiu $t0, $zero, 0x0000 # $t0 = 0x0000, 指向 DM 的开始

      # --- 2. 测试指令并保存结果 ---

      # --- 测试 add 指令 ---
      add $t1, $s0, $s1 # $t1 = 10 + 20 = 30
      sw $t1, 0($t0) # 将结果 30 保存到内存地址 0x0000
      addi $t0, $t0, 4 # 指针移动到 0x0004

      # --- 测试 beq (分支不发生) ---
      beq $s0, $s1, BRANCH_TAKEN # 10 != 20, 不会跳转
      # 如果没跳转,存入 0x0
      addiu $t2, $zero, 0x0
      sw $t2, 0($t0)
      addi $t0, $t0, 4
      j END_BRANCH_1

      BRANCH_TAKEN:
      # 如果跳转了,存入 0x1
      addiu $t2, $zero, 0x1
      sw $t2, 0($t0)
      addi $t0, $t0, 4

      END_BRANCH_1:
      # --- 测试 beq (分支发生) ---
      beq $s0, $s2, BRANCH_TAKEN_2 # 10 == 10, 应该跳转
      # 如果没跳转,存入 0x0
      addiu $t2, $zero, 0x0
      sw $t2, 0($t0)
      addi $t0, $t0, 4
      j END_BRANCH_2

      BRANCH_TAKEN_2:
      # 如果跳转了,存入 0x1
      addiu $t2, $zero, 0x1
      sw $t2, 0($t0)
      addi $t0, $t0, 4

      END_BRANCH_2:
      # --- 3. 结束程序 ---
      # 可以用一个无限循环来停住 CPU
      END_LOOP:
      j END_LOOP
  3. 在Mars中生成标答
    1. 执行程序
    2. 手动点击Pause暂停死循环
    3. 导出DM File - Dump Memory
      • 选择Data Segment
      • 选择Hexadecimal Text格式
      • Dump to file得到txt文件
      • 在txt文件开头加上v2.0 raw

在logisim中运行CPU

  1. 找到IM中的ROM,右键选择Load Image
  2. reset
    1. 看DM
      1. 运行时钟直到进入死循环
      2. 找到DM中的RAM,右键选择Edit Content
    2. 实例化grf后直接观察grf
      1. 右键main中grf,选择view grf
      2. ctrl + T开始观察即可

测试数据

计算类指令功能测试

  1. 寄存器数据:
    1. 0及附近的数:−2,−1,0,1,2
    2. 32 位数边界附近的数:−2147483648,−2147483647,2147483646,2147483647
    3. 32 位数范围内的一些随机数:−1000786109,1919156834,…
  2. 无符号立即数:
    1. 0 及附近的数:0,1,2,3
    2. 16 位无符号数边界附近的数:65533,65534,65535
    3. 16 位无符号数范围内的一些随机数:25779,42528,…
  3. 符号立即数:
    1. 0 及附近的数:−2,−1,0,1,2
    2. 16 位符号数边界附近的数:−32768,−32767,32766,32767
    3. 16 位符号数范围内的一些随机数:−5329,25299,…

存取类指令功能测试

  1. offset :+/-/0
  2. $base:+/-/0

跳转类指令功能测试

  • 跳转,且目标在此跳转指令之前
  • 跳转,且目标是此跳转指令
  • 跳转,且目标在此跳转指令之后
  • 不跳转,且目标在此跳转指令之前
  • 不跳转,且目标是此跳转指令
  • 不跳转,且目标在此跳转指令之后

可能添加的

lb/sb

dm本身不变

  • lb:dm取字后再经MUX,将Addr[1:0]作为选择端,选出真正要读出的字节
  • sb:先利用MUX选出要读出的字节
    • 太麻烦暂时不考虑
    • 多周期CPU中实现……

思考题

  1. 上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能。

    状态存储:GRF、PC、DM; 状态转移:NPC、Controller、EXT、ALU

  2. 现在我们的模块中 IM 使用 ROM, DM 使用 RAM, GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。

    合理。

    1. IM没有增加、修改或删除指令的需求,所以用ROM(Read-Only Memory)可以覆盖需求,而且可以避免指令被意外修改导致程序故障;
    2. DM需要实现数据的写入和读出,所以使用RAM即可;
    3. GRF使用register也可以完状态储存的需求。

    改进:用同一个RAM储存Instructions和Data,可以支持自修改代码,内存使用也更灵活

  3. 在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。

  4. 事实上,实现 nop 空指令,我们并不需要将它加入控制信号真值表,为什么?

    因为nop指令本质上就是sll $0, $0, 0的R型指令,机器码为0x00000000。把它当作R型指令处理,执行时任何存储单元的值都不会发生变化,所以不用把它再加入控制信号真值表。

  5. 阅读 Pre 的 “MIPS 指令集及汇编语言” 一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。

    1. 寄存器数据、立即数方面:没有测试边界附近的数;没有测试目标寄存器为0的情况
    2. 存取类指令:没有测试offset为负的情况
    3. 跳转类指令:没有测试跳转到前面和当前指令的情况

Copyright © 2026 CRZ's Blog

Powered by Hexo • Theme Candelas