RTL Design and SystemVerilog Practice
July 31, 2026·97 min read·advanced
"Experience in Verilog or VHDL" is a minimum qualification on CPU design roles, and an ASIC design role says "Develop RTL using SystemVerilog." That word minimum matters. Nobody is going to hire you because…
01.Part 1, the mental shift
1.1 The one sentence that everything else hangs from
"Experience in Verilog or VHDL" is a minimum qualification on CPU design roles, and an ASIC design role says "Develop RTL using SystemVerilog." That word minimum matters. Nobody is going to hire you because your SystemVerilog is beautiful. They will absolutely decline to hire you if it has a hole in it. So the goal in this note is no gaps, not brilliance, and the way to get there is to understand why each rule exists rather than memorizing the rule.
Here is the sentence. You are describing hardware, not writing a program.
That sounds like a slogan until you see what it costs to forget it. Consider this line.
| always_ff @(posedge clk) count <= count + 1'b1; | |
| ```text | |
| A software reader parses that as "every clock, add one to count." That reading is wrong in a way that will eventually cost you a bug. The correct reading is "**there exists**, permanently, a register called `count`, and permanently wired to its data input is an incrementer whose input is the register's own output." The hardware is not something that happens at each clock. The hardware is a physical object sitting on silicon at all times. Charge sits on the register's internal nodes whether or not a clock arrives. The incrementer's transistors are switching whenever `count` changes, drawing current, taking picoseconds to settle. The clock edge does exactly one thing, which is to tell the register to sample whatever the incrementer is currently presenting. | |
| Read the line that way and a whole family of questions answers itself. Why can two `always_ff` blocks not both assign `count`? Because that would be two different pieces of logic wired to the same physical data pin, which is a short circuit. Why does the order of statements inside an `always_comb` block matter but the order of `assign` statements not matter? Because `always_comb` is a procedural description that the tool must first flatten into a function, while `assign` statements are literally wires that all exist simultaneously. Why can there be no `while (x)` loop? Because silicon is a fixed amount of area that exists whether or not you use it, so every structure must be decidable before the chip is built. | |
| ### 1.2 Three tools read the same text and disagree about what it means | |
| This is the source of the entire "simulation and synthesis mismatch" problem in Part 6, so set it up now rather than being surprised by it later. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig01.svg" alt="One source file has three readers, and each of them extracts a different meaning from it, which is where every simulation-to-synthesis mismatch begins." caption="One source file has three readers, and each of them extracts a different meaning from it, which is where every simulation-to-synthesis mismatch begins." id="fig:04-RTL-Design-and-SystemVerilog-1" /> | |
| The simulator is an event-driven program. It maintains a queue of scheduled value changes, pops the earliest, propagates it, and schedules whatever that triggers. It is a very literal reader. If you write `#10`, it waits ten time units. If a signal is unknown it carries the value `X` and applies specific rules about what `X` does to every operator. | |
| The synthesis tool is a completely different animal. It reads the same text, extracts a **logic function** and a set of **state elements**, throws away everything that does not correspond to hardware, and then searches for a network of library cells that implements that function. It has no clock, no time, no `X`, no queue. It cannot wait ten time units because silicon does not have a wait instruction. | |
| They agree on a large common subset. That subset is what people mean by "synthesizable RTL." Every mismatch bug in Part 6 is a case where you wandered outside the subset without noticing, or wrote something inside the subset that the two tools nonetheless read differently. | |
| ### 1.3 Concurrency, which software does not have an equivalent of | |
| In a C program, statement A completes before statement B starts. That is the entire model. | |
| On a chip, **every gate is computing all the time**. There is no program counter, no thread, no scheduler. A million gates are simultaneously reacting to whatever is on their inputs, and their outputs are simultaneously feeding the next million. An HDL has to express that, and Verilog does it in two ways that are worth separating clearly. | |
| **Continuous assignment** is the honest one. Each `assign` statement is one piece of always-live combinational logic. Write ten of them in any order and you have described ten pieces of logic that all exist at once. Order in the file is meaningless because none of them "runs." | |
| ```systemverilog | |
| assign x = a & b; // these three describe the SAME hardware | |
| assign y = x | c; // no matter what order you | |
| assign z = ~y; // write them in | |
| ```text | |
| **Procedural blocks** are the convenient one and the dangerous one. Inside an `always` block, statements execute in order, like software, and that is why they are easy to write. But the block as a whole still describes a piece of always-live hardware. The tool's job is to read the procedural sequence and figure out what single combinational function or set of registers it is equivalent to. Most of Parts 3 and 4 are about the places where your mental model of "these statements run in order" and the tool's model of "this whole block is one function" come apart. | |
| Two `always` blocks are concurrent with each other, always, in every case. Two statements inside one `always` block are sequential with each other, in source order. Getting those two facts straight is most of the battle. | |
| --- | |
| ## Part 2, the three procedural blocks | |
| ### 2.1 Start with the problem the old syntax had | |
| Before SystemVerilog there was one block type, `always`, and it accepted anything. | |
| ```systemverilog | |
| always @(a or b) y = a & b & c; // combinational, and WRONG | |
| always @(posedge clk) q = d; // sequential, with a race | |
| always @(sel) if (sel) y = a; // an accidental latch | |
| ```text | |
| Every one of those is legal Verilog. The tool compiles them all without complaint. The first one is a bug because `c` is missing from the sensitivity list, so simulation only re-evaluates `y` when `a` or `b` change, while synthesis builds the full three-input AND. The second one uses a blocking assignment where non-blocking was needed, which is Part 3. The third one infers a latch, which is Part 4. The compiler had no way to know what you meant, so it could not tell you that you had failed to say it. | |
| SystemVerilog fixes this by adding blocks that **declare your intent**, so the tool can check that the code matches. That is the whole reason they exist. It is not syntax sugar. | |
| ### 2.2 always_ff, and what it actually instantiates | |
| ```systemverilog | |
| always_ff @(posedge clk or negedge rst_n) begin | |
| if (!rst_n) count_q <= '0; | |
| else if (incr_en) count_q <= count_q + 1'b1; | |
| end | |
| ```text | |
| Read that as a picture rather than as instructions. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig02.svg" alt="The if/else-if chain becomes a multiplexer on the flop's D input with \texttt{incr\_en} as its select, and the path from Q back to the incrementer is the feedback that makes the structure a counter." caption="The if/else-if chain becomes a multiplexer on the flop's D input with \texttt{incr\_en} as its select, and the path from Q back to the incrementer is the feedback that makes the structure a counter." id="fig:04-RTL-Design-and-SystemVerilog-2" /> | |
| The `if / else if` chain became a multiplexer on the flop's `D` input. The `incr_en` condition became the mux select. The absence of a final `else` did **not** create a latch here, because a flip-flop already holds its value when not updated, so "hold" is the natural behavior of the structure. That is the key asymmetry with Part 4. In sequential logic, an unassigned path means hold, and hold is free because the flop is already there. In combinational logic, an unassigned path also means hold, but there is no storage element yet, so the tool has to invent one. | |
| What `always_ff` declares to the tool is "this block contains exactly one set of flip-flops, and the sensitivity list is a clock edge and possibly an asynchronous reset edge." The tool now checks that. It will complain if you put a level-sensitive signal in the sensitivity list, if you assign the same variable from another block, or in most flows if you use a blocking assignment inside it. | |
| The reset convention is worth stating explicitly because it is a compile-or-fail structural rule rather than a style preference. If you write an asynchronous reset into the sensitivity list, the reset test must be the **first** condition in the block and the value it assigns must be a **constant**. Both facts come from the hardware. A real flip-flop has a dedicated asynchronous clear or preset pin, that pin forces the output to 0 or to 1, and there is no third option. So this is legal. | |
| ```systemverilog | |
| always_ff @(posedge clk or negedge rst_n) | |
| if (!rst_n) q <= 1'b0; // constant, first, fine | |
| else q <= d; | |
| ```text | |
| And this is not, because there is no flip-flop in any standard cell library with an asynchronous "load this other signal" pin. | |
| ```systemverilog | |
| always_ff @(posedge clk or negedge rst_n) | |
| if (!rst_n) q <= init_value; // NOT a constant, no such cell exists | |
| else q <= d; | |
| ```text | |
| Synchronous reset is the other choice and it is a different structure entirely. | |
| ```systemverilog | |
| always_ff @(posedge clk) // reset NOT in the sensitivity list | |
| if (!rst_n) q <= 1'b0; // so it is just another mux input | |
| else q <= d; | |
| ```text | |
| Here reset is ordinary data. It goes through the `D`-input mux like any other condition. That costs one more mux level in the data path, which eats setup margin on a critical path, but it buys you a reset that is inherently synchronous to the clock and therefore has no release-timing problem. The tradeoff is covered properly in [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing), and the reason a gated block will not reset synchronously is in [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating). | |
| ### 2.3 always_comb, and why having no sensitivity list is the feature | |
| ```systemverilog | |
| always_comb begin | |
| y = a & b & c; | |
| end | |
| ```text | |
| Three things happen that plain `always @(a or b)` did not give you. | |
| The sensitivity list is **inferred from the block's contents**, automatically and correctly, including every signal read anywhere inside including inside function calls. You cannot forget `c`, because you are not allowed to write the list at all. That deletes an entire bug class, and it is the single best reason to use the block. | |
| The block is **evaluated once at time zero**, before any input changes. Plain `always @(*)` waits for a change on something in its list, so at the start of simulation a combinational block that nothing has poked yet holds `X` on its outputs even though the real gates would already be presenting a settled value. `always_comb` does not have that startup hole. | |
| The variables it assigns are **checked for single-driver discipline**. Assigning the same variable from `always_comb` and from anywhere else is an error rather than a silent multiple-driver mess. | |
| And the fourth thing, which is the one people quote, is that the tool checks the block really is combinational and errors out if the code implies storage. That is Part 4. | |
| Use blocking assignments inside `always_comb`. That is not arbitrary, and Part 3.5 gives the reason. | |
| ### 2.4 always_latch, and the point of being able to say what you mean | |
| ```systemverilog | |
| always_latch begin | |
| if (clk_low_phase) q <= d; | |
| end | |
| ```text | |
| Latches are rare in a modern synchronous design and usually a mistake, but not always. A clock gating cell has one inside it, as [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) shows. Some low-power datapaths use them deliberately for time borrowing. Some high-density register files are latch-based. | |
| The value of `always_latch` is not that it makes latches easier. It is that it makes the accidental ones **loud**. Once the language has a way to say "I want a latch here," the tool is entitled to treat a latch appearing in an `always_comb` block as an error rather than a warning. Without `always_latch` there would be no way to distinguish "I meant this" from "I forgot an else," so the tool would have to permit both. | |
| ### 2.5 The comparison, and the practical rule | |
| | Block | Declares | Assignment style | Tool checks that | | |
| |---|---|---|---| | |
| | `always_ff` | flip-flops | non-blocking `<=` | the sensitivity list is edges, the block is not combinational, no other block drives the same variable | | |
| | `always_comb` | combinational logic | blocking `=` | no storage is implied, sensitivity is complete by construction, single driver | | |
| | `always_latch` | level-sensitive latches | non-blocking `<=` | a latch really is implied | | |
| | plain `always` | nothing | anything | nothing | | |
| The rule for RTL is to use the three intent-declaring blocks exclusively and never write a plain `always` in synthesizable code. Plain `always` belongs in testbenches, where you genuinely do want an unconstrained procedural block. If a lint flow you inherit still permits plain `always` in RTL, tightening that rule is one of the cheapest quality improvements available, and it is exactly the kind of change a flow owner makes. | |
| --- | |
| ## Part 3, blocking versus non-blocking, through the event regions | |
| ### 3.1 Feel the problem before seeing the mechanism | |
| Two blocks. One clock edge. Both are triggered by the same edge. | |
| ```systemverilog | |
| always @(posedge clk) b = a; | |
| always @(posedge clk) c = b; | |
| ```text | |
| The simulator has to run these one at a time, because a program cannot literally do two things at once. So one of them goes first. Which one? | |
| If the first block runs first, `b` becomes `a` immediately, then the second block reads the **new** `b`, so `c` also becomes `a`. Both flops load `a`. | |
| If the second block runs first, it reads the **old** `b`, so `c` gets the previous `b`, and then the first block updates `b`. That is a two-stage shift register. | |
| Two completely different circuits, from the same source file, and the difference is which block the simulator's internal data structures happened to visit first. The IEEE 1800 standard **explicitly does not define** that order. It is legal for a simulator to pick either. So the same file can behave differently on two vendors' simulators, or on the same simulator after an unrelated edit changed the internal ordering, and there is nothing to appeal to because the standard permits both. | |
| That is the disease. Non-blocking assignment is the cure, and to see why it works you have to look at how one time step is actually structured. | |
| ### 3.2 What one simulation time step contains | |
| A simulation time step is one value of `$time`. Inside that single instant, the simulator runs through an ordered sequence of **regions**. Nothing about this is visible in the waveform viewer, because it all happens at one time value, which is exactly why people find it mysterious. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig03.svg" alt="One simulation time step is an ordered sequence of regions, and a non-blocking assignment splits across two of them, evaluating its right-hand side in Active and writing its left-hand side only in NBA Update." caption="One simulation time step is an ordered sequence of regions, and a non-blocking assignment splits across two of them, evaluating its right-hand side in Active and writing its left-hand side only in NBA Update." id="fig:04-RTL-Design-and-SystemVerilog-3" /> | |
| Two facts out of that picture carry all the weight. | |
| **A blocking assignment takes effect in the Active region, immediately, before the next statement executes.** It behaves exactly like an assignment in C. | |
| **A non-blocking assignment splits into two halves that happen in two different regions.** The right-hand side is read in the Active region, at the moment the statement is reached, using whatever values exist then. But the left-hand side is not written until the NBA region, after every triggered block in the design has finished executing. Every non-blocking assignment in the entire design that was triggered by this edge reads its inputs before any of them writes its output. | |
| That second fact is the whole thing. It is a software simulation of what real flip-flops physically do. A real flop samples its `D` input at the edge and only later, after its internal clock-to-Q delay, presents the new value at `Q`. Every flop in the design does this at once. So the natural hardware behavior is "everybody reads, then everybody writes," and the NBA region is precisely that. | |
| ### 3.3 The two-flop shift register, worked both ways | |
| Same block, same edge, only the assignment operator changes. | |
| ```systemverilog | |
| // VERSION 1, non-blocking | |
| always_ff @(posedge clk) begin | |
| b <= a; | |
| c <= b; | |
| end | |
| ```text | |
| ```systemverilog | |
| // VERSION 2, blocking | |
| always_ff @(posedge clk) begin | |
| b = a; | |
| c = b; | |
| end | |
| ```text | |
| Drive `a` with a single-cycle pulse and trace it. `a` is 1 during cycle 1 only, and 0 elsewhere. Start with `b = 0` and `c = 0`. | |
| Version 1, non-blocking. At the edge ending cycle 1, the Active region evaluates both right-hand sides against the **current** values, so it queues `b <- 1` and `c <- 0` (because `b` is still 0 at that moment). Then the NBA region applies both. | |
| | Edge | `a` before edge | `b` before | `c` before | queued | `b` after | `c` after | | |
| |---|---|---|---|---|---|---| | |
| | 1 | 1 | 0 | 0 | `b<-1`, `c<-0` | 1 | 0 | | |
| | 2 | 0 | 1 | 0 | `b<-0`, `c<-1` | 0 | **1** | | |
| | 3 | 0 | 0 | 1 | `b<-0`, `c<-0` | 0 | 0 | | |
| The pulse appears on `b` one cycle after `a` and on `c` two cycles after `a`. A genuine two-deep shift register. | |
| Version 2, blocking. The first statement writes `b` immediately, so the second statement reads the value that was just written. | |
| | Edge | `a` before | `b` before | after stmt 1 | after stmt 2 | `b` after | `c` after | | |
| |---|---|---|---|---|---|---| | |
| | 1 | 1 | 0 | `b = 1` | `c = b = 1` | 1 | **1** | | |
| | 2 | 0 | 1 | `b = 0` | `c = b = 0` | 0 | 0 | | |
| `c` is not delayed relative to `b` at all. They change together. You have described two flops in parallel, both loading `a`. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig04.svg" alt="Non-blocking puts the two flops in series so a value needs two edges to reach c, while blocking puts them in parallel so a reaches both b and c on one edge. These are different circuits, not two styles of writing one circuit." caption="Non-blocking puts the two flops in series so a value needs two edges to reach c, while blocking puts them in parallel so a reaches both b and c on one edge. These are different circuits, not two styles of writing one circuit." id="fig:04-RTL-Design-and-SystemVerilog-4" /> | |
| Say that last line out loud in an interview. These are not two styles of writing the same thing. **They are different hardware.** One of them delays by two cycles and one of them does not, and if this is a synchronizer or a pipeline stage, the difference is a functional failure. | |
| Now the detail that makes the case airtight. Reverse the order of the two blocking statements. | |
| ```systemverilog | |
| always_ff @(posedge clk) begin | |
| c = b; // read b BEFORE it is overwritten | |
| b = a; | |
| end | |
| ```text | |
| That gives you the correct two-stage shift. So with blocking assignments, **the textual order of the statements determines the circuit**. With non-blocking assignments, the order is irrelevant, because every right-hand side is read before any left-hand side is written. Hardware does not have a source-code order. Wanting the circuit to be independent of the order you happened to type the lines in is exactly what non-blocking gives you. | |
| ### 3.4 Back to the race | |
| Return to the two-block version from 3.1, now with non-blocking. | |
| ```systemverilog | |
| always_ff @(posedge clk) b <= a; | |
| always_ff @(posedge clk) c <= b; | |
| ```text | |
| Whichever block the simulator visits first, both right-hand sides are read in the Active region against pre-edge values, and both writes land in the NBA region. Block ordering is now unobservable. The answer is a two-stage shift, on every simulator, on every run, forever. | |
| That is the argument to give when asked "why non-blocking in sequential logic." Not "because that is the rule." Because **blocking assignments across concurrent blocks produce a result that depends on an evaluation order the language standard deliberately leaves undefined, and non-blocking assignments make the result order-independent by construction.** | |
| The reverse question is also asked. Why blocking in combinational logic? Because inside an `always_comb` block you are describing a chain of logic that all settles within one propagation delay, and intermediate variables are wires, not state. You **want** the later statement to see the earlier statement's result, because that is what a wire does. | |
| ```systemverilog | |
| always_comb begin | |
| sum_pp = a + b; // an internal wire | |
| result = sum_pp << 1; // must see the NEW sum_pp | |
| end | |
| ```text | |
| Use non-blocking there and `result` reads the value `sum_pp` had at the start of the evaluation, which is stale by one delta, and in a loop it is stale by an entire iteration. In simple cases the simulator re-triggers the block and eventually converges to the right answer anyway, which is worse than failing, because now simulation matches synthesis by luck and you never learn. | |
| ### 3.5 The rules, each with its reason | |
| | Rule | Reason | | |
| |---|---| | |
| | Non-blocking `<=` in `always_ff` | makes concurrent blocks order-independent, and models the physical read-then-write behavior of real flops | | |
| | Blocking `=` in `always_comb` | intermediate variables are wires, and later statements must see earlier results the way a wire does | | |
| | Never mix `=` and `<=` for the same variable | the variable is either a register or a wire, it cannot be both, and mixing makes the intent unrecoverable | | |
| | Never assign the same variable from two blocks | that is two drivers on one physical pin, which is a short | | |
| | Non-blocking inside an unrolled `for` in `always_ff` | see 5.2, the loop unrolls into parallel flops and non-blocking is what keeps them parallel | | |
| That last row is the one people trip over, and it is worked in 5.2. | |
| --- | |
| ## Part 4, inferred latches | |
| ### 4.1 Build one by accident, in three lines | |
| ```systemverilog | |
| always_comb begin | |
| if (sel) y = a; | |
| end | |
| ```text | |
| Ask the only question that matters. **What is `y` when `sel` is 0?** | |
| The code does not say. And in hardware, "does not say" is not an option, because a wire always has a voltage on it. So the language has a rule, and the rule is that a variable in a procedural block keeps its previous value if nothing assigns it. Keeping a previous value is the definition of memory. So the tool builds memory. | |
| It cannot build a flip-flop, because there is no clock here. The only storage element that works from a level rather than an edge is a **latch**. So it builds a latch, with `sel` as the enable. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig05.svg" alt="With no else branch the tool must keep y unchanged when sel is low, and the only element that holds a value from a level rather than an edge is a latch, so sel silently becomes its enable." caption="With no else branch the tool must keep y unchanged when sel is low, and the only element that holds a value from a level rather than an edge is a latch, so sel silently becomes its enable." id="fig:04-RTL-Design-and-SystemVerilog-5" /> | |
| The same accident in `case` form is more common in real code because the missing branch is harder to see. | |
| ```systemverilog | |
| always_comb begin | |
| case (op) | |
| 2'b00: y = a + b; | |
| 2'b01: y = a - b; | |
| 2'b10: y = a & b; | |
| endcase // 2'b11 assigns nothing -> latch | |
| end | |
| ```text | |
| Three of four values are covered. The fourth silently creates a latch on `y`. In a two-bit selector that is easy to spot in review. In a five-bit opcode with nineteen listed cases it is not. | |
| ### 4.2 Why the tool has no choice about this | |
| People sometimes ask why the tool does not just pick a default. Because picking a default would be inventing behavior the designer did not write, and if the tool guessed wrong the design would be silently incorrect in a way no report would show. Holding is the only interpretation that is faithful to the source text. The tool is doing the honest thing. The problem is that the honest thing is almost never what you meant. | |
| ### 4.3 Why an unintended latch is a real bug and not a style complaint | |
| **It is transparent for a whole phase.** While `sel` is high the latch is a wire, so combinational glitches from [Digital Logic and Timing](/learn/hardware-interview-prep/digital-logic-and-timing) pass straight through to `y` and onward. A flip-flop samples one instant and hides all the glitching. A latch does not. | |
| **Static timing gets much harder.** Timing a flip-flop path is one arrival, one required time, one slack number. Timing a latch involves **time borrowing**, where a path arriving late into a transparent latch can steal time from the next stage, so the analysis becomes a coupled multi-stage problem. The tool can do it. The engineer reading the report often cannot, and the report is dramatically less legible. | |
| **The enable is a data signal.** `sel` came out of combinational logic, so it glitches. A glitch on a latch enable can close the latch momentarily and capture a mid-flight value, which is the same failure mode as the naive AND-gate clock gater in [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating). | |
| **DFT hates it.** Scan chains are built out of flip-flops. A latch in the middle of combinational logic is not on the scan chain, so it holds state that ATPG cannot control or observe, and the coverage number drops. [DFT and Silicon Debug](/learn/hardware-interview-prep/dft-and-silicon-debug) covers what that costs. | |
| **And it usually means you forgot something.** In the `case` example above, the real bug is not the latch. The real bug is that nobody decided what `op = 2'b11` should do. The latch is a symptom. | |
| ### 4.4 Three fixes, and which to prefer | |
| **Fix one, default assignment at the top of the block.** Assign every output an unconditional value first, then let the conditional logic override it. | |
| ```systemverilog | |
| always_comb begin | |
| y = '0; // now y is assigned on EVERY path | |
| if (sel) y = a; | |
| end | |
| ```text | |
| This is the one to use by default. It is one line, it scales to a block with fifteen outputs and forty branches, and adding a new branch later cannot reintroduce the bug. The apparent double-assignment costs nothing in hardware, because the tool flattens the block into a single function before it builds anything. What you actually get is a 2-to-1 mux selecting between `a` and constant zero. | |
| **Fix two, make every branch assign every output.** | |
| ```systemverilog | |
| always_comb begin | |
| if (sel) y = a; | |
| else y = '0; | |
| end | |
| ```text | |
| Correct, and fine for two branches. It does not scale. With four outputs and six branches you are writing twenty-four assignments and one omission puts the latch back. | |
| **Fix three, a fully specified `case` with a `default`.** | |
| ```systemverilog | |
| always_comb begin | |
| case (op) | |
| 2'b00: y = a + b; | |
| 2'b01: y = a - b; | |
| 2'b10: y = a & b; | |
| default: y = '0; // covers 2'b11 and anything else | |
| endcase | |
| end | |
| ```text | |
| Always write the `default`. Even when you have enumerated every legal value, write it, because a `default` is also your protection against a value that should be impossible actually occurring, which Part 6 shows is not a hypothetical. | |
| The strongest form combines fixes one and three. Default assignments at the top **and** a `default` branch. The top-of-block defaults kill latches structurally, and the `default` branch documents what an illegal selector does. | |
| ### 4.5 What always_comb changes about all of this | |
| Nothing about the hardware. Everything about when you find out. | |
| With plain `always @(*)`, an inferred latch shows up as a line buried in a synthesis log that says something like "latch inferred for signal y," in a run that produced fifty thousand other lines and exited successfully. Many teams never read it. | |
| With `always_comb`, the tool is entitled to treat it as an **error**, because the language now has `always_latch` for the case where you meant it. The compile fails. You find out in seconds, at your desk, on the line that caused it. | |
| That transformation, from a silent surprise found weeks later in a synthesis report to a hard error found in seconds at compile time, is the single largest practical benefit of the SystemVerilog intent-declaring blocks. It is also exactly the kind of thing a lint flow enforces, which is Part 8. | |
| --- | |
| ## Part 5, what synthesizes, and the constructs worth knowing | |
| ### 5.1 The table | |
| | Construct | Synthesizes | Note | | |
| |---|---|---| | |
| | `assign`, continuous assignment | yes | one piece of always-live combinational logic | | |
| | `always_ff`, `always_comb`, `always_latch` | yes | the three blocks of Part 2 | | |
| | `if`, `case`, `casez`, ternary `?:` | yes | become muxes and priority chains | | |
| | `+`, `-`, comparison operators | yes | become adders and comparators | | |
| | `*` | yes | becomes a large multiplier array, see [Arithmetic Hardware](/learn/hardware-interview-prep/arithmetic-hardware) | | |
| | `/`, `%` by a non-power-of-two | technically yes | enormous and slow, most teams ban it and require an explicit divider block | | |
| | `>>`, `<<` by a constant | yes | free, it is just wiring | | |
| | `>>`, `<<` by a variable | yes | becomes a barrel shifter, real area | | |
| | `for` with compile-time bounds | yes | fully unrolled into parallel hardware | | |
| | `while`, `forever`, data-dependent `for` | **no** | no fixed structure exists | | |
| | `generate`, `genvar`, generate-`if` | yes | resolved at elaboration, before synthesis | | |
| | `parameter`, `localparam`, `$clog2`, `$bits` | yes | resolved at elaboration | | |
| | `enum`, packed `struct`, packed `union` | yes | packed types are just named bit vectors | | |
| | unpacked array of a packed type | yes | becomes a register file or an SRAM | | |
| | `function` with no side effects | yes | inlined combinational logic | | |
| | `task` | usually no | may contain timing controls | | |
| | `#10` and other delays | **no** | ignored with a warning, or an outright error | | |
| | `initial` | **no** for ASIC | FPGA flows use it for memory initialization | | |
| | `fork` / `join` | **no** | testbench construct | | |
| | dynamic array, queue, associative array, `string`, `class` | **no** | require a heap, which silicon does not have | | |
| | `$display`, `$finish`, `$fatal`, `$random` | **no** | testbench only, ignored by synthesis | | |
| | `force`, `release`, `wait`, named events | **no** | simulation control | | |
| | `real`, `shortreal` | **no** | no floating-point type maps to gates | | |
| | concurrent assertions | not into logic | consumed by simulation and by formal, stripped by synthesis | | |
| The organizing principle behind every row is the same. **Silicon is a fixed structure that must be decidable before manufacture.** Anything whose size or existence depends on runtime data has no hardware meaning. | |
| ### 5.2 Loops that unroll and loops that cannot | |
| A synthesizable `for` loop is not a loop. It is a **copy-paste instruction to the elaborator**. | |
| ```systemverilog | |
| logic [7:0] vec; | |
| logic [3:0] popcount; | |
| always_comb begin | |
| popcount = '0; | |
| for (int i = 0; i < 8; i++) | |
| popcount = popcount + vec[i]; | |
| end | |
| ```text | |
| The tool unrolls that into eight literal additions with no loop remaining, then rebalances the chain into an adder tree so the depth is about three levels rather than eight. What you get is a fixed piece of combinational logic that computes the population count of an 8-bit vector in one propagation delay. | |
| Notice which assignment operator is used. **Blocking.** It has to be, because each iteration must see the accumulated result of the previous one. All eight additions happen inside a single evaluation of a single combinational block, in zero simulated time, and blocking is what makes the sequence within that evaluation work. This is the concrete example that shows the "blocking in combinational" rule is not a convention. It is required for the loop to compute anything. | |
| Now the mirror image. Inside `always_ff`, a `for` loop that shifts an array must use **non-blocking**, and for exactly the opposite reason. | |
| ```systemverilog | |
| logic [7:0] stage [4]; | |
| always_ff @(posedge clk) begin | |
| stage[0] <= din; | |
| for (int i = 1; i < 4; i++) | |
| stage[i] <= stage[i-1]; // NON-blocking | |
| end | |
| ```text | |
| Unrolled, that is `stage[1] <= stage[0]`, `stage[2] <= stage[1]`, `stage[3] <= stage[2]`. Every right-hand side is read in the Active region before any left-hand side is written in the NBA region, so all three read pre-edge values, and the result is a genuine four-deep delay line. | |
| Write the same loop with blocking and watch it collapse. | |
| ```systemverilog | |
| always_ff @(posedge clk) begin | |
| stage[0] = din; | |
| for (int i = 1; i < 4; i++) | |
| stage[i] = stage[i-1]; // BLOCKING, and now broken | |
| end | |
| ```text | |
| `stage[0]` becomes `din`. Then `stage[1] = stage[0]` reads the just-written value, so it becomes `din`. Then `stage[2]` becomes `din`. Then `stage[3]` becomes `din`. Four flops that all load the same value on the same edge. The delay line is gone and the whole thing became a fanout, which is the exact same failure as 3.3 hiding inside a loop where nobody sees it. That is why 3.5's last row exists. | |
| And the thing that cannot synthesize. | |
| ```systemverilog | |
| while (!match) idx = idx + 1; // how many gates is this? | |
| ```text | |
| There is no answer. The number of iterations depends on data that does not exist until the chip runs. If you need a data-dependent number of steps, that is a state machine spending a data-dependent number of **cycles**, not a loop inside a combinational block. | |
| ### 5.3 Parameterization and generate, on a real configurable structure | |
| Hard-coded widths are the enemy of reuse, and reuse is the whole economics of an SoC. Here is a delay line that is configurable in both width and depth, including the degenerate zero-stage case. | |
| ```systemverilog | |
| module delay_line #( | |
| parameter int WIDTH = 8, | |
| parameter int STAGES = 3 | |
| ) ( | |
| input logic clk, | |
| input logic rst_n, | |
| input logic en, | |
| input logic [WIDTH-1:0] din, | |
| output logic [WIDTH-1:0] dout | |
| ); | |
| if (STAGES == 0) begin : g_bypass | |
| // elaboration-time decision: no hardware at all, just a wire | |
| assign dout = din; | |
| end | |
| else begin : g_pipe | |
| logic [WIDTH-1:0] stage [STAGES]; | |
| always_ff @(posedge clk or negedge rst_n) begin | |
| if (!rst_n) begin | |
| for (int i = 0; i < STAGES; i++) stage[i] <= '0; | |
| end | |
| else if (en) begin | |
| stage[0] <= din; | |
| for (int i = 1; i < STAGES; i++) stage[i] <= stage[i-1]; | |
| end | |
| end | |
| assign dout = stage[STAGES-1]; | |
| end | |
| endmodule | |
| ```text | |
| Several things are happening and each is worth naming. | |
| `parameter int` gives a typed parameter with a default, overridable per instance. `WIDTH-1:0` means no number appears twice, so changing the width is a one-token edit at the instantiation, not a search-and-replace through the body. | |
| The `if (STAGES == 0)` is a **generate-if**, resolved at elaboration, before synthesis and before simulation. It is not a mux. When `STAGES` is 0 the `g_pipe` branch does not exist at all, no flops are created, and `dout` is a wire. That is how a single module serves both the pipelined and the bypass configuration without paying for the flops in the bypass case. | |
| The named blocks `g_bypass` and `g_pipe` are not decoration. Generate blocks create hierarchy, and if you do not name them the tool invents names like `genblk1`, which then appear in your synthesis reports, your timing paths, your waveform hierarchy, and your assertion messages. Naming them is a five-second investment that pays off every time anyone debugs the block. | |
| Instantiate it three different ways from one source. | |
| ```systemverilog | |
| delay_line #(.WIDTH(32), .STAGES(2)) u_addr_dly (.*); | |
| delay_line #(.WIDTH(64), .STAGES(4)) u_data_dly (.*); | |
| delay_line #(.WIDTH(1), .STAGES(0)) u_null_dly (.*); // pure wire | |
| ```text | |
| The `genvar` form is the other half of generate and is for replication rather than selection. | |
| ```systemverilog | |
| genvar i; | |
| generate | |
| for (i = 0; i < LANES; i++) begin : g_lane | |
| alu_lane #(.WIDTH(WIDTH)) u_lane ( | |
| .clk (clk), | |
| .op (op[i]), | |
| .a (a[i]), | |
| .b (b[i]), | |
| .result (result[i]) | |
| ); | |
| end | |
| endgenerate | |
| ```text | |
| That creates `LANES` physically separate copies of `alu_lane`, named `g_lane[0].u_lane` through `g_lane[LANES-1].u_lane`. A `genvar` is an elaboration-time integer, not a signal. It does not exist in the netlist. This is how a vector unit with a configurable lane count is written once. | |
| ### 5.4 Interfaces and modports, and what they remove | |
| Count the pain first. A valid-ready data channel needs three signals. Passing it through six levels of hierarchy means declaring three ports in each of six modules and connecting three signals at each of six instantiations, so thirty-six lines that say nothing. Adding a fourth signal, say a `last` bit, means editing all six modules and all six instantiations. Miss one and you get a compile error if you are lucky, or a silently unconnected signal if you are not. | |
| An **interface** bundles them. | |
| ```systemverilog | |
| interface valid_ready_if #(parameter int W = 32) (input logic clk, input logic rst_n); | |
| logic valid; | |
| logic ready; | |
| logic [W-1:0] data; | |
| modport producer (output valid, output data, input ready, input clk, input rst_n); | |
| modport consumer (input valid, input data, output ready, input clk, input rst_n); | |
| modport monitor (input valid, input data, input ready, input clk, input rst_n); | |
| // the protocol rules live WITH the protocol, checked at every instance | |
| property p_data_stable; | |
| @(posedge clk) disable iff (!rst_n) | |
| (valid && !ready) |=> ($stable(data) && valid); | |
| endproperty | |
| a_data_stable: assert property (p_data_stable); | |
| endinterface | |
| ```text | |
| Now a module takes one port. | |
| ```systemverilog | |
| module fifo_writer (valid_ready_if.producer out); | |
| // out.valid, out.data, out.ready are all available | |
| endmodule | |
| ```text | |
| Four things got removed. **Repetition**, because the signal list appears once. **Direction errors**, because a `modport` makes the direction a compile-time contract and a producer physically cannot drive `ready`. **Drift**, because adding `last` to the interface adds it everywhere at once. And **scattered checking**, because the protocol assertions live inside the interface and are therefore instantiated automatically for every channel in the design, so a hundred channels get a hundred checkers for free. | |
| The honest caveat, and raising it unprompted is a good signal. Interfaces are excellent inside a block and at testbench boundaries. At **hard block boundaries**, meaning the ports of a unit that gets synthesized, placed, and delivered as a physical partition, many teams still flatten to plain ports, because some synthesis, DFT, and physical flows handle flat ports more predictably and because a flat port list is the thing that gets formally compared against the specification. Use interfaces internally, flatten at the partition boundary, and know why. | |
| ### 5.5 Packages, so one definition serves the whole design | |
| ```systemverilog | |
| package core_pkg; | |
| localparam int XLEN = 64; | |
| localparam int PHYS_REGS = 192; | |
| localparam int PREG_W = $clog2(PHYS_REGS); // = 8, since 2**7=128 < 192 <= 256 | |
| typedef enum logic [2:0] { | |
| ALU_ADD, ALU_SUB, ALU_AND, ALU_OR, | |
| ALU_XOR, ALU_SLL, ALU_SRL, ALU_SRA | |
| } alu_op_e; | |
| typedef struct packed { | |
| logic valid; | |
| alu_op_e op; | |
| logic [PREG_W-1:0] pdst; | |
| logic [XLEN-1:0] opa; | |
| logic [XLEN-1:0] opb; | |
| } uop_t; // 1 + 3 + 8 + 64 + 64 = 140 bits | |
| endpackage | |
| ```text | |
| Then anywhere in the design, `import core_pkg::*;` and every module agrees on the widths, the opcode encoding, and the micro-op layout. | |
| Why this matters more than it looks. `PREG_W` is **derived** from `PHYS_REGS` by `$clog2`, so raising the physical register file from 192 to 320 entries changes one number and every index width in the design follows. Type the width by hand in forty places and one of them will be wrong, and Part 8.2 shows exactly how invisible that failure is. | |
| The `enum` gives named values that appear **by name** in the waveform viewer instead of as `3'b101`, which saves real debug time. A packed `struct` is layout-defined and is just a 140-bit vector to synthesis, so it can be a single port, a single FIFO entry, or a single flop bank, while still being addressable by field name in source. That is free readability. | |
| Two practical notes. Prefer explicit `import core_pkg::uop_t;` or fully qualified `core_pkg::XLEN` in large designs, because a wildcard import from three packages that happen to define the same name is a genuinely confusing failure. And an `enum` declared with an explicit base type, `enum logic [2:0]`, is two-state-friendly and synthesizes to exactly the width you chose, whereas a bare `enum` lets the tool pick. | |
| ### 5.6 case discipline, and what unique and priority actually assert | |
| An ordinary `case` is a **priority** structure. The first matching branch wins, so the synthesized hardware is a chain of comparisons, and a chain is deep and slow. If you know the branches are mutually exclusive, saying so lets the tool build a flat parallel mux instead. | |
| ```systemverilog | |
| unique case (state_q) | |
| S_IDLE: ... | |
| S_REQ: ... | |
| S_WAIT: ... | |
| S_DONE: ... | |
| endcase | |
| ```text | |
| `unique` asserts **two** things at once. At least one branch matches, meaning the case is full. At most one branch matches, meaning no two branch expressions overlap. Both are checked at runtime in simulation and both are exploited by synthesis, which drops the priority chain and treats any unlisted selector value as a don't-care to be optimized however is cheapest. | |
| `priority case` asserts only the first of those. At least one branch matches, and order is meaningful. Synthesis keeps the priority chain but is allowed to delete branches it can prove unreachable. | |
| `unique0 case` asserts only the second. At most one matches, and zero matches is legal. | |
| Now the part that bites. **`unique` is an assertion, not an assignment.** It tells the tool what you believe. It does not make it true, and it does not assign anything. So this still infers a latch. | |
| ```systemverilog | |
| always_comb begin | |
| unique case (op) // no default, and no top-of-block default | |
| 2'b00: y = a + b; | |
| 2'b01: y = a - b; | |
| 2'b10: y = a & b; | |
| endcase // op = 2'b11 assigns nothing -> LATCH | |
| end | |
| ```text | |
| And the deeper failure. Suppose `op` really can be `2'b11` because of a bug upstream. Simulation prints a `unique case` violation, which is loud and useful if anyone is reading. But **synthesis already used your promise**. It treated `2'b11` as a don't-care and built whatever logic was smallest, which may produce any of the three results or something else entirely. So the RTL simulation, the gate-level netlist, and the silicon can all disagree, and the source of the disagreement is a promise you made and did not keep. | |
| The discipline that follows. Use `unique` when the exclusivity is genuinely guaranteed by construction, such as a one-hot state register or a decoded selector. Pair it with default assignments at the top of the block so no latch can appear. Add a `default` branch anyway, assigning a safe value, so the illegal case has defined behavior in silicon and not just a simulation message. And add an explicit assertion on the selector if the property matters, because an assertion you wrote is visible in the verification plan while a `unique` qualifier buried in a case statement is not. | |
| Two related constructs to know and mostly avoid. `casez` treats `?` and `z` in the branch patterns as wildcards, which is useful for decoders and is generally acceptable. `casex` treats `x` as a wildcard **in the selector as well**, which means a genuinely unknown signal will match a branch it has no business matching, hiding exactly the bug you needed to see. Most modern coding standards ban `casex` outright. | |
| --- | |
| ## Part 6, simulation and synthesis mismatch | |
| ### 6.1 Why this bug class is the worst one there is | |
| Rank hardware bugs by cost and the ordering is roughly this. A bug caught by lint costs seconds. A bug caught in simulation costs hours. A bug caught at synthesis costs a day. A bug caught in gate-level simulation costs a week. A bug caught in emulation costs a month. A bug caught in silicon costs a respin, which is millions of dollars and a quarter of schedule. | |
| Now notice what every one of those has in common except the last. **A test failed.** Somebody saw a red mark. | |
| A simulation-to-synthesis mismatch is the case where **the test passes and the chip is wrong**. Your regression is green. Your coverage is closed. Your reviewers signed off. The RTL model and the netlist are describing two different circuits, and every verification effort you spent was spent on the model. The bug does not survive verification by being subtle. It survives verification by being **invisible to it**. | |
| That is why this gets its own part, and why an interviewer asking about it is really asking whether you understand that the RTL is a model rather than the thing itself. | |
| ### 6.2 Incomplete sensitivity lists | |
| ```systemverilog | |
| always @(a or b) // c is missing | |
| y = a & b & c; | |
| ```text | |
| The simulator wakes this block only when `a` or `b` changes. Change `c` alone and `y` does not update, because nothing scheduled the block. Synthesis reads the same text, extracts the function `y = a & b & c`, and builds a three-input AND gate, which of course responds to `c`. | |
| So the RTL model is a **latch-like thing that samples `c` only when `a` or `b` move**, and the netlist is a plain AND gate. They differ. And the RTL version is the one your tests ran against. | |
| `always_comb` deletes this completely, because you cannot write the list. So the practical answer to "how do you avoid incomplete sensitivity lists" is not "be careful," it is "use a construct where the mistake is unrepresentable." That is the better engineering answer generally, and saying it that way lands well. | |
| The historical middle step was `always @(*)`, which infers the list. It is much better than the manual list but still weaker than `always_comb`, because it does not check for latches, does not enforce a single driver, and does not evaluate once at time zero. | |
| ### 6.3 Blocking assignments in sequential blocks, seen as a mismatch | |
| Part 3 showed that blocking in `always_ff` produces an order-dependent result. Now look at it from the synthesis side. Synthesis does not have a simulator's evaluation order. It reads the block, extracts the register set and their next-state functions, and builds them. For the two-block race in 3.1, synthesis will build **one** answer. Your simulator built one answer. There is no rule that says they are the same answer. | |
| So the mismatch is not "simulation is slow" or "simulation is approximate." It is that you wrote something whose meaning the standard leaves open, two tools resolved the ambiguity independently, and they can resolve it differently. This is why "never use blocking in `always_ff`" is a hard rule in every coding standard rather than a preference. | |
| ### 6.4 X-optimism, worked slowly | |
| This is the one that gets asked, and it is worth building carefully because the intuition is genuinely misleading. | |
| Start with what `X` is. In a four-state simulation a signal can be `0`, `1`, `X`, or `Z`. `X` means the simulator **does not know** the value. It is not a voltage. Real silicon has no `X`. Every node in a real chip has a definite voltage at every instant, and that voltage reads as either a 0 or a 1 to whatever gate is looking at it. `X` is a bookkeeping symbol in a program. | |
| Now the language rule that causes the trouble. **In an `if` condition, `X` is treated as false.** Same for a `case` selector that matches no branch, and same for a `while`. The condition is not true, so the false path is taken. | |
| Work an example that could really happen. | |
| ```systemverilog | |
| logic busy_q; // NOTE: no reset | |
| always_ff @(posedge clk) begin | |
| if (start) busy_q <= 1'b1; | |
| else if (done) busy_q <= 1'b0; | |
| end | |
| always_comb begin | |
| if (busy_q) grant = 1'b0; | |
| else grant = 1'b1; | |
| end | |
| ```text | |
| At time zero, `busy_q` has never been assigned, so the simulator gives it `X`. | |
| In **simulation**, `if (busy_q)` evaluates `X`, treats it as false, and takes the else. So `grant = 1`. A requester asks, gets granted, does its work, asserts `done`, and now `busy_q` resolves to a real 0. From that point everything is clean. Your test passes. Every one of your twenty thousand regression tests passes, because they all start the same way and they all take the same benign path out of the unknown state. | |
| In **silicon**, `busy_q` is a real flip-flop with real transistors. At power-up its internal cross-coupled inverters resolve to whichever side is slightly stronger, which depends on manufacturing mismatch, on the supply ramp rate, and on temperature. It comes up as a definite 0 or a definite 1, and which one is a property of that individual die. Suppose it comes up 1 on some fraction of parts. Then `grant = 0`, the requester waits, nothing sets `done` because nothing was granted, and the machine deadlocks at boot on those parts. | |
| The failure rate is not zero and not one. It is however many dies happen to resolve that flop to 1, which could be five percent or sixty percent, and it will vary by lot and by corner. This is the profile of the very worst kind of silicon bug, an intermittent boot hang that reproduces on some parts and not others. | |
| Put the summary in one sentence. **Simulation is optimistic about `X` because it picks a branch, real hardware picks a branch too, and there is no reason the two picks agree.** The word optimistic means that simulation makes forward progress and produces a plausible-looking result exactly where it should have thrown up its hands. | |
| A precise detail worth having ready, because it shows real familiarity. The **same logical mux written two ways behaves differently under `X`**. | |
| ```systemverilog | |
| // Procedural if/else: X-OPTIMISTIC. sel = X takes the else branch, | |
| // so y gets a definite value that may be wrong. | |
| always_comb begin | |
| if (sel) y = a; | |
| else y = b; | |
| end | |
| // Conditional operator: NOT optimistic. sel = X produces a bitwise | |
| // merge, so bits where a and b agree pass through and bits where they | |
| // disagree become X, which then propagates and is visible. | |
| assign y = sel ? a : b; | |
| ```text | |
| The second form tells you something is wrong. The first form hides it. That is a real, checkable, language-level difference and it is a good thing to be able to state. | |
| ### 6.5 X-pessimism, the opposite problem | |
| Now the mirror image, mostly seen in gate-level simulation. | |
| Take a 2-to-1 mux built from actual gates, with `sel = X` and `a = b = 1`. Physically, whichever way `sel` resolves, the output is 1, because both inputs are 1. But the gate-level simulator evaluates each gate independently. The AND of `sel` and `a` is `X`. The AND of `~sel` and `b` is `X`. The OR of `X` and `X` is `X`. Output `X`, even though the real circuit is unambiguously 1. | |
| That is **X-pessimism**. The simulator propagates unknowns more aggressively than physics does, because it evaluates gate by gate and loses the correlation between `sel` and `~sel`. In a large netlist this compounds, and a single `X` at a control point can flood thousands of nodes with `X` within a few cycles, so the waveform goes red everywhere and the actual origin is buried. | |
| The two problems have the same root, which is that a two-valued physical world is being modeled with a three-valued symbol that has no way to represent "unknown but consistent." | |
| | | X-optimism | X-pessimism | | |
| |---|---|---| | |
| | Where | RTL simulation, `if` and `case` conditions | gate-level simulation, reconvergent logic | | |
| | Symptom | test passes, silicon fails | test fails or floods with X, silicon is fine | | |
| | Cost | a respin | days of debug chasing something that cannot happen | | |
| | Direction | simulation is more forgiving than reality | simulation is harsher than reality | | |
| ### 6.6 Where X problems actually surface, and what to do about them | |
| **Gate-level simulation** is the usual place, because it is the first time the design is simulated with real cells and real X-propagation through them, and because it runs without the RTL's implicit initialization conveniences. A design that has been clean in RTL regression for six months can light up with X on its first gate-level run, and every one of those X sources was already in the RTL, just invisible. | |
| The defenses, roughly in order of value. | |
| **Reset what needs resetting.** The single largest source of X-optimism is state that has no reset. The counterargument is real, since reset costs area and routing and a reset tree is a global signal with its own timing problems, so nobody resets every flop in a large datapath. The discipline is that **control** state gets reset and **data** state may not, and the reason is that a datapath flop holding garbage is harmless as long as a control flop guarantees nobody looks at it. Being able to state that split is the answer the interviewer wants. | |
| **Run with random two-state initialization.** Most simulators can initialize every uninitialized flop to a random 0 or 1 rather than X, with a controllable seed. Run the regression on several seeds. This directly breaks X-optimism, because now the simulator picks a definite value the way silicon does, and a design that only worked because `X` conveniently meant false will fail on some seeds. This is cheap and finds real bugs. | |
| **Turn on X-propagation checking.** Simulators offer modes that make `if (X)` propagate `X` into the assigned variable instead of silently taking the else. That converts the optimistic case into a pessimistic one, which is much easier to debug because it is loud. | |
| **Assert on the unknown.** `assert property (@(posedge clk) disable iff (!rst_n) !$isunknown(state_q));` puts a hard check on the signals that must never be X. Part 7 covers the mechanics. | |
| **Simulate at the gate level early and on purpose,** with at least a reset sequence and a short functional test, rather than treating it as a signoff checkbox in the last week. | |
| ### 6.7 The short list to have memorized | |
| If asked to name the causes of simulation-to-synthesis mismatch, this is the list, and the order is roughly by how often it happens. | |
| Incomplete sensitivity lists in old-style `always` blocks. Unintended latch inference. Blocking assignments in sequential blocks, and non-blocking in combinational ones. X-optimism through `if` and `case`. A `unique` or `priority` qualifier whose promise is violated at runtime. `casex` matching an unknown against a branch pattern. Delays such as `#1` that simulation honors and synthesis discards, which most often shows up as a race the delay was accidentally papering over. Functions with side effects or with different behavior on repeated evaluation. And synthesis pragmas such as `// synopsys translate_off` that hide code from one tool and not the other, which is the mismatch mechanism made explicit and is why those pragmas need a very high bar. | |
| --- | |
| ## Part 7, assertions | |
| ### 7.1 Two kinds, and why the distinction is not cosmetic | |
| An **immediate assertion** is a procedural statement. It executes when control reaches it, evaluates a plain boolean expression right then, and does nothing else. | |
| ```systemverilog | |
| always_comb begin | |
| // fires the instant the condition is false, wherever this block is evaluated | |
| assert (!(push && pop && full && empty)) | |
| else $error("FIFO cannot be both full and empty with traffic"); | |
| end | |
| ```text | |
| That is useful, but it can only talk about **one instant**. It cannot say "within four cycles" or "eventually" or "not until." | |
| A **concurrent assertion** is a different thing entirely. It is a temporal statement, evaluated against a clock, that describes behavior **over time**. It is not a procedural statement that runs, it is a checker that exists, continuously, for the life of the simulation, starting a fresh evaluation attempt on every clock edge. | |
| ```systemverilog | |
| assert property (@(posedge clk) req |-> ##[1:4] gnt); | |
| ```text | |
| The other reason the distinction matters is tooling. Immediate assertions are simulation-only. Concurrent assertions are the **input language of formal property verification**, so the same text that runs as a runtime check in simulation can be handed to a formal engine that attempts to prove it for all possible inputs. That dual life is what makes concurrent assertions worth learning properly, and it is the connection to [Verification Methodology](/learn/hardware-interview-prep/verification-methodology). | |
| ### 7.2 A full property, taken apart piece by piece | |
| ```systemverilog | |
| property p_req_gets_grant; | |
| @(posedge clk) disable iff (!rst_n) | |
| req |-> ##[1:4] gnt; | |
| endproperty | |
| a_req_gets_grant: assert property (p_req_gets_grant) | |
| else $error("req at %0t was not granted within 4 cycles", $time); | |
| ```text | |
| **`property ... endproperty`** names the property so it can be reused, asserted, covered, and referred to by name in a coverage report. Naming it costs one line and buys readability everywhere downstream. | |
| **`@(posedge clk)`** is the **clocking event**. It does two jobs. It defines what "one cycle later" means for the `##` operators. And it defines **when the signals are sampled**, which is the subtle part. Assertion sampling happens in the **Preponed** region from 3.2, meaning the assertion sees the values that existed **just before** the clock edge, not the values the edge produces. That is precisely what you want, because it matches what a flip-flop sees, and it is why an assertion never has a race with the design logic it is checking. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig06.svg" alt="An assertion samples in the Preponed region, so it sees the value that existed just before each clock edge, which is why the evaluation attempt that matters starts at e2 rather than at e1." caption="An assertion samples in the Preponed region, so it sees the value that existed just before each clock edge, which is why the evaluation attempt that matters starts at e2 rather than at e1." id="fig:04-RTL-Design-and-SystemVerilog-6" /> | |
| **`disable iff (!rst_n)`** kills any in-flight evaluation while reset is asserted, and prevents new attempts from starting. Without it, a `req` that was in flight when reset dropped would be scored as a failure, because the grant never arrives, and you would get a wall of spurious failures at every reset in every test. Almost every concurrent assertion in a real design has a `disable iff`, and forgetting it is the single most common reason a newly written assertion produces garbage. | |
| **`req`** is the **antecedent**, the trigger. The property does nothing until this is true at a sampling point. | |
| **`|->`** is **overlapping implication**. When the antecedent matches at cycle $n$, the consequent starts evaluating at cycle $n$, the same cycle. | |
| **`##[1:4] gnt`** is the **consequent**. `##1` means one cycle later, `##[1:4]` means at some cycle between one and four later. So the property reads "whenever `req` is sampled high, `gnt` must be sampled high at some point between one and four cycles after that." If `gnt` arrives at cycle $n+2$, the attempt succeeds and stops looking. If cycle $n+4$ passes with no `gnt`, the attempt fails and the `else` clause fires. | |
| **`a_req_gets_grant:`** is a label. Label every assertion. The label is what appears in the failure message, in the coverage database, and in the formal tool's proof table, and an unlabeled assertion shows up as a file name and line number that means nothing three months later. | |
| ### 7.3 The implication operators, and vacuity | |
| `|->` is **overlapping**. The consequent's cycle zero is the antecedent's cycle. | |
| `|=>` is **non-overlapping**. The consequent starts the next cycle. `a |=> b` is exactly identical to `a |-> ##1 b`, which is the cleanest way to remember it. | |
| Which to use is decided by the hardware. If the response is combinational and appears in the same cycle, use `|->`. If the response comes out of a flop, it appears the next cycle, so use `|=>`. Getting this wrong by one cycle is the second most common assertion bug after a missing `disable iff`. | |
| Now the trap. **An implication whose antecedent never becomes true passes.** Every cycle. Forever. That is called a **vacuous pass** and it is logically correct, since "if A then B" is true whenever A is false, but it is verification-worthless. | |
| So if `req` is never asserted in any test, `a_req_gets_grant` passes on every cycle of every test and reports zero failures, and you have proven nothing whatsoever. The assertion looks like coverage and is not. | |
| The fix is a matching **cover**. | |
| ```systemverilog | |
| c_req_grant_seen: cover property ( | |
| @(posedge clk) disable iff (!rst_n) req ##[1:4] gnt | |
| ); | |
| ```text | |
| A `cover` asks the opposite question. Not "is this always true" but "did this ever happen." If the cover never hits, the assertion above it was vacuous, and you now know it. Pairing every meaningful assertion with a cover for its antecedent is the discipline that separates assertions that verify something from assertions that decorate the file. | |
| ### 7.4 assert, assume, cover, three verbs with three meanings | |
| | Verb | In simulation | In formal | Failure means | | |
| |---|---|---|---| | |
| | `assert` | check it, report a failure if false | try to **prove** it for all reachable states, or produce a counterexample trace | the design is wrong | | |
| | `assume` | usually checked, since the stimulus should obey it | **constrain** the input space, the engine only explores traces where it holds | the environment is wrong, or your model of it is | | |
| | `cover` | record whether it ever happened | try to **find** a trace that reaches it, and produce the trace if it can | the scenario was never exercised, or it is unreachable | | |
| `assert` is the property that must hold. This is the design's obligation. | |
| `assume` is a property the tool is allowed to **take for granted** about the inputs. In simulation the stimulus comes from a testbench that presumably obeys the protocol, so an `assume` is mostly a redundant check. In formal it is completely different, because formal has no testbench. Formal drives the inputs with **every possible sequence of values**, which will include sequences no real system would ever produce, and without constraints you get a mountain of counterexamples where the environment did something impossible. `assume` is how you tell the engine what the environment can actually do. | |
| `cover` asks reachability. In simulation, "did my tests reach this." In formal, "can this ever be reached at all," which is a much stronger question and is how you find dead states, unreachable branches, and, critically, over-constraint. | |
| ### 7.5 The danger of a wrong assume, worked concretely | |
| This is the question that separates people who have run formal from people who have read about it, so have the worked example ready. | |
| You are proving a FIFO. You write the property you care about. | |
| ```systemverilog | |
| a_no_overflow: assert property (@(posedge clk) disable iff (!rst_n) | |
| (count <= DEPTH)); | |
| ```text | |
| The formal engine immediately hands you a counterexample where the environment pushes `DEPTH+1` times without popping. That is not a design bug, it is the environment violating flow control. So you constrain it. | |
| ```systemverilog | |
| m_no_push_when_full: assume property (@(posedge clk) disable iff (!rst_n) | |
| !(push && full)); | |
| ```text | |
| The proof now converges. `a_no_overflow` is **proven**. Green. | |
| Here is what actually happened. You told the engine to only explore traces where a push never occurs while full. The engine obeyed. Within that restricted universe, overflow is impossible **by assumption**, and your assertion was proven in a world where the thing it was checking had been legislated away. If the real producer ever pushes when full, because its `full` signal is pipelined and it sees `full` two cycles late, your proof says nothing about that case. It was never explored. | |
| That is **over-constraint**, and the reason it is so dangerous is that it does not look like a failure. It looks like success. An over-constrained proof is green, fast, and completely empty. The tighter you constrain, the faster it converges and the less it means, so the failure mode rewards exactly the wrong instinct. | |
| Three defenses, and naming all three is a strong answer. | |
| **Cover everything interesting.** If a cover that obviously should be reachable comes back unreachable, an assume killed it. That is the cheapest and most reliable over-constraint detector available. Cover `push && full`, cover the FIFO becoming full, cover simultaneous push and pop, cover reset in the middle of a transfer. | |
| **Read the proof core.** Modern formal tools report which assumptions were actually used in proving each property. An assumption that participates in every proof is doing a great deal of work and deserves scrutiny. | |
| **And the structural discipline, which is the real answer.** **Assert on the driver what you assume on the receiver.** If block B assumes `!(push && full)` on its input, then block A, which drives that input, must carry `assert property (!(push && full))` in its own verification. Now the property is **proven somewhere** rather than believed everywhere. Every assume in the design should have a matching assert on the other side of the wire, and the pair should be tracked as a pair. | |
| This is genuinely your territory. You wrote formal properties for SoC power management logic, which means you have lived the assume problem whether or not you called it that. Power sequencing formal work is almost entirely about what the controller may assume about the fabric and what the fabric may assume about the controller, and the sequencing properties in Part 7.4 of [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) are exactly the shape of property formal proves well and simulation proves badly. Be ready to say what you asserted, what you assumed, how you knew the assumptions were sound, and whether you had the matching assertions on the driver side. | |
| ### 7.6 A small library worth being able to write from memory | |
| These come up constantly and each is one line. | |
| ```systemverilog | |
| // Grant is at most one-hot. $onehot0 allows zero, $onehot requires exactly one. | |
| a_gnt_onehot: assert property (@(posedge clk) disable iff (!rst_n) | |
| $onehot0(gnt)); | |
| // Never grant somebody who did not request. | |
| a_gnt_implies_req: assert property (@(posedge clk) disable iff (!rst_n) | |
| (gnt & ~req) == '0); | |
| // If anybody is requesting, somebody is granted. No idle cycles. | |
| a_no_idle: assert property (@(posedge clk) disable iff (!rst_n) | |
| (|req) |-> (|gnt)); | |
| // Valid-ready stability: once valid is up, data must not change and valid | |
| // must not drop until ready is seen. This is the AXI handshake rule from | |
| // [Interconnect and AMBA](/learn/hardware-interview-prep/interconnect-and-amba) and it is the most-written assertion in SoC work. | |
| a_vr_stable: assert property (@(posedge clk) disable iff (!rst_n) | |
| (valid && !ready) |=> (valid && $stable(data))); | |
| // A FIFO never overflows or underflows. | |
| a_no_overflow: assert property (@(posedge clk) disable iff (!rst_n) | |
| !(push && full && !pop)); | |
| a_no_underflow: assert property (@(posedge clk) disable iff (!rst_n) | |
| !(pop && empty)); | |
| // State register is never unknown after reset. | |
| a_state_known: assert property (@(posedge clk) disable iff (!rst_n) | |
| !$isunknown(state_q)); | |
| // A request eventually retires. $past looks backwards N cycles. | |
| a_no_lost_req: assert property (@(posedge clk) disable iff (!rst_n) | |
| (req && !gnt) |=> req); // req must be held until granted | |
| ```text | |
| The sampled-value functions are worth knowing by name. `$past(sig, n)` gives the value `n` cycles ago. `$rose` and `$fell` detect edges between sampling points. `$stable` and `$changed` compare against the previous sample. `$onehot`, `$onehot0`, `$countones`, and `$isunknown` are the vector predicates. Between those and the implication operators you can express most of what a block-level protocol needs. | |
| --- | |
| ## Part 8, lint, and the discipline that makes RTL reusable | |
| ### 8.1 What lint is and what it catches | |
| **Lint** is a static analyzer that reads RTL and asks structural questions about it without simulating anything and without synthesizing anything. It is the cheapest check in the flow, running in seconds, and it catches a class of bug that no test can reliably find because the bug is about the **shape** of the code rather than about any particular stimulus. | |
| | Finding | What it means | Why it matters | | |
| |---|---|---| | |
| | inferred latch | a combinational block does not assign on every path | Part 4, timing and DFT damage | | |
| | incomplete sensitivity list | old-style `always` missing a read signal | Part 6.2, direct mismatch | | |
| | **width mismatch** | operand and target widths differ | 8.2, silent data corruption | | |
| | multiple drivers | two blocks assign one variable | electrically a short | | |
| | undriven signal | read but never assigned | reads X in simulation, floats in the netlist | | |
| | unused signal | assigned but never read | either dead logic or a forgotten connection | | |
| | combinational loop | a signal depends on itself with no register in the path | may oscillate, and STA cannot analyze it | | |
| | blocking in sequential | `=` inside `always_ff` | Part 3, race | | |
| | non-blocking in combinational | `<=` inside `always_comb` | stale reads and delta-cycle behavior | | |
| | case without default | see 5.6 | latch plus undefined silicon behavior | | |
| | out-of-range index | `mem[i]` where `i` can exceed the array | X in simulation, arbitrary in hardware | | |
| | signed and unsigned mixed | one unsigned operand makes the whole comparison unsigned | negative numbers compare as huge positives | | |
| | clock used as data | a clock net feeding a data pin | breaks clock tree synthesis and CDC analysis | | |
| | clock gated by a plain AND | not through a library ICG cell | glitches and runt pulses, [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) | | |
| | reset polarity mismatch | active-high reset connected to an active-low port | the block never resets, or resets always | | |
| | implicit net declaration | a typo creates a new 1-bit wire instead of an error | classic silent bug, fixed by ``default_nettype none`` | | |
| That last row is worth doing something about right now, because it is free. Putting `` `default_nettype none `` at the top of every file turns every typo in a signal name from "silently creates a one-bit wire" into a compile error. It is one line per file and it eliminates an entire bug family. | |
| A combinational loop is worth one example, since people underestimate it. | |
| ```systemverilog | |
| always_comb begin | |
| a = b | c; | |
| b = a & d; // a depends on b, and b depends on a | |
| end | |
| ```text | |
| There is no register in the loop, so this is a ring of gates feeding itself. In simulation the scheduler may re-trigger the block indefinitely and hang, or may converge to an arbitrary fixed point. In synthesis it becomes an unconstrained combinational ring that static timing analysis cannot analyze at all, since there is no start point and no end point, so the path simply does not appear in any timing report. In silicon it may oscillate at whatever frequency the loop delay implies. Lint catches it in one second. | |
| ### 8.2 Width mismatch, which is the one that really bites | |
| Emphasize this above all the others, because it is the most common **real** bug lint finds, and because silent truncation is genuinely invisible in code review. There is nothing to see. The line looks correct. | |
| ```systemverilog | |
| logic [7:0] a, b; | |
| logic [7:0] sum; | |
| assign sum = a + b; | |
| ```text | |
| Set `a = 8'd200` and `b = 8'd100`. The true answer is 300, which needs nine bits, since $300 = 256 + 32 + 8 + 4 = \texttt{9'b1\_0010\_1100}$. But the expression is evaluated at the **context width**, which here is the maximum of the operand widths and the target width, so eight bits. The result is truncated to `8'b0010_1100`, which is $32 + 8 + 4 = 44$. Equivalently, $300 \bmod 256 = 44$. | |
| Two hundred plus one hundred equals forty-four. No warning, no error, no message. Correct by the language rules and catastrophically wrong by intent. | |
| The fix follows from the same rule that caused it, which is a nice property. **Widen the target and the arithmetic widens with it.** | |
| ```systemverilog | |
| logic [8:0] sum; | |
| assign sum = a + b; // context is now 9 bits, result is 300, correct | |
| ```text | |
| Or capture the carry explicitly, which is the form to prefer because the intent is written down. | |
| ```systemverilog | |
| logic cout; | |
| logic [7:0] sum; | |
| assign {cout, sum} = a + b; // LHS concatenation is 9 bits wide | |
| ```text | |
| The context-width rule is worth stating precisely because it explains both the bug and the fix. **In a context-determined expression, every operand is extended to the width of the widest thing involved, including the assignment target.** Arithmetic, bitwise, and comparison operands are context-determined. Shift right-hand operands, replication counts, and the operands of the concatenation braces are **self**-determined and do not participate. | |
| That last sentence is not pedantry, it is a second real bug. | |
| ```systemverilog | |
| logic carry_out; | |
| logic [7:0] a, b; | |
| assign carry_out = (a + b) >> 8; // ALWAYS ZERO | |
| ```text | |
| The shift's left operand is context-determined, and the context here is the maximum of `carry_out` at one bit and `(a+b)` at eight bits, so eight bits. The addition wraps at eight bits, the nine-bit carry never existed, and shifting an eight-bit value right by eight gives zero. Every time. The synthesizer optimizes `carry_out` to a constant 0 and the logic that consumed it disappears with it. | |
| A third one, which is the version that shows up in FIFOs and is therefore worth recognizing before Part 9. | |
| ```systemverilog | |
| localparam int DEPTH = 16; | |
| logic [3:0] ptr; | |
| if (ptr == DEPTH) ... // NEVER TRUE | |
| ```text | |
| `DEPTH` is an `int`, which is 32 bits. The comparison context is 32 bits, so `ptr` is zero-extended, and a four-bit value can never equal 16. The condition is a constant false. Synthesis deletes the branch and everything downstream of it, silently, and the design is now missing a feature that appears in the source code. | |
| And a fourth, the signedness one. | |
| ```systemverilog | |
| logic [7:0] u; | |
| logic signed [7:0] s; | |
| if (u > s) ... | |
| ```text | |
| If **any** operand in an expression is unsigned, the whole expression is evaluated as unsigned. So `s = -1` is reinterpreted as 255 and compares greater than almost everything. The fix is to keep signed and unsigned quantities apart, use `$signed()` and `$unsigned()` explicitly when they must meet, and let lint flag every mixed comparison. | |
| The reason this section is long is that this single rule family accounts for a large share of the real bugs lint finds in production RTL, and being able to work the 200-plus-100-equals-44 example on a whiteboard is a much better answer than saying "lint catches width mismatches." | |
| ### 8.3 Owning the flow, which is a genuine differentiator | |
| Most candidates have **used** lint. Far fewer have **owned** it, and the difference is visible in about thirty seconds of conversation. | |
| Running lint on a mature design for the first time produces tens of thousands of violations, and that number is why most teams have a lint flow that everybody ignores. Getting from there to a clean gate is not a technical problem, it is a triage and policy problem, and the work looks like this. | |
| **Classify the rules into tiers.** A hard-error tier that blocks the check-in, containing the rules where a violation is essentially always a bug, such as inferred latches, multiple drivers, combinational loops, and blocking in sequential blocks. A must-waive tier where a violation may be legitimate but requires a written, owned, dated waiver, such as intentional truncation or an intentional latch. And an informational tier that is reported and tracked but does not gate anything. | |
| **Make waivers real objects.** A waiver with an owner, a reason, and an expiry date is a decision. A blanket rule disable in a config file is a hole nobody remembers. The difference in a year is enormous. | |
| **Attribute new violations to the commit that introduced them.** This is the change that actually makes the flow stick. Once a new violation is reported against the specific change that created it, rather than appearing in a global count of forty thousand, it gets fixed by the person who caused it while the code is still fresh in their head, and the backlog stops growing while you work it down. | |
| **Hook it into the regression and publish the trend.** A number that goes down every week is a flow people believe in. | |
| You did group-wide lint and physical verification flow upgrade work at Intel. That is flow ownership, and it is worth describing in exactly these terms rather than as "I worked on lint." Say what the tier policy was, how waivers were governed, what the violation count was before and after, and what class of bug it caught that had previously been escaping to synthesis. Interviewers hear "I ran the tool" constantly and "I set the policy the group runs under" rarely. | |
| ### 8.4 Registered outputs at block boundaries, with the arithmetic | |
| This is asked as "why do you register block outputs," and the answer people give is "for timing," which is true and does not demonstrate anything. Do the arithmetic instead. | |
| Two blocks, A and B, on a 3 GHz clock, so the period is $1/(3\times10^9) = 333$ ps. Block A's output is combinational, driven from a flop through 200 ps of logic. The top-level wire between the blocks costs 90 ps. Block B consumes it through 150 ps of logic before its capture flop. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig07.svg" alt="Registering the block output splits one 440 ps path that nobody owns into a 200 ps path inside A and a 240 ps path inside B, each of which fits the 333 ps period and each of which has a single owner." caption="Registering the block output splits one 440 ps path that nobody owns into a 200 ps path inside A and a 240 ps path inside B, each of which fits the 333 ps period and each of which has a single owner." id="fig:04-RTL-Design-and-SystemVerilog-7" /> | |
| The total work did not change. The path was split into two segments, each of which fits, and each of which has a single owner who can close it without talking to anyone. That second property is the one that matters organizationally, and it is the reason the rule is a rule rather than a suggestion. | |
| Four benefits, stated in the order an interviewer will find convincing. **Each timing path has one owner.** **The block can be characterized in isolation**, so its timing contract is "one cycle from my input flop to my output flop" rather than a number that depends on what it is plugged into. **Physical design gets freedom**, because the only thing crossing the boundary is a flop-to-flop path that is mostly wire, so the floorplanner can move the blocks apart without breaking anything inside them. And **the block becomes reusable**, since the next project can integrate it without re-deriving a budget. | |
| The cost is honest and should be stated. One cycle of latency per boundary, and a bank of flops per interface, which for a 140-bit micro-op is 140 flops plus their clock tree. In a deeply pipelined machine that latency is usually free because the pipeline already has the depth. On a latency-critical loop such as a load-to-use path or a branch redirect, it is not free at all, and that is exactly where designers accept an unregistered boundary and pay for it with a negotiated timing budget. Knowing when to break the rule is the senior version of the answer. | |
| ### 8.5 The rest of the reuse discipline | |
| **Parameterize everything and hard-code nothing.** Every width, depth, and count comes from a parameter or from a package constant, and every derived width comes from `$clog2` or `$bits` rather than from a number you typed. The test is whether changing the FIFO depth from 8 to 16 is a one-token edit. If it requires touching a pointer width, an almost-full threshold, and a comparison constant, the module is not parameterized, it is configurable by search-and-replace. | |
| **Separate control from datapath.** The datapath is wide, regular, and dumb. It is adders, muxes, shifters, and register banks, and it is where the area and the power are. The control is narrow, irregular, and where all the bugs are. Keeping them in separate modules or at minimum separate always blocks means the datapath can be optimized, replicated, and physically laid out as a regular structure, and the control can be formally verified as a state machine without dragging 64-bit data through the proof. It also makes clock gating and operand isolation straightforward, because the control block already computes the enables that [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) needs. | |
| **One module per file, and the file is named for the module.** Trivial, and every tool in the flow depends on it. | |
| **Naming conventions, chosen so tools can act on them.** A convention that only helps humans is worth having. A convention that lets a script find things is worth much more. | |
| | Convention | Example | What it buys | | |
| |---|---|---| | |
| | `_n` suffix for active low | `rst_n`, `valid_n` | polarity errors become visible at the connection site | | |
| | `_q` and `_d` for flop output and input | `state_q`, `state_d` | you can tell at a glance whether a signal is registered | | |
| | `_i` and `_o` on ports | `data_i`, `ready_o` | direction is readable inside the body, not just at the header | | |
| | clock and reset named for their domain | `clk_core`, `rst_core_n`, `clk_ref`, `rst_ref_n` | a CDC tool can group signals by domain from the name, and a crossing is visible in the source text | | |
| | `sync_` prefix on synchronizer outputs | `sync_req` | the two-flop boundary is findable by grep and by lint rule | | |
| | `g_` prefix on generate blocks | `g_lane` | readable hierarchy in reports, see 5.3 | | |
| | `a_`, `m_`, `c_` on assert, assume, cover labels | `a_no_overflow` | the verb is visible in every failure message and coverage report | | |
| The domain-suffix convention is the one that is genuinely load-bearing rather than cosmetic. If every signal carries its clock domain in its name, then a clock domain crossing is something you can **see in the source text** at the moment you write it, instead of something a CDC tool tells you about three weeks later, and that visibility is worth more than the tool. | |
| --- | |
| ## Part 9, the live coding exercise | |
| ### 9.1 How to run the forty-five minutes | |
| Expect one. Hardware design roles assume RTL competence, and the fastest way to test it is to watch somebody write forty lines. | |
| The exercise is rarely about whether you know the algorithm. It is about whether you **decide the interface before you write logic**, whether you **say what you are assuming out loud**, and whether you **check your own work**. A candidate who writes a slightly imperfect FIFO and then says "let me trace a full-and-then-pop case to make sure my flags are right" is far ahead of one who writes a perfect FIFO in silence. | |
| Run it in this order. State the interface, including every port and its direction and its width, and write it down before writing any behavior. State the assumptions, such as power-of-two depth, single clock, reset polarity, and what happens on simultaneous push and pop. Write the state elements first, since a sequential design is its registers plus the logic between them. Write the next-state logic. Write the outputs. Then **trace a corner case out loud** with real numbers. Then say what you would assert. | |
| Talking while writing is not a soft skill here. The interviewer is trying to see how you think, and silent code gives them nothing to evaluate. | |
| ### 9.2 Synchronous FIFO, and the pointer problem it is really testing | |
| Start with the problem, because the standard solution looks arbitrary until you have felt the difficulty. | |
| A FIFO of depth 4 needs to index four memory locations, so the obvious pointer width is two bits. Write pointer and read pointer both start at 0. | |
| **Empty** is naturally `wptr == rptr`, since nothing was written that has not been read. | |
| Now push four times. The write pointer goes 0, 1, 2, 3, and then wraps back to 0. The read pointer is still 0. So `wptr == rptr` again, and the FIFO reports **empty** while holding four items. The full condition and the empty condition are the same expression, and there is no way to tell them apart from the pointers alone. | |
| That is the actual content of the question. Every correct answer is a way of distinguishing the two, and the standard one is to make the pointer **one bit wider than it needs to be for addressing**. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig08.svg" alt="Making the pointer one bit wider than addressing needs adds a wrap bit that is not part of the address, and that single extra bit is what tells full apart from empty when the address bits agree." caption="Making the pointer one bit wider than addressing needs adds a wrap bit that is not part of the address, and that single extra bit is what tells full apart from empty when the address bits agree." id="fig:04-RTL-Design-and-SystemVerilog-8" /> | |
| ```systemverilog | |
| module sync_fifo #( | |
| parameter int WIDTH = 32, | |
| parameter int DEPTH = 8 // must be a power of two | |
| ) ( | |
| input logic clk, | |
| input logic rst_n, | |
| input logic push, | |
| input logic [WIDTH-1:0] wdata, | |
| output logic full, | |
| input logic pop, | |
| output logic [WIDTH-1:0] rdata, | |
| output logic empty, | |
| output logic [$clog2(DEPTH):0] count | |
| ); | |
| localparam int AW = $clog2(DEPTH); | |
| logic [WIDTH-1:0] mem [DEPTH]; | |
| logic [AW:0] wptr_q, rptr_q; // AW+1 bits: AW address + 1 wrap | |
| assign empty = (wptr_q == rptr_q); | |
| assign full = (wptr_q[AW] != rptr_q[AW]) && | |
| (wptr_q[AW-1:0] == rptr_q[AW-1:0]); | |
| assign count = wptr_q - rptr_q; // AW+1 bit subtract, gives 0..DEPTH | |
| always_ff @(posedge clk or negedge rst_n) begin | |
| if (!rst_n) begin | |
| wptr_q <= '0; | |
| rptr_q <= '0; | |
| end | |
| else begin | |
| if (push && !full) wptr_q <= wptr_q + 1'b1; | |
| if (pop && !empty) rptr_q <= rptr_q + 1'b1; | |
| end | |
| end | |
| // Memory has no reset. Resetting a RAM is pointless and expensive, | |
| // and the pointers guarantee nobody reads a location never written. | |
| always_ff @(posedge clk) begin | |
| if (push && !full) mem[wptr_q[AW-1:0]] <= wdata; | |
| end | |
| assign rdata = mem[rptr_q[AW-1:0]]; // combinational read, "fall-through" | |
| // ---- checks ---- | |
| a_no_overflow: assert property (@(posedge clk) disable iff (!rst_n) | |
| !(push && full && !pop)); | |
| a_no_underflow: assert property (@(posedge clk) disable iff (!rst_n) | |
| !(pop && empty)); | |
| a_count_range: assert property (@(posedge clk) disable iff (!rst_n) | |
| count <= DEPTH); | |
| a_flags_exclusive: assert property (@(posedge clk) disable iff (!rst_n) | |
| !(full && empty)); | |
| c_becomes_full: cover property (@(posedge clk) disable iff (!rst_n) full); | |
| endmodule | |
| ```text | |
| Now trace it with `DEPTH = 4`, so pointers are three bits, and check the flags at every step. | |
| | Step | Operation | `wptr_q` | `rptr_q` | `empty` | `full` | `count` | | |
| |---|---|---|---|---|---|---| | |
| | 0 | after reset | `000` | `000` | **1** | 0 | 0 | | |
| | 1 | push A | `001` | `000` | 0 | 0 | 1 | | |
| | 2 | push B | `010` | `000` | 0 | 0 | 2 | | |
| | 3 | push C | `011` | `000` | 0 | 0 | 3 | | |
| | 4 | push D | `100` | `000` | 0 | **1** | 4 | | |
| | 5 | pop A | `100` | `001` | 0 | 0 | 3 | | |
| | 6 | push E | `101` | `001` | 0 | **1** | 4 | | |
| | 7 | pop B | `101` | `010` | 0 | 0 | 3 | | |
| | 8 | pop C, D, E | `101` | `101` | **1** | 0 | 0 | | |
| Check step 4 by hand, since that is the whole point. `wptr_q = 100` and `rptr_q = 000`. The wrap bits are `1` and `0`, which differ. The address bits are `00` and `00`, which agree. So full is true. And empty is false because the full three-bit values differ. Both flags correct, from the same pointers that were previously indistinguishable. | |
| Check step 6 the same way. `wptr_q = 101`, `rptr_q = 001`. Wrap bits `1` and `0` differ, address bits `01` and `01` agree, so full again, correctly, because there are four items sitting in locations 1, 2, 3, 0. | |
| And `count` falls out free. It is an `AW+1`-bit subtraction, so it wraps correctly and yields 0 through `DEPTH` with no extra logic. | |
| **The follow-ups the interviewer will ask.** | |
| *What happens on simultaneous push and pop?* In the code above, when the FIFO is neither full nor empty both proceed and the occupancy stays the same, which is right. When it is **full**, the push is rejected even though a pop is freeing a slot in the same cycle. That is conservative and costs one cycle of throughput on a back-to-back full stream. Relaxing it means changing the push condition to `push && (!full || pop)`, which is correct because the read pointer advances on the same edge. Say the tradeoff rather than just picking one. | |
| *Combinational read or registered read?* The code above is **fall-through**, meaning `rdata` shows the head of the queue continuously and `pop` just advances past it. That gives zero read latency, which is what a pipeline stage wants, at the cost of a combinational path from the memory array through to the consumer, which is slow for a real SRAM and which violates the registered-output rule of 8.4. The alternative registers `rdata`, adding one cycle of latency and making the timing clean. Real designs pick per instance. | |
| *What about flow-control latency?* If the producer is two cycles away, it sees `full` two cycles late and can send two more beats after `full` asserts. So you need an `almost_full` that asserts at `DEPTH - 2` and you need those two slots of headroom to actually exist. The general rule is that the FIFO must be deep enough to absorb the round-trip latency of its own backpressure, and getting that number wrong is a real integration bug rather than a coding bug. [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) and [Interconnect and AMBA](/learn/hardware-interview-prep/interconnect-and-amba) have more. | |
| *Why is `DEPTH` required to be a power of two?* Because the wrap-bit trick depends on the address bits rolling over exactly when the pointer overflows. For a non-power-of-two depth you need an explicit occupancy counter instead, incremented on push and decremented on pop, with full and empty compared against it. That is a perfectly good design and costs one adder. | |
| ### 9.3 Round robin arbiter | |
| Motivate it first. A fixed-priority arbiter grants the lowest-numbered requester, always. | |
| ```systemverilog | |
| assign gnt = req & (~req + 1'b1); // isolate the lowest set bit | |
| ```text | |
| Verify the trick on a number before trusting it. With `req = 4'b1010`, the complement `~req` is `0101`, adding one gives `0110`, and `1010 & 0110 = 0010`. That is the lowest set bit, which is requester 1. The identity is that `r & (-r)` isolates the least significant set bit, and it works because two's complement negation flips every bit above the lowest set bit and leaves the lowest set bit itself as the only position where the original and the negation agree. | |
| That is one line and it is completely correct as a priority arbiter. It is also completely unfair. If requester 0 asks every cycle, requesters 1 through 3 **never** get service. Not rarely, never. That is starvation, and in a CPU it means one thread or one port monopolizes a shared resource forever. | |
| Round robin fixes it by remembering who went last and starting the search **after** that point. | |
| ```systemverilog | |
| module rr_arbiter #( | |
| parameter int N = 4 | |
| ) ( | |
| input logic clk, | |
| input logic rst_n, | |
| input logic [N-1:0] req, | |
| output logic [N-1:0] gnt | |
| ); | |
| logic [N-1:0] mask_q; // 1 = this requester is still eligible this round | |
| logic [N-1:0] masked_req; | |
| logic [N-1:0] masked_gnt, unmasked_gnt; | |
| assign masked_req = req & mask_q; | |
| // Lowest-set-bit isolation, applied twice. | |
| // Widths: every operand and the target are N bits, so the context width | |
| // is N and the +1 wraps within N bits. Nothing is truncated. | |
| assign masked_gnt = masked_req & (~masked_req + 1'b1); | |
| assign unmasked_gnt = req & (~req + 1'b1); | |
| // Prefer somebody after the last winner. If nobody is left in this | |
| // round, fall back to plain priority, which IS the wrap-around. | |
| assign gnt = (|masked_req) ? masked_gnt : unmasked_gnt; | |
| always_ff @(posedge clk or negedge rst_n) begin | |
| if (!rst_n) mask_q <= '1; // everybody eligible | |
| else if (|gnt) mask_q <= ~((gnt << 1) - 1'b1); // only indices ABOVE the winner | |
| end | |
| a_gnt_onehot: assert property (@(posedge clk) disable iff (!rst_n) | |
| $onehot0(gnt)); | |
| a_gnt_was_req: assert property (@(posedge clk) disable iff (!rst_n) | |
| (gnt & ~req) == '0); | |
| a_no_idle_grant: assert property (@(posedge clk) disable iff (!rst_n) | |
| (|req) |-> (|gnt)); | |
| endmodule | |
| ```text | |
| The mask update deserves a sentence, because it looks cryptic and is not. `gnt` is one-hot at the winning index $k$. Shifting left by one gives a one-hot at $k+1$. Subtracting one from a one-hot at $k+1$ gives all ones in positions $0$ through $k$. Complementing that gives all ones in positions $k+1$ through $N-1$, which is exactly "everybody strictly after the winner." | |
| Trace it with `N = 4` and `req = 4'b1111` held forever, which is the worst case for fairness. | |
| | Cycle | `mask_q` | `masked_req` | `masked_gnt` | `gnt` | Winner | next `mask_q` | | |
| |---|---|---|---|---|---|---| | |
| | 1 | `1111` | `1111` | `0001` | `0001` | **0** | `~((0010)-1) = ~0001 = 1110` | | |
| | 2 | `1110` | `1110` | `0010` | `0010` | **1** | `~((0100)-1) = ~0011 = 1100` | | |
| | 3 | `1100` | `1100` | `0100` | `0100` | **2** | `~((1000)-1) = ~0111 = 1000` | | |
| | 4 | `1000` | `1000` | `1000` | `1000` | **3** | `~((0000)-1) = ~1111 = 0000` | | |
| | 5 | `0000` | `0000` | `0000` | `0001` | **0** | back to `1110` | | |
| Grant order is 0, 1, 2, 3, 0, 1, 2, 3. Perfectly fair, and nobody waits more than $N-1$ cycles. | |
| Look carefully at cycle 4 into cycle 5. Requester 3 was the last index, so `gnt << 1` shifts the one-hot bit off the top of a four-bit vector and gives zero, then `0 - 1` gives all ones, and complementing gives an all-zero mask. Nobody is eligible. So `masked_req` is zero, the ternary falls through to `unmasked_gnt`, and plain priority picks requester 0. **The wrap-around is not special-cased anywhere.** It falls out of the empty-mask case hitting the fallback path, which is why this implementation is compact. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig09.svg" alt="Each grant clears the winner and everybody below it out of the mask, and when the mask empties the arbiter falls through to unmasked fixed priority, which is where the wrap-around comes from without being special-cased anywhere." caption="Each grant clears the winner and everybody below it out of the mask, and when the mask empties the arbiter falls through to unmasked fixed priority, which is where the wrap-around comes from without being special-cased anywhere." id="fig:04-RTL-Design-and-SystemVerilog-9" /> | |
| Check a sparse request pattern too, since a good arbiter must skip absent requesters rather than wasting a slot on them. With `mask_q = 1110` and `req = 4'b1001`, `masked_req = 1000`, so `masked_gnt = 1000` and requester 3 wins. Requesters 1 and 2 are skipped because they are not asking, not because of anything the mask did. Round robin means fair among **requesting** parties, not a fixed rotation through empty slots. | |
| **Follow-ups worth having ready.** The `~r + 1` form is a **carry chain of length $N$**, so for a 32-way or 64-way arbiter the delay is real and you build a tree-structured find-first-one instead, or split the arbiter hierarchically into groups. The grant here is **combinational** from `req` in the same cycle, which is low latency and puts arbitration on the critical path, and registering the grant costs a cycle and changes the mask update to key off the registered winner. And this is a **masked priority** round robin, which is one of several implementations. A rotating-barrel-shifter version rotates the requests, applies fixed priority, and rotates the grant back. A **matrix arbiter** stores a full pairwise priority relation and gives true least-recently-granted ordering at $O(N^2)$ area. Weighted round robin gives some requesters more slots per round. [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) covers the family, and [Execution Units](/learn/hardware-interview-prep/execution-units) covers why issue-queue arbitration has different constraints again. | |
| ### 9.4 Two-flop synchronizer | |
| Ten lines of code and a great deal of reasoning behind them. The full treatment of metastability is in [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing), and what follows is the coding-exercise version. | |
| ```systemverilog | |
| module sync_2ff #( | |
| parameter int W = 1, | |
| parameter logic RST_VAL = 1'b0 | |
| ) ( | |
| input logic dclk, // DESTINATION clock | |
| input logic drst_n, // DESTINATION reset | |
| input logic [W-1:0] async_in, // from the source domain, unsynchronized | |
| output logic [W-1:0] sync_out | |
| ); | |
| // Tool attributes tell synthesis and place-and-route to use | |
| // metastability-hardened flops and to place them adjacent. | |
| (* ASYNC_REG = "TRUE" *) | |
| logic [W-1:0] meta_q, sync_q; | |
| always_ff @(posedge dclk or negedge drst_n) begin | |
| if (!drst_n) begin | |
| meta_q <= {W{RST_VAL}}; | |
| sync_q <= {W{RST_VAL}}; | |
| end | |
| else begin | |
| meta_q <= async_in; // this flop MAY go metastable | |
| sync_q <= meta_q; // this one gives it a full cycle to settle | |
| end | |
| end | |
| assign sync_out = sync_q; | |
| endmodule | |
| ```text | |
| The things the interviewer is checking for, in order of how often candidates miss them. | |
| **No combinational logic between the two flops.** The entire mechanism is giving the first flop's metastable output a full clock period to resolve exponentially toward a valid level. Insert even one gate and you have shortened that window by the gate delay and lengthened the path, which directly reduces the mean time between failures. This is also why the two flops must be placed physically adjacent, which is what the `ASYNC_REG` attribute and the equivalent synthesis constraints are for. | |
| **This is valid for one bit, not for a bus.** Each bit resolves independently, so a multi-bit value whose bits change at the same time can be sampled mid-transition and produce a combination that **never existed** in the source domain. Take a three-bit counter going from `011` to `100`. All three bits change. Sample it mid-transition and you can get any of the eight values, including `111`, which the counter never held. Multi-bit crossings need either a **gray code**, so only one bit changes per step, or a **handshake**, where a single synchronized control bit certifies that a stable data bus may now be read. Saying this unprompted is a strong signal, because it is the difference between having read about synchronizers and having debugged one. | |
| **The destination clock must be running.** If the destination domain is clock gated, the request sits in `meta_q` forever and never propagates. And if the gating enable is itself derived from that request, you have built a deadlock, which is exactly the failure described in [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating). | |
| **The source must hold long enough.** Going from a slow clock to a fast one is fine, because the destination samples the value several times. Going from a fast clock to a slow one is not, because a single-cycle pulse in the fast domain can fall entirely between two destination edges and vanish. That needs a pulse stretcher, a toggle-based crossing, or a full handshake. | |
| **Two flops is a choice, not a law.** The number of stages sets the mean time between failures, and it is a function of the clock frequency, the data toggle rate, and the flop's metastability resolution time constant. Two is standard. Very high frequency or very high reliability designs use three. | |
| ### 9.5 Asynchronous FIFO, and why gray code | |
| If asked for this one, the interviewer wants the **pointer-crossing argument**, not the memory array. | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig10.svg" alt="Only the pointers cross between the two clock domains, and they cross as gray code through two-flop synchronizers, because the pointers themselves guarantee the reader never reads a location the writer is currently writing." caption="Only the pointers cross between the two clock domains, and they cross as gray code through two-flop synchronizers, because the pointers themselves guarantee the reader never reads a location the writer is currently writing." id="fig:04-RTL-Design-and-SystemVerilog-10" /> | |
| Why binary pointers fail. A three-bit binary pointer going from 3 to 4 changes `011` to `100`, which is **three bits at once**. The other domain samples asynchronously, so it can catch the transition partway through and read any of `000` through `111`. A pointer value of `111` when the true value is 4 means the receiving domain computes a wildly wrong occupancy and can conclude the FIFO is empty when it is not, which corrupts data. | |
| Why gray code works. **A gray code changes exactly one bit per increment.** So a mid-transition sample sees either the old value or the new value, never a third one, because there is only one bit in flight and it is either caught or missed. | |
| The three-bit gray sequence, generated by $g = b \oplus (b \gg 1)$. | |
| | Binary $b$ | $b \gg 1$ | Gray $g = b \oplus (b{\gg}1)$ | | |
| |---|---|---| | |
| | `000` | `000` | `000` | | |
| | `001` | `000` | `001` | | |
| | `010` | `001` | `011` | | |
| | `011` | `001` | `010` | | |
| | `100` | `010` | `110` | | |
| | `101` | `010` | `111` | | |
| | `110` | `011` | `101` | | |
| | `111` | `011` | `100` | | |
| Check a couple by hand. For $b = 3 = \texttt{011}$, $b \gg 1 = \texttt{001}$, and $\texttt{011} \oplus \texttt{001} = \texttt{010}$, which matches the table. Read the gray column down and confirm that every adjacent pair differs in exactly one bit, and that the wrap from the last entry `100` back to the first `000` also differs in exactly one bit. That last property is why it is called a cyclic gray code and it is the reason a wrapping pointer is safe. | |
| Converting back is $b_{MSB} = g_{MSB}$ and $b_i = b_{i+1} \oplus g_i$, which is a small ripple chain. | |
| The comparisons, using pointers that are `AW+1` bits wide exactly as in the synchronous FIFO. | |
| ```systemverilog | |
| // In the READ domain, comparing against the synchronized write pointer: | |
| assign empty = (rptr_gray == wptr_gray_r); | |
| // In the WRITE domain, comparing against the synchronized read pointer: | |
| assign full = (wptr_gray == {~rptr_gray_w[AW:AW-1], rptr_gray_w[AW-2:0]}); | |
| ```text | |
| Empty is a plain equality, because gray code preserves equality. Full is stranger and the top **two** bits get inverted rather than one. The reason is that full means the pointers are one full lap apart, which in binary is the wrap bit differing and the address bits agreeing, and translating that condition through the gray transform flips the top two bits because gray code is reflected at the halfway point. | |
| Check it with `DEPTH = 4`, so `AW = 2` and pointers are three bits. After four pushes and no pops, `wptr_bin = 100` and `rptr_bin = 000`. Gray of `100` is `110`, from the table. Gray of `000` is `000`. The full condition computes `{~rptr_gray_w[2:1], rptr_gray_w[0]}` which is `{~00, 0}` which is `110`. And `wptr_gray` is `110`. They match, so full asserts, correctly. Try three pushes instead. `wptr_bin = 011`, gray `010`, which does not equal `110`, so not full, correctly. | |
| The correctness argument that ties it together, and it is the thing worth saying out loud. **Both flags are conservative in the safe direction.** The synchronized pointer is always stale by two destination cycles, so the reader may think the FIFO is emptier than it really is and the writer may think it is fuller than it really is. Being told "empty" when a word has just arrived costs you latency. Being told "full" when a slot has just freed costs you throughput. Neither costs you data. The errors can never point the other way, because a stale pointer is always **behind**, and behind is always the safe side. That single paragraph is the whole reason the design is correct despite crossing clock domains. | |
| ### 9.6 A small FSM | |
| The three coding styles get argued about. Use the **two-block** style, which puts the state register in one `always_ff` and everything else in one `always_comb`. | |
| ```systemverilog | |
| typedef enum logic [1:0] { | |
| S_IDLE = 2'b00, | |
| S_REQ = 2'b01, | |
| S_WAIT = 2'b10, | |
| S_DONE = 2'b11 | |
| } state_e; | |
| state_e state_q, state_d; | |
| // Block 1: the state register. Trivially reviewable, three lines. | |
| always_ff @(posedge clk or negedge rst_n) begin | |
| if (!rst_n) state_q <= S_IDLE; | |
| else state_q <= state_d; | |
| end | |
| // Block 2: next state and outputs. | |
| always_comb begin | |
| state_d = state_q; // DEFAULT: hold. Kills latches on state_d. | |
| req = 1'b0; // DEFAULT outputs. Kills latches on outputs. | |
| done = 1'b0; | |
| unique case (state_q) | |
| S_IDLE: if (start) state_d = S_REQ; | |
| S_REQ: begin | |
| req = 1'b1; | |
| if (ack) state_d = S_WAIT; | |
| end | |
| S_WAIT: if (rvalid) state_d = S_DONE; | |
| S_DONE: begin | |
| done = 1'b1; | |
| state_d = S_IDLE; | |
| end | |
| default: state_d = S_IDLE; // defined behavior for an illegal state | |
| endcase | |
| end | |
| ```text | |
| <Figure src="/figures/hardware-interview-prep/iv-04-RTL-Design-and-SystemVerilog-fig11.svg" alt="The four-state machine advances only when its input condition holds and otherwise stays put, and the outputs are Moore, so they are decoded from the state alone rather than from the inputs." caption="The four-state machine advances only when its input condition holds and otherwise stays put, and the outputs are Moore, so they are decoded from the state alone rather than from the inputs." id="fig:04-RTL-Design-and-SystemVerilog-11" /> | |
| Every construct in that block is doing a job. The `typedef enum` gives the states names that appear as names in the waveform viewer, which saves real minutes every debug session. `state_d = state_q` at the top is the hold default, so any state whose case branch has no matching condition simply stays put, and no latch can be inferred on the next-state variable. The output defaults do the same job for `req` and `done`. The `unique` tells synthesis the branches are exclusive so it can build a flat mux, and the `default` gives the illegal fourth-encoding case defined behavior in silicon rather than relying on the `unique` promise, which 5.6 showed is not enough on its own. | |
| **Moore versus Mealy**, since it gets asked. A **Moore** output depends only on the current state, so it comes straight out of the state flops through a small decode and it is stable for the whole cycle. A **Mealy** output depends on the state **and** the current inputs, so it responds one cycle earlier but it is a combinational function of an input, which means it glitches whenever that input glitches and it creates a combinational path straight through the block. Mealy outputs at a block boundary are exactly what 8.4 says not to do. Use Moore at boundaries, allow Mealy internally when the cycle matters. | |
| **State encoding**, also asked. | |
| | Encoding | Bits for 8 states | Next-state logic | Best for | | |
| |---|---|---|---| | |
| | binary | 3 | must decode all 3 bits to identify a state, so the decode is wide | small FSMs, area-constrained blocks | | |
| | one-hot | 8 | state test is a single bit, so next-state terms are shallow ORs of ANDs | speed-critical FSMs, FPGAs where flops are plentiful | | |
| | gray | 3 | same as binary | state values crossing a clock domain, and lower switching power since one bit toggles per transition | | |
| One-hot is the interesting answer, because the tradeoff is not obvious. It uses more flops, which sounds worse, but it makes every next-state expression shallow, since asking "am I in state five" is reading one bit instead of comparing three, so the combinational depth drops and the FSM runs faster. For an FSM in a critical loop that is a good trade. It also pairs naturally with `unique case` and with a one-hot assertion, `assert property ($onehot(state_q))`, which catches a corrupted state immediately. | |
| ### 9.7 Find the bug | |
| The other exercise format. A snippet is put in front of you and you have thirty seconds. In practice the bug is almost always a blocking assignment in sequential logic or an inferred latch, so check those two first, every time, before reading anything else. Here are six with their answers. | |
| ```systemverilog | |
| // (1) | |
| always_ff @(posedge clk) begin | |
| stage1 = din; | |
| stage2 = stage1; | |
| stage3 = stage2; | |
| end | |
| ```text | |
| Blocking in a sequential block. All three flops load `din` on the same edge, so the intended three-deep delay line collapses to a fanout. Fix by changing `=` to `<=`. This is 3.3 with one extra stage. | |
| ```systemverilog | |
| // (2) | |
| always_comb begin | |
| case (sel) | |
| 2'b00: out = in0; | |
| 2'b01: out = in1; | |
| 2'b10: out = in2; | |
| endcase | |
| end | |
| ```text | |
| Inferred latch on `out` for `sel = 2'b11`. Fix with a default assignment at the top, or a `default` branch, or preferably both. | |
| ```systemverilog | |
| // (3) | |
| always @(a or b) begin | |
| y = (a & b) | c; | |
| end | |
| ```text | |
| Incomplete sensitivity list. `c` is read but not listed, so simulation misses updates that synthesis will implement. Fix by using `always_comb`. | |
| ```systemverilog | |
| // (4) | |
| always_ff @(posedge clk or negedge rst_n) begin | |
| if (!rst_n) count_q <= reset_value; | |
| else count_q <= count_q + 1'b1; | |
| end | |
| ```text | |
| An asynchronous reset must assign a **constant**. There is no standard cell with an asynchronous parallel-load pin. Either use a synchronous reset, which can load anything because it goes through the data mux, or reset to zero and load `reset_value` on a separate synchronous condition. | |
| ```systemverilog | |
| // (5) | |
| logic [7:0] a, b, sum; | |
| assign sum = a + b; | |
| assign carry = sum[8]; | |
| ```text | |
| Two bugs stacked. `sum` is eight bits so `sum[8]` does not exist, and the addition truncated the carry away before anyone could look for it. This is 8.2. Fix with `assign {carry, sum} = a + b;`. | |
| ```systemverilog | |
| // (6) | |
| always_ff @(posedge clk) if (en) q <= d; | |
| always_ff @(posedge clk) if (clr) q <= 1'b0; | |
| ```text | |
| Two blocks driving one variable. Physically that is two logic cones wired to one flop input, which does not exist. In `always_ff` this is a compile error, which is one more argument for the intent-declaring blocks. Merge into one block with a priority ordering that says explicitly which of `clr` and `en` wins. | |
| --- | |
| ## Part 11, check yourself | |
| Answer out loud, in full sentences, as if an interviewer asked. If you cannot, reread the section named. | |
| 1. Explain what `always_ff @(posedge clk) count <= count + 1'b1;` actually instantiates, and why "do this every clock" is the wrong reading. (1.1) | |
| 2. Name the simulation regions of one time step in order, and say exactly which region a blocking assignment takes effect in and which two regions a non-blocking assignment splits across. (3.2) | |
| 3. Write a two-flop shift register with non-blocking and then with blocking, draw both circuits, and say why they are different hardware rather than different styles. Then say what happens if you reverse the order of the two blocking statements. (3.3) | |
| 4. Two `always` blocks trigger on the same edge and both use blocking assignments on a shared signal. What does the standard say about the evaluation order, and why does non-blocking make the question irrelevant? (3.1, 3.4) | |
| 5. Show three lines of code that infer a latch, explain why the tool has no alternative interpretation, and give three fixes with the reason you would prefer one of them. (4.1, 4.4) | |
| 6. Why does a `for` loop inside `always_comb` require blocking assignments while a `for` loop inside `always_ff` requires non-blocking? Work the four-stage delay line both ways. (5.2) | |
| 7. What does `unique case` assert, and what happens in simulation, in the netlist, and in silicon when the assertion turns out to be false? (5.6) | |
| 8. Explain X-optimism with a worked example where every regression test passes and some fraction of silicon parts hang at boot. Then say how the same mux written as `if/else` and as `?:` behaves differently. (6.4) | |
| 9. What is X-pessimism, where does it show up, and how is it the opposite problem from X-optimism? (6.5) | |
| 10. Take the property `@(posedge clk) disable iff (!rst_n) req |-> ##[1:4] gnt` apart piece by piece. In which region are the signals sampled, what breaks without the `disable iff`, and what is the difference between `|->` and `|=>`? (7.2, 7.3) | |
| 11. Distinguish `assert`, `assume`, and `cover`. Then explain, with a concrete FIFO example, why a wrong `assume` produces a green proof that proves nothing, and give three ways to detect it. (7.4, 7.5) | |
| 12. Work the arithmetic on why `logic [7:0] sum; assign sum = a + b;` gives 44 when `a` is 200 and `b` is 100, state the context-width rule precisely, and give two ways to fix it. (8.2) | |
| 13. Two blocks with 200 ps, 90 ps, and 150 ps of path on a 333 ps clock. Show why the unregistered boundary fails, why the registered one closes, and what the cost is. Then say when you would deliberately not register the boundary. (8.4) | |
| 14. A FIFO of depth 4 with 2-bit pointers cannot distinguish full from empty. Show why, then show how the extra pointer bit fixes it, and trace the flags through four pushes and four pops. (9.2) | |
| 15. Why must there be no logic between the two flops of a synchronizer, why is it wrong for a multi-bit bus, and why do asynchronous FIFO pointers therefore use gray code? (9.4, 9.5) | |
| --- | |
| ## Part 12, related notes | |
| - [Verification Methodology](/learn/hardware-interview-prep/verification-methodology) for where assertions, formal, coverage, and gate-level simulation sit in a real verification plan, and for the UVM material this note deliberately does not cover | |
| - [STA Synthesis and Physical Design](/learn/hardware-interview-prep/sta-synthesis-and-physical-design) for what synthesis does with this code, what static timing analysis reports back, and why the registered-boundary argument in 8.4 is really a timing-closure argument | |
| - [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) for the arbiter and FIFO family in depth, including matrix arbiters, weighted schemes, and the CAM structures the coding exercise sometimes reaches for | |
| - [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing) for metastability itself, the mean-time-between-failures arithmetic behind the two-flop synchronizer, reset synchronization, and glitch-free clock muxing | |
| - [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) for the clock gating cell that contains the one latch you actually want, for why gating a synchronizer deadlocks, and for the power sequencing properties that formal proves well | |
| - [Digital Logic and Timing](/learn/hardware-interview-prep/digital-logic-and-timing) for gates, delay, glitches, setup and hold, and the latch-versus-flip-flop distinction that Part 4 depends on | |
| - [DFT and Silicon Debug](/learn/hardware-interview-prep/dft-and-silicon-debug) for why an inferred latch costs test coverage and for the observability work that connects to the debug triggers | |
| - [Interconnect and AMBA](/learn/hardware-interview-prep/interconnect-and-amba) for the valid-ready handshake that the interface in 5.4 and the stability assertion in 7.6 are checking | |
| - [Arithmetic Hardware](/learn/hardware-interview-prep/arithmetic-hardware) for what the `*` operator actually builds and why divide is banned in most coding standards | |
| - [Programming and Tooling](/learn/hardware-interview-prep/programming-and-tooling) for the scripting side of a lint and regression flow | |
| - [Hardware Description Languages](/learn/computer-architecture/hardware-description-languages) for the vault's language survey and the simulation-versus-synthesis framing | |
| - [Sequential Logic and Timing](/learn/computer-architecture/sequential-logic) for the vault's treatment of flip-flops, latches, and setup and hold | |
| - [Lab --- The Open-Source HDL Workflow](/learn/computer-architecture/lab-hdl-workflow) for a zero-cost Yosys and Icarus and Verilator flow to actually run the Part 9 code in |