P7

Coooookie hahah

P7_Study

设计概述

本文设计的是又Verilog实现的MIPS微系统,该微系统支持33条汇编指令,为实现该功能,笔者设计PC,CMP,GRF,EXT,NPC,ALU,MD,Load,Store,CP0,D_Reg,E_Reg,M_Reg,W_Reg,MCU,HCU,Bridge,TC模块。
顶层结构如下:
1
CPU内部如下:
1

指令说明

  • cal_R : add,sub,and,or,slt,sltu,lui
  • cal_I : addi,andi,ori
  • load : lb,lh,lw
  • store : sb,sh,sw
  • B类指令 : beq,bne
  • J类指令 : jal,jr
  • md类 :mult,multu,div,divu
  • mf类 : mfhi,mflo
  • mt类 : mthi,mtlo
  • CP0相关 : mfc0,mtc0
  • 异常中断返回 : eret
  • 其他 : syscall

设计思考——两个思想

P7做的比较艰难,主要是刚开始对教程不太理解,后来在对于微系统的实现以及debug过程中与同学、助教进行了许多交流,才觉得对于P7微系统的稍微理解。回顾来看,我认为P7困扰我的其实是两个思想——软硬件协同的视角以及模块封装的思想,遂作此文,以分享笔者粗浅的思考。

软硬件协同的视角

此处引用一位助教的原话“我们的硬件只需要忠诚地翻译指令”,所以我们只是实现硬件而不用去看软件要干什么,我们只要知道软件提出了什么需求,也就是我们的硬件要去实现怎样的接口(这里助教给我讲了一个形象的例子,后端-API-前端,后端不用去思考前端去干什么,实现好API接口即可)。
这也就解答了我在最初设计时两个很大的困惑:

  • 如何处理中断?是在中断时把m_data_addr变成0x7f20吗?
    答案是不用的, 是否访问0x7f20是软件程序去决定的事情,而我们的硬件只用支持软件去访问0x7f20即可。这也就实现了教程所言“对中断发生器的响应是通过系统桥来实现的,通过 store 类指令访问地址 0x7F20,就可以达到响应中断的目的”。
  • 异常处理是怎样处理的?
    这个问题真的困扰了我很久,但其实这是我们不用考虑的,异常处理是异常处理程序的事情,而我们的硬件只需要实现能够支持异常处理程序执行,也就是可以忠诚地翻译相应指令并且实现异常处理程序所需要的接口(也可以理解为保存好现场)即可。

模块封装

我感觉刚做P7会有一种手忙脚乱的感觉,现在回看,模块封装的思想会帮助我们理清到底要干什么。其实我们要做的事情就是:

  • 在原来的流水线CPU中加入CP0模块,将其封装成新的CPU模块(这其中当然包括各种异常信号的识别与流水)
  • 实现Bridge模块
  • 实例化两个Timer
  • 将CPU、Bridge 、Timer0、Timer1封装成我们的MIPS微系统
    然后按照一个模块一个模块来实现就好

工程模块定义

CP0

信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 同步复位信号
en I 1 写使能信号
BDIn I 1 是否延迟槽指令
EXLClr I 1 eret指令,复位EXL
ExcCode I 5 异常类型编码
HWInt I 6 中断信号
CP0Addr I 5 需要访存的寄存器地址
CP0In I 32 需要向CP0寄存器写入的数据,mtc0
vPC I 32 受害PC,即M_pc
Req O 1 进入异常处理程序请求
EPCout O 32 EPC的值输出
CP0out O 32 CP0寄存器读出的数据,mfc0

CPU

信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 同步复位信号
i_inst_rdata I 32 读入指令,来自Bridge
m_data_rdata I 32 从DM/Timer/IG读入数据,来自bridge
interrupt I 1 IG输入中断信号(?)
Timer1 I 1 Timer1输入中断信号
Timer0 I 0 Timer0输入中断信号
i_inst_addr O 32 F级pc,写向Bridge
m_data_addr O 32 M级访存数据addr,写向Bridge
m_data_wdata O 32 M级写入数据,写向Bridge
m_data_byteen O 4 M级写入使能,写向Bridge
m_inst_addr O 32 M级PC
w_grf_we O 1 GRF写入使能
w_grf_addr O 5 GRF写入的寄存器编号
w_grf_wdata O 32 GRF写入数据
w_inst_addr O 32 W级PC
macroscopic_pc O 32 宏观PC,这里是M级PC

Bridge

信号名 方向 位宽 描述
A_in I 32 读写外设单元地址,来自CPU
WD_in I 32 写入外设单元的数据,来自CPU
byteen I 4 写入外设单元使能,来自CPU
DM_RD I 32 DM读取值输入,来自外设
T0_RD I 32 T0读取值输入,来自外设
T1_RD I 32 T1读取值输入,来自外设
RD_out O 32 从外设读取的数据,写向CPU
A_out O 32 读写外设单元地址,写向外设
WD_out O 32 写入外设单元的数据,写向外设
DM_byteen O 4 DM写入字节使能,写向外设
T0_WE O 1 T0写入使能
T1_WE O 1 T1写入使能
m_int_addr O 32 中断发生器写入地址,写向外设
m_int_byteen O 32 中断发生器写入字节使能,写向外设

TC

信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 同步复位信号
Addr I 30 Timer写入地址
WE I 1 Timer写入使能
Din I 32 Timer写入数据
Dout O 32 Timer读出数据
IRQ O 1 Timer中断请求

mips顶层模块

信号名 方向 位宽 描述
clk I 1 时钟信号
reset I 1 同步复位信号
interrupt I 1 外部中断信号
macroscopic_pc O 32 宏观pc
i_inst_addr O 32 IM读取地址,pc
i_inst_rdata I 32 IM读取数据,来自外设
m_data_addr O 32 DM读写地址
m_data_rdata I 32 DM读取地址,来自外设
m_data_wdata O 32 DM待写入数据
m_data_byteen O 32 DM字节使能信号
m_int_addr O 32 中断发生器待写入地址
m_int_byteen O 4 中断发生器字节使能信号
m_inst_addr O 32 M级PC
w_grf_we O 1 GRF写使能信号
w_grf_wdata O 32 GRF待写入数据
w_inst_addr O 32 W级pc

重要方法实现

CP0响应

  • 中断信息通过HWInt传入{3’b000,interrupt,Timer1,Timer0}
  • 异常信息通过ExcCode和BDIn传入
    异常信号
    1
    2
    3
    assign IntReq = ((|(`IM & HWInt)) && `IE && (!`EXL)); //Interrupt
    assign ExcReq = ((|ExcCodeIn) && (!`EXL)); //Exception
    assign Req = IntReq || ExcReq;
    中断优先级高于异常
    1
    `ExcCode <= (IntReq) ? 5'b00000 : ExcCodeIn;
    EPC的确定以及对于Req的操作
    1
    2
    3
    4
    5
    6
    7
    assign EPCout = (Req) ? ((BDIn) ? (vPC - 4) : vPC) : EPC ;
    if (Req) begin //int or exc
    `EXL <= 1'b1;
    `BD <= BDIn;
    `ExcCode <= (IntReq) ? 5'b00000 : ExcCodeIn;
    EPC <= EPCout;
    end

系统桥

系统桥实际上是实现与外设交换的功能,纯组合逻辑运算,代码如下:

1
2
3
4
5
6
7
8
9
10
11
assign A_out = A_in;
assign WD_out = WD_in;
assign DM_byteen = (A_in >= 32'h0000_0000 && A_in <= 32'h0000_2fff) ? byteen : 4'b0000;
assign T0_WE = (A_in >= 32'h0000_7f00 && A_in <= 32'h0000_7f0b) ? (&byteen) : 1'b0;
assign T1_WE = (A_in >= 32'h0000_7f10 && A_in <= 32'h0000_7f1b) ? (&byteen) : 1'b0;
assign m_int_addr = (|m_int_byteen) ? A_in : 32'h0000_0000;
assign m_int_byteen = (A_in >= 32'h0000_7f20 && A_in <= 32'h0000_7f23) ? byteen : 4'b0000;
assign RD_out = (A_in >= 32'h0000_0000 && A_in <= 32'h0000_2fff) ? DM_RD :
(A_in >= 32'h0000_7f00 && A_in <= 32'h0000_7f0b) ? T0_RD :
(A_in >= 32'h0000_7f10 && A_in <= 32'h0000_7f1b) ? T1_RD :
32'h0000_0000;

异常识别

前一级异常信号优先级更高,中断比异常优先级高

  • F级
    • AdEL 取指异常
      1
      assign F_ExcCode =  ((F_pc[1:0] != 2'b00) || (!((F_pc >= 32'h0000_3000) && (F_pc <= 32'h0000_6ffc))) ) ? `AdEL : 5'b00000;
  • D级
    • RI 未知指令
    • Syscall指令
      1
      2
      3
      4
      assign D_ExcCode_fixed = (|D_ExcCode) ? D_ExcCode :
      (D_syscall) ? `Syscall :
      (D_RI) ? `RI :
      5'b00000 ;
  • E级
    • Ov 算术指令计算溢出
    • AdEL load类指令范围计算溢出
    • AdES store类指令范围计算溢出
      1
      2
      3
      4
      5
      assign E_ExcCode_fixed = (|E_ExcCode) ? E_ExcCode :
      (E_OvCalInstr) ? (E_Overflow ? `Ov : E_ExcCode) :
      (E_OvLoadInstr) ? (E_Overflow ? `AdEL : E_ExcCode) :
      (E_OvSaveInstr) ? (E_Overflow ? `AdES : E_ExcCode) :
      5'b00000;
  • M级
    • AdEL lw取数没有字对齐
    • AdES sw存数没有字对齐
    • AdEL lh取数没有半字对齐
    • AdES sh存数没有半字对齐
    • AdEL load类指令取数超出DM、T0、T1、IG范围
    • AdES store类指令存数超出DM、T0、T1、IG范围
    • AdEL lh、lb指令访问Timer寄存器
    • AdES sh、sb指令访存Timer寄存器
    • AdES sw向Timer的count寄存器存数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      assign M_ExcCode_fixed = (|M_ExcCode) ? M_ExcCode :
      ((M_lw || M_lh || M_lb) && (!M_DM_Addr) && (!M_Timer_Addr) && (!M_IG_Addr)) ? `AdEL :
      ((M_sw || M_sh || M_sb) && (!M_DM_Addr) && (!M_Timer_Addr) && (!M_IG_Addr)) ? `AdES ://越界
      ((M_lw) && (M_AR[1:0] != 2'b00)) ? `AdEL :
      ((M_sw) && (M_AR[1:0] != 2'b00)) ? `AdES : //没有字对齐
      ((M_lh) && (M_AR[0] != 1'b0)) ? `AdEL :
      ((M_sh) && (M_AR[0] != 1'b0)) ? `AdES : //没有半字对齐
      ((M_lh || M_lb) && M_Timer_Addr) ? `AdEL : //半字或字节访问Timer
      ((M_sh || M_sb) && M_Timer_Addr) ? `AdES : //半字或字节写入Timer
      ((M_sw) && ((M_AR == 32'h0000_7f08) || (M_AR == 32'h0000_7f18))) ? `AdES :
      5'b00000;

思考题解答

  1. 请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)
    答:这样更方便CPU的统一设计。我认为是可以的,用户需要多输入一个信号,即异常处理程序的初始地址。问题就是,用户提供的初始地址可能与本来的其他存储地址起冲突。
  2. 为何与外设通信需要 Bridge?
    答:因为我们的CPU可能会与许多外设相连,如果为每一个外设提供一套数据和地址,就会让CPU变得非常复杂。这里Bridge充当一个交互的角色,将多个外设与CPU连起来,这样我们的CPU只用提供一个接口即可。
  3. 倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?
    答:此时宏观pc会突然变成0x3000这与我们想把整个CPU包装成单周期CPU的想法冲突。stall清空E级流水寄存器应该保留pc(保证宏观pc正确)和BD信号(保证该nop走到M级遇见中断写入CP0的pc信息是正确的)
  4. 为什么 jalr 指令为什么不能写成 jalr $31, $31?
    答:因为jalr $31,$31,是要同时读写$31,而GRF具有内部转发,也就是在其他i1与i2同时读写一个寄存器时,会直接把写的数据加载到读端口,如此jalr就读出的是PC+4,并没有实现原先的跳转功能。

Bug

因为自己和身边的同学都或多或少找到了中测没测出来的bug,我觉得大家如果愿意的话可以在这篇帖子下面分享自己bug。(P7debug过程实在艰难)

  • Req越沿采样错误,应该是Req置高的下一个周期F级pc才变为0x4180(笔者的Req是wire型,组合逻辑运算输出)
    此处再次引用助教的讲解,信号在一个时钟周期中应该是左开右闭的。左开是为了在时钟上升沿保持稳定,右闭是为了保证下一时钟上升沿在稳定状态采样,所以要遵循这个理念,不能够越沿采样。
  • 中断的优先级高于异常,即如果CP0中中断信号有效那么ExcCode就是5'd0,否则才是输入的ExcCode
  • sw指令可以访存Timer的前两个寄存器,而不能访问count寄存器,这里可以根据Timer设计文档确定count的Addr
  • W级流水寄存器在Req信号置高时也是要清空的,所有流水寄存器都要在Req信号置高时清空
  • 如果E级是mtlo/mthi/mult/div这些指令,M级输出Req,那么不要执行E级计算
  • 各种地址范围敲错
  • 中断的时候DM写使能也要关掉!req|ExcCode别写混了,想清楚再写

测试方案

  • function测试
    过了P6就问题不大,其实功能部分是不用怎么修改的
  • exception测试
    就是把教程提到的各种异常都枚举测试一遍,尤其注意乘除槽相关
  • interrupt测试
    枚举各种情况下的中断,写一个支持中断的tb

祝上机顺利!

  • Post title:P7
  • Post author:Coooookie
  • Create time:2023-01-02 20:30:20
  • Post link:https://coooookie0913.github.io/2023/01/02/P7/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.