RTL Review

← 04 Low Power, Area and Synthesis | Index | 06 Verification and Testbench →

Round 05 — Combined answers (Gemini · GPT · Claude)

Contents

0. Resume probe

M5Q1 · "What is the worst bug you have ever let through review? What did you change afterwards?"

Gemini Answer

GPT Answer

"My resume combines TSF/beacon ownership, block verification, and lucisv tooling. I separate mechanically detectable RTL defects from protocol/CDC defects: lint is valuable, but a clean lint report cannot prove timestamp coherence or correct beacon scheduling." Use a real project bug if asked for an incident; the resume does not describe one.

Claude Answer

Template: pick a real one, and structure it as symptom → root cause → why review missed
it → what changed. Example shape that fits this profile:

"An equality compare on a timestamp in a block I reviewed. It was correct for a TSF that
steps by one, and the review happened before the smooth-drift correction existed; once the
TSF could double-step the event was skipped forever, and it showed up as a missed TWT
service period weeks later in FPGA. Review missed it because the diff was clean and the
assumption lived outside the diff. Afterwards I added a lint rule in lucisv flagging ==
against any signal tagged as a timer, and a review-checklist line: 'what invariant does
this compare assume, and who owns it'."

Part A — Rapid-fire snippets (0:05–0:20, ~60 s each)

For each snippet: what is wrong, what is the silicon symptom, would a linter catch it?

M5Q2 · A1. Snippet 1

always_comb begin
  if (sel) y = a;
end

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A1 Missing else / default assignment in always_comb. Infers a transparent level-sensitive latch on y when sel == 0 (glitch/timing hazard). Yes (hard error in always_comb).

GPT Answer

Missing assignment when sel=0: unintended latch/stale output. Add an else or default. Lint/synthesis should flag.

Claude Answer

y not assigned when !sel → latch. Symptom: timing loop on a transparent latch,
holds stale data, DFT headaches. Lint: yes.

M5Q3 · A2. Snippet 2

logic [7:0] a, b, sum;
assign sum = a + b;

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A2 Adding two 8-bit numbers into an 8-bit target sum. Carry-out bit (bit 8) is silently truncated, overflowing for any sum ≥256. Yes (width-truncation warning, though often waived unless strict).

GPT Answer

Eight-bit addition discards carry. It is a bug only if modulo-256 behavior is unintended. For full sum, use a 9-bit result and explicit extensions. Width lint may flag; intent needs review.

Claude Answer

8-bit sum of two 8-bit operands drops the carry. Symptom: silent wrap at 256.
Lint: yes (width/overflow warning), if the rule is enabled.

M5Q4 · A3. Snippet 3

always_ff @(posedge clk) begin
  cnt = cnt + 1;
  if (cnt == MAX) done <= 1'b1;
end

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A3 Blocking assignment cnt = cnt + 1 mixed with <= inside always_ff. if (cnt == MAX) compares the already-incremented cnt (fires 1 cycle early at MAX-1), and any other always_ff reading cnt has a simulation race! Yes (blocking in always_ff + mixed =/<= error).

GPT Answer

Blocking assignment makes the comparison see the incremented value; other processes can race with cnt. Use a sized next-count expression and nonblocking state assignments. Also decide whether done is sticky or needs a default clear. Lint catches blocking-in-sequential; specification review resolves off-by-one behavior.

Claude Answer

Blocking cnt in always_ff, then done compares the already-incremented value:
fires one count early, and other blocks reading cnt race in sim. Lint: yes.

M5Q5 · A4. Snippet 4

case (op)
  2'b00: y = a & b;
  2'b01: y = a | b;
  2'b10: y = a ^ b;
endcase

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A4 Incomplete case (op) missing 2'b11 / default. In always_comb, infers a latch on y holding the stale value when op == 2'b11. Yes (incomplete case / latch error).

GPT Answer

Missing op=11/default assignment can infer a latch in a combinational process. Add a safe default and an illegal-op assertion. Lint catches incomplete assignment/case.

Claude Answer

No 2'b11 item and no default. In always_comb that's a latch on y; if
unique/full_case is applied, 2'b11 is a don't-care and synthesis picks whatever is
cheapest, so sim and silicon can differ. Lint: yes.

M5Q6 · A5. Snippet 5

logic [3:0] a;
if (a == 4'b1xxx) hit = 1'b1; else hit = 1'b0;

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A5 Logical equality a == 4'b1xxx with X literals. In simulation, a == 4'b1xxx evaluates to 1'bx, so if always takes the else branch (hit = 0)! Synthesis treats x as don't-care (hit = a[3]) → fatal sim-vs-synth mismatch. Yes (X-literal in == error; use casez or inside).

GPT Answer

== 4'b1xxx does not mean wildcard matching: it can return X, and if treats X as not true. If only the top bit matters, compare a[3] and check unknowns separately. Lint can flag X comparisons.

Claude Answer

== against a literal with x is never true in sim, so hit is stuck at 0, while
synthesis may read the x as don't-care and build hit = a[3]. Classic sim/synth
mismatch. Lint: yes in most tools. Fix: casez/inside with ?, or a[3].

M5Q7 · A6. Snippet 6

// valid/ready sink
assign out_valid = in_valid & out_ready;
assign in_ready  = out_ready;

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A6 out_valid depends combinationally on out_ready (in_valid & out_ready). Violates AXI/valid-ready protocol: if downstream waits for out_valid == 1 before asserting out_ready == 1, the bus deadlocks permanently. No (syntactically valid; caught by SVA protocol checker or human review).

GPT Answer

Valid depends on ready, violating the source contract and risking deadlock with a receiver waiting for valid. For a transparent path use out_valid=in_valid, with the appropriate data path and in_ready=out_ready; add storage when cutting the ready path. Protocol checks/review, sometimes specialized lint.

Claude Answer

out_valid depends on out_ready: violates the valid/ready rule. A sink that waits
for valid before raising ready deadlocks. Lint: no; protocol checker or human.

M5Q8 · A7. Snippet 7

localparam int DEPTH = 16;
logic [$clog2(DEPTH)-1:0] wptr, rptr;
assign full  = (wptr == rptr) && wrapped;   // wrapped maintained elsewhere
assign empty = (wptr == rptr);

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A7 empty = (wptr == rptr) does not check !wrapped (and 4-bit ptrs lack an explicit wrap MSB). When the FIFO becomes full (wptr == rptr && wrapped), empty also asserts 1'b1 simultaneously! No (semantic bug; requires human review or FIFO SVA !(full && empty)).

GPT Answer

Equal pointers can mean full or empty; the current expressions assert both at full. If wrapped means different wrap phases, empty must also require !wrapped; alternatively use extended pointers or an occupancy count. Formal/reference FIFO checks are stronger than generic lint.

Claude Answer

4-bit pointers for depth 16 can't tell full from empty; the hand-rolled wrapped
flag has to be updated on every wrap of either pointer and is wrong the first time both
wrap in the same cycle. Use $clog2(DEPTH)+1-bit pointers. Lint: no.

M5Q9 · A8. Snippet 8

logic signed [15:0] x;
assign y = x >> 1;

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A8 Logical right shift >> on a signed variable x. Zero-fills MSB instead of sign-extending (-4 >> 1 becomes +32766 instead of -2). Should use arithmetic shift >>>. Yes (in strict linters like SpyGlass / lucisv signed-shift rule).

GPT Answer

>> zero-fills even for signed x. Use >>> with a signed left operand and explicit output width. Negative samples otherwise change value incorrectly. Signedness lint may help.

Claude Answer

Logical shift on a signed value zero-fills the sign bit: negative numbers become
large positives. Need >>> (and y declared signed). Lint: some tools.

M5Q10 · A9. Snippet 9

always_ff @(posedge clk_b) begin
  data_b <= data_a;          // data_a is a 32-bit bus from clk_a domain
  valid_b <= valid_a_sync;   // valid_a_sync is 2-flop synchronized
end

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A9 Multi-bit bus data_a[31:0] sampled across clock domains on the same cycle as valid_a_sync. No guarantee data_a was stable when valid_a launched or that routing skew didn't delay data_a past valid_a_sync; needs an enable pulse valid_rise + MCP stability constraint. CDC Tool: Yes (DMUX / unsynchronized multi-bit crossing). Syntax Linter: No.

GPT Answer

Synchronizing valid does not by itself make the 32-bit bus coherent; unconditional sampling can capture torn data. Use a held payload and destination capture after synchronized control, with acknowledgment and physical timing bounds, or an async FIFO. CDC analysis plus protocol review.

Claude Answer

32-bit bus captured on a separately synchronized valid: nothing guarantees data_a
is stable when valid_b arrives, and the bus itself is sampled with no sync. Needs data held
≥ 2–3 clk_b cycles around valid with set_max_delay -datapath_only, or a handshake. CDC
tool: yes; lint: no.

M5Q11 · A10. Snippet 10

always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) q <= '0;
  else if (en) q <= d;

always_ff @(posedge clk)
  if (clear) q <= '0;

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A10 Signal q is driven in two separate always_ff blocks. Multi-driven net / synthesis compilation error (cannot connect two flip-flop outputs together). Yes (hard error in synthesis & lint).

GPT Answer

Two processes drive q. Merge into one sequential block with explicit reset/clear/enable priority. Lint and synthesis should reject/flag.

Claude Answer

q driven from two always_ff blocks, one async-reset and one not. Multi-driven
net: compile/synthesis error, or last-wins in a lenient flow. Lint: yes, hard error.

M5Q12 · A11. Snippet 11

genvar i;
integer k;
generate for (i = 0; i < N; i++) begin
  always_ff @(posedge clk) for (k = 0; k < 4; k++) acc[i] <= acc[i] + in[i][k];
end endgenerate

Gemini Answer

# What is wrong Silicon / Sim Symptom Caught by Linter?
A11 (1) Shared static integer k across all generated always_ff blocks; (2) non-blocking <= accumulation inside a for loop. (1) Sim race on k across parallel instances; (2) acc[i] <= acc[i] + in[i][k] overwrites the RHS schedule 4 times—only k=3 (in[i][3]) is actually added! Yes (k shared var + NBA in loop warning).

GPT Answer

All nonblocking RHS values use old acc[i]; the last assignment wins rather than summing four terms. Compute a properly widened temporary sum, then one NBA per accumulator. A shared module-scope loop variable is also poor practice; use local loop variables and named generate scopes. Lint can flag repeated assignments; arithmetic intent requires review.

Claude Answer

integer k is shared by all N generated always_ff blocks → sim race on k. And
the non-blocking accumulate in a loop only keeps the last iteration (acc[i] + in[i][3]),
not the sum. Lint: partial (shared loop variable yes, the semantic error no).

M5Q13 · A12. Snippet 12

assign tbtt_fire = (tsf_us == next_tbtt_us);

Gemini Answer

(tsf_us is a 64-bit timestamp that normally increments by 1 but can be corrected by ±N by
a sync engine.)

# What is wrong Silicon / Sim Symptom Caught by Linter?
A12 Exact equality tsf_us == next_tbtt_us on a timestamp that can jump or double-step by +N. If a +2 μs slew step or forward TSF jump skips over next_tbtt_us, tbtt_fire never asserts and TBTT hangs forever! Must use >=. No (requires domain/protocol human review).

GPT Answer

(tsf_us is a 64-bit timestamp that normally increments by 1 but can be corrected by ±N by
a sync engine.)

Equality misses targets when TSF skips over them and can remain true across a pause. Use a wrap-aware crossing/greater-or-equal check with one-shot target advancement, coherent snapshots, and a defined jump policy. Needs architectural checks.

Claude Answer

(tsf_us is a 64-bit timestamp that normally increments by 1 but can be corrected by ±N by
a sync engine.)

Equality on a timestamp that can skip values under slew correction: the event is
missed forever. Use >= with a one-shot. Lint: no; human.

Part B — Live code review (0:20–0:50)

Read the following module as if it had just landed in your review queue. Narrate what you
look at first. Produce a prioritised findings list (silicon-killer / functional /
style). Then answer: which findings would a linter catch, which need a CDC tool, which need a
human?

// tbtt_gen.sv -- Target Beacon Transmission Time generator
//
// Generates a one-cycle tbtt_pulse when the 64-bit TSF reaches the next TBTT and a
// pre_tbtt_pulse `pre_tbtt_us` microseconds before it so firmware can prepare the
// beacon. TSF is in microseconds. Beacon interval is in TUs (1 TU = 1024 us).
//
// Clock domains:
//   clk      : MAC core clock (160 MHz)
//   tsf_tick : 1 MHz tick from the always-on timer block (separate domain)
//   tsf_us   : 64-bit TSF value, updated by the timer block on tsf_tick
module tbtt_gen #(
  parameter int BI_W = 16
)(
  input  logic              clk,
  input  logic              rst_n,

  // config from register block (clk domain)
  input  logic              en,
  input  logic [BI_W-1:0]   beacon_interval_tu,
  input  logic [15:0]       pre_tbtt_us,
  input  logic              cfg_wr,          // one-cycle pulse on any config write

  // TSF from timer block
  input  logic              tsf_tick,
  input  logic [63:0]       tsf_us,

  // outputs
  output logic              tbtt_pulse,
  output logic              pre_tbtt_pulse,
  output logic [63:0]       next_tbtt_us,
  output logic              tbtt_missed      // sticky, cleared by cfg_wr
);

  typedef enum logic [1:0] {IDLE, ARMED, FIRED} state_e;
  state_e state, state_d;

  logic [BI_W-1:0] bi_us;
  logic            tick_q, tick_rise;
  logic [63:0]     tsf_q;
  logic            armed;
  logic            pre_hit, tbtt_hit;

  // ---------------------------------------------------------------
  // Beacon interval in microseconds
  assign bi_us = beacon_interval_tu << 10;

  // ---------------------------------------------------------------
  // Tick edge detect and TSF sample
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      tick_q <= 1'b0;
      tsf_q  <= '0;
    end else begin
      tick_q <= tsf_tick;
      if (tick_rise) tsf_q <= tsf_us;
    end
  end
  assign tick_rise = tsf_tick & ~tick_q;

  // ---------------------------------------------------------------
  // Arm / state machine
  always_comb begin
    case (state)
      IDLE:  if (en && cfg_wr) state_d = ARMED;
      ARMED: if (tbtt_hit)     state_d = FIRED;
      FIRED: state_d = ARMED;
    endcase
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state <= IDLE;
      armed <= 1'b0;
    end else begin
      state <= state_d;
      armed = (state_d == ARMED) || (state_d == FIRED);
    end
  end

  // ---------------------------------------------------------------
  // Next TBTT tracking
  assign tbtt_hit = armed && (tsf_q >= next_tbtt_us);
  assign pre_hit  = armed && (tsf_q == next_tbtt_us - pre_tbtt_us);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      next_tbtt_us <= '0;
    end else if (cfg_wr) begin
      next_tbtt_us <= tsf_q + bi_us;
    end else if (tbtt_hit) begin
      next_tbtt_us <= next_tbtt_us + bi_us;
    end
  end

  always_ff @(posedge clk) begin
    if (!en) next_tbtt_us <= '0;
  end

  // ---------------------------------------------------------------
  // Outputs
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      tbtt_pulse     <= 1'b0;
      pre_tbtt_pulse <= 1'b0;
    end else begin
      tbtt_pulse     <= tbtt_hit & tick_rise;
      pre_tbtt_pulse <= pre_hit  & tick_rise;
    end
  end

  always_ff @(posedge clk) begin
    if (tbtt_hit && (tsf_q > next_tbtt_us + 64'd1))
      tbtt_missed <= 1'b1;
    else if (cfg_wr)
      tbtt_missed <= 1'b0;
  end

endmodule

M5Q14 · B-overview. Overall approach

Gemini Answer

How I Read a Module First (30-Second Scan)
  1. Header & Clock/Reset Boundary: Check every input port's clock domain (clk at 160 MHz vs. tsf_tick/tsf_us at 1 MHz) and reset polarity.
  2. Sequential Blocks & Multi-Drivers: Scan all always_ff blocks for reset consistency, blocking assignments, and multiply-driven signals.
  3. Combinational Blocks & Widths: Scan assign and always_comb for latches, bit-shift truncations, and underflow/overflow.
  4. Protocol & Corner-Case Behavior: Trace TSF jumps, slew pauses/skips, and wrap-around.
Prioritized Findings List
Priority 1 — Silicon-Killers (CDC, Synthesis Failures, Latches, Truncation)
  1. [CDC Silicon-Killer — Lines 160–169] Unsynchronized async input tsf_tick & combinational edge detect tick_rise:
    • Symptom: tsf_tick comes from an asynchronous 1 MHz domain and is used directly in assign tick_rise = tsf_tick & ~tick_q; and captured into a single flop tick_q. tick_rise will glitch and go metastable, corrupting the 64-bit tsf_q load enable and output pulses (tbtt_pulse).
    • Fix: Pass tsf_tick through a 2-flop synchronizer (tick_sync1_q, tick_sync2_q) and a 3rd delay flop (tick_sync3_q), then form tick_rise = tick_sync2_q & ~tick_sync3_q. Ensure tsf_us is held stable in the timer domain for ≥3 clk cycles after tsf_tick rises.
    • Caught by: CDC tool (and lucisv clock-domain rule).
  2. [Multi-Driven Register — Lines 196–208] next_tbtt_us driven in two always_ff blocks:
    • Symptom: next_tbtt_us is assigned in both the async-reset block (line 196) and the reset-less if (!en) block (line 206). This will fail synthesis with a multi-driven net error.
    • Fix: Merge if (!en) next_tbtt_us <= '0; into the single always_ff block at line 196.
    • Caught by: Linter / Compiler.
  3. [Severe Width Truncation — Line 148 & 156] bi_us is [BI_W-1:0] (16 bits) but assigned beacon_interval_tu << 10:
    • Symptom: Shifting a 16-bit TU value left by 10 bits inside a 16-bit wire truncates the upper 10 bits! Standard beacon interval 100 TU (100×1024=102,400 μs=0x19000) truncates to 16'h9000 (36,864 μs), and any multiple of 64 TU truncates to 0, causing an infinite TBTT loop!
    • Fix: Declare logic [BI_W+9:0] bi_us; assign bi_us = {beacon_interval_tu, 10'b0};.
    • Caught by: Linter (shift width truncation warning).
  4. [Inferred Latch in FSM — Lines 173–179] state_d has no default assignment in always_comb:
    • Symptom: When state == IDLE && !(en && cfg_wr), or state == ARMED && !tbtt_hit, or state == 2'b11, state_d is unassigned → infers a latch on state_d.
    • Fix: Add state_d = state; at the top of always_comb and default: state_d = IDLE; in case (state).
    • Caught by: Linter.
  5. [Missing Reset on Sticky Status Output — Lines 222–227] tbtt_missed has no reset:
    • Symptom: Powers up as X in simulation and random 0/1 in silicon, firing false missed-TBTT status to firmware before cfg_wr ever occurs.
    • Fix: Add negedge rst_n reset (if (!rst_n) tbtt_missed <= 1'b0;).
    • Caught by: Linter.
Priority 2 — Functional & Protocol Bugs
  1. [Back-to-Back Beacon Burst on Forward TSF Jump — Lines 201–203] Naive next_tbtt_us <= next_tbtt_us + bi_us:
    • Symptom: If cross-chip sync jumps tsf_q forward by +3 ms (or +300 ms, spanning K beacon intervals), tsf_q >= next_tbtt_us stays 1 for K consecutive cycles/ticks, emitting K back-to-back tbtt_pulse events!
    • Caught by: Human reviewer.
  2. [Pre-TBTT Equality & Unsigned Underflow — Line 194] assign pre_hit = armed && (tsf_q == next_tbtt_us - pre_tbtt_us);:
    • Symptom: (a) Exact equality == misses pre_tbtt_pulse if the smooth-drift slew engine steps by +2 μs across the target value; (b) if next_tbtt_us < pre_tbtt_us near t=0, unsigned subtraction underflows to 64'hFFFF...; (c) no one-shot flag prevents pre_hit from firing again if TSF pauses (+0 slew) on that exact microsecond.
    • Fix: Write armed && !pre_fired_q && (tsf_q + 64'(pre_tbtt_us) >= next_tbtt_us) and set pre_fired_q <= 1'b1 until the next tbtt_hit.
    • Caught by: Human reviewer.
  3. [Pulse Gating Drop — Line 217] tbtt_pulse <= tbtt_hit & tick_rise; while next_tbtt_us updates on every clk cycle (Line 201):
    • Symptom: tbtt_hit is evaluated on clk (160 MHz). As soon as tsf_q updates on the cycle after tick_rise, tbtt_hit asserts one cycle after tick_rise has already fallen to 0! Then on that same cycle, line 201 increments next_tbtt_us <= next_tbtt_us + bi_us, clearing tbtt_hit before the next tick_rise—so tbtt_pulse is permanently 0!
    • Caught by: Human reviewer / Simulation.
  4. [Broken tbtt_missed Condition — Line 223] if (tbtt_hit && (tsf_q > next_tbtt_us + 64'd1)):
    • Symptom: Flags tbtt_missed on a harmless +2 μs slew step instead of checking whether a full beacon interval was skipped (tsf_q >= next_tbtt_us + bi_us).
Priority 3 — Coding Style & Race Hazards
  1. [Blocking Assignment in always_ff — Line 187] armed = (state_d == ARMED) || (state_d == FIRED);:
    • Symptom: Uses = inside always_ff instead of <=.
    • Caught by: Linter.

GPT Answer

Start with clocks/resets and ownership of each state variable, then widths, state completeness, and event semantics.

Prioritized findings

Silicon/signoff blockers:

Functional defects:

Tool split: lint catches widths, latches, drivers, reset omissions, and assignment style. CDC/RDC tools catch unsafe domain/reset structures. Humans/spec models are needed for alignment, jump policy, missed semantics, and exactly-once events; assertions/formal test those choices.

Claude Answer

What I look at first: the clock-domain comment, every async input and where it lands,
every signal with more than one driver, widths on anything with a shift or add, and then
the state machine. Findings in priority order:

Silicon-killers

  1. CDC on tsf_tick / tsf_us. tsf_tick is async and goes raw into tick_rise,
    which enables a 64-bit capture. Metastable tick_q → corrupt tsf_q. Worse, the timer
    block updates tsf_us on that same tick, so the capture samples the bus while it is
    changing. Fix: 2-flop sync on tsf_tick, edge-detect on the synced signals, capture
    tsf_us a few clk later when the timer guarantees stability, with a documented
    stability window and set_max_delay -datapath_only; or a proper handshake. (CDC tool.)
  2. next_tbtt_us is multi-driven by two always_ff blocks (async reset and none).
    Synthesis error, or last-wins and a reset-less path into a 64-bit register. (Lint.)
  3. bi_us truncation. beacon_interval_tu << 10 into BI_W bits keeps the low 16
    bits: any BI > 63 TU wraps, and 100 TU is the default. Needs BI_W+10 bits. (Lint.)
  4. The pulses never fire. tsf_q is captured at the edge after tick_rise; tbtt_hit
    becomes true one cycle later, when tick_rise is already 0; next_tbtt_us advances the
    cycle after that. tbtt_pulse <= tbtt_hit & tick_rise is therefore always 0. Register
    the hit as a one-shot and drop the tick_rise qualifier. (Human; a simple test catches it.)

Functional
5. Catch-up after a TSF jump. After any forward jump the design adds one BI per hit, so
tbtt_hit stays true for consecutive evaluations. And next_tbtt_us <= tsf_q + bi_us on
cfg_wr is not aligned to a multiple of BI, which 802.11 requires (TBTT ≡ TSF mod BI = 0).
6. pre_hit equality compare on a timestamp that double-steps under slew → missed;
next_tbtt_us − pre_tbtt_us underflows while next_tbtt_us is 0 after reset. Use a
window compare with a one-shot.
7. tbtt_missed has no reset (X / random at power-up in a sticky status bit FW reads),
a miss beats a coincident clear so a FW clear can be lost, and the > next + 1
threshold is off at µs granularity (document the tolerance). (Lint for the reset.)
8. No disable path. !en zeroes next_tbtt_us but the FSM stays ARMED, so tbtt_hit
is true every cycle; and any cfg_wr (e.g. a pre_tbtt_us change) re-bases the TBTT.
9. FSM latch. state_d unassigned in IDLE-without-write and ARMED-without-hit, no
default. Add state_d = state; at the top and a default. (Lint.)

Style / standard violations
10. Blocking assignment to armed in always_ff — works, but it is a race with any
reader and a standard violation. (Lint.)
11. Mixed reset styles across blocks; case should be unique case; the FIRED state does
nothing and can go.

Who catches what: lint → 2, 3, 7 (reset), 9, 10. CDC tool → 1. Human (or a protocol/
scenario test) → 4, 5, 6, 7 (priority), 8.

M5Q15 · B1. The firmware team reports that after a cross-chip TSF correction of +3 ms, the AP transmits a burst of several beacons back to back.

Follow-ups once you have your list:

  1. The firmware team reports that after a cross-chip TSF correction of +3 ms, the AP
    transmits a burst of several beacons back to back. Which finding explains it, and what is
    the correct behaviour per 802.11 (hint: TBTT is defined as TSF mod BI == 0)?

Gemini Answer

  1. Why firmware saw a burst of beacons after a +3 ms TSF jump & 802.11 spec behavior:
    • Finding #6 (next_tbtt_us <= next_tbtt_us + bi_us firing a pulse on every step while catching up). Per IEEE 802.11, TBTTs are strictly periodic at multiples of BI (k×BI); if TSF jumps forward past one or more TBTTs, the missed intermediate TBTTs must be skipped silently (setting tbtt_missed <= 1) and next_tbtt_us must advance to the first multiple of BI strictly greater than tsf_q without pulsing tbtt_pulse.

GPT Answer

  1. Burst after +3 ms: incrementing an expired target by one interval repeatedly replays old deadlines. Keep TBTTs on the TSF interval grid. After a jump, mark skipped deadlines as missed, emit at most the event permitted by the agreed recovery policy, and re-arm a strictly future aligned deadline. Scheduled TBTT and actual transmitted-beacon time are different: medium access can delay transmission. The number of skipped deadlines depends on the configured interval; +3 ms alone does not imply multiple default-length beacon intervals.

Claude Answer

  1. Beacon burst after +3 ms correction: finding 5. Once the TSF jumps past one or more
    TBTTs, next_tbtt_us only catches up one BI per hit, so each evaluation is another hit
    and (with the pulse logic fixed) another beacon. Correct behaviour: skip the missed TBTTs,
    flag tbtt_missed once, and fire at the next TSF value with tsf mod BI == 0.

M5Q16 · B2. Rewrite the next-TBTT computation so that after an arbitrary TSF jump it lands on the correct next multiple of the beacon interval without a divider.

  1. Rewrite the next-TBTT computation so that after an arbitrary TSF jump it lands on the
    correct next multiple of the beacon interval without a divider.

Gemini Answer

  1. Divider-less Next-TBTT Catch-Up FSM:
    • Note that 1 TU=1024 μs=210 μs, so next_tbtt_us[9:0] is always 10'b0. For arbitrary jumps without a 64-bit divider, use a binary-scaled shift-subtract / iterative catch-up FSM in clk (160 MHz): while next_tbtt_us <= tsf_q, suppress tbtt_pulse, set tbtt_missed <= 1'b1, and add the largest power-of-two multiple (bi_us << k) that keeps next_tbtt_us <= tsf_q (or add bi_us once per 6.25 ns clk cycle—catching up a 1-second jump of 10 beacons takes only 10 cycles = 62.5 ns!).

GPT Answer

  1. No-divider realignment: for positive BI, obtain remainder r=TSF mod BI using a bounded restoring-modulo FSM, then compute next=TSF+(BI-r); r=0 deliberately advances to the next interval. A 65-bit accumulator prevents intermediate overflow. Pseudocode for each of 64 timestamp bits, high to low: r=(r<<1)|bit; if(r>=BI) r-=BI. This uses a comparator/subtractor, not a combinational divider. With BI in TUs, instead reduce TSF[63:10] mod beacon_interval_tu; reconstruct the next TU-aligned multiple. A full-width overflow/epoch policy is still required. Capture a coherent timestamp, handle newer snapshots while busy, and verify the result remains future before arming; rerun/advance if time has overtaken it. A bit mask works only for power-of-two BI.

Claude Answer

  1. Catch-up without a divider. bi_us is a multiple of 1024, so align with a
    shift-and-add long division in a small FSM: start next = 0, then for k = 53 down to
    0, if next + (bi_us << k) <= tsf_q add it (54 cycles worst case, once per jump or
    config write); finish with next += bi_us so next > tsf_q. Hold tbtt_hit off while
    the FSM is busy. For small jumps the same FSM degenerates to a few single-BI adds.

M5Q17 · B3. Which of your fixes change the number of flops? Does that matter for anything downstream?

  1. Which of your fixes change the number of flops? Does that matter for anything downstream?

Gemini Answer

  1. Do these fixes change the flop count & downstream impact?
    • Yes: adding the 3-flop tsf_tick synchronizer, pre_fired_q one-shot flop, and widening bi_us if registered adds ~5 flops.
    • Downstream impact: Pre-freeze RTL: negligible area delta, automatic in synthesis/LEC/scan. Post-freeze ECO: adding flops requires spare SDFF cells, scan-chain stitching, and manual key-point mapping in Conformal LEC.

GPT Answer

  1. Flop changes: synchronizers, snapshot/handshake state, widened BI, event flags, and modulo-FSM state add flops; removing redundant state may subtract some. Rerun synthesis, sequential equivalence as appropriate, STA/CDC/RDC, scan insertion/ATPG and release checks. A post-scan or metal-only context changes what is feasible.

Claude Answer

  1. Flop changes: +2 synchronizer flops, a 64-bit delayed capture if the stability
    window needs it, the catch-up FSM (a 6-bit shift index and a state bit), a one-shot for
    the pulse; the width fix, the reset on tbtt_missed and the multi-driver removal don't
    add flops. Pre-freeze it only shifts the area number. As a post-freeze ECO, every new flop
    has to be stitched into a scan chain and mapped in LEC, so I'd prefer to fix it in RTL.

Part C — Failing-test debug scenario (0:50–0:55)

A nightly regression of 300 tests has one test that fails ~1 in 8 seeds with a scoreboard
mismatch: the expected PPDU length is off by exactly one OFDM symbol, only for LDPC + MU
cases. The RTL has not changed in two weeks; the testbench had a "refactor only" commit
yesterday.

M5Q18 · C1. Describe, in order, the first five things you do.

Describe, in order, the first five things you do. For each: what question it answers, and
what you do if the answer is yes/no.

Gemini Answer

Scenario: 1 in 8 seeds fails with PPDU length off by 1 OFDM symbol only for LDPC + MU. RTL unchanged for 2 weeks; TB had a "refactor only" commit yesterday.

  1. Step 1: Run the exact failing seed on the pre-refactor TB commit (git checkout HEAD~1).
    • Question answered: Did the yesterday TB commit introduce the failure, or just shift randomization order?
    • If it passes on HEAD~1 with identical stimulus: The TB refactor broke the reference predictor. If stimulus changed: Force the failing (MCS, RU, PSDU_LEN) vector as a directed test on both commits.
  2. Step 2: git diff the TB refactor commit specifically around the LDPC encoding-parameter predictor and DPI/class variables.
    • Question answered: Did the refactor leave a local variable uninitialized across transactions (explaining the 1-in-8 seed history dependence!) or alter integer division/rounding in N_avbits / N_pld?
    • If yes: Fix the uninitialized variable / rounding in the predictor. If no: Inspect how MU per-user loop ordering changed.
  3. Step 3: Print all intermediate IEEE 802.11 LDPC variables (N_SYM_init, N_avbits, N_pld, L_LDPC, N_cw, N_shrt, N_punc) from both the RTL and the TB scoreboard for the failing transaction.
    • Question answered: Which exact step diverges first, and which one matches the IEEE 802.11 hand-calculated equation?
    • If RTL matches IEEE spec: Bug is 100% in the TB reference model. If TB matches IEEE spec: Latent RTL corner-case bug exposed by new random trajectory.
  4. Step 4: Check for state leakage from the previous PPDU in the failing seed.
    • Question answered: Why does it fail on only 1 in 8 seeds for a deterministic math block?
    • Action: Run the failing transaction as the very first PPDU after reset vs. immediately after the preceding PPDU of the failing seed to catch stale per-user running-max or LDPC registers in RTL or TB.
  5. Step 5: Add a permanent directed regression test + SVA for that exact LDPC boundary condition and re-run 100 seeds.
    • Question answered: Is the root cause completely eliminated and locked down against regression?

GPT Answer

  1. Reproduce and freeze artifacts: rerun the failing seed with exact RTL/TB/tool/config revisions and logs. Reproducible? Reduce the trace; otherwise preserve all randomness and investigate scheduling/uninitialized state before changing logic.
  2. A/B the TB refactor: run old/new TB against the same RTL and stimulus. Only new fails? Diff copying, transaction lifetime, rounding, and sampling. Both fail? Investigate an old bug or stimulus/environment change too. A refactor correlation is evidence, not proof.
  3. Trace transaction identity/order: compare accepted PPDU/user IDs, input values, reset/flush epochs, and output sampling. Misaligned? Fix that first. Aligned? Compare arithmetic intermediates.
  4. Check LDPC/MU boundaries independently: calculate initial symbols, STBC rounding, LDPC extra-symbol condition, and common maximum using the specification. Model wrong? Repair model and regression-check it. RTL wrong? Produce a minimal boundary case and RTL fix.
  5. Minimize and close: reduce users/payload/rate combinations while preserving failure; inspect shared object handles, missing clone/copy, stale per-user arrays, DPI widths, and fork races. Add a targeted boundary test/checker, then rerun relevant matrix and seed regressions.

Claude Answer

  1. Re-run the failing seed on the pre-refactor testbench commit, same RTL. Question:
    testbench regression or latent RTL bug? Pass → the refactor changed expected values or
    stimulus; fail → latent RTL bug exposed by a new seed (also check whether that seed ever
    ran before the refactor).
  2. Diff the refactor for anything near LDPC/MU expected-value computation, DPI-C calls,
    randomisation order or $urandom consumption ("refactor only" that reorders randomize
    calls changes stimulus). Hit → bisect within it; nothing → step 3.
  3. Dump the intermediates for the failing PPDU on both sides: N_pld, N_avbits,
    N_cw, L_LDPC, N_shrt, N_punc and the extra-symbol decision. Hand-compute the spec
    result for that vector. RTL wrong → file a real bug; model wrong → fix the model.
  4. Characterise the flakiness. Same seed, same result every time? Yes → a specific
    parameter corner (an N_avbits threshold, a particular user RU); no → non-determinism:
    uninitialised memory in the C model, a static variable across DPI calls, a race between
    the scoreboard and the monitor on MU user ordering. 1-in-8 with no RTL change smells
    like the latter.
  5. Pin it with an assertion on the exact intermediate, run 50+ seeds on both commits to
    confirm cause and fix, then add the vector as a directed test and a coverage bin at the
    LDPC threshold so it can't come back silently.

Part D — Your questions (0:55–1:00)

M5Q19 · D1. Questions for the interviewer

Gemini Answer

  1. "What does your merge-request code review checklist look like—are lint and CDC reports automatically attached to the MR before human review starts?"
  2. "Of the silicon or late-emulation bugs you've seen in the MAC, what class of bug slipped past both lint and block-level DV most often?"
  3. "How big is a typical RTL merge request here, and how many reviewers sign off?"

GPT Answer

Which defects block review immediately? Who defines timer jump/missed-event semantics? How are TB-only refactors validated against existing regressions?

Claude Answer

  1. What is the review SLA, the typical MR size, and is there a required checklist?
  2. What fraction of escapes to FPGA/silicon were caught by lint vs. review vs. DV, and
    which class dominates?
  3. Does the team want tooling contributions (lint rules, checkers) from RTL engineers, or
    is that a separate methodology group?