A C-like HDL (hardware description language) used by ASIC (and other chip) designers to model/simulate the logic required to make the chip function. If you think in terms of serial-threaded software, the first time you sit down to look at Verilog your head will hurt because most every statement runs in parallel -- the way most hardware works anyway.

"If it weren't hard, they wouldn't call it hardware"

Introduction

Verilog is a hardware description language, similar in scope and flexibility to VHDL. However, where VHDL is wordy and cautious, verilog provides brevity and more than enough rope to hang your verification staff with. You could say that Verilog is to C as VHDL is to Ada.

It started its life sometime in the 80s at a design firm which was then bought up by Cadence Design Systems in 1990. It was designed and first used purely as a simulation language, to simulate the behaviour of digital circuits for purposes of system verification, but proved ideally suited for logic synthesis too.

Structure

The structure of a system as specified in verilog (or, equivalently, the structure of a specification of a system) is based, logically enough, on the notion of a "module". In verilog, they look a lot like C functions, except lacking in return values. Parameters to the module instantiation define the connections of the modules.

The hierarchical structure of a design is represented by modules containing instantiations of their subordinate modules. For simulation, typically a top-level testbench module having no inputs or outputs of its own instantiates an instance of the design, and provides test stimuli to the module in order to activate the simulation.

For example, let's say we have a design, 'design' made up of two 'component's.

    +---------------------------------+
    | testbench                       |
    |  +---------------------------+  |
    |  | design +---------------+  |  |
    | -+--------+ component A   |  |  |
    |  | x      +--+------------+  |  |
    |  |           | z             |  |
    |  |        +--+------------+  |  |
    | -+--------+ component B   |  |  |
    |  | y      +---------------+  |  |
    |  +---------------------------+  |
    +---------------------------------+

In verilog, this would look like:

module component (i, o);
  input i;
  output o;
  /* behaviour would be defined here... */
endmodule

module design (x, y);
  input x;
  output y;
  wire z;
  component A (x, z);
  component B (z, y);
endmodule

module testbench;
  reg x;
  wire y;
  design (x, y);
endmodule

Specifying Behaviour

The behaviour of modules can be specified in a combination of several ways. For modules whose outputs are simple logical functions of their inputs, the assign construct can be used to simply assign an invariant expression to a signal. For example, if our 'component' in the above example is a simple NOT gate, this can be represented by:

wire o;
assign o = ~ i;

This functionality is similar to older HDLs such as ABEL, and it provides a description that maps trivially to real logic: in this case, a simple NOT gate.

However, the behaviour of modules can also be specified in a manner resembling a conventional imperative programming language, and it's in this aspect that the resemblance to C becomes most apparent. We could equivalently (more or less...) specify the behaviour of 'component' as:

always @(i) begin
  o = ~i;
end

The difference is hardly visible here except in the surrounding syntax, but for the moment you'll have to take my word for it: I assure you that that really was an imperative assignment, just like in C.

But when does that assignment happen? In C, we know that before the assignment, o would have some value, and only afterwards would it have the value ~i, and that there's a definite point in the program at which the assignment would take place. That point is defined by the control flow of the program. But since we're describing a piece of hardware (a NOT gate), which physically exists such that its output is always the one's complement of its input, we want this to be executed all the time; hence "always".

Conceptually, the code inside an always is imperative, but takes no time to actually execute. All such blocks "execute" concurrently, or as defined by their sensitivity list.

The sensitivity list is the @(i) part of the definition given above. It defines a set of events which cause the imperative code to "execute" (or, in logic terms, which will cause the outputs of the block to change.

In the example above, the block is sensitive to the values of i. Thus when the value of i changes in a simulation, the block must execute to calculate the new value of o. Sensitivity lists can also include a specification of a signal edge (for example, a clock edge), via the posedge or negedge operators.

In this way, an always can infer a register in a circuit. Any value which forms an input to a block, but which is not on the sensitivity list, must be maintained by the block such that its output will only change when the sensitivity list changes, not when the input changes. The sensitivity list of the block defines the clock signal to such registers.

We can, of course, include all sorts of familiar-looking imperative programming constructs: if, while are a lot like their C equivalents. The case construct is a little more fully formed, and supplemented by casex and casez for handling don't care values.

In addition to these, there are a raft of expression constructs to support bit-level manipulation of signals: extraction of bits from bit vectors, concatenation of bit vectors, etc.

Let's have a go at something vaguely useful now. Saturating arithmetic (arithmetic which does not overflow, but instead saturates to the maximum or minimum values representable by the data type) is frequently found in graphics and DSP algorithms, and is starting to appear in microprocessor instruction set extensions like SSE and AltiVec. So here's a module which specifies an 8-bit signed saturating addition operation.

module add_saturate(sum, x, y);
  input x, y;
  output sum;
  wire [7:0] x;
  wire [7:0] y;
  reg [7:0] sum;

  wire [8:0] xs;                // sign-extended inputs
  wire [8:0] ys;
  reg [8:0] sums;               // signed sum
  
  // Sign-extend x and y to 9 bits by duplicating the most significant bit
  // of the bit vectors (x[7]), and concatenating it with the rest of the
  // vector using the {} operator.
  assign xs = {x[7], x},
         ys = {y[7], y};

  always @(xs, ys) begin
    sums = xs + ys;     // calculate 9-bit signed sum.
    if (sums[8] == sums[7])
      // Top two bits equal: no signed overflow.
      sum = sums[7:0];  // truncate sum back to 8 bits.
    else
      if (sums[8] == 1'b0)
        // top bit of signed sum is zero, indicating a positive sum.
        sum = 8'd127;   // maximum positive value representable by 8 bits.
      else
        sum = 8'd128;   // two's complement representation of -128.
  end
endmodule

This illustrates a few points. Extracting a bit from a bit vector looks a lot like an array dereference in C. Bits (which are equivalent to bit vectors of length one) can be concatenated to form larger bit vectors. Numeric literals in verilog have a somewhat wacky form: n'bdigits, where n is the number of bits in the resultant bit vector, b is a character representing the base of the digits ('d' for decimal, 'b' for binary and 'h' for hexadecimal).

Moreover, the bit vector is the only fundamental type in verilog. There is no notion of signed arithmetic, or structured data formats. Much like the hardware being modelled, everything is really a binary signal.

This is one of the most dangerous facets of Verilog as a language: an almost complete lack of type-safety.

Simulation

Several of verilog's constructs have meaning only in the context of simulation. There are initial blocks which, like always blocks contain imperative code, but are executed only once, at the very beginning of simulation. These can be used to set up initial parameters like register values, contents of memory, and driving the simulation.

VPI provides an interface to simulator facilities, including user-provided libraries in C which can then run inside the simulator. Calls to VPI are identified by a $ in front of their identifiers.

Simulation delays can be introduced with the # construct, which delays signals or imperative statements by a given amount of simulation time. To illustrate a few of these, here's a rough and ready testbench module for the above adder module.

module testbench;
  integer X, Y;
  reg [7:0] x;
  reg [7:0] y;
  wire [7:0] out;

  // instantiate module under test.
  add_saturate s(out, x, y);

  initial begin    
    for (X = 0; X < 256; X = X + 1) begin
      // fast forward though uninteresting values.
      if (X == 2) X = 62;
      if (X == 65) X = 127;
      if (X == 129) X = 254;

      for (Y = 0; Y <= 256; Y = Y + 1) begin
        // fast forward again.      
        if (Y == 2) Y = 62;
        if (Y == 65) Y = 127;
        if (Y == 129) Y = 254;

        x = X; y = Y;   // Set signal values.

        // Delay 1 unit of simulation time, and then use the $display
        // VPI call to display the values of inputs and outputs
        #1 $display("%x+%x=%x", X, Y, out);

      end // for Y
    end // for X
  end // initial
endmodule

Interesting points include:

  • initial block begins execution at simulation start time.
  • #1 causes a delay of 1 time unit.
  • $display causes some plain old C code in the simulator to execute. The format string is a lot like a C format string, but with awareness of bit vector lengths rather than assuming a fixed-width integer.
  • Verilog has no += operator.

Synthesis

Behavioural verilog code can be used to synthesise real logic implementing the same functionality as demonstrated by the simulation of the code. Intuitively, the sequential nature of imperative code seems to imply a sequential state machine in the resultant logic: repeated assignment to a single value, for example, seem to imply two different states, one later in time than the other, in which the same register would be given a new value. However, each assignment can be treated as a different value, and the outputs of an imperative block can be re-expressed as an expression in terms of the blocks inputs and any registers inferred by the block.

The sequential block in the saturating adder, above, can be re-expressed as assigns:

assign sums = xs + ys;
assign sum = (sums[8] == sums[7])? 
                sums[7:0] :
                ( sums[8] == 1'b0)?
                    8'd127 :
                    8'd128;

The ?: operators can be implemented by multiplexors and comparisons by XOR gates, so it is now a fairly straightforward task for a software tool (synthesis tool) to match this code to descriptions of logic gates in a technology library, and create a structural description of the logic to implement the functionality (which can itself be expressed in verilog). If we have modules xor(x, y), add9(sum9, x9, y9), mux8(out8, sel, sel0_x8, sel1_x8), we can re-express the module in structural terms as:

wire [8:0] sums;
add9 adder (sums, {x[7], x}, {y[7], y});
wire overflow;
xor overflow_xor (overflow, sums[8], sums[9]);
wire [8:0] sum_satval;
mux8 satval_mux (sum_satval, sums[8], 8'd127, 8'd128);
mux8 overflow_mux (sum, overflow, sums[7:0], sum_satval);

This description contains only instantiations of available modules, wires and constant values (corresponding to wires connected to power or ground rails), and so can be trivially mapped to real hardware by a layout program and (possibly with manual intervention) used to create a printed circuit board or masks to create an integrated circuit. The synthesis tool will perform many optimisations including logic simplification by DeMorgan's Laws, selection of alternative modules and structures by optimisation for power, time or other cost metrics.

Log in or register to write something here or to contact authors.