Sequential Logic Emulation with F# - Part 2

This post concludes the work from chapter three of the book 'The Elements of Computing Systems'.
As with the other posts in the series, I have emulated the sequential logic chips defined in the book using F#.

In the previous post I sidetracked from the book in order to investigate what constituted a Data Flip Flop. The result, was a greater understanding of the sequential ordering of combinatorial chips in order to achieve state retention.

From the DFF, we can now look towards building the constructs that the book outlines.
These include:

  • Registers - Starting from simple 1 bit stores
  • RAM chips - In various sizes
  • Counters - Counts sequentially per execution and can be reset or loaded as required

As always, I will outline the representation of these chips using chips from previous stages of the book, but also define a more idiomatic F# approach as well.

Revisiting the Flip Flop

For simplicities sake and to avoid people having to refer back, I will first redefine the Data Flip Flop from the previous post.
While I'm at it, I'll remove all its dependencies on other gates/chips so it is clear it functions precisely as expected.

As stated by the book, the DFFs intention is to take and return a single bit, where the bit returned is the state from the previous clock cycle.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
type DFF() =
    inherit Chip()
    let mutable state = 0s
    let mutable pState = 0s
    override x.doWork clk inputs =
        match clk with //Only set the state on a tick - The falling edge
        | clk.Tick -> state <- pState
        | _ -> pState <- inputs.[0]
        [|state|]

Now that we have our DFF implementation to work with, lets move on to the first of the chips required in the book, the register.

Registers

Registers vary in size from a single bit to multi bit stores.
The amount of bits that a register can store is known as its width. The contents of a register are referred to as a word. Hence a registers width is also the word size.

In order to create multi bit registers we must first start with a binary cell.

The Binary Cell

A binary cell is a single bit register. It is created by making use of a DFF, a multiplexor and a pair of inputs.

The basic premise is that we feed the DFFs output into one of the multiplexors inputs, while the other comes from an input to the chip.

The other chip input is a load bit. This bit is used to control whether or not to replace the value held in the store.
When load is set, the incoming value to the chip is selected by the multiplexor and passed to the DFF. When load is not set, the DFF value is passed back to itself, hence holding its state.

It's a beautifully simple design, and also easy to represent like so.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
//Binary Cell
//Stores a single bit.
//The Mux chip acts as a selector on whether to store a new value or keep hold of the old DFF value
type Bit() =
    inherit Chip()
    let dff = new DFF()
    let mutable state = 0s
    override x.doWork clk inputs =
        state <-([|Mux inputs.[0] state inputs.[1]|]
                |> dff.execute clk).[0]
        [|state|]

Multi Bit Registers

Now we have our binary cell, it's an incredibly easy task to create n-bit registers. We simply create an n size array of binary cells.
In order to use an n-bit register we pass each input bit to the registers in turn, along with the load bit.

The book calls for a 16 bit register, an implementation of which can be seen below.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
//A 16 bit register 
type Register() = 
    inherit Chip()
    let bits = [|for i in 1 .. 16 -> new Bit()|]
    override x.doWork clk inputs =
        let inBits = inputs.[0] |> toTwosCompliment 16
        bits |> Array.mapi (fun i b -> ([|inputs.[1]; inBits.[i]|] 
                                        |> b.execute clk).[0])

I chose to utilise Array.map in order to process the output of each registers execution into the resulting array.
If we were sticking to the Hardware Definition Language (HDL) used in the book, this would have to be 16 individual statements.
I will stick to this approach going forward as it reduces code and is more inline with standard F#, however it brings with it the consequence of detracting from the book.

Well, creating the register was easy.
Onto, the RAM!

RAM Chips

Random Access Memory (RAM) chips have both a width and a size.

As we have just seen, the width is the amount of bits held in each register. (We will stick to the books requirement of 16 bits here)
Size is the number of registers in the the array that makes up the RAM chip.

The tricky part of creating a RAM chip comes when providing random access to any word (registers content) held in the chip, at equal speed.
To accomplish this, each register needs to be given an address in the form of an integer value.
The RAM then provides some access logic that can select the required register and either output or set its value.

Therefore RAM chips have three inputs, The value to store, an address and a load bit.

The logic for the addressing works as follows:

  • First we run the load bit and the binary representation of the integer address through a DMux8Way chip. This has the effect of creating an 8 bit array with the load bit in the correct position.
  • Next we push the input value to be stored into each of the registers along with the corresponding load bit from the previously created array. This results in the correct array being set.
  • Finally we run the value of each of the registers through a Mux8WayChip, along with the address in order to select the correct word to return.

Below is the implementation of an 8 Register RAM chip.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
//A 16 bit wide 8 register array.
//Utilises Mux and DMux to select the correct register to store the value in
type RAM8() =
    inherit Chip()
    let registers = [|for i in 1 .. 8 -> new Register()|]
    override x.doWork clk inputs =
        let (inBits, load, address) = (inputs.[0], inputs.[1], inputs.[2] |> toTwosCompliment 3)
        let loadArray = DMux8Way load address
        let state = registers 
                    |> Array.mapi (fun i r -> [|inBits; loadArray.[i]|]
                                              |> r.execute clk)
        Mux8Way16 state.[0] state.[1] state.[2] state.[3] state.[4] state.[5] state.[6] state.[7] address

The RAM chips are built sequentially to utilise the previous implementations (As with most of the chips we have created).
We use blocks of 8 registers as our standard. So in the case of a 64 register RAM block, we can make it from 8, 8 register RAM blocks.

The 64 register RAM chip can be seen below.
It is much the same as the 8 register chip except that it now requires a larger binary address. To be exact it now requires 6 bits, where as the 8 register RAM chip required 3 bits.
This follows the rule bits = log2n where n is the number of registers.

Therefore we need to repeat a little bit of the selection logic from the previous chip.
We take the 3 most significant bits (MSB) and use them to select one of the RAM8 chips.
The remaining 3 bits are then passed on to this chip in order to select one of the registers within it. Nice and recursive!

So here it is, the 64 register RAM chip:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
type RAM64() =
    inherit Chip()
    let ramArray = [|for i in 1 .. 8 -> new RAM8()|]
    override x.doWork clk inputs =
        let (inBits, load, address) = (inputs.[0], inputs.[1], inputs.[2] |> toTwosCompliment 6)
        let ramLoad = DMux8Way load address.[0..2]
        let state = ramArray |> Array.mapi (fun i r -> r.execute clk [|inBits; ramLoad.[i]; (toDecimal 16 address.[3..5]) |])
        Mux8Way16 state.[0] state.[1] state.[2] state.[3] state.[4] state.[5] state.[6] state.[7] address

This pattern then continues as many times as we like.
The book calls for up to a 16 thousand Register RAM block. You can see the implementations below.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
type RAM512() =
    inherit Chip()
    let ramArray = [|for i in 1 .. 8 -> new RAM64()|]
    override x.doWork clk inputs =
        let (inBits, load, address) = (inputs.[0], inputs.[1], inputs.[2] |> toTwosCompliment 9)
        let ramLoad = DMux8Way load address.[0..2]
        let state = ramArray |> Array.mapi (fun i r -> r.execute clk [|inBits; ramLoad.[i]; (toDecimal 16 address.[3..8]) |])
        Mux8Way16 state.[0] state.[1] state.[2] state.[3] state.[4] state.[5] state.[6] state.[7] address

type RAM4k() =
    inherit Chip()
    let ramArray = [|for i in 1 .. 8 -> new RAM512()|]
    override x.doWork clk inputs =
        let (inBits, load, address) = (inputs.[0], inputs.[1], inputs.[2] |> toTwosCompliment 12)
        let ramLoad = DMux8Way load address.[0..2]
        let state = ramArray |> Array.mapi (fun i r -> r.execute clk [|inBits; ramLoad.[i]; (toDecimal 16 address.[3..11]) |])
        Mux8Way16 state.[0] state.[1] state.[2] state.[3] state.[4] state.[5] state.[6] state.[7] address

type RAM16k() =
    inherit Chip()
    let ramArray = [|for i in 1 .. 4 -> new RAM4k()|]
    override x.doWork clk inputs =
        let (inBits, load, address) = (inputs.[0], inputs.[1], inputs.[2] |> toTwosCompliment 16)
        let ramLoad = DMux8Way load address.[0..2]
        let state = ramArray |> Array.mapi (fun i r -> r.execute clk [|inBits; ramLoad.[i]; (toDecimal 16 address.[3..13]) |])
        Mux8Way16 state.[0] state.[1] state.[2] state.[3] state.[4] state.[5] state.[6] state.[7] address

As is all too clear to see, there is a lot of repetition here.
We'll look at cleaning that up later on.

But first, let's look at the final chip needed for this chapter, the counter.

The Counter

The counter is a chip that contains a register alongside some combinatorial logic in order to increment its value each execution.
In addition, this logic should also allow us to set the register to a particular value, or reset it to zero.
In other words, we need a loadable, resettable register that can increment its value per clock cycle.

All of the building blocks for the counters logic have been created in the previous chapters of the book.
What follows is a step by step breakdown of what is needed:

  1. Get the next value by incrementing the current register value using our Increment chip.
  2. Select whether we want to use the bits passed in or our next value by checking the load control bit (via a MultiMux).
  3. Select whether we want to use the value chosen in the last step, or reset to 0 by checking the reset control bit (via a MultiMux).
  4. Determine whether we are performing an increment, load, or reset. (This determines whether we are loading the register or not)
  5. Pass the selected value and the new load bit to the underlying register.

Translated into code, this becomes the following.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
type Counter() = 
    inherit Chip()
    let register = new Register()
    override x.doWork clk inputs = 
        let (inBits, load, inc, reset) = (inputs.[0], inputs.[1], inputs.[2], inputs.[3])
        let current = register.execute clk [|0s; 0s|]
        let next = Increment (current) //Increment the current value
        let mux1 = MultiMux load next (inBits |> toTwosCompliment 16) 
        let mux2 = MultiMux reset mux1 [|for i in 1..16 -> 0s|]
        let regLoad = Or inc load
                      |> Or reset
        register.execute clk [| mux2 |> toDecimal 16; regLoad|]

This is quite a nice chip.
The logic is simple and clear to understand, yet it is powerful in its function.

We will look at testing this chip later on, but first, let's think about utilising a more F# centric approach.

A slightly more idiomatic F# approach

While all the above is perfectly good, there are many things I would like to change.
First up, we need to get rid of all the repetition, mostly in the RAM chips.

To achieve this, we could make a simple Enum style Discriminated Union (DU) to specify the available RAM sizes, and then just create a flat array of registers in our RAM chip like so:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
type RamSize =  
    | R8 = 8
    | R64 = 64
    | R512 = 512
    | R4KB = 4096
    | R16KB = 16384
    
    
type RAM(size:RamSize) = 
    inherit Chip()
    let regArray = [|for i in 1 .. int size -> new Register()|]
    override x.doWork clk inputs =
        let (inBits, load, address) = (inputs.[0], inputs.[1], inputs.[2])
        regArray.[int address].execute clk [|inBits; load|]

This makes addressing the correct register much simpler to understand.

Another place we could really make use of some F# goodness, is the Counter.
By utilising our old friend pattern matching, we remove the need to pre-calculate and can simply switch to the needed functionality.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
type Counter2() =
    inherit Chip()
    let register = new Register()
    override x.doWork clk inputs =
        let (inBits, load, inc, reset) = (inputs.[0], inputs.[1], inputs.[2], inputs.[3])
        let current = register.execute clk [|0s; 0s|]
        let toSet = 
            match reset, load, inc with
            | 1s,_,_ -> [|for i in 1..16 -> 0s|]
            | 0s,1s,_ -> inBits |> toTwosCompliment 16
            | 0s,0s,1s -> Increment (current)
            |_,_,_ -> register.execute clk [|inBits; 0s|]
        register.execute clk [|toSet |> toDecimal 16; (load ||| reset ||| inc)|]

As with every other post so far, it is clear that F# provides a much more concise way of expressing the required chips.
It does however, skip over the logical building blocks by doing so.

I think I will continue both approaches, but will aim to take a more drastic detour in the coming posts.
This will allow me to focus more on the power of the F# language.

Testing

For our testing needs I will utilise the test harness from the last post.
This allows for some quick familiar tests to be run.

First up, the Counter.
I chose to test this as it nicely shows the use of various previous chips, drawing together a lot of past work.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
let TestCounter = 
    let harness = {inputs = [|0s; 0s; 1s; 0s;|]; outputs = Array.empty; chips = [|new Counter()|]}
    let result = harness
                |> cycle 3  3
                |> setInputs [|0s; 0s; 0s; 1s;|]
                |> cycle 2 3
                |> setInputs [|17s; 1s; 0s; 0s;|]
                |> cycle 1 3
                |> setInputs [|17s; 0s; 1s; 0s;|]
                |> cycle 5 3
                |> setInputs [|0s; 0s; 0s; 0s;|]
                |> cycle 2 3
    result.outputs |> toDecimal 16
//Output:
Executing 3 cycles with inputs = [|0s; 0s; 1s; 0s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
   Cycle 2 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s|]
   Cycle 2 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s|]
   Cycle 3 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s|]
   Cycle 3 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s|]
Executing 2 cycles with inputs = [|0s; 0s; 0s; 1s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 1s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 1s|]
   Cycle 2 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
   Cycle 2 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
Executing 1 cycles with inputs = [|17s; 1s; 0s; 0s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
Executing 5 cycles with inputs = [|17s; 0s; 1s; 0s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 0s; 1s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 0s; 1s|]
   Cycle 2 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 1s; 0s|]
   Cycle 2 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 1s; 0s|]
   Cycle 3 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 1s; 1s|]
   Cycle 3 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 1s; 1s|]
   Cycle 4 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 0s; 0s|]
   Cycle 4 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 0s; 0s|]
   Cycle 5 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 0s; 1s|]
   Cycle 5 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 0s; 1s|]
Executing 2 cycles with inputs = [|0s; 0s; 0s; 0s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 1s; 0s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 1s; 0s|]
   Cycle 2 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 1s; 0s|]
   Cycle 2 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 1s; 1s; 0s|]


val TestCounter : int16 = 22s

Note that the printed value is always one clock cycle behind.
This is because the register in the counter only commits to holding the value on the falling edge of the clock. Hence, we have to probe the register in the subsequent clock cycle to ensure the value was set.

Finally, we will test a 64 register RAM array.
To do this, I have bent the use of test harness slightly in order to store some values during execution of a set of functions.

I will test the following steps:

  1. Store two binary representations of integers in arbitrary locations in RAM
  2. Extract these integers storing them locally (we actually store an entire snapshot of the test harness state)
  3. Add these two integers together and store them at position one of the RAM array
  4. Retrieve the value back out of RAM and print the output

This translates to the following code.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
let TestRam64 = 
    let mutable harness = {inputs = [|15s; 1s; 5s;|]; outputs = Array.empty; chips = [|new RAM64()|]}
                        |> cycle 1  3
                        |> setInputs [|33s;1s;7s|]
                        |> cycle 1  3

    harness <- harness
            |> setInputs [|0s;0s;5s|]
            |> cycle 1 3
    let a = harness.outputs

    harness <- harness
            |> setInputs [|0s;0s;7s|]
            |> cycle 1 3
    let b = harness.outputs

    printfn "Adding a (%i) to b (%i) and storing at position 1." (a |> toDecimal 16) (b |> toDecimal 16)
    harness <- harness
            |> setInputs [| (Adder a b) |> toDecimal 16  ;1s; 1s|]
            |> cycle 1 3
            |> setInputs [|0s;0s;1s|]
            |> cycle 1 3

    printfn "The value was %i" (harness.outputs |> toDecimal 16)

Overall, not the best looking test case as my harness wasn't really thought through enough to be useful in this situation.
It does show the use of our RAM chip quite nicely though.

The output of running this is shown below:

//Output:
Executing 1 cycles with inputs = [|15s; 1s; 5s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
Executing 1 cycles with inputs = [|33s; 1s; 7s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
Executing 1 cycles with inputs = [|0s; 0s; 5s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 1s; 1s; 1s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 1s; 1s; 1s|]
Executing 1 cycles with inputs = [|0s; 0s; 7s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 0s; 0s; 1s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 0s; 0s; 0s; 0s; 1s|]
Adding a (15) to b (33) and storing at position 1.
Executing 1 cycles with inputs = [|48s; 1s; 1s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s|]
Executing 1 cycles with inputs = [|0s; 0s; 1s|]
   Cycle 1 - clk: Tick - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 1s; 0s; 0s; 0s; 0s|]
   Cycle 1 - clk: Tock - outputs: [|0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 0s; 1s; 1s; 0s; 0s; 0s; 0s|]
The value was 48

Conclusion

Utilising the work from my previous posts, it has been nice and easy to create the chips needed and test them as required.
However, there are some major drawbacks that I wish to fix. The biggest being the use of the int16 array to pass state around.

Doing this bypasses one of the strongest features of F#, it's type system.
It adds far too many possibilities for errors into the code and so it must be removed.

Therefore, before venturing into the next chapter of the book, I'm going to see how I can go about refactoring my current work to take full advantage of the features of F#.
That should help with reducing the possibility of errors and working towards the goal of making invalid state unrepresentable.

module FsEmulation
module ArithmeticLogicUnit
module Sequential
module Utils
Multiple items
type DFF =
  inherit Chip
  new : unit -> DFF
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.DFF

--------------------
new : unit -> DFF
Multiple items
type Chip =
  new : unit -> Chip
  abstract member doWork : clk -> int16 array -> int16 array
  member execute : clk:clk -> inputs:int16 array -> int16 array
  member outputs : int16 array
  member outputs : int16 array with set

Full name: Sequential.Chip

--------------------
new : unit -> Chip
val mutable state : int16
val mutable pState : int16
val x : DFF
override DFF.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.DFF.doWork
Multiple items
val clk : clk

--------------------
type clk =
  | Tick = 0s
  | Tock = 1s

Full name: Sequential.clk
val inputs : int16 array
Multiple items
type Bit =
  inherit Chip
  new : unit -> Bit
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Bit

--------------------
new : unit -> Bit
val dff : DFF
val x : Bit
override Bit.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Bit.doWork
val Mux : sel:int16 -> a:int16 -> b:int16 -> int16

Full name: ArithmeticLogicUnit.Mux
member Chip.execute : clk:clk -> inputs:int16 array -> int16 array
Multiple items
type Register =
  inherit Chip
  new : unit -> Register
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Register

--------------------
new : unit -> Register
val bits : Bit []
val i : int
val x : Register
override Register.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Register.doWork
val inBits : int16 []
val toTwosCompliment : b:int -> i:int16 -> int16 []

Full name: Utils.toTwosCompliment
module Array

from Microsoft.FSharp.Collections
val mapi : mapping:(int -> 'T -> 'U) -> array:'T [] -> 'U []

Full name: Microsoft.FSharp.Collections.Array.mapi
val b : Bit
Multiple items
type RAM8 =
  inherit Chip
  new : unit -> RAM8
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM8

--------------------
new : unit -> RAM8
val registers : Register []
val x : RAM8
override RAM8.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM8.doWork
val inBits : int16
val load : int16
val address : int16 []
val loadArray : int16 []
val DMux8Way : x:int16 -> sel:int16 array -> int16 []

Full name: ArithmeticLogicUnit.DMux8Way
val state : int16 array []
val r : Register
val Mux8Way16 : a:int16 [] -> b:int16 [] -> c:int16 [] -> d:int16 [] -> e:int16 [] -> f:int16 [] -> g:int16 [] -> h:int16 [] -> sel:int16 array -> int16 []

Full name: ArithmeticLogicUnit.Mux8Way16
Multiple items
type RAM64 =
  inherit Chip
  new : unit -> RAM64
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM64

--------------------
new : unit -> RAM64
val ramArray : RAM8 []
val x : RAM64
override RAM64.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM64.doWork
val ramLoad : int16 []
val r : RAM8
val toDecimal : b:int -> binary:int16 array -> int16

Full name: Utils.toDecimal
Multiple items
type RAM512 =
  inherit Chip
  new : unit -> RAM512
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM512

--------------------
new : unit -> RAM512
val ramArray : RAM64 []
val x : RAM512
override RAM512.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM512.doWork
val r : RAM64
Multiple items
type RAM4k =
  inherit Chip
  new : unit -> RAM4k
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM4k

--------------------
new : unit -> RAM4k
val ramArray : RAM512 []
val x : RAM4k
override RAM4k.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM4k.doWork
val r : RAM512
Multiple items
type RAM16k =
  inherit Chip
  new : unit -> RAM16k
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM16k

--------------------
new : unit -> RAM16k
val ramArray : RAM4k []
val x : RAM16k
override RAM16k.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM16k.doWork
val r : RAM4k
Multiple items
type Counter =
  inherit Chip
  new : unit -> Counter
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Counter

--------------------
new : unit -> Counter
val register : Register
val x : Counter
override Counter.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Counter.doWork
val inc : int16
val reset : int16
val current : int16 array
val next : int16 []
val Increment : aBits:int16 [] -> int16 []

Full name: ArithmeticLogicUnit.Increment
val mux1 : int16 []
val MultiMux : sel:int16 -> (int16 [] -> int16 [] -> int16 [])

Full name: ArithmeticLogicUnit.MultiMux
val mux2 : int16 []
val regLoad : int16
val Or : a:int16 -> b:int16 -> int16

Full name: ArithmeticLogicUnit.Or
type RamSize =
  | R8 = 8
  | R64 = 64
  | R512 = 512
  | R4KB = 4096
  | R16KB = 16384

Full name: FsEmulation.RamSize
RamSize.R8: RamSize = 8
RamSize.R64: RamSize = 64
RamSize.R512: RamSize = 512
RamSize.R4KB: RamSize = 4096
RamSize.R16KB: RamSize = 16384
Multiple items
type RAM =
  inherit Chip
  new : size:RamSize -> RAM
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM

--------------------
new : size:RamSize -> RAM
val size : RamSize
val regArray : Register []
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
val x : RAM
override RAM.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.RAM.doWork
val address : int16
Multiple items
type Counter2 =
  inherit Chip
  new : unit -> Counter2
  override doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Counter2

--------------------
new : unit -> Counter2
val x : Counter2
override Counter2.doWork : clk:clk -> inputs:int16 array -> int16 array

Full name: FsEmulation.Counter2.doWork
val toSet : int16 []
val TestCounter : int16

Full name: FsEmulation.TestCounter
val harness : TestHarness
val empty<'T> : 'T []

Full name: Microsoft.FSharp.Collections.Array.empty
val result : TestHarness
val cycle : iterations:int -> clkIters:int -> harness:TestHarness -> TestHarness

Full name: Sequential.cycle
val setInputs : i:int16 array -> harness:TestHarness -> TestHarness

Full name: Sequential.setInputs
TestHarness.outputs: int16 array
val TestRam64 : 'a

Full name: FsEmulation.TestRam64
val mutable harness : TestHarness
val a : int16 array
val b : int16 array
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val Adder : aBits:int16 [] -> bBits:int16 [] -> int16 []

Full name: ArithmeticLogicUnit.Adder
val using : resource:'T -> action:('T -> 'U) -> 'U (requires 'T :> System.IDisposable)

Full name: Microsoft.FSharp.Core.Operators.using
Previous Post Next Post