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 8 opcodes: NOP=0, LOAD=1, STORE=2, MOVE=3, ADD=4, SUB=5, MUL=6, and JNZ=7.

  • NOP is the all-zero instruction and has no side effects.

  • Two instruction formats:

    • Address type: LOAD, STORE, JNZ take a data-memory address (addr) and register index (i_reg_a)

    • Register type: MOVE, ADD, SUB, MUL take indices of three registers. Two sources (i_reg_b, i_reg_c) and one destination (i_reg_a).

  • JNZ jumps to addr when regs[i_reg_a] 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_a opcode
MOVE, ADD, SUB, MUL Register i_reg_c i_reg_b i_reg_a 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 1251 : 1=LOAD regs[5] <- dmem[0x12]
0x 0124 : 4=ADD  regs[2] <- regs[1] + regs[0]

Example: Sum to N numbers

The algorithm described in C:

// setup:
uint16_t mem[256];
mem[0] = 0;    // sum seed
mem[1] = 1;    // the constant one
mem[2] = 10;   // N

// run:
uint16_t r0_sum   = mem[0];
uint16_t r1_one   = mem[1];
for (r2_count = mem[2]; r2_count !=0; r2_count -= r1_one) {
  r0_sum += r2_count;
}
mem[4] = r0_sum;  //55

The algorithm described in our machine code and assembly:

0: R0_SUM     = *(0);
1: R1_ONE     = *(1);
2: R2_COUNTER = *(2);
3: R0_SUM     = R0_SUM + R2_COUNTER;
4: R2_COUNTER = R2_COUNTER - R1_ONE;
5: if (R2_COUNTER != 0) goto 3;
6: *(4) = R0_SUM;

Fibonacci Code

Full CPU (40 LOC)

module cpu (
  input  logic        clk, reset,
  output logic [7 :0] pc,          addr,
  input  logic [15:0] instruction, read_data,
  output logic [15:0] write_data,
  output logic        dmem_wen
);
  enum logic [3:0] {NOP, LOAD, STORE, MOVE, ADD, SUB, MUL, JNZ} opcode;
  logic [ 3:0] i_reg_a, i_bus_b, i_bus_c;
  logic [15:0][15:0] regs;
  logic [15:0] bus_a, bus_b, bus_c, alu_out;
  logic reg_wen, jump;

  always_comb begin
    {addr,             i_reg_a, opcode} = instruction;
    {i_bus_c, i_bus_b, i_reg_a, opcode} = instruction;

    bus_a      = regs[i_reg_a];
    bus_b      = regs[i_bus_b];
    bus_c      = regs[i_bus_c];

    write_data = bus_a;
    dmem_wen   = opcode == STORE;
    jump       = (opcode == JNZ) && (bus_a != '0);

    alu_out = '0;
    reg_wen = 1'b1;
    case (opcode)
      LOAD   : alu_out = read_data;
      MOVE   : alu_out = bus_b;
      ADD    : alu_out = bus_b + bus_c;
      SUB    : alu_out = bus_b - bus_c;
      MUL    : alu_out = bus_b * bus_c;
      default: reg_wen = 1'b0;
    endcase
  end

  always_ff @(posedge clk)
    if (reset) {pc, regs}   <= '0;
    else begin
      pc <= jump ? addr : pc + 1;
      if (reg_wen) regs[i_reg_a] <= alu_out;
    end
endmodule