Timing closure and the build re-roll¶
Now and then a board fails timing although nothing is wrong with the design:
one hold check in the HyperRAM read path misses, usually by a few
picoseconds. Which board and which build it hits is a matter of placement
luck, so build_all.sh re-rolls such a board automatically. This note
explains the path, why at present neither a tool setting nor a design change
can fix it, and why the re-roll is safe.
The short version:
- The failing check is the HyperRAM read capture of the M2M framework. Its hold margin is only 0 to 0.5 ns, and it is decided by the fabric route of the RWDS strobe, which changes with every placement.
- Vivado cannot repair it, and building again unchanged gives the identical result: Vivado is deterministic and has no seed.
- The design-side lever, the IDELAY tap of the strobe, is field-calibrated and must not change.
- A new placement is a fresh draw from the same distribution that every
shipped build came from. Most placements pass (12 of the 13 R3 and R6
builds of August and September 2026), so
build_all.shkeeps drawing until one does.
The path¶
M2M/vhdl/controllers/hyperram/hyperram_rx.vhd samples the eight HyperRAM
data lines with IDDRs that are clocked by the RWDS strobe:
hr_d_io[n] -> IBUF -> IDDR D (hard-wired)
hr_rwds_io -> IBUF -> IDELAYE2 (FIXED, 20 taps) -> fabric route -> IDDR C
The strobe is a clock on general fabric routing (CLOCK_BUFFER_TYPE NONE in
M2M/common.xdc) with 36 loads: the eight IDDRs, the receive FIFO and a
clock-domain-crossing flop. For each data bit, the hold slack in the slow
corner comes down to
Everything else in the path is fixed silicon: the pins, the IDDR and IDELAY
sites and the constraints are identical on R3, R4, R5 and R6. The route is
not fixed. It depends on where the placer puts the fabric loads of the
strobe, and it moves by a few tenths of a nanosecond from build to build
(0.87 to 1.55 ns have been seen). The WIP-V2-A10 build is a typical case:
R3 closed with DQ4's branch at 1.356 ns (hold +0.187 ns). R6 placed the same
netlist so that the branch grew to 1.552 ns (hold −0.008 ns), and a re-roll
with the ExtraTimingOpt placer directive brought it to 1.285 ns
(hold +0.258 ns).
Why the tools cannot fix it¶
- The router repairs hold violations by making the data path longer. Here the data path is the hard-wired pad-to-IDDR connection, so there is nothing to lengthen, and the router does not shorten the strobe route to gain hold.
- Post-route
phys_opt_designonly works on setup. - Vivado is deterministic: the same sources, constraints and settings give the same bitstream, bit for bit. There is no placer seed; a different directive is the closest equivalent.
Why we do not change the design¶
The sampling point of the HyperRAM read is the IDELAY delay plus the fabric route of the strobe. The IDELAY value 20 is the result of a long field process: individual MEGA65 machines (serial numbers, not board revisions) differ slightly in their HyperRAM behaviour, and 20 works on 99.999% of them. So:
- Changing the tap would move the sampling point of every MEGA65 away from the field-proven setting. That needs a new field campaign, not a build fix.
- Forcing the strobe route, with fixed routing or by re-routing single strobe branches with delay targets, moves the sampling point just the same. Re-routing a single branch does not even help much: the new branch has to hang off the existing strobe tree, so it cannot be faster than the slow trunk that caused the miss.
- Loosening the input delay constraints would only hide the missing margin.
A new placement changes none of this. It gives the strobe a new route from the same distribution that every shipped build was drawn from, so the sampling point stays inside the calibrated range.
The re-roll in build_all.sh¶
After all selected boards are built, build_all.sh re-rolls every board
whose build only missed timing: TIMING-FAILED with a setup or hold miss of
at most REROLL_MAX_MISS (0.3 ns by default). Synthesis failures, failed
sign-off gates and crashes are never re-rolled, because they are real
problems.
CORE/reroll_bitstream.tcl then works through every attempt until one
meets setup and hold:
- After a hold miss it tries new placements first (ten placer directives), then five router directives on the existing placement.
- After a setup-only miss it first runs a harder post-route
phys_opt_design, then the re-routes, then the new placements. - An attempt that misses setup only gets one more, harder
phys_opt_designpass before it counts as failed.
Every attempt starts from the checkpoints of the failed build, which
build_all.sh checks are newer than that build's start. That is the same
synthesized and optimized netlist with the same constraints; only placement,
routing and phys_opt_design are redone. The result passes the same sign-off
as a first pass: full timing analysis in both corners, the bitstream
design-rule check and the gates of build_bitstream.tcl. A re-rolled
bitstream is therefore exactly as valid as a first-pass one.
The winner replaces mega65_<board>.bit and mega65_<board>.mmi in
CORE/CORE-<board>.runs/impl_1, so make_release.py, m65 and everything
else find it where they always do. Next to it, mega65_<board>_reroll.txt
records which attempt won, mega65_<board>_reroll_timing.rpt holds its
timing report and mega65_<board>_reroll.dcp its checkpoint. All other
reports in that folder describe the failed first pass. The console output
of a re-roll goes to CORE/build_<board>_reroll.log.
The summary at the end of build_all.sh shows the outcome per board, for
example:
RESULT R3 OK WNS=0.133346 WHS=0.031509 bit=CORE-R3.runs/impl_1/mega65_r3.bit
RESULT R6 OK after re-roll 2/15 (place_design ExtraTimingOpt, route_design Explore) WNS=0.093 WHS=0.055 bit=CORE-R6.runs/impl_1/mega65_r6.bit
first pass: TIMING-FAILED WNS=0.201746 WHS=-0.008183
REROLL R6 1/15 place_design ExtraPostPlacementOpt, route_design Explore: fail WNS=0.161 WHS=-0.017 (12 min)
REROLL R6 2/15 place_design ExtraTimingOpt, route_design Explore: PASS WNS=0.093 WHS=0.055 (14 min)
An attempt takes about as long as the implementation part of a normal
build: roughly 15 minutes on R6 and 50 on R3. ./build_all.sh --no-reroll
skips the re-roll pass, and ./build_all.sh --help lists all options.