Baseline & Toolchain
Two IP blocks were taken as baselines for optimization. The focus was silicon area, Fmax, and throughput — navigating their trade-offs under a consistent toolchain:
- Synthesis: Yosys 0.52 + ABC (technology mapping, retiming, logic optimization)
- Library: Nangate45 — a publicly available 45nm standard-cell library with full timing/power models, widely used for academic and pre-competitive research synthesis
- Timing:
stime(per-module hierarchical) and flat critical-path extraction viasynth -flatten - Area: Yosys
statpostdfflibmap— the post-DFF-mapping figure, not ABC’s pre-mapping WireLoad estimate
The starting points:
| IP Block | Fmax | Area | Throughput / Latency |
|---|---|---|---|
| AES-128 (loop) | 516 MHz | 13,405 μm² | 6.0 Gbps / 21.3 ns |
| JPEG encoder | 1,030 MHz | 84,261 μm² | 1 block/64 cyc (steady-state) / 85.4 ns (88-cycle fill latency) |
AES-128
Codebase Structure
The baseline AES-128 is a loop-based iterative design:
aes_cipher_top.v— top level;dcntcounter walks 11 → 0 over 11 active cycles, then assertsdoneaes_sbox.v— 256-entrycasestatement, replicated ×20 across data path and key expansionaes_key_expand_128.v— real-time key expansion; 4 S-box instances sit on the critical path
The baseline critical path is state regs → S-box → MixColumns XOR chain → AddRoundKey → next state regs at 1,937 ps, measured via flat synthesis. Hierarchical stime on aes_sbox alone reports 404 ps — a frequently misread number that represents only the ROM delay, not the register-to-register system path.
S-box area breakdown: 20 instances × ~438 μm² = ~8,760 μm² ≈ 65% of total chip area.
Attempt 1: Flat Synthesis + Aggressive ABC (No RTL Change)
The first pass applied synthesis flags to the existing RTL:
synth -flatten
opt -full
abc -liberty lib/Nangate45.lib -D 5000 \
-script "+strash;dc2;dretime;strash;&get -n;&dch -f;&nf -D 5000;&put;stime -p" \
-showtmp
Result: Area increased +1.3% (13,405 → 13,580 μm²). Fmax unchanged.
This failed for a structural reason. The 20 S-box instances are pure ROM case statements — 256 hardwired input-to-output mappings. After flattening, ABC sees 20 identical copies of that lookup. There are no shared combinational sub-expressions across instances to merge; each output is independently driven from the 8-bit input. Flattening only forces ABC to use slightly larger drive-strength cells on a longer combined path. The tool cannot manufacture sharing that does not exist in the logic structure.
Attempt 2: 11-Stage Fully-Pipelined Architecture
The correct lever was architectural: convert the 11-cycle loop into a registered 11-stage pipeline.
Stage assignment:
| Stage | Operation |
|---|---|
| 0 | AddRoundKey (XOR with round_key[0]) |
| 1 – 9 | aes_round: SubBytes + ShiftRows + MixColumns + AddRoundKey |
| 10 | aes_final_round: SubBytes + ShiftRows + AddRoundKey (no MixColumns) |
Key expansion is pipelined alongside the data path. Each stage is fully registered. Steady-state throughput: 1 ciphertext block per clock cycle after an 11-cycle fill latency.
Timing methodology — the dretime artifact:
Full-pipeline flat synthesis with dretime reports 4,001 ps for the pipelined design. This is not a valid timing number. ABC’s dretime identifies all 11 identical aes_round stages and folds them into a single combinational cloud to reduce register count — the 11-stage pipeline collapses into one unregistered path and dretime reports its end-to-end delay.
The correct approach: synthesize a single aes_round module in isolation.
# synth/aes_round_stage.ys
read_liberty -lib lib/Nangate45.lib
read_verilog src/aes_pipelined/aes_round.v src/aes_pipelined/aes_sbox.v
synth -top aes_round
dfflibmap -liberty lib/Nangate45.lib
abc -liberty lib/Nangate45.lib -D 5000 \
-script "+strash;dc2;dretime;strash;&get -n;&dch -f;&nf -D 5000;&put;stime -p" \
-showtmp
stat
Note: stime is an ABC-internal command invoked inside the -script string (as stime -p). It is not a top-level Yosys command. The exact scripts used are in synth/ in the repository.
This gives the true per-stage critical path: 658.6 ps (S-box → ShiftRows → MixColumns → AddRoundKey, with no cross-stage merging). Fmax = 1,518 MHz.
Area note: ABC’s WireLoad line in the synthesis log reports Area = 83,630 μm² — this is the pre-dfflibmap combinational area. Yosys stat after dfflibmap reports 97,219 μm². The 13,589 μm² gap is entirely DFF cells added during register mapping. Always use the stat figure for accurate post-technology-mapping area.
Results:
| Metric | Baseline | Pipelined | Delta |
|---|---|---|---|
| Critical path | 1,937 ps | 659 ps | −66% |
| Fmax | 516 MHz | 1,518 MHz | +194% |
| Latency | 21.3 ns | 7.25 ns | −66% |
| Throughput | 6.0 Gbps | 194 Gbps | +32× |
| Area | 13,405 μm² | 97,219 μm² | +7.3× |
| Compute density | 0.45 Mbps/μm² | 2.00 Mbps/μm² | +4.4× |
The 32× throughput gain compounds two effects: 11× pipeline parallelism and 2.94× higher Fmax from removing the loop MUX/counter overhead from the critical path.
Designed but Not Implemented: GF(2⁸) Composite-Field S-Box
The S-box accounts for 65% of AES area. A known area-reduction technique replaces the 256-entry ROM with a GF(2⁸) inversion circuit implemented over a tower field GF((2⁴)²) — the Canright 2005 construction.
The decomposition:
- Isomorphism φ: GF(2⁸) → GF((2⁴)²) — 8 XOR gates
- GF((2⁴)²) inversion: Δ = a_h² · ν ⊕ (a_h ⊕ a_l) · a_l, then Δ⁻¹, then d_h/d_l — GF(2⁴) squaring is pure wiring; GF(2⁴) multiply is 7 AND + 9 XOR
- Inverse isomorphism φ⁻¹ — 8 XOR
- Affine transform — 8 XOR + constant
Expected: ~150 cells/instance vs ~405 baseline → ~−30–40% total AES area with equal or better Fmax.
This was not committed to RTL. The isomorphism basis constants must be derived and verified exhaustively — getting them wrong produces an S-box that maps most inputs correctly but silently fails on specific values, which is undetectable without an exhaustive 256-vector known-answer testbench. Implementing and verifying the basis derivation correctly was not pursued further. The architectural decision to withhold was deliberate: synthesizing unverified cryptographic primitives produces numbers that cannot be trusted.
JPEG Encoder
Codebase Structure
| Module | Role |
|---|---|
fdct.v / dct.v / dctub.v / dctu.v | 8×8 DCT: 64 parallel MAC units in an 8×8 dctu grid |
dct_mac.v | 8×11-bit pipelined MAC (Multiply-Accumulate) — 64 instances |
dct_cos_table.v | 4,362-line cosine ROM — included into every dctu instance |
jpeg_qnr.v | QNR (Quantize & Round) via non-restoring iterative divider (div_uu, 12 stages) |
div_uu.v / div_su.v | 12-stage pipeline divider, dep[15] latency counter |
jpeg_rle.v / jpeg_rle1.v / jpeg_rzs.v | Run-length encoder |
System pipeline: DCT (68 cycles) → QNR (15 cycles) → RLE (5 cycles) = 88-cycle fill latency. Throughput: 1 new 8×8 block every 64 cycles (MAC accumulation rate).
Per-module hierarchical timing on the baseline:
| Module | Delay (ps) | Fmax (MHz) |
|---|---|---|
dct_mac | 970.76 | 1,030 ← system bottleneck |
div_su | 839.29 | 1,190 |
div_uu | 739.22 | 1,353 |
jpeg_rle1 | 738.24 | 1,354 |
dct_mac owns the critical path. The divider is not the timing problem — it becomes relevant only for area and latency.
Attempt 1: Flat Synthesis (No RTL Change)
synth -flatten
opt -full
abc -liberty lib/Nangate45.lib -D 5000 \
-script "+strash;dc2;strash;&get -n;&dch -f;&nf -D 5000;&put;stime -p" \
-showtmp
Result: −15% area (84,261 → 71,627 μm²), −16% cells. Fmax unchanged at 1,030 MHz.
This worked because the DCT core has 64 structurally identical dctu instances sharing the same cosine-table ROM. In hierarchical synthesis, each instance is optimized in isolation — ABC cannot see across module boundaries, so no inter-instance sharing occurs. Flattening exposes all 64 instances as a single netlist; ABC merges redundant logic cones across instance boundaries and eliminates duplicate gates.
The flat timing artifact: Running stime on the flattened JPEG netlist reports 62,325 ps (~16 MHz “Fmax”). This is meaningless. The non-restoring divider div_uu has 12 sequential feedback stages. When flattened, those stages collapse into a single combinational chain and ABC reports the end-to-end unrolled delay as the critical path. Hierarchical synthesis, which preserves the div_uu module boundary, reports the correct 970.76 ps from dct_mac. This is a systematic pitfall with iterative sequential blocks under flat synthesis.
The coef_width no-op: dctub.v has parameter coef_width = 16 as its default, but dct.v always instantiates it with coef_width = 11 passed explicitly. Changing the default parameter to 11 and re-synthesizing produced an identical netlist (71,627 μm²). The parameter default is never active during synthesis because the parent always overrides it at instantiation. Tracing parameter flow through the hierarchy before changing anything would have made this obvious immediately.
Attempt 2: Reciprocal-Multiply Quantization v1
The QNR stage computes coef / Q via a 15-cycle non-restoring divider. The divider accounts for ~2,586 cells of the JPEG total. Division is area-expensive because it requires iterative hardware; multiplication is cheaper and can be pipelined more aggressively.
Transform: Replace coef / Q with abs(coef) × round(2^15 / Q) >> 15. The reciprocal round(2^15 / Q) is precomputed externally and delivered as qnt_recip[15:0] — an interface change from the original qnt_val[7:0].
3-stage pipeline replacing the 15-cycle divider:
| Stage | Operation |
|---|---|
| 1 | Sign extraction + abs value + latch recip |
| 2 | abs_s1 * recip_s1 — 11×16-bit → 28-bit product |
| 3 | Shift by 15 + round-to-nearest + apply sign |
Result: Area improved to 68,436 μm² (−19% vs baseline). But the 11×16-bit multiply in Stage 2 synthesized to 1,207 ps — worse than the original dct_mac bottleneck at 970 ps. Fmax dropped from 1,030 → 828 MHz (−20%).
The key failure mode: the per-stage path of the original div_uu was only 739 ps per stage. Replacing it with a monolithic 11×16-bit multiply in a single stage created a new bottleneck worse than the block being replaced. The lesson: always synthesize the replacement module in isolation before committing to the full design. A standalone synthesis of jpeg_qnr with the v1 multiply would have flagged the 1,207 ps path immediately, before integrating it.
Attempt 3: Split-Multiply Quantization v2
Fix: Split Stage 2 into two sub-stages — two parallel narrow multiplies followed by a combining adder.
// Stage 2a: two parallel 11×8-bit partial products (registered)
always_ff @(posedge clk) begin
partial_hi <= abs_s1 * recip_s1[15:8]; // 19-bit max
partial_lo <= abs_s1 * recip_s1[7:0]; // 19-bit max
end
// Stage 2b: combine with correct bit alignment (registered)
always_ff @(posedge clk) begin
product <= {partial_hi, 8'b0} + {8'b0, partial_lo}; // 27-bit
end
Bit-width verification:
- 11×8-bit: max = 2047 × 255 = 521,985 < 2¹⁹ ✓
- Combined:
{partial_hi, 8'b0}is 27-bit;{8'b0, partial_lo}is 27-bit; sum ≤ 2²⁷ ✓ product[25:15]= 11 integer bits → fitsdout[10:0]✓- v2 partial-product combination is algebraically identical to v1:
(abs × recip[15:8]) × 256 + abs × recip[7:0] = abs × recipexactly ✓
The 11×8-bit multiplies synthesize to ~600 ps each (parallel, not serial). The 27-bit adder stage adds ~150 ps. The resulting QNR critical path is 1,045 ps — down from 1,207 ps in v1, and only 74 ps above dct_mac.
Results vs baseline:
| Metric | Baseline | Flat Synth | Recip v1 | Recip v2 | v2 vs Baseline |
|---|---|---|---|---|---|
| Critical path | 971 ps | 971 ps | 1,207 ps | 1,045 ps | +7.6% |
| Fmax | 1,030 MHz | 1,030 MHz | 828 MHz | 957 MHz | −7% |
| QNR latency | 15 cycles | 15 cycles | 3 cycles | 4 cycles | −73% |
| Pipeline fill | 88 cycles | 88 cycles | 76 cycles | 77 cycles | −13% |
| Latency | 85.4 ns | 85.4 ns | 91.8 ns | 80.4 ns | −6% |
| Area | 84,261 μm² | 71,627 μm² | 68,436 μm² | 69,489 μm² | −17.5% |
| Cells | 58,677 | 49,280 | 48,038 | 48,716 | −17% |
The area reduction in v2 comes from two sources: flat synthesis cross-instance sharing (−15%) plus removal of div_uu (~2,301 cells, 4,484 μm²) and div_su (~285 cells, 638 μm²).
Remaining gap: jpeg_qnr at 1,045 ps sits 74 ps above dct_mac (971 ps). A third split — three parallel 11×6-bit multiplies (~400 ps each) followed by a two-stage reduction tree — would push jpeg_qnr below dct_mac and restore full 1,030 MHz Fmax while preserving the −17.5% area reduction. Identified after v2 synthesis; not implemented.
Synthesis Pitfalls
Flat synthesis does not help ROMs
Flat synthesis is effective when structurally similar instances share sub-expressions that hierarchical synthesis cannot merge across module boundaries (JPEG DCT, 64 identical MAC units). It does nothing for pure ROM-based case statements (AES S-box). There is no shared combinational logic to merge; ABC only sees 20 independent lookup tables. Recognizing this distinction before running synthesis saves time.
The dretime artifact on identical pipeline stages
dretime minimizes register count across a pipeline by moving registers across combinational boundaries. When all stages are structurally identical — as in the AES round pipeline — it folds all 11 stages into a single combinational cloud and reports that cloud’s delay as the “critical path.” The resulting number (4,001 ps for an 11-stage AES pipeline clocked at 1,518 MHz) is impossible to clock. Always synthesize a single representative stage in isolation to recover the true per-stage timing.
Iterative dividers under flat synthesis
div_uu is a 12-iteration non-restoring divider — each iteration feeds back into the next, making it inherently sequential. Flattening exposes all 12 iterations as combinational logic; ABC reports the end-to-end unrolled delay (62,325 ps for the JPEG baseline). This is a false reading — it represents the total delay of 12 cascaded iterations, not a real clock constraint. Hierarchical synthesis with stime per module gives the correct answer.
Pre-DFF vs post-DFF area
ABC’s WireLoad report and Yosys stat report different areas for the same design. The WireLoad line appears before dfflibmap; Yosys stat runs after. The gap is the area of DFF standard cells added during register mapping. For the pipelined AES, this gap was 97,219 − 83,630 = 13,589 μm². The Yosys stat figure is the correct post-technology-mapping area to report.
Parameter defaults vs synthesis-time values
A parameter default in a submodule is irrelevant if every instantiation in the hierarchy overrides it explicitly. Changing coef_width = 16 to coef_width = 11 in dctub.v produced an identical netlist because dct.v always passes coef_width = 11 at the point of instantiation. Tracing the full parameter instantiation tree from the synthesis top before modifying any parameter avoids this category of wasted run.
Verification
AES pipelined:
- Each pipeline stage traced against the baseline loop iteration — same SubBytes, ShiftRows, MixColumns, AddRoundKey computation confirmed
- Interface compatibility:
kld/ld/donesignal behavior preserved; pipelined version accepts one block per cycle after the 11-cycle fill - Both implementations elaborate and map without errors in Yosys / Nangate45
JPEG reciprocal quantization:
- Bit-width proofs verified algebraically (11×8 max sum < 2¹⁹; combined sum < 2²⁷;
product[25:15]fits 11-bitdout) - v2 partial-product equivalence to v1 verified:
(abs × recip[15:8]) × 256 + abs × recip[7:0] = abs × recip - Rounding behavior consistent with ITU-T T.81 truncation-based JPEG quantization
- Interface:
dout[10:0]width anddoutentiming relative todstrbpreserved
Not completed: Full dynamic functional simulation (known-answer vectors via Icarus Verilog) was deferred in favor of architectural iteration and timing closure analysis — the higher-value work given the optimization goals.
Open-Source vs. Commercial Synthesis
This flow uses Yosys + ABC, which exposes every synthesis decision explicitly — there is no black-box compile_ultra pass absorbing the complexity. That makes it a better learning environment than a commercial tool for understanding what is actually happening at the gate level, and it is why the pitfalls in this writeup are visible at all.
In an enterprise flow using Synopsys Design Compiler or Cadence Genus, several of the manual steps here would be automated:
- Multiplier balancing:
compile_ultrawith datapath extraction enabled would likely identify the 11×16 multiply critical path in v1 and restructure it automatically into a tree of narrower partial products, potentially reaching the same result as the manual v2 split without a second iteration. - Retiming: DC’s register retiming (
set_optimize_registers) operates at the netlist level and respects clock constraints, avoiding the stage-merging artifact thatdretimeproduces on identical pipeline stages in ABC. - Flat vs. hierarchical: Commercial tools expose this as a
compilestrategy option withungroupcontrol, and the timing is always computed correctly regardless of hierarchy — the flat JPEG timing artifact does not occur in DC because it maintains awareness of sequential elements across hierarchy boundaries.
The value of doing this manually in Yosys is precisely that none of those automations are available. Every structural insight — why flat synthesis helps JPEG but not AES, why a single wide multiply is worse than two narrow ones, how to get true per-stage Fmax from a pipelined design — had to be derived from first principles and validated through the synthesis logs directly.
Tools
| Tool | Version | Role |
|---|---|---|
| Yosys | 0.52 | Synthesis, ABC optimization, technology mapping |
| ABC | (bundled) | Logic optimization (dc2, dch, nf), retiming (dretime) |
| Nangate45 | 45nm | Standard-cell library (timing + power models) |
| Python | 3.x | Metric extraction from synthesis logs (compute_metrics.py) |