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.
For my primary system controller, I’ll build a state machine that will follow the process flow I am looking to design.
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.
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.
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!