一生一芯--用RTL实现最简单的处理器

用于记录学习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 语言函数。
使用方法如下:

  1. 在 Verilog 代码中,使用 import "DPI-C" function <return_type> <function_name>(<argument_list>); 来声明一个 C 语言函数;
  2. 在 Verilog 代码中,使用 <function_name>(<argument_list>); 来调用这个函数;
  3. 在 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部分是有错误的,我都没有修改

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值