前言
本文介绍自己前期用Verilog编写的基于RV32I指令集的CPU,个人认为是最简单最易懂的版本(即没做什么优化,即代码很水,没有考虑一些奇怪的情况,只适用简单指令序列,即题目所示最简单的处理器),适合新手练手Verilog以及了解简单CPU的运行过程。本文基于一生一芯,利用Verilator进行仿真,并用gtkwave查看波形,不了解verilator和gtkwave的可以看前几篇文章。
CPU数据通路
首先参考南京大学的单周期RV32ICPU,画出自己处理器的数据通路,主要包括PC自增,跳转(Branch),指令寄存器,通用寄存器,译码,ALU,内存Ram等部件。
基本上是之前实验的一个集成。

不多说了,直接上代码。
Makefile
同样,makefile参考NVboard的编写,不了解的可以看前几篇文章
TOPNAME = top
NXDC_FILES = constr/top.nxdc
INC_PATH ?=
VERILATOR = verilator
VERILATOR_CFLAGS += -MMD --build --trace -cc \
-O3 --x-assign fast --x-initial fast --noassert
BUILD_DIR = ./build
OBJ_DIR = $(BUILD_DIR)/obj_dir
BIN = $(BUILD_DIR)/$(TOPNAME)
default: $(BIN)
$(shell mkdir -p $(BUILD_DIR))
# constraint file
SRC_AUTO_BIND = $(abspath $(BUILD_DIR)/auto_bind.cpp)
$(SRC_AUTO_BIND): $(NXDC_FILES)
python3 $(NVBOARD_HOME)/scripts/auto_pin_bind.py $^ $@
# project source
VSRCS = $(shell find $(abspath ./vsrc) -name "*.v")
CSRCS = $(shell find $(abspath ./csrc) -name "*.c" -or -name "*.cc" -or -name "*.cpp")
CSRCS += $(SRC_AUTO_BIND)
# rules for NVBoard
include $(NVBOARD_HOME)/scripts/nvboard.mk
# rules for verilator
INCFLAGS = $(addprefix -I, $(INC_PATH))
CXXFLAGS += $(INCFLAGS) -DTOP_NAME="\"V$(TOPNAME)\""
LDFLAGS += -lSDL2 -lSDL2_image
$(BIN): $(VSRCS) $(CSRCS) $(NVBOARD_ARCHIVE)
@rm -rf $(OBJ_DIR) wave.vcd
$(VERILATOR) $(VERILATOR_CFLAGS) \
--top-module $(TOPNAME) $^ \
$(addprefix -CFLAGS , $(CXXFLAGS)) $(addprefix -LDFLAGS , $(LDFLAGS)) \
--Mdir $(OBJ_DIR) --exe -o $(abspath $(BIN))
all: default
run: $(BIN)
./build/top +trace
clean:
rm -rf $(BUILD_DIR) wave.vcd
.PHONY: default all clean run
sim_main.cpp
cpp文件也是可以参考前几篇文章
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<nvboard.h>
#include"Vtop.h"
#include"verilated.h"
#include"verilated_vcd_c.h"
//static Vtop* top;
void nvboard_bind_all_pins(Vtop* top);
// void single_cycle(){
// top->clk = 0; top->eval(); m_trace->dump(contextp->time()); // update output
// top->clk = 1; top->eval(); m_trace->dump(contextp->time());
// }
// void reset(int n) {
// top->rst = 1;
// while (n-- > 0) single_cycle();
// top->rst = 0;
// }
int main(int argc, char** argv) {
VerilatedContext* contextp = new VerilatedContext;
contextp->traceEverOn(true);
contextp->commandArgs(argc, argv);
Vtop* top = new Vtop{
contextp};
nvboard_bind_all_pins(top);
nvboard_init();
VerilatedVcdC* m_trace = new VerilatedVcdC;
top->trace(m_trace, 99);
m_trace->open("wave.vcd");
//reset(10);
while (!contextp->gotFinish()) {
//single_cycle();
nvboard_update();
contextp->timeInc(1);
top->clk = 0; top->eval(); // update output
m_trace->dump(contextp->time());
contextp->timeInc(1);
top->clk = 1; top->eval();
m_trace->dump(contextp->time());
}
m_trace->close();
delete top;
delete contextp;
return 0;
}
Verilog
指令寄存器instr_mem
首先我们考虑如何存储我们的指令,这是一切的开始,cpu本质上就是在不断执行一条条的指令。
很简单,利用一个每个元素为32位的大数组instr_memory进行指令存储。每次取指令只需要根据instr_addr进行数组访问,即可取出指令。需要注意的是,pc+4相当于指令寄存器的地址+1。(为什么? 详见计组)
module instr_mem(
input [31:0] instr_addr,
output reg [31:0] instr
);
reg [31:0] instr_memory [65535:0];
always@(*)begin
instr = instr_memory[instr_addr>>2]; // pc + 4 -> instr_addr + 1
end
initial begin
instr_memory[0] = {
12'b000000000001,5'b00001,3'b000,5'b00010,7'b0010011};
// imm rs1 func3 rd opcode
instr_memory[1] = {
12'b000000000001,5'b00010,3'b001,5'b00011,7'b0010011};
end
endmodule
id
取完指令,就该进行指令的译码了。一条指令的各个字段代表什么意思,即如何解读指令,是由指令集定义的。这里使用的RISC-V指令集。

译码器本质上就是根据指令的内容,将其分割并根据各个字段的意义,生成控制字段送往各个部件。
译码器写得很垃圾,但是足够简单明了,旨在希望你理解译码器的工作流程(其实是没时间优化)一共400多行的译码代码。
1.根据opcode字段(最低7位),识别出指令是什么类型(R-TYPE还是I-TYPE)
2.再根据func3(12-14位)识别出是什么具体的指令,有的指令还需要根据func7(28-31位)进行识别。
3.经过前两步可以识别一条具体的指令,根据具体指令的操作将对应的控制信号传出。
这就是译码部件的全部过程,可能你疑惑rs1和rs2为什么不传出,建议回去再看一下数据通路图,你应该能理解。(具体原因描述在通用寄存器实现处)
module id(
input [31:0] instr,
output reg [31:0] imm,
output reg [3:0] ALUctr,
output reg ALUBsrc,
output reg RegWr,
output reg [2:0] branch,
output reg MemtoReg,
output reg [2:0] MemOp,
output reg MemWr
);
reg [6:0] opcode;
reg [2:0] func3;
reg [6:0] func7;
reg [31:0] immI;
reg [31:0] immS;
reg [31:0] immB;
reg [31:0] immJ;
assign opcode = instr[6:0];
assign func3 = instr[14:12];
assign func7 = instr[31:25];
assign immI = {
{
20{
instr[31]}}, instr[31:20]};
assign immS = {
{
20{
instr[31]}}, instr[31:25], instr[11:7]};
assign immB = {
{
20{
instr[31]}}, instr[7], instr[30:25], instr[11:8], 1'b0};
assign immJ = {
{
12{
instr[31]}}, instr[19:12], instr[20], instr[30:21], 1'b0};
always @(*) begin
case(opcode)
7'b0010011: begin // instr type I
case(func3)
3'b000: begin // addi
imm = immI;
ALUctr = 4'b0000;
ALUBsrc = 1'b1;
RegWr = 1'b1;
branch = 3'b000;
148




被折叠的 条评论
为什么被折叠?



