This guide explains how to turn a specification into RTL in a structured way. Use it after you have read and understood the spec (see UNDERSTANDING_THE_SPEC.md).
Translation follows the same order you use when reading the spec:
- Interface → Module declaration and port list.
- Timing → Choice of
alwaysblock (e.g.,always @(posedge clk)). - Reset → First branch inside the block (what happens when reset is active).
- Normal behavior → Remaining branches (enable, increment, hold, etc.).
- Edge cases → Same blocks (e.g., wrap is often “free” with the right width).
You implement one concern at a time and keep a clear mapping: this line of RTL implements this sentence in the spec.
From the spec: The “Interface” or “Ports” section lists every input and output with direction and width.
In RTL: Write the module name and port list. Use the exact names and widths from the spec so that verification and integration don’t break.
| Spec says | RTL you write |
|---|---|
| input, 1 bit | input wire sig_name or input wire sig_name |
| output, 8 bits | output reg [7:0] count (if it’s a register) or output wire [7:0] count |
| Clock | input wire clk |
| Active-low reset | input wire rst_n |
Rule of thumb: Outputs that hold state (e.g., a counter value) are output reg and are assigned in an always block. Outputs that are pure combinational logic can be output wire and assigned with assign.
Example (counter):
- Spec:
clk(input, 1),rst_n(input, 1),enable(input, 1),count(output, 8). - RTL:
module counter ( input wire clk, input wire rst_n, input wire enable, output reg [7:0] count );
From the spec: Look for “synchronous to the positive edge of clk” or “on the rising edge.” That tells you:
- Use a clocked
alwaysblock:always @(posedge clk). - All registered outputs (like
count) are updated only in this block.
If the spec says asynchronous reset (e.g., “reset takes effect immediately”), the sensitivity list includes the reset edge, e.g. always @(posedge clk or negedge rst_n). If it says synchronous reset, only the clock is in the sensitivity list: always @(posedge clk).
Example (counter with synchronous reset):
- Spec: “All behavior is synchronous to the positive edge of clk” and “active-low synchronous reset.”
- RTL:
always @(posedge clk) begin ... end
Nonegedge rst_nin the list; reset is evaluated inside the block on each clock edge.
From the spec: “When reset is asserted, output X is 0” (or whatever the reset values are).
In RTL: Make reset the first condition in the block. Use the same polarity as the spec (e.g., if (!rst_n) for active-low).
always @(posedge clk) begin
if (!rst_n) begin
count <= 8'h00; // or whatever the spec says
end
else ...
endWhy first? So that nothing else (enable, increment, etc.) can override reset. Reset has highest priority.
Checklist: For every output (and important internal register), assign the exact reset value the spec requires (same width and value, e.g. 8'h00 for an 8-bit zero).
From the spec: “When reset is not asserted, if enable is high, count increments; otherwise count holds.”
In RTL: After the reset branch, use else if (and possibly else) for each case. Order of branches = priority (same as in the spec).
Typical pattern:
always @(posedge clk) begin
if (!rst_n)
count <= 8'h00;
else if (enable)
count <= count + 8'd1;
// else: hold (no write to count, so it keeps its value)
endMapping:
- “If enable is high, increment” →
else if (enable) count <= count + 8'd1; - “If enable is low, hold” → no assignment to
countin that case; the register keeps its value.
Width: Use the same width as the spec (e.g., 8-bit counter → 8'd1, [7:0] count). That way wrap (255 → 0) is automatic and matches the spec.
From the spec: “If count is 255, the next increment wraps to 0.”
In RTL: For an 8-bit register, count + 8'd1 when count == 255 already gives 0 in Verilog (unsigned wrap). So you don’t need an extra if (count == 255) count <= 0. The spec’s edge case is satisfied by correct width and a single increment.
For other blocks you might need explicit logic (e.g., “if full, do not increment”). The key is: one edge case in the spec → one place in the RTL (or one decision) that implements it.
As you write RTL, keep a mental (or written) map:
| Spec section / sentence | RTL location |
|---|---|
| Interface table | Module port list |
| “Synchronous to posedge clk” | always @(posedge clk) |
| “When rst_n is low, count is 0” | if (!rst_n) count <= 8'h00; |
| “When enable is high, increment” | else if (enable) count <= count + 8'd1; |
| “When enable is low, hold” | No assignment in else branch |
| “255 wraps to 0” | 8-bit addition (implicit) |
If someone asks “where do we implement X?”, you should be able to point to a specific line or block. If the spec is updated, you know exactly which RTL to change.
- Ports from the interface table → module declaration.
- Clock and reset type → sensitivity list and reset style.
- Reset values → first branch in the
alwaysblock. - Normal behavior →
else if/elsebranches in priority order. - Edge cases → either implicit (e.g., wrap) or one explicit condition.
- Traceability → every requirement in the spec has a corresponding RTL place.
A full walkthrough of the counter spec turned into RTL—with each spec sentence mapped to the exact line of Verilog—is in:
Open SPEC.md, dut/counter.v, and the walkthrough side by side to see the translation in practice.