Welcome to Ceramic Hacker

Hi, I'm Alexander (Sasha) Skvortsov. I'm a computer science and math major at Penn State. I'll be using this blog to share some of my projects and thoughts, mainly focused around software engineering and pottery.
Hardcaml MIPSBlog

[7/x] Registers and Stateful Design

Hi, welcome back to our Hardcaml MIPS project! Today, we'll finish the first version of our instruction fetch stage by adding feedback via registers, so that we can keep track of (and increment) our position in the program we're executing. If you'd like to see the end-result of this post, it's tagged as v0.4.0 on GitHub.

Combinational vs Sequential Logic

Recall that there are 2 "types" of circuits:

  • Combinational circuits produce output only based on current input. They are deterministic, meaning they will always produce the same output given the same input. For example, when given address 0, we always want our instruction fetch circuit to output the first instruction.
  • Sequential circuits use (and update) some state in addition to inputs when calculating outputs. For example, we want to increment our program counter every clock cycle.

If you've ever worked in a functional programming language (like OCaml!), you might notice strong parallels between combinational vs sequential circuits and pure vs impure functions. This similar structure is yet another advantage of using Hardcaml .

Combinational circuits are generally easier to reason about than sequential ones, because we don't need to worry about internal state or the outside world, only how outputs should be generated from inputs. This implies a design pattern of separating combinational pieces, where we put complex logic and calculations, from sequential pieces, which update and retrieve stored state.

In fact, this is how we'll complete our instruction fetch stage! We've already built a combinational circuit that takes a program counter input (pc), and returns an incremented pc and the instruction at pc. Now, what we need is a register that updates pc to next_pc every clock cycle. For the reasons stated before, it would be cleanest to keep instruction fetch combinational when we add this.

Given these requirements, the best place to put our sequential register logic seems to be the datapath itself. Let's take a step back and think about how this could apply to our whole design.

Our CPU's Design

So far, we've been working almost exclusively on the instruction fetch stage of our CPU while we explore Hardcaml. Our full design will eventually have many more pieces:

If this seems a bit overwhelming, that's because it is. There's a lot of parts here, and the minimalistic wire naming certainly doesn't help. Let's go over some key aspects:

  • The long red rectangles are registers between pipeline stages. They ensure that outputs of each stage propogate to the next stage once every clock cycle. For example, the instruction we fetch during clock cycle 1 will be decoded during clock cycle 2, processed by the ALU during clock cycle 3, interact with data memory during clock cycle 4, and write to registers (if applicable) during clock cycle 5.
  • The shapes and wires between red rectangles represent the logic of each stage.

And that's actually a simplified version that doesn't include jumping, branching, and a few other features. The full design is even bigger!

Trying to understand this whole system at once is very tricky, because the design is very flat. It's easier to understand a system with 5 components, each of which has 5 subcomponents, than a system with 25 direct components.

It would be better if we could treat each stage as a self-contained, combinational module, and then have the datapath just consist of wiring between the 5 stages and their state management registers. So let's do that!

Before we continue, I want to note that our stages won't exactly be combinational, because some include writable memory (in particular, the register file and data memory). We sacrifice a bit of purity here so each stage is a bit more self-contained, and as a result, easier to understand.

Hardcaml Feedback

Let's apply this design pattern to our instruction fetch module. We need to change Datapath so that the pc input to instruction fetch is its next_pc output from the previous clock cycle. We can do this by running next_pc through a register that updates every clock cycle, and using that value for pc.

This isn't really possible in regular programming: we can't use a variable as an input before it has been declared. Hardcaml works around this through the wire primitive:

wire is a signal which can be declared prior to providing its input driver.

Why is this useful? Well, we can create a wire for pc that'll be an input to our PC register, which in turn will be an input to instruction fetch. Then, once we get next_pc from the instruction fetch output, we can use that as the value for the pc wire. For the PC register itself, we can just use Hardcaml's reg primitive.

Let's take a look at the new code:

open Hardcaml
open Hardcaml.Signal

module I = struct
  type 'a t = { clock : 'a } [@@deriving sexp_of, hardcaml]
end

module O = struct
  type 'a t = { instruction : 'a [@bits 32] } [@@deriving sexp_of, hardcaml]
end

let create ~program (scope : Scope.t) (input : _ I.t) =
  let r = Reg_spec.create ~clock:input.clock () in

  (* Instruction Fetch Stage *)
  let pc = wire 32 in
  let pc_reg = reg ~enable:vdd r pc in
  let instruction_fetch = Instruction_fetch.hierarchical ~program scope { pc = pc_reg } in
  pc <== instruction_fetch.next_pc;

  {O.instruction = instruction_fetch.instruction};

Note that we declare the wire before assigning a value (instruction_fetch.next_pc) into it. This allows us to create circuits with cyclical dependencies, AKA feedback.


Hardcaml Observations

  • Hardcaml's primitives for registers, muxes, etc are very useful! They really help cut down on boilerplate code.
  • Hardcaml's wires have a bunch of really useful checks to make sure you haven't forgotten to set an input to a wire, or tried to assign multiple inputs to the same wire. These things seem trivial, but Verilog allows anti-patterns like this, and it's a pain to debug. This is yet another thing you don't need to worry about if using Hardcaml instead of Verilog.
  • We initially planned to use the Always DSL for stateful systems, but decided not to since it doesn't support instantiations. If we end up with complex conditional logic at some point in our circuit (which seems likely, especially for the control unit), that might still be useful.
Previous articleNext article

Comments (0)

Hi, welcome back to our Hardcaml MIPS project! Today, we'll finish the first version of our instruction fetch stage by adding feedback via registers, so that we can keep track of (and increment) our position in the program we're executing. If you'd like to see the end-result of this post, it's tagged as v0.4.0 on GitHub.

Combinational vs Sequential Logic

Recall that there are 2 "types" of circuits:

  • Combinational circuits produce output only based on current input. They are deterministic, meaning they will always produce the same output given the same input. For example, when given address 0, we always want our instruction fetch circuit to output the first instruction.
  • Sequential circuits use (and update) some state in addition to inputs when calculating outputs. For example, we want to increment our program counter every clock cycle.

If you've ever worked in a functional programming language (like OCaml!), you might notice strong parallels between combinational vs sequential circuits and pure vs impure functions. This similar structure is yet another advantage of using Hardcaml .

Combinational circuits are generally easier to reason about than sequential ones, because we don't need to worry about internal state or the outside world, only how outputs should be generated from inputs. This implies a design pattern of separating combinational pieces, where we put complex logic and calculations, from sequential pieces, which update and retrieve stored state.

In fact, this is how we'll complete our instruction fetch stage! We've already built a combinational circuit that takes a program counter input (pc), and returns an incremented pc and the instruction at pc. Now, what we need is a register that updates pc to next_pc every clock cycle. For the reasons stated before, it would be cleanest to keep instruction fetch combinational when we add this.

Given these requirements, the best place to put our sequential register logic seems to be the datapath itself. Let's take a step back and think about how this could apply to our whole design.

Our CPU's Design

So far, we've been working almost exclusively on the instruction fetch stage of our CPU while we explore Hardcaml. Our full design will eventually have many more pieces:

If this seems a bit overwhelming, that's because it is. There's a lot of parts here, and the minimalistic wire naming certainly doesn't help. Let's go over some key aspects:

  • The long red rectangles are registers between pipeline stages. They ensure that outputs of each stage propogate to the next stage once every clock cycle. For example, the instruction we fetch during clock cycle 1 will be decoded during clock cycle 2, processed by the ALU during clock cycle 3, interact with data memory during clock cycle 4, and write to registers (if applicable) during clock cycle 5.
  • The shapes and wires between red rectangles represent the logic of each stage.

And that's actually a simplified version that doesn't include jumping, branching, and a few other features. The full design is even bigger!

Trying to understand this whole system at once is very tricky, because the design is very flat. It's easier to understand a system with 5 components, each of which has 5 subcomponents, than a system with 25 direct components.

It would be better if we could treat each stage as a self-contained, combinational module, and then have the datapath just consist of wiring between the 5 stages and their state management registers. So let's do that!

Before we continue, I want to note that our stages won't exactly be combinational, because some include writable memory (in particular, the register file and data memory). We sacrifice a bit of purity here so each stage is a bit more self-contained, and as a result, easier to understand.

Hardcaml Feedback

Let's apply this design pattern to our instruction fetch module. We need to change Datapath so that the pc input to instruction fetch is its next_pc output from the previous clock cycle. We can do this by running next_pc through a register that updates every clock cycle, and using that value for pc.

This isn't really possible in regular programming: we can't use a variable as an input before it has been declared. Hardcaml works around this through the wire primitive:

wire is a signal which can be declared prior to providing its input driver.

Why is this useful? Well, we can create a wire for pc that'll be an input to our PC register, which in turn will be an input to instruction fetch. Then, once we get next_pc from the instruction fetch output, we can use that as the value for the pc wire. For the PC register itself, we can just use Hardcaml's reg primitive.

Let's take a look at the new code:

open Hardcaml
open Hardcaml.Signal

module I = struct
  type 'a t = { clock : 'a } [@@deriving sexp_of, hardcaml]
end

module O = struct
  type 'a t = { instruction : 'a [@bits 32] } [@@deriving sexp_of, hardcaml]
end

let create ~program (scope : Scope.t) (input : _ I.t) =
  let r = Reg_spec.create ~clock:input.clock () in

  (* Instruction Fetch Stage *)
  let pc = wire 32 in
  let pc_reg = reg ~enable:vdd r pc in
  let instruction_fetch = Instruction_fetch.hierarchical ~program scope { pc = pc_reg } in
  pc <== instruction_fetch.next_pc;

  {O.instruction = instruction_fetch.instruction};

Note that we declare the wire before assigning a value (instruction_fetch.next_pc) into it. This allows us to create circuits with cyclical dependencies, AKA feedback.


Hardcaml Observations

  • Hardcaml's primitives for registers, muxes, etc are very useful! They really help cut down on boilerplate code.
  • Hardcaml's wires have a bunch of really useful checks to make sure you haven't forgotten to set an input to a wire, or tried to assign multiple inputs to the same wire. These things seem trivial, but Verilog allows anti-patterns like this, and it's a pain to debug. This is yet another thing you don't need to worry about if using Hardcaml instead of Verilog.
  • We initially planned to use the Always DSL for stateful systems, but decided not to since it doesn't support instantiations. If we end up with complex conditional logic at some point in our circuit (which seems likely, especially for the control unit), that might still be useful.
Write a Reply...