The file doc/examples/hardware/pipe_cpu.ivy models a very simple two-stage pipelined processor. It brings together all of the hardware features of Ivy — wires, registers, arrays (a register file and memories), a clock and reset, and a memory initializer that loads a program — in one design that translates to RTL and can be simulated.

The instruction set

Instructions are 16 bits wide, with fixed fields:

[15:13] opcode   [12:10] rd   [9:7] ra   [6:4] rb   [7:0] imm/target

There are seven opcodes:

opcode mnemonic effect
0 NOP
1 ADD rd, ra, rb rf[rd] := rf[ra] + rf[rb]
2 SUB rd, ra, rb rf[rd] := rf[ra] - rf[rb]
3 LI rd, imm rf[rd] := zero_extend(imm8)
4 LD rd, [ra] rf[rd] := mem[ rf[ra] ]
5 ST [ra], rb mem[ rf[ra] ] := rf[rb]
6 BEQZ ra, target if rf[ra] == 0 then pc := target

The design uses three interpreted types: a 16-bit word (data and instructions), an 8-bit addr (the program counter and memory addresses), and a 3-bit reg (a register-file index).

Architectural state and the pipeline register

The processor’s state consists of the program counter, the register file, data memory and an instruction ROM (the last three are arrays, and become RTLIL memories):

var pc : addr                 # program counter
var rf(R:reg) : word          # register file
var mem(A:addr) : word        # data memory
var imem(A:addr) : word       # instruction memory (ROM; never written)

Between the two pipeline stages sits a pipeline register holding the instruction currently being executed and a bit saying whether it is real (as opposed to a squashed bubble):

var ir : word                 # instruction being executed in X
var ir_valid : bool           # false => bubble

The two stages

The fetch stage (F) is combinational: it reads the instruction at the current PC from the instruction ROM.

wire fetched : word
definition fetched = imem(pc)

The execute stage (X) works on the instruction fetched on the previous clock, which is held in ir. All of the decoding and the ALU are combinational, so they are wires. The instruction fields are extracted with bfe, the operands are read from the register file, and the branch condition is computed:

wire opcode : opc
definition opcode = ir<<15:13>>
...
wire a_val : word
definition a_val = rf(ra)
...
wire take_branch : bool
definition take_branch = ir_valid & (opcode = 6) & (a_val = 0)

Because operands are read and results are written in the same stage, consecutive instructions never race on the register file: there are no data hazards, and no forwarding is needed.

The state updates happen on the clock edge, in after posedge. First the X-stage instruction is written back (guarded by ir_valid, so a bubble has no effect), and then the PC and pipeline register advance:

after posedge {
    if ir_valid {
        if opcode = 1 { rf(rd) := a_val + b_val }        # ADD
        else { ... }                                     # SUB / LI / LD / ST
    };
    if take_branch {
        pc := target;          # redirect to the branch target
        ir_valid := false;     # squash the shadow-fetched instruction
    } else {
        pc := pc + 1;
        ir := fetched;
        ir_valid := true;
    }
}

The control hazard

The conditional branch is the interesting part. BEQZ is resolved in the execute stage, one cycle after it was fetched. By then the fetch stage has already fetched the next sequential instruction — the one in the branch’s shadow. When the branch is taken, that instruction must not take effect, so the update sets ir_valid := false, turning it into a one-cycle bubble, while the PC is redirected to the target. This is the classic control hazard of a pipelined processor, and it costs one cycle per taken branch.

Reset and loading a program

after init gives the reset/power-on behavior. It clears the PC and the pipeline valid bit, zeroes the register file (a broadcast assignment over the index variable R), and loads a small program into the instruction ROM:

after init {
    pc := 0;
    ir_valid := false;
    rf(R) := 0;            # zero the register file
    imem(0) := 0x6405;     # LI   r1, 5
    imem(1) := 0x6801;     # LI   r2, 1
    imem(2) := 0x44a0;     # SUB  r1, r1, r2
    imem(3) := 0xc002;     # BEQZ r0, 2   (r0 == 0, so an unconditional jump to 2)
}

The register/PC resets become synchronous-reset logic driven by the global rst; the memory assignments become power-on memory contents. So this one Ivy file is a self-contained processor with its program.

Finally, so the design has an observable output, the current PC is exposed as an export wire:

export wire pc_out : addr
definition pc_out = pc

Translating and simulating

Translate the design to RTLIL in the usual way:

$ ivy_to_rtl pipe_cpu.ivy

The register file and the two memories become RTLIL memories, and the program becomes $meminit_v2 cells:

$ grep 'memory\|meminit' pipe_cpu.il
  memory width 16 size 256 \imem
  memory width 16 size 256 \mem
  memory width 16 size 8 \rf
  cell $meminit_v2 $meminit$imem$21   # imem[0] = 0x6405
  ...

Because the program is baked in, we can simulate directly, with no hand-written stimulus beyond the clock and reset:

$ yosys -p "read_rtlil pipe_cpu.il; hierarchy -top cpu; proc; memory_collect;
            sim -clock posedge -reset rst -n 12 -vcd cpu.vcd"

Reading pc_out from cpu.vcd, the program-counter trace after reset is:

0, 1, 2, 3, 4, 2, 3, 4, 2, 3, 4, ...

The processor fetches 0, 1, 2, 3 sequentially. The BEQZ at address 3 is resolved in the execute stage while address 4 is being fetched, so the PC visits 4 (whose instruction is then squashed) before redirecting to the branch target 2 — the 3, 4, 2 in the trace is exactly the one-cycle branch bubble. The loop 2, 3, 4 then repeats, decrementing r1 through the SUB at address 2 on each pass.

Limitations

This is deliberately a minimal example. In particular there is no instruction memory port for loading code at run time (the ROM is fixed by the initializer), the LD/ST address is only the low 8 bits of a register, and the branch is unconditional-friendly only because the register file is zeroed at reset. It is meant to illustrate the Ivy hardware features and the RTL translation end to end, not to be a complete CPU.