Programming lesson
Building Memory Blocks on FPGA: A Hands-On Guide for Ee371 Lab 2
Learn how to implement 32x3 RAM on an FPGA using library modules and SystemVerilog. Step-by-step tutorial for Ee371 Lab 2 with simulation tips and DE1-SoC deployment.
Introduction to FPGA Memory Blocks
In modern computer systems, memory is a critical component. When using FPGA technology, you can leverage dedicated memory resources like the M10K blocks found in Intel FPGAs. Each M10K block contains 10,240 memory bits and can be configured to various aspect ratios. In this tutorial, we focus on building a 32 × 3 RAM module, which stores 32 words of 3 bits each. This is a common exercise in digital design courses like Ee371 Lab 2, and understanding it will help you design more complex memory systems for applications ranging from AI accelerators to gaming consoles.
Understanding the 32x3 RAM Module
The RAM module we implement has a depth of 32 words and a width of 3 bits. It includes a 5-bit address port, a 3-bit bidirectional data port, and a write enable control. However, due to the properties of M10K blocks, we use separate registered inputs for address, data, and write enable, and an unregistered data output. This matches the structure shown in Figure 1b of the lab.
Task 1: Using Library Modules (IP Catalog)
Quartus Prime provides prebuilt modules in its IP Catalog. To create the RAM:
- Create a new Quartus project.
- Open the IP Catalog (Tools > IP Catalog). Expand Library > Basic Functions > On Chip Memory, then double-click RAM: 1-PORT.
- In the dialog, name the file
ram32x3and select Verilog as file type. Click OK. - In the MegaWizard, set width to 3 and depth to 32. Choose M10K as memory block type and Single clock as clocking method. Click Next.
- Deselect q output port under Registered outputs to match the unregistered output. Click Next and then Finish.
- Add the generated IP file to your project.
Now, create a SystemVerilog top module that instantiates ram32x3. For example:
module memory_top(input logic clk, input logic [4:0] addr, input logic [2:0] data_in, input logic wren, output logic [2:0] data_out);
ram32x3 u0 (.address(addr), .clock(clk), .data(data_in), .wren(wren), .q(data_out));
endmoduleCompile the design. The Compilation Report will show that 96 memory bits are used in one M10K block.
Simulation with ModelSim
Create a testbench to verify read and write operations. You may encounter the error: Instantiation of 'altsyncram' failed. The design unit was not found. To fix this, in ModelSim, go to Simulate > Start Simulation, click the Libraries tab, add altera_mf_ver under Search libraries. Then, on the Design tab, select your testbench module and click OK. Alternatively, add +altera_mf_ver to the vsim command in your simulation script.
Task 2: Memory Using SystemVerilog
Instead of using the IP Catalog, you can describe the memory directly in SystemVerilog using a multidimensional array. Declare a 32 × 3 array as:
logic [2:0] memory_array [31:0];For synchronous reads (registered), the array will be mapped to memory blocks. Write a module that implements the RAM behavior:
module ram32x3_sv(input logic clk, input logic [4:0] addr, input logic [2:0] data_in, input logic wren, output logic [2:0] data_out);
logic [2:0] mem [31:0];
always_ff @(posedge clk) begin
if (wren)
mem[addr] <= data_in;
data_out <= mem[addr];
end
endmoduleThis module can be used with the same testbench from Task 1. Note that the read is registered (synchronous), so the array will be implemented using M10K blocks.
Top-Level Module for DE1-SoC
Create a top-level module that connects to the DE1-SoC board. Use switches SW[4:0] for address, SW[7:5] for data input, KEY[0] for write enable (active low), and display the output on HEX0 (7-segment). A basic 7-segment driver is provided; you may modify it to display hex values. For example, to show the 3-bit data on HEX0, you can use a case statement or a lookup table. After synthesis, download the bitstream to the DE1-SoC via LabsLand, selecting the Standard user interface.
Task 3: Library Memory with Independent Read and Write Ports
For this task, you create a dual-port RAM with separate read and write addresses. Use the IP Catalog again, but this time select RAM: 2-PORT. Configure it with 32 words of 3 bits, single clock, and unregistered outputs. Also, create a Memory Initialization File (MIF) to preload the memory with data. The MIF is a text file that specifies initial content. For example:
DEPTH = 32;
WIDTH = 3;
ADDRESS_RADIX = HEX;
DATA_RADIX = HEX;
CONTENT
BEGIN
0 : 0;
1 : 1;
...
1F : 7;
END;In the MegaWizard, on the Mem Init tab, load the MIF file. Then instantiate the module in your design and test it. This type of memory is useful in applications where read and write operations happen simultaneously, such as in a video game frame buffer or a network packet buffer.
Conclusion
Implementing memory blocks on FPGA is a fundamental skill for digital designers. Whether you use library modules or write your own SystemVerilog code, understanding the aspect ratio, synchronous vs asynchronous reads, and memory initialization will serve you well in more advanced projects. The concepts you learned here apply to modern hardware design in AI, gaming, and high-frequency trading systems. Practice by modifying the memory size or adding more ports, and always simulate before deploying to hardware.