用于记录学习PA2的过程,技术还很菜,要是有错误或者改进的地方加一下qq:2084625064
注意:初学者不要用行为建模
解释Verilog代码
我们不建议初学者在Verilog代码中编写任何always语句. 为了方便大家使用触发器和选择器, 我们提供了如下Verilog模板给大家进行调用:
// 触发器模板
module Reg #(WIDTH = 1, RESET_VAL = 0) (
input clk,
input rst,
input [WIDTH-1:0] din,
output reg [WIDTH-1:0] dout,
input wen
);
always @(posedge clk) begin
if (rst) dout <= RESET_VAL;
else if (wen) dout <= din;
end
endmodule
// 使用触发器模板的示例
module example(
input clk,
input rst,
input [3:0] in,
output [3:0] out
);
// 位宽为1比特, 复位值为1'b1, 写使能一直有效
Reg #(1, 1'b1) i0 (clk, rst, in[0], out[0], 1'b1);
// 位宽为3比特, 复位值为3'b0, 写使能为out[0]
Reg #(3, 3'b0) i1 (clk, rst, in[3:1], out[3:1], out[0]);
endmodule
// 选择器模板内部实现
module MuxKeyInternal #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1, HAS_DEFAULT = 0) (
output reg [DATA_LEN-1:0] out,
input [KEY_LEN-1:0] key,
input [DATA_LEN-1:0] default_out,
input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
localparam PAIR_LEN = KEY_LEN + DATA_LEN;
wire [PAIR_LEN-1:0] pair_list [NR_KEY-1:0];
wire [KEY_LEN-1:0] key_list [NR_KEY-1:0];
wire [DATA_LEN-1:0] data_list [NR_KEY-1:0];
genvar n;
generate
for (n = 0; n < NR_KEY; n = n + 1) begin
assign pair_list[n] = lut[PAIR_LEN*(n+1)-1 : PAIR_LEN*n];
assign data_list[n] = pair_list[n][DATA_LEN-1:0];
assign key_list[n] = pair_list[n][PAIR_LEN-1:DATA_LEN];
end
endgenerate
reg [DATA_LEN-1 : 0] lut_out;
reg hit;
integer i;
always @(*) begin
lut_out = 0;
hit = 0;
for (i = 0; i < NR_KEY; i = i + 1) begin
lut_out = lut_out | ({DATA_LEN{key == key_list[i]}} & data_list[i]);
hit = hit | (key == key_list[i]);
end
if (!HAS_DEFAULT) out = lut_out;
else out = (hit ? lut_out : default_out);
end
endmodule
// 不带默认值的选择器模板
module MuxKey #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1) (
output [DATA_LEN-1:0] out,
input [KEY_LEN-1:0] key,
input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
MuxKeyInternal #(NR_KEY, KEY_LEN, DATA_LEN, 0) i0 (out, key, {DATA_LEN{1'b0}}, lut);
endmodule
// 带默认值的选择器模板
module MuxKeyWithDefault #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1) (
output [DATA_LEN-1:0] out,
input [KEY_LEN-1:0] key,
input [DATA_LEN-1:0] default_out,
input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
MuxKeyInternal #(NR_KEY, KEY_LEN, DATA_LEN, 1) i0 (out, key, default_out, lut);
endmodule
解释上面的Verilog代码,其中第一个代码比较简单,就直接跳过。第二个代码是否带默认值的选择器就是给HAS_DEFAULT赋值的不同判断,所以我们这里只关注MuxKeyInternal模块。
其中PAIR_LEN就是保存lut列表中一段数据键值对的长度,pair_list就是保存保存每一个键值对的内容,key_list只保存匹配值的内容,data_list保存的是对应匹配值输出的内容。genvar
是一种通用变量类型,用于在生成结构(如 generate
块)中进行循环控制或其他动态生成的场景。generate
是 Verilog 中的生成语句,用于在代码中动态地生成重复的结构或模块实例。在generat的for循环便是对上面说的三个值进行赋值。下面的always语句就分别对lut_out和hit赋值。lut_out就是保存输出值,hit是判断是否输入的key和匹配值匹配。
下面是2选1多路选择器和4选1多路选择器的例子
module mux21(a,b,s,y);
input a,b,s;
output y;
// 通过MuxKey实现如下always代码
// always @(*) begin
// case (s)
// 1'b0: y = a;
// 1'b1: y = b;
// endcase
// end
MuxKey #(2, 1, 1) i0 (y, s, {
1'b0, a,
1'b1, b
});
endmodule
module mux41(a,s,y);
input [3:0] a;
input [1:0] s;
output y;
// 通过MuxKeyWithDefault实现如下always代码
// always @(*) begin
// case (s)
// 2'b00: y = a[0];
// 2'b01: y = a[1];
// 2'b10: y = a[2];
// 2'b11: y = a[3];
// default: y = 1'b0;
// endcase
// end
MuxKeyWithDefault #(4, 2, 1) i0 (y, s, 1'b0, {
2'b00, a[0],
2'b01, a[1],
2'b10, a[2],
2'b11, a[3]
});
endmodule
注意在模块名和宏定义前添加学号
在NPC中实现第一条指令
任务1.:
如果你是初学者, 尝试自己画出架构图
如果你是第一次接触处理器设计, 尝试自己画出仅支持addi
指令的单周期处理器的架构图.
是第一次接触这方面的知识,不一定对,注意甄别
这里的架构图只画出了addi命令的图,所以运算器是加法操作的过程。
addi命令,对imm符号位扩展后与x[rs1]相加,结果写入x[rd]。忽略算出溢出
在网上我又搜索了一些知识IFU的知识:
IFU:根据程序计数器(PC)的值从指令存储器中读取当前要执行的指令。更新PC的值。负责从指令存储器中获取指令,并将其传送到指令解码单元(IDU)。
任务2:
具体地, 你需要注意以下事项:
- PC的复位值设置为
0x80000000
- 存储器中可以放置若干条
addi
指令的二进制编码(可以利用0号寄存器的特性来编写行为确定的指令) - 由于目前未实现跳转指令, 因此NPC只能顺序执行, 你可以在NPC执行若干指令之后停止仿真
- 可以通过查看波形, 或者在RTL代码中打印通用寄存器的状态, 来检查
addi
指令是否被正确执行 - 关于通用寄存器, 你需要思考如何实现0号寄存器的特性; 此外, 为了避免选择Verilog的同学编写出不太合理的行为建模代码, 我们给出如下不完整的代码供大家补充(大家无需改动
always
代码块中的内容):
module RegisterFile #(ADDR_WIDTH = 1, DATA_WIDTH = 1) (
input clk,
input [DATA_WIDTH-1:0] wdata,
input [ADDR_WIDTH-1:0] waddr,
input wen
);
reg [DATA_WIDTH-1:0] rf [2**ADDR_WIDTH-1:0];
always @(posedge clk) begin
if (wen) rf[waddr] <= wdata;
end
endmodule
任务3:
实现addi的单周期指令。
这一段我花了很多时间,想怎么可以从计算机中取出对应地址的内容,后面阅读了一些文章,我打算从激励代码中下手。而且发现自己对Verilator比较陌生,于是读了下面这篇文章Verilator 使用指南 - USTC CECS 2023,下面我会附上自己的代码,注意学术诚信,这里只是提供一种思路。
这里先介绍一下DPI-C
DPI-C 是 Verilator 提供的一种机制,可以在 Verilog 代码中调用 C/C++ 中定义的 C 语言函数。
使用方法如下:
- 在 Verilog 代码中,使用
import "DPI-C" function <return_type> <function_name>(<argument_list>);
来声明一个 C 语言函数; - 在 Verilog 代码中,使用
<function_name>(<argument_list>);
来调用这个函数; - 在 C/C++ 代码中,实现这个函数。
写的时候突然想到我当时的一个误区,可能大家也存在,就是rv32从PC地址取出来的指令,里面imm直接保存的就是数值,但是寄存器保存的是寄存器编号。
module ysyx_24080014_cpu(
input clk,
input rst,
output reg[31:0]outdata
);
//声明
reg [31:0]next_pc;
//reg wen;//用来判断是否写入
reg [6:0]op;
reg [4:0]rd;
reg [2:0]func3;
reg[31:0]pc;
wire [31:0]ins;
//rs1,imm保存的是地址,需要取出对应的内容,所以需要扩展到32位
reg [31:0] rs1;
reg [31:0] imm;
reg [31:0] rs1_data;
reg [31:0] imm_data;
//初始化
initial begin
pc = 32'h8000_0000;
next_pc = pc + 4;
end
//pc
always @(posedge clk or posedge rst)begin
if(rst)begin
pc <= 32'h8000_0000;
next_pc <= pc + 4;
end
else begin
pc <= next_pc;
next_pc <= pc + 4;
end
end
//IFU取指
ysyx_24080014_ifu ifu(
.pc(pc),
.clk(clk),
.ins(ins)
);
//IDU单纯取指,这里因为是单周期addi指令,所以只有rs1没有rs2
ysyx_24080014_idu idu(
.ins(ins),
.op(op),
.rd(rd),
.clk(clk),
.func3(func3),
.rs1(rs1),
.imm(imm)
);
//ALU
ysyx_24080014_alu alu(
.imm(imm_data),
.rs1(rs1_data),
.func3(func3),
.clk(clk),
//.wen(wen),
.outdata(outdata)
);
endmodule
import "DPI-C" function int gpr(int idx);
module ysyx_24080014_idu(//单纯取指
input [31:0] ins, // 指令
output [6:0] op,
output [4:0] rd,
input clk,
output [2:0] func3,
output reg[31:0] rs1,
output reg[31:0] imm
);
reg [31:0]ins1;
initial
ins1 = {{27{1'b0}},ins[19:15]};
always @(posedge clk) begin
rs1 = gpr(ins1);
imm = {{20{1'b0}},ins[31:20]};
end
endmodule
module ysyx_24080014_ifu(
input [31:0]pc,
input clk,
output reg[31:0]ins
);
//import "DPI-C" function int init_mem(int size);
always @(posedge clk)
ins = 32'b000000000101_00000000000010010011;
endmodule
module ysyx_24080014_alu(
input [31:0]imm,
input [31:0]rs1,
input [2:0]func3,
input clk,
// output reg wen,
output reg [31:0]outdata
);
always @(posedge clk)begin
case(func3)
3'b000:begin
outdata <= 32'b0 + imm;//加法
//wen <= 1'b1;
end
default:begin
$display("ERROR!");
end
endcase
end
endmodule
这里写的是一个不完全的代码,可以运行但是我觉得不是最好的,最后好的代码我没有放出来,这里只是提供一个思路。比如取指ifu这些都应该用DPI-C来编写,但是我第一次测试的时候为了方便就没有那样写,直接把指令写了进去 ,还有rst部分是有错误的,我都没有修改