🛠️ Verilog RTL Design – Writing Rules & Best Practices #
When writing RTL modules in Verilog, adhering to consistent structure and syntax is crucial for readability, synthesizability, and scalability. Here are the golden rules to follow:
📌 1. Module Definition #
Start every design block with a module
and end with endmodule
.
In the image below, you can see an abstract representation of this module. It includes only the module declaration and port definitions. At this stage, it can now be connected to another module or instantiated inside another module.
module module_name(
input wire clk,
input wire rst,
input wire a,
output wire y
);
// Internal logic here
endmodule
📌 2. Use Meaningful Port Names #
Avoid single-letter names in real designs — use clk
, rst_n
, data_in
, valid
, etc.
📌 3. Port Direction & Type Declaration #
Use input
, output
, and optionally inout
. Ports should be typed as wire
by default. Use reg
only for values driven inside always
blocks.
input wire clk;
output reg valid_out; // Driven inside always block
📌 4. Consistent Formatting #
Keep clean indentation and consistent naming. Example:
assign y = a & b;
📌 5. Combinational Logic → assign
or always @(*)
#
Use assign
for continuous assignments (preferred for simple logic):
assign y = a & b;
Or, for more complex logic:
always @(*) begin
case (sel)
2'b00: out = in0;
2'b01: out = in1;
...
endcase
end
📌 6. Sequential Logic → always @(posedge clk)
#
Use reg
type and non-blocking assignment (<=
) for sequential logic.
always @(posedge clk or posedge rst) begin
if (rst)
count <= 0;
else
count <= count + 1;
end
📌 7. Don’t Mix Blocking and Non-blocking #
In the same always
block, use only one type:
=
for combinational logic (always @(*)
)<=
for sequential logic (always @(posedge clk)
)
📌 8. Reset Logic #
Always define reset behavior if required by synthesis/initialization.
if (rst) begin
state <= IDLE;
end
📌 9. Avoid Delay #
in RTL
#
Delays like #10
are for testbenches only. They are not synthesizable and should never appear in RTL.
📌 10. Avoid initial
in RTL
#
Use initial
only in testbenches or simulation-only code.
✅ Summary Table #
Rule | Do This | Avoid This |
---|---|---|
Use module and endmodule |
✅ module mymod(...); |
❌ Unnamed or inline code |
Use assign or always |
✅ assign y = a & b; |
❌ always y = a & b; |
Use <= for seq logic |
✅ count <= count + 1; |
❌ count = count + 1; |
Use reg inside always |
✅ reg valid; always @(...) |
❌ wire assigned in always |
Avoid delays in RTL | ✅ assign #no_delay |
❌ #10 , #5 in logic |
Clear reset logic | ✅ if (rst) q <= 0; |
❌ No reset, or floating states |
🧪 Verilog Testbench – Writing Rules & Best Practices #
A testbench is a simulation-only Verilog module used to stimulate and observe the behavior of your RTL design. It is not synthesizable and is essential for verifying correctness before hardware implementation.
📌 1. No Ports in Testbench Module #
Testbenches are not connected to external pins, so no input
/output
.
module tb_my_design;
📌 2. Declare Signals to Connect to DUT #
Define reg
for inputs and wire
for outputs of your DUT.
reg clk, rst, a, b;
wire y;
📌 3. Instantiate the DUT (Design Under Test) #
Connect declared signals to your RTL module using named mapping.
my_design dut (
.clk(clk),
.rst(rst),
.a(a),
.b(b),
.y(y)
);
📌 4. Generate Clock Signal #
Use an always
block to toggle clock.
initial clk = 0;
always #5 clk = ~clk; // 10ns clock period
📌 5. Apply Stimulus Using initial
Block
#
Use initial
to define your test sequence.
initial begin
rst = 1; a = 0; b = 0;
#10 rst = 0;
#10 a = 1;
#10 b = 1;
#20 $finish;
end
📌 6. Monitor Outputs with $monitor
or $display
#
Use these for observing signal values in the terminal or waveform.
initial begin
$monitor("Time=%0t : a=%b b=%b y=%b", $time, a, b, y);
end
📌 7. Use $dumpfile
and $dumpvars
for Waveform (GTKWave etc.)
#
Enable waveform generation for viewing in tools like GTKWave.
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb_my_design);
end
✅ Sample Testbench Template #
module tb_and_gate;
reg a, b;
wire y;
// Instantiate DUT
and_gate dut (
.a(a),
.b(b),
.y(y)
);
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb_and_gate);
$monitor("Time: %0t | a=%b b=%b y=%b", $time, a, b, y);
// Stimulus
a = 0; b = 0; #10;
a = 1; b = 0; #10;
a = 0; b = 1; #10;
a = 1; b = 1; #10;
$finish;
end
endmodule
🎯 Summary Table #
Rule | Do This | Avoid This |
---|---|---|
No ports in testbench | ✅ module tb_my_module; |
❌ Using input/output |
Clock generation | ✅ always #5 clk = ~clk; |
❌ Manual toggling inside initial |
Use initial for stimulus |
✅ a = 1; #10; b = 0; |
❌ Using always with delays |
Monitor output | ✅ $monitor or $display |
❌ No visibility into values |
Dump waveform | ✅ $dumpfile("wave.vcd") |
❌ No waveform = no debug |
📚 Recommended Verilog Coding Style Guide #
When writing Verilog code—whether for FPGA prototyping, ASIC design, or formal verification—following a clear and consistent coding style is crucial. It not only improves readability and maintainability but also helps teams collaborate more effectively.
For this reason, I highly recommend referring to the lowRISC Verilog Coding Style Guide as a baseline for best practices.
🔗 Guide Link: 👉
Why follow this guide? #
- ✅ Clear conventions for signal naming and module hierarchy
- ✅ Guidelines for formatting, spacing, and indentation
- ✅ Recommendations for synthesizable vs non-synthesizable constructs
- ✅ Practices for simulation clarity and debugging ease
Whether you’re just getting started with Verilog or you’re building a large-scale design project, adopting this guide can save time and prevent costly misunderstandings.