Hardware implementations for basic digital circuit designs applied to a Digilent Basys 3 development board with a Xilinx Artix-7 FPGA chip.
These circuit designs are written in the Verilog hardware description language, using the Vivado Design Suite.
Contents: What is this? · What is an FPGA? · Steps to create and implement hardware designs on an FPGA · Circuits
I began this project as part of my exporation into FPGAs coming from a software engineering background with limited hardware and electrical engineering knowledge, and hope that it can be useful for other beginners.
If you do not have a background in digital design or hardware, I suggest at least partially reading Digital Design and Computer Architecture by David Harris & Sarah L. Harris before getting started with FPGA development. The book provides a good foundation and is written very well, easy to follow and has a lot of follow-up exercises to test your knowledge.
If you want to follow along and try implementing some of the circuits, here are some suggestions:
- Implement circuit designs in the order they are mentioned in this README.
- If you don't have a development board yet, it is still possible to start with module design and simulation.
- If you do have a board, always try out your modules on hardware — simulations aren't always the truth!
- Other development boards should be fine as long as they have similar I/O to the Basys 3.
- There is no need to stick to Verilog, feel free to try other HDLs.
- Make sure to write a testbench or multiple for each module.
- Compare your module designs with mine, but don't take them as gospel — I'm a learner too!
- Feel free to create an issue to suggest new circuit designs that are suitable for beginners.
A field-programmable gate array (FPGA) is a reprogrammable integrated circuit consisting of:
- look-up tables (LUTs) that can be programmed to implement any combinationial logic function,
- routing architecture (fabric) that can be programmed to connect logic blocks (comprising LUTs) to I/O blocks. With this flexibility FPGAs can be programmed to implement arbitrary hardware devices through the use of a hardware description language (HDL) such as Verilog.
For example, FPGAs can be used to implement:
- low-latency network interfaces for receiving, manipulating and sending packets over different protocols,
- video and image processors for applying transformations such as filtering and compression,
- optimized inference pipelines for trained machine learning models,
- and even microprocessors fully implemented in digital logic (called soft microprocessors).
FPGAs are typically integrated into development boards such as the Digilent Basys 3 which consists of a Xilinx Artix-7 FPGA chip. Development boards provide:
- on-board I/O devices such as push buttons, slide switches, LEDs, seven-segment displays,
- ports for ethernet, VGA, HDMI, USB and PMOD connections,
- on-board block RAM, digital signal processing cores and sometimes even CPU cores.
Digilent Basys 3 development board with a Xilinx Artix-7 FPGA chip (in the centre).
Image courtesy of Digilent (from the Basys 3 reference manual).
Despite some similarities, hardware design and implementation of digital circuits on an FPGA has a very different approach and process to software development.
Digital circuits are designed at this stage through the use of a HDL such as Verilog or VHDL. Most computer-aided design (CAD) tools such as Vivado can produce a schematic of the module for visual inspection.
Example: Verilog module design and corresponding schematic for an 8-bit register file / SRAM.
// 8-bit register file with one read port and one write port.
// Uses 3-bit addresses (8 words) with 8-bit words, i.e. 64-bit capacity.
module RegisterFile #(parameter ADDRESS_WIDTH = 3, WORD_WIDTH = 8) (
// 100MHz oscillator
input wire clk,
// Flag required for enabling data provided to the write port to be stored
input wire write_enable,
// Address of the register holding the word to read from the register file
input wire [ADDRESS_WIDTH-1:0] read_addr,
// Output word from the read port of the register file
output reg [WORD_WIDTH-1:0] read_data,
// Address of the register that a word should be written to in the register file
input wire [ADDRESS_WIDTH-1:0] write_addr,
// Input word provided to the write port of the register file
input wire [WORD_WIDTH-1:0] write_data
);
// Create register space
parameter N_ADDRESSES = 2 ** ADDRESS_WIDTH;
reg [WORD_WIDTH-1:0] register_space [ADDRESS_WIDTH-1:0];
// Initialize all registers to zero
integer addr;
initial
for (addr = 0; addr < N_ADDRESSES; addr = addr + 1)
register_space[addr] = {WORD_WIDTH {1'b0}};
// Asynchronous reads
always @(*)
read_data = register_space[read_addr];
// Synchronous writes
always @(posedge clk)
if (write_enable)
register_space[write_addr] <= write_data;
endmodule
In this case Vivado has correctly inferred that we have implemented a RAM module, and has represented the circuit as a single diagram element. In practice, schematics will usually be more complex (as we will see later in the implementation stage).
Before designed modules are synthesized, implemented and programmed into an FPGA, behavioural simulation lets us inspect a circuit design by building a testbench which accepts test input signals and allows us to compare expected outputs against simulated outputs through the use of timing diagrams.
Behavioural simulation is somewhat analogous to writing unit tests for a modular software package.
Example: Simulation test bench and corresponding timing diagram for the above register file module.
`timescale 1ns / 1ps
module TestRegisterFile #(parameter ADDRESS_WIDTH = 3, WORD_WIDTH = 8) ();
reg clk = 1'b0;
reg write_enable;
reg [ADDRESS_WIDTH-1:0] read_addr;
wire [WORD_WIDTH-1:0] read_data;
reg [ADDRESS_WIDTH-1:0] write_addr;
reg [WORD_WIDTH-1:0] write_data;
RegisterFile CUT (
.clk(clk),
.write_enable(write_enable),
.read_addr(read_addr),
.read_data(read_data),
.write_addr(write_addr),
.write_data(write_data)
);
always
// simulate a clock with a 10ns period (i.e. 100MHz)
#5 clk = ~clk;
initial begin
// initial wait
#10;
// read data from address 0 - should be zero initialized
read_addr = 3'b000; #10;
// set data to be written to address 0 (on next clock tick after high enable)
write_addr = 3'b000; write_data = 8'b0000_1111; #12.5;
// enable write to address 0 then disable
write_enable = 1'b1; #10;
write_enable = 1'b0; #10;
// set data to be written to address 1 (on next clock tick after high enable)
write_addr = 3'b001; write_data = 8'b1111_0000; #12.5;
// read data from address 1
read_addr = 3'b001; #10;
// enable write to address 1 then disable
write_enable = 1'b1; #10;
write_enable = 1'b0; #10;
// set data to copy from address 1 into address 2
write_addr = 3'b010; write_data = read_data; #12.5;
// enable write to copy from address 1 into address 2
write_enable = 1'b1; #10;
write_enable = 1'b0; #10;
// read from address 2
read_addr = 3'b010; #10;
$stop;
end
endmodule
Once tested through simulation, a circuit design can then be synthesized by mapping high level HDL code into the available hardware resources of the FPGA, called primitives. In other words, synthesis converts (or elaborates) a circuit schematic into an FPGA netlist.
With the produced netlist from synthesis, implementation is the process of translating the described primitives into the specific programmable logic blocks (placement) and fabric (routing) physically available on the FPGA.
Specifying constraints for a design is one of the key parts of implementation. This involves mapping the external pins on the FPGA to the ports specified in HDL modules. They also tell the tool which IO standard to use, as well as electrical circuit details such as voltages, resistor types and slew rate.
Constraints in Vivado are written in XDC files.
Example: Constraint file showing a subset of pin connections, voltages and clock settings corresponding to the register file HDL module ports.
# Clock constraints
# - Connect to pin W5 on the board
# - Specify 3.3V voltage
# - 10ns period (i.e 100MHz clock)
set_property PACKAGE_PIN W5 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
create_clock -period 10.000 -name clk -waveform {0.000 5.000} -add [get_ports clk]
# Write port enable constraints
# - Connect to pin V17 on the board (a slide switch)
# - Specify 3.3V voltage
set_property PACKAGE_PIN V17 [get_ports write_enable]
set_property IOSTANDARD LVCMOS33 [get_ports write_enable]
# Read port input address constraints
# - Connect to pins W14, V15 and W15 (three slide switches)
# - Specify 3.3V voltage
set_property PACKAGE_PIN W14 [get_ports {read_addr[2]}]
set_property PACKAGE_PIN V15 [get_ports {read_addr[1]}]
set_property PACKAGE_PIN W15 [get_ports {read_addr[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {read_addr[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {read_addr[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {read_addr[0]}]
Once the hardware implementation is generated, we can inspect the actual logic that will be implemented on the FPGA board.
The final step of development is to generate a bitstream, which is a complete description of the logic and routing necessary to implement the design on hardware. Bitstreams are typically vendor-specific, meaning that each tool for FPGA development (e.g. Vivado) has a unique format and proprietary set of instructions for producing and representing bitstreams for hardware programming on supported FPGAs.
With a produced bitstream, the development board can be connected and the FPGA configured by pushing the bitstream to the device.
Below are a selection of introductory circuits that are useful for learning basic FPGA development.
Each circuit has its own project folder and set of subdirectories for project files:
modules/
: Verilog design for the main module and any related modules used within the circuit.simulations/
: Simulation testbenches for behavioural testing of the module.constraints/
: XDC/TCL constraint files for placement and routing.
Each circuit has more information on its own linked README.md
, giving a high level description of the module as well as its expected inputs and outputs.
Combinational logic circuits are time-independent, meaning that the output is purely a function of the current inputs. These circuits are also described as memoryless or stateless, as they do not need to maintain any knowledge of past outputs.
Combinational logic circuits are typically much simpler to program as timing and state is less of a concern during development and testing.
- LED - Switch-powered
- LED - Switch-powered with AND gate
- Seven-segment display discoder
- One-digit seven-segment display
- Arithmetic logic unit with LED display
Sequential logic circuits are circuits whose output is a function of their inputs, as well as past outputs. Due to the dependence on past outputs, sequential logic circuits require memory which is typically implemented in the form of flip-flips for synchronous circuits (synchronized with a clock signal).
Sequential logic circuits are usually synchronous for predictability and ease of design and testing, though may be asynchronous in certain cases.
- Four-digit seven-segment display
- Full seven-segment display
- Arithmetic logic unit with seven-segment display
- Shift register - Switch-powered LED display
- Shift register - Button-powered LED display
- Counter - Button-powered with variable increment/decrement and seven-segment display
- Divide-by-3 counter - Button-powered with seven-segment display
- Register file / SRAM
© 2024-2026, Edwin Onuonga - Released under the MIT license.
Authored and maintained by Edwin Onuonga.