1 Introduction
Lots of FPGAs have been offered for roadtesting, but I never applied to any because I felt intimidated by the complexity of the FPGAs and was not sure I would be able to properly roadtest them in a 2 months period. Luckily for me the E14 team gave me the opportunity to play with a Digilent Cmod S7 and blog about my experience.
Even though many FPGA boards have been given to members of the E14 community, I have seen very little content on how to program these boards through hardware description languages (HDLs). As I have no practical experience with FPGAs, I'll start my journey into learning how to program these devices from scratch. Hopefully other members of the community will find this blog interesting and can learn a few things on how to program these devices using a HDL.
2 The plan
My plan is to learn as much as possible on how to program these board through multiple small projects. There are different ways to program FPGAs such as VHDL, Verilog, visual programming, high level synthesis (HLS) and softcore programming among others, but here I will mostly focus on the Verilog HDL. There are many good introductory FPGA articles on the net, so I'll just leave the links at the bottom, and completely skip the basic theory on how FPGAs operate. I also won't post hundreds of screenshots of how to install Vivado or create a project, it isn't hard and there are already many posts on E14 that show how to do it. Let's roll!
3 The board
The Cmod S7 is a Spartan 7th generation FPGA, which is the latest Xilinx FPGA generation, and only one that works with Vivado, their latest IDE. There are different families of FPGAs in the 7th generation, Spartan FPGAs are the entry level ones. The board is very small and lean, besides DIP pins and a Pmod interface, it is connected to 4 green LEDs, 1 RGB LED, 2 buttons, SPI Flash memory, an FTDI FT2232H USB-UART bridge, a 12 Mhz crystal oscillator and an LTC3569 triple output buck regulator (more details can be found in Digilent's page).
4 The Projects
4.1 Hello FPGA
Powering up an LED is probably the simplest thing that can be done with an FPGA. To power up an LED I first need to select in Vivado the target FPGA chip model from a list and then create two files, a "constraints" file that describes how the pins of the board are connected to the FPGA , and the Verilog file. Before editing the constraints file I need to know how the FPGA pins are wired to the LEDs and buttons, so I download the board schematic from Digilent's board page:
(Digilent reference manual)
The wiring is defined through Tcl commands in an "XDC" constraints file. For my "Hello FPGA" I just need to write a single line:
set_property -dict {PACKAGE_PIN E2 IOSTANDARD LVCMOS33} [get_ports {led}];
This tells Vivado that the pin "E2" uses 3.3V and that it will be referred as "led" in the Verilog file. The Verilog file is also very compact:
module Hello(led); output wire led; assign led = 1; endmodule
Here I defined a module called "Hello" (be aware that his is not the equivalent of function of a typical programming language) which outputs to the "led" wire (already defined in the constraints file as being pin E2). Within the module I set the wire to 1 (3.3 V), and the result of the design is:
We can see 4 glowing LEDs in the image, the red (power) and orange (FPGA programming done) are not user controlled, but LED0 (RGB) and LED1 are. You may be wondering why LED0 is on when I just set LED1 (led) to 1 in the Verilog code; a quick look into how LEDs are wired to the FPGA pins will probably help you figuring that out. Unassigned pins are pulled-down (the default behaviour for all pins can be changed in Vivado), and the RGB LED is turned on by setting each of the component pins to 0. To turn the RGB LED off I need to slightly modify the design:
set_property -dict {PACKAGE_PIN F1 IOSTANDARD LVCMOS33} [get_ports {rgb[0]}]; set_property -dict {PACKAGE_PIN D3 IOSTANDARD LVCMOS33} [get_ports {rgb[1]}]; set_property -dict {PACKAGE_PIN F2 IOSTANDARD LVCMOS33} [get_ports {rgb[2]}]; set_property -dict {PACKAGE_PIN E2 IOSTANDARD LVCMOS33} [get_ports {led}];
module Hello(rgb, led); output wire [2 : 0] rgb; output wire led; assign rgb = 3'b111; assign led = 1; endmodule
I added a 3 bit vector wire for the RGB LED. By setting rgb to 3'b111 (3 bits binary 111) I effectively turned off all the RGB components of the LED:
Vivado allows us to see the output schematic of our design, which in this case isn't particularly interesting:
Enough of this boring design, let's move to something more interesting and interactive.
4.2 Button powered LEDs
A very simple modification to previous code allows us to control the LEDs through button presses:
set_property -dict {PACKAGE_PIN D2 IOSTANDARD LVCMOS33} [get_ports {btn[0]}]; set_property -dict {PACKAGE_PIN D1 IOSTANDARD LVCMOS33} [get_ports {btn[1]}]; set_property -dict {PACKAGE_PIN F1 IOSTANDARD LVCMOS33} [get_ports {rgb[0]}]; set_property -dict {PACKAGE_PIN D3 IOSTANDARD LVCMOS33} [get_ports {rgb[1]}]; set_property -dict {PACKAGE_PIN F2 IOSTANDARD LVCMOS33} [get_ports {rgb[2]}]; set_property -dict {PACKAGE_PIN E2 IOSTANDARD LVCMOS33} [get_ports {led[0]}]; set_property -dict {PACKAGE_PIN K1 IOSTANDARD LVCMOS33} [get_ports {led[1]}]; set_property -dict {PACKAGE_PIN J1 IOSTANDARD LVCMOS33} [get_ports {led[2]}]; set_property -dict {PACKAGE_PIN E1 IOSTANDARD LVCMOS33} [get_ports {led[3]}];
module LedButton(btn, rgb, led); input wire [1 : 0] btn; output wire [2 : 0] rgb; output wire [3 : 0] led; assign rgb[2] = btn[0] ? 0 : 1; assign rgb[1] = btn[1] ? 0 : 1; assign rgb[0] = 1; assign led[1 : 0] = btn[0] ? 2'b11 : 2'b00; assign led[3 : 2] = btn[1] ? 2'b11 : 2'b00; endmodule
The Verilog code uses the "?:" ternary operator to set the state of the LED pins. When BTN0 is pressed, the red component of the RGB LED (rgb[2]), LED1 (led[0]) and LED2 (led[1]) are powered. When BTN1 is pressed, the green component of the RGB LED (rgb[1]) , LED3 (led[2]) and LED4 (led[3]) are powered. Pressing both buttons at the same time powers red and green components and all 4 green LEDs.
Vivado generates the following schematic:
When not pressed, button pins are set to 0, when pressed, to 1. Green LED pins take the same value than the button pins and we can see a direct connection between them. The RGB LED pins work differently, they require a NOT boolean operator, in other words, when a button pin is set 1, an RGB pin is set to 0, and vice versa.The schematic shows 2 LUTs (LookUp Tables) between input and output buffers, which in this design are configured to operate as NOT boolean operators. LUTs of course can be configured to perform any other kind of boolean operations, and they are the way FPGAs usually handle combinational logic.
Let's see the design in action:
These simple examples don't use the clock signal, but as we will soon see, more interesting designs require the use of the clock signal.
4.3 Counting/Blinking LEDs
Now that you know the basics of how to write your own constraints file based on the schematics of the board, I'll tell you the truth: Digilent already provides a constraints file for each of its boards where you just have to uncomment the pins that you require for your design. Why didn't I introduce Digilents constraint file right from the start? Well, if I did you probably wouldn't know how to write your own constraints files when there are none available, for instance if you decide to use a custom-made FPGA board or if you decide to design your own board.
In previous projects the output of the FPGA depended solely on its current input signals, this is called combinational logic, and the output signal is the result of of boolean operations applied to the current input signal. Combinational logic doesn't allow the use of memory and makes it impossible to produce an output that depends on past signals. To use memory and be able to produce an output that depends on current and/or past signals we need sequential logic, and this requires a clock signal.
The Cmod S7 board comes with a 12 MHz clock, but we are not really stuck to that clock frequency, it is possible to use the mixed-mode clock manager (MMCM) of the FPGA to divide or multiply the clock frequency as needed. To show how the clock can be used I wrote a simple sequential logic program that counts from 0 to 15 using the 4 green LEDs. Again, this would not have been possible with combinational logic only, as I would not be able to save and update states (ie. increment the LED counter).
Since the clock runs at 12 MHz I can’t just update the LED binary counter once per cycle. To slow down the LED counter, I introduced a delay counter, which increments the LED counter once every 1,200,000 delay counts. This result in the LED counter to update once every 0.1 s. This design includes many new concepts, let’s see:
`define DELAY 1_200_000 module Blink(clk, rgb, led); input wire clk; output reg [2 : 0] rgb = 3'b111; output reg [3 : 0] led = 4'b0000; reg [$clog2(`DELAY) - 1:0] counter = 0; always @(posedge clk) begin if (counter == `DELAY - 1) begin led <= led + 1; counter <= 0; end else counter <= counter + 1; end endmodule
`define is analog to the C/C++ #define, it’s a macro, and `DELAY gets replaced by 1_200_000. The underscores in-between are separators and only have meaning to the designer, the synthesizer completely ignores them.
reg variables can store values while wire variables can’t. reg variables are usually implemented as flip-flops, where for each bit of the reg one flip-flop is required. In some situations, the synthesizer may treat a reg as a wire, as it occurs in this case with the rgb reg (see the next schematic figure); the synthesizer figured out that value of rgb doesn’t change and no flip-flop is required, so it just wires each bit to whatever values I initialized the reg to.
$clog2() returns the ceiling of log base 2, so I used it to calculate the number of bits that I need for the delay counter (21 bits).
The always @(posedge clk) tells the synthesizer to execute the operations within the block once every rising edge of the clk wire. There two type of assignments that can be used inside of an always block, the blocking (=) and the non-blocking (<=) assignments. Blocking assignments are guaranteed execute in the same order than the source code lines, while non-blocking assignments occur in parallel.
if is similar to the C/C++ if, and begin and end are similar to { and } of C/C++.
Here is the design in action:
And this is its schematic:
Very intimidating, isn't it? I’ll give you an intuition on how it works. We got 3 new blocks here, the BUFG, the FDRE and the CARRY4. The BUFG block is a high-fanout buffer that connects the signal to global routing resources. This is especially useful for clock signals as they must be fed into many other blocks with minimum delay between them. The FDRE block is a D flip-flop with clock enable and synchronous reset, its output is shown in the following table:
The flip-flop outputs its current state all the time to the Q pin. When reset (R) is set to 1, the state of the flip-flop is set to 0 at the rising edge of the clock (C). If clock enable (CE) is set to 1, the flip-flop state is set at the rising edge of the clock (C) to the data signal (D). The CARRY4 is a Fast carry logic with look ahead, and here is used in conjunction with FDRE blocks to implement a 21 bit delay counter.
Let’s take a closer look to how the design works.The clock signal feeds the clock input (C) of all the FDRE blocks of the schematic directly. The delay counter is made of 21 FDRE, 1 LUT1 and 5 CARRY4 blocks. The single LUT1 and CARRY4 blocks are responsible of incrementing the values of the counter. 4 LUT4 and 2 LUT5 are used to sample all the bits of the counter and compare them to 1,199,999. When the delay counter reaches this number, the counter[20]_i_1 LUT5 outputs a 1 and this has 2 effects. First it sends a reset signal (R) to the delay counter FDRE blocks, which causes them to reset to 0 at the next clock (C) rising edge; and second, it sets the clock enable (CE) of the 4 LED counter FDRE blocks to 1, effectively increasing in this way the LED counter to 1 during the clock rising edge (C). Notice that the LED counter is made of 4 FDRE, 1 LUT1, 1 LUT2, 1 LUT3 and 1 LUT4. These LUTs are configured to increment the counter, during the clock (C) rising edge only when clock enable (CE) has been set to 1 (once every 1,200,000 clock ticks).
If such a simple piece of Verilog code is synthesized into such large schematic, just imagine what kind of schematic the synthesizer would generate from higher complexity designs. Most of the times we don’t really need to know what kind of schematic is synthesized, but its good to have an intuition of how HDL code is converted into a circuit. Learning how each of these FPGA primitives operate is also useful for when they need to be use directly from an HDL. These primitives are presented as modules in Verilog and can be used like any other Verilog module. The drawback of using these primitives is that they are dependent of each specific FPGA and using them could make the code non-portable.
Let’s suppose that our Verilog design doesn’t behave as we want it to behave, what can we do? Manually checking the schematic is very time consuming and in many cases not a practical solution, because of the high number of primitives and states. An alternative is to simulate the design, simulations allow us to see how signals change with time, which can be very helpful in figuring out what went wrong in a design.
Let’s see how Vivado’s built-in simulator simulates the module if I set the DELAY macro to 3 (otherwise I would not be able to show in a single image the coordination of all the signals)!
As expected, the clk clock signal is the fastest changing signal. At every clk rising edge counter increases 1 until it reaches 2, at which point it resets to 0 and led increases 1. The moment led reaches 15 it wraps around and the whole process repeats.
And just for fun, lets see the schematic of the design:
Even though the design looks much simpler than the previous one, it works in a very similar way except for 2 relevant differences. First, the circuit doesn’t use CARRY4 but LUT2 instead in the delay counter, and second, it doesn’t reset the delay counter through the FDRE reset (R); the counter is reset through the LUT2 (who also are in charge of increment the counter).
4.4 Debouncing
When a switch is toggled, contacts have to physically move to settle to their new position. This does not occur immediately, and before contacts settle to their final position, they bounce, causing the circuit to open and close several times. If you only care about the current status of a button, like in project 4.2, you can just ignore the bouncing effect, as it occurs too fast to be visible to the eye. But if you care about button pressing or releasing events, then you may require debouncing, or otherwise you could end up detecting multiple "fake events".
There are many ways to do debouncing, I decided to solve the bouncing problem like this: If the button changes its state, I generate a button "state change" event and reset a timer, and unless a defined amount of time has passed since last reset, any new event gets ignored as its likely caused by bouncing. Here is the code:
`define FREQUENCY 12_000_000 module Top(clk, btn, rgb, led); input wire clk; input wire [1 : 0] btn; output reg [2 : 0] rgb = 3'b111; output wire [3 : 0] led; wire [1 : 0] event0; wire [1 : 0] event1; Debouncer #(0.1) Debouncer0 (clk, btn[0], event0); Debouncer #(0.1) Debouncer1 (clk, btn[1], event1); LedState ledState0(clk, event0, led[0]); LedState ledState1(clk, event1, led[2]); assign led[1] = led[0]; assign led[3] = led[2]; endmodule module Debouncer(clk, in, out); parameter waitTime = 0.05; input wire clk; input wire in; output reg [1 : 0] out; localparam waitTicks = $rtoi(waitTime * `FREQUENCY); reg lastState = 0; reg [$clog2(waitTicks) - 1 : 0] counter = 0; always @(posedge clk) begin if (lastState == in) begin counter <= (counter == waitTicks) ? waitTicks : counter + 1; out <= 0; end else begin counter <= 0; if (counter == waitTicks) begin lastState <= in; out <= (in == 1) ? 1 : 2; end else out <= 0; end end endmodule module LedState(clk, in, out); input wire clk; input wire [1 : 0] in; output reg out = 0; always @(posedge clk) if (in == 1) out = ~out; endmodule
In contrast to previous projects, we now defined 3 modules. Modules are in some way analog to classes of object oriented programming languages, and except for the top module, they have to be instantiated to be used. Instantiation syntax is again similar to the C++ syntax, the "Top" module instantiates 2 Debouncer and 2 LedState modules. To connect them I created 2 2-bit wires (event0 and event1) which are used to communicate the switch events from each Debouncer instance to their respective LedState instance.
Each of the 2 Debouncer instances debounces a single button as defined in the instantiation. The debouncer outputs a 0 when no event occurs, a 1 when the button is pressed and a 2 when the button is released. Parameters are constants typically used to specify the width of variables and time delays. Parameters are evaluated during synthesis, and can't be wire or reg data types. I defined waitTime as a parameter that stores the amount of time in seconds that have to pass before a new button event can be generated. I made the default value of the parameter 0.05, but this value is overridden to 0.1 during the instantiation of the module. Its important to note that Verilog synthesizable code does not natively support real numbers. To overcome that limitation I defined the localparam waitTicks and set its value to the waitTime converted from seconds to clock ticks. This was done by multiplying waitTime by `FREQUENCY, and then using the $rtoi() function to convert the result from real to integer. localparam is similar to parameter, its only difference is that its value can't be overridden at the instantiation.
The LedState instances receive events from Debouncer instances and set the state of the LEDs depending on the received button press events.
Here is a video of the the design:
And the schematic looks like this:
The schematic is very lean, because its encapsulating the content of the instances. Vivado allows you see the contents of the instance by either expanding it by pressing the "+" button at the top left corner of the block, or opening its content by double clicking it. As you can imagine, the Debouncer instances contain many cells and nets, so I'm not going to show them. One interesting aspect of the synthesized schematic is that the Debouncer instances connect directly to the LED wires and also appear to have one extra pin, caused by synthesizer optimizations. Vivado can also display the pre-optimized design using generic symbols that are independent of the target FPGA device:
Now this schematic is almost a perfect graphical representation of the Top module, Debouncer has only 3 ports now and the port names are identical to the names in the source code.
Let's take a real challenge!
4.5 Vector display graphics
In this project I perform computer graphics with an FPGA: Vector Display GPU Project
4.6 Xmas light show
And here I broadcast stereo FM music through a pin of the board: Xmas Audiovisual Show
5 Final words
In less than a month without having any previous experience with FPGAs I got from roughly knowing basic theory of how they work, to getting a decent mental model of how to implement different algorithms in the FPGA. In this blog I skipped many key aspects of FPGAs, such as test benches, metastability, setup and hold time, blocking and non-blocking assignments, among others, because they would have taken me a great amount of time to explain well, and also because they aren't that interesting to FPGA beginners. Many of the presented projects were explained in very general terms, but I still expect the explanations to be of enough depth to give the beginner a good intuition of how to program FPGAs. If I managed to motivate any reader to get into FPGA programming, I'll consider this post a success!
6 References
Get Started with Programmable Logic
Programmable Devices: Programmable SoCs
FPGA and Programmable SoC Programming Languages
Digilent Cmod S7 Reference manual
Verilog tutorial (verilog.com)
Verilog, Formal Verification and Verilator Beginner's Tutorial (Gisselquist Technology)
Top Comments