The Whiteboard and RTL Coding Exercise Playbook
July 31, 2026·45 min read·advanced
A 45 to 60 minute slot runs roughly five minutes of introductions, ten on your background, twenty-five to thirty on the exercise, five for your questions. The exercise is the largest block of scoreable…
01.Part 1, what actually happens in the room
1.1 Where the exercise sits
A 45 to 60 minute slot runs roughly five minutes of introductions, ten on your background, twenty-five to thirty on the exercise, five for your questions. The exercise is the largest block of scoreable evidence in the loop, so two interviewers who disagree resolve it there.
1.2 The three exercise types
Write this RTL. A bounded named block such as a synchronous FIFO, an arbiter, a synchronizer, or a sequence detector. The interviewer knows the answer and the point is whether you produce clean synthesizable code unaided. Highest rehearsal payoff, because the set of blocks that get asked is small and finite.
Design this block at whiteboard level. Open-ended and too large to code, such as a store buffer, a cache, or a power controller. What is expected is a decomposition, an interface list, a block diagram, datapath and control described separately, and the cases that break it.
Debug this. Twenty to sixty lines with planted bugs, often a warm-up or a closer, and the fastest to prepare for because the planted bugs come from a short list. One slot often contains two types, commonly a coding exercise followed by "now make this FIFO cross clock domains."
1.3 What is actually being scored
| Axis | Strong signal | Weak signal |
|---|---|---|
| Clarified before starting | three targeted interface questions in two minutes | starts coding on the first sentence |
| Stated assumptions | "I assume depth is a power of two, stop me if not" | assumes it silently |
| Structured before writing | block diagram before any syntax | code line by line with no visible plan |
| Code quality | non-blocking in always_ff, defaults in always_comb, reset handled | mixed styles, latches, no reset |
| Caught own bugs | "that full condition breaks on wrap, let me fix it" | interviewer has to point it out |
| Took a hint | "so the pointer needs an extra bit, here is the change" | argues, freezes, or restarts |
| Reasoned about the result | walks a transaction through the code out loud | can only re-read the code back |
| Knew the cost | critical path, what changes at 64 entries | describes function only |
Six of eight are process rather than artifact. They are hiring somebody who will be handed an ambiguous spec on a Tuesday and has to turn it into RTL a verification engineer can sign off, and every process axis measures that.
1.4 The counterintuitive part
The intuitive model is that this is a test, so the better answer wins. That is wrong. A candidate who reaches a slightly worse answer with clean visible reasoning usually beats one who silently produces a better answer.
The interviewer is estimating how you will do on a problem they have not seen, and your answer to a known toy problem is weak evidence, since they already know a competent person can memorize a FIFO. Reasoning is what transfers. Silence is also ambiguous, because a candidate thinking clearly and a candidate who is lost look identical from outside, and nobody gives the benefit of the doubt while writing a hiring recommendation. The consequence is an inversion, where seventy percent of a hard problem with everything visible beats one hundred percent of an easy one with nothing visible.
02.Part 2, the universal opening
2.1 Four steps, run before every exercise
Four minutes out of twenty-five, and it prevents the ten-minute rewrite that happens when you learn at minute fifteen that the interviewer wanted different throughput. Step 4 is the one people skip and the one that pays most, because inventing structure while writing syntax is two jobs at once and under observation your working memory is smaller than at your desk.
2.2 The clarifying questions for a FIFO prompt
"Write me a FIFO" is deliberately underspecified. Six questions change the answer and you would ask perhaps four.
Single clock or two? The biggest fork, so ask first, because a synchronous FIFO is twenty lines and an asynchronous one needs gray pointers, two synchronizers, and a careful flag comparison.
Is the depth a power of two? The extra-bit trick in 3.1 needs the pointer to wrap naturally, and at depth 6 it breaks and you fall back to an occupancy counter.
Can a read and a write happen in the same cycle? Almost always yes, and it changes the flag logic and forces the memory to support both ports at once.
Registered output or first-word fall-through? Registered means rd_data appears one cycle after rd_en, fall-through means the head is visible as soon as the FIFO is non-empty. The second suits a valid and ready handshake, the first suits timing, and asking separates people who have used a FIFO in a pipeline from people who have only written one.
Almost-full, and with how much warning? A producer separated by pipeline registers needs warning before the FIFO is actually full, and that depth sets the threshold.
Reset style, and does the memory reset? Pointers must, and the data array almost never needs to, because an entry is unreadable until written and resetting 512 entries costs area for no functional gain.
2.3 Why interviewers weight this so heavily
A real spec is worse than the prompt. It gives throughput and says nothing about a simultaneous read and write to the same entry, it names the reset and does not say whether it is synchronous, and every gap is a decision somebody makes silently and wrongly nine months before emulation finds it. When you ask "is the depth a power of two," they are not learning that you know about pointer wrapping. They are learning that when a spec is silent, you notice.
03.Part 3, the RTL coding exercises worked in full
3.1 The synchronous FIFO
Theory in Arbiters FIFOs and CAMs sections 4.1 to 4.4. Assume single clock, power-of-two depth, simultaneous read and write allowed, first-word fall-through, synchronous active-low reset.
Build the idea on four entries first. The whole problem is that when the pointers are equal you cannot tell empty from full, since writing four entries into a four-deep FIFO walks the write pointer through 0, 1, 2, 3 and back to 0, where the read pointer sits. Make both pointers one bit wider than the address, so the bottom bits index the array and the top bit is a wrap parity that toggles each time the pointer goes around.
| Event | wr_ptr | rd_ptr | low bits equal | top bits equal | flag |
|---|---|---|---|---|---|
| after reset | 000 | 000 | yes | yes | empty |
| write 1 | 001 | 000 | no | yes | neither |
| write 2, 3, 4 | 100 | 000 | yes | no | full |
| read 4 | 100 | 100 | yes | yes | empty |
| write 4 more | 000 | 100 | yes | no | full |
Point at the last row, where the write pointer has wrapped to 000 and sits numerically below the read pointer and full is still correct. Bonus, the 3-bit difference is the occupancy for free, since 000 - 100 is 100 in three-bit two's complement, which is 4.
| module sync_fifo #(parameter int DW = 32, parameter int DEPTH = 8) ( | |
| input logic clk, rst_n, | |
| input logic wr_en, | |
| input logic [DW-1:0] wr_data, | |
| output logic full, | |
| input logic rd_en, | |
| output logic [DW-1:0] rd_data, | |
| output logic empty, | |
| output logic [$clog2(DEPTH):0] count | |
| ); | |
| localparam int AW = $clog2(DEPTH); | |
| logic [DW-1:0] mem [DEPTH]; | |
| logic [AW:0] wr_ptr, rd_ptr; // one extra bit above the address | |
| logic do_wr, do_rd; | |
| assign do_wr = wr_en & ~full; // a bad caller cannot corrupt state | |
| assign do_rd = rd_en & ~empty; | |
| always_ff @(posedge clk) begin | |
| if (!rst_n) begin | |
| wr_ptr <= '0; | |
| rd_ptr <= '0; | |
| end else begin | |
| if (do_wr) wr_ptr <= wr_ptr + 1'b1; | |
| if (do_rd) rd_ptr <= rd_ptr + 1'b1; | |
| end | |
| end | |
| always_ff @(posedge clk) begin // data array deliberately unreset | |
| if (do_wr) mem[wr_ptr[AW-1:0]] <= wr_data; | |
| end | |
| assign rd_data = mem[rd_ptr[AW-1:0]]; // first-word fall-through | |
| assign empty = (wr_ptr == rd_ptr); | |
| assign full = (wr_ptr[AW-1:0] == rd_ptr[AW-1:0]) && | |
| (wr_ptr[AW] != rd_ptr[AW]); | |
| assign count = wr_ptr - rd_ptr; | |
| endmodule | |
| ```text | |
| Narrate three choices while writing them. The `do_wr` qualification costs one gate and removes a class of integration bug, the missing reset on `mem` is intentional, and `count` falls out of the subtraction rather than needing a third counter. | |
| **Follow-ups.** Non-power-of-two depth kills the wrap parity, so switch to an occupancy counter. Almost-full is `count >= DEPTH-3`, but the threshold must cover the producer's round trip through its own flop and not just the burst length. Small multi-ported arrays become flops and a mux tree, large ones become an SRAM whose synchronous read forces an output bypass to keep fall-through. The critical path is the flag feeding the requester, or at depth 512 the 512-to-1 read mux. | |
| ### 3.2 The round robin arbiter | |
| Theory in [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) section 3.3. Two bit tricks make it twelve lines. | |
| **Trick one, isolate the lowest set bit**, written `x & (~x + 1)`. On `x = 4'b1010`, `~x = 0101`, `~x + 1 = 0110`, and `1010 & 0110 = 0010`, which is bit 1 alone. It works because negating flips every bit above the lowest set one and leaves that one set, and it is a fixed-priority arbiter in one line. | |
| **Trick two, mask everything strictly above the winner**, written `~((gnt - 1) | gnt)`. On `gnt = 4'b0100`, `gnt - 1 = 0011`, `0011 | 0100 = 0111`, and inverting gives `1000`, exactly the requesters above the winner. Storing the pointer as a mask rather than an index is what makes this cheap. | |
| Apply the mask, run fixed priority on what survives, and if nothing survives run fixed priority on the raw request, which is the wrap. | |
| <Figure src="/figures/hardware-interview-prep/iv-26-Whiteboard-and-Coding-Playbook-fig03.svg" alt="The request vector runs down two paths at once, one masked to the requesters above the last winner and one raw, and the masked result is taken whenever anything survives the mask, which is what makes the wrap free." caption="The request vector runs down two paths at once, one masked to the requesters above the last winner and one raw, and the masked result is taken whenever anything survives the mask, which is what makes the wrap free." id="fig:26-Whiteboard-and-Coding-Playbook-3" /> | |
| ```systemverilog | |
| module rr_arb #(parameter int N = 4) ( | |
| input logic clk, rst_n, | |
| input logic [N-1:0] req, | |
| output logic [N-1:0] gnt | |
| ); | |
| logic [N-1:0] mask, masked_req, gnt_masked, gnt_unmasked; | |
| assign masked_req = req & mask; | |
| assign gnt_masked = masked_req & (~masked_req + 1'b1); | |
| assign gnt_unmasked = req & (~req + 1'b1); | |
| assign gnt = (|masked_req) ? gnt_masked : gnt_unmasked; | |
| always_ff @(posedge clk) begin | |
| if (!rst_n) mask <= '1; // first pass is low-index | |
| else if (|gnt) mask <= ~((gnt - 1'b1) | gnt); // strictly above the winner | |
| end | |
| endmodule | |
| ```text | |
| | Cycle | `req` | `mask` in | `masked_req` | path | `gnt` | `mask` next | | |
| |---|---|---|---|---|---|---| | |
| | 1 | `1011` | `1111` | `1011` | masked | `0001` | `1110` | | |
| | 2 | `1010` | `1110` | `1010` | masked | `0010` | `1100` | | |
| | 3 | `1000` | `1100` | `1000` | masked | `1000` | `0000` | | |
| | 4 | `0011` | `0000` | `0000` | **unmasked, wrap** | `0001` | `1110` | | |
| Cycle 4 is the interesting one. The mask has walked to zero, nothing survives, the design falls back to the raw request, and the pointer wraps, which is the entire round robin behavior for one OR-reduce and one mux. | |
| **Follow-ups.** No requester waits more than $N-1$ grants, because once skipped you sit above the pointer until it passes you and the pointer advances past exactly one requester per grant, so 64 requesters at 3 GHz is 63 grants or 21 nanoseconds worst case. At 64 the find-first is a 64-bit carry chain that will not fit in 333 picoseconds, so either build a **hierarchical arbiter** of eight groups of eight, which costs fairness because a group with one requester gets the same share as a group with eight, or **pipeline** the arbitration a cycle ahead, which costs latency and misbehaves when a request withdraws. For multi-cycle grants, hold the grant and freeze the mask while `busy` is high, and say that this reintroduces starvation unless the burst is capped. There is no combinational loop, because `mask` comes out of a flop. | |
| ### 3.3 The two-flop synchronizer | |
| Theory and the MTBF arithmetic in [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing) Parts 3 to 5. The code is four lines and the discussion is the exercise. | |
| ```systemverilog | |
| module sync_2ff #(parameter int W = 1) ( | |
| input logic dclk, drst_n, // DESTINATION clock only | |
| input logic [W-1:0] async_in, | |
| output logic [W-1:0] sync_out | |
| ); | |
| logic [W-1:0] meta_q, sync_q; | |
| always_ff @(posedge dclk or negedge drst_n) begin | |
| if (!drst_n) begin | |
| meta_q <= '0; | |
| sync_q <= '0; | |
| end else begin | |
| meta_q <= async_in; // may go metastable, this is expected | |
| sync_q <= meta_q; // nothing else may read meta_q | |
| end | |
| end | |
| assign sync_out = sync_q; | |
| endmodule | |
| ```text | |
| <Figure src="/figures/hardware-interview-prep/iv-26-Whiteboard-and-Coding-Playbook-fig04.svg" alt="Both flops run on the destination clock and the node between them is reserved entirely for settling, which is why nothing at all may be connected to it." caption="Both flops run on the destination clock and the node between them is reserved entirely for settling, which is why nothing at all may be connected to it." id="fig:26-Whiteboard-and-Coding-Playbook-4" /> | |
| **Three reasons nothing may tap the middle, and the second is the one to lead with.** A metastable node sits at an undefined voltage, so two consumers with different thresholds can read it differently and two parts of one design disagree about a bit in a way no test reproduces. Quantitatively, the second flop gives the node a full clock period to settle and resolution is **exponential** in the time allowed, so a 40 picosecond gate inside a 333 picosecond period removes twelve percent of the window and can cost one or two orders of magnitude of MTBF, turning a once-per-century failure into a once-per-year one. And physically, a CMOS gate fed a mid-rail input has both networks partly on, so it burns crowbar current and emits a slow edge, propagating the analog problem forward. | |
| **Follow-ups.** Two flops normally, three where the destination period is short or the failure cost is high, answered with the mechanism rather than the number. An 8-bit bus may not use this, because each bit resolves independently so the destination can see a value that never existed, and the fixes are gray coding for a counter, a handshake for arbitrary data, or an asynchronous FIFO for a stream. The path needs a false path or better a `set_max_delay`, since there is no common clock but the wire still needs bounding. | |
| ### 3.4 The asynchronous FIFO | |
| The hardest common ask and entirely mechanical once memorized. Theory in [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing) section 5.6. In 3.1 the flags compared the two pointers directly, and in two domains that is a multi-bit crossing, which 3.3 just ruled out. | |
| **Gray code is the fix.** A gray code orders values so consecutive ones differ in exactly one bit, converted with $g = b \oplus (b \gg 1)$. | |
| | $b$ | $b \gg 1$ | $g$ | | $b$ | $b \gg 1$ | $g$ | | |
| |---|---|---|---|---|---|---| | |
| | `000` | `000` | `000` | | `100` | `010` | `110` | | |
| | `001` | `000` | `001` | | `101` | `010` | `111` | | |
| | `010` | `001` | `011` | | `110` | `011` | `101` | | |
| | `011` | `001` | `010` | | `111` | `011` | `100` | | |
| Read the gray column down. `000` to `001` changes bit 0, `001` to `011` changes bit 1, `011` to `010` changes bit 0, and the wrap from `100` to `000` changes bit 2 only. The theorem is two sentences. When only one bit changes, the sampling clock in the other domain either catches the change or does not, so it sees the new pointer or the old one. Both genuinely existed, and there is no third possibility because there is no second bit that could disagree. | |
| <Figure src="/figures/hardware-interview-prep/iv-26-Whiteboard-and-Coding-Playbook-fig05.svg" alt="Each domain increments its own binary pointer, converts it to gray, and ships it across a two-flop synchronizer, so the only thing that ever crosses is a code in which exactly one bit changes at a time." caption="Each domain increments its own binary pointer, converts it to gray, and ships it across a two-flop synchronizer, so the only thing that ever crosses is a code in which exactly one bit changes at a time." id="fig:26-Whiteboard-and-Coding-Playbook-5" /> | |
| ```systemverilog | |
| module async_fifo #(parameter int DW = 32, parameter int AW = 3) ( | |
| input logic wclk, wrst_n, wr_en, | |
| input logic [DW-1:0] wr_data, | |
| output logic wfull, | |
| input logic rclk, rrst_n, rd_en, | |
| output logic [DW-1:0] rd_data, | |
| output logic rempty | |
| ); | |
| localparam int DEPTH = 1 << AW; | |
| logic [DW-1:0] mem [DEPTH]; | |
| logic [AW:0] wbin, wgray, wbin_nxt, wgray_nxt, wq1_rgray, wq2_rgray; | |
| logic [AW:0] rbin, rgray, rbin_nxt, rgray_nxt, rq1_wgray, rq2_wgray; | |
| // ---------------- write domain ---------------- | |
| assign wbin_nxt = wbin + (wr_en & ~wfull); | |
| assign wgray_nxt = (wbin_nxt >> 1) ^ wbin_nxt; | |
| always_ff @(posedge wclk or negedge wrst_n) | |
| if (!wrst_n) begin wbin <= '0; wgray <= '0; end | |
| else begin wbin <= wbin_nxt; wgray <= wgray_nxt; end | |
| always_ff @(posedge wclk) | |
| if (wr_en && !wfull) mem[wbin[AW-1:0]] <= wr_data; | |
| always_ff @(posedge wclk or negedge wrst_n) | |
| if (!wrst_n) begin wq1_rgray <= '0; wq2_rgray <= '0; end | |
| else begin wq1_rgray <= rgray; wq2_rgray <= wq1_rgray; end | |
| // full when the gray pointers match with the TOP TWO bits inverted | |
| assign wfull = (wgray_nxt == {~wq2_rgray[AW:AW-1], wq2_rgray[AW-2:0]}); | |
| // ---------------- read domain ---------------- | |
| assign rbin_nxt = rbin + (rd_en & ~rempty); | |
| assign rgray_nxt = (rbin_nxt >> 1) ^ rbin_nxt; | |
| always_ff @(posedge rclk or negedge rrst_n) | |
| if (!rrst_n) begin rbin <= '0; rgray <= '0; end | |
| else begin rbin <= rbin_nxt; rgray <= rgray_nxt; end | |
| assign rd_data = mem[rbin[AW-1:0]]; | |
| always_ff @(posedge rclk or negedge rrst_n) | |
| if (!rrst_n) begin rq1_wgray <= '0; rq2_wgray <= '0; end | |
| else begin rq1_wgray <= wgray; rq2_wgray <= rq1_wgray; end | |
| assign rempty = (rgray_nxt == rq2_wgray); | |
| endmodule | |
| ```text | |
| **Work the full condition with numbers.** Take `AW = 2`, so depth 4 and 3-bit pointers. After four writes, `wbin = 100` and its gray is `100 ^ 010 = 110`, while `rgray = 000`, and the test builds `{~00, 0} = 110`, which matches. Wrapped case, `wbin = 110` is six writes and `rbin = 010` is two reads, so occupancy is 4. Gray of `110` is `101`, gray of `010` is `011`, and the test builds `{~01, 1} = 101`, matching again. It is the top **two** bits because gray bit $k$ is the XOR of binary bits $k$ and $k+1$, so "top bit differs, rest equal" becomes "top two bits differ, rest equal." | |
| **The conservatism argument, which separates people.** The read pointer arrives in the write domain two `wclk` cycles late, so `wfull` can assert when the FIFO is no longer full. It can never fail to assert when it really is full, because the stale read pointer is always behind the true one, making computed occupancy larger than truth. **Being falsely full is a performance loss. Being falsely not-full is data corruption.** The error is in the safe direction by construction, and the same runs symmetrically for `rempty`. | |
| **Follow-ups.** Compare `wgray_nxt` rather than `wgray`, because the registered version asserts a cycle late and allows one overflow. Gray needs a power-of-two range, so at depth 6 you round up and waste locations. Each domain needs its own reset synchronizer per [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing) Part 7, coordinated at a higher level. The dual-port memory needs no synchronization, because the pointer protocol guarantees the reader never reads a location being written. Flag latency is two destination cycles plus the source cycle, so the depth calculation must include it. | |
| ### 3.5 The sequence detector, Moore and Mealy | |
| Theory in [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) sections 2.1 to 2.3. Detect `1011` with overlaps allowed, meaning the trailing `1` can start the next match, so `1011011` contains two. | |
| **The mechanical rule that generates every transition** is that each state remembers **the longest suffix of the input so far that is also a prefix of the target**. In "seen 101" the input `0` gives a stream ending `1010`, whose longest useful suffix is `10`, so you go to "seen 10" and not to the start. In "seen 1" the input `1` gives `11`, whose longest useful suffix is `1`, so you stay. | |
| <Figure src="/figures/hardware-interview-prep/iv-26-Whiteboard-and-Coding-Playbook-fig06.svg" alt="Each state is named by the longest prefix of 1011 that the input so far ends with, and every backward edge drops to the longest such prefix that survives the new bit rather than restarting." caption="Each state is named by the longest prefix of 1011 that the input so far ends with, and every backward edge drops to the longest such prefix that survives the new bit rather than restarting." id="fig:26-Whiteboard-and-Coding-Playbook-6" /> | |
| ```systemverilog | |
| // ---- Moore ---- | |
| module seq_1011_moore (input logic clk, rst_n, din, output logic found); | |
| typedef enum logic [2:0] {S0, S1, S2, S3, S4} state_e; | |
| state_e state, next; | |
| always_ff @(posedge clk) begin | |
| if (!rst_n) state <= S0; | |
| else state <= next; | |
| end | |
| always_comb begin | |
| next = S0; // default prevents a latch | |
| case (state) | |
| S0: next = din ? S1 : S0; | |
| S1: next = din ? S1 : S2; // "11" keeps the trailing 1 | |
| S2: next = din ? S3 : S0; | |
| S3: next = din ? S4 : S2; // "1010" keeps the trailing 10 | |
| S4: next = din ? S1 : S2; // overlap handled here | |
| default: next = S0; | |
| endcase | |
| end | |
| assign found = (state == S4); // depends on STATE ONLY | |
| endmodule | |
| ```text | |
| ```systemverilog | |
| // ---- Mealy, four states, same flop block, S4 deleted ---- | |
| always_comb begin | |
| next = S0; | |
| found = 1'b0; // default both outputs | |
| case (state) | |
| S0: next = din ? S1 : S0; | |
| S1: next = din ? S1 : S2; | |
| S2: next = din ? S3 : S0; | |
| S3: begin | |
| next = din ? S1 : S2; | |
| found = din; // depends on STATE AND INPUT | |
| end | |
| default: next = S0; | |
| endcase | |
| end | |
| ```text | |
| | Cycle | `din` | Mealy state | Mealy `found` | Moore state | Moore `found` | | |
| |---|---|---|---|---|---| | |
| | 0 | 1 | S0 | 0 | S0 | 0 | | |
| | 1 | 0 | S1 | 0 | S1 | 0 | | |
| | 2 | 1 | S2 | 0 | S2 | 0 | | |
| | 3 | **1** | S3 | **1** | S3 | 0 | | |
| | 4 | 0 | S1 | 0 | **S4** | **1** | | |
| | 5 | 1 | S2 | 0 | S2 | 0 | | |
| | 6 | **1** | S3 | **1** | S3 | 0 | | |
| | 7 | - | S1 | 0 | **S4** | **1** | | |
| Mealy asserts in cycle 3, during the final input bit, and Moore asserts in cycle 4 because it must first register that it reached S4. Mealy is a cycle earlier with one fewer state, but its output is combinational from `din`, so it inherits every glitch and creates an input-pin to output-pin path, which is bad at a module boundary where Moore's flop output is what the next block times against. | |
| **Follow-ups.** Binary encoding at five states, one-hot above roughly eight to sixteen because next-state logic becomes a wide OR rather than a decode, and gray for a state register that crosses domains. The `default` arm recovers an unreachable encoding instead of sticking, and it prevents a latch. | |
| ### 3.6 The pulse and toggle synchronizer | |
| Theory in [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing) section 5.7. **Show the failure first.** A one-cycle pulse pushed straight into a two-flop synchronizer is lost entirely if the destination is slower and the pulse falls between two destination edges, and is duplicated into three pulses if the destination is much faster and samples the same level three times. The fix converts an event into a level, because levels survive resampling. | |
| <Figure src="/figures/hardware-interview-prep/iv-26-Whiteboard-and-Coding-Playbook-fig07.svg" alt="The toggle flop turns the event into a level that survives resampling, and the destination recovers the event by detecting an edge on the synchronized level with an XOR across the last two stages." caption="The toggle flop turns the event into a level that survives resampling, and the destination recovers the event by detecting an edge on the synchronized level with an XOR across the last two stages." id="fig:26-Whiteboard-and-Coding-Playbook-7" /> | |
| ```systemverilog | |
| module pulse_sync ( | |
| input logic aclk, arst_n, pulse_a, // one aclk cycle wide | |
| input logic bclk, brst_n, | |
| output logic pulse_b // one bclk cycle wide | |
| ); | |
| logic tgl_a, q1, q2, q3; | |
| always_ff @(posedge aclk or negedge arst_n) | |
| if (!arst_n) tgl_a <= 1'b0; | |
| else tgl_a <= tgl_a ^ pulse_a; | |
| always_ff @(posedge bclk or negedge brst_n) | |
| if (!brst_n) {q1, q2, q3} <= 3'b000; | |
| else {q1, q2, q3} <= {tgl_a, q1, q2}; | |
| assign pulse_b = q2 ^ q3; // edge detect on the SYNCHRONIZED level | |
| endmodule | |
| ```text | |
| **State the limitation before you are asked.** This handles one pulse at a time, and two pulses closer together than the round trip flip the toggle twice so the destination sees one edge or none. Minimum spacing is roughly two source cycles plus three destination cycles, and a faster source needs a request and acknowledge handshake, or an asynchronous FIFO for a stream. | |
| --- | |
| ## Part 4, the find-the-bug exercise | |
| ### 4.1 Blocking assignment in a sequential block | |
| ```systemverilog | |
| always_ff @(posedge clk) begin | |
| q1 = d; // BUG, blocking | |
| q2 = q1; | |
| q3 = q2; | |
| end | |
| ```text | |
| **Symptom.** Not a shift register. Blocking assignments execute in written order, so `q2` takes the already-updated `q1` and `q3` the already-updated `q2`, and all three hold `d`. | |
| **Fix and the honest story.** Non-blocking for anything that represents a flop, because non-blocking reads all the old values then updates together, which is what a bank of flops does. Inside one always block the broken version is well-defined rather than a race, and the genuinely dangerous case is blocking assignments in **separate** always blocks feeding each other, where the language does not guarantee scheduling order so two simulators can disagree and the gates can differ from both. | |
| ### 4.2 The inferred latch from a missing else | |
| ```systemverilog | |
| always_comb begin | |
| if (sel == 2'b00) y = a; | |
| else if (sel == 2'b01) y = b; | |
| else if (sel == 2'b10) y = c; | |
| // BUG, nothing for sel == 2'b11 | |
| end | |
| ```text | |
| **Symptom.** For `sel == 2'b11` nothing is assigned, so `y` keeps its old value, and keeping a value needs memory, so the tool builds a **level-sensitive latch** where you wanted a mux. It is transparent while its enable is high so glitches pass through, and simulation shows the value persisting, which often looks correct, so the bug hides until synthesis. | |
| **Fix.** Assign a default at the top of the block, which always executes. | |
| ```systemverilog | |
| always_comb begin | |
| y = '0; // default kills the latch outright | |
| if (sel == 2'b00) y = a; | |
| else if (sel == 2'b01) y = b; | |
| else if (sel == 2'b10) y = c; | |
| end | |
| ```text | |
| A complete `else` also works but does not scale, because a block assigning six signals across ten branches needs every signal in every branch. | |
| ### 4.3 The missing default in a case | |
| ```systemverilog | |
| typedef enum logic [2:0] {IDLE, REQ, WAIT_ACK, DONE} state_e; | |
| always_comb begin | |
| case (state) | |
| IDLE: next = req ? REQ : IDLE; | |
| REQ: next = gnt ? WAIT_ACK : REQ; | |
| WAIT_ACK: next = ack ? DONE : WAIT_ACK; | |
| DONE: next = IDLE; | |
| // BUG, no default | |
| endcase | |
| end | |
| ```text | |
| **Symptom, two of them.** The same latch as 4.2, because the enumeration is 3 bits and four of eight encodings are unlisted. And worse, if the state register ever lands on an unlisted encoding, from an upset or an X at power-up, there is no transition out and the machine hangs permanently with no error signal. | |
| **Fix.** A `default` arm that recovers to `IDLE`, plus the top-of-block default. Add that `unique case` fires a simulation assertion when no branch matches, and that RTL `case` semantics are **optimistic** about X, since an X selector may take no branch in simulation while the gates take a definite and possibly wrong one. | |
| ### 4.4 Logic between the two synchronizer flops | |
| ```systemverilog | |
| always_ff @(posedge dclk) begin | |
| meta_q <= async_req; | |
| sync_q <= meta_q & enable; // BUG, logic on the metastable node | |
| end | |
| ```text | |
| **Symptom.** Nothing, ever, in simulation, because zero-delay RTL has no model of metastability and `meta_q` is always clean. It fails only in silicon, at a rate set by temperature and voltage, as an occasional dropped or duplicated request nobody can reproduce. | |
| **Fix.** Two flops back to back with nothing between them, with the qualification after the synchronizer. The settling window shrinks by the gate delay and MTBF is exponential in that window, per 3.3. This is one of the few bugs CDC tools catch reliably, so shipping it usually means the flow had no CDC check at all. | |
| ### 4.5 Valid depends on ready | |
| ```systemverilog | |
| assign valid = has_data & ready; // BUG | |
| assign valid = has_data; // FIX | |
| assign xfer = valid & ready; | |
| ```text | |
| **Symptom, two variants.** If the consumer computes `ready` combinationally from `valid`, which many do, this is a genuine combinational loop and the tool either oscillates or refuses to time it. If `ready` is registered there is no loop but there is still a protocol violation, because a consumer that drops `ready` makes the producer retract an offer it already made. | |
| **Why the rule exists.** The handshake works because exactly one side may wait on the other, so the producer asserts `valid` unconditionally when it has data and the consumer may look at `valid` when deciding `ready`. The rule to have ready is that **`ready` may depend on `valid`, `valid` may never depend on `ready`**, and once asserted, `valid` and the data hold until `ready` arrives. | |
| ### 4.6 The missing reset, and the one that should not be there | |
| ```systemverilog | |
| always_ff @(posedge clk) state <= next; // BUG, control state unreset | |
| always_ff @(posedge clk) begin | |
| if (!rst_n) data_pipe <= '0; // wasteful, not wrong | |
| else data_pipe <= data_in; | |
| end | |
| ```text | |
| **Symptom and the distinction that is the real content.** The state register powers up arbitrary, possibly on an unlisted encoding, and combined with 4.3 the block hangs before it ever runs. Blanket-resetting costs area in the reset tree and effort in timing its release, so **control state needs reset and datapath state usually does not**, because a datapath flop is unreadable until valid control reaches it. The fix is to reset `state` and leave `data_pipe` alone, the opposite of what the broken code did in both places. | |
| ### 4.7 The systematic scan order | |
| Do not read sixty unfamiliar lines top to bottom hoping something feels wrong. Run this order out loud so the method is audible before you find anything. | |
| 1. **Read the port list only.** Widths, directions, clocks, resets and their polarity. Two clocks means you are hunting a CDC bug before reading any logic. | |
| 2. **Classify every always block by its header.** `always_ff` holds only non-blocking, `always_comb` only blocking. Violations are 4.1. | |
| 3. **Every `always_comb` needs a top default or complete branch coverage.** Missing is 4.2. | |
| 4. **Every `case` needs a `default`.** Missing is 4.3. | |
| 5. **Check reset.** Which flops have it, polarity, and whether control state is covered. That is 4.6. | |
| 6. **Check clock domains.** One clock per always block, every crossing through a clean synchronizer. That is 4.4. | |
| 7. **Check handshakes.** Apply 4.5 to every `valid` and `ready` or `req` and `ack` pair. | |
| 8. **Check widths and indices.** Off-by-one pointers, an address bus one bit too narrow, `$clog2` where `$clog2 + 1` was needed. | |
| 9. **Walk one transaction through by hand**, cycle by cycle. | |
| Steps 1 to 8 take four minutes and catch the planted bug most of the time. Step 9 catches the rest and shows you can reason rather than pattern-match, and repeating it on the fixed version is a habit interviewers notice. | |
| --- | |
| ## Part 5, the open-ended design question | |
| ### 5.1 The skeleton, and why the order is the answer | |
| <Figure src="/figures/hardware-interview-prep/iv-26-Whiteboard-and-Coding-Playbook-fig08.svg" alt="Requirements and interfaces come first because they are the only steps that can invalidate everything after them, and the datapath and the control are described as two separate branches before the corner cases pull them back together." caption="Requirements and interfaces come first because they are the only steps that can invalidate everything after them, and the datapath and the control are described as two separate branches before the corner cases pull them back together." id="fig:26-Whiteboard-and-Coding-Playbook-8" /> | |
| **Going straight to a detailed answer without establishing requirements reads as inexperience.** A junior model of design is that a block has a right implementation and the job is recall, while an experienced model is that the implementation is a **consequence** of the requirements. "32 kilobytes, 8-way, 64-byte lines" sounds like a recited configuration, and "what is the hit latency target and the miss bandwidth I must sustain, because those two decide associativity and MSHR count" sounds like somebody who has made the decision. Steps 1 and 2 are also the only ones you can get wrong in a way that invalidates everything after them, since a wrong corner case gets patched but a missed throughput requirement throws away everything from step 3 onward. | |
| ### 5.2 Worked in full, design a clock gating controller | |
| Chosen because it sits on top of the Resource Controller work, so steer here given a choice. Mechanisms in [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating). | |
| **Step 1, requirements.** Ask the granularity, the wake latency budget, whether power gating is in scope, and whether multiple clients can independently need the block awake. Then state your version. One coarse gater, multiple independent requesters, two cycles from request to first usable edge, clock gating only. Idle detection is **conservative**, meaning it may keep the clock running when it could have stopped and may never stop it with work in flight, and stating that safety property early is worth a lot because everything downstream follows from it. | |
| **Steps 2 and 3, interfaces and block diagram.** | |
| <Figure src="/figures/hardware-interview-prep/iv-26-Whiteboard-and-Coding-Playbook-fig09.svg" alt="Every activity source is ORed into one signal that reloads an idle countdown, and the whole controller sits on the free-running clock because a controller on the gated clock could never turn its own clock back on." caption="Every activity source is ORed into one signal that reloads an idle countdown, and the whole controller sits on the free-running clock because a controller on the gated clock could never turn its own clock back on." id="fig:26-Whiteboard-and-Coding-Playbook-9" /> | |
| Say two things while drawing. The controller runs on the **free-running** clock, because a controller on the gated clock cannot turn its own clock back on, which is the deadlock in [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) section 4.4. And the ICG is a library cell rather than an AND gate you write, because its internal latch must be characterized against the clock. | |
| **Steps 4 and 5, datapath and control.** The datapath is almost nothing, which itself signals that you classified the problem correctly, being an OR tree over the activity sources, a down-counter for hysteresis, and a status register. The ICG costs roughly two flop clock loads permanently, so against a 400-flop block gating pays after a couple of idle cycles and a hysteresis of 8 is generous, which is 4 bits. Control is three states, where RUN delivers the clock while the countdown runs, GATED stops it, and WAKE is a single cycle asserting the enable so the ICG passes the next edge, which is needed because the enable must be set up before the edge it enables. | |
| **Step 6, corner cases, and this is where the exercise is won.** The **wake path must never be gated**, so the controller and every synchronizer feeding it sit on `clk_free`, otherwise a request never arrives and the block never wakes. **Reset** needs an edge, so the clock must be forced on while reset is asserted, and the failure looks like a block coming out of reset in a random state only on runs where it happened to be gated. **DFT** needs every flop clocked during scan shift, so `scan_en` must force `gate_en` high combinationally rather than through the FSM, whose own flops are in the chain, per [DFT and Silicon Debug](/learn/hardware-interview-prep/dft-and-silicon-debug). **The race** where `outstanding` hits zero on the same edge a new transaction is accepted is fixed by reloading the countdown combinationally from the activity tree. **Thrash**, where a block idle for exactly the hysteresis period keeps paying wake latency and saving nothing, is why the hysteresis belongs in a configuration register tuned on silicon. **Debug**, because a gated block's counters and trace buffers stop too, is why `force_on` exists. | |
| **Steps 7 and 8, testing and optimization.** One directed test per corner case, with assertions as the primary mechanism because the interesting properties are temporal. Write that `gate_en` is never low while `outstanding != 0`, that it is always high while `scan_en` is high, and that a request produces `gate_en` within two cycles, then add randomized request patterns with coverage on the state transitions. Then the thing that matters most, which is that the safety property belongs to **formal** rather than simulation, because formal proves it over all sequences and simulation samples the ones you generated. If time remains, mention per-power-state hysteresis, gating the local clock tree buffers as well as the leaves, and escalation to power gating using the break-even arithmetic in [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) section 6.3. | |
| ### 5.3 The same skeleton, design a store buffer | |
| Theory in [Load Store and Memory Ordering](/learn/hardware-interview-prep/load-store-and-memory-ordering) and [Load-Store Queue Design](/learn/computer-architecture/load-store-queue). Ask how many stores in flight, what memory model, and whether loads must forward, then state the version. Sixteen entries, stores enter at commit and drain to L1 in order, one drain and one load lookup per cycle, and loads must read data from a committed store not yet in cache. The interfaces are an address, data, and byte enables from commit, the same plus backpressure to L1, a lookup address from the load unit, and back to it a hit indication, forwarded data, and a critical third output meaning "match but cannot forward." | |
| Structurally it is the FIFO of 3.1 plus a comparator per entry against the load address, which is a CAM per [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) Part 7, plus an age-ordered selector picking the youngest matching store older than the load. Sixteen entries is small enough for flops, which is what makes a full-width CAM affordable. | |
| The corner cases are the real content. A load partially overlapping a store, for example a 4-byte load taking two bytes from the buffer and two from cache, cannot forward and must stall until the store drains, which is why the third output exists. Multiple older matching stores means the youngest wins, which is why the selector is age-ordered rather than a plain find-first. A store draining on the same cycle a load matches it needs defined precedence, and full must backpressure commit. Testing is directed tests for every alignment and overlap combination, an assertion that a load never forwards from a younger store, and a random test against a byte-level shadow memory with coverage aimed at partial overlaps, because random stimulus produces exact aligned matches constantly and partial ones rarely. The skeleton did not change between a power controller and a store buffer, which is the point. | |
| --- | |
| ## Part 6, thinking out loud without rambling | |
| ### 6.1 Narrate decisions, not keystrokes | |
| "Think out loud" is near-useless as stated, because the natural reading is to describe what you are doing and that is noise. The actionable version is to **narrate every point where you chose between two options, with the reason, and say nothing when there was no choice.** Weak sounds like "now I am writing an always_ff block sensitive to posedge clk." Strong sounds like this. | |
| > "I will track occupancy with a counter rather than the extra pointer bit. The counter reads more clearly and works for any depth, where the extra bit needs a power of two. I would switch back if this ever crosses clock domains, because a counter cannot be gray coded usefully and a pointer can." | |
| That does four things in fifteen seconds, stating the choice, giving the reason, naming the alternative so it is clear you knew there was one, and naming the condition that would flip it. The live test is whether what you are about to say would still be true for a different block, so declaring the pointer width is a keystroke and widening it so full and empty are distinguishable is a decision. Aim for one decision every thirty to sixty seconds. | |
| ### 6.2 When you are stuck | |
| Silence is the worst response and the most natural one, so it has to be overridden deliberately. **Say what you know**, for example "the full condition has to distinguish the wrapped case from empty, and comparing raw pointers cannot, because they are equal in both." **Say what you are unsure about**, for example "I am not sure whether to widen the pointer or keep an occupancy counter." **Propose two options and pick one with a reason**, for example "the extra bit is cheaper and gives occupancy for free, the counter is clearer and handles any depth, and you said power of two, so I will take the extra bit." Twenty seconds, and it turns the worst-looking part of the interview into some of the best evidence in it, while frequently unsticking you because saying what you know forces the missing piece into view. | |
| ### 6.3 How to take a hint | |
| Hints are given because the interviewer wants you to finish, since a candidate stuck for ten minutes produces no further signal. The move is **acknowledge, incorporate, continue**, in about eight seconds, for example "you are right, the read pointer is in the other domain so I cannot compare it directly, which means synchronizing it in, and since it is multi-bit it has to be gray coded." Incorporating by saying what the hint implies proves you understood it rather than merely accepted it. | |
| Do not become defensive, because arguing for a version that does not work costs the correctness point you already lost plus the collaboration point you had not. Do not over-apologize, and do not accept a hint you do not understand, since "do you mean the pointer itself crosses, or just the comparison result?" is a good signal. When the hint is actually wrong, or right for a different assumption than the one you agreed on, make the disagreement concrete with "I think that holds if reads and writes cannot happen in the same cycle, and we said they can, so can I walk the two-cycle case on the board?" | |
| --- | |
| ## Part 7, common failure modes and the fix for each | |
| **Starting to code immediately.** The most common failure, driven by anxiety, because writing feels like progress and asking feels like stalling. **Fix.** Make the 2.1 opening mechanical so it runs before your anxiety gets a vote, and in practice never write a character until all four steps are done. | |
| **Silence.** **Fix.** Narrate a decision every thirty to sixty seconds, and run the 6.2 move when stuck. If you catch yourself having been silent, do not apologize, just start narrating from where you are. | |
| **Defending a wrong answer after a hint.** Costs the technical point and the collaboration point together. **Fix.** The 6.3 script, rehearsed until reflexive. | |
| **Overengineering a simple ask.** Delivering a parameterized, ECC-protected, programmable-threshold monster that never finishes. **Fix.** Build the simplest thing that meets the stated requirements, then say "here is what I would add for a real design and why I left it out." | |
| **Forgetting reset.** **Fix.** Make it physical. Whenever you type `always_ff @(posedge clk)`, type the reset line before the body, then decide per 4.6 whether this register wants one. | |
| **Forgetting the full and empty edge cases.** **Fix.** A fixed self-check before declaring done. Empty, one entry, full, wrapped and full, simultaneous read and write when empty, same when full. | |
| **Not testing your own code by walking a case through it.** **Fix.** Never say "done." Say "let me walk a write through this," pick a cycle, and trace. It often finds a bug you then get credit for catching, which by 1.4 is worth more than not having made it. | |
| **Saying "we" instead of "I" about your own work.** A whole interview in "we" makes your contribution invisible and the interviewer assumes it was small. **Fix.** "We" for the team's scope and "I" for yours, in adjacent sentences. "The team was delivering the resource controller for the SoC. I owned the clock gating architecture and wrote the enable derivation and the formal properties for the sequencing." | |
| --- | |
| ## Part 8, the practice plan | |
| ### 8.1 Why paper first, and the loop | |
| Practicing in an editor produces false readiness, because a large amount of the work is done by tools you have stopped noticing. The editor closes your `begin`, the linter reports the inferred latch, the compiler catches the width mismatch, and the simulator catches the logic error. Your unaided ability is the residual after subtracting all of that, and the interview measures exactly the residual, because a whiteboard has no linter and a shared document has no compiler. | |
| **Pass one, cold on paper, timed.** Twenty minutes by hand with no editor, reference, or internet. Run the four-step opening first, including saying the clarifying questions aloud and drawing the block diagram, then write the module and stop at twenty minutes, finished or not. | |
| **Pass two, transcribe and compile.** Type what you wrote exactly, mistakes included, then run the open-source flow from [Lab --- The Open-Source HDL Workflow](/learn/computer-architecture/lab-hdl-workflow) and [Programming and Tooling](/learn/hardware-interview-prep/programming-and-tooling). Verilator's lint mode finds inferred latches, width mismatches, and unassigned signals, which is most of Part 4's list, and Yosys elaboration finds what is unsynthesizable. | |
| **Pass three, diagnose the log rather than the code.** Sort each error into a syntax slip, a structural mistake like a missing default, or a design error like a wrong full condition. Syntax slips disappear with repetition, structural mistakes go on a personal checklist, and design errors mean re-reading the theory note. The metric is whether the log is shorter than last time. | |
| ### 8.2 The schedule | |
| | Session | Content | Time | | |
| |---|---|---| | |
| | 1 | Synchronous FIFO, cold on paper, then compile and diagnose | 45 min | | |
| | 2 | Round robin arbiter, same loop | 45 min | | |
| | 3 | Two-flop and pulse synchronizers, same loop, both short | 45 min | | |
| | 4 | Sequence detector, Moore and Mealy, same loop | 45 min | | |
| | 5 | Asynchronous FIFO, expect to fail the first attempt | 60 min | | |
| | 6 | Part 4 bug list, write each broken snippet from memory and fix it | 30 min | | |
| | 7 | Clock gating controller, out loud, timed at 25 min, no notes | 40 min | | |
| | 8 | Store buffer, same | 40 min | | |
| | 9 | Cold repeat of sessions 1 to 5 | 90 min | | |
| | 10 | Cold repeat again, all five inside 20 min each with a clean lint | 90 min | | |
| Ten sessions, roughly nine hours, under an hour a day across two weeks. **The completion criterion is not "I can do these," it is "I can do these cold, on paper, inside twenty minutes, while talking."** Add the talking in sessions 9 and 10 by recording yourself or explaining to an empty room, because most people find their first talking-while-writing attempt takes fifty percent longer and the gap closes in about three repetitions. The asynchronous FIFO deserves two extra sessions if the first goes badly. | |
| --- | |
| ## Part 10, check yourself | |
| Answer out loud, in full sentences, as if an interviewer asked. If you cannot, reread the section named. | |
| 1. Name the three exercise types and say which has the highest rehearsal payoff, with a reason. (1.2) | |
| 2. Explain why a slightly worse answer with visible reasoning beats a better answer produced silently. (1.4) | |
| 3. Recite the four-step opening with its time budget, and say which step people skip and what it costs. (2.1) | |
| 4. Given "write me a FIFO," give four clarifying questions and say what each changes about the design. (2.2) | |
| 5. Explain why the pointer is one bit wider than the address, then walk a four-deep FIFO through a full wrap showing empty, full, and full-after-wrap. (3.1) | |
| 6. Derive the lowest-set-bit trick on `4'b1010` and the above-the-winner mask on `4'b0100`, then explain the wrap when the masked request is zero. (3.2) | |
| 7. Give three reasons nothing may tap the node between the two synchronizer flops, and say which one is quantitative. (3.3) | |
| 8. Explain in two sentences why gray coding makes a multi-bit pointer safe to cross, then work the full condition for a four-deep asynchronous FIFO with real bit patterns. (3.4) | |
| 9. Explain why the asynchronous FIFO's `full` can be wrong and why that is safe, and give the matching statement for `empty`. (3.4) | |
| 10. State the rule that generates every transition of the overlapping 1011 detector, and say which cycle the Moore and Mealy outputs assert on the same stream. (3.5) | |
| 11. Show why a single-cycle pulse cannot go through a plain two-flop synchronizer, describe the toggle fix, and state its minimum spacing limit. (3.6) | |
| 12. Give the symptom and fix for each without looking. Blocking assignment in `always_ff`. Missing `else` in `always_comb`. Missing `default` in a `case`. A gate between the two synchronizer flops. `valid` computed from `ready`. (4.1 to 4.5) | |
| 13. Give the systematic scan order for unfamiliar code, and say which step catches what the checklist misses. (4.7) | |
| 14. Recite the eight-step decomposition, then design a clock gating controller out loud covering requirements, the FSM, and four corner cases including reset and DFT. (5.1, 5.2) | |
| 15. Give an example of narrating a decision rather than a keystroke, the three-part move for being stuck, and the three-step response to a hint. (6.1, 6.2, 6.3) | |
| --- | |
| ## Part 11, related notes | |
| - [Arbiters FIFOs and CAMs](/learn/hardware-interview-prep/arbiters-fifos-and-cams) for the theory behind four of the six coded exercises, meaning FIFO flags, round robin fairness, FSM construction, and the valid and ready rule Part 4 tests | |
| - [Clocking Reset and Domain Crossing](/learn/hardware-interview-prep/clocking-reset-and-domain-crossing) for metastability, the MTBF arithmetic behind the tapping ban, gray coding, and reset synchronization | |
| - [RTL Design and SystemVerilog](/learn/hardware-interview-prep/rtl-design-and-systemverilog) for blocking versus non-blocking, inferred latches, and coding for synthesis, the source material for the Part 4 bug list | |
| - [Digital Logic and Timing](/learn/hardware-interview-prep/digital-logic-and-timing) for setup, hold, skew, and glitching, which is why the Part 3 code is written the way it is | |
| - [Power Fundamentals and Clock Gating](/learn/hardware-interview-prep/power-fundamentals-and-clock-gating) for the ICG, the enable timing problem, and the sequencing the 5.2 example is built on | |
| - [Load Store and Memory Ordering](/learn/hardware-interview-prep/load-store-and-memory-ordering) for the store buffer sketched in 5.3 | |
| - [Verification Methodology](/learn/hardware-interview-prep/verification-methodology) for assertions, coverage, and the formal versus simulation split invoked in step 7 | |
| - [DFT and Silicon Debug](/learn/hardware-interview-prep/dft-and-silicon-debug) for scan shift and why the clock gating controller needs a test override | |
| - [Programming and Tooling](/learn/hardware-interview-prep/programming-and-tooling) for the open-source flow and debug workflow used in the Part 8 loop | |
| - *Apple Context and Behavioral* for the first-person framing that Part 7's last failure mode depends on | |
| - [Hardware Description Languages](/learn/computer-architecture/hardware-description-languages) for the vault's SystemVerilog reference and the procedural-block templates | |
| - [Lab --- The Open-Source HDL Workflow](/learn/computer-architecture/lab-hdl-workflow) for installing and running Yosys, Icarus Verilog, and Verilator | |
| - [Digital Building Blocks](/learn/computer-architecture/digital-building-blocks) for the muxes, priority encoders, and counters every Part 3 exercise is assembled from | |
| - [Load-Store Queue Design](/learn/computer-architecture/load-store-queue) for the vault's treatment of the store buffer prompt in 5.3 |