assign and always @(*) — and recognize the single most common bug in
combinational Verilog: accidentally describing a latch.
Before writing expressions you need Verilog's literal syntax: <size>'<radix><value>.
4'b1010 // 4 bits, binary: 1010
8'hFF // 8 bits, hex: 11111111
3'd5 // 3 bits, decimal: 101
1'b1 // 1 bit: 1
Size is in bits, not digits. Radix is b (binary), h (hex), d
(decimal), or o (octal). Omitting the size (just 'h1F) is legal but leaves the
width up to context — always size your literals explicitly once you're inside real logic; it avoids
a class of subtle width-mismatch bugs.
assign: a standing relationship, not a one-time computation
The clearest analogy for a CS background: assign is like a spreadsheet formula, not an
assignment statement. =B2+B3 in a spreadsheet cell doesn't compute once — it silently
recomputes forever, every time B2 or B3 changes. assign works
identically:
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (cin & (a ^ b));
These two lines describe two pieces of gate-level hardware (an adder's sum and carry logic) that are
permanently, continuously live. Change a at any moment, in simulation or in real silicon,
and sum and cout update immediately (in a real circuit, after gate delay; in
simulation, in the same time step). There's no "running" assign — it never finishes and
never re-executes, because it never stopped.
| Category | Operators | Notes |
|---|---|---|
| Bitwise | & | ^ ~ ^~ | Per-bit, on vectors of any width. |
| Logical | && || ! | Treat the whole operand as one boolean;
result is always 1 bit. Use in conditions (if), not to combine bit-vectors. |
| Reduction | &v |v ^v | Unary prefix — collapses every bit of
vector v into one bit. &v is "AND of all bits" (useful for "are all bits
1?"). |
| Relational | == != < > <= >= | Result is 1 bit (or
X if either side has an unknown bit). |
| Shift | << >> | Logical shift; fills with 0. |
| Concatenation | {a, b} | Glues signals into a wider vector, MSB-first. |
| Replication | {4{1'b0}} | Repeats a pattern — here, four zero bits (0000). |
| Ternary | cond ? a : b | A multiplexer, spelled as an expression. |
a = 4'b0110 and b = 4'b0011: a & b is
4'b0010 (per-bit AND). a && b is 1'b1 (both operands are
"truthy" — nonzero — so the logical AND is true). Mixing these up is a common source of quietly-wrong
width-1 results where you meant a full bitwise operation.
From Lesson 2's mux2: assign y = sel ? b : a; is exactly a 2-to-1 multiplexer —
"if sel, drive y continuously from b; otherwise from
a." No branch is "taken" in the software sense; both possibilities are wired in, and
sel continuously steers which one reaches the output.
See it as one file: code/mux2to1.v (source also in code/mux2to1.v).
always @(*): the same hardware, with if/case
assign gets awkward once logic needs more branches than a ternary comfortably expresses.
always @(*) describes the exact same category of hardware — purely combinational, no
memory — but lets you use familiar control-flow keywords as a way of describing a circuit, not
as a runtime sequence:
module mux4 (
input wire [1:0] sel,
input wire [7:0] in0, in1, in2, in3,
output reg [7:0] y
);
always @(*) begin
case (sel)
2'd0: y = in0;
2'd1: y = in1;
2'd2: y = in2;
2'd3: y = in3;
endcase
end
endmodule
Three things to notice, because each one is a rule, not a style choice:
y is declared reg — recall from Lesson 2 that any signal
assigned inside an always or initial block must be a reg, even
though (as you're about to confirm) this circuit has no actual memory.@(*) means "sensitive to everything read inside this block." The
simulator/synthesis tool automatically figures out that this block should re-evaluate whenever
sel, in0, in1, in2, or in3 changes —
you never list them by hand. Old code you'll encounter lists signals explicitly
(always @(sel or in0 or in1 or in2 or in3)); it's easy to forget one and get a subtly
wrong simulation. Always use @(*) for combinational logic.= (blocking), not <=. You'll meet
<= in Lesson 4 for sequential logic. The short version, justified properly next
lesson: combinational always blocks use =.
A reg retains its last value until told otherwise — that's the whole point of the keyword.
Inside a clocked block (Lesson 4) that's exactly what you want. Inside a combinational
always @(*) block, it's a trap: if there's some input combination for which your
if or case doesn't assign the output at all, the tool has only one honest way
to satisfy "hold the last value" — insert a real latch (unwanted memory) to remember it. You've
accidentally described sequential hardware while trying to describe combinational hardware.
always @(*) begin
if (enable)
y = data;
// no else —
// what is y when
// enable is 0?
end
always @(*) begin
if (enable)
y = data;
else
y = 8'b0;
end
The same rule applies to case: every branch of the case must assign the output, or you
need a default that does. The fix is always the same shape — every signal assigned
in the block must be assigned on every possible path through it.
always @(*) block, why does an incomplete if (no matching else) on a reg output cause a latch to be inferred?full_adder module you started in Lesson 2 using two assign
statements: sum = a ^ b ^ cin and cout = (a & b) | (cin & (a ^ b)).
Then rewrite it a second way using always @(*) with plain Boolean expressions assigned to
reg outputs, and convince yourself both versions describe identical hardware — the
assign/always@(*) choice is style, not substance, as long as every output is
assigned on every path.
New terms — combinational logic, latch, sensitivity list, literal / radix — are in the glossary.