SM4 32-Stage Pipeline: From 50MHz to 94MHz Timing Closure
How Active Bypass, EDA retiming, and progressive SDC constraints took a 32-stage SM4 crypto pipeline from -6.5ns setup violation to 94.19MHz Fmax on Cyclone V.
TL;DR
A 32-stage fully-unrolled SM4 encryption pipeline started with a -6.564ns setup violation at ~80MHz. Through three interventions — Active Bypass to eliminate dead logic in the defence module, progressive SDC constraints from 50→65→100MHz, and EDA retiming that automatically inserted 752 registers — the design reached 94.19MHz Fmax with 12.1 Gbps throughput on a Cyclone V 5CGXFC9E7F35C8. All 12 verification tests pass with a perfect avalanche ratio of 50.1%.
Why 32-Stage Fully-Unrolled Pipeline
SM4 is a 128-bit block cipher defined in GB/T 32907-2016. It operates on four 32-bit words, iterating 32 rounds of identical transformations. Each round applies:
- XOR — four words plus round key
- S-box substitution — four parallel byte-wise lookups
- L linear transform — bit rotations and XORs
The architecture choice is straightforward for high-throughput crypto: fully unroll all 32 rounds into a pipeline.
| Architecture | Latency | Throughput | Area (ALMs) |
|---|---|---|---|
| Iterative (1 round/cycle) | 32 cycles | Fmax/32 | ~2,000 |
| 32-stage fully unrolled | 1 cycle | Fmax × 128-bit | ~17,800 |
| Partially unrolled (4×) | 8 cycles | Fmax × 128-bit / 8 | ~5,000 |
The fully-unrolled version trades area for maximum throughput. One clock cycle, one 128-bit block processed. At 94MHz: 12.1 Gbps.
Critical Path Analysis
The critical path runs through a single round of the pipeline:
tmp = word_1 ⊕ word_2 ⊕ word_3 ⊕ rk_i ← 2级 XOR (~0.3ns)
│
▼
sbox_replace × 4 (parallel byte-wise) ← LUT (~1.5ns)
│
▼
L transform: B ⊕ B<<<2 ⊕ B<<<10 ⊕ B<<<18 ⊕ B<<<24 ← (~0.5ns)
│
▼
result = data_after_transform ⊕ word_0 ← (~0.3ns)
═══════════════════════════════════════════════════════
Total (ideal): ~2.6ns → ~384 MHz theoretical Fmax The ideal case gives ~2.6ns — enough headroom for 100MHz. But the actual implementation had a problem.
The Round 0/31 Problem
Round 0 and Round 31 aren’t ordinary rounds. They instantiate the defence module — a side-channel countermeasure that implements dual S-box architectures:
- Lookup-table S-box — standard ROM-based
- Composite field GF((2⁴)²) S-box — algebraic, used for masking
The critical bug: even when safe_en == 2'b00 (defence disabled), the composite field S-box path still exists in the netlist. Quartus can’t optimize it away because it’s structurally connected.
Round 0: tmp → defence模块(查表S-box + 复合域S-box) → output
Round 31: tmp → defence模块(查表S-box + 复合域S-box) → output
The composite field S-box adds ~3.5ns of combinational delay. The critical path balloons from ~2.6ns to ~6ns, causing a -6.564ns setup violation.
Active Bypass Mechanism
The fix: add an explicit bypass MUX around the defence module, controlled by safe_en.
┌─────────────────┐
tmp_2 ──────────▶│ sbox_replace ×4 │──▶ fast_sbox_out
└─────────────────┘
│
safe_en ──────┐ │
▼ ▼
┌────────────────────┐
│ MUX (safe_en) │──▶ defence_data_out_mux
└────────────────────┘
▲
│
tmp_2 ──────▶ sm4_encdec_defence ──▶ defence_data_out
(查表 + 复合域) When safe_en == 2'b00, the MUX selects the fast path — standard S-box lookup only. Quartus can now completely optimize away the dead composite field logic. The key insight: making the selection explicit lets the synthesizer prune unreachable paths.
// Minimal bypass logic (~3 lines)
wire [127:0] fast_sbox_out;
sbox_replace u_fast (.data_in(tmp_2), .data_out(fast_sbox_out));
wire [127:0] defence_data_out;
sm4_encdec_defence u_defence (.data_in(tmp_2), .data_out(defence_data_out));
assign defence_data_out_mux = (safe_en == 2'b00)
? fast_sbox_out
: defence_data_out;
Result: critical path drops from ~6ns to ~2.6ns.
Four-Phase Timing Evolution
The timing closure wasn’t a single leap — it was a systematic, four-phase process.
| Phase | Target | Fmax (Slow 85°C) | Setup Slack | TNS | ALM | Registers | Throughput |
|---|---|---|---|---|---|---|---|
| 0: No constraints | — | ~80 MHz | -6.564 ❌ | -30,975 | 15,502 | 9,951 | ~10.2 Gbps |
| 0.5: 50MHz | 50 MHz | — | +4.683 ✅ | 0 | 17,851 | 11,460 | 6.4 Gbps |
| 1: 65MHz | 65 MHz | ~65 MHz | +1.485 ✅ | 0 | 17,852 | 11,652 | 8.3 Gbps |
| 2: 100MHz + Opt | 100 MHz | 94.19 MHz | -0.617 ⚠️ | -5.122 | 17,868 | 12,404 | 12.1 Gbps |
Phase 0: The Raw Mess
No SDC constraints, no optimization directives. Quartus picks its own defaults. The result: -6.564ns setup violation across the entire pipeline, TNS of -30,975ns (meaning timing is broken in hundreds of paths).
Phase 0.5: First Success at 50MHz
Set create_clock -period 20.0 (50MHz). Quartus now knows the target and applies register retiming aggressively. Slack goes from -6.564ns to +4.683ns — healthy margin. TNS drops to zero. The design works.
Phase 1: Push to 65MHz
Tighten to create_clock -period 15.38 (65MHz). Slack shrinks to +1.485ns. Still passing. This confirms the design has headroom.
Phase 2: The 100MHz Push + EDA Optimization
Set create_clock -period 10.0 (100MHz) and enable aggressive synthesis:
# Quartus EDA optimization directives
set_global_assignment -name FITTER_EFFORT "Standard Fit"
set_global_assignment -name ALM_REGISTER_PACKING_EFFORT "HIGH"
set_global_assignment -name ADVANCED_PHYSICAL_OPTIMIZATION ON
Quartus retiming kicks in — it automatically inserts 752 additional registers along long combinational paths, breaking them into shorter segments. The trade-off: ~700 more registers for a 44.3% Fmax improvement (from ~65MHz to 94.19MHz).
The design lands at 94.19MHz with -0.617ns slack — a minor violation on one path. TNS of -5.122ns is negligible for a 32-stage pipeline.
Verification Results
All 12 verification tests PASS against a Python reference model:
| Test | Result | Detail |
|---|---|---|
| S-box 256 entries | ✅ | Byte-for-byte match |
| Standard plaintext encrypt | ✅ | Matches NIST test vector |
| 256 random encrypt/decrypt round-trips | ✅ | Every block recovers |
| Avalanche effect | ✅ | 50.1% (ideal: 50.0%) |
| Key sensitivity | ✅ | 50.0% (exact hit on ideal) |
| Continuous throughput | ✅ | 1M blocks, 32,651 blocks/sec |
The avalanche ratio of 50.1% confirms the implementation behaves like a proper cipher — flipping one input bit flips each output bit with ~50% probability. Key sensitivity at exactly 50.0% means every key bit matters equally.
Continuous throughput of 32,651 blocks/sec × 128 bits = 4.18 Gbps on the software verification path. The hardware target is 12.1 Gbps at 94MHz — nearly 3× faster.
Resource Utilization
| Resource | Used | Available | % |
|---|---|---|---|
| ALM | 17,868 | ~50,400 | ~35% |
| Registers | 12,404 | ~100,800 | ~12% |
| BRAM | 0 | 608 | 0% |
| DSP | 0 | 112 | 0% |
Zero BRAM — all S-boxes are implemented as pure LUT logic. This is deliberate: BRAM has fixed read latency (1 cycle minimum), which would add pipeline stages. LUT S-boxes have zero-cycle combinational delay, keeping the pipeline at exactly 32 stages.
The ALM utilization at ~35% leaves significant headroom for integration — adding a DMA engine, AXI bus interface, or additional crypto modes (ECB/CBC/CTR) won’t crowd the device.
What’s Next
Three natural follow-ups:
- Push to 95MHz — Tighten SDC to
create_clock -period 10.526. With the current -0.617ns slack, this should close. If not, targeted retiming on the single violating path. - S-box BRAM化 — Move S-boxes to block RAM. This adds one pipeline stage (33 total) but enables 150+ MHz by eliminating LUT routing congestion. The throughput equation changes: Fmax × 128-bit / 1.
- Folded architecture — Reduce to 4-stage unrolled (8 cycles/block). Cuts area by ~60% while maintaining ~8 Gbps throughput. Useful for area-constrained deployments.
Related Articles:
- SM4 Side-Channel Defence: LFSR Masking + Composite Field S-box — How the dual S-box architecture resists DPA/EMA attacks.
Project: This post is part of the SM4 Crypto Accelerator project.