While I was waiting for my weekly chicken roast to cook, I had this really bad idea for a challenge. how much riscv verilog i can write from scratch in the next 2 hours until the chicken is done?

So, yeah that’s what I did on the last day of new year vacation.

2 hours to V hours Link to heading

I thought to start with a skeleton for single-cycle (See [H and H][1]) and try to build the blocks bottom up style.

At the 2 hour mark(minus the chicken prep time), I wrote

  • Basic small blocks.
  • Loaded make-shift hex.
  • Spent 20 minutes to make VCD work with iverilog(I want that time back)

Not bad but it was obvious I failed the 2-hour challenge. So, I changed it to 5 hours instead. It seemed like a lucky number :)

Basic blocks Link to heading

  • RAM
module ram #(parameter WIDTH=32, parameter ADDR=32) (
	input wire clk,
	input wire we,
    input wire [ WIDTH - 1: 0] din,
    input wire [ADDR-1 : 0] addr,
    output wire [ WIDTH - 1: 0] dout
);
	reg [WIDTH-1:0] mem [ 100 : 0]; 

	assign dout = mem[addr]; 


	always @(posedge clk) 	begin
		if (we) begin
			$display("%t Writing ram[0x%0x]=0x%0x", $time, addr, din);
			mem[addr] = din;
		end
	end
endmodule
  • ROM

module rom #(parameter WIDTH=32, parameter ADDR=32) (
    input  wire [ADDR  - 1 : 0] addr,
    output wire [WIDTH - 1 : 0] dout
);

	reg [WIDTH-1:0] mem [ 100 : 0];

	assign dout = mem[addr]; 
endmodule
  • Register file
module regfile#(parameter WIDTH=32) (
	input clk,
	input [4:0] addr1,
	output [31:0] dout1,
	
	input [4:0] addr2,
	output [31:0] dout2,

	input [4:0] addr3,
	input [31:0] din3,
	input we3
);
	reg [WIDTH-1:0] mem [0:31] ;
	
	// x0 is zero
	assign dout1 = addr1 == 0 ? 5'b00000: mem[addr1];
	assign dout2 = addr2 == 0 ? 5'b00000: mem[addr2];

	always @(posedge clk) begin
		// Don't write to x0
		if (we3 && (addr3 != 5'b00000)) begin
			$display("%t Writing regfile[%0d]=0x%0x", $time, addr3, din3);
			mem[addr3] <= din3;
		end
	end

endmodule
  • ALU(add only)

Data-path and control unit Link to heading

I started with LW datapath then control to pipe-clean the connection all the way to memory and back to regfile and I used the usual stages:

  • Fetch
  • Decode
  • Execute
  • Write-back

LW and SW follow similar format except the immediate. So, Decoding was not that hard

        assign opcode   = instr[`OP_RANGE];
        assign funct3   = instr[`FUNCT3_RANGE];
        assign rf1              = instr[`RS1_RANGE];
        assign rf2              = instr[`RS2_RANGE];
        assign rf3              = instr[`RD_RANGE];
        assign imm11_0 = (imm_mux == 0 )? instr[`IMM_11_0_RANGE] : {instr[31:25], instr[11:7]};

And control unit is not complicated as well.

       case (opcode)
                        7'b0000011 :
                                case (funct3)
                                3'b010 : // LW
                                        begin
                                                rf3_we_control  <= 1;
                                                alu_control     <= 0;
                                                mem_we                  <= 0;
                                                imm_mux                 <= 0;
                                        end
                                endcase

                        7'b0100011 :
                                case (funct3)
                                3'b010 : //SW
                                        begin
                                                rf3_we_control  <= 0;
                                                alu_control     <= 0;
                                                mem_we                  <= 1;
                                                imm_mux                 <= 1;
                                        end
                                endcase
                        endcase
                end
        end

I used machine code from previous post. Hopefully I can complete the simple assembler to generate hex myself. For now, I used the following two instruction:

 fe042623                sw      zero,-20(s0)
 fec42783                lw      a5,-20(s0)

And it works! Kinda!

                  25 Writing ram[0xa]=0x0
                  35 Writing regfile[15]=0x0

and mandatory gtkwave pic

Example image

Gotchas Link to heading

Issue 1 iverilog doesn’t play nice with multi-dim arrays(ie memories)

`ifdef SIM
generate
  genvar idx;
  for(idx = 0; idx < 32; idx = idx+1) begin: x
    wire [31:0] tmp;
    assign tmp = mem[idx];
  end
endgenerate
`endif

Issue 2 Reset to control unit as i start the PC at 0, after reset the first instruction is executed twice until the PC changes to next instruction. The solution(Hacky as hell), feed reset to control unit and keep default control signals to prevent regfile or memory write in reset.

[1] https://www.amazon.co.uk/Digital-Design-Computer-Architecture-2012-12-25/dp/B01MZ3QSGK/ref=sr_1_2?crid=1XTIG8ZPN6M62&keywords=digital-Design-Computer-Architecture-Harris&qid=1641236355&sprefix=digital-design-computer-architecture-harris%2Caps%2C423&sr=8-2

[2] RISCV specs