Skip to content

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:

where_signals_live.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

signals_wire_the_hierarchy.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

architecture scope showing a signal visible to multiple processes and componentsin scopein scopesame net (stage1)same net (link)architecture rtldeclarative region: signalstage1, valid, linkprocess Adrives stage1process Breads stage1component u_srcdrives linkcomponent u_dstreads link12
A signal's scope is the region of the design that shares its net. An architecture-level signal (here 'link' and 'stage1') is an internal wire visible to every process and component instance in that architecture — they all attach to the same node. A port is the same idea extended to the entity boundary so a parent can connect to it. A process variable is NOT on this picture: it is local sequential storage, invisible as a net, which is why a wire that crosses process or component boundaries must be a signal.

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.

simulation timeline from elaboration through a delta-cycle signal updateElaborationsignal/net createdTime 0 — initialvalue':=' or type'left ('U' forstd_logic)Event wakes a processe.g. rising clock edgeProcess executesvariables update now;signal assignmentsscheduledProcess suspendsscheduled updates pendingDelta cyclesignal takes its new valueFanout wakesreaders of the signalre-evaluate12
The simulation timeline for a signal, from elaboration to its first update. The signal exists before time starts, holds its initial value at t=0, and changes only when a process drives it: the clock edge wakes a process, variables update immediately, signal assignments are scheduled, the process suspends, and one delta later the signal updates and its readers (fanout) wake. This is the same engine every Module 3 lesson returns to.

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 cycles
A declared std_logic signal starts at 'U' and holds it until first drivent=0: 'valid' holds its initial value (here 'U' — no init given)t=0: 'valid' holds its…first time the driving process runs → 'valid' becomes definedfirst time the driving…clkvalidUUU00110t0t1t2t3t4t5t6t7
Before any process drives it, the signal carries its initial value. With no explicit ':=' a std_logic signal is 'U' (uninitialised). Giving 'valid' an explicit ':= '0'' would start it at 0 instead — which matters when downstream logic samples it before reset. The signal's value is never undefined-by-accident; it is always exactly the declared (or default) initial value until a driver acts.

6. 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.

scope_mismatch.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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 := …, a std_logic signal 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.