Digital Design / Module 3: Verilog that means hardware
14. assign and always blocks
Pick the right construct for each kind of logic.
See it move
The idea
There are three common ways to describe hardware, and each has a clear purpose.
| Construct | Describes | Assignment |
|---|---|---|
assign | Simple combinational logic | = |
always @(*) or always_comb | Larger combinational logic | = |
always @(posedge clk) or always_ff | <= |
assign
An assign is a permanent connection. The left side follows the right side whenever it changes. This is the simplest form, and it suits one-line expressions.
always blocks
For logic with case or several conditions, an always block reads better. @(*) means "re-evaluate whenever any input changes", so the block behaves as combinational logic.
// The same 2:1 mux, written two ways
assign y = sel ? b : a;
always_comb begin
if (sel) y = b;
else y = a;
endWhy always_comb and always_ff are better
They state your intent, and tools check it. Accidentally create a inside always_comb, and you get a warning or an error. A plain always @(*) accepts it silently. always_comb also runs once at time zero, which avoids some start-up simulation mismatches. Use these forms whenever your tools support SystemVerilog.
Sequential logic
always_ff @(posedge clk)
q <= d;This describes a flop for each bit of q. Everything inside happens at the clock edge.
Common mistake
Writing a sensitivity list by hand, such as always @(a) when the logic also reads b. Simulation won't react to b, whereas the synthesized gates will. The two then disagree, and it is painful to debug. Use @(*) or always_comb.
Key ideas
- assign is continuous.
- always @(*) describes combinational logic; always @(posedge clk) describes registers.
- SystemVerilog always_comb and always_ff state your intent.
Your exercise
Find the bug
This code has one deliberate bug. Read the symptom, then click the line you think is wrong.
The fix
always @(*) y = a & b;
Why it failed
A hand-written sensitivity list tells the simulator when to re-run the block. b isn't listed, so a change on b does nothing in simulation, whereas real gates always respond. Simulation and hardware now disagree. @(*) or always_comb includes every input automatically.
Interview questions
coreWhy prefer always_comb over always @(*)?
It checks intent, handles time-zero evaluation, and tools can flag a latch.
basicWhich construct would you use for a one-line expression, a decode written with case, and a register?
A one-line expression suits assign. A decode with case needs an always @(*) block (always_comb in SystemVerilog), because case cannot appear in an assign. A register uses always @(posedge clk) with non-blocking assignments (always_ff in SystemVerilog).
basicA 2:1 mux is written once with assign and once with always @(*) and if-else. Is the hardware different?
No. Both describe the same combinational mux: the output follows the selected input whenever any input changes. The choice is about readability; always blocks are clearer when the logic has several conditions or a case.
coreWhat hardware do these two blocks describe?
always @(*) y = a & b; always @(posedge clk) q <= a & b;
The first is just an AND gate, since it re-evaluates whenever a or b changes. The second puts the same AND gate in front of a flop, so q only takes the new value at the rising clock edge. The logic expression is the same; what wakes the block decides whether there is a register.
coreThis block reads b but has only a in its sensitivity list. What goes wrong?
always @(a) y = a & b;
The simulator only re-runs the block when a changes, so when b changes, y keeps its stale value until a toggles. Synthesis ignores the list and builds a normal AND gate, so the gates and the simulation disagree. Use @(*), or always_comb in SystemVerilog, so every input is included.
coreWhy should a signal not be assigned from two different always blocks?
Each always block describes its own driver, so two blocks mean two drivers for one signal. In hardware that is a conflict with no defined winner, and synthesis tools typically reject or flag it. In simulation, the last block to run wins, which depends on scheduling, so the result can differ between simulators. Put all assignments to a signal in one block.
coreHow does adding posedge rst to the event list change this flop?
always @(posedge clk or posedge rst) if (rst) q <= 1'b0; else q <= d;
With rst in the list, the block wakes on a rising rst as well as on the clock, so q clears immediately, without waiting for a clock edge. That is an asynchronous reset, and the if (rst) must be tested first so reset takes priority. Without rst in the list, the same code would describe a synchronous reset that acts only at a clock edge.
advancedWhat is the value of k in simulation, and why?
reg k; always @(*) k = 1'b1;
k stays x. The block reads no signals, so @(*) has nothing to wait for and the block never wakes up; Icarus Verilog, for one, warns about exactly this. In hardware it is just a constant 1. SystemVerilog's always_comb runs once at time zero, which avoids this kind of start-up mismatch; in plain Verilog, use assign k = 1'b1;.
advancedWhy does a hand-written sensitivity list cause bugs that @(*) avoids, even when the list is complete on the day it is written?
A manual list is a second copy of information already in the code. When someone later adds a new input to the logic and forgets the list, simulation stops reacting to that input while synthesis still builds the full logic, and nothing reports the mismatch. @(*) derives the list from the body, so it stays correct as the code changes.