FPGA logo large

Ruminating on the concept of ROM circuits

This article explores the concept of a ROM or Read-Only-Memory circuits. Today, the venerable ROM chip is rare, largely replaced by updateable non-volatile storage devices like flash memory. Before the age of flash updateable firmware, most consumer electronic devices had their data and code permanently encoded in one or more chips. If you needed to update the information in a device, you replaced the chips.

Some common devices which have used ROM were game cartridges, automotive chips containing tuning parameters, microwave ovens, and television remotes

The popularity of these devices had much to do with their simplicity, low cost and reliability. As a very simple memory device, simple hard-wired combinational circuits provided a direct mapping between input addresses and output data. Because of this, these devices were easy to manufacture and integrate into even low-cost devices.

How can we model a ROM circuit?

How then can we model a ROM device in Verilog for an FPGA? It is simpler than you might think. Shown below is an example of a read-only circuit that takes a 2-bit address as input and returns an 8-bit value. With a 2-bit address space we can address only 4 unique values. We have in effect created a simple lookup table using combinational logic.

module basic_rom
(
    input [1 : 0] addr,
    output reg [7 : 0] data
);

// combinational logic
always @*
    case(addr)
        2'b00: data = 8'b00000000;
        2'b01: data = 8'b10101010;
        2'b10: data = 8'b01010101;
        2'b11: data = 8'b11111111;
    endcase

endmodule

The code is about as simple as Verilog code gets, we have one 2-bit input port addr, and one 8-bit output port data. Notice that since this is a purely combinational circuit there are no clk or rst signals. A single case statement maps our input to the output. This circuit will be synthesized using logic cells to represent the logic circuits.

This is a nice simple design. And it should not be difficult to see that we can scale this to much larger sizes. Unfortunately, for large tables we can quickly use up precious logic cell resources on the FPGA. Depending on the specifics of our FPGA chipset, it is often a better choice to leverage block RAM. Fortunately, FPGA devices usually include block RAM specifically for the purpose of storing and retrieving large amounts of data efficiently, without consuming logic cells.

That sounds exactly like what we want! But as you might imagine, there is a small catch. Block RAM, like all memory, is a synchronous circuit. Which means we will need to add a clk signal and an address register to our design. Let’s see how that looks.

Synchronous read-only memory

In the code below you can see that we’ve added a clk signal as a new input. And we’ve also added a register to hold the current address value called addr_register. The combinational logic remains the same, except that uses addr_register instead of addr to map our inputs to outputs synchronized to the clock. The other addition is a small bit of sequential logic to synchronize updating addr_register to the rising edge of the clk signal.

module basic_sync_rom
(
    input clk,
    input [1 : 0] addr,
    output reg [7 : 0] data
);

// state variables
reg [1 : 0] addr_register;

// sequential logic
always @(posedge clk)
    addr_register <= addr;

// combinational logic
always @*
    case(addr_register)
        2'b00: data = 8'b00000000;
        2'b01: data = 8'b10101010;
        2'b10: data = 8'b01010101;
        2'b11: data = 8'b11111111;
    endcase

endmodule

Not bad! But having seen both examples, you may well be wondering exactly where in the Verilog code we specify the use of logic cells versus block RAM. The somewhat unsatisfying answer is that we can’t. The Verilog compiler is making that choice for us! The reasoning is that the Verilog compiler likely knows a lot more about the low-level details of the specific FPGA device you are targeting. And with that knowledge, the compiler is able to carefully optimize which resources are used for a given design.

Our primary role then is to provide clear hints of our intentions to the compiler. Those hints inform the options that it can consider. If you think about it, this is exactly what we want. In return for ceding certain low-level implementation decisions to the compiler we are freed from the need to tailor our Verilog code for the capabilities of specific FPGA devices.

Populating ROM with data

Now we know how to create a circuit to represent read-only data. But I certainly do not want to have to type out 4K worth of case statements in order to populate a 4K read-only memory. So it would be nice to have a way to more easily create and update these.

Given how simple the format is, it should not be too difficult to write a C program to generate the Verilog for us. So I wrote a simple program to do just that. Take a look at the rom_gen program below.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

int addr_bits = 8;
int data_bits = 8;
const char *g_szOutputFilename = "rom_file";

// display program usage
void usage() {
	puts("\nromgen [-a addr_bits] [-d data_bits] [-o module_name] \n");
	exit(0);
}

// get options from the command line
int getopt(int n, char *args[])
{
	int i;
	for (i = 1; i < n && args[i][0] == '-'; i++)
	{
		if (args[i][1] == 'a') {
			addr_bits = atoi(args[i + 1]);
			i++;
		}

		if (args[i][1] == 'd') {
			data_bits = atoi(args[i + 1]);
			i++;
		}

		if (args[i][1] == 'o')
		{
			g_szOutputFilename = args[i + 1];
			i++;
		}
	}

	return i;
}

// generate template prologue
void prologue(FILE *f, char *filename) {
	fprintf(f, "module %s\n", filename);
	fprintf(f,
		"(\n"
		"\tinput wire clk,\n"
		"\tinput wire [%d : 0] addr,\n"
		"\toutput reg [%d : 0] data\n"
		");\n\n"
		"\treg [%d : 0] addr_reg;\n\n"
		"\t// Sequential logic\n"
		"\talways @(posedge clk)\n"
		"\t\taddr_reg <= addr;\n\n"
		"\t// Combinational logic\n"
		"\talways @*\n"
		"\t\tcase (addr_reg)\n",
		addr_bits - 1,
		data_bits - 1,
		addr_bits - 1
	);
}

// generate template epilog
void epilog(FILE *f) {
	fputs("\t\tendcase\n", f);
	fputs("endmodule\n", f);
}

// generate ROM data
void datagen(FILE *fout) {
	int totalCount = (1 << addr_bits);
	char buf[1024];
	char *s;
	int num;
	int count = 0;

	// read a line
	while (fgets(buf, 1023, stdin)) {
		// tokenize the line
		s = strtok(buf, "\t ");

		while (s) {
			// parse decimal numbers
			if (isdigit(s[0])) {
				num = atoi(s);
				fprintf(fout, "\t\t\t%d'd%d: data = %d'd%d;\t// $%X\n", addr_bits, count, data_bits, num, num);
				count++;
			}

			s = strtok(NULL, "\t ");
		}
	}

	fprintf(fout, "\t\t\tdefault: data = %d'd0;\n", data_bits);
}

//
int main(int argc, char *argv[]) {
	char buf[256];

	if (1 == argc)
		usage();

	getopt(argc, argv);

	sprintf(buf, "%s.v", g_szOutputFilename);
	FILE *fptr = fopen(buf, "wt");

	prologue(fptr, g_szOutputFilename);
		datagen(fptr);
	epilog(fptr);

	fclose(fptr);
}

With this tool in hand, we now have the capability to generate read-only memory with arbitrary address and data sizes. The program is very simple and reads values from the standard input. It generates a Verilog file called rom_file.v unless you specify another name using the -o option.

Demonstrating ROM usage

Now we can generate read-only memory. It would be nice to have a way to test it. I’ve been using the Simulator quite a lot, but for this I would like to use the Basys-3 board and do something interesting with the LEDs. Something like generating a set of data-driven patterns on the LEDs. Those patterns will be pre-defined and stored in our ROM.

The top-level module will use the Basys-3 clk and rst (in the form of btnC) signals as input and as output the array of led signals. It will also instantiate the read-only memory circuit. And as we’ve discussed in previous articles, we will want to use a divide by n counter to that the LED’s will update at a reasonable rate. For that, the top-level module will also instantiate a 23-bit counter circuit which will generate a logic_clk signal each time the counter overflows. The logic_clk signal determines the rate at which we will update the LED display.

module top
(
    input clk,
    input btnC,
    output [15:0] led
);

localparam ADDR_BITS = 9, DATA_BITS = 16;
localparam CTR_BITS = 23;

wire logic_clk;
wire [ADDR_BITS - 1 : 0] addr_bus;
wire [DATA_BITS - 1 : 0] led_bus;

// instantiate divide by n counter
counter_n #(.BITS(CTR_BITS)) counter
(
    .clk(clk),
    .rst(btnC),
    .tick(logic_clk)
);

// instantiate logic device
sld #(.ADDR_BITS(ADDR_BITS), .DATA_BITS(DATA_BITS)) logic_device
(
    .clk(logic_clk),
    .rst(btnC),
    .Din(led_bus),
    .addr(addr_bus),
    .Dout(led)
);

// instantiate ROM
rom_file rom
(
    .clk(logic_clk),
    .addr(addr_bus),
    .data(led_bus)
);

endmodule

Adding a simple logic controller

We also need some logic to track and update the memory address so that we can retrieve the next bit pattern from memory and output that pattern on the LED bus. For that purpose, let’s create a very simple controller circuit that I will call a simple_logic_device. This circuit updates at logic_clk frequency, and it maintains an address register called dp that is incremented each clock cycle. The dp register is used to read data from the ROM circuit and return it on a data bus called Dout.

// A simple logic device
module simple_logic_device
# (parameter ADDR_BITS = 8, DATA_BITS = 8)
(
    input clk,
    input rst,
    input [DATA_BITS - 1 : 0] Din,
    output [ADDR_BITS - 1 : 0] addr,
    output [DATA_BITS - 1 : 0] Dout
);

// data pointer
reg [ADDR_BITS - 1 : 0] dp, dp_next;

// sequential logic
always @(posedge clk, posedge rst)
begin
    if (rst) begin
        dp <= 0;
    end
    else begin
        dp <= dp_next;
    end
end

// combinational logic
always @*
    dp_next = dp + 1;
    
// output logic
assign addr = dp;
assign Dout = Din;

endmodule

There really is not a great deal going on in this circuit. On each clock the addr bus is updated with the current address value from dp. And the data at that address is retrieved from the ROM on Din. The simple logic device doesn’t do any processing of the data, so the data is passed directly from Din to Dout.

Enhancing the logic controller

While the current logic controller is quite simple, if you think about it we created a very rudimentary processor. Albeit a processor with only a single implicit instruction – read 16-bits of data from the location pointed to by our dp register and output that data. In our processor the data pointer register acts like an instruction pointer. Our processor is not Turing Complete and so does not quality as a One Instruction Set Computer (OISC). Thus it is called a controller and not a true processor.

However, with a little bit of work we can expand on the controller design to provide additional functionality. For example, we can add an internal 16-bit A (accumulator) register to hold the data that we read from the ROM. And we could decide to add 2 more bits to the ROM data. Those extra bits could then encode an action to be taken on the data. These four instructions might look something like:

  • 00 – Read the next data into the internal register and output it
  • 01 – Add the signed value of the next data to the internal register and output it
  • 10 – Shift the internal register by the signed value in the next data and output it
  • 11 – Set dp to the value of the next data

I will leave these modifications as an exercise for the reader. Or perhaps as a topic for a future post!

High-level schematic

The schematic for the overall circuit is shown below. From this you can see that the divide-by-n counter generates the clock signal that is used by both the ROM and the simple logic device. Notice that the schematic shows the output from the ROM is sent directly to the LED array. This is not strictly true, the output from the ROM goes to the controller, which sends it to the LED array. However, in this case the Vivado Verilog compiler rightly deduced that the controller passes the data directly through. And so that is how it is represented in the schematic.

ROM Example Schematic

Seeing it in action

Bringing all the pieces together the video below demonstrates the code running on the Basys-3. I used the rom_gen program to produce a ROM file containing some interesting LED patterns. Rebuilding the ROM with different patterns changes the LED behavior without changing any other Verilog code. The processor continuously updates the dp register, which eventually wraps around to zero, executing the one and only operation that we’ve defined.

Video Demonstration

And that’s it for now! As always, the source code for this project is on github.

Please remember to Like and Share these posts on social media. And remember that if you have feedback, questions or suggestions regarding this or other posts, please leave a comment!

Discover more from FPGA Coding

Subscribe now to keep reading and get access to the full archive.

Continue reading