The starting point of every chip. Learn to write clean, synthesizable Verilog — the rules the synthesizer actually enforces, not just the simulator.
Register Transfer Level (RTL) is an abstraction that describes how data flows between registers and how combinational logic transforms that data each clock cycle. RTL is the level of abstraction that synthesis tools consume — above gate-level netlist, below behavioral/algorithmic.
An RTL description must be synthesizable: every construct must have a well-defined hardware equivalent. Simulation-only constructs (delays #10, $display, initial blocks with random inputs) are ignored or cause errors during synthesis.
always block infers either combinational logic or flip-flops. The synthesizer does not "understand" your intent; it follows rules mechanically.Combinational logic has no memory — output depends only on current inputs. In Verilog, use always @(*) (or always_comb in SystemVerilog). All inputs must appear in the sensitivity list; @(*) handles this automatically.
// Correct: 4-to-1 MUX module mux4 ( input logic [3:0] d, input logic [1:0] sel, output logic y ); always_comb begin case (sel) 2'b00: y = d[0]; 2'b01: y = d[1]; 2'b10: y = d[2]; default: y = d[3]; // default prevents latch endcase end endmodule
always block does not assign an output in every branch (missing else or missing default), the synthesizer infers a latch to "hold" the previous value. Latches are almost always unintentional and cause timing problems. Always use default in case and cover all if/else branches.Sequential logic stores state in flip-flops clocked on a clock edge. Use always_ff @(posedge clk) in SystemVerilog. Resets can be synchronous (sampled by the clock) or asynchronous (in sensitivity list).
// D flip-flop — synchronous reset always_ff @(posedge clk) begin if (rst_n == 1'b0) q <= 1'b0; else q <= d; end // D flip-flop — asynchronous reset always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 1'b0; else q <= d; end
| Property | Synchronous Reset | Asynchronous Reset |
|---|---|---|
| Reset timing | Sampled at clock edge | Immediate (any time) |
| Timing analysis | Reset path analyzed like data | Recovery/removal checks required |
| FPGA resources | Uses dedicated sync reset | Uses dedicated async reset |
| Preferred in | Most ASIC flows | Power-on reset, safety-critical |
| CDC concern | Must synchronize reset | Reset synchronizer needed |
This is the most common RTL bug source. The rule is simple and must be followed without exception:
<= (non-blocking) — always use in always_ff (sequential blocks)= (blocking) — always use in always_comb (combinational blocks)// WRONG — blocking in sequential: creates combinational chain, not pipeline always_ff @(posedge clk) begin a = b + c; // BAD: '=' in sequential d = a + e; // a is already updated above — no pipeline register! end // CORRECT — non-blocking: both sample old values, update simultaneously always_ff @(posedge clk) begin stage1 <= b + c; // samples b, c at current clock edge stage2 <= stage1 + e; // samples OLD stage1 — proper pipeline register end
Non-blocking assignments schedule updates to occur after all RHS expressions in the time step have been evaluated — this models registers correctly because all FFs sample their inputs simultaneously at the clock edge.
FSMs are the backbone of control logic. The recommended coding style separates state registers, next-state logic, and output logic into three always blocks — called 3-always style. This maps cleanly to hardware and passes lint cleanly.
| Style | State reg | Next-state | Output | Notes |
|---|---|---|---|---|
| Mealy | FF | Comb | Comb (uses input) | Faster output, harder to pipeline |
| Moore | FF | Comb | Reg or Comb (state only) | Easier to time, preferred in ASIC |
// 3-process Moore FSM: simple 3-state traffic light controller typedef enum logic [1:0] {RED, GREEN, YELLOW} state_t; state_t state, next_state; // Process 1: state register always_ff @(posedge clk or negedge rst_n) if (!rst_n) state <= RED; else state <= next_state; // Process 2: next-state logic (combinational) always_comb begin next_state = state; // default: stay in current state case (state) RED: if (timer_done) next_state = GREEN; GREEN: if (timer_done) next_state = YELLOW; YELLOW: if (timer_done) next_state = RED; endcase end // Process 3: output logic (Moore — depends only on state) always_comb begin {red_light, green_light, yellow_light} = 3'b000; case (state) RED: red_light = 1'b1; GREEN: green_light = 1'b1; YELLOW: yellow_light = 1'b1; endcase end
Missing signals in always @(a, b) cause simulation/synthesis mismatch. Always use always @(*) or always_comb.
Every output of a combinational block must be assigned in every code path. Add a default assignment at the top of the always_comb block.
Assigning the same signal from two separate always blocks creates a multiple-driver error. A signal can only have one driver in synthesizable RTL.
Mixing rising and falling edge-triggered FFs on the same data path creates half-cycle timing paths — legal but extremely tricky to close timing on. Avoid unless required.
// Bug: 8-bit + 8-bit = 8-bit result, carry lost logic [7:0] a, b, sum; sum = a + b; // overflow if a+b > 255 // Fix: extend result width logic [8:0] sum_ext; sum_ext = {1'b0, a} + {1'b0, b}; // 9-bit result captures carry
logic (SystemVerilog) instead of reg/wire — eliminates a common bug classparameter WIDTH = 8i_ for inputs, o_ for outputsfor loops with variable bounds — use generate for parameterizable replicationcasex — use casez with explicit don't-caresalways_comb for combinational, always_ff for sequential logic<= in sequential blocks, blocking = in combinationaldefault/else in combinational blocks infers latches