Programmable Logic/Verilog RTL Coding Guidelines

From Wikibooks, open books for an open world
< Programmable Logic
Jump to navigation Jump to search

Verilog RTL Coding Guidelines[edit]

Verilog RTL is the subset of the Verilog language that is used to describe real hardware. RTL code can be synthesized into ASIC gates or FPGA cells. Therefore, it must conform to very strict rules.

How Verilog RTL Relates to Real Hardware Structure[edit]

When RTL designers look at lines of Verilog RTL code, they don't see lines, they see structures such as gates, multiplexors adders and flip-flops, and how they are wired together. For instance:

always @(posedge clk) begin
  if(cond)
    res <= a;
  else
    res <= b;
end

In the code above, an RTL designer will immediately vizualize a flip-flop (res), whose input is driven by a multiplexor (because of the if statement). The select pin of the multiplexor is the signal cond, and the inputs of the mux are a and b.

It takes practice to seamlessly convert between Verilog RTL and gate structure, but it's a critical skill to be a good RTL designer.

Verilog RTL Coding Style[edit]

Assignments inside Always Blocks[edit]

If a signal is assigned within an always block, it should be assigned for every possible path of that always block. If that is not the case, when the always block executes, simulation will happily preserve the previous value of the signal. Such behavior is not possible with real gates, unles using latches to retain values, which is usually not the intended effect. Therefore, make sure to assign all signals in all branches. It is ok to assign default values early on (using non-blocking assignment) and override them later. Verilog clearly specifies that only the later assignment will win.

Below is an example of incorrect code:

always @(b or c or e or f or cond)
  if(cond)
    a <= b + c; // forgot to assign d in that case
  else
    d <= e + f; // forgot to assign a in that case
end

Blocking vs. Non-Blocking Assignments[edit]

While not strictly required, a good coding style is to use only blocking assignments for all signals that are used outside an always block, and use non-blocking assignments only for temporary signals that are fully evaluated inside one always block and not used anywhere else.

Consider the code below, where we want to assign the sum of either a+1 or b+1 depending on a condition. We'd rather not infer two adders, but rather infer one multiplexor and one adder.

reg [N-1:0] sum;

always @(posedge clk)
  if(cond)
    sum <= a + 1'b1;
  else
    sum <= b + 1'b1;
end

The use of a temporary signal (tmp), using an assignment statement outside the always block, achieves our goal:

reg [N-1:0] sum;
wire [N-1:0] tmp;

assign tmp = cond ? a : b;

always @(posedge clk)
begin
  sum <= tmp + 1'b1;
end

It is best for RTL design practices to not mix blocking and non-blocking statements inside an always block. Temporary signals can be computed outside an always block or in a separate always block using blocking assignments. There should be no path where they don't get assigned but are used. Notice that tmp needs to be declared as a reg in this alternative RTL code, since it is assigned procedurally (inside an 'always' block).

reg [N-1:0] sum;
reg [N-1:0] tmp;

always @*
   tmp = cond ? a : b;

always @(posedge clk)
begin
  sum <= tmp + 1'b1;
end

Multiple Drivers[edit]

You can't drive the same signal from multiple always blocks, it would result in multiple drivers trying to write to the same signal, which is not possible (unless you are explicitly generating a tri-state bus). Therefore, a signal can only be driven from one always block. The code below is incorrect because signal foo is driven from two different always blocks:

always @(posedge clk) begin
  if(reset)
    foo <= 1'b0;
end

always @(posedge clk) begin
  if(count)
    foo <= a;
end


Initial Statements and Reset[edit]

Initial statements are ignored during synthesis, they do not translate into any logic structures. Therefore, RTL code should not contain initial statements.

Typically, all chips have some kind of reset signal or sequence. Use the reset sequence of your chip to reset the values you want, instead of reseting values inside an initial block.

always @(posedge clk) begin
  if(reset) begin
    // Reset all state
    sig1 <= 1'b0;
    sig2 <= 1'b0;
  end else begin
    // Design behavior
  end
end

Incomplete Sensitivity Lists[edit]

The sensitivity list of a combinational always block must include all the inputs used in that block. If you forget some signals from the sensitivity list, synthesis will silently assume they are included and generate gates. However, the Verilog simulator will respect your incomplete sensitivity list and the simulation behavior will be incorrect. This means that what you think works, based on simulation passing, is actually incorrect (once you synthesize your design).

Here is an example of incorrect code: can you spot which signal is missing from the sensitivity list?

always @(b or c or e or f)
  if(cond)
    a = b + c;
  else
    a = e + f;
end

The answer: cond.

The best RTL method is to use the new Verilog 2001 option of @* as this will be much quicker to write and it will automatically include all inputs.

always @*
  if(cond)
    a = b + c;
  else
    a = e + f;
end

Loops[edit]

To generate actual gates during synthesis, all loops will be unrolled. That also implies that the iterations can be determined once and for all, they can't depend on dynamic values. Loop ranges can be constants, parameters or `define, but not expressions based on signals.

Coding a very large loop that infers too much logic is a typical mistake. It's easy with just a few lines of Verilog to generate thousands of gates or more, unexpectedly.

Signal Naming Convention[edit]

Naming conventions help make sense of the design. They are not stricly required for Verilog to be part of the RTL subset, but they are strongly encouraged.

For pipelined design, it is customary to postfix signal names with the pipeline stage they correspond to. For instance, in a typical 4-stage CPU pipeline (F,D,E,W), valid_W is the valid signal for stage W. It is one cycle delayed from valid_E. This helps when looking at waveforms: either make sure you are lining up all signals that belong to the same pipeline stage, or mentally shift cycles based on the pipeline stage indicated in the name. It also helps make sure that expressions that compute a W stage signal all come from the previous pipeline stage (E).

Constants Widths[edit]

When you use constants, Verilog expands the width of a constant to 32 bits. In the following example when the compiler generates logic it will first expand the 1 constant to a 32 bit value of 1. Then it will truncate the value to 6 bits to add to bar. This will typically generate a truncation warning.

wire [5:0] foo;
wire [5:0] bar;

assign foo = bar + 1;

An alternative is to make the size of the constant equal to the size of bar. This will remove the truncation warning, but you will still get a size warning due to the fact the addition result is 7 bits, not 6 bits (as declared for foo) and thus the result stored in foo is truncated. Depending on the actual design this may be perfectly fine, but the compiler is warning the designer to be sure your design will work if the result is larger than the location it's being assigned to.

wire [5:0] foo;
wire [5:0] bar;

assign foo = bar + 6'd1;