VHDL · Chapter 3.3 · Signals & Hardware Behavior
Declaring Signals and Their Scope
Where you declare a signal is not just bookkeeping; it decides which wire the signal is and who can connect to it. A signal declared in an architecture is an internal net visible to every process and component in that architecture, a port is a net that reaches the block boundary, and, crucially, you cannot declare a signal inside a process, because a process is sequential code rather than a place where wires live. This lesson treats a signal's scope as its connectivity domain, the set of things that can see and drive the same physical net. It also pins down why every signal powers up at its type's initial value, which is U for standard-logic signals, how that shows up at time zero in simulation, and the scope mistakes that split one intended connection into separate, unconnected nets.
Foundation14 min readVHDLSignalsScopeArchitectureInitial ValuesConnectivity
1. Engineering intuition — declaration is connectivity
A signal is a wire, and a wire exists somewhere in the design hierarchy. When you declare a signal in an architecture, you are saying "this net exists inside this block, and everything in this block can attach to it." Scope is therefore not a software notion of variable visibility — it is which parts of the circuit are electrically on the same net.
Two processes that reference the same architecture signal are wired to the same node. Two identically-named signals declared in two different architectures are two different wires that happen to share a name — exactly like two separate nets on a schematic.
2. Formal explanation — where signals may be declared
A signal declaration has the form signal name : type [:= initial]; and is legal in a few
places, each with a defined scope:
entity datapath is
port ( clk : in std_logic; -- ports ARE signals, scope = whole architecture
din : in std_logic_vector(7 downto 0);
dout : out std_logic_vector(7 downto 0) );
end entity;
architecture rtl of datapath is
signal stage1 : std_logic_vector(7 downto 0); -- internal net, default init
signal valid : std_logic := '0'; -- internal net, explicit init
begin
-- every process / concurrent statement below can read and (if it drives) write
-- stage1 and valid, and can read the ports. They share these exact nets.
end architecture;Key rules: signals are declared in the architecture declarative region (between architecture … is and begin), or in a block/package. Ports are signals with architecture-wide
scope. You cannot declare a signal inside a process — a process may only declare variables
(lesson 3.1), because a process is sequential behaviour, not a wire-bearing region.
3. Production-quality RTL — scope as the wiring plan
Scope is how you wire components and processes together. A signal declared in the parent architecture becomes the net between two sub-blocks:
architecture struct of top is
signal link : std_logic_vector(7 downto 0); -- the wire BETWEEN the two blocks
begin
u_src : entity work.producer port map (clk => clk, data => link); -- drives link
u_dst : entity work.consumer port map (clk => clk, data => link); -- reads link
-- 'link' has architecture scope, so both instances bind to the same physical net.
end architecture;What hardware does this become? link is a real 8-bit bus routed from producer's output
port to consumer's input port. Its declaration location (the top architecture) is the level
of hierarchy at which that bus physically exists.
4. Hardware interpretation — scope is the net's home in the hierarchy
Each signal maps to a net at the hierarchy level where it is declared:
5. Simulation interpretation — existence and the time-zero value
In simulation a signal is created during elaboration (before time starts) and is given an
initial value at time zero: the explicit := … if present, otherwise the type's leftmost
value — 'U' for std_logic/std_ulogic (lesson 2.2), which deliberately means
"uninitialised." The signal then holds that value until a driver gives it a new one.
The time-zero initial value is visible on a waveform: a std_logic net sits at 'U' until its
first driven value arrives.
A declared std_logic signal starts at 'U' and holds it until first driven
8 cycles6. Debugging example — the two-nets-that-should-be-one bug
Expected: a status flag set in one process is read in another. Observed: the reader never sees the change; in the waveform the two "same" signals differ. Root cause: the flag was declared in the wrong scope — e.g. as a process variable, or a second same-named signal was declared in a nested block — so the writer and reader are on different nets. Fix: declare a single signal at the architecture level both processes share. Engineering takeaway: when a value mysteriously fails to propagate, check that writer and reader actually reference the same declaration — a name match is not a net match.
-- WRONG: each process tries to use its own local notion of 'busy'
-- process A: variable busy ... (invisible outside A)
-- process B reads a signal 'busy' that A never drives → stuck at init.
-- RIGHT: one architecture-level signal, driven by A, read by B:
architecture rtl of ctrl is
signal busy : std_logic := '0'; -- single shared net, sane initial value
begin
proc_a : process(clk) begin if rising_edge(clk) then busy <= start; end if; end process;
proc_b : process(clk) begin if rising_edge(clk) then gate <= busy; end if; end process;
end architecture;7. Common mistakes & what to watch for
- Trying to declare a signal inside a process. Processes declare variables only; signals belong to the architecture/block. A wire that crosses processes must be an architecture signal.
- Name match mistaken for a connection. Same-named signals in different scopes are different nets. Connection requires a shared declaration (or an explicit port map).
- Forgetting the initial value. With no
:= …, astd_logicsignal is'U'; logic that samples it before reset sees'U', which propagates as'X'. - Shadowing a port with a local signal. Declaring a signal with a port's name hides the port and breaks the intended boundary connection.
- Reaching for shared variables to "share" data. Shared variables bypass the signal/driver model and invite races; use signals for anything multiple processes touch.
8. Engineering insight
Treat declaration as floor-planning the nets. The level of hierarchy where a signal is
declared is the level where that wire physically exists, and its scope is precisely the set of
processes and instances electrically tied to it. This reframing makes connectivity bugs obvious
(writer and reader must share one declaration) and makes the initial value a deliberate design
input rather than an afterthought — you choose 'U' to catch unreset logic, or an explicit
reset value to define power-on state.
9. Summary
Signals are declared in the architecture declarative region (or blocks/packages); ports are
signals too; processes cannot declare signals. A signal's scope is its connectivity domain — the
parts of the circuit on the same net — so a connection requires a shared declaration, not just a
shared name. Every signal exists from elaboration and holds its initial value (explicit := or
the type's leftmost, 'U' for std_logic) until a driver changes it.
10. Learning continuity
You now know where a signal lives and that its value starts from a defined initial state and changes only when something drives it — one delta after the driving process runs. The next lesson zooms into that step: Delta Cycles and the Simulation Engine explains exactly how the simulator orders zero-delay updates so the timeline above is deterministic and race-free. After that, the driver model details what actually holds and projects a signal's value, which — together with this lesson's scope — sets up multiple drivers and resolution.