SEC Module · Secure Enclave
Implementation notes · selected design issues

Engineering observations from implementation

This page records selected issues that influenced the final design and verification flow. Each entry describes the observed behavior, the root cause, and the change made in response. The examples include a data-dependent Quad-SPI read error, a race in the behavioral flash model, protocol changes during integration, and reset-interface assumptions.

Intermittent nibble slip during quad flash reads

On the FPGA, reading helper data back from flash returned a small number of corrupted words. The count varied across bitstreams (for example 37, 41, then 32 of 88), while the digital simulation did not reproduce the failure.

Investigation

The errors were consistent with a one-nibble (4-bit) slip, not random bit errors. The missing nibble frequently had both DQ3=1 and DQ0=1, indicating a data-dependent effect. Re-reading one address produced different values, slipped words contained the next word's upper nibble, and an external reader recovered the flash contents correctly. Together, these checks localized the issue to the quad read path.

Root cause & fix

A quad read includes eight dummy cycles for bus turnaround: the master must release the data lines before the flash drives them. The dummy state was still using the transmit branch and held the DQ lines low until the data phase. The fix was to tri-state the lines during the dummy interval. Single-line reads did not show the issue because they use a different input/output path.

Lesson: digital simulation does not model all board-level bus contention effects. The full diagnosis is summarized on the Verification page.

Race condition in the flash model

To test the Quad-SPI master, we wrote a behavioral model of the W25Q64JV flash. Early on, the master would wait indefinitely in its status-poll loop — waiting for a BUSY bit that, in the model, never seemed to clear.

Root cause & fix

The first model split its logic across four event-driven blocks triggered by SCLK, chip-select, and the system clock. Several blocks wrote the same status registers. In some schedules, the read-status decoder updated one clock after the edge that should have driven the first output bit, so the master sampled an undefined value instead of BUSY. The model was rewritten as a single clocked process that decodes each command on the edge that receives it.

Lesson: verification models also need clear clocking discipline. A simpler event structure made the model easier to reason about and debug.

FIFO entry width mismatch

Every flash job is one FIFO entry: a read/write flag, a 24-bit address and a 32-bit data word, for a total of 57 bits. During integration, writes were interpreted as reads: no write command reached the flash, and several assertions failed together.

Root cause & fix

The FIFO had been declared 56 bits wide. The top bit — the read/write flag — was being truncated, so it always read back as 0 (= read). Widening the FIFO entry to 57 bits corrected the command interpretation and resolved the related failures.

Lesson: when many tests fail in the same direction, check shared structural assumptions such as packed widths before debugging each symptom independently.

FIFO depth and read-path liveness

Reading one record means fetching 62 words from flash. The read path enqueues all 62 read commands first and only then starts popping the results. With 16-entry FIFOs, the command queue filled before the transaction could complete, causing a reproducible deadlock.

Root cause & fix

The deadlock threshold is simply TX depth + RX depth < 62. We deepened both the TX and RX FIFOs from 16 to 64, enough to hold a full packet of read commands and the 58 plaintext words used by the write path.

Lesson: buffer depth can be a correctness constraint when one side of a protocol must enqueue a full transaction before the other side drains it.

Record format revision

The first design stored 4-word records with an explicit commit marker to mark a page as valid. After review, the format changed to a full 256-byte page: 2 words of associated data, 58 words of ciphertext, a 4-word tag — and no marker at all.

What it touched

  • The page payload grew from 4 to 58 words; the parameter package, FIFO depths and Main-FSM buffers all had to scale with it.
  • The commit marker, related FSM states, and the ERR_NO_RECORD error were removed. A record is accepted only if its ASCON tag authenticates.
  • Sector handling changed to "erase only on the first page of a 4 KB sector," and testbenches built around the old 4-word protocol were replaced.
Lesson: using authentication as the validity criterion removed extra state and reduced the number of cases the FSM had to handle.

Reset-style review across the design

A design review question about reset release led to a systematic audit of reset style across 98 modules covering BCH, ASCON, the fuzzy extractor, FIFOs, the PUF, and SPI.

What we found

  • The dominant style is asynchronous assertion with active-low reset, but some boundaries mix synchronous and asynchronous reset behavior.
  • The PUF uses an intentionally free-running seed LFSR with no reset, because boot-to-boot variability is part of the model.
  • Some documentation described reset behavior differently from the RTL and needed correction.

The conclusion: an external async reset is fine, but it must be deasserted through a two-flip-flop synchronizer per clock domain to avoid recovery/metastability hazards — and intentional exceptions such as the PUF seed, behavioral models, and legacy cores should be documented explicitly.

Lesson: reset behavior should be treated as an interface contract. Documented exceptions are manageable; undocumented ones are integration risk.

BCH codec reset assumptions

Error correction for the fuzzy extractor builds on an existing open BCH codec — a deep pipeline of syndrome, Berlekamp-Massey and Chien-search stages. Most of its internal blocks have no global reset: they restart only on operation-local start/first strobes. Reset the wrapper mid-decode and the machinery underneath keeps running for a few cycles.

How we handled it

Rather than rewrite the arithmetic core, we treated it as a start/done accelerator that may only be reset while idle: the FE wrapper controls the external reset, gates start until after reset release, and the contract is abort-and-restart, never pause-and-resume. Deep reset is added only where it is genuinely needed.

Lesson: reused RTL should be integrated with its assumptions made explicit at the wrapper boundary.

RTL that Vivado accepted, Design Compiler did not

The design simulates under XSim and maps cleanly to the FPGA through Vivado. The first time the SEC subsystem (tum_ss) was pushed through standard-cell ASIC synthesis with Synopsys Design Compiler, several constructs that both simulation and FPGA synthesis had tolerated were rejected at elaboration.

Two representative cases

  • An accidental half-megabit register. A BCH syndrome helper built its lookup table in a local variable before returning it. The elaborator materialized that intermediate as a single ~524 288-bit register and stopped. Rewriting the function to build the table directly in its return value, with explicit widths, removed the oversized implicit storage.
  • A cross-generate reference. One LFSR term reached into a sibling generate block's scope — a hierarchical reference that simulators resolve but the ASIC elaborator does not accept. The term was restructured to be computed locally, without reaching across generate instances.
Lesson: simulation and FPGA synthesis are more permissive than a sign-off ASIC elaborator. Portable RTL avoids huge implicit intermediates and cross-generate hierarchical references — the kind of thing only a second toolchain surfaces. The synthesis bring-up status and the silicon budget are tracked on the Verification page.

Module-name collision during SoC synthesis

Unit and subsystem simulations passed. The first time the design went through full SoC synthesis, it failed: a module of ours was simply named counter — and so was a cell in the SoC's shared component library. The names collided only once both were pulled into the same elaboration.

Fix

We renamed the local module to bch_counter and re-ran the regression. The failure was not visible until subsystem sources were elaborated together with the full SoC.

Lesson: a flat Verilog/SystemVerilog namespace is shared across the integrated design. Subsystem-specific prefixes reduce this risk.

Additional implementation notes

  • Subsystem base address. One SoC document listed a base address that differed from the RTL interconnect decode (0x01051000). Firmware and documentation were aligned with the RTL-visible address.
  • FPGA area constraints. Debug-oriented hardware, including the QSPI command trace and performance-counter unit, was made optional behind a synthesis switch.
  • Simulator scheduling in tests. Some testbench loops sampled a multi-bit guard before a non-blocking update became visible. Rewriting the checks with explicit loop control removed the false failures.
  • Enrollment ordering. Helper data, key hash, counter, and ENROLLED flag are written in an order that lets an interrupted enrollment restart cleanly on the next boot.