diff --git a/docs/howto/set-clock-frequency.rst b/docs/howto/set-clock-frequency.rst
index 07900c52..f074f3da 100644
--- a/docs/howto/set-clock-frequency.rst
+++ b/docs/howto/set-clock-frequency.rst
@@ -103,6 +103,40 @@ Frequency Guidelines
- The user's ``vrtd`` role must include the ``clock`` permission.
See :doc:`/reference/vrtd/configuration`.
+.. _clock-hz-not-a-wish:
+
+Asking for more than the design can reach
+==========================================
+
+``--clock-hz`` is the frequency the reconfigurable module is *implemented
+against*, not an upper bound to aim at. After routing, the linker measures the
+worst negative slack and records the frequency actually achieved in the vbin,
+so the runtime never clocks user logic faster than it was timed for.
+
+Setting a target the design cannot reach is therefore counterproductive. An
+over-constrained implementation routes worse than an achievable one, and the
+recorded frequency is derated from that worse result. Measured on
+``00_axilite``:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Shell
+ - ``--clock-hz 200000000``
+ - ``--clock-hz 250000000``
+ * - service
+ - 184 MHz delivered
+ - 146 MHz delivered
+ * - compute
+ - 200 MHz delivered (timing met)
+ - 161 MHz delivered
+
+Asking for 250 MHz produced a *slower* device than asking for 200 MHz on both
+shells. When the target is missed the linker prints a warning naming the
+requested and recorded frequencies; treat it as a prompt to re-link at a lower
+target rather than as advisory noise. Raise ``--clock-hz`` incrementally and
+keep the highest value that still meets timing.
+
Next Steps
==========
diff --git a/linker/slashkit/__main__.py b/linker/slashkit/__main__.py
index 520829fa..fa30af56 100644
--- a/linker/slashkit/__main__.py
+++ b/linker/slashkit/__main__.py
@@ -38,6 +38,7 @@
from slashkit.emit.emu.project_gen import build_emu_project, package_emu_artifacts
from slashkit.emit.metadata.prog_image import build_vbin
+from slashkit.emit.metadata.timing_freq import apply_timing_frequency_cap
from slashkit.core.command_config import (
LinkerConfiguration,
Platform,
@@ -148,6 +149,18 @@ def link(config: LinkerConfiguration) -> None:
elif config.platform == Platform.EMULATION:
package_emu_artifacts(config)
else:
+ # Inspect the achieved user-clock frequency from the RM timing report and
+ # cap the value recorded in system_map.xml at the requested target, before
+ # it is packed into the vbin.
+ timing_report = (
+ config.build_dir / "slash_rm" /
+ f"report_timing_{config.project_name}.txt"
+ )
+ apply_timing_frequency_cap(
+ project_name=config.project_name,
+ system_map_path=config.build_dir / "system_map.xml",
+ timing_report=timing_report,
+ )
generate_util_report(config)
build_vbin(config)
diff --git a/linker/slashkit/core/command_config.py b/linker/slashkit/core/command_config.py
index a2f47346..477980ba 100644
--- a/linker/slashkit/core/command_config.py
+++ b/linker/slashkit/core/command_config.py
@@ -217,7 +217,7 @@ def populate_argument_parser(cls, ap: argparse.ArgumentParser):
ap.add_argument("--pre-synth-tcls", type=Path, nargs="*", default=[],
help="Paths to TCL scripts to run before synthesis (applies to hardware builds only).")
ap.add_argument("--clock-hz", required=False,
- type=int, default=None, help="Target clock frequency in MHz.")
+ type=int, default=None, help="Target clock frequency in Hz.")
def __init__(self, args: argparse.Namespace):
super().__init__(args)
diff --git a/linker/slashkit/emit/hw/project_gen.py b/linker/slashkit/emit/hw/project_gen.py
index e6d69a8f..b2a38518 100644
--- a/linker/slashkit/emit/hw/project_gen.py
+++ b/linker/slashkit/emit/hw/project_gen.py
@@ -38,7 +38,10 @@
CommandConfiguration,
ShellType,
)
-from slashkit.emit.metadata.timing_freq import require_static_shell_timing_or_confirm
+from slashkit.emit.metadata.timing_freq import (
+ require_static_shell_timing_or_confirm,
+ read_system_map_clock_hz,
+)
from slashkit.core.launcher import (
TASK_AVED,
TASK_BOOTGEN,
@@ -460,6 +463,61 @@ class RM_KIND(Enum):
SERVICE_LAYER = "service_layer"
+def _resolve_target_user_clock_hz(config: LinkerConfiguration) -> Optional[int]:
+ # The resolved target user-clock frequency has already been written to
+ # system_map.xml by generate_tcl(), so read it back rather than re-deriving
+ # it, keeping a single source of truth (see resolve_system_map_clock).
+ system_map_path = config.build_dir / "system_map.xml"
+ target_hz = read_system_map_clock_hz(system_map_path)
+ if target_hz is None or target_hz <= 0:
+ logger.warning(
+ "No valid target ClockFrequency in %s; skipping user-clock constraint",
+ system_map_path,
+ )
+ return None
+ return target_hz
+
+
+def _generate_user_clock_xdc(
+ config: LinkerConfiguration, target_hz: int
+) -> Path:
+ # Turn the resolved target user-clock frequency into an actual Vivado timing
+ # constraint for the reconfigurable-module implementation.
+ period_ns = 1e9 / float(target_hz)
+ xdc_path = config.build_dir / "user_clock.xdc"
+ # Defining a clock at the RM's user_clk port overrides, downstream of that
+ # point, the clock the abstract shell propagates in. That clock is
+ # clkout1_primitive_2, auto-derived by Vivado from the clocking wizard's
+ # MMCME5 CLKOUT0 and therefore pinned to the 200 MHz the static shell was
+ # built with -- it does not follow the wizard's runtime DRP reprogramming,
+ # so it has to be overridden here rather than trusted.
+ #
+ # Overriding at the RM boundary rather than at the MMCM output keeps the
+ # signed-off static timing untouched, and needs no reference to a
+ # static-region hierarchy path, which would differ between the service and
+ # compute shells.
+ #
+ # Known limitation: this leaves two clock objects on one physical net --
+ # user_clk at the requested period inside the RM, and the shell's
+ # clkout1_primitive_2 at 200 MHz outside it. A handful of static-side flops
+ # do drive into the RM's clock domain (2 endpoints on 00_axilite), and
+ # Vivado times those crossings against the beat frequency of the two
+ # periods rather than treating them as the same clock: at 250 MHz the
+ # 4 ns / 5 ns pair yields a bogus 1 ns requirement. Both are physically the
+ # same net and run at the same rate once the wizard is reprogrammed, so
+ # those paths are not really failing. Measured cost is under 1 MHz on both
+ # shells, and it is latent at the default -- at 200 MHz the two clocks share
+ # a 5 ns grid and the crossings pass with positive slack.
+ constraint = (
+ f"create_clock -name user_clk -period {period_ns:.6f}"
+ " [get_ports user_clk]\n"
+ )
+ xdc_path.write_text(constraint, encoding="utf-8")
+ logger.info("Wrote user-clock constraint (%.6f ns / %d Hz) to %s",
+ period_ns, target_hz, xdc_path)
+ return xdc_path
+
+
def _run_rm_build(config: LinkerConfiguration, rm_kind: RM_KIND) -> None:
# Copy all base IP cores into the ip repository
config.ip_repository.mkdir(parents=True, exist_ok=True)
@@ -555,6 +613,16 @@ def _run_rm_build(config: LinkerConfiguration, rm_kind: RM_KIND) -> None:
for path in config.pre_synth_tcls:
cmd.extend(["--pre-synth-tcl", str(path)])
+ # Both halves of the user-clock chain come from the same resolved
+ # target: --user-clock-hz retargets the RM block design (and with
+ # it the module's synthesis constraints), --user-clock-xdc
+ # constrains the implementation run.
+ target_hz = _resolve_target_user_clock_hz(config)
+ if target_hz is not None:
+ cmd.extend(["--user-clock-hz", str(target_hz)])
+ user_clock_xdc = _generate_user_clock_xdc(config, target_hz)
+ cmd.extend(["--user-clock-xdc", str(user_clock_xdc)])
+
if rm_kind == RM_KIND.SERVICE_LAYER:
opt_post_tcl = stack.enter_context(
resources.path(
diff --git a/linker/slashkit/emit/metadata/timing_freq.py b/linker/slashkit/emit/metadata/timing_freq.py
index 70304529..4fb9dd92 100644
--- a/linker/slashkit/emit/metadata/timing_freq.py
+++ b/linker/slashkit/emit/metadata/timing_freq.py
@@ -229,11 +229,46 @@ def _find_timing_report(project_name: str, hw_build_dir: Path) -> Optional[Path]
return None
+def _warn_target_not_met(
+ *,
+ requested_hz: int,
+ final_clock_hz: int,
+ wns_ns: float,
+ timing_report: Path,
+) -> None:
+ # The recorded frequency is derated from a run that failed its own target,
+ # and an over-constrained run routes worse than an achievable one. Measured
+ # on 00_axilite: asking 250 MHz where the design closes at 200 MHz pushed
+ # the critical path out by ~1.2-1.3 ns on both shells, so the derated result
+ # (146 MHz service, 161 MHz compute) came out *below* what asking for
+ # 200 MHz delivered (184 MHz and 200 MHz). Raising --clock-hz past what the
+ # design can reach therefore costs frequency rather than gaining it, which
+ # is unintuitive enough to be worth saying out loud rather than only
+ # recording in the log.
+ print(
+ "WARNING: user clock target not met for the reconfigurable module.",
+ file=sys.stderr,
+ )
+ print(f" Requested: {requested_hz} Hz", file=sys.stderr)
+ print(f" Recorded: {final_clock_hz} Hz (WNS(ns)={wns_ns:.3f})",
+ file=sys.stderr)
+ print(f" Report: {timing_report}", file=sys.stderr)
+ print(
+ " The recorded frequency is derated from a run that missed its target."
+ " An over-constrained\n"
+ " implementation routes worse than an achievable one, so re-linking"
+ " with a lower --clock-hz\n"
+ " will often deliver a HIGHER final frequency than this one.",
+ file=sys.stderr,
+ )
+
+
def apply_timing_frequency_cap(
*,
project_name: str,
system_map_path: Path,
- base_freq_hz: int = 400_000_000,
+ base_freq_hz: Optional[int] = None,
+ timing_report: Optional[Path] = None,
hw_build_dir: Optional[Path] = None,
) -> Optional[int]:
user_clock_hz = read_system_map_clock_hz(system_map_path)
@@ -242,17 +277,33 @@ def apply_timing_frequency_cap(
"ClockFrequency missing or invalid in system_map.xml: %s", system_map_path)
return None
- resolved_hw_build_dir = _resolve_hw_build_dir(hw_build_dir)
- if resolved_hw_build_dir is None:
- logger.warning(
- "HW build directory env var is unset; keeping user clock_hz=%d", user_clock_hz)
- return user_clock_hz
+ # The user region is implemented against the target period, so the WNS in
+ # the timing report is measured relative to the target frequency. Default the
+ # analysis base to the target so the computed achievable frequency is correct.
+ if base_freq_hz is None:
+ base_freq_hz = user_clock_hz
- timing_report = _find_timing_report(project_name, resolved_hw_build_dir)
if timing_report is None:
+ resolved_hw_build_dir = _resolve_hw_build_dir(hw_build_dir)
+ if resolved_hw_build_dir is None:
+ logger.warning(
+ "HW build directory env var is unset; keeping user clock_hz=%d", user_clock_hz)
+ return user_clock_hz
+
+ timing_report = _find_timing_report(
+ project_name, resolved_hw_build_dir)
+ if timing_report is None:
+ logger.warning(
+ "Timing report not found under %s for project %s; keeping user clock_hz=%d",
+ resolved_hw_build_dir,
+ project_name,
+ user_clock_hz,
+ )
+ return user_clock_hz
+ elif not timing_report.is_file():
logger.warning(
- "Timing report not found under %s for project %s; keeping user clock_hz=%d",
- resolved_hw_build_dir,
+ "Timing report %s not found for project %s; keeping user clock_hz=%d",
+ timing_report,
project_name,
user_clock_hz,
)
@@ -288,6 +339,12 @@ def apply_timing_frequency_cap(
)
if final_clock_hz != user_clock_hz:
+ _warn_target_not_met(
+ requested_hz=user_clock_hz,
+ final_clock_hz=final_clock_hz,
+ wns_ns=wns_ns,
+ timing_report=timing_report,
+ )
write_system_map_clock_hz(system_map_path, final_clock_hz)
logger.info("Updated system_map ClockFrequency to %d: %s",
final_clock_hz, system_map_path)
diff --git a/linker/slashkit/resources/base/common/scripts/slash_project_build.tcl b/linker/slashkit/resources/base/common/scripts/slash_project_build.tcl
index 3a69d47f..4627eb99 100644
--- a/linker/slashkit/resources/base/common/scripts/slash_project_build.tcl
+++ b/linker/slashkit/resources/base/common/scripts/slash_project_build.tcl
@@ -43,6 +43,8 @@ array set opts {
--rm-work-dir ""
--artifact-out-dir ""
--util-report-file ""
+ --user-clock-xdc ""
+ --user-clock-hz ""
--jobs 8
}
@@ -89,6 +91,14 @@ set linker_results_dir [file normalize $opts(--linker-results-dir)]
set rm_work_dir $opts(--rm-work-dir)
set artifact_out_dir $opts(--artifact-out-dir)
set util_report_file $opts(--util-report-file)
+set user_clock_xdc $opts(--user-clock-xdc)
+if {$user_clock_xdc ne ""} {
+ set user_clock_xdc [file normalize $user_clock_xdc]
+}
+set user_clock_hz $opts(--user-clock-hz)
+if {$user_clock_hz ne "" && ![string is integer -strict $user_clock_hz]} {
+ error "Value for '--user-clock-hz' must be an integer, got '$user_clock_hz'."
+}
set jobs $opts(--jobs)
file mkdir $rm_work_dir
@@ -146,6 +156,26 @@ open_bd_design $imported_bd
foreach p [get_bd_intf_ports] {
set_property HDL_ATTRIBUTE.LOCKED {TRUE} $p
}
+
+# Retarget the reconfigurable module's clock port to the requested user-clock
+# frequency before the kernels are added. slash_base.bd hardcodes user_clk at
+# 200 MHz, so without this every AXI IP inside the RM is configured -- and the
+# out-of-context constraints the module's synthesis run is timed against are
+# derived -- for 200 MHz regardless of what --clock-hz asked for. The generated
+# BD Tcl ends in validate_bd_design, which propagates this to everything
+# connected downstream, so the retarget has to happen first.
+#
+# Only user_clk moves. The AXI interfaces crossing the partition boundary run on
+# static_region_clk at 400 MHz and are locked above; they are untouched by this.
+if {$user_clock_hz ne ""} {
+ set user_clk_port [get_bd_ports -quiet user_clk]
+ if {$user_clk_port eq ""} {
+ error "No user_clk port found in $imported_bd; cannot apply --user-clock-hz."
+ }
+ puts "Retargeting user_clk: [get_property CONFIG.FREQ_HZ $user_clk_port] -> $user_clock_hz Hz"
+ set_property CONFIG.FREQ_HZ $user_clock_hz $user_clk_port
+}
+
source $generated_bd_tcl
# Apply the default implementation strategy before sourcing the user pre-synth Tcl, so a
@@ -157,6 +187,20 @@ foreach pre_synth_tcl $pre_synth_tcls {
source $pre_synth_tcl
}
+# Apply the user-region clock timing constraint (create_clock on user_clk),
+# derived from the requested --clock-hz target. Scoped to the slash cell so it
+# overrides, inside the reconfigurable module only, the 200 MHz clock the
+# abstract shell propagates in from the clocking wizard. See
+# _generate_user_clock_xdc in project_gen.py for why the override belongs at
+# the RM boundary rather than at the wizard output.
+if {$user_clock_xdc ne ""} {
+ puts "Applying user-clock constraint: $user_clock_xdc"
+ _require_file $user_clock_xdc "user-clock XDC"
+ add_files -fileset constrs_1 -norecurse $user_clock_xdc
+ set_property USED_IN {synthesis implementation} [get_files $user_clock_xdc]
+ set_property SCOPED_TO_CELLS {top_i/slash} [get_files $user_clock_xdc]
+}
+
launch_runs "${slash_rm_name}_synth_1" -jobs $jobs
wait_on_run "${slash_rm_name}_synth_1"
diff --git a/linker/test/emit/metadata/__init__.py b/linker/test/emit/metadata/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/linker/test/emit/metadata/test_timing_freq.py b/linker/test/emit/metadata/test_timing_freq.py
new file mode 100644
index 00000000..913975a2
--- /dev/null
+++ b/linker/test/emit/metadata/test_timing_freq.py
@@ -0,0 +1,148 @@
+# ##################################################################################################
+# The MIT License (MIT)
+# Copyright (c) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+# and associated documentation files (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge, publish, distribute,
+# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all copies or
+# substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+# ##################################################################################################
+
+"""Tests for apply_timing_frequency_cap — achieved-frequency capping at target."""
+
+import textwrap
+from pathlib import Path
+
+from slashkit.emit.metadata import timing_freq
+
+
+def _write_system_map(path: Path, clock_hz: int) -> None:
+ path.write_text(
+ f'\n\n'
+ f' {clock_hz}\n\n',
+ encoding="utf-8",
+ )
+
+
+def _write_timing_report(path: Path, wns_ns: float) -> None:
+ # Minimal shape matching what extract_design_wns_ns() scans for: a
+ # "Design Timing Summary" section, a WNS(ns)/TNS(ns) header, then a data row
+ # whose first value is WNS and third is WHS.
+ path.write_text(
+ textwrap.dedent(
+ f"""\
+ Design Timing Summary
+ ---------------------
+
+ WNS(ns) TNS(ns) WHS(ns) THS(ns)
+ ------- ------- ------- -------
+ {wns_ns:.3f} 0.000 0.100 0.000
+ """
+ ),
+ encoding="utf-8",
+ )
+
+
+def test_positive_slack_is_capped_at_target(tmp_path):
+ # Target 250 MHz (4 ns). Positive WNS (headroom) would allow a higher
+ # frequency, but the result must be capped at the requested target.
+ target_hz = 250_000_000
+ system_map = tmp_path / "system_map.xml"
+ report = tmp_path / "report_timing_proj.txt"
+ _write_system_map(system_map, target_hz)
+ _write_timing_report(report, wns_ns=1.0) # achievable ~333 MHz
+
+ result = timing_freq.apply_timing_frequency_cap(
+ project_name="proj",
+ system_map_path=system_map,
+ timing_report=report,
+ )
+
+ assert result == target_hz
+ assert timing_freq.read_system_map_clock_hz(system_map) == target_hz
+
+
+def test_negative_slack_lowers_below_target(tmp_path):
+ # Target 250 MHz (4 ns period). Negative WNS means timing failed: the
+ # achievable frequency is 1e9 / (4 - (-1)) = 200 MHz, below the target.
+ target_hz = 250_000_000
+ system_map = tmp_path / "system_map.xml"
+ report = tmp_path / "report_timing_proj.txt"
+ _write_system_map(system_map, target_hz)
+ _write_timing_report(report, wns_ns=-1.0)
+
+ result = timing_freq.apply_timing_frequency_cap(
+ project_name="proj",
+ system_map_path=system_map,
+ timing_report=report,
+ )
+
+ assert result == 200_000_000
+ assert timing_freq.read_system_map_clock_hz(system_map) == 200_000_000
+
+
+def test_missing_report_keeps_target(tmp_path):
+ target_hz = 300_000_000
+ system_map = tmp_path / "system_map.xml"
+ _write_system_map(system_map, target_hz)
+
+ result = timing_freq.apply_timing_frequency_cap(
+ project_name="proj",
+ system_map_path=system_map,
+ timing_report=tmp_path / "does_not_exist.txt",
+ )
+
+ assert result == target_hz
+ assert timing_freq.read_system_map_clock_hz(system_map) == target_hz
+
+
+def test_missed_target_warns_on_stderr(tmp_path, capsys):
+ # Missing the target is unintuitive enough to warrant more than a log line:
+ # an over-constrained run routes worse, so the derated result can come out
+ # below what a lower --clock-hz would have delivered. Verify the user is
+ # told, and that the message names both frequencies.
+ target_hz = 250_000_000
+ system_map = tmp_path / "system_map.xml"
+ report = tmp_path / "report_timing_proj.txt"
+ _write_system_map(system_map, target_hz)
+ _write_timing_report(report, wns_ns=-1.0)
+
+ timing_freq.apply_timing_frequency_cap(
+ project_name="proj",
+ system_map_path=system_map,
+ timing_report=report,
+ )
+
+ err = capsys.readouterr().err
+ assert "user clock target not met" in err
+ assert str(target_hz) in err
+ assert "200000000" in err
+ assert "--clock-hz" in err
+
+
+def test_met_target_does_not_warn(tmp_path, capsys):
+ # The warning must stay quiet when the design closes, otherwise it becomes
+ # noise that gets filtered out.
+ target_hz = 250_000_000
+ system_map = tmp_path / "system_map.xml"
+ report = tmp_path / "report_timing_proj.txt"
+ _write_system_map(system_map, target_hz)
+ _write_timing_report(report, wns_ns=1.0)
+
+ timing_freq.apply_timing_frequency_cap(
+ project_name="proj",
+ system_map_path=system_map,
+ timing_report=report,
+ )
+
+ assert "target not met" not in capsys.readouterr().err
diff --git a/vrt/include/vrt/device.hpp b/vrt/include/vrt/device.hpp
index 730e4992..999be758 100644
--- a/vrt/include/vrt/device.hpp
+++ b/vrt/include/vrt/device.hpp
@@ -92,7 +92,6 @@ namespace impl {
class Device {
static constexpr uint64_t QDMA_LOGIC_BASE = 0x20100020000; ///< Base address for QDMA logic
static constexpr uint32_t QDMA_LOGIC_OFFSET = 0x1000; /// Offset for QDMA logic
- static constexpr uint32_t CLOCK_MAX_FREQ = 333333333;
uint8_t bar = 0; ///< Base Address Register (BAR)
uint64_t offset = 0; ///< Offset for memory operations
uint16_t pci_bdf = 0; ///< PCI Bus:Device.Function identifier
diff --git a/vrt/src/device.cpp b/vrt/src/device.cpp
index 13651b9f..5a6deb79 100644
--- a/vrt/src/device.cpp
+++ b/vrt/src/device.cpp
@@ -265,12 +265,19 @@ Device::Device(const std::string& bdf, const std::string& vrtbinPath, bool progr
sleep(1); // wait for device to be ready after programming before accessing BAR
}
- if (vrtdDevice.has_value()) {
- if (clockFreq > CLOCK_MAX_FREQ) {
- utils::Logger::log(utils::LogLevel::WARN, __PRETTY_FUNCTION__,
- "Clock frequency {} exceeds maximum frequency {}", clockFreq, CLOCK_MAX_FREQ);
- vrtdDevice->setUserClockRate(static_cast(CLOCK_MAX_FREQ));
+ if (program && vrtdDevice.has_value() && clockFreq > 0) {
+ // Program the user clock to the achieved (timed) frequency recorded
+ // in the vbin, rounding down so the user logic never runs faster
+ // than it was timed for. The user may still override afterwards via
+ // setFrequency().
+ if (clockFreq > std::numeric_limits::max()) {
+ throw std::runtime_error(
+ "Achieved clock frequency exceeds vrtd clock API limits");
}
+ uint32_t achieved =
+ vrtdDevice->setUserClockRateRoundDown(static_cast(clockFreq));
+ utils::Logger::log(utils::LogLevel::INFO, __PRETTY_FUNCTION__,
+ "Programmed user clock to {} Hz (target {} Hz)", achieved, clockFreq);
}
} else if (platform == Platform::EMULATION) {
parseSystemMap();
diff --git a/vrt/vrtd/include/vrtd/wire.h b/vrt/vrtd/include/vrtd/wire.h
index 8b25339a..996f95a0 100644
--- a/vrt/vrtd/include/vrtd/wire.h
+++ b/vrt/vrtd/include/vrtd/wire.h
@@ -522,6 +522,10 @@ enum vrtd_clock_region {
enum vrtd_clock_op {
VRTD_CLOCK_OP_GET = 0,
VRTD_CLOCK_OP_SET = 1,
+ /* Like SET, but never selects a realizable rate above the request. Used to
+ * program the user clock to the achieved (timed) frequency at load without
+ * ever running the user logic faster than it was timed for. */
+ VRTD_CLOCK_OP_SET_ROUND_DOWN = 2,
};
/**
diff --git a/vrt/vrtd/libvrtd/include/vrtd/vrtd.h b/vrt/vrtd/libvrtd/include/vrtd/vrtd.h
index 9dd68423..c3104a6d 100644
--- a/vrt/vrtd/libvrtd/include/vrtd/vrtd.h
+++ b/vrt/vrtd/libvrtd/include/vrtd/vrtd.h
@@ -633,6 +633,30 @@ enum vrtd_ret vrtd_clock_set_rate(
uint32_t *rate_hz_out
);
+/**
+ * @brief Set the clock rate for a device region, never exceeding the request.
+ *
+ * Like vrtd_clock_set_rate(), but the daemon only selects a realizable rate
+ * less than or equal to @p rate_hz_in. Used to program the user clock to the
+ * achieved (timed) frequency without ever running the logic faster than timed.
+ *
+ * @param fd Connected vrtd socket file descriptor.
+ * @param dev Device index (0‑based).
+ * @param region One of vrtd_clock_region.
+ * @param rate_hz_in Requested rate in Hz (upper bound).
+ * @param rate_hz_out Output pointer for the achieved rate in Hz.
+ *
+ * @return #VRTD_RET_OK on success; otherwise a #vrtd_ret error code.
+ * @pre @p rate_hz_out must not be NULL.
+ */
+enum vrtd_ret vrtd_clock_set_rate_round_down(
+ int fd,
+ uint32_t dev,
+ uint32_t region,
+ uint32_t rate_hz_in,
+ uint32_t *rate_hz_out
+);
+
struct vrtd_buffer {
int sock_fd;
diff --git a/vrt/vrtd/libvrtd/src/requests.c b/vrt/vrtd/libvrtd/src/requests.c
index 276a10b9..b2ce8838 100644
--- a/vrt/vrtd/libvrtd/src/requests.c
+++ b/vrt/vrtd/libvrtd/src/requests.c
@@ -1038,11 +1038,12 @@ enum vrtd_ret vrtd_clock_get_rate(
return VRTD_RET_OK;
}
-enum vrtd_ret vrtd_clock_set_rate(
+static enum vrtd_ret vrtd_clock_set_rate_op(
int fd,
uint32_t dev,
uint32_t region,
uint32_t rate_hz_in,
+ uint8_t op,
uint32_t *rate_hz_out
)
{
@@ -1053,7 +1054,7 @@ enum vrtd_ret vrtd_clock_set_rate(
struct vrtd_req_clock_op req = {
.dev_number = dev,
.region = region,
- .op = VRTD_CLOCK_OP_SET,
+ .op = op,
.rate_hz = rate_hz_in,
};
struct vrtd_resp_clock_op resp = {0};
@@ -1070,6 +1071,30 @@ enum vrtd_ret vrtd_clock_set_rate(
return VRTD_RET_OK;
}
+enum vrtd_ret vrtd_clock_set_rate(
+ int fd,
+ uint32_t dev,
+ uint32_t region,
+ uint32_t rate_hz_in,
+ uint32_t *rate_hz_out
+)
+{
+ return vrtd_clock_set_rate_op(fd, dev, region, rate_hz_in,
+ VRTD_CLOCK_OP_SET, rate_hz_out);
+}
+
+enum vrtd_ret vrtd_clock_set_rate_round_down(
+ int fd,
+ uint32_t dev,
+ uint32_t region,
+ uint32_t rate_hz_in,
+ uint32_t *rate_hz_out
+)
+{
+ return vrtd_clock_set_rate_op(fd, dev, region, rate_hz_in,
+ VRTD_CLOCK_OP_SET_ROUND_DOWN, rate_hz_out);
+}
+
enum vrtd_ret vrtd_open_bar_file(
int fd,
uint32_t dev,
diff --git a/vrt/vrtd/libvrtdpp/include/vrtd/device.hpp b/vrt/vrtd/libvrtdpp/include/vrtd/device.hpp
index 1cb5fa5c..40588e16 100644
--- a/vrt/vrtd/libvrtdpp/include/vrtd/device.hpp
+++ b/vrt/vrtd/libvrtdpp/include/vrtd/device.hpp
@@ -337,10 +337,11 @@ class Device {
*
* @param region Clock region.
* @param rate_hz Requested rate in Hz.
+ * @param roundDown If true, never select a realizable rate above the request.
* @return Achieved rate in Hz.
* @throws vrtd::Error on error.
*/
- uint32_t setClockRate(ClockRegion region, uint32_t rate_hz) const;
+ uint32_t setClockRate(ClockRegion region, uint32_t rate_hz, bool roundDown = false) const;
/**
* @brief Convenience helper for service region get.
@@ -374,6 +375,17 @@ class Device {
return setClockRate(ClockRegion::User, rate_hz);
}
+ /**
+ * @brief Set the user region clock, never exceeding the requested rate.
+ *
+ * Programs the closest realizable rate that is <= @p rate_hz, so the user
+ * logic never runs faster than the requested (timed) frequency.
+ * @see setClockRate
+ */
+ uint32_t setUserClockRateRoundDown(uint32_t rate_hz) const {
+ return setClockRate(ClockRegion::User, rate_hz, true);
+ }
+
/**
* @brief Query all sensor readings for this device.
*
@@ -416,7 +428,7 @@ class Device {
std::function fCfgmemProgram,
std::function fCfgmemProgramFile,
std::function fGetClockRate,
- std::function fSetClockRate,
+ std::function fSetClockRate,
std::function(const Device&)> fGetSensorInfo);
uint32_t num;
@@ -441,7 +453,7 @@ class Device {
std::function fCfgmemProgram;
std::function fCfgmemProgramFile;
std::function fGetClockRate;
- std::function fSetClockRate;
+ std::function fSetClockRate;
std::function(const Device&)> fGetSensorInfo;
};
diff --git a/vrt/vrtd/libvrtdpp/include/vrtd/session.hpp b/vrt/vrtd/libvrtdpp/include/vrtd/session.hpp
index d62fdd9d..4235c8c5 100644
--- a/vrt/vrtd/libvrtdpp/include/vrtd/session.hpp
+++ b/vrt/vrtd/libvrtdpp/include/vrtd/session.hpp
@@ -394,10 +394,12 @@ class Session {
* @param device Device owning the clock.
* @param region One of vrtd_clock_region.
* @param rate_hz Requested rate in Hz.
+ * @param roundDown If true, never select a realizable rate above the request.
* @return Achieved rate in Hz.
* @throws vrtd::Error on error.
*/
- uint32_t setClockRate(const Device& device, ClockRegion region, uint32_t rate_hz) const;
+ uint32_t setClockRate(const Device& device, ClockRegion region, uint32_t rate_hz,
+ bool roundDown = false) const;
/**
* @internal Start, stop or delete an existing QDMA qpair.
diff --git a/vrt/vrtd/libvrtdpp/src/device.cpp b/vrt/vrtd/libvrtdpp/src/device.cpp
index 4419e40a..575f9a18 100644
--- a/vrt/vrtd/libvrtdpp/src/device.cpp
+++ b/vrt/vrtd/libvrtdpp/src/device.cpp
@@ -43,7 +43,7 @@ Device::Device(uint32_t num,
std::function fCfgmemProgram,
std::function fCfgmemProgramFile,
std::function fGetClockRate,
- std::function fSetClockRate,
+ std::function fSetClockRate,
std::function(const Device&)> fGetSensorInfo) {
this->num = num;
this->name = name;
@@ -161,8 +161,8 @@ uint32_t Device::getClockRate(ClockRegion region) const {
return fGetClockRate(*this, region);
}
-uint32_t Device::setClockRate(ClockRegion region, uint32_t rate_hz) const {
- return fSetClockRate(*this, region, rate_hz);
+uint32_t Device::setClockRate(ClockRegion region, uint32_t rate_hz, bool roundDown) const {
+ return fSetClockRate(*this, region, rate_hz, roundDown);
}
std::vector Device::getSensorInfo() const {
diff --git a/vrt/vrtd/libvrtdpp/src/session.cpp b/vrt/vrtd/libvrtdpp/src/session.cpp
index 34b68dda..4ff80d6f 100644
--- a/vrt/vrtd/libvrtdpp/src/session.cpp
+++ b/vrt/vrtd/libvrtdpp/src/session.cpp
@@ -182,7 +182,7 @@ Device Session::getDevice(size_t i) const {
return cfgmemProgramFile(device, path, bootDevice, partition);
},
[&](const Device& device, ClockRegion region) { return getClockRate(device, region); },
- [&](const Device& device, ClockRegion region, uint32_t rate_hz) { return setClockRate(device, region, rate_hz); },
+ [&](const Device& device, ClockRegion region, uint32_t rate_hz, bool roundDown) { return setClockRate(device, region, rate_hz, roundDown); },
[&](const Device& device) { return getSensorInfo(device); }
);
}
@@ -259,7 +259,7 @@ Device Session::getDeviceByBdf(std::string_view bdf) const {
return cfgmemProgramFile(device, path, bootDevice, partition);
},
[&](const Device& device, ClockRegion region) { return getClockRate(device, region); },
- [&](const Device& device, ClockRegion region, uint32_t rate_hz) { return setClockRate(device, region, rate_hz); },
+ [&](const Device& device, ClockRegion region, uint32_t rate_hz, bool roundDown) { return setClockRate(device, region, rate_hz, roundDown); },
[&](const Device& device) { return getSensorInfo(device); }
);
}
@@ -636,14 +636,19 @@ uint32_t Session::getClockRate(const Device& device, ClockRegion region) const {
return rate;
}
-uint32_t Session::setClockRate(const Device& device, ClockRegion region, uint32_t rate_hz) const {
+uint32_t Session::setClockRate(const Device& device, ClockRegion region, uint32_t rate_hz,
+ bool roundDown) const {
if (isClosed()) {
throw Error(VRTD_RET_BAD_LIB_CALL);
}
std::lock_guard lk(*m);
uint32_t achieved = 0;
- auto ret = vrtd_clock_set_rate(fd, device.getNum(), static_cast(region), rate_hz, &achieved);
+ auto ret = roundDown
+ ? vrtd_clock_set_rate_round_down(fd, device.getNum(),
+ static_cast(region), rate_hz, &achieved)
+ : vrtd_clock_set_rate(fd, device.getNum(),
+ static_cast(region), rate_hz, &achieved);
if (ret != VRTD_RET_OK) {
throw Error(ret);
}
diff --git a/vrt/vrtd/src/clock.c b/vrt/vrtd/src/clock.c
index 88f296f3..c85eba2c 100644
--- a/vrt/vrtd/src/clock.c
+++ b/vrt/vrtd/src/clock.c
@@ -812,7 +812,8 @@ static size_t clock_driver_generate_candidates(
struct clock_driver *clk,
uint32_t target_hz,
struct clock_candidate *cands,
- size_t max_candidates
+ size_t max_candidates,
+ bool no_exceed
)
{
if (clk == NULL || target_hz == 0 || cands == NULL || max_candidates == 0) {
@@ -859,6 +860,11 @@ static size_t clock_driver_generate_candidates(
for (uint32_t o = o_lo; o <= o_hi; ++o) {
uint64_t achieved_hz = fvco_hz / o;
+ /* Round-down mode: never program a rate above the target, so the
+ * user logic never runs faster than the timed frequency. */
+ if (no_exceed && achieved_hz > target_hz) {
+ continue;
+ }
uint64_t diff_hz = (achieved_hz > target_hz)
? (achieved_hz - target_hz)
: (target_hz - achieved_hz);
@@ -924,7 +930,8 @@ static int clock_driver_try_set_rate_hz(
struct clock_driver *clk,
uint32_t wizard_offset,
uint32_t clock_id,
- uint32_t *rate_hz_inout
+ uint32_t *rate_hz_inout,
+ bool no_exceed
)
{
if (clk == NULL || rate_hz_inout == NULL || *rate_hz_inout == 0) {
@@ -944,7 +951,8 @@ static int clock_driver_try_set_rate_hz(
clk,
*rate_hz_inout,
candidates,
- CLOCK_DRIVER_DEFAULT_MAX_CANDIDATES
+ CLOCK_DRIVER_DEFAULT_MAX_CANDIDATES,
+ no_exceed
);
if (count == 0) {
errno = ERANGE;
@@ -1075,7 +1083,8 @@ int clock_driver_set_service_region_rate_hz(struct clock_driver *clk, uint32_t *
clk,
CLOCK_DRIVER_SERVICE_REGION_WIZARD_OFFSET,
CLOCK_DRIVER_WIZARD_CLKOUT_ID,
- rate_hz_inout
+ rate_hz_inout,
+ false
);
if (ret != 0) {
LOG(
@@ -1127,7 +1136,11 @@ int clock_driver_get_user_region_rate_hz(struct clock_driver *clk, uint32_t *rat
* actual achieved frequency in Hz.
* @return 0 on success, -1 on error (logged).
*/
-int clock_driver_set_user_region_rate_hz(struct clock_driver *clk, uint32_t *rate_hz_inout)
+int clock_driver_set_user_region_rate_hz(
+ struct clock_driver *clk,
+ uint32_t *rate_hz_inout,
+ bool no_exceed
+)
{
if (clock_driver_check_wizard_bounds(clk, CLOCK_DRIVER_USER_REGION_WIZARD_OFFSET) != 0) {
LOG(LOG_ERR, "clock_driver: user region wizard bounds check failed: %m");
@@ -1137,7 +1150,8 @@ int clock_driver_set_user_region_rate_hz(struct clock_driver *clk, uint32_t *rat
clk,
CLOCK_DRIVER_USER_REGION_WIZARD_OFFSET,
CLOCK_DRIVER_WIZARD_CLKOUT_ID,
- rate_hz_inout
+ rate_hz_inout,
+ no_exceed
);
if (ret != 0) {
LOG(
diff --git a/vrt/vrtd/src/clock.h b/vrt/vrtd/src/clock.h
index ee41044e..a0b9535b 100644
--- a/vrt/vrtd/src/clock.h
+++ b/vrt/vrtd/src/clock.h
@@ -35,6 +35,7 @@
#ifndef VRTD_CLOCK_H
#define VRTD_CLOCK_H
+#include
#include
#include
@@ -159,14 +160,21 @@ int clock_driver_get_user_region_rate_hz(struct clock_driver *clk, uint32_t *rat
/**
* @brief Set the user-region clock to the requested frequency.
*
- * Behaves identically to @c clock_driver_set_service_region_rate_hz but
- * targets the user-region clock wizard.
+ * Behaves like @c clock_driver_set_service_region_rate_hz but targets the
+ * user-region clock wizard. When @p no_exceed is true, the driver only
+ * considers realizable rates less than or equal to the request, so the user
+ * clock never runs faster than the requested (timed) frequency.
*
* @param clk The clock driver instance.
* @param[in,out] rate_hz_inout On entry, the desired frequency in Hz.
* On exit, the actual frequency achieved.
+ * @param no_exceed If true, never select a rate above the request.
* @return 0 on success, -1 on error.
*/
-int clock_driver_set_user_region_rate_hz(struct clock_driver *clk, uint32_t *rate_hz_inout);
+int clock_driver_set_user_region_rate_hz(
+ struct clock_driver *clk,
+ uint32_t *rate_hz_inout,
+ bool no_exceed
+);
#endif // VRTD_CLOCK_H
diff --git a/vrt/vrtd/src/serve.c b/vrt/vrtd/src/serve.c
index cde03298..87a0f36d 100644
--- a/vrt/vrtd/src/serve.c
+++ b/vrt/vrtd/src/serve.c
@@ -3258,7 +3258,8 @@ static uint16_t client_handle_request_clock_op(
(unsigned int)req_body->dev_number);
return VRTD_RET_INTERNAL_ERROR;
}
- } else if (req_body->op == VRTD_CLOCK_OP_SET) {
+ } else if (req_body->op == VRTD_CLOCK_OP_SET
+ || req_body->op == VRTD_CLOCK_OP_SET_ROUND_DOWN) {
if (rate == 0) {
LOG(
LOG_WARNING,
@@ -3266,7 +3267,8 @@ static uint16_t client_handle_request_clock_op(
);
return VRTD_RET_INVALID_ARGUMENT;
}
- if (clock_driver_set_user_region_rate_hz(d->clock_driver, &rate) != 0) {
+ bool no_exceed = (req_body->op == VRTD_CLOCK_OP_SET_ROUND_DOWN);
+ if (clock_driver_set_user_region_rate_hz(d->clock_driver, &rate, no_exceed) != 0) {
LOG(
LOG_ERR,
"Failed to set user region frequency to %u Hz: %m",