Guiding Synthesis Without Fighting It
The last two articles opened up what the synthesis tool does. There is a temptation that follows from that knowledge: to hand-craft RTL into the exact gate structure you have in mind, second-guessing the optimiser on every line. This article is about why that instinct is usually wrong, and about the narrower set of cases, two of which you have already seen, where intervening is right.
The default: describe intent, trust the optimiser
A synthesis tool’s logic optimiser runs the automated cousins of the K-map minimisation you did by hand in Module 1, at a scale and speed no designer matches across millions of gates. Second-guessing it by restructuring straightforward RTL, on the theory that you know a better gate structure than it will find, is usually a losing bet, and it has a cost even when it works: hand-restructured RTL is harder to read, harder to verify and harder for the next engineer to trust or change, exactly the cost Section 8.1 spent an article on.
// Don't do this: manually "optimizing" a mux tree that the tool
// would produce identically well from the clear version below
assign y = (sel[1] & sel[0] & d3) | (sel[1] & ~sel[0] & d2) |
(~sel[1] & sel[0] & d1) | (~sel[1] & ~sel[0] & d0);
// Do this: clear intent, and the optimizer builds the same
// (or better) gates from it anyway
always @(*) begin
case (sel)
2'b00: y = d0;
2'b01: y = d1;
2'b10: y = d2;
2'b11: y = d3;
endcase
end
The first version is harder to verify by eye (you have to re-derive that it is a 4-to-1 mux at all) for no synthesis benefit, since the optimiser extracts the same function from either and very likely produces the same gates.
When intervention is warranted
You have met two legitimate cases already.
Preventing resource sharing on a resource that sits on a known critical path (the previous article), where you have measured evidence, a timing report, that the default choice is hurting you.
generate if and generate case from Section 7.1, choosing between genuinely different architectures (a small ripple adder versus a wide carry-select structure) based on a parameter. That is not second-guessing the optimiser’s gate-level choices within one architecture; it is an architectural decision the optimiser cannot make on its own, because it does not know your width requirements or performance targets ahead of time.
The pattern connecting both: you are not trying to out-optimise the tool at logic minimisation. You are supplying information the tool does not have, whether that is “this path is timing-critical” (which the tool only knows once you tell it, through constraints) or “these are different implementations, pick based on this parameter” (which is a design decision, not a minimisation question).
Constrain honestly instead of micro-managing
The highest-leverage thing you can do to guide synthesis is not restructuring RTL. It is giving the tool honest, accurate constraints: the real target clock, the real input and output timing, the real area or power budget if there is one. A tool making decisions (including the resource sharing trade) under accurate constraints consistently beats one making them under vague or loose constraints that do not reflect what the design must do. Section 8.3 builds on this directly: the tool’s whole optimisation strategy is driven by the constraints you hand it, which is why correct constraints matter at least as much as any RTL-level tuning.
A failure pattern I see in engineers newly aware of how synthesis works: hours spent restructuring RTL to “help” the tool, when the actual problem was a missing or wrong timing constraint the whole time. The tool was never trying to optimise for the real target because it was never told what the target was. Check the constraints are correct and complete before concluding that RTL restructuring is what is needed. In a large fraction of cases it is not.
The judgement, stated plainly
Write clear, intent-expressing RTL by default. Use generate if or generate case when choosing between genuinely different architectures, not different phrasings of the same one. Use a synthesis directive (such as suppressing sharing) only after a timing report has told you specifically where the default choice hurts, not pre-emptively on a guess. And before either, check that the constraints reflect the real requirements, because a large share of “the tool made a bad choice” complaints turn out to be “the tool was never told what a good choice would look like”.
Practice Exercise
An engineer notices the synthesized area is larger than expected and proposes rewriting several always @(*) blocks into hand-optimised Boolean assign statements, like the “don’t do this” example above, expecting to reduce area. What should they check first?
Solution
Before any rewrite, they should check whether the constraints for the run are right, in particular whether an over-aggressive timing target is forcing the tool into a fast but area-hungry implementation the design does not need. A tight timing constraint pushes the tool toward larger, faster cells, less area-efficient mapping, and less resource sharing, purely because it is doing what it was told: speed at any area cost. If the design does not need to run as fast as the constraint says, relaxing it to the real requirement is very likely to shrink the area on its own, with no RTL change, and is a smaller, safer and more maintainable change than rewriting working always blocks into hard-to-verify Boolean equations for a benefit that may not exist.
The default posture toward synthesis is trust: write RTL that expresses intent and let the optimiser do the job K-maps did by hand in Module 1. Intervene only when supplying information the tool lacks, a measured bottleneck or a real architectural choice, and check the constraints before concluding an RTL rewrite is needed. That closes Section 8.2. In the next article, Section 8.3 makes this concrete: how to write the timing constraints that drive every trade this section has described.