Summary
The linker writes the requested clock frequency into system_map.xml before Vivado runs and then never updates it. If Vivado fails to close timing at that frequency (which is the common case today, given Issue 1), the value reported to the runtime via system_map.xml is a lie: the bitstream actually runs slower than what <ClockFrequency> claims.
Interestingly, the helper that would fix this already exists — apply_timing_frequency_cap in linker/slashkit/emit/metadata/timing_freq.py parses the Vivado timing report, computes the achievable frequency from WNS, and rewrites system_map.xml with min(user_clock_hz, computed_max_hz). It is simply never called from anywhere in the codebase.
Expected behavior
After Vivado finishes place-and-route, the linker should:
- Locate the design timing summary for the user region.
- Parse WNS(ns) from it.
- Compute the achievable frequency from WNS and the base frequency that PnR was run against.
- If the achieved frequency is lower than what was written to
system_map.xml, overwrite <ClockFrequency> with the achieved value so the runtime uses the correct clock.
Actual behavior
system_map.xml always contains the value that was chosen before Vivado ran (either --clock-hz, the first per-kernel clock_hz, or the default 200 MHz from resolve_system_map_clock). No post-PnR adjustment ever happens.
Pointers to the relevant code
- The existing-but-unused helper:
linker/slashkit/emit/metadata/timing_freq.py:146-212 (apply_timing_frequency_cap).
- It already handles report discovery (
_find_timing_report, line 134), WNS parsing (extract_design_wns_ns, line 36), the freq math (compute_max_freq_hz_from_wns, line 73), and the read/write of <ClockFrequency> (read_system_map_clock_hz / write_system_map_clock_hz, lines 88 / 107).
- It locates the HW build dir from
SLASH_HW_BUILD_DIR (see HW_BUILD_DIR_ENV_KEYS, line 33, and _resolve_hw_build_dir, line 123). For the call to be useful, either this env var must be set by whatever orchestrates the Vivado run, or the helper should accept the build dir explicitly from the linker.
- Where
system_map.xml is written before Vivado runs and never revisited:
- The actual Vivado invocation / VBIN packaging is where a post-PnR hook would naturally live. The
apply_timing_frequency_cap call should be added after Vivado returns successfully and before system_map.xml is bundled into the VBIN archive.
Suggested fix direction
- After a successful HW build, call
apply_timing_frequency_cap(project_name=config.project_name, system_map_path=config.build_dir / "system_map.xml", base_freq_hz=<actual_target_freq>, hw_build_dir=<resolved_build_dir>).
base_freq_hz should be the frequency that PnR actually targeted — once Issue 1 is fixed, this is the same value as --clock-hz and the two issues compose naturally. Until then, it must match the hard-coded 400 MHz baked into slash_base.tcl.
- Consider making the dependency on
SLASH_HW_BUILD_DIR explicit by passing hw_build_dir from the linker rather than relying on an environment variable.
Summary
The linker writes the requested clock frequency into
system_map.xmlbefore Vivado runs and then never updates it. If Vivado fails to close timing at that frequency (which is the common case today, given Issue 1), the value reported to the runtime viasystem_map.xmlis a lie: the bitstream actually runs slower than what<ClockFrequency>claims.Interestingly, the helper that would fix this already exists —
apply_timing_frequency_capinlinker/slashkit/emit/metadata/timing_freq.pyparses the Vivado timing report, computes the achievable frequency from WNS, and rewritessystem_map.xmlwithmin(user_clock_hz, computed_max_hz). It is simply never called from anywhere in the codebase.Expected behavior
After Vivado finishes place-and-route, the linker should:
system_map.xml, overwrite<ClockFrequency>with the achieved value so the runtime uses the correct clock.Actual behavior
system_map.xmlalways contains the value that was chosen before Vivado ran (either--clock-hz, the first per-kernelclock_hz, or the default 200 MHz fromresolve_system_map_clock). No post-PnR adjustment ever happens.Pointers to the relevant code
linker/slashkit/emit/metadata/timing_freq.py:146-212(apply_timing_frequency_cap)._find_timing_report, line 134), WNS parsing (extract_design_wns_ns, line 36), the freq math (compute_max_freq_hz_from_wns, line 73), and the read/write of<ClockFrequency>(read_system_map_clock_hz/write_system_map_clock_hz, lines 88 / 107).SLASH_HW_BUILD_DIR(seeHW_BUILD_DIR_ENV_KEYS, line 33, and_resolve_hw_build_dir, line 123). For the call to be useful, either this env var must be set by whatever orchestrates the Vivado run, or the helper should accept the build dir explicitly from the linker.system_map.xmlis written before Vivado runs and never revisited:linker/slashkit/emit/hw/tcl_gen.py:313-328(HW flow renderssystem_map.xmlfrom the requestedclock_hz).linker/slashkit/emit/sim/tcl_gen.py:356andlinker/slashkit/emit/emu/tcl_gen.py:72-88.apply_timing_frequency_capcall should be added after Vivado returns successfully and beforesystem_map.xmlis bundled into the VBIN archive.Suggested fix direction
apply_timing_frequency_cap(project_name=config.project_name, system_map_path=config.build_dir / "system_map.xml", base_freq_hz=<actual_target_freq>, hw_build_dir=<resolved_build_dir>).base_freq_hzshould be the frequency that PnR actually targeted — once Issue 1 is fixed, this is the same value as--clock-hzand the two issues compose naturally. Until then, it must match the hard-coded 400 MHz baked intoslash_base.tcl.SLASH_HW_BUILD_DIRexplicit by passinghw_build_dirfrom the linker rather than relying on an environment variable.