Every wire you've used so far was a port: declared in the interface, connected to the outside world. Real implementations also need wires of their own: a named midpoint in a computation, one line feeding two different outputs. Those private wires are signals. Declaring one forces two questions you've been able to ignore until now. What can a wire actually carry? And what should the tools do if two statements drive the same one? Both answers live in the type you give the signal.
Remember the oven buzzer from Logic Gates, the one that sounds when the oven is hot and either door is open? Suppose the door condition is now needed twice. The buzzer needs it, and so does a panel lamp that shows "a door is open" even while the oven is cold. Computing it twice would work; naming it once is better:
architecture rtl of oven_buzzer is
signal any_door : std_logic; -- a wire of your own: name + type
begin
any_door <= door_a or door_b; -- the one driver
buzzer <= hot and any_door; -- reader 1
doors <= any_door; -- reader 2
end architecture rtl;
Signals are declared in the architecture's declaration region, between architecture ... is and begin.
Three things to notice:
in and out are contracts about who drives a port; a signal lives entirely inside the box.any_door has one driver and two readers.You've already met four wire values: '0' and '1' from day one, then 'Z' ("nobody is driving") and 'X' ("two drivers are fighting") in Port Directions. That is the everyday working set:
| Value | Meaning | Where you see it |
|---|---|---|
'0', '1' | driven low / high | normal operation |
'Z' | high impedance: no driver connected | released shared wires |
'X' | unknown: conflicting or indeterminate drivers | bus contention, real bugs |
'U' | uninitialized: no driver has acted yet | simulation start |
std_logic carries nine values in total. The remaining four ('W', 'L', 'H', '-') model weak electrical levels and synthesis don't-cares, and you can leave them for much later.
Port Directions called two active drivers on one wire bus contention and showed turn-taking as the discipline. The type system is where that discipline becomes checkable, and it offers two kinds of wire: a refereed kind and a strict kind.
The refereed kind is std_logic: a resolved type.
A built-in rule called the resolution function takes all driver values and computes what the wire shows, every moment:
| Driver A | Driver B | Wire shows |
|---|---|---|
'0' | '0' | '0': agreement |
'1' | '1' | '1': agreement |
'0' | '1' | 'X': a fight |
| anything | 'Z' | the non-'Z' value; 'Z' always loses |
Resolution is exactly what deliberately shared wires need. The tri-state recipe works because the wire is resolved: everyone parks at 'Z', the one active driver wins. This is also the rule you took on faith in Port Directions.
The strict kind is std_ulogic, where the u means unresolved: it has no resolution function, so it refuses the question. Give such a signal two drivers and the tool stops before any simulation runs: GHDL reports several sources for unresolved signal and names the signal for you.
Side by side, the whole choice is one word in the declaration:
signal pad_line : std_logic; -- resolved: multiple drivers get refereed
signal midpoint : std_ulogic; -- strict: a second driver is an error
Why does compile-time versus run-time matter so much here? Look at the table again: an accidental second driver only shows 'X' on inputs where the two drivers disagree. If they happen to agree throughout your test, the waveform looks perfectly healthy and the bug ships. The strict types don't depend on luck: a second driver is an error every time.
TIP
When a mysterious 'X' appears in the waveform, don't hunt it by eye. Flip the suspect signal's declaration to std_ulogic and rebuild. std_logic is the resolved subtype of std_ulogic, so the two connect freely and the change costs nothing (since VHDL-2008 the vector types connect freely too). Elaboration now names the double-driven signal for you.
So the practical split: wires that are meant to be shared (inout pads, tri-state buses) must be the resolved kind. Everything else has one driver, and the strict kind turns wiring mistakes into named compile errors.
VHDL convention here (and in most codebases) is std_logic throughout for consistency, but std_ulogic is always one keystroke away as the bug-catcher. The exercise after this lesson uses it exactly that way.
This lesson ships two examples. Start with Naming the Midpoint in the panel on the right: the oven buzzer above, complete. In the waveform, find any_door sitting between the inputs and outputs: an internal signal, displayed like any port. Watch it rise whenever either door opens, then watch its two readers split: doors copies it every time, while buzzer follows it only while hot is high.
Then switch the panel to Resolution in Action: two pad drivers wired to one shared, resolved line. Unlike the disciplined turn-taking of Port Directions, this testbench breaks the rules on purpose. Follow shared_line through the phases. It reads 'Z' while nobody drives, then '1' while card A drives alone. It stays a clean '1' when card B starts driving '1' too: agreement, and the waveform shows nothing wrong. Then it shows 'X' the instant B flips to '0' against A's '1'. Keep following: the line snaps back to '1' the moment B releases. The fight ends with the fighter: an 'X' is not sticky, and 'Z' still loses to A's '1'. Finally the line parks at 'Z' once A lets go too. That trace is the resolution table, row by row, in copper.
std_logic referees multiple drivers: agreement passes, 'Z' always loses, a fight reads 'X'. Deliberately shared wires must be resolved.std_ulogic rejects a second driver before simulation starts. Let the type system find what a lucky waveform can hide.sync-content --examples to generate.