CPU in 40 lines of SV

This project builds a small 16-bit CPU in five incremental steps. Each step is complete, and adds only few new lines. Programs are written directly in SV testbench. Full CPU here.

16-bit CPU circuit in 7nm (ASAP7) visualized in 3D. Drag to rotate, scroll to zoom.

Quickstart

make fresh     # if you havent started the container
make enter     # to enter the running container

make sim gtkwave DESIGN=cpu_3_store_data      # simulate the step 3 design & view waveform
# Ctrl+C to exit gtkwave

make sim gtkwave DESIGN=cpu_factorial         # run factorial on the final CPU

make gds show_layout DESIGN=cpu_factorial     # Run GDS flow

Example programs:

Incremental evolution

Level

Feature

RTL

0_memory

Simple memory with zero-latency read and 1-cycle-latency write

RTL

1_load_instruction

Just a counter to load instructions (PC)

RTL

2_load_data_into_registers

Sixteen registers and LOAD

RTL

3_store_data

STORE

RTL

4_move_alu

MOVE, ADD, SUB, and MUL

RTL

5_jump

JNZ: jump to a given address if a given register is not zero

RTL

CPU Design

  • Only 7 opcodes: LOAD=0, STORE=1, MOVE=2, ADD=3, SUB=4, MUL=5, and JNZ=6.

  • Two instruction formats:

    • Address type: LOAD, STORE, JNZ take an address and register index (i_reg)

    • Register type: MOVE, ADD, SUB, MUL take indices of three registers. Two source (i_rs1, i_rs2) and one destination i_rd.

  • JNZ jumps to the addr when regs[i_reg] is nonzero.

Instructions Format 4 Bits [15:12] 4 Bits [11:8] 4 Bits [7:4] 4 Bits [3:0]
LOAD, STORE, JNZ Address addr i_reg opcode
MOVE, ADD, SUB, MUL Register i_rs2 i_rs1 i_rd opcode

Reading Instructions

Each instruction field is 4-bits, so it becomes a character when displayed as hex, making it easy to read binary. Read right to left (little endian). e.g.

0x 1250 : 0=LOAD regs[5] <- dmem[0x12]
0x 0123 : 3=ADD  regs[2] <- regs[1] + regs[0]

Waveform of Fibonacci program: Fibonacci Code

Full CPU (40 LOC)

module cpu (
  input  logic        clk, reset,
  output logic [7 :0] imem_addr,  dmem_addr,
  input  logic [15:0] imem_rdata, dmem_rdata,
  output logic [15:0] dmem_wdata,
  output logic        dmem_wen
);
  logic [7:0] pc, addr;
  enum logic [3:0] {LOAD, STORE, MOVE, ADD, SUB, MUL, JNZ} opcode;
  logic [ 3:0] i_rs1, i_rs2, i_rd, i_reg;
  logic [15:0] regs [16];
  logic [15:0] reg_1, reg_2;

  always_comb begin
    imem_addr = pc;
    {addr        , i_reg, opcode} = imem_rdata;
    {i_rs2, i_rs1, i_rd , opcode} = imem_rdata;

    dmem_addr  = addr;
    dmem_wdata = regs[i_reg];
    dmem_wen   = opcode == STORE;

    reg_1      = regs[i_rs1];
    reg_2      = regs[i_rs2];
  end

  always_ff @(posedge clk)
    if (reset) begin
      pc   <= '0;
      for (int i = 0; i < 16; i++) regs[i] <= '0;
    end else begin
      pc   <= pc + 1'b1;

      case (opcode)
        LOAD: regs[i_reg] <= dmem_rdata;
        MOVE: regs[i_rd ] <= reg_1;
        ADD : regs[i_rd ] <= reg_1 + reg_2;
        SUB : regs[i_rd ] <= reg_1 - reg_2;
        MUL : regs[i_rd ] <= reg_1 * reg_2;
        JNZ : if (regs[i_reg] != '0) pc <= addr;
        default: ;
      endcase
    end
endmodule