Volume 11 Interview 40 problems ~70 min

The Big-Tech Silicon Interview Master Vault

Forty curated practice problems modeling the core digital design and verification principles evaluated across leading semiconductor engineering teams - twenty-five on design, fifteen on verification - followed by three real silicon failure case studies. Attempt each one before opening the answer. Every solution links back to the volume that derives it.

What is actually being tested Almost none of these questions have a hard answer. They are set because the follow-up is where the signal is: why that structure? what does it cost? what breaks at 2 GHz? how would you verify it? Answer in one sentence, then volunteer the trade-off. That pattern is what separates a candidate who memorised a solution from one who understands it.

11.1 Twenty-five digital design problems

25 problems

Gate-level & combinational

1 Build a 4-input AND gate using only 2:1 multiplexers.

A 2:1 MUX computes Y = S ? B : A. Tie A = 0 and it becomes Y = S & B - an AND gate. So each AND costs one MUX with its low input grounded.

For four inputs build a tree: two MUXes produce a&b and c&d, and a third ANDs those. Three MUXes, two levels deep.

Volunteer the follow-up: "A chain also works with three MUXes but is three levels deep. The tree is the same area and one level faster." → Volume 2.1

2 Implement XOR using only 2-input NAND gates. Minimum count?

Four NANDs.

n1 = NAND(a, b)
n2 = NAND(a, n1)
n3 = NAND(b, n1)
y  = NAND(n2, n3) = a ⊕ b

Expanding, n2 = (a·b')' and n3 = (a'·b)', so the final NAND gives a·b' + a'·b - XOR.

Follow-up: "Why does anyone care about NAND-only?" NAND is functionally complete and, in static CMOS, cheaper than AND or OR - an AND is physically a NAND followed by an inverter. Standard cell libraries are NAND/NOR heavy for that reason. → Volume 2.1

3 Build a 2:1 MUX from NAND gates only.

Start from Y = (A·~S) + (B·S) and apply De Morgan:

Y = NAND( NAND(A, ~S), NAND(B, S) )

With ~S = NAND(S, S) that is four NAND gates: one inverter, two product terms, one combiner. → Volume 2.1

4 Detect the leading '1' in an 8-bit value, MSB to LSB.

A priority encoder. casez expresses it literally:


always @(*) begin
  valid = |din;
  casez (din)
    8'b1???????: pos = 3'd7;
    8'b01??????: pos = 3'd6;
    8'b001?????: pos = 3'd5;
    8'b0001????: pos = 3'd4;
    8'b00001???: pos = 3'd3;
    8'b000001??: pos = 3'd2;
    8'b0000001?: pos = 3'd1;
    8'b00000001: pos = 3'd0;
    default:     pos = 3'd0;
  endcase
end

Say the trade-off: "Priority is real logic - it costs depth, because each bit must know no higher bit was set. For a wide input I would build it as a tree of 'any bit set in this half' terms rather than a linear chain." Also mention valid: without it, position 0 is ambiguous with "no bits set". → Volume 2.1

5 Count the 1s in a 32-bit vector with minimum delay.

A balanced adder tree, not serial accumulation. Pair the 32 bits into 16 two-bit sums, then 8 three-bit, 4 four-bit, 2 five-bit, 1 six-bit - 5 levels, not 32. Widths grow only as needed, so early levels are nearly free.

Pre-empt the follow-up: "At 2 GHz I would pipeline it - five levels makes a natural two- or three-stage pipeline." → Volume 2.5

6 Detect whether a number is a power of two.
is_pow2 = (n != 0) && ((n & (n - 1)) == 0)

Subtracting one from a power of two clears the single set bit and sets everything below it, so the AND is zero. The n != 0 term is required - zero would otherwise pass.

The same expression checks that a one-hot FSM state has not been corrupted. → Volume 4.5

7 Detect signed overflow in an adder.
V = Cin(MSB) ⊕ Cout(MSB)

Equivalently: adding two numbers of the same sign must produce that sign. If it does not, the result wrapped. Opposite-signed operands can never overflow.

The distinction being tested: "C_out alone signals unsigned overflow. The adder hardware is identical for signed and unsigned - signedness exists only in how you interpret the result, which is why an ALU produces both flags and lets the instruction choose." → Volume 2.3

8 Turn an adder into a subtractor without a second adder.
A - B = A + (~B) + 1

wire [W-1:0] b_in = b ^ {W{sub}};        // XOR row: inverts when sub = 1
assign {cout, result} = a + b_in + sub;  // the spare carry-in supplies the +1

Cost: W XOR gates. Volunteer this: "For subtraction cout is a not-borrow flag - it is 1 when A ≥ B unsigned, which is how unsigned less-than comes out for free." → Volume 2.2

9 Why is a carry lookahead adder faster, and what does it cost?

A ripple carry adder's carry is a serial dependency - bit i waits for bit i-1, so delay is O(N). CLA computes G = A·B and P = A ⊕ B for every bit simultaneously (they depend only on the inputs), then derives each carry as a flat sum-of-products. Delay becomes O(log N).

The cost, raised unprompted: "A flat 64-bit CLA would need a 64-input AND gate and p[0] driving 64 loads - both physically impossible. Real adders build it in 4-bit blocks with group generate and propagate, then a second level of lookahead across the groups." → Volume 2.2

10 Design a parameterized barrel shifter.

Decompose the shift amount into its binary digits and build one stage per digit: stage i shifts by 2i or passes through, controlled by shamt[i].

log₂(N) stages of N 2:1 MUXes, and the delay does not depend on how far you shift. Replace the fill bits with the bits falling off the bottom and the same structure becomes a rotator. → Volume 2.3

Sequential & clocking

11 Design an edge detector - rising, falling, and both.

Register the signal once and compare it with itself:


reg sig_d;
always @(posedge clk or negedge rst_n)
  if (!rst_n) sig_d <= 1'b0;
  else        sig_d <= sig_in;

assign rise = sig_in & ~sig_d;   //  0 -> 1
assign fall = ~sig_in & sig_d;   //  1 -> 0
assign both = sig_in ^ sig_d;    //  any change

Two follow-ups to pre-empt: "If sig_in is asynchronous it must pass through a 2-FF synchronizer first, or the flop can go metastable. And these outputs are combinational, so they glitch - if the consumer is in another domain, register them." → Volume 5.2

12 Generate a 25 MHz clock with 50% duty from 100 MHz.

Divide by 4 - an even divisor, so two cascaded toggle flip-flops give an exact 50% duty cycle by construction. No counter decode needed.

Open with this instead and you will stand out: "Before dividing I would ask whether a real 25 MHz clock is needed or just slower logic. A derived clock creates a second domain with its own skew and constraints. If the logic just needs to run four times slower, a clock enable on the original 100 MHz clock is strictly better - one domain, no CDC, trivial timing." → Volume 3.5

13 Now divide by 3 with an exact 50% duty cycle.

Odd division needs the output high for 1.5 input periods, and you cannot get half a period from rising edges alone - so use the falling edge too.

Run two modulo-3 counters, one on posedge and one on negedge, decode each to a wave high for 2 of 3 states, and AND them. The negedge copy is offset by half a period, so the AND is high for 2T - 0.5T = 1.5T out of 3T - exactly 50%.

Say this unprompted and you have shown you have built one: "That final AND is a combinational gate on a clock, so it can glitch. In an ASIC flow it must be a proper clock-gating cell from the library, not an ordinary AND2." → Volume 3.5

14 Design an asynchronous reset synchronizer. Why does recovery time matter?

reg [1:0] sync;
always @(posedge clk or negedge arst_n)
  if (!arst_n) sync <= 2'b00;              // ASSERT: async, immediate
  else         sync <= {sync[0], 1'b1};    // DE-ASSERT: shifts in on clock

assign rst_n = sync[1];

Recovery is the minimum time reset must be de-asserted before the next clock edge - the setup-time equivalent for the reset pin. Violate it and the flop can go metastable on reset release.

The systemic reason it matters: "Reset reaches tens of thousands of flops through different wire delays. If it releases near a clock edge, some flops leave reset a cycle before others - half the design starts on cycle N and half on N+1. Asserting asynchronously is safe; the release is the dangerous half. And you need one synchronizer per clock domain." → Volume 3.2

15 How does an LFSR guarantee maximal length? What if the state is all zeros?

Maximal length comes from tap positions corresponding to a primitive polynomial over GF(2) for that width. The sequence then visits 2ⁿ - 1 states before repeating.

All zeros is a dead state. The XOR of any number of zeros is zero, so the register shifts in a zero forever. That is precisely why the period is 2ⁿ - 1 and not 2ⁿ.

The variant they probe with: "With XNOR feedback the dead state is all ones instead, for the same reason. FPGAs often prefer XNOR because registers power up to zero, so the LFSR self-starts without an explicit non-zero seed." → Volume 3.4

16 Design a switch debouncer.

Three stages, in this order:

  1. Synchronize - the switch is asynchronous, so two flops first.
  2. Filter - require the synchronized level to be stable for N counts (typically 10-20 ms worth) before accepting it.
  3. Edge-detect - produce a one-cycle pulse from the accepted level.

The point of the question is whether you remember step 1. A debouncer that filters an unsynchronized input still has a metastable flop at its front door. → Volume 5.2

17 Why use a Gray counter instead of a binary one?

A binary counter can change several bits at once - 0111 → 1000 flips four. Another clock domain sampling mid-transition can read a value that never existed. A Gray counter changes exactly one bit per step, so a mistimed sample returns either the old value or the new one.

gray = bin ^ (bin >> 1)

Two caveats worth volunteering: "Keep the binary counter alongside - you need binary for arithmetic and Gray only for crossing. And single-bit stepping only holds when the count wraps at a power of two." → Volume 3.3

18 Convert Gray code back to binary.

An XOR prefix chain - each binary bit is the XOR of all Gray bits from the MSB down:

bin[MSB] = gray[MSB]
bin[i]    = gray[i] ⊕ bin[i+1]

The asymmetry is the interesting part: "Binary→Gray is one XOR per bit in parallel. Gray→binary is a serial XOR chain, O(N) deep unless built as a prefix tree. That is why an async FIFO Gray-codes on the way out and never converts back on the far side." → Volume 6.3

19 Design a round-robin arbiter.

A fixed-priority arbiter always grants the lowest-numbered requester, so a busy low channel starves the others. Round-robin rotates the priority base: after granting channel k, priority starts at k+1 next time.

The standard implementation is two priority encoders - one masked to requests above the pointer, one unmasked - taking the masked result if any, otherwise the unmasked one (which wraps around).

Volunteer the property being tested: "The guarantee is bounded wait - no requester waits more than N-1 grants. That is what makes it fair, and why round-robin appears in every shared-bus and NoC design." → Volume 4.4

20 Design an overlapping 1011 sequence detector.

The method matters more than the answer. Name each state after the longest prefix of 1011 the stream currently ends with: - , 1, 10, 101, 1011. Then for each state and input bit, append it and ask what the longest prefix is now. Transitions fall out mechanically and overlap handling is automatic.

From 1011 on a 0 you go to state 10, because the stream now ends …10110 whose longest useful suffix is 10.

Moore needs 5 states and asserts a cycle later; Mealy needs 4 and asserts in the same cycle, at the cost of a combinational input-to-output path. → Volume 4.1

Timing, CDC & memory

21 Compute setup and hold slack for a datapath with skew.
Setup T + tskew - tsu - tunc - tcq - tcomb(max)
Hold tcq + tcomb(min) - th - tskew - tunc

Worked: T = 5 ns, skew = 0.10, tsu = 0.15, tunc = 0.05, tcq = 0.25, tcomb = 3.80 → setup slack = +0.85 ns, Tmin = 4.15 ns → fmax ≈ 241 MHz.

Point at what is missing: "T does not appear in the hold equation. A hold violation therefore fails at every frequency including DC, and positive skew helps setup while hurting hold." → Volume 7.2

22 Crossing 200 MHz → 100 MHz - can data be lost?

Yes. Two separate problems, and candidates usually name only the first.

  1. Metastability - solved by a two-flop synchronizer, which gives the first flop a full destination period to resolve.
  2. Missed data - not solved by a synchronizer. The destination samples half as often, so a one-cycle pulse in the fast domain (5 ns) can vanish entirely between two 10 ns destination edges. A level must be stable for at least 1.5 destination clock periods to be caught reliably.

The fix depends on the traffic: toggle synchronizer for single pulses, handshake for occasional multi-bit transfers, asynchronous FIFO for sustained throughput. → Volume 5.2

23 Why can't you put a 2-FF synchronizer on each bit of a bus?

Because each bit resolves its metastability independently. Bits that left the source on the same edge can arrive on different edges, so the destination latches a mixture of old and new - a value that was never on the bus.

Concretely: a counter crossing 0111 → 1000 where bit 3 resolves a cycle early is sampled as 1111. Every individual synchronizer worked perfectly; the system is still broken.

The unifying answer: "Gray code, a held-stable bus with a synchronized flag, a handshake, or an async FIFO - they all work by reducing the crossing to a single bit." → Volume 5.3

24 Design a synchronous FIFO and derive its full and empty conditions.

Both full and empty put the pointers at the same address, so the address bits alone cannot distinguish them. Make each pointer one bit wider than the address - that bit counts laps.

empty = (wptr == rptr)
full  = (wptr[AW] != rptr[AW]) && (wptr[AW-1:0] == rptr[AW-1:0])
count = wptr - rptr

count works because two's complement subtraction wraps correctly. Also qualify the enables - do_wr = wr_en && !full - so a write when full is a harmless no-op rather than silent corruption. → Volume 6.2

25 Size a FIFO for a 120-item burst: writer 80 MHz, reader 50 MHz.
Depth = B × (1 - Fread / Fwrite)

Burst duration = 120 / 80 MHz = 1.5 µs. Items read in that time = 50 MHz × 1.5 µs = 75. Depth = 120 - 75 = 45 → round up to 64 for a Gray-pointer FIFO.

Use effective rates: if the writer only writes on one clock in two, its effective rate is 40 MHz, not 80. Round the read count down - assuming the reader kept up better than it did would under-size the FIFO.

The trap version: "What if the writer never stops?" Then no finite depth works - occupancy grows without bound. FIFO depth only solves bursts. Sustained overload needs back-pressure, or a defined drop policy if the source cannot be stalled. → Volume 6.4

11.2 Fifteen SystemVerilog verification problems

15 problems
1 Difference between logic and bit?

logic is 4-state (0, 1, X, Z); bit is 2-state (0, 1). bit simulates roughly twice as fast and uses half the memory.

The answer that matters: "Use logic in RTL and on the DUT boundary, because X propagation is a free bug detector - an unreset register shows XXXX and fails the test. A 2-state variable silently reads 0, which often matches what the test expected, so a genuine missing-reset bug passes simulation and shows up in the lab. Use 2-state for testbench internals like loop counters where X is meaningless." → Volume 8.1

2 Difference between new() and new[]?

new() is a class constructor - it allocates an object and returns a handle. new[n] sizes a dynamic array.


Transaction t = new();       // construct one object
int q[];  q = new[8];        // allocate 8 array elements
q = new[16](q);              // resize to 16, PRESERVING the old contents

The third form is the one candidates miss: new[16](q) copies the existing elements across. Without the argument, resizing discards everything.

3 join_any versus join_none?

join_any blocks the parent until the first child finishes. join_none does not block at all - the parent continues immediately and the children run when it next blocks.

Two things to add. After join_any you almost always want disable fork to kill the losers, or a stale watchdog fires on a later transaction. And with join_none inside a loop, declare automatic int idx = i; in the loop body - otherwise every spawned thread sees i's final value. → Volume 9.1

4 What happens if a base-class method is not declared virtual?

The call resolves from the type of the handle, not the object. A Base handle pointing at a Derived object calls Base's implementation and the override is ignored.

Lead with the failure mode: "The dangerous part is that nothing errors. Your derived driver is constructed, connected, and never called - and the test reports a pass while exercising base behaviour. That is why UVM marks nearly everything virtual." → Volume 9.2

5 Shallow copy versus deep copy of a class handle?

t2 = t1 copies the handle - both point at the same object, and modifying one changes "both". A shallow copy duplicates the object's fields but any nested handles still point at the originals. A deep copy clones the nested objects too.

The bug this causes: "Pushing the same handle into a mailbox twice and then randomising it gives you two references to one randomised object. Every UVM sequence item has copy() and clone() for exactly this reason." → Volume 9.2

6 How does randc work, and what happens when permutations are exhausted?

randc is random-cyclic: it visits every value in its range in a random permutation before repeating any. A 4-bit randc produces all 16 values in random order, then reshuffles and starts a new permutation.

The practical limits: "It applies to integral types and, in practice, modest widths - the solver tracks the whole permutation, so a 32-bit randc is not viable. And the cyclic property applies to that variable alone; it says nothing about combinations with other rand variables." → Volume 9.4

7 Write a constraint for 10 unique random numbers between 1 and 100.

class Gen;
  rand int unsigned vals[10];

  constraint c_range  { foreach (vals[i]) vals[i] inside {[1:100]}; }
  constraint c_unique { unique { vals }; }      // SystemVerilog-2012
endclass

Without unique support, express it pairwise - but say why you would avoid it:


constraint c_uniq_pairs {
  foreach (vals[i])
    foreach (vals[j])
      if (i < j) vals[i] != vals[j];
}

It is O(n²) constraints and the solver slows sharply as the array grows. → Volume 9.4

8 What does solve … before actually do?

It changes the probability distribution, never which solutions are legal. The solver picks the named variable first, then the rest consistently with it.

Classic case: rand bit a; rand bit [7:0] b; constraint { a -> b == 0; }. There are 257 legal (a,b) pairs and only one has a=1, so a uniform solver gives P(a=1) = 1/257 ≈ 0.4%. Add solve a before b and it becomes 50%.

Why it matters: "A constraint that reads like a coin flip can leave a scenario essentially untested. Cross coverage is what exposes it." → Volume 9.4

9 How do you know randomize() failed?

It returns 0. It does not throw, and an unchecked call leaves the object holding its previous values while the test carries on regardless.


if (!pkt.randomize()) $fatal(1, "packet randomization failed");

// Inline constraints are ANDed with the class constraints:
if (!pkt.randomize() with { length > 1000; inject_err == 1; })
  $fatal(1, "over-constrained");

Failure almost always means over-constrained - two constraints with no overlapping solution. Debug with constraint_mode(0) to disable them one at a time.

10 |-> versus |=> in SVA?

Overlapping (|->): if the antecedent matches at cycle N, the consequent is checked at cycle N. Non-overlapping (|=>): checked at cycle N+1.

a |=> b   ≡   a |-> ##1 b

Why getting it backwards is worse than no assertion: it produces a check that passes for the wrong reason, so you believe you have coverage you do not have. → Volume 9.5

11 Can you tape out with 100% code coverage and 50% functional coverage?

You can, and you would probably ship a bug.

Code coverage is collected automatically and measures which lines, branches, conditions and states were executed. Functional coverage is written by hand from the verification plan and measures which scenarios occurred.

The example that lands: "Executing every line of a FIFO proves nothing about whether you ever hit full-and-write-simultaneously. Code coverage tells you what you failed to execute; only functional coverage tells you what you failed to try." Then point at the Pentium FDIV case below - that is exactly this failure, and it cost half a billion dollars. → Volume 9.5

12 Mailbox, semaphore or event - which and when?
  • Mailbox - pass objects between threads with buffering. Generator→driver, monitor→scoreboard.
  • Semaphore - guard a shared resource so only N users touch it. Two drivers sharing one bus.
  • Event - signal that something happened, no data attached. "Reset done."

Add: "Always parameterise a mailbox - mailbox #(Transaction). A bare mailbox accepts any type and turns a type error into a runtime surprise." → Volume 9.3

13 @(event) versus wait(event.triggered)?

@ is edge-sensitive: if the trigger fires before you reach the @ - even in the same timestep - you wait forever. wait(e.triggered) stays true for the rest of the timestep, so it cannot miss.

Same class of race as the stratified event queue ordering; wait(...triggered) is the safe default. → Volume 9.3

14 What is a virtual interface and why is it needed?

An interface is a static construct, elaborated as part of the design hierarchy. Classes are dynamic, created at runtime, and cannot contain a static interface instance. A virtual interface is a handle to an interface, which a class can hold.

It is the bridge between the class-based testbench and the signal-level DUT: the environment receives it through configuration, and the driver and monitor use it to drive and sample real wires.

Pair it with clocking blocks: the driver and monitor both touch the same signals on the same edge, and the clocking block's input #1step is what stops them racing the DUT. → Volume 8.4

15 Name the UVM phases. Which one consumes simulation time?

Nine: build, connect, end_of_elaboration, start_of_simulation, run, extract, check, report, final.

Only run_phase consumes time. Every other phase completes in zero simulation time.

The detail that shows you have used it: "build_phase runs top-down because a parent constructs its children; connect_phase runs bottom-up because the ports it wires must already exist. And run_phase has twelve sub-phases - reset, configure, main, shutdown and their pre/post variants - for coordinating stimulus across components." → Volume 9.5

11.3 Real-world silicon bug case studies

Three failures worth knowing by name. The first two are documented public incidents; the third is a bug class rather than a single event, included because it is the one you are most likely to cause yourself.

Case 1 - The Intel Pentium FDIV bug (1994)

The Pentium FDIV quotient prediction lookup table with five missing entries out of 1066 THE SRT QUOTIENT-DIGIT LOOKUP TABLE 1066 entries - quotient digit predictions 5 entries missing 4195835 / 3145727 correct: 1.333820449136241002 Pentium: 1.333739068902037589 wrong from the 4th significant digit
Figure 11.1 - Five cells out of 1066. Roughly 0.5% of the table, in a region ordinary test vectors never reached.
Detail
What brokeThe floating-point divider used an SRT algorithm driven by a lookup table of quotient-digit predictions. Five entries were missing.
SymptomA small set of divisions returned results wrong from around the 4th significant digit.
CostA recall and a roughly $475 million charge - plus lasting reputational damage from the initial "most users will never notice" response.
Root causeThe logic was correct. The data it consulted was not.
Why every verification engineer should know this one 100% code coverage of the divider RTL would not have found it. Every line executed; every branch was taken. The bug lived in five table entries in a corner of the quotient-digit space no test vector reached. Only functional coverage - a covergroup over the operand space, with cross bins - would have shown the hole.

This is the concrete answer to problem 11 above. Intel taped out with the logic verified and the data unverified, and it cost them half a billion dollars.

Case 2 - Intel "Cougar Point" chipset (2011)

Detail
What brokeA transistor in the PLL clocking circuit for the 3 Gbps SATA ports had a higher-than-intended voltage applied.
SymptomNothing at first. The circuit degraded over time, so SATA ports 2-5 could fail after months or years in the field.
CostDisclosed January 2011; roughly $700 million to repair and replace, plus a further revenue impact.
Root causeA circuit-level design issue, not a logic error.
Some bugs are invisible to every form of functional verification The logic was right. Simulation passed. Silicon passed at test. The failure only appears with age, which no amount of RTL simulation, formal proof or coverage closure can reveal.

It is a useful counterweight to this whole course: Volumes 01-09 are about getting the logic right, and that is necessary but not sufficient. Reliability, electromigration, IR drop and aging analysis are a separate discipline - a chip can be logically perfect and still fail in the field.

Case 3 - The blocking-assignment pipeline race

Unlike the first two this is a bug class rather than a single documented incident - included because it is the one you are most likely to write yourself, and it has shipped in real designs many times.


// Two stages of a DSP pipeline,
// written by two engineers.
always @(posedge clk)
  stage1 = mult_out;

always @(posedge clk)
  stage2 = stage1;

// Non-blocking: both RHS values are
// sampled before either LHS updates.
always @(posedge clk)
  stage1 <= mult_out;

always @(posedge clk)
  stage2 <= stage1;
Why it survives to silicon The standard does not define which always block runs first. Depending on the order, stage2 receives either the old stage1 (correct - two pipeline stages) or the new one (wrong - the stage collapses).

The reason it escapes: one simulator picks an order and sticks to it. The design passes regression for months. Then a tool version changes, or the design is re-synthesized, or a different simulator is used for sign-off - and the answer changes. The RTL never changed, so nobody suspects it.

This is Cummings' Golden Rule 1, and it is why "use <= in every clocked block" is not style advice. → Volume 1.4
That is Course 1 Eleven volumes, from what a transistor physically is to the core technical principles required for senior silicon engineering interviews. If you can work the forty problems above without opening the answers, you are ready.

The Academy continues with FPGA Mastery and the ASIC physical design flow - both syllabi are published, and volumes ship in order.