Introduction to FPGA Part 8 - Memory and Block RAM
2021-12-27 | By ShawnHymel
License: Attribution
A field-programmable gate array (FPGA) is a reconfigurable integrated circuit (IC) that lets you implement a wide range of custom digital circuits. Throughout the series, we will examine how an FPGA works as well as demonstrate the basic building blocks of implementing digital circuits using the Verilog hardware description language (HDL).
Storing large amounts of data can be difficult in FPGAs if you use just logic cells. Each cell (usually) contains one look-up table (LUT) and a D flip-flop. Storing data in the D flip-flops (e.g. 1 bit of information) would quickly consume all of your logic cells. Some synthesis tools will allocate LUTs to be used as memory if the data requirements are low enough. This is known as “distributed” random access memory (RAM).
However, most FPGAs offer other types of memory alongside their reprogrammable fabric. Most commonly, you’ll see chunks of memory called “block RAM” that you can allocate to store data. Unless specified in code, the synthesis tool will determine (based on your data requirements) if distributed RAM or block RAM is used for your design.
In the previous tutorial, we demonstrated how to write Verilog testbenches and simulate designs using Icarus Verilog. This time, we show how to allocate block RAM in the iCE40 and read/write data to/from it. We also show how to initialize values in the RAM from a text file.
Video
If you have not done so, please watch the following video, which explains the concepts required to complete the challenge. It also demonstrates a working version of the challenge:
Required Hardware
For this challenge, you will need the following hardware:
- FPGA development board based on the Lattice iCE40. I recommend the iCEstick for this series. However, any of the development boards listed as “supported” by the apio project should work.
- Breadboard
- Pushbuttons
- Jumper wires
- (Optional) USB extension cable
Hardware Connections
The PMOD connector at the end of the iCEstick has the following pinout:
A full pinout of the iCEstick can be found here.
Connect 4 pushbuttons to the iCEstick as follows. Note that you do not need pull-up resistors on the buttons. We will use the internal pull-up resistors available in the FPGA.
Resources
The following datasheets and guides might be helpful as you tackle the challenges:
- GitHub repository - contains all examples and solutions for this series
- Verilog documentation
- Apio tool usage
- iCE40 LP/HX Datasheet
- iCEstick Evaluation Kit User’s Guide
Challenge
Your challenge is to create a simple LED sequencer. You should configure your 4 pushbuttons as follows:
- RST: reset button (globally reset all counters, state machines, etc.)
- SET: set button that records the value on VAL[1:0] into a memory location and then increments the memory pointer
- PTN[0]: first bit of the value/pattern
- PTN[1]: second bit of the value/pattern
The idea is that you can record a simple sequence (up to 8 values) in block RAM. You do this by holding down VAL[1:0] to create a number (e.g. binary 00, 01, 10, or 11) and then pressing the SET button. When the SET button is pressed, the 2-bit binary value is stored in memory and the memory address pointer is increased by one. You repeat the process, holding down a new 2-bit binary number.
You continue doing this until you’ve filled up the sequencer memory with 8 values. At the same time, the sequencer is continuously cycling through the memory, reading each value and displaying it on 2 of the LEDs.
So, if you enter the sequence 0, 1, 0, 1, 2, 3, 2, 1, on the next full loop, the sequencer would display the following binary values on the LEDs: 00, 01, 00, 01, 10, 11, 10, 01.
Hint: you will likely need to use many of the concepts and modules you wrote from previous videos/challenges! This includes a clock divider and a button debounce module. You will also likely want to make these designs modular and instantiate them in a top-level design. Finally, I highly recommend simulating your design to make sure everything works.
Solution
Spoilers below! I highly encourage you to try the challenge on your own before comparing your answer to mine. Note that my solution may not be the only way to solve the challenge.
The code for this project is too large to paste onto this page in its entirety. As a result, you can find all of the Verilog modules, physical constraint file, memory initialization text file, and testbench/simulation files in this repository.
In this project, I use the clock-divider, debouncer, and memory Verilog modules written in this and previous episodes/challenges. I also create a top-level design (named sequencer-top.v) that brings together these modules and provides some glue logic:
sequencer-top.v
// Top-level design for sequencer
//
// Inputs:
// clk - 12 MHz clock
// rst_btn - pushbutton (RESET)
// set_btn - pushbutton (SET value at next memory address)
// ptn_0_btn - pushbutton (PATTERN[0] for sequence)
// ptn_1_btn - pushbutton (PATTERN[1] for sequence)
//
// Outputs:
// led[1:0] - LEDs
//
// Push RESET to set everything to 0. Hold PATTERN[0] and/or PATTERN[1] and
// press SET to record value in memory. Continue doing this for up to 8 memory
// locations. Sequence will play on LEDs and loop forever.
//
// Date: November 15, 2021
// Author: Shawn Hymel
// License: 0BSD
// Top-level design for the sequencer
module sequencer_top #(
// Parameters
parameter DEBOUNCE_COUNTS = 480000 - 1, // Counts for debounce wait
parameter STEP_COUNTS = 6000000 - 1, // Clock cycles between steps
parameter NUM_STEPS = 8 // Number of steps
) (
// Inputs
input clk,
input rst_btn,
input set_btn,
input ptn_0_btn,
input ptn_1_btn,
// Outputs
output reg [1:0] led,
output [2:0] unused_led
);
// Internal signals
wire rst;
wire set;
wire set_d;
wire [1:0] ptn;
wire div_clk;
wire [1:0] r_data;
// Storage elements (initialize some values)
reg w_en = 0;
reg r_en = 1'b1; // Always high!
reg [2:0] w_addr = 0;
reg [2:0] r_addr;
reg [1:0] w_data;
reg [2:0] step_counter;
reg [2:0] mem_ptr = 0;
// Turn off unused LEDs
assign unused_led = 3'b000;
// Invert active-low buttons
assign rst = ~rst_btn;
assign set = ~set_btn;
assign ptn[0] = ~ptn_0_btn;
assign ptn[1] = ~ptn_1_btn;
// Clock divider
clock_divider #(
.COUNT_WIDTH(24),
.MAX_COUNT(STEP_COUNTS)
) div (
.clk(clk),
.rst(rst),
.out(div_clk)
);
// Button debouncer for set buttons
debouncer #(
.COUNT_WIDTH(24),
.MAX_CLK_COUNT(DEBOUNCE_COUNTS)
) set_debouncer (
.clk(clk),
.rst(rst),
.in(set),
.out(set_d)
);
// Memory unit
memory #(
.MEM_WIDTH(2),
.MEM_DEPTH(NUM_STEPS),
.INIT_FILE("mem_init.txt")
) mem (
.clk(clk),
.w_en(w_en),
.r_en(r_en),
.w_addr(w_addr),
.r_addr(r_addr),
.w_data(w_data),
.r_data(r_data)
);
// Read from memory each divided clock cycle
always @ (posedge div_clk or posedge rst) begin
if (rst == 1'b1) begin
led <= 0;
r_addr <= 0;
step_counter <= 0;
end else begin
r_addr <= step_counter;
step_counter <= step_counter + 1;
led <= r_data;
end
end
// Register write data as soon as debounced set signal goes high
always @ (posedge set_d) begin
w_data <= ptn;
end
// Handle writing pattern to memory
always @ (posedge clk or posedge rst) begin
// Reset memory address pointer and write enable signal
if (rst == 1'b1) begin
mem_ptr <= 0;
w_en <= 1'b0;
// Set write enable high and increment memory pointer
end else if (set_d == 1'b1) begin
w_addr <= mem_ptr;
w_en <= 1'b1;
mem_ptr <= mem_ptr + 1;
// Reset write enable signal
end else begin
w_en <= 1'b0;
end
end
endmodule
In this design, we invert the button signals and instantiate the clock divider, debouncer, and memory modules. Note that we instantiate the memory module to use block RAM that is 2 bits wide and 8 elements deep.
Next, we read from memory on each divided clock cycle:
// Read from memory each divided clock cycle
always @ (posedge div_clk or posedge rst) begin
if (rst == 1'b1) begin
led <= 0;
r_addr <= 0;
step_counter <= 0;
end else begin
r_addr <= step_counter;
step_counter <= step_counter + 1;
led <= r_data;
end
end
The value in memory is clocked/registered to the output LEDs. The step_counter (i.e. the read memory address pointer) is also increased by one. Because step_counter is only 3 bits wide, it will automatically wrap from 0x7 to 0x0, so we don’t need additional logic to prevent the pointer from exceeding the memory address space.
In parallel, the value on the pattern buttons (ptn[1:0]) is registered so that it can be written to memory on the next (non-divided) clock cycle (even if you let go of the ptn buttons after pressing the set button).
// Register write data as soon as debounced set signal goes high
always @ (posedge set_d) begin
w_data <= ptn;
end
Finally, we handle writing the pattern to memory:
// Handle writing pattern to memory
always @ (posedge clk or posedge rst) begin
// Reset memory address pointer and write enable signal
if (rst == 1'b1) begin
mem_ptr <= 0;
w_en <= 1'b0;
// Set write enable high and increment memory pointer
end else if (set_d == 1'b1) begin
w_addr <= mem_ptr;
w_en <= 1'b1;
mem_ptr <= mem_ptr + 1;
// Reset write enable signal
end else begin
w_en <= 1'b0;
end
end
Whenever the debounced set signal (a single pulse) is seen, the write enable (w_en) line is pulsed for a clock cycle while the pattern is present on the write data (w_data) bus. Note that the pointer for the write address (w_addr) is set from a separate mem_ptr value, which is also incremented.
Save all of these files in a single folder. Initialize the project with apio, verify, and run simulation:
apio init -b icestick
apio verify
apio sim
You should see GTKWave open with the simulated design. Zoom out, and make sure that the pattern is stored in memory. Note that the initialization memory file has the pattern stored as: 0, 0, 0, 0, 1, 1, 1, 1. So, you should see: 3, 3, 0, 0, 1, 1, 1, 1 after the pattern has been set from the simulated buttons.
Now, enter the following to upload the design to your FPGA:
apio build
apio upload
Try entering new patterns on the buttons and see if the sequencer replays them!
Recommended Reading
The following content might be helpful if you would like to dig deeper:
- Memory Usage Guide for iCE40 Devices
- Nandland’s guide for Block RAM
- Project F’s guide to initialize memory in Verilog
Introduction to FPGA Part 1 - What is an FPGA?
Introduction to FPGA Part 2 - Toolchain Setup
Introduction to FPGA Part 3 - Getting Started with Verilog
Introduction to FPGA Part 4 - Clocks and Procedural Assignments
Introduction to FPGA Part 5 - Finite State Machine (FSM)
Introduction to FPGA Part 6 - Verilog Modules and Parameters
Introduction to FPGA Part 7 - Verilog Testbenches and Simulation
Introduction to FPGA Part 9 - Phase-Locked Loop (PLL) and Glitches
Introduction to FPGA Part 10 - Metastability and FIFO
Introduction to FPGA Part 11 - RISC-V Softcore Processor
Introduction to FPGA Part 12 - RISC-V Custom Peripheral
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum