框架介绍
仅仅完成 CPU 的设计是不够的,我们需要一些特别的工具检验并控制 CPU,这就需要仿真框架和上板框架。
如何判断是否使用的是最新的 PDU?
你可以查看 UART_TX.v 的第 52 行,如果为 if (counter == 2604) begin
,则代表使用的是最新的 PDU。如果你下载的不是最新的 PDU,可以在清除浏览器缓存后重新下载。注意:不要通过私自修改文件的方式将 PDU 升级!
CPU 顶层接口
提醒
无论是 RV32I 还是 LA32R,本次实验都需要使用下面的接口!
本次实验中 CPU 的顶层模块如下所示:
CPU | |
---|---|
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 |
|
其中:
-
global_en
信号为来自 PDU 的控制信号。该信号在本次实验中会控制 PC 寄存器的写使能,从而控制 CPU 是否执行指令。我们可以使用下面的代码例化 PC 寄存器:1 2 3 4 5 6 7
PC my_pc ( .clk (clk ), .rst (rst ), .en (global_en ), // 当 global_en 为高电平时,PC 才会更新,CPU 才会执行指令。 .npc (cur_npc ), .pc (cur_pc ) );
-
Memory (inst)
下的接口将连接到指令存储器。请注意程序段的地址范围:RV32I 指令集 0x00400000 ~ 0x0ffffffc、LA32R 指令集 0x1c000000~0x1c7ffffc; Memory (data)
下的接口将连接到数据存储器。本次实验中,dmem_we
、dmem_addr
和dmem_wdata
端口在 CPU 模块内部无需连接;Debug
下的接口会用于仿真和上板框架,下面介绍了这些端口应当如何连接。
CPU 与仿真框架的适配
本学期开发的仿真框架内部使用 C++ 搭建了一个经过正确性测试的 CPU(以下统称标准核)。在运行仿真时,框架会首先将用户编写的 Verilog 代码翻译成 C++ 代码(以下统称用户核)。接下来,框架会单步运行用户核与标准核,并比较二者运行的状态。如果不一致,框架会记录相应的 CPU 状态并告知用户,从而高效定位设计中可能存在的问题。
这里涉及到一个问题:如何精确描述 CPU 的状态呢?自然,我们可以记录 CPU 内部所有的信号,然后挨个进行对比。然而不同用户的设计也不完全相同,我们也不能统一各个信号的名称与作用,所以这样的思路是不现实的。事实上,CPU 的状态可以由 PC、寄存器堆和存储器的写入状态完全确定:只要保证 CPU 正确写入了这些组件,就可以保证 CPU 运行的正确性。比如:如果 ALU 结果算错了,那么最后写回寄存器堆的数据就会出错;如果分支跳转出错了,那么写入 PC 的数据就会出错;如果访存地址算错了,那么写入数据存储器的地址就会错误。
我们的仿真框架正是基于这样的思想设计的:比较每一条指令运行完成后,标准核与用户核在 PC、寄存器堆和存储器的写入状态上的异同,即可判断用户核是否存在潜在的问题。
回到我们 CPU 的端口上:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这些端口中,commit
系列的端口即为每一条指令执行完成后,提交(Commit)给仿真框架的写状态信号。其中:
commit
端口用于指示当前是否有一条指令执行完成。在单周期 CPU 中,只要 PC 发生了变化,就必然有一条指令执行完成(因为一个周期执行一条指令),所以我们在后面的代码中将其设置为 1。commit_pc
为这条指令的 PC。commit_inst
为这条指令本身。commit_halt
用于指示这条指令是否是指令序列中的最后一条指令。你可能已经注意到了,Lars 在编译时会在汇编代码末尾添加一条指令 0x80000000,这条指令是我们约定的HALT
指令,用于指示当前程序已经运行完成。仿真框架在获得这个信号后就会停止比对。commit_reg
系列用于指示寄存器堆的写状态。commit_dmem
系列用于指示数据存储器的写状态。
debug
系列的端口用于仿真框架进行更为具体的比对操作,即判断用户核的 32 个寄存器的数值是否与标准核一致。
为了同步比对的时序,我们将上述信号在 CPU 中暂存一级后给出。因此,CPU 模块的最后需要添加如下的代码:
适配框架的代码 | |
---|---|
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 |
|
HALT 指令
请根据你所使用的指令集正确设置 HALT 指令
为了保证汇编程序运行完成后,PDU 能够知道 CPU 已经运行完成,CPU 需要在程序执行完成后向 PDU 发出一个特殊的信号,在这里就是我们的 halt
信号。为此,我们可以在汇编程序最后添加一个特殊的「指令」,保证其不会被用在正常的汇编程序中即可。
- 对于 RV32R 指令集,你可以使用
ebreak
指令,它对应的机器码是 0x00100073,因此我们需要将HALT_INST
设置为 32'H00100073。你可以在汇编代码的最后添加一条ebreak
指令,以标识程序的结束; - 对于 LA32R 指令集,我们的仿真框架规定了中断指令的机器码是 0x80000000,因此在硬件设计时需要保持一致。你需要手动在 COE 文件的末尾添加这条机器码。这样,当程序执行到这一条「指令」时,CPU 就可以产生
commit_halt
信号,进而被 PDU 捕捉到。
上板框架概述
本次实验提供的上板框架基本结构如下:
项目的文件结构如下:
vsrc
├─ CPU <---------------------------------- 你只需要修改 CPU 目录下的内容
│ └─ CPU.v
├─ MEM <---------------------------------- 内存管理
│ ├─ MEM_BRIDGE.v
│ └─ MMIO.v
├─ PDU <---------------------------------- PDU 及其附属模块
│ ├─ PDU.v
│ ├─ RTL
│ │ ├─ BP_LIST_REG.v <------------------ 用于记录并处理断点
│ │ ├─ INFO_SENDER.v <------------------ 用于向串口输出提示信息
│ │ └─ PDU_decode.v <-------------------- 用于对串口数据进行译码
│ ├─ UART
│ │ ├─ UART_RX.v
│ │ └─ UART_TX.v
│ └─ UTILS
│ ├─ HEX2ASCII.v
│ ├─ HEX2UART.v
│ ├─ POSEDGE_GEN.v
│ ├─ QUEUE.v
│ └─ Segment.v
└─ TOP.v <-------------------------------- 项目的顶层文件
其中,我们在 TOP.v 文件中例化了指令存储器和数据存储器。为此,你需要在自己的项目中创建两个分布式存储器 IP 核,并按照下图所示的参数进行配置(注意将 IP 核命名为 INST_MEM 和 DATA_MEM):
完成后,在 Vivado 的项目中将 vsrc 文件夹导入,即可使用本次实验的上板框架。