DESIGN TRACK · CHAPTER 14

Implement the RTL in SystemVerilog

Translate the architecture into synthesizable, maintainable, cycle-accurate hardware.

Estimated work: 8-12 hoursChapter goal: Implement the streaming accelerator and pass lint with no serious warnings. Every assignment should have a clear hardware meaning.

RTL describes circuits

always_ff models registers, and nonblocking assignments make state updates occur together after the clock edge. always_comb models combinational logic and must assign every output on every path, or it infers a latch. Drive each register from one sequential block. Avoid arbitrary delays, dynamic simulation-only structures, and code whose result depends on simulator scheduling.

always_ff @(posedge clk) begin
  if (!rst_n) begin
    count_q <= '0;
    sum_q   <= '0;
  end else if (accept) begin
    count_q <= count_q + 1'b1;
    sum_q   <= sum_q + {{8{1'b0}}, in_data};
  end
end
assign accept = in_valid && in_ready;

Make widths and signedness explicit. Unsized constants, signed/unsigned mixing, and silent truncation are common sources of silicon bugs. A useful naming convention is state_q for registered state and state_d for its combinational next value.

Separate control from datapath

Use IDLE, RUN, and RESULT states. IDLE accepts start; RUN accepts samples; RESULT holds out_valid and out_sum until the consumer completes a handshake. When output is stalled, both must remain stable. Keep the FSM responsible for sequencing and the datapath responsible for count and sum.

unique case (state_q)
  IDLE:   if (start_pulse) state_d = RUN;
  RUN: begin
    in_ready = 1'b1;
    if (accept && last_sample) state_d = RESULT;
  end
  RESULT: begin
    out_valid = 1'b1;
    if (out_ready) state_d = IDLE;
  end
endcase

Check the last-sample boundary carefully: the reported result must include the sample accepted on that same edge, not only the previous value of sum_q.

Reset and clock-domain crossings

Reset only state that affects control or externally visible behavior; large datapaths can often be masked by valid bits. For clock crossings, use a two-flop synchronizer for a stable single-bit level, a handshake for events, and an asynchronous FIFO for multi-bit streams. Synchronizing each bit of a bus independently does not preserve a coherent word.

Parameterize dimensions that genuinely vary, such as DATA_WIDTH, and assert legal parameter ranges. Every new configuration multiplies the verification space; a first tapeout benefits more from one proven configuration than unbounded genericity.

Chapter deliverables

  • Top-level RTL, register block, and streaming datapath.
  • Verilator lint or equivalent with all width, latch, and multiple-driver findings resolved.
  • Waveform review for normal traffic, input bubbles, output stalls, and reset during a batch.
  • Concise module documentation for clocks, resets, handshakes, and parameters.
Exit criterion: arbitrary pauses at either interface never lose, duplicate, or alter data, and reset always restores the specified external state.

Tools and further reading

← Chapter 13: From Product Requirements to Chip Architecture