Digital Electronics · Chapter 30

Introduction to VHDL for Digital Design

Part 6 · Describing hardware in text so that a synthesiser can build it.

Dr. Mithun MondalEngineering DevotionDigital Textbook
i Learning Objectives

By the end of this chapter you should be able to:

  • Explain why textual description replaced schematic capture for anything above a few hundred gates.
  • Write an entity declaration with correctly chosen port modes, and pair it with one or more architectures.
  • Describe the same circuit in dataflow, behavioural and structural style, and say which is appropriate when.
  • State why std_logic has nine values and what each of 'U', 'X', 'Z', 'W', 'L', 'H' and '-' is for.
  • Use processes and sensitivity lists correctly, and explain the difference between signal and variable assignment.
  • Write synthesisable descriptions of a full adder, a 4-to-1 multiplexer, a D flip-flop and a mod-10 synchronous counter.
  • Outline a test bench, and recognise the incomplete if, case or sensitivity list that produces an unwanted latch.

Every circuit in this course so far has been specified by drawing it. That works while the drawing fits on a page. It stops working at about the scale of Chapter 20's arithmetic logic unit, and it has been hopeless for thirty years at the scale of the devices Chapter 28 described, where a mid-range FPGA holds hundreds of thousands of logic blocks. Nobody draws a hundred thousand gates, and if they did, nobody could review the drawing, keep it under version control, or search it for the one signal that is wrong.

A hardware description language solves that by writing the circuit down as text. The word description is the one to hold on to: VHDL is not a programming language that happens to control hardware, it is a notation for saying what a piece of hardware is, and a synthesiser reads it and builds the corresponding gates and flip-flops. This chapter introduces enough VHDL to describe the circuits of the previous chapters — a full adder, a multiplexer, a flip-flop, a counter — in a form a synthesiser will accept, and spends its last section on the single commonest way of getting it wrong.

You are not writing a program; you are describing a circuit that already exists in your head. Every concurrent statement in an architecture is a permanently connected piece of hardware, all of them live at the same instant, and the order in which you write them changes nothing. The moment you start thinking of a <= b as "copy b into a" rather than as "there is a wire from b to a", the synthesiser's output will start surprising you — and the commonest surprise is a latch you never asked for.

1 Why an HDL Replaced Schematic Capture

A schematic is a fine specification for a circuit of twenty gates and a poor one for a circuit of twenty thousand. The reasons are practical rather than theoretical, and they are worth listing because they explain the shape of the language.

  • Scale. Drawing effort grows with the number of symbols; writing effort grows with the number of distinct ideas. A 32-bit adder is one line of text and 160 gate symbols on a page.
  • Parameterisation. A text description can be written once for a width \(N\) and elaborated at 4, 8 or 64 bits. A drawing must be redrawn.
  • Version control and review. Text diffs. Two revisions of a schematic can only be compared by looking at them, which is how errors survive reviews.
  • Portability. A schematic is drawn from a particular vendor's symbol library and dies with it. VHDL is an IEEE standard (1076, first ratified in 1987) and the same source targets a CPLD, an FPGA or an ASIC.
  • Simulation and synthesis from one source. The description you verify is the description you build, so the two cannot drift apart.

VHDL — VHSIC Hardware Description Language — came out of a United States Department of Defense programme in the early 1980s whose purpose was documentation: contractors were delivering custom chips with no machine-readable record of what they did. Simulation came next, and synthesis, the automatic translation of a description into gates, arrived only around 1990. That history explains a language feature that surprises beginners: much of VHDL is not synthesisable. Constructs such as wait for 10 ns, floating-point arithmetic and file input describe behaviour a simulator can imitate but no gate can implement. They are perfectly proper VHDL and belong in test benches, never in a design.

The discipline to acquire now is to keep in your head, at every line, the hardware you expect, and to use the synthesis report — so many look-up tables, so many registers — as a check on whether the tool built what you meant. A designer who writes VHDL the way one writes C and then inspects the result to see what appeared will get working silicon occasionally and predictable silicon never.

2 Entity, Architecture, Ports and Modes

A VHDL design unit comes in two parts, and the split is exactly the one an engineer already makes between a block's symbol and its contents. The entity declares the interface: the name of the block and its ports, with the direction and type of each. The architecture describes what is inside. One entity may have several architectures — a behavioural one for early simulation and a structural one for the final build — and because they share the interface, one can be substituted for another without touching anything that uses the block.

</> The entity: interface only
library ieee;
use ieee.std_logic_1164.all;

entity full_adder is
    port ( a, b, cin : in  std_logic;
           sum, cout : out std_logic );
end entity full_adder;

The two lines at the top matter. std_logic is not built into the language; it lives in the package std_logic_1164 in the library ieee, and without the use clause the compiler will not know the name. The habit of writing both lines at the head of every file saves a great deal of confusion.

Each port has a mode that says which way information flows, and the compiler enforces it:

  • in — read inside the architecture, never assigned. Assigning to an input is an error, and rightly so: the signal is driven from outside.
  • out — assigned inside, and in VHDL-87 and -93 not readable inside. If you need to read back what you drove, drive an internal signal and assign that signal to the port. VHDL-2008 relaxed this, but the internal-signal habit is still the portable one.
  • inout — genuinely bidirectional, for a real tri-state pin such as a shared data bus of the kind Chapter 13 described. Use it only when the hardware really is bidirectional.
  • buffer — driven inside and readable inside. It looks like the answer to the out restriction and causes trouble when the entity is instantiated, so prefer the internal signal.
entity full_adder — the interface, and only the interface full_adder architecture dataflow, behavioural or structural — interchangeable, because the ports do not change a in b in cin in sum out cout out Port modes in read inside, never assigned out assigned inside, not readable (before VHDL-2008) inout bidirectional — a real tri-state pin buffer driven inside and read back; use a signal instead
Figure 30.1 — Entity and architecture: one interface, any number of bodies

Inside an architecture, signals are the wires. A signal is declared between the architecture line and the begin, it has a type, and it exists for the life of the circuit. The essential property of a signal — the one that separates VHDL from a programming language — is that assignment to it is scheduled rather than immediate. Writing s <= x; does not change s at that moment; it says that s will take the value of x after a delay, which for synthesis is an infinitesimal one called a delta cycle. That is exactly how a wire driven by a gate behaves, and Section 5 shows what goes wrong when it is forgotten.

3 std_logic and Why It Has Nine Values

VHDL does define a two-valued type, bit, with values '0' and '1'. Nobody uses it, because a real wire has more states than two and a simulation that cannot represent them will report success on circuits that fail on the bench. The standard type is std_logic, defined in std_logic_1164, and it has nine values.

ValueNameWhat it represents
'U'UninitialisedThe default before anything drives the signal. Seeing 'U' in a waveform means a register was never reset.
'X'Forcing unknownTwo strong drivers are fighting, or an unknown has propagated. This is bus contention, and on real silicon it is a short circuit.
'0'Forcing 0A strong low, an output driving to ground.
'1'Forcing 1A strong high.
'Z'High impedanceA tri-state output turned off, so the wire is driven by somebody else — the modelling of Chapter 13's tri-state bus.
'W'Weak unknownTwo weak drivers in conflict, for instance two pull resistors of opposite sense.
'L'Weak 0A pull-down resistor: it wins against 'Z' and loses against '1'.
'H'Weak 1A pull-up resistor, the open-collector arrangement of Chapter 13.
'-'Don't careThe don't-care of Chapter 8, offered to the synthesiser as freedom to minimise. In simulation it is not a wildcard and never matches anything.

The list is exactly what a multi-driver wire needs: a strength (forcing, weak, or none at all) crossed with a level (0, 1 or unknown), plus the uninitialised state. The package supplies a resolution function that says what happens when several drivers act on one signal — forcing beats weak, weak beats high impedance, and two conflicting forcing drivers give 'X'. That is a model of the electrical behaviour Chapter 13 derived for wired-AND and tri-state outputs, and it is why bus contention shows up as a screenful of 'X' in simulation instead of silently working.

Three practical points. Only '0', '1', 'Z' and, with care, '-' mean anything to a synthesiser; the rest are simulation values. Comparisons are literal, so if x = '-' asks whether x holds that value and matches neither '0' nor '1'. And std_logic_vector is a bare array of bits with no numeric meaning: to do arithmetic, convert it to unsigned or signed from numeric_std, as the counter in Section 6 does.

4 Three Description Styles for One Circuit

VHDL offers three ways of saying what is inside an architecture. They are not three languages and they are not ranked; the same synthesiser produces the same netlist from all three. What differs is which one lets you say what you mean most directly. The comparison is only convincing when the circuit is held fixed, so all three descriptions below are of the full adder of Chapter 16, with the same entity of Section 2.

Dataflow style writes the Boolean equations as concurrent signal assignments. Each statement is a permanently connected piece of hardware; they all act at once, and reordering them changes nothing.

</> Style 1 — dataflow: concurrent signal assignment
architecture dataflow of full_adder is
begin
    sum  <= a xor b xor cin;
    cout <= (a and b) or (cin and (a xor b));
end architecture dataflow;

These are the expressions Chapter 16 derived, transcribed. Dataflow style is the natural choice whenever you already have the algebra: adders, parity trees, comparators, the code converters of Chapter 20.

Behavioural style describes what the circuit does rather than how it is wired, inside a process. Statements within a process execute in order, like a program, which lets you write if, case and loops; the process as a whole is still one concurrent block of hardware.

</> Style 2 — behavioural: a process containing the truth table
architecture behavioural of full_adder is
begin
    process (a, b, cin) is                 -- sensitivity list: every input read
        variable v : std_logic_vector(2 downto 0);
    begin
        v := a & b & cin;                  -- concatenate into one 3-bit value
        case v is
            when "000" => sum <= '0'; cout <= '0';
            when "001" => sum <= '1'; cout <= '0';
            when "010" => sum <= '1'; cout <= '0';
            when "011" => sum <= '0'; cout <= '1';
            when "100" => sum <= '1'; cout <= '0';
            when "101" => sum <= '0'; cout <= '1';
            when "110" => sum <= '0'; cout <= '1';
            when "111" => sum <= '1'; cout <= '1';
            when others => sum <= 'X'; cout <= 'X';
        end case;
    end process;
end architecture behavioural;

This is the truth table of Chapter 16 written out, and it synthesises to the same gates as the dataflow version — the synthesiser minimises it for you, so the K-maps of Chapter 8 are being applied inside the tool rather than on your page. The when others branch covers the metalogical values of Section 3: without it the case would not be exhaustive, and an inexhaustive case infers a latch, which is the subject of Section 7.

Structural style builds the circuit from other components, wiring them together with internal signals. It is the schematic, written down.

</> Style 3 — structural: two half adders and an OR gate
architecture structural of full_adder is

    component half_adder is
        port ( x, y : in  std_logic;
               s, c : out std_logic );
    end component;

    signal s1, c1, c2 : std_logic;         -- the internal wires

begin
    ha1 : half_adder port map (x => a,  y => b,   s => s1,  c => c1);
    ha2 : half_adder port map (x => s1, y => cin, s => sum, c => c2);

    cout <= c1 or c2;
end architecture structural;

Each instantiation has a label (ha1, ha2) and a port map that associates the component's ports with signals in this architecture. Naming the ports explicitly, as here, rather than relying on position, is worth the extra characters: positional association silently connects the wrong wires the day somebody reorders a port list. Structural style is how hierarchy is built — a 4-bit ripple-carry adder is four instances of this entity with the carries chained, exactly the drawing in Chapter 16.

dataflow concurrent signal assignment behavioural a process with a sensitivity list structural component instantiation synthesiser a b cin XOR XOR AND AND OR s1 sum cout All three architectures elaborate to this one netlist. The style is a matter of what you find clearest to write and to read.
Figure 30.2 — Three descriptions, one netlist: the full adder of Chapter 16

5 Processes, Sensitivity Lists, Signals and Variables

A process is a block of sequential statements that behaves, from the outside, as one concurrent element. It runs, suspends, and runs again when something wakes it. What wakes it is its sensitivity list, the signal names in brackets after the keyword.

For combinational logic the rule is absolute: every signal read inside the process must appear in the sensitivity list. If one is missing, the simulator will not re-evaluate the process when that signal changes, so the simulated output holds its old value — behaving, in simulation, like a latch. The synthesiser, which ignores sensitivity lists and looks only at what is read, builds combinational logic anyway. The result is a design whose simulation and hardware disagree, which is the worst kind of bug because the simulation is the thing you trusted. VHDL-2008 added process (all) precisely to remove this trap; where it is supported, use it.

For sequential logic the sensitivity list is short and deliberate. A synchronous element is sensitive to the clock, and to the asynchronous reset if there is one, and to nothing else — not because the other signals are unimportant but because they are only sampled at the clock edge.

</> A D flip-flop with an asynchronous reset
process (clk, rst) is
begin
    if rst = '1' then                     -- asynchronous: outside the edge test
        q <= '0';
    elsif rising_edge(clk) then           -- everything here is edge-triggered
        q <= d;
    end if;
end process;

The shape is a template, and it is worth learning as one. rising_edge(clk) is a function from std_logic_1164 that is true on a '0'-to-'1' transition; every assignment inside that branch becomes the D input of a flip-flop. Move the reset test inside the edge branch and you get a synchronous reset instead — a different circuit, and often the better one, since it uses the flip-flop's data path rather than its asynchronous pin. The setup and hold times of Chapter 21 are what the timing analyser of Chapter 28 will check against this flip-flop after routing.

Inside a process you may also declare variables, and the difference between a variable and a signal is the one point in this chapter that repays real care.

  • A signal is assigned with <=. The assignment is scheduled, so the new value is not visible to later statements in the same process run. A signal models a wire.
  • A variable is assigned with :=. The assignment takes effect immediately, so later statements see the new value. A variable models a temporary name for an intermediate result — and, sometimes, a register.
</> The same two lines, with a signal and with a variable
-- (a) signals: b gets the OLD value of s, so two flip-flops in series
process (clk) is
begin
    if rising_edge(clk) then
        s <= a;
        b <= s;                           -- s is still the previous value here
    end if;
end process;

-- (b) variable: b gets the NEW value of v, so only one flip-flop
process (clk) is
    variable v : std_logic;
begin
    if rising_edge(clk) then
        v := a;
        b <= v;                           -- v has already changed
    end if;
end process;

Version (a) is a two-stage shift register of the kind Chapter 25 built: a reaches b two clock edges later. Version (b) is a single flip-flop: a reaches b after one edge, and v becomes a wire, not a register. Two characters of source, two different circuits. The rule of thumb is to use signals for anything that connects one part of the design to another, and variables only for short-lived intermediate results inside a single process — a running total in a loop, a concatenation like v in Section 4.

6 Synthesisable Descriptions of Earlier Circuits

With the entity, the process and the two assignment operators in hand, the standard blocks of Parts 4 and 5 can be written down. Each of these compiles, simulates and synthesises as it stands.

The 4-to-1 multiplexer of Chapter 19 is a pure selection, so the selected signal assignment says it in one statement with no process at all.

</> A 4-to-1 multiplexer, dataflow style
library ieee;
use ieee.std_logic_1164.all;

entity mux4 is
    port ( d   : in  std_logic_vector(3 downto 0);
           sel : in  std_logic_vector(1 downto 0);
           y   : out std_logic );
end entity mux4;

architecture rtl of mux4 is
begin
    with sel select
        y <= d(0) when "00",
             d(1) when "01",
             d(2) when "10",
             d(3) when "11",
             'X'  when others;             -- covers 'U', 'X', 'Z', ... exhaustively
end architecture rtl;

The when others is not optional. sel is a two-bit std_logic_vector, so it has \(9^2 = 81\) possible values, not four; without a final catch-all the assignment is incomplete and the tool will infer storage to hold y for the cases you did not mention.

The mod-10 synchronous counter of Chapter 24 needs a register, arithmetic and a terminal count. Arithmetic means converting the bit vector to a numeric type, which is what numeric_std provides.

</> A mod-10 synchronous counter with enable and terminal count
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity counter_mod10 is
    port ( clk, rst, en : in  std_logic;
           q            : out std_logic_vector(3 downto 0);
           tc           : out std_logic );
end entity counter_mod10;

architecture rtl of counter_mod10 is
    signal count : unsigned(3 downto 0);   -- internal, so it can be read back
begin

    process (clk, rst) is
    begin
        if rst = '1' then
            count <= (others => '0');
        elsif rising_edge(clk) then
            if en = '1' then
                if count = 9 then
                    count <= (others => '0');   -- 9 -> 0, the mod-10 wrap
                else
                    count <= count + 1;
                end if;
            end if;
        end if;
    end process;

    q  <= std_logic_vector(count);
    tc <= '1' when (count = 9 and en = '1') else '0';

end architecture rtl;
1 Worked Example 30.1 — Reading the counter as hardware

Four questions answer themselves if the description is read as a circuit rather than as a program.

  • How many flip-flops? Four, one per bit of count, because count is the only signal assigned inside the edge branch. The sequence 0–9 needs \(\lceil \log_2 10 \rceil = 4\), so nothing is wasted; the six unused states 10–15 are entered only from an illegal power-up value, and the count = 9 test rather than count > 9 is what makes the counter recover — from state 10 it counts up to 15 and rolls to 0. Chapter 24 called this self-starting behaviour.
  • Why is count a signal and not the port q? Because the next-state logic must read the present count, and an out port cannot be read. The last-but-one line copies the internal register to the port and costs no hardware.
  • Where is the combinational logic? In the comparison against 9 and the incrementer, both of which the synthesiser builds from the adders of Chapter 16 and feeds to the flip-flops' D inputs. This is exactly the excitation logic that Chapter 24's design procedure produced by hand from a state table.
  • What does tc cost? One four-input AND-type function of count and en, combinational and unregistered. Because it depends on en, it is a Mealy output in the sense of Chapter 26, and it will glitch; registering it would remove the glitch at the price of one clock of delay.

Dividing a 1 MHz clock with this counter gives 100 kHz on the carry, and cascading two of them gives the familiar decade chain.

7 Test Benches, and the Latch You Did Not Ask For

A design is not finished when it compiles. A test bench is a second VHDL file that instantiates the design, drives its inputs and checks its outputs, and it is the one place where the non-synthesisable half of the language belongs. A test bench has no ports at all: it is a closed system, with nothing outside it to connect to.

</> A test bench in outline
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_full_adder is
end entity tb_full_adder;                  -- no ports: nothing is outside

architecture sim of tb_full_adder is
    signal a, b, cin, sum, cout : std_logic;
begin

    uut : entity work.full_adder(dataflow) -- unit under test
        port map (a => a, b => b, cin => cin, sum => sum, cout => cout);

    stimulus : process is
        variable v : std_logic_vector(2 downto 0);
    begin
        for i in 0 to 7 loop               -- all eight input combinations
            v := std_logic_vector(to_unsigned(i, 3));
            a <= v(2);  b <= v(1);  cin <= v(0);
            wait for 10 ns;                -- simulation only, never synthesised
            assert sum = (v(2) xor v(1) xor v(0))
                report "wrong sum for input " & integer'image(i)
                severity error;
        end loop;
        wait;                              -- suspend for ever: run complete
    end process;

end architecture sim;

Three things make it a test bench rather than a design. The wait for statements create time out of nothing, which no circuit can do. The assert statements check the result automatically, so the test either passes or prints a message — far better than staring at waveforms, and the only approach that scales past a handful of vectors. And the final bare wait suspends the process for ever, which is how a simulation stops; without it the loop would restart and run until the simulator was killed. For a sequential design a second process generates the clock, and the stimulus process waits on clock edges rather than on absolute times.

Now the error that catches everyone. VHDL insists that a signal keeps its value until something assigns a new one. If a combinational description leaves any path along which a signal is not assigned, the synthesiser has no choice: it must build something that remembers the old value, and that something is a transparent D latch. Nobody asks for it and the tool rarely refuses; it issues a warning, in a log full of warnings, and carries on.

</> The three ways to infer a latch by accident
-- (1) an if with no else
process (sel, a) is
begin
    if sel = '1' then
        y <= a;                            -- what is y when sel = '0'?  It holds.
    end if;
end process;

-- (2) an incomplete case
process (op, a, b) is
begin
    case op is
        when "00"   => y <= a;
        when "01"   => y <= b;             -- "10" and "11" unmentioned: y holds
        when others => null;               -- 'null' assigns nothing at all
    end case;
end process;

-- (3) only some outputs assigned on some branches
process (sel, a, b) is
begin
    if sel = '1' then
        y <= a;  z <= b;
    else
        y <= b;                            -- z not assigned here, so z latches
    end if;
end process;
if sel = '1' then y <= a; end if; y unassigned when sel = '0' → it must remember D latch transparent while G = 1, holds when G = 0 a D sel G y the feedback you never asked for Level-sensitive, so it passes glitches, and static timing analysis cannot check a path through it against any clock. if sel = '1' then y <= a; else y <= b; end if; every path assigns y → no storage needed 2:1 a b sel y Purely combinational: one LUT input on an FPGA, and a path the timing analyser can measure end to end.
Figure 30.3 — The same signal, with and without an else: a latch or a multiplexer

Why it matters is more than tidiness. A latch is level-sensitive, so it passes any glitch arriving while it is transparent — the static hazards of Chapter 11 go straight through. It is not clocked, so the static timing analysis of Chapter 28 cannot check a path through it against any clock period and will report the path as unconstrained. On an FPGA it wastes a flip-flop that was there anyway. And in a design that was meant to be entirely synchronous, it is a piece of asynchronous logic hiding in the middle of it.

The cures are simple and worth making habits. Give every if an else. End every case with when others, and assign real values there rather than null. Better still, assign default values to every output at the top of the process, before the if, so that every path assigns everything whatever happens afterwards. And read the synthesis report: if it says "inferred latch for signal y" and you did not want a latch, the description is wrong, however well it simulates.

8 Summary and Key Results

Chapter 30 — the constructs, and what each one becomes in silicon
ConstructWhat it meansWhat the synthesiser builds
entity … port(…)The interface: names, directions and types of the portsThe block's pins; nothing inside
Port modes in / out / inoutRead only / written only / bidirectionalAn input pin, an output pin, or a tri-state pin
architectureOne possible body for an entity; several may existWhichever body the configuration selects
Concurrent assignment <=A permanent connection; order of statements is irrelevantCombinational gates
process (a, b, c)Sequential statements re-run when a listed signal changesCombinational logic if every read signal is listed and every path assigns
rising_edge(clk) in a processEverything in the branch happens at the clock edgeD flip-flops, one per assigned signal
Signal <= vs variable :=Scheduled update vs immediate updateTwo flip-flops in series vs one — the same two lines give different circuits
std_logicNine values: strength × level, plus 'U' and '-'Only '0', '1', 'Z' and '-' survive synthesis
Incomplete if or caseSome path leaves a signal unassignedA transparent D latch, unclocked and unwanted

9 Common Mistakes

! Leaving a signal unassigned on some path

An if with no else, a case without when others, or a branch that assigns two outputs where another assigns one — each of them means the signal must keep its old value, and the only hardware that keeps a value is a latch. Assign default values to every output at the top of the process, so that whatever the branches do afterwards, no path can leave anything undriven.

! An incomplete sensitivity list

Omitting a signal that the process reads makes the simulator hold the output when that signal changes, so the simulation shows a latch that the synthesised hardware does not have — or, worse, hides a genuine error. The synthesiser ignores the list entirely and infers from what is read, so simulation and silicon then disagree. List every signal read, or use process (all) where VHDL-2008 is available.

! Treating <code>&lt;=</code> as though it copied a value at once

A signal assignment is scheduled, not immediate, so a statement later in the same process still sees the old value. Writing s <= a; b <= s; inside a clocked process gives two flip-flops in series, not one, because b receives the previous s. If you want the new value immediately, use a variable and :=. This is not a quirk of the language: it is what a wire driven by a gate actually does.

10 Chapter Review

  1. 1. Write the entity for a 4-bit magnitude comparator with inputs A and B and outputs gt, eq and lt, and say why the outputs should not be declared with mode buffer even though the internal logic might want to read them.

    The entity is entity comparator4 is port ( a, b : in std_logic_vector(3 downto 0); gt, eq, lt : out std_logic ); end entity comparator4;, preceded by library ieee; use ieee.std_logic_1164.all;. Mode buffer would make the outputs readable inside, but it changes the port's kind, and any entity that instantiates this one must then connect a buffer-mode signal to it, which propagates the restriction upwards and breaks reuse. The portable idiom is to declare internal signals — signal gt_i, eq_i, lt_i : std_logic; — compute and read those freely, and end the architecture with gt <= gt_i; eq <= eq_i; lt <= lt_i;, which costs no hardware.

  2. 2. Describe a 2-to-4 decoder with an active-high enable in behavioural style, and explain what must be present to keep the description free of latches.

    A process sensitive to the address and the enable, with a default assignment first: process (en, a) is begin y <= "0000"; if en = '1' then case a is when "00" => y <= "0001"; when "01" => y <= "0010"; when "10" => y <= "0100"; when others => y <= "1000"; end case; end if; end process;. Two things keep it combinational. The sensitivity list names both signals the process reads, so the simulation matches the hardware. And the default y <= "0000"; before the if guarantees that y is assigned on every path, including the one where en is '0' — so no branch can require the old value to be remembered, and no latch is inferred. The when others is also needed, since a two-bit std_logic_vector has 81 possible values, not four.

  3. 3. What is the difference between the two processes below, in flip-flop count and in behaviour? (a) if rising_edge(clk) then x <= d; y <= x; end if; (b) variable v : std_logic; … if rising_edge(clk) then v := d; y <= v; end if;

    In (a) both x and y are signals, so both assignments are scheduled and neither takes effect until the process suspends. The second statement therefore reads the value x had before this clock edge, and d reaches y two edges after it is applied: two flip-flops in series, a two-stage shift register of the kind Chapter 25 describes. In (b) v is a variable, so v := d takes effect at once and the next line reads the new value; d reaches y after one edge and v becomes a wire rather than a register — one flip-flop in total. The source differs by two characters and the circuits differ by a whole register and a clock cycle of latency.

  4. 4. A student writes process (sel) begin if sel = '1' then y <= a; else y <= b; end if; end process;. What is wrong, what does the simulation show, and what does the synthesiser build?

    The sensitivity list is incomplete: the process reads a and b as well as sel, but only sel wakes it. In simulation the process re-runs only when sel changes, so a change in a while sel is '1' does not reach y — the output appears to be latched on sel. The synthesiser ignores sensitivity lists and looks at what the process reads, and since every path assigns y it builds a plain 2-to-1 multiplexer with no storage. Simulation and hardware therefore disagree, which is the dangerous case: the design may well be correct and the verification wrong, or the reverse, and nothing in the flow flags it. The fix is process (sel, a, b), or process (all) in VHDL-2008.

  5. 5. Take the mod-10 counter of Section 6 and say what changes are needed to make it a mod-12 up/down counter with a synchronous reset. How many flip-flops does it need, and how would you verify it?

    Four changes. The reset test moves inside the edge branch, so it becomes if rising_edge(clk) then if rst = '1' then count <= (others => '0'); elsif en = '1' then …, and rst leaves the sensitivity list, which becomes process (clk) alone. A direction input up is added to the entity. The counting branch tests both ends: counting up, wrap from 11 to 0; counting down, wrap from 0 to 11. And the terminal count becomes tc <= '1' when (up = '1' and count = 11) or (up = '0' and count = 0) else '0';. It still needs four flip-flops, since \(\lceil \log_2 12 \rceil = 4\), with four unused states 12–15 from which the wrap tests must recover. Verification: a test bench with a clock process, a stimulus process that releases reset, counts up through at least fourteen edges to see the wrap at 11, then asserts up = '0' and counts back through zero, with assert statements comparing q against an expected sequence computed in the test bench rather than read off a waveform.