· 5 min read

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.

#fpga #crypto #verilog #timing #deep-dive

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:

  1. XOR — four words plus round key
  2. S-box substitution — four parallel byte-wise lookups
  3. L linear transform — bit rotations and XORs

The architecture choice is straightforward for high-throughput crypto: fully unroll all 32 rounds into a pipeline.

ArchitectureLatencyThroughputArea (ALMs)
Iterative (1 round/cycle)32 cyclesFmax/32~2,000
32-stage fully unrolled1 cycleFmax × 128-bit~17,800
Partially unrolled (4×)8 cyclesFmax × 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:

Critical Path — Single SM4 Round
  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.

Active Bypass Architecture
                    ┌─────────────────┐
  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.

PhaseTargetFmax (Slow 85°C)Setup SlackTNSALMRegistersThroughput
0: No constraints~80 MHz-6.564 ❌-30,97515,5029,951~10.2 Gbps
0.5: 50MHz50 MHz+4.683 ✅017,85111,4606.4 Gbps
1: 65MHz65 MHz~65 MHz+1.485 ✅017,85211,6528.3 Gbps
2: 100MHz + Opt100 MHz94.19 MHz-0.617 ⚠️-5.12217,86812,40412.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:

TestResultDetail
S-box 256 entriesByte-for-byte match
Standard plaintext encryptMatches NIST test vector
256 random encrypt/decrypt round-tripsEvery block recovers
Avalanche effect50.1% (ideal: 50.0%)
Key sensitivity50.0% (exact hit on ideal)
Continuous throughput1M 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

ResourceUsedAvailable%
ALM17,868~50,400~35%
Registers12,404~100,800~12%
BRAM06080%
DSP01120%

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:

  1. 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.
  2. 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.
  3. 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:

Project: This post is part of the SM4 Crypto Accelerator project.

🔙 Back to Projects