Beginning Logic Design – Part 9

Hello and welcome to Part 9 of my Beginning Logic Design series!

In this post, I’ll build a design that utilizes the system bus from the previous post and the ALU I started in Post 5. This design will read data from a ROM mapped to the system bus address space, add the numbers together, and store the results in RAM.

Design Plan

To build this design I will need to decide how I want my system bus address space mapped. I’ll need to implement a new ROM module that can reside in that address space. For this build I will give the RAM all addresses that start with 0, and the ROM will handle addresses that start with 1 to evenly split my address space between them.

Memory Map Diagram

For my primary system controller, I’ll build a state machine that will follow the process flow I am looking to design.

State machine diagram for this controller

This machine will begin with a START state to set up it’s internal variables. Next, in the REQUEST_A state the next a value will be requested from ROM via the system bus. In READ_A the value from ROM will be read on the system bus; if a is zero the system will stop, otherwise it will request b and transition to b. In STORE, the sum of a and b will be written to RAM. The process loops until a is zero.

This setup will allow use of a arbitrarily long null-terminated list of values to add, limited by the size of the ROM.



Reusing Components

It can be significantly more time efficient to reuse designs that are already available so I will start this build by pulling in my top and ram modules from the previous post and the final ALU design from post 7.

I’ll modify my top module to add a reset signal, and will mock out my new controller and rom modules. I’ll also add to my initial block some simulation steps that toggle the reset signal on for 4 nanoseconds.

`timescale 1ns / 1ps
module top ();
  logic reset;
  logic clock;

  // System Bus
  logic slave_clock;
  logic read;
  logic write;
  logic [15:0] address_bus;
  wire logic [7:0] data_bus;

  assign slave_clock = ~clock;
  master controller (
    reset,
    clock,
    read,
    write,
    address_bus,
    data_bus
  );

  ram memory (
    slave_clock,
    read,
    write,
    address_bus,
    data_bus
  );

  rom storage (
    slave_clock,
    read,
    write,
    address_bus,
    data_bus
  );

  initial begin
    clock = 0;
    reset = 1;
    #4 reset = 0;
  end

  always begin
    #1 clock = ~clock;
  end

endmodule

The RAM module remains the same as before.

`timescale 1ns / 1ps

module ram (
  input logic clock,
  input logic read,
  input logic write,
  input logic [15:0] address_bus,
  inout logic [7:0] data_bus
);

  assign data_bus = (read && address_bus[15] == 0) ? memory[address_bus[14:0]] : 'bZ;

  logic [7:0] memory [0:(1<<15)-1];

  always_ff @ (posedge clock) begin
    if (address_bus[15] == 0 && write) begin
      memory[address_bus[14:0]] <= data_bus;
    end
  end


endmodule

Building a ROM

Before building the state machine, I’ll build my ROM to store the numbers I’d like to add together.

The SystemVerilog language provides two handy functions for implementing ROMs, readmemb and readmemh, which can set a memory to the values from a local file. The difference between these functions is the expected format of the files to be read. readmemb expects binary notation in ASCII, while readmemh expects the file to be in hexadecimal notation. I prefer to work in hexadecimal so I’ll be using readmemh.

The ROM design is very similar to RAM, but we can drop the write logic and change the read handling to only handle addresses that start with 1.

The main thing we’ll need to add is an initial block that sets our rom modules memory to the values from our specified file, in this case I’ll have it read from a file named rom.hex.

`timescale 1ns / 1ps

module rom (
  input logic clock,
  input logic read,
  input logic write,
  input logic [15:0] address_bus,
  inout logic [7:0] data_bus
);

  assign data_bus = (read && address_bus[15] == 1) ? memory[address_bus[14:0]] : 'bZ;

  logic [7:0] memory [0:(1<<15)-1];

  initial begin
    $readmemh("rom.hex", memory);
  end


endmodule

Now I’ll write a small rom.hex file to perform a few operations. The file is a representation of hexadecimal using normal text characters, so every 2 digits represents one byte in hexadecimal. Whitespace and line breaks are ignored.

02 02
09 07
20 02
00

This sequence should instruct our controller to perform the operations 2 + 2, 9 + 7 and 32 + 2.

The Controller

The most interesting part of this design is the new controller.

I’ll start off by cleaning out most of the controller from the previous post and adding my ALU module along with the signals to communicate with it. In this case I don’t care about all of the outputs, so I will use a different syntax to map the signals connecting to the ALU by name instead of by the order they are defined in the ALU module. I’ll also set my operation set to ADD and my carry_in to 0.

`timescale 1ns / 1ps

import ALU::*;

module master (
  input logic reset,
  input logic clock,
  output logic read,
  output logic write,
  output logic [15:0] address_bus,
  inout logic [7:0] data_bus
);

  opcode operation;
  logic [7:0] a;
  logic [7:0] b;
  logic carry_in;
  logic [7:0] y;

  assign operation = ADD,
    carry_in = 0;

  alu ALU (
    .clock(clock),
    .operation(operation),
    .a(a),
    .b(b),
    .carry_in(carry_in),
    .y(y)
  );

endmodule

Next I will add an enumeration to give a unique number to each of my states. Since I have 6 states, I can use a 3 bit number to support all of my desired states.

typedef enum logic [2:0] {
  START,
  REQUEST_A,
  READ_A,
  READ_B,
  STORE,
  STOP
} master_state;

Inside of the module definition, I’ll add a new variable to hold the current state.

master_state state;

Next, I’ll start my sequential logic with always_ff. I’ll have the logic follow the rising edge of clock or reset. Including the rising edge for reset lets this module asynchronously reset immediately when reset goes high, instead of waiting for the next clock signal. Inside the block I’ll have a reset handler that sets the state to START and I’ll mock out the remaining states.

always_ff @ (posedge clock or posedge reset) begin
  if (reset) begin
    state <= START;
  end else begin
    case(state)
      START: begin
        
      end
      REQUEST_A: begin
        
      end
      READ_A: begin
        
      end
      READ_B: begin
        
      end
      STORE: begin
        
      end
      STOP: begin
        
      end
    endcase
  end
end

Implementing the State Machine

For proper operation here, I’ll need a few internal variables. I already have registers to hold my a and b values, but I’ll need two more to use as pointers to where I’m currently reading from in ROM, and where I’m storing the result in RAM.

logic [15:0] read_pointer;
logic [15:0] write_pointer;

To support writing, I also need a means to put the output of the ALU onto the data bus. For this I’ll use an assign to put the y signal on data_bus when the write signal is high since the only writes I’ll have are from y.

assign data_bus = write ? y : 'bz;

Now I’ll add the logic for my START state. It will set the system bus signals to a known state and initialize my read and write pointers. It will also change the state variable to transition to the next step.

START: begin
  read <= 0;
  write <= 0;
  read_pointer <= 'h8000;
  write_pointer <= 0;
  state <= REQUEST_A;
end

The REQUEST_A state will implement request to start reading from the address that read_pointer is set to.

REQUEST_A: begin
  address_bus <= read_pointer;
  read <= 1;
  state <= READ_A;
end

In the READ_A state, I’ll look at what was returned from ROM on the data bus. If zero, the state will change to STOP. If the value returned on data_bus is non-zero, that value will be stored in a, I’ll increment the read_pointer and update the address_bus to point to the next value and transition to the READ_B state.

READ_A: begin
  if (data_bus == 0) begin
    state <= STOP;
  end else begin
    a <= data_bus;
    address_bus = ++read_pointer;
    state <= READ_B;
  end
end

For READ_B I’ll store the returned value from data_bus into b, pre-increment read_pointer in anticipation of the next loop and transition to STORE. I’ll also stop my read operation since I am done reading for this iteration.

READ_B: begin
  b <= data_bus;
  read_pointer++;
  read <= 0;
  state <= STORE;
end

Finally, in STORE my ALU will have updated it’s output on y to reflect the inputs on a and b. I’ll set the address bus to my write_pointer and set write so that my y value will be available on data_bus. I’ll transition back to REQUEST_A to continue the loop.

STORE: begin
  address_bus <= write_pointer++;
  state <= REQUEST_A;
  write <= 1;
end

With this as-is, I have a design flaw as nothing within the loop is clearing my write signal. To fix this I’ll add a statement to REQUEST_A to make sure write is 0. Lastly, I’ll add a $finish(); to my STOP state so that simulation stops there.

Here’s how my master module is defined after all of this implementation.

`timescale 1ns / 1ps

import ALU::*;

typedef enum logic [2:0] {
  START,
  REQUEST_A,
  READ_A,
  READ_B,
  STORE,
  STOP
} master_state;

module master (
  input logic reset,
  input logic clock,
  output logic read,
  output logic write,
  output logic [15:0] address_bus,
  inout logic [7:0] data_bus
);

  opcode operation;
  logic [7:0] a;
  logic [7:0] b;
  logic carry_in;
  logic [7:0] y;

  master_state state;
  logic [15:0] read_pointer;
  logic [15:0] write_pointer;

  assign operation = ADD,
    carry_in = 0,
    data_bus = write ? y : 'bz;

  alu ALU (
    .clock(clock),
    .operation(operation),
    .a(a),
    .b(b),
    .carry_in(carry_in),
    .y(y)
  );

  always_ff @ (posedge clock or posedge reset) begin
    if (reset) begin
      state <= START;
    end else begin
      case(state)
        START: begin
          read <= 0;
          write <= 0;
          read_pointer <= 'h8000;
          write_pointer <= 0;
          state <= REQUEST_A;
        end
        REQUEST_A: begin
          write <= 0;
          address_bus <= read_pointer;
          read <= 1;
          state <= READ_A;
        end
        READ_A: begin
          if (data_bus == 0) begin
            state <= STOP;
          end else begin
            a <= data_bus;
            address_bus = ++read_pointer;
            state <= READ_B;
          end
        end
        READ_B: begin
          b <= data_bus;
          read_pointer++;
          read <= 0;
          state <= STORE;
        end
        STORE: begin
          address_bus <= write_pointer++;
          state <= REQUEST_A;
          write <= 1;
        end
        STOP: begin
          $finish();
        end
      endcase
    end
  end

endmodule

It’s certainly become a bit more sophisticated with all these steps each implementing a piece of the process that grabs my numbers to add from ROM and storing the results in RAM.

Finally, I’ll simulate this design to look at how my states and various signals are changing over time.

Waveforms from simulation

In the building of this design I did create several flawed implementations, and this wave viewer helped me inspect the changes that were going on for each cycle, ultimately I’ll look at the RAM to make sure I’m only seeing the set values that are expected.

Inspecting the memory at the end of simulation

The proper results of the 3 addition operations from my ROM are in RAM right where expected, validating this design! I hope this design helps give some understanding of how ROMs and state machines can be used in SystemVerilog. If you have any questions or feedback I welcome your response in the comments. Keep tinkering!

Save

Leave a Reply

Your email address will not be published. Required fields are marked *