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”.
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: 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
endmoduleRead the two versions line by line:
- Libraries. VHDL starts with
library ieee; use ieee.std_logic_1164.all;, which makes the typestd_logicavailable. Verilog's built-in types need no import. - Ports. Each port has a direction (
input/output, orin/out) and a type. In Verilog the type iswire; in VHDL it isstd_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;andsum <= a xor b;both mean “connectsumto the output of an XOR gate fed byaandb”. 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;andend 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:
| Meaning | Verilog | VHDL std_logic | Where you see it |
|---|---|---|---|
| logic 0 / logic 1 | 0, 1 | '0', '1' | normal operation |
| unknown / conflict | x | '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 care | x 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_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));
endmoduleThe 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, thenentity work.<name>(the design is in the default librarywork), thenport => signal. This “direct entity instantiation” is VHDL-93; older code declares acomponentfirst, 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.
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.
| Concept | Verilog | VHDL |
|---|---|---|
| Comment | // to end of line /* block */ | -- to end of line (/* block */ in 2008) |
| Block declaration | module m (...); ... endmodule | entity m is port (...); end entity; |
| Block body | (inside the module) | architecture rtl of m is ... begin ... end architecture; |
| 1-bit port | input wire a | a : in std_logic |
| 8-bit port | output wire [7:0] d | d : out std_logic_vector(7 downto 0) |
| Internal net | wire s; reg r; | signal s : std_logic; |
| Continuous assignment | assign y = a & b; | y <= a and b; |
| Bitwise operators | & | ^ ~ | and or xor not |
| Equality / inequality | == != | = /= |
| Single bit literal | 1'b0 1'b1 | '0' '1' |
| Vector literal | 4'b1010 8'hFF 8'd200 | "1010" x"FF" to_unsigned(200, 8) |
| Bit select / slice | d[0] d[7:4] | d(0) d(7 downto 4) |
| Concatenation | {a, b} | a & b |
| Instantiation | m u1 (.p(x)); | u1 : entity work.m port map (p => x); |
| Parameter | parameter N = 8 | generic (N : positive := 8) |
& 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.
| Construct | Verilog | VHDL | Synthesis |
|---|---|---|---|
| Gates, muxes, arithmetic | assign, always @(*) | <=, process | yes |
| Flip-flops | always @(posedge clk) | rising_edge(clk) | yes |
| Instances, parameters, generate | yes | yes | yes |
| Initial values on registers | reg r = 1'b0; | signal r : std_logic := '0'; | yes on Xilinx (power-up value) |
| Time delays | #10 | wait for 10 ns; after 5 ns | ignored or rejected |
| Run-once stimulus | initial begin ... end | process without sensitivity list | no (testbench only) |
| Printing, files | \$display, \$readmemh* | report, textio* | no (*memory init is OK) |
| Ending the simulation | $finish | std.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.
`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
endmoduleBoth 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.
# 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
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.
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 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- Create a Vivado RTL project for part
xc7z020clg400-1, addhalf_adderandfull_adder(in one language) andfull_adder.xdc; setfull_adderas top. - 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.
- With the boot jumper on JTAG, program the device from Hardware Manager.
- Walk through the truth table: SW0 =
a, SW1 =b, hold BTN0 forcin. LD0 showssum, LD1 showscout. 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
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?data (bit 7 the most significant) and a 1-bit output parity.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?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?