Day 3 · Procedural Combinational Logic

The always @(*) Block

Video 1 of 4 · ~12 minutes

Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF

always @(*)if/else & caseLatch ProblemBlock vs Nonblock

🌍 Where This Lives

In Industry

Procedural combinational blocks are where the decision logic of every digital system lives. ALU opcode decoders, address decoders, instruction decoders, bus arbiters — all always @(*) blocks with case statements or nested ifs. Read any CPU's RTL and half of it looks like today's material.

In This Course

Day 3 ALU. Day 7 FSM output logic (Block 3). Day 11 UART state-to-output. Day 14 assertion fire-logic. Every remaining day builds on always @(*). Get the syntax clean now and you're set for the rest of the course.

Career tag: Clean, latch-free combinational RTL is the single most-reviewed code type in any FPGA/ASIC shop. This video is literally the style guide senior engineers read your code against.

⚠️ always Is Not a Loop

❌ Wrong Model

always sounds like while(true). The block runs forever, executing its statements top-to-bottom, over and over.”

✓ Right Model

An always @(*) block describes a piece of combinational logic that re-evaluates whenever any input changes. The statements inside aren't “executed” — they're analyzed once by the synthesizer, producing a fixed chunk of gates that exist simultaneously on the chip.

Consequence: Writing for or while loops inside always @(*) doesn't mean runtime iteration — it means the synthesizer unrolls the loop and builds parallel copies of the loop body as hardware.

Why Procedural Blocks Exist

assign works great for simple expressions:

assign y = (sel == 2'b00) ? a :
           (sel == 2'b01) ? b :
           (sel == 2'b10) ? c : d;   // 4:1 mux — nested ternary gets unreadable fast

But multi-way decisions become unreadable. The same logic with always @(*):

always @(*) begin
    case (sel)
        2'b00: y = a;
        2'b01: y = b;
        2'b10: y = c;
        2'b11: y = d;
    endcase
end
Same hardware. Much clearer intent. always @(*) gives us if/else, case, and procedural constructs for complex combinational logic.

Sensitivity Lists: Use @(*)

// Manual list — DON'T do this
always @(a or b or sel)
    if (sel) y = a; else y = b;
// Wildcard — ALWAYS do this for combinational
always @(*)
    if (sel) y = a; else y = b;
Forget a signal in a manual list: simulation won't update when that signal changes, but synthesis will still build the logic correctly. Your simulation silently disagrees with your hardware. Use @(*) — no exceptions.

👁️ I Do — The Combinational Pattern

reg [3:0] r_out;        // declared reg (assigned in always)

always @(*) begin        // wildcard sensitivity
    r_out = 4'b0000;     // ← DEFAULT at top — prevents latches
    if (sel)
        r_out = i_data;
end
My thinking: Four rules, every time: (1) reg declaration, (2) always @(*), (3) default assignment first, (4) begin/end for multi-line. The default at top is what keeps you out of latch territory — we prove why in Video 3.

🤝 We Do — Spot the Problem

// What's wrong here?
always @(posedge sel) begin
    y = a & b;
end

always @(a, b)
    y = a | b;

always @(*) begin
    if (sel) y = a;
    // where's the else?
end
Answers: (1) posedge sel makes this sequential — infers a flip-flop on a level signal, disaster for combinational intent. (2) Manual list is fine here (both sigs listed), but use @(*) for consistency. (3) Missing else → “hold value when sel=0” → inferred latch. We dive into this in Video 3.

🧪 You Do — Predict the Hardware

reg [3:0] r_result;
always @(*) begin
    r_result = a;
    if (sel_b) r_result = b;
    if (sel_c) r_result = c;
    if (sel_d) r_result = d;
end

Predict:

  1. What hardware does Yosys build? (Flops? LUTs? Mux tree?)
  2. If sel_b = sel_c = 1 and others 0, what's the output?
  3. Is this priority-encoded or parallel?
Answers: (1) Three 2:1 muxes in series (a priority chain) — no flops. (2) c — the later if overrides the earlier one. (3) Priority — later statements win when multiple are true, in reverse of if/else if. This is exactly why we prefer case or if/else if for clarity.
▶ LIVE DEMO

always @(*) vs assign — Same Hardware

~4 minutes

▸ COMMANDS

cd labs/week1_day03/ex_mux_compare/
make sim      # test both versions
make wave     # GTKWave
# Both modules: identical
diff <(yosys -qp "read_verilog mux_assign.v; \
        synth_ice40; stat" 2>&1) \
     <(yosys -qp "read_verilog mux_always.v; \
        synth_ice40; stat" 2>&1)

▸ EXPECTED OUTPUT

PASS: all 8 mux tests
=== 8 passed, 0 failed ===

# diff output:
(empty — identical LUT counts)

Both: 4 SB_LUT4, 0 SB_DFF

▸ KEY OBSERVATION

Two completely different code styles synthesize to exactly the same hardware. Use whichever is clearer for the problem. assign for one-liners, always @(*) for multi-way decisions.

🔧 What Did the Tool Build?

A 4-bit 4:1 mux written with always @(*) and case:

$ yosys -p "read_verilog mux4_always.v; synth_ice40 -top mux4; stat" -q

=== mux4 ===
   Number of wires:                  5
   Number of cells:                  4
     SB_DFF                          0     ← still no flops (reg doesn't mean flop!)
     SB_LUT4                         4     ← one LUT per output bit
Re-confirm: reg [3:0] r_y + always @(*) + case → zero flops. The reg keyword is syntactic: whether it becomes a flip-flop depends entirely on the sensitivity list. No posedge clk, no flops.

🤖 Check the Machine

Ask AI: “Rewrite this assign-based mux as an always @(*) block. Will it synthesize to the same hardware?”

TASK

Ask AI to translate assign→always. Ask about hardware.

BEFORE

Predict: same hardware, different syntax. Good AI says “identical LUT count.”

AFTER

Some models incorrectly say always blocks “add latency” or “need a clock” — false.

TAKEAWAY

If AI says always adds latency, it's confused between @(*) and @(posedge).

Key Takeaways

always @(*) for combinational — never manual sensitivity lists.

reg in always @(*) is still combinational.

 Default assignment at the top prevents latches.

 Always use begin/end for multi-line blocks.

always is not a loop. It's a description of a chunk of gates.

🔗 Transfer

if/else and case

Video 2 of 4 · ~14 minutes

▸ WHY THIS MATTERS NEXT

Now that you have the container (always @(*)), we need the decision constructs that go inside: if/else, case, and casez. Here's the subtlety: if/else synthesizes to a priority chain, case to a parallel mux. Same abstract logic, different gates, different timing. Video 2 shows you when to use which.