The RTL that goes to silicon does not run unchanged on a prototype: it contains an analog PLL, a power-on-reset cell, latch-based clock gates, power switches, isolation cells and memory macros that the platform does not have. Every replacement changes what the prototype can prove. Its one job is the list: what is replaced, how, and which NanoSoC bug or budget becomes invisible as a result.
Keep the logic, replace the technology. The prototype build swaps each technology-specific element for a platform equivalent through a wrapper, so the integration RTL above the wrappers stays the same. The rule for the review is that every swap is written down with the behaviour it preserves and the behaviour it loses; the second column is the one that matters.
NanoSoC element | On the prototype | Preserved | Lost |
|---|---|---|---|
PLL (24 MHz in, 768 MHz out) | The platform's clock generator, with all domain clocks scaled by one factor | The 8 : 4 : 1 ratios; the wrapper's lock logic | Real lock time (256 clk_per) and analog behaviour |
Power-on-reset cell | A board or platform reset input | RST_CAUSE bits for pad and software reset | The POR threshold and supply ramp behaviour |
Latch-based clock gates (clk_cpu, clk_dma, clk_sbr) | Clock enables on one free-running clock, one enable per flop | Function: a block with CLK_EN cleared holds its state, so the bug 10 hang still happens | Glitch-freedom, gating-cell timing, clock-tree power |
PD_SW power switches and 114 isolation clamps | Absent: SLEEP is clock gating and reset only | The reset and wake software flow | Isolation (planted bug 6 is invisible), retention, power-up sequencing |
SRAM and ROM macros | Block memories behind wrappers, ROM initialised with the real image | Contents, 2-cycle read latency if the wrapper keeps it | Macro timing; the single-port limit unless the wrapper enforces one access per clock |
Pads, crystal, SPI flash, UART terminal | Board connectors, a host adapter, a real or modelled flash | The pin-level protocols | Pad timing and drive; the pad pulls (a pull-down on the chip-select pad, pad bug P1) and the simultaneous-switching limit (pad bug P2), which an FPGA I/O cell does not reproduce |
Two rows deserve care. The memory wrapper must keep one access per clock: a dual-port block memory would serve the SBR's read and write streams at once and make the same-bank slowdown of 361.4 to 180.7 MB/s disappear, so a performance run on the prototype would pass a trap that silicon fails. And the clock scaling must use one factor for every domain. NanoSoC runs 192, 96 and 24 MHz; dividing all three by 8 gives 24, 12 and 3 MHz with the ratios intact, so every crossing and every cycle-count latency in the boot and latency tables keeps its meaning. Scaling only the fast clocks would change the sys-to-per ratio and test a different chip.
// Prototype replacement for a gated clock: the flops stay on the free-running
// clock and the gate becomes an enable on each of them. Function is preserved,
// the glitch-free gating cell and its timing are not.
module nano_gate_as_enable (
input logic clk_sys,
input logic rst_n,
input logic clk_en, // CLK_EN bit for this block
input logic d,
output logic q
);
always_ff @(posedge clk_sys or negedge rst_n) begin
if (!rst_n) q <= 1'b0;
else if (clk_en) q <= d; // clk_en low: the state holds, as with a stopped clock
end
endmodule# Clock scaling for the prototype: one factor for every domain keeps ratios, cycle
# counts and the UART and SPI settings meaningful. NanoSoC values from the clock table.
SCALE = 8
CPU, SYS, PER = 192e6 / SCALE, 96e6 / SCALE, 24e6 / SCALE
assert (CPU / PER, SYS / PER) == (8.0, 4.0) # ratios unchanged
BAUDDIV, CLKDIV = 13, 1
baud = PER / (16 * BAUDDIV)
spi_bps = PER / (2 * (CLKDIV + 1))
silicon_baud = 24e6 / (16 * BAUDDIV)
err_pct = (baud / 14_400 - 1) * 100
print(f"UART {baud:.1f} baud ({err_pct:+.2f}% vs 14,400), SPI {spi_bps / 1e3:.0f} kb/s")
assert abs(err_pct - 0.16) < 0.01 # the same +0.16% error as 115,200 on silicon
assert abs(silicon_baud / baud - SCALE) < 1e-9
boot_silicon_ms = 12.580917
boot_proto_ms = boot_silicon_ms * SCALE # every time-based step stretches by the factor
assert abs(boot_proto_ms - 100.647) < 0.01
# the check can fail: scaling only the fast clocks changes the sys : per ratio
bad_sys = 96e6 / SCALE
bad_per = 24e6 # peripheral clock left at full speed
assert bad_sys / bad_per != 4.0
print(f"boot takes {boot_proto_ms:.1f} ms on the prototype; ratios 8 : 4 : 1 kept")
print("PASS: scale by 8 keeps the ratios, UART at 14,400 baud, SPI at 750 kb/s")A table of every technology swap with a preserved column and a lost column, signed by the person who owns the wrapper.
One scale factor for all domains, with the UART divisor, the SPI rate and the host-side settings recomputed from it.
The prototype runs the real ROM image and the real boot code, not a shortened copy, so the sequence proved is the one in silicon.
When a prototype run disagrees with simulation, diff the platform changes before the design: confirm the clock enables hold state exactly as the gated clocks did, that the memory wrapper reproduces the read latency and the single port, and that the reset the board applies is released in the same staged order. A prototype that releases every reset at once hides planted bug 2 by construction: the CPU is never early because nothing is staged. Add the reset-order assertion to the prototype's wrapper, not only to simulation.
The peripheral and power logic count cycles of clk_per, and the crossings depend on the 8 : 4 : 1 ratios. Changing one ratio changes the chip under test: the synchroniser latencies, the interrupt window and the APB bridge timing would no longer be those of the design that is taped out.