4. Field Programmable Gate Arrays
It’s time to get our hands on some actual hardware 📟 and put our new logic-design skills to the test. Most hardware components we use day-to-day have a fixed design. This means the gates and their wiring are hardwired. In other words, the design and purpose of such a device remain the same throughout its operating life. For example, suppose you buy a processor. This could be the Broadcom BCM2712 used in the Raspberry Pi 5 which we’ll use as our development platform when we discuss the Arm architecture. We can use this processor by running our software on it. Software consists of instructions that the processor executes. However, we cannot change the design of the processor itself.
In contrast, field-programmable gate arrays (FPGAs) allow us to (re)configure or “program” the logic fabric itself. This means that we can decide which logic gates to use and how they should be connected. The same manufactured FPGA could, for example, implement a tailored microprocessor one day and act as a graphics processing unit the next. In fact, FPGAs are often used as a primary platform for prototyping processor designs. For example, the RISC-V Exchange lists entire CPU-core IP blocks, including open-source options.
In this section we’ll use the AMD Urbana board, which is built around AMDs Spartan-7 FPGA. Additional information is available through the Urbana Board Reference Manual. We will use the open source toolchain openXC7 to configure the board.
Hint
Compile all your modules with Icarus Verilog using flags -Wall -Winfloop -g2012 before programming an FPGA.
Additionally, write testbenches and run simulations before using openXC7 to program the board.
4.1. RTL Modeling
In Section 3 we specified our hardware modules with elementary logic gates. Given an abstract description of a function, we had to manually lower it to logic gates. This process is cumbersome, error-prone and time-consuming, especially when aiming for an optimized circuit that may have multiple viable realizations. From now on, we’ll harness more powerful SystemVerilog constructs under the umbrella of register-transfer-level (RTL) modeling. This lets us write more abstract code and use a synthesis tool to generate the gate-level netlist. Even though RTL modeling greatly simplifies our work, we should keep in mind the gate-level results that synthesis will produce.
We discuss RTL modeling in class. All details can be found in the IEEE Standard for SystemVerilog. Further, Ch. 4 of the SystemVerilog Guide from Harvard’s CS141 course provides a gentler introduction.
4.2. 7-Segment Display Decoder
Fig. 4.2.1 Picture of the finished 7-segment display decoder where only switch SW1 is active.
The decoder connects the switches SW3, SW2, SW1 and SW0 with the display 0.
Thus, 0010 is the visualized binary number which corresponds to the shown hex number 2.
Let’s start our FPGA journey by designing a decoder that connects some of the board’s switches to a hexadecimal display.
For this we’ll use the first four switches SW3, SW2, SW1, and SW0.
Each switch can either be 0 (down) or 1 (up).
We may represent a hexadecimal number alternatively through exactly four binary digits.
Thus, in our design, the configuration of the four switches will encode a single hexadecimal number.
Each hexadecimal display contains seven segments.
While the Urbana board has eight of these displays, we’ll only use the right-most one in this task.
The segments of the display are active low.
Drive a segment high (1) to turn it off, low (0) to turn it on.
Fig. 4.2.2 Illustration of the hexadecimal digits 0-F when shown on a seven-segment display of the Urbana board. The numbers show segment indices.
Fig. 4.2.2 illustrates all sixteen outputs our decoder might have.
For example, for the hex digit 0 all but segment 6 have to be active.
This means that we have to set all but the seventh bit of D1_SEG to 0, i.e. set D1_SEG[6:0] to 7'b100_0000.
If we’d like to encode the hex digit 1, we’d set all bits to 1 except the second and third bits, i.e. set D1_SEG[6:0] to 7'b111_1001.
The respective (incomplete) truth table for our decoder is given in Table 4.2.1.
Hex Number |
SW[3:0] |
D1_SEG[6:0] |
|---|---|---|
0 |
|
|
1 |
|
|
2 |
|
|
3 |
|
|
4 |
|
|
5 |
|
|
6 |
|
|
7 |
|
|
8 |
|
|
9 |
|
|
A |
|
|
B |
|
|
C |
|
|
D |
|
|
E |
|
|
F |
|
|
Once again, to kickstart your work, templates for your modules are provided.
The template for the decoder module is located in Listing 4.2.1.
This template only provides the decoding cases for the two binary numbers \(0000_2\) and \(0001_2\).
The remaining 14 numbers shown in Table 4.2.1 have to be added still.
1/**
2 * Decoder for a seven-segment display.
3 *
4 * @param i_binary_number input binary number.
5 * @param o_display output bits driving the seven-segment display.
6 **/
7module decoder( input logic [3:0] i_binary_number,
8 output logic [6:0] o_display );
9 always_comb
10 begin
11 case( i_binary_number )
12 4'b0000: o_display[6:0] = 7'b100_0000; // 0
13 4'b0001: o_display[6:0] = 7'b111_1001; // 1
14 // TODO: add remaining hex digits 2-F
15 default: o_display[6:0] = 7'bxxx_xxxx;
16 endcase
17 end
18endmodule
Listing 4.2.2 provides a template for the top-level module.
It simply wraps the decoder and connects the four slide SW3-SW0 to the decoder input and the decoder output to the 7-segment display.
In addition the control signals of the 7-segment display must be set correctly for the AMD Urbana board.
The segment outputs D1_SEG are active low, meaning that a segment is illuminated by driving the corresponding signal low.
The digit selecting signals D0_AN[3:0] and D1_AN[3:0] are also active low.
1/**
2 * Top level module for the urbana board
3 * The decoder shows the binary number represented
4 * by SW3-SW0 on display 0 as a hex number.
5 *
6 * @param SW switch signals
7 * @param D1_SEG output bits which drive the seven-segment display 7
8 * @param D0_AN anode signal 0 for seven-segment displays 0-3
9 * @param D1_AN anode signal 1 for seven-segment displays 4-7
10 **/
11module top( input logic [3:0] SW,
12 output logic [7:0] D1_SEG,
13 output logic [3:0] D0_AN,
14 output logic [3:0] D1_AN );
15
16 // TODO: use your decoder to connect SW to HEX0
17endmodule
An XDC (Xilinx Design Constraints) file is a text file containing Tcl-based commands that specify physical and timing constraints.
It maps the ports of our top-level SystemVerilog module to physical FPGA pins using PACKAGE_PIN and selects their electrical I/O standards using IOSTANDARD. Port names in get_ports must match those in the top-level module.
In Listing 4.2.3 is an initial template for the required structure.
As you can see, for example, in line 2, the variable from the top-level module SW[0] is mapped to digital port G1 of the Urbana FPGA.
Additional signal names are available from the Urbana Board Reference Manual.
1# Slide switches SW3..SW0
2set_property -dict {PACKAGE_PIN G1 IOSTANDARD LVCMOS25} [get_ports {SW[0]}]
3# TODO: add more switches
4
5# 7-segment display 0-3
6set_property -dict {PACKAGE_PIN F3 IOSTANDARD LVCMOS25} [get_ports {D1_SEG[0]}]
7# TODO: add remaining ports
8
9# 7-segment anode 0/1
10set_property -dict {PACKAGE_PIN G6 IOSTANDARD LVCMOS25} [get_ports {D0_AN[0]}]
11# TODO: add D0_AN 1-3
12
13set_property -dict {PACKAGE_PIN E4 IOSTANDARD LVCMOS25} [get_ports {D1_AN[0]}]
14# TODO: add D1_AN 1-3
Tasks
Add the missing encodings for the seven-segment display to Table 4.2.1.
Implement the module
decoder. Use the template in Listing 4.2.1.Finish the implementation of the module
decoder_topand thedecoder.xdcscript. Use Listing 4.2.2 and Listing 4.2.3 as a template.Compile your modules using the openXC7
Makefileand program the FPGA of a Urbana board.Make sure that the board shows the correct hexadecimal numbers in display 0 for all combinations of the switches
SW0,SW1,SW2,SW3. Provide three pictures of the programmed FPGA for the hex numbers7,BandE. One should see the hex display and the switches in your pictures. An example is given in Fig. 4.2.1.
4.3. Tiny Calculator
Now that we have a way to display four-bit binary numbers, let’s take it up a notch.
In this task we’ll implement a tiny calculator that takes two four-bit binary numbers \(A_{[3:0]}\) and \(B_{[3:0]}\) as inputs.
We map the first four switches, SW[3:0], to \(A_{[3:0]}\) and the next four, SW[7:4], to \(B_{[3:0]}\).
Our tiny calculator computes the five-bit result \(C_{[4:0]} = A_{[3:0]} + B_{[3:0]}\).
We display the input \(A_{[3:0]}\) on display 0 and \(B_{[3:0]}\) on display 1.
The result \(C_{[4:0]}\) has five bits due to the carry-out.
Thus, we use two hex digits for \(C_{[4:0]}\).
One for the four bits [3:0] (shown on display 2) and one for the carry-out on display 3.
Fig. 4.3.1 Picture of the tiny calculator in action.
In the configuration shown, only switches SW0, SW1 and SW5 are up (1).
This means \(A_{[3:0]} = 0011_2\), \(B_{[3:0]} = 0010_2\) and \(C_{[4:0]} = 00101_2\).
We see that the corresponding hexadecimal representations appear on displays 0 - 3: \(0011_2 = 3_{16}\) on display 0, \(0010_2 = 2_{16}\) on display 1 and \(00101_2 = 05_{16}\) split across display 2 and 3.
A picture of an example is provided in Fig. 4.3.1.
Here, SW[3:0] = 4'b0011 and SW[7:4] = 4'b0010.
The corresponding hexadecimal numbers \(3_{16}\) and \(2_{16}\) are shown on display 0 and 1.
Additionally, the displays 2 and 3 show the result \(05_{16}\).
SW[3:0] |
SW[7:4] |
DISPLAY 0 |
DISPLAY 1 |
DISPLAY 2 |
DISPLAY 3 |
|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1/**
2 * Top-level entry of the tiny calculator.
3 * The calculator adds the 4-bit binary numbers in SW[3:0] and SW[7:4].
4 * The two inputs are shown on displays 7 and 6.
5 * The result of the addition is shown on displays display 5 and 4.
6 *
7 * @param CLK_100MHZ clock signal.
8 * @param SW bits of switches SW7, SW6, SW5, SW4, SW3, SW2, SW1 and SW0.
9 * @param D1_SEG output bits which drive the seven-segment display 0-3
10 * @param D0_AN anode signal 0 for seven-segment displays 4-7
11 * @param D1_AN anode signal 1 for seven-segment displays 0-3
12 **/
13module tiny_calculator( input logic CLK_100MHZ,
14 input logic [7:0] SW,
15 output logic [7:0] D1_SEG,
16 output logic [3:0] D0_AN,
17 output logic [3:0] D1_AN );
18 logic [7:0] l_sum;
19 logic [1:0] digit;
20 logic [3:0] display;
21 logic [6:0] seg;
22 logic [17:0] refresh = '0;
23
24
25 // TODO: implement the tiny calculator
26
27 // clock signal to circulate the displays
28 always_ff @( posedge CLK_100MHZ )
29 refresh <= refresh + 1'b1;
30
31 // advance every 2^16 clocks = 655 us (fast enough to see a steady state)
32 assign digit = refresh[17:16];
33
34 always_comb
35 begin
36 case( digit )
37 2'd0: display = 4'bxxxx;
38 2'd1: display = 4'bxxxx;
39 2'd2: display = 4'bxxxx;
40 2'd3: display = 4'bxxxx;
41 endcase
42 end
43
44 // TODO: implement the decoder module
45
46 assign D1_SEG = { 1'b1, seg };
47
48 // assign power to each display
49 assign D0_AN = 4'b1111;
50 always_comb
51 begin
52 case( digit )
53 2'd0: D1_AN = 4'b0111;
54 2'd1: D1_AN = 4'b1011;
55 2'd2: D1_AN = 4'b1101;
56 2'd3: D1_AN = 4'b1110;
57 endcase
58 end
59
60endmodule
Tasks
Implement the module
tiny_calculator. Use the template in Listing 4.3.1 as a basis for your module. Use your four-bit ripple carry adder of Section 3.3 for the addition.Add the testbench
tiny_calculator_tbfor yourtiny_calculatormodule. Check that your calculator produces the correct outputs, for all inputs in Table 4.3.1.Compile your module using the openXC7 toolchain and program the FPGA of a Urbana board.
Make sure that the board’s displays show the correct hexadecimal numbers for the configurations in Table 4.3.1. Provide a picture for each of the five configurations.