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

Worked example: Blocking vs non-blocking

Given: always @(posedge clk) begin b = a; c = b; end

Find: Hardware inferred.

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.

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
endmodule

The 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

Applications

Key takeaways

Going further