VLSIChaps Academy · Track 01

Control Flow, if, case, and the Traps

~12 min Hands-on lab 1 exercise Verilator
Page 12 of 50
01

Two Decision Structures

SystemVerilog gives you two ways to make a decision. An if else chain evaluates conditions in order, and the first true condition wins, which makes it the natural tool when conditions are arbitrary or can overlap. A case statement compares one selector against a list of values, one branch per value, which makes it the natural tool for a parallel decode: one input, many defined meanings. The rule of thumb working engineers use: deciding on one selector's value, reach for case; deciding among unrelated or overlapping conditions, reach for the if chain. Choosing the structure that matches the decision is the first habit of readable decision logic.

02

The Latch Returns, In Its Real Form

You met the accidental latch on the combinational logic page: leave any path through the block without assigning the output, and the output must remember its old value, so the tool infers storage you never intended. Here is the form that bug actually takes in real code. Real decision blocks drive several outputs at once, and the trap is no longer a missing branch, it is a present branch that assigns two outputs and quietly forgets the third. Every branch exists, the code looks complete, and one output on one path still grows a latch. The defence is an idiom you will use for the rest of your career: assign every output a safe default value in the first lines of the block, before any decision, then override per branch. Do that, and no branch can ever leave an output unassigned, because every output was assigned before the branching began.

03

Unique and Priority, Declaring Your Intent

A case can carry a qualifier that turns your assumptions into checked claims. Marking a case unique asserts two things: exactly one branch matches any value that arrives, no overlaps, and nothing falls through unmatched. Marking it priority asserts that overlaps are deliberate and the first matching branch is meant to win, while still claiming full coverage. These qualifiers matter because they move your intent out of your head and into the code, where tools can check it and colleagues can read it. A case with no qualifier makes no claims; a qualified case is a small contract, and tools are entitled to complain when the contract is broken.

04

Key Insight: A Missing Branch Is a Hardware Bug

Two closing traps complete the picture. First, unknowns: an ordinary case matches exactly, so a selector carrying an unknown bit matches none of the value branches and falls through to the default, if you wrote one — the same unknown-bit behavior you saw on Page 03. That is one more reason the default and the assign-first idiom are not optional courtesies. Second, and this is the insight to carry out of the page: an uncovered path in decision logic is not a style issue or a cosmetic gap. It is a functional hardware bug, storage where you meant computation, wrong values that arrive later and get blamed on some other block. The engine on this page treats it exactly the way a professional flow does: it names the latch, and the harness walks every input value to prove nothing was left uncovered.

Lab, Every Output, Every Path

Write the decision logic for a small instruction decoder. A two bit opcode selects one of four operations, and the decoder drives three control outputs. For opcode zero, load: load_en is one, the others zero. For opcode one, store: store_en is one, the others zero. For opcode two, add: alu_en is one, the others zero. For opcode three, halt: all three outputs zero. Use always_comb with a case, and make sure every output is assigned on every path; the safest way is to assign all three outputs zero first, then override per branch. Run and submit. Forget one output in one branch, and the engine will name the latch it inferred and the exact opcode where the stale value showed.

Count the promises. Three outputs times four opcodes is twelve assignments your block must guarantee. You can write all twelve explicitly, or you can make three default assignments at the top and then override only the ones per branch. The second way is shorter, and it is impossible to get wrong by forgetting.
Compiling, linting, and simulating...
Correct. Every output is assigned on every path, so there is nothing for the logic to remember and no latch to infer, and all four opcodes decode exactly to specification. If you used the assign first idiom, you have just learned the pattern that makes this entire class of bug impossible; if you covered every branch explicitly, that works too, and the idiom is there when the block grows.
Not quite. Your decode ran, but a path through the block left an output unassigned, or a decode came out wrong. Where an output was forgotten, it held its previous value, and the engine inferred the latch and the harness caught the stale value at a specific opcode. The engine output and the mentor below name the output, the opcode, and the fix.
Compile error.
Something went wrong reaching the engine. Try again in a moment.
AI Mentor
Thinking...
📲 Join 20K+ Engineers