RTL Review

Index | 02 Timing, Pipelining and Microarchitecture →

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

Contents

0. Resume probe

M1Q1 · "Pick the gnarliest SystemVerilog construct you had to support in your parser and tell me why it was hard."

Gemini Answer

GPT Answer

"I built lucisv in Rust: a lexer, recursive-descent parser, comment-preserving AST, formatter, 60+ lint rules, and a language server. Formatting is guarded by token-preservation and idempotence checks. It achieved 99.3% file-level parse success on an 820-file corpus and is in daily use on production RTL. A useful technical discussion is distinguishing declarations from expressions and preserving comments through transformations. I would use an actual implementation example when asked which construct was hardest." The resume does not identify that hardest construct.

Claude Answer

Template answer (adapt to what actually bit you):

"The preprocessor. Macros with arguments and `ifdef blocks don't respect the grammar:
a macro can expand to half a port list, and an `ifdef can split a case item in two.
A formatter that must be token-preserving and idempotent can't just expand macros and
parse the result, because the output has to be written back with the macros still in it.
I ended up parsing the unexpanded token stream, attaching macro invocations and
conditional directives as trivia on the AST nodes they sit inside, and refusing to reformat
any region where a directive cuts across a syntax node. The idempotence check
(format twice, compare) and the token-preservation check (non-trivia tokens before/after
must match exactly) are what let me ship that safely on production RTL."

Runner-up if asked for a second: the ' ambiguity — T'(x) cast vs '{...} assignment
pattern vs '0 fill literal — needs lookahead because T could be a type or a parameter.

Part A — Rapid-fire Q and A (0:05–0:20, ~1 min each)

M1Q2 · A1. Blocking vs. non-blocking: give one concrete example where using = inside an always_ff produces a simulation result that differs from the synthesized netlist.

Gemini Answer

GPT Answer

Blocking/nonblocking. always_ff @(posedge clk) a = b; plus a separate always_ff @(posedge clk) c <= a; races: simulation can give c old or new a, depending on process order. Hardware flops sample the old value if timing is met. Use nonblocking assignments for state. In contrast, a=b; c=a; within one process can synthesize the same collapsed logic that it simulates; that alone is not a guaranteed sim/synthesis mismatch.

Claude Answer

Two blocks, one uses blocking:

always_ff @(posedge clk) a = d;      // blocking
always_ff @(posedge clk) q <= a;     // reads a

Sim: the two processes run in arbitrary order in the same timestep. If the first runs first,
q sees the new d (one flop stage); otherwise the old a (two stages). The netlist is
always two flops. Blocking inside always_ff turns process ordering into functionality.

M1Q3 · A2. Name three differences between always_comb and always @(*).

Gemini Answer

  1. Function sensitivity: always_comb automatically sensitizes to signals read inside functions called from the block; always @(*) only sensitizes to the function arguments.
  2. Time-0 evaluation: always_comb is guaranteed to execute once at time 0 after all initial and always blocks have started, avoiding time-0 X-latches/races.
  3. Single-driver enforcement: always_comb makes it a compile/elaboration error for any other process to write to a variable assigned inside the always_comb block, and disallows timing controls (#, @, wait) inside it.

GPT Answer

always_comb. Runs once at time zero, includes reads inside called functions in its implicit sensitivity, and restricts other processes from writing its assigned variables. It also disallows blocking timing controls. Read-before-write ordering still matters because written expressions are excluded from sensitivity.

Claude Answer

always_comb: (1) sensitivity includes signals read inside called functions,
@(*) only sees the arguments; (2) executes once at time 0 so outputs aren't X until the
first input change; (3) variables assigned in it may not be assigned anywhere else and no
timing controls are allowed — tools error, @(*) silently allows both.

M1Q4 · A3. What does logic give you over reg/wire? Name one situation where you cannot use logic and must use wire.

Gemini Answer

GPT Answer

logic. A four-state variable type usable for procedural state or a single continuous/module driver. Resolved multiple-driver or bidirectional connections need a net, e.g. wire logic, rather than a plain variable. logic itself does not mean flop.

Claude Answer

logic is one 4-state type for both procedural and continuous assignment, and it
is single-driver so a second driver is a compile error. You must use wire for anything
with multiple drivers or resolution: tri-state buses, inout pads, pull-ups.

M1Q5 · A4. Write (speak) the smallest always_comb block that infers a latch. Then fix it two different ways.

Gemini Answer

GPT Answer

Latch. always_comb if (sel) y = a; leaves y unchanged otherwise. Fix with else y=b;, or assign y=b; before the if. Tools should diagnose unintended storage.

Claude Answer

always_comb if (en) y = a; infers a latch. Fix 1: default at the top
(y = '0; before the if). Fix 2: complete the branch (else y = '0; or y = en ? a : '0;).

M1Q6 · A5. unique case vs. priority case vs. plain case: what does synthesis do differently, and what does simulation do differently, for each?

Gemini Answer

GPT Answer

Case qualifiers. unique promises at most one match and, without default, a match; priority promises a match while retaining first-match priority. Simulation reports violations, normally warnings. Synthesis can exploit those promises. Plain case makes neither promise, but mutually exclusive items can still optimize to parallel logic. A default removes the no-match warning, not the overlap check for unique.

Claude Answer

The danger is the "full" assumption: it removes the latch and any safety default.

M1Q7 · A6. logic signed [7:0] a; logic [7:0] b; logic [8:0] y; assign y = a + b; Is the addition signed or unsigned? What gets extended, and to how many bits?

Gemini Answer

GPT Answer

Mixed addition. Unsigned, evaluated at the 9-bit assignment context. Both operands zero-extend to 9 bits; a loses signed interpretation. If signed addition was intended, explicitly sign/zero-extend each input and cast both to signed.

Claude Answer

Unsigned. One unsigned operand makes the whole expression unsigned; the LHS only
sets width. a and b are zero-extended to 9 bits (context width from y), so a's MSB
is +128 instead of −128. Fix: y = a + $signed({1'b0, b}) or declare b signed.

M1Q8 · A7. logic [7:0] a, b; logic [7:0] p; assign p = a * b; — what width is the multiplier actually evaluated at, and what does the netlist contain?

Gemini Answer

GPT Answer

Multiply width. Eight-bit expression context; only the low eight product bits are observable. Synthesis can prune the upper product logic. Assign into a 16-bit destination to retain the full unsigned product.

Claude Answer

Context width is max(8,8,8) = 8, so an 8×8 multiplier of which only the low 8
product bits survive; synthesis prunes the partial products feeding bits [15:8]. Declare
p 16 bits if you want the full product.

M1Q9 · A8. == vs. ===: which is synthesizable and why does the other one matter for X-propagation bugs that escape to silicon?

Gemini Answer

GPT Answer

Equality. == can return X; === always returns 0/1 and compares X/Z literally in simulation. === is not universally nonsynthesizable: tools can synthesize binary comparisons, but hardware cannot detect a simulator's X state. Use case inequality in scoreboards and explicit unknown checks in assertions. An X condition in an if can hide a real control ambiguity.

Claude Answer

== synthesizes; === is sim-only (compares X/Z literally). With x unknown,
if (x == 1'b1) evaluates to X, which if treats as false, so sim deterministically takes
else. Silicon has a real 0/1 and may take the then branch. Uninitialized flops hide
behind this X-optimism; === in checkers or an X-prop sim mode catches it.

M1Q10 · A9. casez vs. casex: why is casex banned in most coding standards?

Gemini Answer

GPT Answer

Wildcards. casex treats X/Z in either the selector or item as wildcards; casez treats Z/? in either as wildcards. Thus even casez can mask a floating selector bit. Prefer explicit masks or carefully controlled case inside patterns.

Claude Answer

casex treats X/Z in the selector as wildcards too, so an X on the design
signal matches the first item instead of propagating. casez only wildcards z/? in
the items. Banned because it hides X bugs and creates sim/synth mismatch.

M1Q11 · A10. When do you use a generate loop vs. a parameterized module array instance (sub u_sub[N-1:0] (...))? How does each affect hierarchical names for backend scripts?

Gemini Answer

GPT Answer

Replication. Generate supports conditional structure and per-instance parameters; instance arrays suit uniform replication. Name generated scopes explicitly and agree on elaborated hierarchy with backend; both constructs create indexed paths that scripts must handle.

Claude Answer

generate for when iterations differ (per-index parameters, if (i==0) edge
wiring, intermediate per-slice signals). Array of instances for pure replication with port
slicing. Generate adds a scope level: gen_lane[i].u_sub; leave it unnamed and you get
tool-specific genblk1[i], which breaks SDC/UPF/ECO scripts. Instance arrays give
u_sub[i] with no extra scope. Always name generate blocks.

M1Q12 · A11. Packed vs. unpacked arrays: which one does synthesis map to a memory macro, and what coding pattern makes a tool infer a RAM vs. flops?

Gemini Answer

GPT Answer

Arrays/RAM. An unpacked array of packed words is a common memory declaration. Inference depends on supported read/write ports, synchronous/asynchronous read behavior, and reset style, not just the declaration. Resetting every word often prevents RAM inference; asynchronous-read memories exist where supported. ASIC SRAMs commonly require explicit macro wrappers.

Claude Answer

Unpacked array of packed words: logic [W-1:0] mem [DEPTH];. RAM inference wants
a single synchronous write, a synchronous (registered address or registered data) read,
and no reset on the array. Async read, a reset, or more write ports than the library
macro has turns it into flops plus a mux tree.

M1Q13 · A12. When is a reset-less flop acceptable in an ASIC? What do you need to prove to sign it off?

Gemini Answer

GPT Answer

Resetless data. Acceptable when reset valid/control state prevents observation until every used bit is initialized. Prove that invariant through reset, flush, clock gating, and power transitions; check X propagation, DFT, and security requirements.

Claude Answer

Datapath registers whose contents are always qualified by a reset-controlled valid
bit. Sign-off: show no X from them can reach a control flop, FSM, write enable, or output
while valid is low — X-prop simulation, formal, or GLS without X-init. Also confirm they are
on scan chains so ATPG initializes them. Payoff: smaller reset tree and no reset recovery
timing on the datapath.

M1Q14 · A13. function vs. task in synthesizable RTL. What does automatic change, and why does it matter inside a loop?

Gemini Answer

GPT Answer

Function/task. Functions consume no simulation time; tasks may consume time, but synthesizable task bodies cannot contain arbitrary timing behavior. automatic gives each invocation separate locals. It helps concurrent/reentrant calls; a sequential loop alone does not require it. Static loop bounds, definite assignments, and supported constructs determine synthesis.

Claude Answer

function: zero time, returns a value, synthesizable. task: may consume time,
testbench territory (zero-time tasks synthesize in some tools but don't rely on it).
automatic gives each call its own locals. A static function's locals are shared, so two
calls active in the same timestep (unrolled loop iterations, two processes) can clobber
each other in sim; recursion also needs it.

M1Q15 · A14. Interfaces with modports for IP delivery: give one real pro and one real con you have seen (think SoC integration, LEC, and backend scripts).

Gemini Answer

GPT Answer

Interfaces. Pro: one shared signal/type definition reduces wiring errors. Con: parameterization and flattening can complicate LEC, hierarchy-based scripts, and tool compatibility. I would validate the delivery flow before adopting them; the resume does not document a specific interface-related incident.

Claude Answer

Pro: one definition of the MAC-to-baseband bus, adding a signal touches one file
instead of every hierarchy hop. Con: at the IP boundary the SoC integrator, LEC and backend
see flattened names (u_bb_if_tx_valid vs u_bb_if.tx_valid depending on tool), so port
matching, UPF and ECO scripts break. We deliver flat ports at the IP top for that reason.

M1Q16 · A15. Name four classes of bugs a linter catches that a passing simulation would not.

Gemini Answer

  1. Implicit width truncation / extension (e.g., assigning a 10-bit sum to an 8-bit wire where tests only used small numbers).
  2. Inferred latches in always_comb on error branches never hit by existing tests.
  3. Simulation-synthesis mismatch constructs (casex, blocking = in always_ff, incomplete sensitivity lists in legacy always @(...)).
  4. Structural/clocking hazards (asynchronous reset pin driven by combinational logic or used as synchronous data, undriven/multi-driven nets).

GPT Answer

Lint. Width/sign truncation, incomplete combinational assignments, multiple drivers, and suspicious/unreachable case items. A simulation can pass simply because its stimulus never exercises the affected behavior.

Claude Answer

(1) Width mismatch/truncation that tests never exercise. (2) Latch inference on
branches not covered. (3) Sim/synth mismatch constructs: casex, blocking in always_ff,
incomplete sensitivity lists. (4) Structural: multi-driven or undriven nets, combinational
loops, async reset used as data, clock used as data, missing default, signed/unsigned
mixing. All are "green in sim, wrong in the netlist" classes.

Part B — Live coding (0:20–0:55)

M1Q17 · B1. Valid/ready pipeline stage with a skid buffer

Write a synthesizable, parameterized module:

module skid_buf #(parameter int W = 32) (
  input  logic         clk, rst_n,
  input  logic         in_valid,
  output logic         in_ready,
  input  logic [W-1:0] in_data,
  output logic         out_valid,
  input  logic         out_ready,
  output logic [W-1:0] out_data
);

Requirements:

Gemini Answer

module skid_buf #(
  parameter int W = 32
) (
  input  logic         clk,
  input  logic         rst_n,
  input  logic         in_valid,
  output logic         in_ready,
  input  logic [W-1:0] in_data,
  output logic         out_valid,
  input  logic         out_ready,
  output logic [W-1:0] out_data
);

  // Two storage slots: main output register (out_data/out_valid)
  // and overflow skid register (skid_data_q/skid_valid_q).
  logic [W-1:0] skid_data_q;
  logic         skid_valid_q;

  wire push = in_valid && in_ready;
  wire pop  = out_valid && out_ready;

  // in_ready is registered: we can accept whenever the skid slot is empty.
  assign in_ready = ~skid_valid_q;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      out_valid    <= 1'b0;
      skid_valid_q <= 1'b0;
    end else begin
      // Main output slot occupancy:
      if (skid_valid_q) begin
        // If skid has data, out_valid stays 1 even after a pop
        out_valid <= 1'b1;
      end else if (push) begin
        out_valid <= 1'b1;
      end else if (pop) begin
        out_valid <= 1'b0;
      end

      // Skid slot occupancy: fills when we push into an already-full output that isn't popping
      if (skid_valid_q && pop) begin
        skid_valid_q <= 1'b0;
      end else if (push && out_valid && !out_ready) begin
        skid_valid_q <= 1'b1;
      end
    end
  end

  // Datapath registers (reset-less for area/power; gated by enables)
  always_ff @(posedge clk) begin
    // Load skid register only when stalling with an active push
    if (push && out_valid && !out_ready) begin
      skid_data_q <= in_data;
    end

    // Load main output register from skid on pop (if skid valid), else from input
    if (skid_valid_q && pop) begin
      out_data <= skid_data_q;
    end else if (push && (!out_valid || out_ready)) begin
      out_data <= in_data;
    end
  end

endmodule

GPT Answer

A two-entry FIFO is a simple skid-buffer implementation. in_ready is explicitly registered from next occupancy. It has one-cycle output latency and sustains one transfer per cycle with an always-ready sink. After completely filling, it deliberately waits one cycle after a dequeue before accepting again.

module skid_buf #(parameter int W = 32) (
  input  logic clk, rst_n,
  input  logic in_valid,
  output logic in_ready,
  input  logic [W-1:0] in_data,
  output logic out_valid,
  input  logic out_ready,
  output logic [W-1:0] out_data
);
  logic [W-1:0] data_q [2];
  logic rd_q, wr_q;
  logic [1:0] count_q, count_d;
  logic push, pop;
  assign out_valid = (count_q != 2'd0);
  assign out_data = data_q[rd_q];
  assign push = in_valid && in_ready;
  assign pop = out_valid && out_ready;
  always_comb begin
    count_d = count_q;
    case ({push, pop})
      2'b10: count_d = count_q + 2'd1;
      2'b01: count_d = count_q - 2'd1;
      default: ;
    endcase
  end
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rd_q <= 1'b0;
      wr_q <= 1'b0;
      count_q <= 2'd0;
      in_ready <= 1'b1;
    end else begin
      count_q <= count_d;
      in_ready <= (count_d < 2'd2);
      if (push) wr_q <= ~wr_q;
      if (pop) rd_q <= ~rd_q;
    end
  end
  // Data need not reset: count_q qualifies all observations.
  always_ff @(posedge clk)
    if (rst_n && push) data_q[wr_q] <= in_data;
endmodule

Claude Answer

module skid_buf #(parameter int W = 32) (
  input  logic         clk, rst_n,
  input  logic         in_valid,
  output logic         in_ready,
  input  logic [W-1:0] in_data,
  output logic         out_valid,
  input  logic         out_ready,
  output logic [W-1:0] out_data
);
  logic         skid_valid;
  logic [W-1:0] skid_data;
  logic         out_load, in_fire;

  assign in_ready = ~skid_valid;                 // registered: pure flop output
  assign out_load = ~out_valid | out_ready;      // output register can advance
  assign in_fire  = in_valid & in_ready;

  // control (reset)
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      out_valid  <= 1'b0;
      skid_valid <= 1'b0;
    end else begin
      if (out_load)
        out_valid <= skid_valid | in_valid;      // skid first, else input
      if (in_fire && !out_load)
        skid_valid <= 1'b1;                      // accepted but output stalled
      else if (out_load)
        skid_valid <= 1'b0;                      // drained into output register
    end
  end

  // datapath (no reset)
  always_ff @(posedge clk) begin
    if (out_load)
      out_data <= skid_valid ? skid_data : in_data;
    if (in_fire && !out_load)
      skid_data <= in_data;
  end
endmodule

in_valid can only reach out_valid when skid_valid is 0, and in_ready is 1 exactly
then, so the "skid first, else input" line is correct without a mux on valid.

M1Q18 · B1.1 How many data registers does your design have? Can it be done with one? Why not?

Gemini Answer

GPT Answer

Claude Answer

M1Q19 · B1.2 Where is the critical path? What breaks if out_ready fans out to 200 of these?

Gemini Answer

GPT Answer

Claude Answer

M1Q20 · B1.3 Sketch the three SVA properties you would put on this module.

Gemini Answer

GPT Answer

assert property (@(posedge clk) disable iff (!rst_n)
  out_valid && !out_ready |=> out_valid && $stable(out_data));
assert property (@(posedge clk) disable iff (!rst_n)
  count_q <= 2'd2 && in_ready == (count_q < 2'd2));
// Third check: on push append in_data to a reference queue;
// on pop compare out_data to its oldest entry, then remove it.

Claude Answer

M1Q21 · B2. N-way round-robin arbiter feeding the skid buffer

module rr_arb #(parameter int N = 4, parameter int W = 32) (
  input  logic           clk, rst_n,
  input  logic [N-1:0]   req_valid,
  output logic [N-1:0]   req_ready,
  input  logic [W-1:0]   req_data [N],
  output logic           out_valid,
  input  logic           out_ready,
  output logic [W-1:0]   out_data,
  output logic [$clog2(N)-1:0] out_id
);

Requirements:

Gemini Answer

module rr_arb #(
  parameter int N = 4,
  parameter int W = 32,
  parameter int ID_W = (N > 1) ? $clog2(N) : 1
) (
  input  logic            clk,
  input  logic            rst_n,
  input  logic [N-1:0]    req_valid,
  output logic [N-1:0]    req_ready,
  input  logic [W-1:0]    req_data [N],
  output logic            out_valid,
  input  logic            out_ready,
  output logic [W-1:0]    out_data,
  output logic [ID_W-1:0] out_id
);

  logic            sb_in_valid;
  logic            sb_in_ready;
  logic [W-1:0]    win_data;
  logic [ID_W-1:0] win_id;

  // Mask holds 1s for indices >= current highest-priority pointer (thermometer mask)
  // After granting i, mask_q has 1s for [N-1 : i+1] (or all 1s if i == N-1)
  logic [N-1:0] mask_q, mask_d;
  logic [N-1:0] req_masked;
  logic [N-1:0] gnt_masked, gnt_unmasked, gnt;

  assign req_masked = req_valid & mask_q;

  // Isolate lowest set bit: x & (~x + 1)
  assign gnt_masked   = req_masked & (~req_masked + N'(1));
  assign gnt_unmasked = req_valid  & (~req_valid  + N'(1));

  // If any masked request exists, use gnt_masked; otherwise wrap around to gnt_unmasked
  assign gnt = (|req_masked) ? gnt_masked : gnt_unmasked;

  assign sb_in_valid = |req_valid;
  assign req_ready   = sb_in_ready ? gnt : '0;

  // Next thermometer mask: all bits strictly above the granted bit are 1
  // Note: ~( (gnt - 1) | gnt ) sets bits > granted index to 1, and wrap to all-1s if MSB granted
  always_comb begin
    mask_d = mask_q;
    if (sb_in_valid && sb_in_ready) begin
      if (gnt[N-1]) mask_d = {N{1'b1}};
      else          mask_d = ~((gnt - N'(1)) | gnt);
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) mask_q <= {N{1'b1}};
    else        mask_q <= mask_d;
  end

  // One-hot mux for data and binary ID
  always_comb begin
    win_data = '0;
    win_id   = '0;
    for (int i = 0; i < N; i++) begin
      if (gnt[i]) begin
        win_data |= req_data[i];
        win_id   |= ID_W'(i);
      end
    end
  end

  // Instantiate skid buffer for {out_id, out_data}
  skid_buf #(
    .W(ID_W + W)
  ) u_skid_buf (
    .clk       (clk),
    .rst_n     (rst_n),
    .in_valid  (sb_in_valid),
    .in_ready  (sb_in_ready),
    .in_data   ({win_id, win_data}),
    .out_valid (out_valid),
    .out_ready (out_ready),
    .out_data  ({out_id, out_data})
  );

endmodule

GPT Answer

Arbitrate admission into the buffer, advance priority only on acceptance, and buffer the ID with the payload. A masked encoder followed by an unmasked fallback avoids an N-by-N search. Lock selection when the buffer stalls so newly arriving requests cannot change a presented transfer.

The original out_id[$clog2(N)-1:0] declaration is unsuitable for N=1; the guarded width below supports all N>=1. W>=1 is assumed.

module rr_arb #(parameter int N = 4, parameter int W = 32,
                parameter int IW = (N > 1 ? $clog2(N) : 1)) (
  input logic clk, rst_n,
  input logic [N-1:0] req_valid,
  output logic [N-1:0] req_ready,
  input logic [W-1:0] req_data [N],
  output logic out_valid,
  input logic out_ready,
  output logic [W-1:0] out_data,
  output logic [IW-1:0] out_id
);
  logic [IW-1:0] ptr_q, chosen, lock_id_q;
  logic lock_q, found, arb_valid, buf_ready;
  logic [W+IW-1:0] buf_in, buf_out;
  always_comb begin
    chosen = '0;
    found = 1'b0;
    for (int i = 0; i < N; i++) begin
      if (!found && req_valid[i] && (i >= int'(ptr_q))) begin
        chosen = IW'(i);
        found = 1'b1;
      end
    end
    if (!found) begin
      for (int i = 0; i < N; i++) begin
        if (!found && req_valid[i]) begin
          chosen = IW'(i);
          found = 1'b1;
        end
      end
    end
    if (lock_q) chosen = lock_id_q;
    arb_valid = lock_q ? req_valid[lock_id_q] : found;
    req_ready = '0;
    if (arb_valid) req_ready[chosen] = buf_ready;
    buf_in = {chosen, req_data[chosen]};
  end
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      ptr_q <= '0;
      lock_q <= 1'b0;
      lock_id_q <= '0;
    end else if (arb_valid && buf_ready) begin
      ptr_q <= (chosen == IW'(N-1)) ? '0 : chosen + IW'(1);
      lock_q <= 1'b0;
    end else if (arb_valid && !lock_q) begin
      lock_q <= 1'b1;
      lock_id_q <= chosen;
    end
  end
  skid_buf #(.W(W+IW)) u_buf (
    .clk, .rst_n, .in_valid(arb_valid), .in_ready(buf_ready),
    .in_data(buf_in), .out_valid, .out_ready, .out_data(buf_out)
  );
  assign {out_id, out_data} = buf_out;
endmodule

Claude Answer

module rr_arb #(parameter int N = 4, parameter int W = 32) (
  input  logic           clk, rst_n,
  input  logic [N-1:0]   req_valid,
  output logic [N-1:0]   req_ready,
  input  logic [W-1:0]   req_data [N],
  output logic           out_valid,
  input  logic           out_ready,
  output logic [W-1:0]   out_data,
  output logic [$clog2(N)-1:0] out_id
);
  localparam int IW = $clog2(N);

  logic [IW-1:0]   ptr;                       // requester with top priority
  logic [2*N-1:0]  req_dbl, grant_dbl;
  logic [N-1:0]    req_rot, grant_rot, grant_c, grant_q, grant;
  logic            lock_q;

  // rotate right by ptr (double-width shift avoids modulo), fixed-priority
  // pick of the lowest set bit, rotate back left.
  assign req_dbl   = {req_valid, req_valid} >> ptr;
  assign req_rot   = req_dbl[N-1:0];
  assign grant_rot = req_rot & ~(req_rot - 1'b1);   // isolate LSB, O(N) chain
  assign grant_dbl = {grant_rot, grant_rot} << ptr;
  assign grant_c   = grant_dbl[2*N-1:N];
  // hold the grant while the stage below is stalled (valid/ready hold rule)
  assign grant     = lock_q ? grant_q : grant_c;

  logic            sk_in_valid, sk_in_ready, fire;
  logic [W-1:0]    sk_in_data;
  logic [IW-1:0]   sk_in_id;

  always_comb begin                            // one-hot AND-OR mux + encode
    sk_in_data = '0;
    sk_in_id   = '0;
    for (int i = 0; i < N; i++) begin
      if (grant[i]) begin
        sk_in_data = sk_in_data | req_data[i];
        sk_in_id   = sk_in_id   | IW'(i);
      end
    end
  end
  assign sk_in_valid = |grant;
  assign req_ready   = grant & {N{sk_in_ready}};
  assign fire        = sk_in_valid & sk_in_ready;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      ptr     <= '0;
      lock_q  <= 1'b0;
      grant_q <= '0;
    end else begin
      lock_q  <= sk_in_valid & ~sk_in_ready;
      grant_q <= grant;
      if (fire)
        ptr <= (sk_in_id == IW'(N-1)) ? '0 : sk_in_id + 1'b1;   // non-pow2 wrap
    end
  end

  logic [W+IW-1:0] sk_out;
  skid_buf #(.W(W+IW)) u_skid (
    .clk, .rst_n,
    .in_valid (sk_in_valid), .in_ready (sk_in_ready),
    .in_data  ({sk_in_id, sk_in_data}),
    .out_valid, .out_ready,
    .out_data (sk_out)
  );
  assign {out_id, out_data} = sk_out;
endmodule

(For N == 1, $clog2(1) = 0 makes the out_id port [-1:0]; guard with
localparam int IW = (N > 1) ? $clog2(N) : 1; and a matching port if N = 1 is real.)

M1Q22 · B2.1 Describe the classic "double-width priority encoder" trick for round-robin and its gate count versus a rotate-encode-rotate implementation.

Gemini Answer

GPT Answer

Claude Answer

M1Q23 · B2.2 A new requirement arrives: requester 0 must get at least 50% of grants when it is always requesting. Smallest change to your design?

Gemini Answer

GPT Answer

Claude Answer

M1Q24 · B2.3 What functional coverage bins would you want to see hit before trusting this arbiter?

Gemini Answer

GPT Answer

Claude Answer

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

M1Q25 · Questions for the interviewer

Gemini Answer

  1. "Since you own the SystemVerilog coding guidelines, what are the top two or three constructs that cause the most friction in code reviews or lint waivers here?"
  2. "How much of the coding standard is enforced automatically in pre-merge CI (SpyGlass / Ascent / custom scripts) versus human review?"
  3. "What is the team's philosophy on SystemVerilog interfaces vs. flattened structs at subsystem boundaries when handing off to synthesis and LEC?"

GPT Answer

  1. Which lint rules are release gates, and how are waivers reviewed?
  2. What SystemVerilog constructs are supported consistently across synthesis, LEC, and integration?
  3. How are small handshake blocks verified: simulation, formal, or both?

Claude Answer

  1. What fraction of the coding standard is enforced by lint vs. caught in review, and is
    the waiver process per-line or per-rule?
  2. Is the group moving to interfaces for IP delivery, and how are the LEC/backend naming
    issues handled if so?
  3. Which sim/synth mismatch has escaped furthest here, and what rule came out of it?

Additional notes

Notes. Material not tied to a question

Gemini Answer

No answer in gemini_answer.md.

GPT Answer

These are practice answers. Resume statements below are supported by the supplied resume; proposed implementations and tradeoffs are not claims about the production design. Question numbers follow interview.md.

Technical reference and validation

The code is an interview implementation, not a claimed extract from lucisv or NXP RTL. No HDL simulator is installed in the current environment, so compilation and RTL simulation remain unverified.

Tool-specific equality support: AMD UG901 supported expressions. In particular, the local answer key's blanket claim that === is simulation-only is too strong.

Claude Answer

No answer in claude_answer.md.