Module 2: Logic Synthesis ยท 2.1 From RTL to Gates

RTL Style and Its Physical Consequences

The previous article showed that one function has many mappings. This one shows that the RTL you write steers which mapping you get.

If two Verilog snippets give identical testbench results for every input, are they the same from a physical design standpoint? Many engineers would say yes: same truth table in, same truth table out. Module 8 of the Verilog course already made the case that RTL style matters for readability and maintainability. This article makes the sharper, physical version of the claim: RTL style also determines the shape of the netlist that has to be placed and routed, and shape has consequences long before you open a floorplanner.

Same function, different netlist

Take a 4-to-1 multiplexer, written two ways.

Version A, a cascaded priority chain:

always @(*) begin
    if (sel == 2'b00)      y = in0;
    else if (sel == 2'b01) y = in1;
    else if (sel == 2'b10) y = in2;
    else                   y = in3;
end

Version B, a case statement:

always @(*) begin
    case (sel)
        2'b00: y = in0;
        2'b01: y = in1;
        2'b10: y = in2;
        default: y = in3;
    endcase
end

For a genuine 4-to-1 mux with mutually exclusive selects, most synthesis tools recognise both patterns and map them to the same balanced structure. But that equivalence is fragile, and it breaks down quickly in common real patterns: priority logic where later conditions are only reached if earlier ones are false, resource-sharing decisions across arithmetic, and incompletely specified case statements that infer a latch. The if/else if chain naturally synthesizes toward a cascade of two-input decisions; a well-formed case toward a balanced tree. As the number of branches grows, the cascade gets deeper (more levels in series) while the tree stays shallower (more logic in parallel).

A balanced two-level mux tree versus a three-level priority chain for the same 4:1 mux
Figure 1: Same function, different depth

Depth versus width is not cosmetic. A deeper cascade has more levels on its critical path, worse for timing, and a serial dependency structure that later stages have less freedom to place flexibly. A wider, shallower tree gives placement more independent pieces to spread and pack, at the cost of the silicon the parallel gates occupy.

Resource sharing: a bigger lever

An even sharper example is arithmetic resource sharing:

// Version A: two full adders, both always computed
assign sum_a = x + y;
assign sum_b = x + z;
assign result = sel ? sum_a : sum_b;
// Version B: one adder, muxed inputs
assign result = x + (sel ? y : z);

Version A describes two independent adders whose outputs are chosen between. Version B describes one adder fed by a mux that picks y or z before adding. Both compute the same result for every sel. But Version A gives the tool no logical reason to merge the adders. It has to assume you want two, unless an area-aggressive optimisation happens to discover the redundancy, which it may or may not. Version B removes the ambiguity by construction: there is one adder in the description.

This pattern, an unintentionally duplicated arithmetic unit hiding behind two similar assign statements, is one of the most common sources of “why is my area so much bigger than expected” in real synthesis reports. It never shows up as an error. It shows up as a cell count that is harder to explain than it should be.

An illustrative before and after

A representative comparison for the sharing example, through Yosys with a fixed strategy (illustrative numbers showing the shape of the effect, not a guaranteed output):

Version Adders in netlist Approx. cell count Approx. logic levels
A, two adders, muxed output 2 34 5
B, one adder, muxed input 1 19 5

Same levels on the critical path, nearly half the cell count. Version A spends silicon on an adder whose result is discarded half the time.

Modern synthesis optimisation is powerful, but it works on the structure you gave it. It restructures locally within what the code describes; it does not redesign the algorithm. Writing two adders when you meant “pick one of two inputs, then add” asks the tool to prove an equivalence it may not go looking for.

Practice Exercise

You are reviewing an 8-bit comparator block used in three places:

assign gt_a = (x > y);
assign gt_b = (x > z);
assign gt_c = (y > z);

The three comparisons share operands in different pairs. Is there a rewrite that reduces hardware the way the adder example did, or is this case different? Why?

Solution

This case is different, and that is the point to notice. In the adder example only one of the two sums was used at a time, selected by sel, which is what made sharing one adder possible. Here all three comparisons are independent outputs needed at the same time; there is no select choosing between them. Synthesis needs three comparators (or equivalent logic), because all three results are live outputs, not alternatives. Sharing is only available when results are mutually exclusive in time or selection, not when they merely look similar in source. Rewriting this to resemble the adder pattern would not reduce area; it would only obscure that three real comparisons are required.

Both examples skipped past a detail worth naming: what Yosys’s intermediate netlist looks like before and after it commits to real cells. In the next article we look at that two-stage process, generic cells first and standard cells second, and why Yosys keeps them separate.

โ† Technology Mapping Generic Cells Versus Standard Cells โ†’