Designing a Single-Port Memory in Verilog and SystemVerilog

Memory elements are fundamental building blocks in digital circuits, used to store and retrieve digital data. A single-port memory is a type of memory element that has one write port and one read port. In this tutorial, we will discuss the design of a single-port memory in Verilog and SystemVerilog using the provided module.

Overview of Single-Port Memory

A single-port memory is a memory element that can store digital data and has one input port for writing data and one output port for reading data. It is used in applications such as microcontrollers, microprocessors, and other digital circuits.

The memory has a write enable signal, which is used to control the write operation. When the write enable signal is high, the data at the input port is written to the memory. The memory also has an address input port, which is used to select the memory location that will be written to or read from.

Designing a Single-Port Memory in Verilog and SystemVerilog

The following code shows the implementation of a single-port memory in Verilog and SystemVerilog using the provided module:

module SinglePortMemory #(
  parameter  NumEntries = 256,
  parameter  DataWidth  = 8,
  localparam AddrWidth  = $clog2(NumEntries)
) (
  input  logic                 clk,
  input  logic                 writeEn,
  input  logic [AddrWidth-1:0] writeAddr,
  input  logic [DataWidth-1:0] writeData,
  input  logic [AddrWidth-1:0] readAddr,
  output logic [DataWidth-1:0] readData
);

  logic [DataWidth-1:0] mem[NumEntries];

  always_ff @(posedge clk) begin
    if (writeEn) begin
      mem[writeAddr] <= writeData;
    end
  end

  assign readData = mem[readAddr];

endmodule

In the above code, we define a module named SinglePortMemory that has six ports: clk, writeEn, writeAddr, writeData, readAddr, and readData. The module has three parameters: NumEntries, DataWidth, and AddrWidth.

The NumEntries parameter specifies the number of memory entries, which is set to a default value of 256. The DataWidth parameter specifies the width of the data bus, which is set to a default value of 8 bits. The AddrWidth parameter is set to the ceiling of the base-2 logarithm of NumEntries. This sets the width of the address bus to the minimum number of bits required to address all memory locations.

Inside the module, we define a memory array mem, which is implemented as a register array in hardware. The size of the array is determined by the NumEntries parameter. The width of each memory element is determined by the DataWidth parameter.

The always_ff block is used to describe the behavior of the memory. It triggers on the rising edge of the clock signal clk. When the write enable signal writeEn is high, the data at the input port writeData is written to the memory location specified by the address input writeAddr.

The assign statement is used to assign the value stored in the memory location specified by readAddr to the output readData.

Designing a Byte-Enable Memory in Verilog and SystemVerilog

In some applications, it is necessary to write only part of a memory element, rather than the entire element. In these cases, byte-enable memory can be used. Byte-enable memory allows you to write data to only a portion of a memory location.

The SinglePortMemoryByteEn module is a modified version of the SinglePortMemory module, which supports byte-enable memory. The module has an additional input signal, byteEn, which is a vector of bytes that specifies which bytes of the write data should be written. The following code shows the implementation of the SinglePortMemoryByteEn module:

module SinglePortMemoryByteEn #(
  parameter  NumEntries = 256,
  parameter  DataWidth  = 32,
  localparam AddrWidth  = $clog2(NumEntries),
  localparam ByteWidth   = 8,
  localparam ByteNum    = DataWidth / ByteWidth
) (
  input  logic                 clk,
  input  logic                 writeEn,
  input  logic [  ByteNum-1:0] byteEn,
  input  logic [AddrWidth-1:0] writeAddr,
  input  logic [DataWidth-1:0] writeData,
  output logic [DataWidth-1:0] readData
);

  logic [DataWidth-1:0] mem[NumEntries];

  always_ff @(posedge clk) begin
    if (writeEn) begin
      for (int i = 0; i < ByteNum; i++) begin
        if (byteEn[i]) begin
          mem[writeAddr][i*ByteWidth+:ByteWidth] <= writeData[i*ByteWidth+:ByteWidth];
        end
      end
    end
  end

  assign readData = mem[readAddr];

endmodule

In the above code, the SinglePortMemoryByteEn module has six ports: clk, writeEn, byteEn, writeAddr, writeData, and readData. It also has four parameters: NumEntries, DataWidth, AddrWidth, ByteWidth, and ByteNum.

The NumEntries parameter specifies the number of memory entries. The DataWidth parameter specifies the width of the data bus, which is set to a default value of 32 bits. The AddrWidth parameter sets the width of the address bus to the minimum number of bits required to address all memory locations. The ByteWidth parameter specifies the width of each byte in the memory. The ByteNum parameter is the number of bytes in the memory element.

The memory element is implemented as a register array in hardware, with a size of NumEntries and a width of DataWidth. The always_ff block is used to describe the behavior of the memory. When the write enable signal writeEn is high, the loop iterates over each byte of the write data and checks the corresponding byte in the byteEn vector. If the byte is selected, the data is written to the corresponding byte of the memory location.

When reading data from a byte-enabled memory, the read data is returned as a single data element, with the selected bytes of the memory element.

Register at the Read Data Path

In some cases, a register is added at the read data path of a single-port memory. This register serves as a buffer between the memory and the rest of the circuitry. The reason for adding this register is to avoid timing problems that may arise due to the delay in the memory read operation.

When a read operation is performed on a memory element, there is a delay between the time the address is presented and the time the data is available on the output. This delay is known as the memory access time. If the read data is used immediately without any buffering, there is a risk of timing violations in the circuitry that uses the read data.

By adding a register at the read data path, the data is temporarily stored in the register, allowing time for the data to settle and become stable. This ensures that the read data is not used prematurely, preventing any timing issues that may arise in the circuit.

The register also has the added benefit of improving the timing performance of the circuitry that uses the read data. By introducing a pipeline stage in the read data path, the clock frequency can be increased, allowing for faster operation of the circuit.

Conclusion

In this tutorial, we discussed how to design a single-port memory in Verilog and SystemVerilog, which is an essential component in digital circuits for storing and retrieving digital data. We covered how to modify the SinglePortMemory module to support byte-enable memory and the importance of adding a register at the read data path to avoid timing issues.

Single-port memories are used in a variety of applications, such as microcontrollers, microprocessors, and other digital circuits. By understanding the principles behind their design, you can create more complex digital circuits with confidence.