RTL Design with Verilog
Register-transfer level design describes hardware as registers and the logic between them. Learn Verilog modules, ports, wires and regs, continuous vs procedural assignments, blocking vs non-blocking, combinational and sequential always blocks, finite state machines, parameters, testbenches, and synthesisable coding rules.
Verilog (IEEE 1364, now part of SystemVerilog 1800) is the most widely used hardware description language. Unlike software, every line describes hardware that runs in parallel — so the mental model is circuits, not sequential instructions.
An analogy
Writing RTL is like drawing a factory floor plan in text: registers are storage bins updated at each shift bell (clock), and combinational logic is the conveyor machinery between bins.
Modules and ports
A module has inputs, outputs and internal signals. Modules are instantiated inside other modules to build hierarchy — like placing chips on a board.
Coding rules
Use non-blocking (<=) in clocked blocks and blocking (=) in combinational blocks. Assign every output in every path of combinational always blocks (defaults) to avoid unintended latches. One clock edge per always block; synchronous or asynchronous reset consistently.
FSMs
Moore (outputs depend on state) vs Mealy (on state and inputs). Encodings: binary, Gray, one-hot (fast in FPGAs). Always include a default state for recovery.
Synthesis awareness
Not all Verilog is synthesisable (delays #, initial blocks in ASICs, $display). Think about the hardware each construct creates: case → MUX, + → adder, * → multiplier. Lint tools (Verilator, Spyglass) catch issues early.
Formulas
- Sequential logic:
always @(posedge clk) q <= d; - Combinational logic:
always @* y = a & b; or assign y = a & b; - FSM state bits:
binary: ⌈log₂ N⌉; one-hot: N
Worked example: Blocking vs non-blocking
Given: always @(posedge clk) begin b = a; c = b; end
Find: Hardware inferred.
- Blocking: c gets the new b (= a) → one flip-flop chain collapsed
- With <=: c gets the old b → two-stage shift register
Answer: Use <= for registers to get the intended pipeline
Worked example: State bits
Given: FSM with 12 states
Find: Binary vs one-hot flip-flops.
- Binary: ⌈log₂ 12⌉ = 4
- One-hot: 12
Answer: 4 vs 12 flip-flops (one-hot has simpler logic)
Traffic-light FSM with testbench
module traffic_fsm #(parameter GREEN_T = 30, YELLOW_T = 5, RED_T = 25) (
input wire clk, rst_n, // 1 Hz tick clock for simplicity
output reg [2:0] light // {red, yellow, green}
);
localparam S_GREEN = 2'd0, S_YELLOW = 2'd1, S_RED = 2'd2;
reg [1:0] state, next;
reg [5:0] timer;
// state register + timer (sequential: non-blocking)
always @(posedge clk or negedge rst_n)
if (!rst_n) begin state <= S_RED; timer <= 0; end
else if (next != state) begin state <= next; timer <= 0; end
else timer <= timer + 1'b1;
// next-state logic (combinational: blocking, defaults avoid latches)
always @* begin
next = state;
case (state)
S_GREEN: if (timer == GREEN_T - 1) next = S_YELLOW;
S_YELLOW: if (timer == YELLOW_T - 1) next = S_RED;
S_RED: if (timer == RED_T - 1) next = S_GREEN;
default: next = S_RED;
endcase
end
// Moore outputs
always @* case (state)
S_GREEN: light = 3'b001;
S_YELLOW: light = 3'b010;
default: light = 3'b100;
endcase
endmodule
module tb;
reg clk = 0, rst_n = 0;
wire [2:0] light;
traffic_fsm #(.GREEN_T(3), .YELLOW_T(1), .RED_T(2)) dut (clk, rst_n, light);
always #5 clk = ~clk;
initial begin
$monitor("%0t state light=%b", $time, light);
#12 rst_n = 1;
#200 $finish;
end
endmoduleThe classic three-block FSM style separates the state register, next-state logic and outputs. Parameters let the testbench shrink timings for quick simulation.
Common mistakes
- Wrong: Missing defaults in combinational blocks. — Right: Assign defaults to avoid latches.
- Wrong: Mixing blocking and non-blocking in one block. — Right: Keep sequential and combinational blocks separate.
Applications
- Controllers: Protocol engines and FSMs.
- Datapaths: DSP pipelines and ALUs.
- FPGA prototypes: Rapid hardware development.
Key takeaways
- RTL = registers + logic between them.
- <= in clocked blocks, = in combinational.
- Avoid latches with defaults.
- FSM: register + next-state + output blocks.
Going further
- Clock-domain crossing: Synchronisers and async FIFOs in RTL.
- Linting: Verilator/Spyglass rule checks.
- Parameterised design: generate blocks and reusable IP.