Introduction to Verilog

Introduction

Field-Programmable Gate Arrays (FPGAs) are fascinating integrated circuits (ICs) that just like central processing units (CPUs) can compute, but unlike them, operate in a completely different way. While a CPU implements an algorithm as a sequence of machine instructions, an FPGA implements it as interconnection and configuration of logic elements. Said differently, FPGA programming is closer to designing circuits than to writing instructions that get executed sequentially (unless the FPGA is programmed to operate as a processor, or it actually implements one).

FPGAs are usually programmed using hardware description languages (HDLs). These languages describe the digital logic so that the synthesis tool can generate the design implementation. There are two major HDLs: VHDL and Verilog. They are more or less equivalent, but some relevant differences are that Verilog is less verbose and uses a C-like syntax.

In this post I will give a practical introduction to how to program the Digilent Cmod S7 in Verilog.

Digilent Cmod S7

The Cmod S7 is an inexpensive board based on Xilinx’s 7th generation FPGAs. In contrast to previous FPGA generations, the 7th generation supports Vivado, Xilinx’s latest design tool.

Cmod S7 board

The board’s FPGA chip is connected to 4 green LEDs, 1 RGB LED, 2 push buttons, SPI Flash memory, an FTDI FT2232H USB-UART bridge, a 12 MHz crystal oscillator and an LTC3569 triple output buck regulator.

The FPGA designs that wrote use buttons and LEDs, so before starting, let’s see how they are wired to the FPGA.

Cmod S7 circuit

The diagram shows that:

  • The push buttons turn 1 (high) when pressed.
  • The color components of the RGB LED are turned on by driving the signals to 0 (low).
  • The 4 green LEDs are turned on by driving the signals to 1 (high).

Hello FPGA

The simplest design that can be programmed on an FPGA is one that drives an FPGA pin to either high or low. So to make things more interesting, I wrote a design that wires the LEDs to the push buttons.

To program in Verilog at least 2 files are required, a constraints (.xdc) file to set up the FPGA pins, and a Verilog (.v) file to specify the logic design.

Vivado’s constraints files contain Tcl commands that define the requirements that must be met for a design to be functional. Companies like Digilent provide constraints templates that need minor editing to use, but to make the post more informative I wrote my own minimal constraints file.

set_property -dict {PACKAGE_PIN D2 IOSTANDARD LVCMOS33} [get_ports {button[0]}];
set_property -dict {PACKAGE_PIN D1 IOSTANDARD LVCMOS33} [get_ports {button[1]}];

set_property -dict {PACKAGE_PIN F2 IOSTANDARD LVCMOS33} [get_ports {rgb[0]}];
set_property -dict {PACKAGE_PIN D3 IOSTANDARD LVCMOS33} [get_ports {rgb[1]}];
set_property -dict {PACKAGE_PIN F1 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]}];

The constraints file associated the specified pins to 3 busses:

  • button: A 2 bit bus wired to each button.
  • rgb: A 3 bit bus wired to each component of the RGB LED.
  • led: A 4 bit bus wired to the 4 green LEDs.
I wrote the following design to perform the logic.
module Top(button, rgb, led);
    input  wire [1 : 0] button;
    output wire [2 : 0] rgb;
    output wire [3 : 0] led;
    
    assign rgb[0] = ~button[0];
    assign rgb[1] = ~button[1];
    assign rgb[2] = 1;
    
    assign led[0] = button[0];
    assign led[1] = button[1];
    assign led[2] = button[0] | button[1];
    assign led[3] = 1;
endmodule

An this short video shows what the design does.

So how does the design work?

Modules are structures that encapsulate the design logic. The top module (Top) is the main module and encapsulates the logic of the complete design. The Top module is wired to the 3 busses, button (an input bus), rgb (an output bus) and led (an output bus). 

The assign statement is used to connect the LEDs in the following way:

  • Red component turns on (low) when pressing button 0. (“~” is the not logical operator)
  • Green component turns on (low) when pressing button 1.
  • Blue component stays off (high).
  • LED 0 turns on (high) when pressing button 0.
  • LED 1 turns on (high) when pressing button 1.
  • LED 2 turns on (high) when pressing either button 0 or button 1 (“|” is the  logical or operator).
  • LED 3 stays on (high).
Once the design is synthesized, the schematic can be inspected. Vivado provides 2 views of the schematic, the Register-transfer level (RTL) view and the technology view. The RTL schematic uses generic symbols for logic or arithmetic operations that are independent of the target FPGA device. The technology schematic on the other side shows the architecture-specific schematic in terms of logic primitives of the target FPGA.
 
Let’s take a look at the RTL and technology schematic generated from the design.
RTL schematic
Technology schematic

RTL schematics are almost as easy to understand as Verilog code. In contrast, the technology schematics tend to be orders of magnitude more complex and difficult to understand (even though this particular schematic is still very simple). 

The technology schematic shows that the FPGA performs logic operations using look-up tables (LUTs). The way they work is by outputting the bit stored at the input index. The LUTs bit content is initialized so that their output is equivalent to the “emulated” logic operations (e.g., the not operator is implemented with the LUT1 primitive by setting the index 0 to 1, and the index 1 to 0).

IBUF and OBUF are input and output buffers and basically set the pins as either input or output.

The process of converting an abstract representation of a design into technology-specific logic primitives is called inference. Through inference code can be made portable across multiple FPGA platforms and can be optimized as needed for performance, area or power. The software analogy to inferring would be compiling.

In contrast, instantiation is the process where the primitives are specified in the code. This gives the highest control of the design, but at the cost of loss of portability and higher complexity. The software analogy to instantiating would be assembling.

There is no need to stick to a single approach, both approaches can be used together.

Let’s see how the previous design can be written by manually setting the LUTs to act as logic operators.

module Top(button, rgb, led);
    input  wire [1 : 0] button;
    output wire [2 : 0] rgb;
    output wire [3 : 0] led;
    
    LUT1 #(.INIT(2'b01)) rgb0 (.O(rgb[0]), .I0(button[0]));
    LUT1 #(.INIT(2'b01)) rgb1 (.O(rgb[1]), .I0(button[1]));
    assign rgb[2] = 1;
    
    assign led[0] = button[0];
    assign led[1] = button[1];
    LUT2 #(.INIT(4'b1110)) led2 (.O(led[2]), .I0(button[0]), .I1(button[1]));
    assign led[3]   = 1;
endmodule

The instanced code is identical to the inferred code, except for the lines 6, 7 and 12, where LUTs are instantiated to perform logical operations. To instantiate a LUT the table must be initialized (.INIT) and the input (I0, I1) and output (.O) must be wired. 

A not requires the LUT to have a 1 at the index 0, and 0 at index 1, this is done initializing LUT1 to the binary “01” (2’b01). Analogously an or operator requires a LUT2 (2 inputs) initialized to the binary “1110” (4’b1110).

The buffering primitives can also be manually instantiated, but to keep things simple I let the synthesis tool infer them.

A complete list of all the available primitives can be found in a 600 pages document called: Vivado Design Suite 7 Series FPGA and Zynq-7000 SoC Libraries Guide (Code ug953).

PWM-regulated LED Brightness

Combinational logic circuits are characterized by output signals that depend only on the actual input signal values. With combinational logic it is not possible to generate an output that depends on past signals or states. Sequential logic solves this, but requires a clock signal.

The Cmod S7 includes a 12 MHz crystal oscillator connected to pin M9. However, it is possible to divide or multiply the clock frequency using the mixed-mode clock manager (MMCM) primitive if a design requires a different frequency.

To show how sequential logic works I wrote a simple design that uses the FPGA clock to output a pulse width modulation (PWM) signals to the board LEDs.

set_property -dict {PACKAGE_PIN M9 IOSTANDARD LVCMOS33} [get_ports {clk}];

set_property -dict {PACKAGE_PIN F2 IOSTANDARD LVCMOS33} [get_ports {rgb[0]}];
set_property -dict {PACKAGE_PIN D3 IOSTANDARD LVCMOS33} [get_ports {rgb[1]}];
set_property -dict {PACKAGE_PIN F1 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 Top(clk, rgb, led);
    input  wire clk;
    output wire [2 : 0] rgb;
    output wire [3 : 0] led;

    assign rgb = 3'b111;

    Pwm pwm0 (clk,  1, led[0]);
    Pwm pwm1 (clk,  4, led[1]);
    Pwm pwm2 (clk, 16, led[2]);
    Pwm pwm3 (clk, 64, led[3]);
endmodule


module Pwm(clk, value, state);
    input  wire clk;
    input  wire [6 : 0] value;
    output reg  state;

    reg [5 : 0] counter = 0;
    
    always @(posedge clk)
    begin
        counter = counter + 1;
        state = (counter < value);
    end
endmodule

The constraints file sets the clock wire (clk), and the LED busses (rgb and led) up. The clock signal can be used like any other signal, but what makes it special is that it switches from 0 to 1 and back at a defined frequency.

The Verilog design defines 2 modules, a top module (Top) and a PWM module (Pwm). The top module turns off the RGB LED, and instantiates the PWM module 4 times. Each PWM instance generates a different output signal to each of the 4 green LEDs.

The PWM module takes 3 parameter, a clock signal input (clk), a duty cycle input (value) and a PWM output (state). The value parameter specifies how many time units state stays high during each PWM cycle (arbitrarily set to 64 time units).

The module uses a 6 bit counter reg (counter) to count the passed time since the beginning of the PWM cycle. In contrast to wire, reg does not need to be continuously driven, and can retain values.

The always block describes things that will repeat indefinitely at every clk rising edge (posedge clk). Clock rising edges increase the counter by one until it wraps around. state on the other side is set to 1 if counter < value, or 0 otherwise.

The design sets the PWM frequency to 187.5 kHz (12 MHz / 64), and the duty cycle of LED 0 to 1/64, LED 1 to 4/64, LED 2 to 16/64 and LED 3  to 64/64.

PWM design

As designs get more complex, debugging becomes more important. There many alternatives to debug FPGAs, but one of the simplest is to write a testbench to simulate the circuit behavior.

Testbenches are programs that are used to test designs and verify their correctness. As they run in a simulator (not in the FPGA), they can use non-synthesizable features like for-loops, printing to the screen and reading and saving files.

To verify the correctness of the complete design I wrote a minimal testbench that instantiates Top.

module TopTb();
    reg  clk = 0;
    wire [2 : 0] rgb;
    wire [3 : 0] led; 

    Top top (clk, rgb, led); 

    always 
        #1 clk = ~clk; 
endmodule

The testbench controls the clock signal (clk) and switches its state every single time unit. This is done with #1, which tells the simulator to wait 1 time unit before continuing (delays are not synthesizable an can only be used in testbenches).

The testbench generated the following signals.

PWM design simulation

The generated traces show that the PWM period is 64 time units, and that the PWM signal goes high when counter = 0, and goes low when counter = value.

These results validate that the design is correct.

Conclusion

Learning to program FPGAs can be very time consuming. First, the tools are complex and non-standard. Second, the programming paradigm is different, it is more about pipelining operations than sequencing instructions as in traditional programming. Third, debugging is also completely different than in traditional programming, as it is not possible to directly detect what part of the code originates the bug. And fourth, the code can be hard to maintain and make it portable across platforms.

As a beginner one the most difficult parts is to learn how to translate an algorithm into an FPGA design. In this post I covered the very basics of FPGA HDL programming, mostly to give the intuition on how FPGAs operate and how they are programmed. Complex HDL designs still rely on the same building blocks (i.e. combinational and sequential logic), so even though the shown designs are simple, they illustrate well how FPGAs are programmed.

The code is available on GitHub.