Thinking in Hardware

Why HDL code is not software: concurrency, synthesis versus simulation, modules and entities, ports and signals, your first design in both languages.

1. Describing a circuit, not running a program

Verilog and VHDL look like programming languages: they have keywords, operators, if statements and semicolons. That resemblance is the source of almost every beginner mistake. A hardware description language (HDL) does not give a processor a list of instructions to execute one after another. It describes a circuit — a set of gates, flip-flops and wires — that a tool will then build out of the FPGA's look-up tables and flip-flops (Chapters 4 and 5).

In software, the line x = a & b runs once, at one moment, when the program counter reaches it. In hardware, assign x = a & b; places an AND gate on the chip. That gate exists for as long as the chip is powered, and its output follows its inputs continuously. Every other gate in the design is doing the same thing at the same time. There is no program counter, no “next line”.

Software: one statement at a times1 = a ^ bt = 1c1 = a & bt = 2y = s1 | c1t = 3timeswap two lines → different resultHardware: everything at onceabXORANDs1c1ORyall three gates are always active;a change on a or b ripples through within nanoseconds
Left: a processor executes statements one at a time, so their order matters. Right: the same three statements in an HDL become three gates that all operate simultaneously; the order they are written in is irrelevant — only the wires connecting them matter.
Key idea: if you cannot draw it, do not write it
Before writing any HDL, sketch the hardware you want: which signals are wires, which are stored in flip-flops, which blocks feed which. Then write the code that describes that drawing. Every construct in this part of the course is introduced together with the hardware it produces.

2. The unit of design: module, or entity + architecture

Every design is built from blocks with a name, a set of input and output pins, and a body that says what happens inside. Verilog calls this a module. VHDL splits it in two: the entity describes the outside (name and ports — what you would see on a datasheet), and the architecture describes the inside. One entity may have several architectures (for example a behavioural one and a gate-level one); in practice you will almost always write one, conventionally named rtl (register-transfer level) or structural.

Our first design is the half adder: it adds two one-bit numbers. The sum \(a + b\) can be 0, 1 or 2, which needs two bits: the low bit sum is \(a \oplus b\) (exclusive OR) and the high bit carry is \(a \cdot b\) (AND).

half_adder — your first design, in both languages
// half_adder: adds two 1-bit numbers.
// sum is the low bit of a + b, carry is the high bit.
module half_adder (
    input  wire a,
    input  wire b,
    output wire sum,
    output wire carry
);
    assign sum   = a ^ b;   // XOR gate
    assign carry = a & b;   // AND gate
endmodule

Read the two versions line by line:

  • Libraries. VHDL starts with library ieee; use ieee.std_logic_1164.all;, which makes the type std_logic available. Verilog's built-in types need no import.
  • Ports. Each port has a direction (input/output, or in/out) and a type. In Verilog the type is wire; in VHDL it is std_logic, a single bit. Note the punctuation: Verilog separates ports with commas, VHDL with semicolons, and the last one has neither.
  • Body. assign sum = a ^ b; and sum <= a xor b; both mean “connect sum to the output of an XOR gate fed by a and b”. VHDL's <= is the signal assignment operator — read it as “is driven by”, not as “less than or equal” (the same symbol means that inside a condition).
  • End. endmodule; end entity; and end architecture;.

Synthesis turns either version into the same thing. On an FPGA both outputs are functions of the same two inputs, so Vivado will typically pack them into one 6-input LUT with two outputs (the dual-output LUT of Chapter 5).

3. Ports, wires and signals — and what a bit can be

Inside a block you need names for the connections between parts. In Verilog these are declared as wire (a connection that is continuously driven by something) or reg (a variable assigned inside an always block — Chapter 8 explains why the name is misleading: a reg is not necessarily a register). In VHDL every internal connection is a signal, declared between architecture … is and begin.

A real wire in a simulator can be in more states than 0 and 1. Verilog has four values; VHDL's std_logic has nine, of which you will meet five in practice:

MeaningVerilogVHDL std_logicWhere you see it
logic 0 / logic 10, 1'0', '1'normal operation
unknown / conflictx'X'two drivers fight, or an input was never set
high impedance (not driven)z'Z'tri-state pins, disconnected inputs
uninitialised(x)'U'a VHDL signal before anything assigned it
don't carex in a case item'-'tells synthesis any value is fine

These extra values are a simulation aid. In the FPGA fabric every internal net is 0 or 1. When your waveform shows red X or U, the simulator is telling you that your description does not define the value — usually a missing reset, an unconnected input, or two blocks driving the same wire.

Multi-bit buses are vectors: wire [7:0] d in Verilog, std_logic_vector(7 downto 0) in VHDL. In both, the first index written is the most significant bit, and d[0] / d(0) is the least significant. Chapter 8 covers slicing, concatenation and arithmetic on vectors.

4. Concurrent statements and structural instantiation

A full adder adds three bits: \(a + b + c_\text{in}\), giving a sum bit and a carry out. It can be built from two half adders: the first adds \(a\) and \(b\), the second adds that partial sum to \(c_\text{in}\), and the carry out is 1 if either half adder produced a carry. (The two carries can never both be 1 — try it — so an OR gate is enough.)

full_adderabcinha1half_adderabsumcarrys1ha2half_adderabsumcarryc1c2ORcoutsum
The full adder built structurally: two instances of half_adder (ha1, ha2) and one OR gate, connected by the internal nets s1, c1, c2. The HDL below is a text form of exactly this schematic.
full_adder — structural description using two half_adder instances
// full_adder: a + b + cin, built from two half adders and an OR gate.
module full_adder (
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);
    wire s1, c1, c2;   // internal wires between the parts

    // The OR gate is written first on purpose: order does not matter.
    assign cout = c1 | c2;

    half_adder ha1 (.a(a),  .b(b),   .sum(s1),  .carry(c1));
    half_adder ha2 (.a(s1), .b(cin), .sum(sum), .carry(c2));
endmodule

The body contains three concurrent statements: one assignment and two instantiations. An instantiation places a copy of another block and connects its ports. Each copy needs its own instance name (ha1, ha2); the block is written once and used as many times as you like, just as a schematic can contain many copies of the same chip.

  • Verilog: half_adder ha1 (.a(a), .b(b), .sum(s1), .carry(c1)); — module name, instance name, then named port connections .port(net).
  • VHDL: ha1 : entity work.half_adder port map (a => a, …); — label, then entity work.<name> (the design is in the default library work), then port => signal. This “direct entity instantiation” is VHDL-93; older code declares a component first, which you will still see often.

Note that the OR gate is written before the half adders that produce its inputs. In software that would read uninitialised variables. In an HDL it is perfectly normal: all three statements describe pieces of hardware that exist simultaneously, and the wires c1 and c2 connect them regardless of where in the text the pieces are described.

Common mistake: positional port maps
Both languages also allow connecting ports by position: half_adder ha1 (a, b, s1, c1); or port map (a, b, s1, c1). It is shorter and fragile: if someone reorders the ports of half_adder, every instance silently connects the wrong wires and still compiles. Always use named connections.

5. A Rosetta stone for the two languages

The two languages express the same ideas with different words. Verilog is terse and C-like, and is case-sensitive. VHDL is verbose and Ada-like, is not case-sensitive (Sum and SUM are the same name), and is strongly typed: it refuses to mix a bit with a number or a 4-bit vector with an 8-bit one unless you convert explicitly. Keep this table at hand for the rest of the course.

ConceptVerilogVHDL
Comment// to end of line /* block */-- to end of line (/* block */ in 2008)
Block declarationmodule m (...); ... endmoduleentity m is port (...); end entity;
Block body(inside the module)architecture rtl of m is ... begin ... end architecture;
1-bit portinput wire aa : in std_logic
8-bit portoutput wire [7:0] dd : out std_logic_vector(7 downto 0)
Internal netwire s; reg r;signal s : std_logic;
Continuous assignmentassign y = a & b;y <= a and b;
Bitwise operators& | ^ ~and or xor not
Equality / inequality== !== /=
Single bit literal1'b0 1'b1'0' '1'
Vector literal4'b1010 8'hFF 8'd200"1010" x"FF" to_unsigned(200, 8)
Bit select / sliced[0] d[7:4]d(0) d(7 downto 4)
Concatenation{a, b}a & b
Instantiationm u1 (.p(x));u1 : entity work.m port map (p => x);
Parameterparameter N = 8generic (N : positive := 8)
Common mistake: & means different things
In Verilog & is bitwise AND. In VHDL & is concatenation and AND is the keyword and. Likewise <= is assignment in VHDL (and in Verilog always blocks — Chapter 9) but “less or equal” inside an if condition in both.

6. Synthesis versus simulation

The same HDL text is read by two very different programs:

  • A simulator (Icarus Verilog, GHDL, Vivado's xsim) executes the description on your PC and shows you what the signals do over time. It accepts the whole language.
  • A synthesiser (Vivado synthesis, Yosys) turns the description into a netlist of LUTs, flip-flops and other FPGA primitives. It accepts only the synthesisable subset: constructs that correspond to real hardware.
ConstructVerilogVHDLSynthesis
Gates, muxes, arithmeticassign, always @(*)<=, processyes
Flip-flopsalways @(posedge clk)rising_edge(clk)yes
Instances, parameters, generateyesyesyes
Initial values on registersreg r = 1'b0;signal r : std_logic := '0';yes on Xilinx (power-up value)
Time delays#10wait for 10 ns; after 5 nsignored or rejected
Run-once stimulusinitial begin ... endprocess without sensitivity listno (testbench only)
Printing, files\$display, \$readmemh*report, textio*no (*memory init is OK)
Ending the simulation$finishstd.env.finish / wait;no

Code written only for the simulator is called a testbench. It instantiates the design under test (by convention named dut), drives its inputs, waits, and checks its outputs. Here is a testbench that tries all eight input combinations of the full adder (and checks the half adder on the way) against the arithmetic sum. It has no ports — it is the whole universe of the simulation.

full_adder_tb — exhaustive self-checking testbench
`timescale 1ns/1ps
// Exhaustive testbench for half_adder and full_adder: all 8 input combinations.
// Simulation-only: initial, #delays, for loops over time, $display.
module full_adder_tb;
    reg  a, b, cin;
    wire sum, cout, hsum, hcarry;
    integer i, errors = 0;

    full_adder dut    (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
    half_adder dut_ha (.a(a), .b(b), .sum(hsum), .carry(hcarry));

    initial begin
        $display(" a b cin | cout sum");
        for (i = 0; i < 8; i = i + 1) begin
            {a, b, cin} = i[2:0];          // drive the inputs
            #10;                           // let the circuit settle
            $display(" %b %b  %b  |  %b    %b", a, b, cin, cout, sum);
            if ({cout, sum} != a + b + cin) errors = errors + 1;
            if ({hcarry, hsum} != a + b)    errors = errors + 1;
        end
        if (errors == 0) $display("PASS: all 8 combinations correct");
        else             $display("FAIL: %0d errors", errors);
        $finish;
    end
endmodule

Both versions print the full truth table and then PASS: all 8 combinations correct at 80 ns. Inside a testbench process, statements do execute one after another, like software — that is what makes it a convenient place to write stimulus. The design under test, however, is still hardware, and it reacts to every change of its inputs.

Run it
# Verilog with Icarus
iverilog -g2012 -o tb half_adder.v full_adder.v full_adder_tb.v && vvp tb

# VHDL with GHDL (analyse in dependency order, then elaborate and run)
ghdl -a --std=08 half_adder.vhd full_adder.vhd full_adder_tb.vhd
ghdl -e --std=08 full_adder_tb && ghdl -r --std=08 full_adder_tb
Key idea: design code and testbench code are different worlds
Design files must stay inside the synthesisable subset and describe hardware you could draw. Testbench files may use any language feature — delays, loops over time, printing, file I/O — because they never become hardware. Keeping the two strictly in separate files avoids a whole class of “works in simulation, not on the board” surprises. Part IV is devoted to writing good testbenches.

7. Python: why statement order does not matter

How does a simulator, which runs on a sequential processor, reproduce hardware where everything happens at once? It uses event-driven simulation. It keeps the current value of every net and a queue of future changes. When a net changes, every gate that reads it is scheduled for re-evaluation; all those gates read the same snapshot of values, and their new outputs are applied a delay later, which may trigger more gates. When the queue is empty the circuit has settled.

The program below models the full-adder netlist this way. It first shuffles the list of gates 20 times — the equivalent of reordering the statements in the HDL — and checks that every ordering gives \(2\,c_\text{out} + \text{sum} = a + b + c_\text{in}\) for all eight inputs. Then it plots a waveform with one time unit per gate, so you can watch changes ripple through s1, c2 and on to the outputs.

Python
script.py82 lines

Click Run to execute the Python code

Code will be executed with Python 3 on the server

Look at the plot just after \(t = 25\), when b rises while a and cin are already 1. s1 falls and c1 rises after one gate delay; one delay later c2 falls, and cout — which was already 1 because of c2 — stays at 1 because c1 took over. The outputs settle a few delays after each input change. Real gates and LUTs have delays too (on the Zynq, a LUT plus its routing costs a few hundred picoseconds), which is why Chapter 15 (timing closure) cares about the longest such path. Note also how few evaluations the event-driven approach needs: only gates whose inputs change are touched.

8. Optional: the full adder on the board

You do not need the board yet — everything in Part III can be learned in simulation — but a purely combinational design is a satisfying first thing to load. Map the three inputs to the two slide switches and a push-button, and the two outputs to LEDs. There is no clock, so the constraint file has only pin assignments.

full_adder.xdc
## full_adder on the PYNQ-Z2: switches and a button in, LEDs out.
## No clock: this design is purely combinational.
set_property -dict { PACKAGE_PIN M20 IOSTANDARD LVCMOS33 } [get_ports a]     ;# SW0
set_property -dict { PACKAGE_PIN M19 IOSTANDARD LVCMOS33 } [get_ports b]     ;# SW1
set_property -dict { PACKAGE_PIN D19 IOSTANDARD LVCMOS33 } [get_ports cin]   ;# BTN0
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports sum]   ;# LD0
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports cout]  ;# LD1
On the board: a full adder you can touch
  1. Create a Vivado RTL project for part xc7z020clg400-1, add half_adder and full_adder (in one language) and full_adder.xdc; set full_adder as top.
  2. Generate the bitstream. Before that, open the synthesised design's schematic: you should see the five gates replaced by LUTs connected to I/O buffers.
  3. With the boot jumper on JTAG, program the device from Hardware Manager.
  4. Walk through the truth table: SW0 = a, SW1 = b, hold BTN0 for cin. LD0 shows sum, LD1 shows cout. With both switches up and the button pressed, both LEDs light: \(1+1+1 = 3 = 11_2\).

The LEDs change the instant you move a switch — there is no clock and no program, just a path of a few nanoseconds from pin to LUT to pin. In Chapter 8 we write combinational logic more compactly, and in Chapter 9 we add the clock and flip-flops that give a circuit memory.

Practice Problems

Progress: 0/5 (0%)
1
⭐ Easy
In the full adder, the line assign cout = c1 | c2; (VHDL: cout <= c1 or c2;) is moved from the top of the body to the bottom. What changes in the synthesised circuit? In the simulation?
2
⭐ Easy
Write the Verilog port declaration and the VHDL port declaration for an 8-bit input data (bit 7 the most significant) and a 1-bit output parity.
3
⭐⭐ Medium
A student writes assign y = y ^ x; in Verilog, hoping that y toggles whenever x is 1. What hardware does this describe, and what does a simulator do with it?
4
⭐⭐ Medium
Using only half_adder instances and gates, how many half adders and OR gates does a 4-bit ripple-carry adder need if you build it from full adders as in this chapter? How many gate delays does the carry take, worst case, from a[0] to the final carry out, assuming each gate costs one delay?
5
⭐⭐⭐ Hard
In the Python model, all gates triggered at time \(t\) read a snapshot of the net values before any of them writes its output, and their outputs appear at \(t+1\). What would go wrong if the model instead updated each net immediately, in list order, within the same time step? Give a concrete example with the full-adder netlist.

Problem Set Statistics

Total Problems
5
Attempted
0
Completion
0%
Share:XRedditLinkedIn
Rate this chapter: