Skip to content

Instantly share code, notes, and snippets.

@alexispurslane
Created January 14, 2022 01:52
Show Gist options
  • Save alexispurslane/fca115a85f5ba9257fb5a4b56440b3b3 to your computer and use it in GitHub Desktop.
Save alexispurslane/fca115a85f5ba9257fb5a4b56440b3b3 to your computer and use it in GitHub Desktop.
title tags
Verilog and Vivado Basics
cmpen331

1. Types of Verilog

There are two basic ways that you can write a Verilog program:

  1. Structural Verilog: this is where you specify the logic gates and connections between them of your circuit directly, as if you were doing a textual version of drawing logic gate circuits. This is very low level and nitty-gritty. Here's an example of what that looks like:

    module MUX
        (
            input a,
            input b,
            input sel,
            output f
        );
        wire f1, f2, nsel;
        assign nsel = ~sel;
        assign f1 = a & nsel;
        assign f2 = b & sel;
        assign f = f1 | f2;
    endmodule
  2. Behavioral Verilog: this is where you specify the behavior you want from your circuit, a lot like regular programming (except more continuous and less procedural) and then Verilog itself figures out the actual logic gates and stuff you need. This is higher level, faster, and more productive usually. It generally looks like this:

    module MUX
        (
            input a,
        	input b,
        	input sel,
        	output f
        );
        always @(sel or a or b)
        begin
            if (sel == 1) begin
                f = b;
            end
            else begin
                f = a;
            end
        end
    endmodule
  3. Simulation-Only Verilog: there are also Verilog commands that aren't synthesizable, meaning you can't hand convert them into a circuit and print them out, which are useful for running simulations of synthesizable Verilog circuits. These simulation-only Verilog commands are how you write tests for your modules, which I'll show in the third section.

It is recommended that you use behavioral Verilog (with always blocks) for selectors, deselectors, multiplexors, and other similar circuits.

Conversely, for simpler combinational logic that you'd do with logic gates instead of higher-level ICs, you should use structural Verilog.

For registers and associated input logic, use behavioral Verilog with always blocks sensitive to a synchronous clock.

2. Basics of Structural Verilog

The core concept that you have to grasp about writing structural Verilog is that it is a specification of a circuit, just another way of drawing a circuit, not a program that directly runs or makes things happen. That means that you are setting up integrated circuits (logic gates, and the modules you define in terms of logic gates, as we will see later) and connecting them with wires, like on a bread board, not giving the computer step by step instructions. Therefore inputs and outputs are continuous: inputs are continually fed into the connections and wires you define, like streams of data, and then your program calculates out how it should change as that stream of inputs changes. Its a state of constant flow through an circuit you arranged, not something that executes in order step-by-step.

2.1 Wires and Assigning

To define a wire in Verilog, which is a constant stream of data from one end (where it's assigned) to the other (wherever it is used, it can "split up" just by being used in multiple places), you use this command:

wire w1; // you can use whatever name you want instead of 'w1'
wire w2, xyzzy, fuck; // you can also define multiple wires in the same statement

To attach something to one end of the wire, that changes the bit that's being carried by the wire, you can use the assign command, like so:

assign w1 = <some command here>;

You can use basic logic gates and operators in assign commands, that's how you wire up basic circuits without having to define all your logic gates from scratch. For example:

assign w1 = w2 | xyzzy | fuck;

If you want to use more than one logic gate (operator) in an assign command, you have to define a wire and set them up separately:

wire x;
assign x = w2 & xyzzy;
assign w1 = x | fuck;

2.2 Modules

You can create new circuits to use as if they were integrated circuits, without having to repeat their implementation all over the place, by creating modules. The syntax for this is a lot like it is for procedures in other languages:

module my_module
    (
        input a,
        output b,
        inout c
    );
    // do stuff with the inputs, outputs and input-outputs here, with wires, logic gates, other modules, as you like
endmodule

Note: modules can be used in Behavioral Verilog too!

There are a couple different ways of doing the syntax for modules though, and you should be aware of that because different sources do it different ways. Here's another way to specify the inputs and outputs of a module:

module my_module (a, b, c);
    input a;
    output b;
    inout c;
    // ...
endmodule

You can also define an input or output to be a vector, or array of bits, instead of a scalar, or single bit. By default, all inputs and outputs are scalars, so to make an input or output a vector, you need to specify a size. This is done like so:

input [7:0] byte_in, [1:0] two_bit;

Notice that these ranges are "backwards." This is because the indexing order of Verilog vectors is defined by the order that you put the size ranges in, so putting the sizes from right to left makes the indexes go from right to left (so that byte_in[0] is the rightmost bit, for example), which makes them in order of significance, since the rightmost bit is the Least Significant Bit. You don't have to do this but all the code examples I've seen do, and I recommend it.

Once it is defined, you can use a module by making an instance of it. In this way you can think of modules as a little like classes, and instances of modules like objects. This is because Verilog is all about wiring things up so they can run continuously in an ongoing flow, instead of doing things step-by-step, so modules aren't really analogous to procedures, which you "call" to run certain steps at the time when they're called. Instead they're more like classes, where they describe something you can make a copy of to wire into an existing circuit. Here's how you make an instance of a module:

wire x, y, z;
my_module m (
    .a (x),
    .b (y),
    .c (z)
);

Basically, you connect all of the inputs and outputs of the module to things you've already got going in the circuit (wires, most likely). As a more complete example, let's build a four-value multiplexer using the two-value one I gave in the opening section.

module MUX4(a, b, c, d, sel, f)
    input a, b, c, d, [1:0] sel;
    output f;
    wire f1, f2;
    MUX m1 (
        .a (a),
        .b (b),
        .sel (sel[0]),
        .f (f1)
    );
    MUX m2 (
        .a (c),
        .b (d),
        .sel (sel[0]),
        .f (f2)
    );
    MUX m3 (
        .a (f1),
        .b (f2),
        .sel (sel[1]),
        .f (f)
    );
endmodule

3. Basics of Behavioral Verilog

3.1 Behavioral Blocks

You still use modules, inputs, and outputs in Verilog, but the way you use them is different. Instead of wiring them up using gates and modules, you instead describe the behavior you want in a way that is more similar to modern high-level programming languages — i.e. procedural. The places where you can put this behavioral specification are called behavioral blocks.

First of all, keep in mind that all behavioral blocks in a module operate simultaneously, because behavioral blocks are just a different way of describing a circuit in the constant flow of data, not actually a sequential list of instructions, even though they look like it. Likewise, within a behavioral block you can have sequential or parallel operations.

There are several types of behavioral blocks:

3.1.1 Initial Blocks

The initial is invoked only once, at the start of simulation. It looks like this:

initial begin
    x = 0; // set things up
end

It's useful for test-benches and initializing memories and flip-flips, but isn't appropriate for describing actual circuit (combinational) logic, because it doesn't keep on executing as the inputs change like a circuit should. This is an example of a block that isn't synthesizable, for obvious reasons.

3.1.2 Always Blocks

The always block works like an assign or a circuit/module would: it is invoked continuously, as its input values change.

To tell it what values it should watch, so that when they change it gets invoked again and updates its outputs, you have a sensitivity list. There are a couple ways to specify sensitivity lists:

  1. @(a or b or c) is the classic way, which you will see most often. This tells the always block to be invoked whenever a, b, or c changes.
  2. Equivalent to the above but a little shorter: @(a, b, c)
  3. Alternatively, you can use @(*), which will make an always block pay attention to any value that appears on the right hand side of an assignment within that block. Basically what this means is that any value you use (but not assign to!) in the always block will be be a value that invokes the always block when it changes.

You can actually also leave out a sensitivity list, to make an always block run as often as the simulation ticks (at maximum speed) . You'll probably want to put delays inside it, so that it actually runs on a certain interval. For example:

`timescale 1ns/1ps
module testbench;
    reg clock;
    
    initial clock = 1'b0; // this is an initial block, but it only has one statement so you don't need the begin/end stuff
    
    always begin // always block, executes continuously as fast as it can
        #10 clock = ~clock // wait 10 ns, then invert the clock; this way the block executes at an interval of 10ns
    end
endmodule

Note that this is simulation only, because a continuously executed always block, and a time delay, are both things that don't really make sense as circuits.

3.2 Procedural Programming Inside Behavioral Blocks

Since behavioral Verilog is more procedural it actually has a way of assigning values to variables, instead of just connecting gates to wires. Along with that, that means it has to have a way of defining variables. These methods are obviously to be used inside behavioral blocks.

3.2.1 Assignments

There are two ways to assign values to a variable in a behavioral block:

  1. =, which is a blocking (sequential) assignment. The RHS is evaluated immediately and the assignment is made immediately as well. Basically, execution of a blocking rule is very simple:
    1. Evaluate the RHS
    2. Update the LHS
  2. <= which is a non-blocking assignment. All assignments of this type are deferred until all right-hand sides have been evaluated (so basically, till the end of the simulation step). Thus execution of these rules is a little more complicated:
    1. Take a snapshot of the values of all the variables used in every RHS
    2. Evaluate RHS's in terms of that snapshot
    3. Update all the LHS's (and the new LHS values are what are snapshotted for the next round).

To illustrate the pitfalls of using the wrong kind of assignment, look at the following code:

module ao4
    (
        input a,
        input b,
        input c,
        input d,
        output y
    );
    reg y, tmp1, tmp2;
    
    always @(a, b, c, d) begin
        tmp1 <= a & b;
        tmp2 <= c & d;
        y <= tmp1 | tmp2;
    end
endmodule

This code does not implement the desired logic ((a & b) | (c & d)) because it uses the non-blocking assignment operator, which means the versions of tmp1 and tmp2 used to update y are the ones from a snapshot of before they were updated in the previous two lines. Since these assignments rely on being executed in order, instead of "all at the same time," you actually want to use a blocking (sequential) assignment operator, like so:

tmp1 = a & b;
tmp2 = c & d;
y = tmp1 | tmp2;

Non-blocking assignments, because they take a snapshot, allow you to swap variables without having to make a temporary variable yourself. So this:

a <= b
b <= a

Works, whereas this:

a = b
b = a

Does not.

Generally, you want to use the non-blocking assignment operator for sequential logic blocks, and the blocking ones for combinational logic blocks. Don't mix them in a single block.

3.2.2 Variables

There are three variable types that you can use in Verilog behavioral blocks:

  1. reg - you've seen this one before. A register acts like a variable in a procedural programming language, but it represents a single bit (or a vector if you use array notation like for inputs and outputs)
  2. integer
  3. real

You can define a variable like this:

reg x, y, z;

3.3 Edge Triggering

A Verilog signal can take on the following values:

  • 0
  • 1
  • X (unknown)
  • Z (high-impedance)

The latter two are errors, basically.

An edge is a signal transition — it represents the time when a signal changes from one value to another, for a signal that is synchronous and on a clock.

A negative edge is on transitions that go:

  • 1 -> x, 1 -> z, 1-> 0
  • x -> 0, z -> 0

You can denote a positive edge in an always block's sensitivity list by using posedge before the name of the variable you want to listen for a positive edge on.

A positive edge is on transitions that go:

  • 0 -> x, 0 -> z, 0 -> 1
  • x -> 1, z -> 1

You can denote a positive edge in an always block's sensitivity list by using negedge before the name of the variable you want to listen for a negative edge on.

As an example, here's the logic for a D flip-flop:

always @(posedge clk) begin
    Q <= D
end

Every positive edge in the clock cycle, the stored value is updated to whatever is being sent to the input. This lets you access that value for the rest of a clock cycle.

You can listen for more than one positive edge, too:

always @(posedge clk, posedge reset) begin
    if (reset = 1'b1) begin
        Q <= 1'b0;
    end
    else begin
        Q <= D;
    end
end

Or a negative edge, or even only check the reset when you're on the clock's positive edge, etc. I won't belabor the point.

3.4 Race Conditions

Since behavioral blocks "execute" at the same time as each other, if you have two of them mutating and using the same variables with blocking operations (which don't take snapshots to make everything safe) based on the same sensitivity lists, you can end up with race conditions, which is basically when the actual output of your program depends on random factors internal to the simulation and is therefore ambiguous and unpredictable. An example of how to make a race condition:

always @(posedge clk or posedge rst) begin
    if (rst)
        y1 = 0;
    else
        y1 = y2;
end

always @(posedge clk or posedge rst) begin
    if (rst)
        y2 = 1;
    else
        y2 = y1;
end

In this case, both blocks have the same sensitivity list, so whenever there is a positive edge on the clock or the reset, they should both run at the same time, in parallel. Since the simulation is synchronous, however, they'll be run sequentially, just in a random order depending on what the scheduler decides to do, which can result in an unpredictable outcome. For instance, let's say you reset and both blocks run. The next clock positive edge after that reset, if the first block runs first, then y1 = y2 gets run first, so y1 = y2 = 1. But if the second block runs first, then y2 = y1 gets run first, so y2 = y1 = 0. This is ungood!

Here are some tips on how to avoid race conditions:

  1. When modeling sequential logic use non-blocking assignments.
  2. When modeling latches, use non-blocking assignments
  3. When modeling combinational logic (logic gates) with an always block, use blocking assignments
  4. When modeling both sequential and combinational logic within the same always block, use non-blocking assignments
  5. Do not mix blocking and non-blocking assignments in the same always block
  6. Do not make assignments to the same variable from more than one always block
  7. Use $strobe to display values that have been assigned using non-blocking assignment

4. Simulation Verilog (for testbenches)

For this section, I'll just put in an example of a test bench, and break it down for you. It's pretty simple, so there's not much else to go over.

// This sets the default time unit (used later), as well as the resolution that you can go down to for the simulator (how many parts that basic default unit gets to be made up of).
// In this case, we're setting the default unit of time to be nanoseconds, and we can go all the way down to picoseconds (0.0001 nanoseconds) if necessary.
`timescale 1ns/1ps

// This tester module will vary various outputs over some period of time, for testing purposes.
module tester
(
    output reg x;
    output reg y;
    output reg z;
);
    // always use an initial block for this, so that it starts running at the beginning of the simulation
    initial begin
        // set the registers to zero
        x = 0;
        y = 0;
        z = 0;
        // The number after the hash symbol is the amount of time to wait before running the next simulation command
        #10 x = 1; // x=1,y=0,z=0 @ t=10ns
        #10 z = 1; // x=1,y=0,z=1 @ t=20ns
        #10 y = 1; // x=1,y=1,z=1 @ t=30ns
        #15 x = 0; // x=0,y=1,z=1 @ t=45ns
        #10 x = 1; // x=1,y=1,z=1 @ t=55ns
        #0.002 z = 0; // this will execute two picoseconds later
        // but this: #0.0003 is too small for the picosecond level resolution we chose for the simulation
        #20 $finish;
    end
endmodule

module testbench ();
    wire l, m, n, out;
    tester t // the tester will produce varying values
    (
         .x (l),
         .y (m),
         .z (n)
    );
    circuit c // ... while the circuit will be fed them and produce an output
    (
         .a (l),
         .b (m),
         .c (n),
         .o (out)
    );
    
    initial begin
        $monitor ($time, "l=%b, m=%b, n=%b, out=%b", l, m, n, out); // we can send all of these to the monitor application so we can watch and see what happens
    end
endmodule
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment