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",