From 6c30473c06311047d68c8364d7967036d94b5d1a Mon Sep 17 00:00:00 2001 From: xuyipei Date: Sun, 8 Nov 2026 11:22:47 +0800 Subject: [PATCH] fix(ista): restore timing checks and refine parasitic delay analysis --- .../python/py_ista/py_ista_utils.cpp | 6 +- .../tcl/tcl_ista/src/tcl_init_sta.cpp | 5 +- .../iSTA/interface/MinTimingDataExporter.cpp | 56 ++-- src/operation/iSTA/interface/STAInterface.cpp | 16 +- src/operation/iSTA/interface/STAInterface.hpp | 2 +- .../interface/SparseTimingExportAdapter.cpp | 67 ++-- .../iSTA/interface/TimingDataExporter.cpp | 16 +- .../iSTA/source/data_manager/Config.hpp | 3 + .../iSTA/source/data_manager/DataManager.cpp | 4 + .../iSTA/source/data_manager/graph/Graph.hpp | 24 ++ .../data_manager/graph/TimingStateQuery.cpp | 13 +- .../data_manager/liberty/TimingCheckArc.hpp | 5 + .../data_manager/liberty/TimingCheckType.hpp | 10 +- .../module/StructuralTimingStateBuilder.cpp | 23 +- .../iSTA/source/module/TimingAnalyzer.cpp | 4 +- .../iSTA/source/module/TimingAnalyzer.hpp | 6 +- .../arnoldi/ArnoldiDelayCalculator.cpp | 48 ++- .../ArnoldiResponseCalculator.cpp | 2 + .../interconnect/DmpResponseCalculator.cpp | 2 + .../InputPortResponseCalculator.cpp | 2 + .../interconnect/LoadCalculator.cpp | 2 + .../interconnect/RcModelBuilder.cpp | 13 +- .../interconnect/RcTreeAnalyzer.cpp | 66 +++- .../interconnect/RcTreeAnalyzer.hpp | 11 + .../interconnect/WireDelayCalculator.cpp | 7 +- .../module/path_analyzer/PathGroupBuilder.cpp | 2 + .../source/module/reader/ReadParasitics.cpp | 63 +++- .../source/module/reader/ReadParasitics.hpp | 5 +- .../iSTA/source/module/sdc/SdcCommand.cpp | 12 +- .../iSTA/source/module/sdc/SdcTclCmd.cpp | 25 +- .../iSTA/source/module/sdc/SdcTclCmd.hpp | 4 + .../module/sdc/exception/SetFalsePath.cpp | 47 +++ .../module/timing_checker/CMakeLists.txt | 2 +- .../timing_checker/ClockGatingChecker.cpp | 169 ++++++++++ .../timing_checker/ClockGatingChecker.hpp | 18 + .../timing_checker/ElectricalChecker.cpp | 23 +- .../timing_checker/ElectricalChecker.hpp | 6 +- .../module/timing_checker/FanoutChecker.cpp | 3 + .../module/timing_checker/PathChecker.cpp | 122 ++++--- .../module/timing_checker/PathChecker.hpp | 19 +- .../timing_propagator/PathStatePruning.hpp | 12 +- .../timing_reporter/path/PathReportWriter.cpp | 3 +- .../path/ReportPathContext.cpp | 6 + src/operation/iSTA/test/CMakeLists.txt | 9 +- src/operation/iSTA/test/clock_gating_test.cpp | 308 ++++++++++++++++++ .../iSTA/test/driver_response_test.cpp | 299 +++++++++++++---- .../iSTA/test/electrical_check_test.cpp | 6 + src/operation/iSTA/test/path_check_test.cpp | 55 +++- src/operation/iSTA/test/read_config_test.cpp | 15 +- .../iSTA/test/read_parasitics_test.cpp | 121 +++++-- src/operation/iSTA/test/sdc_core_test.cpp | 33 ++ src/operation/iSTA/test/sdc_frontend_test.cpp | 57 +++- src/operation/iSTA/test/sdc_limits_test.cpp | 18 + .../iSTA/test/sdc_semantics_test.cpp | 51 ++- .../iSTA/test/sta_interface_test.cpp | 89 +++-- 55 files changed, 1690 insertions(+), 325 deletions(-) create mode 100644 src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.cpp create mode 100644 src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.hpp create mode 100644 src/operation/iSTA/test/clock_gating_test.cpp diff --git a/src/interface/python/py_ista/py_ista_utils.cpp b/src/interface/python/py_ista/py_ista_utils.cpp index 9d14e0afa..91201c928 100644 --- a/src/interface/python/py_ista/py_ista_utils.cpp +++ b/src/interface/python/py_ista/py_ista_utils.cpp @@ -57,7 +57,8 @@ bool initStaConfigMapByJSON(const std::string& config, std::map& config_dict, std if (config_dict.count("-timing_path_limit") > 0 && !config_dict["-timing_path_limit"].empty()) { config_map["-timing_path_limit"] = std::stoi(config_dict["-timing_path_limit"]); } - for (const std::string key : {"-min_slew_degradation", "-adjust_driver_resistance"}) { + for (const std::string key : + {"-min_slew_degradation", "-adjust_driver_resistance", "-small_rc_fanout_reduction", "-check_constant_capacitance"}) { const auto found = config_dict.find(key); if (found != config_dict.end() && !found->second.empty()) { const std::string& value = found->second; diff --git a/src/interface/tcl/tcl_ista/src/tcl_init_sta.cpp b/src/interface/tcl/tcl_ista/src/tcl_init_sta.cpp index e0736b2ab..884c4d9b4 100644 --- a/src/interface/tcl/tcl_ista/src/tcl_init_sta.cpp +++ b/src/interface/tcl/tcl_ista/src/tcl_init_sta.cpp @@ -38,6 +38,8 @@ TclInitSTA::TclInitSTA(const char* cmd_name) : TclCmd(cmd_name) // int32_t min_slew_degradation; // optional _config_list.push_back(std::make_pair("-min_slew_degradation", ValueType::kString)); _config_list.push_back(std::make_pair("-adjust_driver_resistance", ValueType::kString)); + _config_list.push_back(std::make_pair("-small_rc_fanout_reduction", ValueType::kString)); + _config_list.push_back(std::make_pair("-check_constant_capacitance", ValueType::kString)); // std::string timing_corner; // optional _config_list.push_back(std::make_pair("-timing_corner", ValueType::kString)); // int32_t max_paths; // optional @@ -60,7 +62,8 @@ unsigned TclInitSTA::exec() return 0; } std::map config_map = TclUtil::getConfigMap(this, _config_list); - for (const std::string key : {"-min_slew_degradation", "-adjust_driver_resistance"}) { + for (const std::string key : + {"-min_slew_degradation", "-adjust_driver_resistance", "-small_rc_fanout_reduction", "-check_constant_capacitance"}) { if (!config_map.contains(key)) continue; std::string value = std::any_cast(config_map.at(key)); diff --git a/src/operation/iSTA/interface/MinTimingDataExporter.cpp b/src/operation/iSTA/interface/MinTimingDataExporter.cpp index cfd149992..08003e9f7 100644 --- a/src/operation/iSTA/interface/MinTimingDataExporter.cpp +++ b/src/operation/iSTA/interface/MinTimingDataExporter.cpp @@ -17,11 +17,11 @@ #include "PinCapacitanceCalculator.hpp" #include "TimingData.hpp" #include "graph/TimingStateQuery.hpp" -#include "timing_checker/PathChecker.hpp" #include "liberty/TimingArc.hpp" #include "liberty/TimingCheckArc.hpp" #include "network/Instance.hpp" #include "network/Pin.hpp" +#include "timing_checker/PathChecker.hpp" namespace ista { namespace { @@ -46,8 +46,7 @@ bool hasMinCaptureState(Database& database, const std::string& pin_name, TransTy for (const auto& [clock_name, state] : point->second.get_clock_state_map()) { if (state.is_source_path_only || !database.sdc().get_clock_map().contains(clock_name)) continue; - if (state.is_structural - && !clockPinValues(database, pin_name, clock_name, AnalysisType::kMin, transition).propagated) + if (state.is_structural && !clockPinValues(database, pin_name, clock_name, AnalysisType::kMin, transition).propagated) return true; const auto value = state.arrival(AnalysisType::kMin, transition); if (value.has_value() && std::isfinite(*value)) @@ -93,8 +92,8 @@ bool hasCheckTable(const LibertyArcData& arc, std::size_t edge) return edge == 0 ? !arc.rise_check.values.empty() : !arc.fall_check.values.empty(); } -TimingMinQualification qualifyMinCheck(Database& database, const TimingEndpointArcData& endpoint, - const TimingData& snapshot, const std::set& active_checks) +TimingMinQualification qualifyMinCheck(Database& database, const TimingEndpointArcData& endpoint, const TimingData& snapshot, + const std::set& active_checks) { const auto check_type = static_cast(endpoint.check_type); if (check_type != TimingCheckType::kHold && check_type != TimingCheckType::kRemoval) @@ -102,11 +101,8 @@ TimingMinQualification qualifyMinCheck(Database& database, const TimingEndpointA if (!hasMinCaptureState(database, endpoint.source_pin, endpoint.timing_type == 1 ? TransType::kRise : TransType::kFall)) return excluded(TimingMinQualificationReason::kNoCaptureClockState); auto result = excluded(TimingMinQualificationReason::kInactiveCheck); - const auto cell = std::find_if(snapshot.lib_cells.begin(), snapshot.lib_cells.end(), [&](const auto& value) { - return value.name == endpoint.library_cell; - }); - if (cell == snapshot.lib_cells.end() || endpoint.library_arc_id < 0 - || static_cast(endpoint.library_arc_id) >= cell->arcs.size()) + const auto cell = std::find_if(snapshot.lib_cells.begin(), snapshot.lib_cells.end(), [&](const auto& value) { return value.name == endpoint.library_cell; }); + if (cell == snapshot.lib_cells.end() || endpoint.library_arc_id < 0 || static_cast(endpoint.library_arc_id) >= cell->arcs.size()) return excluded(TimingMinQualificationReason::kInactiveCheck); const auto& arc = cell->arcs[endpoint.library_arc_id]; for (std::size_t edge = 0; edge < 2; ++edge) { @@ -115,8 +111,7 @@ TimingMinQualification qualifyMinCheck(Database& database, const TimingEndpointA continue; } auto active = [&](int32_t capture_transition) { - return active_checks.contains({endpoint.source_pin, endpoint.sink_pin, endpoint.check_type, - edge == 0 ? 1 : -1, capture_transition}); + return active_checks.contains({endpoint.source_pin, endpoint.sink_pin, endpoint.check_type, edge == 0 ? 1 : -1, capture_transition}); }; const bool legal_context = endpoint.timing_type == 0 ? active(1) || active(-1) : active(endpoint.timing_type); result.valid[edge] = hasCheckTable(arc, edge) && legal_context; @@ -139,8 +134,7 @@ void exportMinTiming(TimingData& snapshot, Database& database, const Config& con pin.min_rise_aat_ps = pin.min_fall_aat_ps = 9.0e7; pin.min_rise_rat_ps = pin.min_fall_rat_ps = -9.0e7; pins.emplace(pin.name, &pin); - if (pin.is_port && (pin.direction == static_cast(PinDirection::kOutput) - || pin.direction == static_cast(PinDirection::kInout))) { + if (pin.is_port && (pin.direction == static_cast(PinDirection::kOutput) || pin.direction == static_cast(PinDirection::kInout))) { // HoldRC adds these to endpoint pin caps before summing the net load. // Export only this output port's external load, never the other sinks. pin.min_output_rise_load_pf = pin_capacitance.pinCapacitance(pin.name, AnalysisType::kMin, TransType::kRise) * snapshot.capacitance_unit_pf; @@ -177,7 +171,7 @@ void exportMinTiming(TimingData& snapshot, Database& database, const Config& con } if (start_points.contains(name)) { const auto key = std::make_tuple(name, path.get_clock_name(), path.get_path_state_tag(), static_cast(source_type), - transitionCode(transition), transitionCode(path.get_launch_clock_trans_type())); + transitionCode(transition), transitionCode(path.get_launch_clock_trans_type())); if (launch_keys.insert(key).second) { TimingMinLaunchData launch; launch.start_point = name; @@ -191,18 +185,18 @@ void exportMinTiming(TimingData& snapshot, Database& database, const Config& con launch.slew_ps = path.get_slew() * snapshot.time_unit_ps; launch.launch_time_ps = path.get_launch_time() * snapshot.time_unit_ps; if (!launch.launch_pin.empty()) - launch.launch_slew_ps = query.getClockSlew(launch.launch_pin, launch.launch_clock, AnalysisType::kMin, - path.get_launch_clock_trans_type(), path.get_launch_clock_trans_type()) - * snapshot.time_unit_ps; + launch.launch_slew_ps = query.getClockSlew(launch.launch_pin, launch.launch_clock, AnalysisType::kMin, path.get_launch_clock_trans_type(), + path.get_launch_clock_trans_type()) + * snapshot.time_unit_ps; snapshot.min_launches.push_back(std::move(launch)); } } if (!endpoints.contains(name)) continue; const auto endpoint_pin = database.network().pins().find(name); - const bool is_output_port = endpoint_pin != database.network().pins().end() && endpoint_pin->second.get_is_port() - && (endpoint_pin->second.get_direction() == PinDirection::kOutput - || endpoint_pin->second.get_direction() == PinDirection::kInout); + const bool is_output_port + = endpoint_pin != database.network().pins().end() && endpoint_pin->second.get_is_port() + && (endpoint_pin->second.get_direction() == PinDirection::kOutput || endpoint_pin->second.get_direction() == PinDirection::kInout); if (is_output_port && !hasLaunchClockState(database, path)) continue; const auto checked = checker.checkPathCandidates(path, name, AnalysisType::kMin); @@ -213,6 +207,8 @@ void exportMinTiming(TimingData& snapshot, Database& database, const Config& con // consumers must retain the context before endpoint reduction. for (const auto& candidate : checked) { const auto& check = candidate.check(); + if (check.type == TimingCheckType::kClockGatingHold) + continue; // schema 2 consumers require a Liberty hold LUT. TimingMinContextData context; context.start_point = path.get_start_point(); context.end_point = name; @@ -238,9 +234,9 @@ void exportMinTiming(TimingData& snapshot, Database& database, const Config& con context.uncertainty_ps = check.uncertainty * snapshot.time_unit_ps; context.cppr_ps = check.reconvergence * snapshot.time_unit_ps; if (!check.capture_pin.empty()) - context.capture_slew_ps = query.getClockSlew(check.capture_pin, check.capture_clock, AnalysisType::kMin, - check.capture_pin_transition, check.capture_transition) - * snapshot.time_unit_ps; + context.capture_slew_ps + = query.getClockSlew(check.capture_pin, check.capture_clock, AnalysisType::kMin, check.capture_pin_transition, check.capture_transition) + * snapshot.time_unit_ps; if (!context.launch_pin.empty()) context.launch_clock_slew_ps = query.getClockSlew(context.launch_pin, context.launch_clock, AnalysisType::kMin, path.get_launch_clock_trans_type(), path.get_launch_clock_trans_type()) @@ -291,13 +287,17 @@ void exportMinTiming(TimingData& snapshot, Database& database, const Config& con std::set active_checks; for (const auto& context : snapshot.min_contexts) if (context.check_type != 0) - active_checks.emplace(context.capture_pin, context.end_point, context.check_type, - context.data_transition, context.capture_pin_transition); + active_checks.emplace(context.capture_pin, context.end_point, context.check_type, context.data_transition, context.capture_pin_transition); for (auto& arc : snapshot.timing_check_arcs) arc.min_qualification = qualifyMinCheck(database, arc, snapshot, active_checks); for (auto& arc : snapshot.constraint_arcs) arc.min_qualification = qualifyMinCheck(database, arc, snapshot, active_checks); for (auto& pin : snapshot.pins) { + if (database.graph().clockGatingChecks().contains(pin.name)) { + pin.min_qualification.valid = {false, false}; + pin.min_qualification.reason = {TimingMinQualificationReason::kUnsupportedConstraint, TimingMinQualificationReason::kUnsupportedConstraint}; + continue; + } if (pin.is_port) { auto output = qualifyMinOutput(database, pin.name); const bool has_output_context = pin.min_qualification.valid[0] || pin.min_qualification.valid[1]; @@ -317,8 +317,8 @@ void exportMinTiming(TimingData& snapshot, Database& database, const Config& con pin.min_qualification.valid[edge] = false; pin.min_qualification.reason[edge] = TimingMinQualificationReason::kDisabledTransition; } else if (pin.min_qualification.valid[edge] && !pin.min_path_valid[edge] - && !(pin.is_port && (pin.direction == static_cast(PinDirection::kOutput) - || pin.direction == static_cast(PinDirection::kInout)))) { + && !(pin.is_port + && (pin.direction == static_cast(PinDirection::kOutput) || pin.direction == static_cast(PinDirection::kInout)))) { pin.min_qualification.valid[edge] = false; pin.min_qualification.reason[edge] = TimingMinQualificationReason::kNoMinPathState; } diff --git a/src/operation/iSTA/interface/STAInterface.cpp b/src/operation/iSTA/interface/STAInterface.cpp index f0f07e892..c712bdab4 100644 --- a/src/operation/iSTA/interface/STAInterface.cpp +++ b/src/operation/iSTA/interface/STAInterface.cpp @@ -146,7 +146,7 @@ bool STAInterface::initSTA(std::map config_map) bool STAInterface::initialize(std::map config_map, InputScope scope) { - for (const std::string key : {"-min_slew_degradation", "-adjust_driver_resistance"}) { + for (const std::string key : {"-min_slew_degradation", "-adjust_driver_resistance", "-small_rc_fanout_reduction", "-check_constant_capacitance"}) { if (config_map.contains(key)) { int32_t value = std::any_cast(config_map.at(key)); if (value != 0 && value != 1) @@ -175,7 +175,7 @@ bool STAInterface::initialize(std::map config_map, InputS STALOG.info(Loc::current(), "Starting..."); DataManager::initInst(); - readDesign(config_map, scope); + readDesign(config_map); DelayCalculator::initInst(); SdcCommand::initInst(); sdc::registerSdcCommands(SdcCommand::getInst()); @@ -184,6 +184,8 @@ bool STAInterface::initialize(std::map config_map, InputS if (!file.empty()) readHierarchy(STADM.getDatabase().network(), *dmInst->get_idb_layout(), file); } + if (scope == InputScope::kTiming) + ReadParasitics(STADM.getDatabase().parasitics(), STADM.getDatabase().network()).read(); // Simulation values are derived from constraints and are rebuilt by analysis. STADM.getDatabase().graph().simValues().clear(); if (!readSdc(STADM.getDatabase().sdc())) { @@ -220,7 +222,7 @@ void STAInterface::prepareTimingModel() if (!_initialized) { throw std::runtime_error("STA must be initialized before prepareTimingModel"); } - TimingAnalyzer analyzer(STADM.getDatabase(), STADC); + TimingAnalyzer analyzer(STADM.getDatabase(), STADC, STADM.getConfig().check_constant_capacitance != 0); if (_model_ready && analyzer.isStructureCurrent()) { return; } @@ -245,7 +247,7 @@ void STAInterface::runSTA() STALOG.info(Loc::current(), "Starting..."); try { - TimingAnalyzer(STADM.getDatabase(), STADC).analyze(_model_ready); + TimingAnalyzer(STADM.getDatabase(), STADC, STADM.getConfig().check_constant_capacitance != 0).analyze(_model_ready); TimingReporter(STADM.getDatabase(), STADM.getConfig()).report(); @@ -284,7 +286,7 @@ void STAInterface::extractLib() STALOG.info(Loc::current(), "Starting..."); try { - TimingAnalyzer(STADM.getDatabase(), STADC).analyze(); + TimingAnalyzer(STADM.getDatabase(), STADC, STADM.getConfig().check_constant_capacitance != 0).analyze(); auto timing_model = ModelExtractor(STADM.getDatabase()).extract(); std::string output_path = STADM.getConfig().tc_temp_directory_path; @@ -392,7 +394,7 @@ ClockTreeGraphData STAInterface::exportClockTreeGraph() return ista::exportClockTreeGraph(STADM.getDatabase()); } -void STAInterface::readDesign(std::map& config_map, InputScope scope) +void STAInterface::readDesign(std::map& config_map) { Monitor monitor; STALOG.info(Loc::current(), "Starting..."); @@ -400,8 +402,6 @@ void STAInterface::readDesign(std::map& config_map, Input STADM.getDatabase().sdc().set_sdc_file_path(dmInst->get_config().get_sdc_path()); readNetwork(STADM.getDatabase().network()); ReadLiberty(STADM.getDatabase().liberty(), STADM.getDatabase().network()).read(); - if (scope == InputScope::kTiming) - ReadParasitics(STADM.getDatabase().parasitics()).read(); STADM.buildDesign(); STALOG.info(Loc::current(), "Completed", monitor.getStatsInfo()); } diff --git a/src/operation/iSTA/interface/STAInterface.hpp b/src/operation/iSTA/interface/STAInterface.hpp index 2c63749eb..eac9343ad 100644 --- a/src/operation/iSTA/interface/STAInterface.hpp +++ b/src/operation/iSTA/interface/STAInterface.hpp @@ -54,7 +54,7 @@ class STAInterface kConstraints }; bool initialize(std::map config_map, InputScope scope); - void readDesign(std::map& config_map, InputScope scope); + void readDesign(std::map& config_map); bool _initialized = false; bool _model_ready = false; diff --git a/src/operation/iSTA/interface/SparseTimingExportAdapter.cpp b/src/operation/iSTA/interface/SparseTimingExportAdapter.cpp index 5977b92b4..d2e4aede8 100644 --- a/src/operation/iSTA/interface/SparseTimingExportAdapter.cpp +++ b/src/operation/iSTA/interface/SparseTimingExportAdapter.cpp @@ -14,14 +14,14 @@ #include "DelayCalculator.hpp" #include "Logger.hpp" #include "Monitor.hpp" -#include "liberty/LibertyCell.hpp" -#include "liberty/TimingArc.hpp" -#include "liberty/TimingCheckArc.hpp" #include "NetThresholdAdjustment.hpp" -#include "SparseTimingSnapshot.hpp" #include "SparseElectricalExport.hpp" +#include "SparseTimingSnapshot.hpp" #include "TimingData.hpp" #include "graph/TimingStateQuery.hpp" +#include "liberty/LibertyCell.hpp" +#include "liberty/TimingArc.hpp" +#include "liberty/TimingCheckArc.hpp" #include "path/PathEnd.hpp" #include "sdc/ExceptionMatcher.hpp" #include "timing_checker/PathChecker.hpp" @@ -49,8 +49,7 @@ struct EndpointArcHash std::size_t operator()(const EndpointArcKey& key) const { auto hash = std::hash{}(key.sink); - for (auto value : {std::hash{}(key.source), std::hash{}(key.check_type), - std::hash{}(key.library_arc)}) + for (auto value : {std::hash{}(key.source), std::hash{}(key.check_type), std::hash{}(key.library_arc)}) hash ^= value + 0x9e3779b9 + (hash << 6) + (hash >> 2); return hash; } @@ -100,8 +99,8 @@ std::string localPortName(const std::string& pin_name) return separator == std::string::npos ? pin_name : pin_name.substr(separator + 1); } -int32_t selectedCheckLibraryArcId(Database& database, const std::string& data_pin, const std::string& clock_pin, - TimingCheckType check_type, TransType clock_transition, int32_t selected_arc_idx) +int32_t selectedCheckLibraryArcId(Database& database, const std::string& data_pin, const std::string& clock_pin, TimingCheckType check_type, + TransType clock_transition, int32_t selected_arc_idx) { if (selected_arc_idx < 0) return -1; @@ -187,6 +186,8 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal STALOG.info(Loc::current(), "Compiling ", early ? "min" : "max", " sparse timing snapshot..."); view.enabled = true; view.early = early; + for (const auto& [endpoint, checks] : database.graph().clockGatingChecks()) + view.unsupported.push_back("inferred clock-gating check requires a gate-window rule at " + endpoint); if (!early) snapshot.schema_version = 3; auto& exceptions = database.sdc().get_path_exception_list(); @@ -287,10 +288,8 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal // tagged graph from a register clock-to-Q arc. const auto pin = database.network().pins().find(name); const auto port_constraint = database.sdc().get_port_constraint_map().find(name); - if (pin != database.network().pins().end() && pin->second.get_is_port() - && port_constraint != database.sdc().get_port_constraint_map().end() - && (early ? port_constraint->second.get_has_input_delay_min() : port_constraint->second.get_has_input_delay_max()) - && state.source_type != 2) { + if (pin != database.network().pins().end() && pin->second.get_is_port() && port_constraint != database.sdc().get_port_constraint_map().end() + && (early ? port_constraint->second.get_has_input_delay_min() : port_constraint->second.get_has_input_delay_max()) && state.source_type != 2) { state.seed = state.valid; } pin_states[name].emplace(key, view.states.size()); @@ -301,11 +300,9 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal }; collectStates(database.graph().points(), 0); collectStates(database.graph().driverPoints(), 1); - STALOG.info(Loc::current(), "Sparse ", early ? "min" : "max", " states collected: states=", view.states.size(), - ", tags=", view.tags.size(), ", launch_tags=", view.launch_tags.size(), "; phase ", monitor.getStatsInfo()); - const auto peak = std::max_element(pin_states.begin(), pin_states.end(), [](const auto& a, const auto& b) { - return a.second.size() < b.second.size(); - }); + STALOG.info(Loc::current(), "Sparse ", early ? "min" : "max", " states collected: states=", view.states.size(), ", tags=", view.tags.size(), + ", launch_tags=", view.launch_tags.size(), "; phase ", monitor.getStatsInfo()); + const auto peak = std::max_element(pin_states.begin(), pin_states.end(), [](const auto& a, const auto& b) { return a.second.size() < b.second.size(); }); if (peak != pin_states.end()) { STALOG.info(Loc::current(), "Sparse highest-state pin: ", peak->first, ", states=", peak->second.size()); int samples = 0; @@ -413,17 +410,18 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal for (int output = 0; output < 2; ++output) { bool legal = input == output; if constexpr (std::is_same_v, TimingGraphArcData>) { - legal = (arc.sense == 1 ? input == output : arc.sense == -1 ? input != output : true) + legal = (arc.sense == 1 ? input == output + : arc.sense == -1 ? input != output + : true) && (arc.timing_type == 0 || arc.timing_type == (input == 0 ? 1 : -1)); } if (!legal) continue; const auto progress = matcher.advanceState(through_index, path->get_exception_state_list(), arc.sink_pin, - output == 0 ? TransType::kRise : TransType::kFall, - cell ? "" : arc.source_pin); + output == 0 ? TransType::kRise : TransType::kFall, cell ? "" : arc.source_pin); StateKey sink_key = key; std::get<2>(sink_key) = matcher.makePathStateTag(path->get_clock_name(), path->get_launch_clock_trans_type(), progress, - path->get_is_clock_source_path(), path->crprPathTag(), path->get_launch_clock_edge_index()); + path->get_is_clock_source_path(), path->crprPathTag(), path->get_launch_clock_edge_index()); std::get<3>(sink_key) = sink_role; auto sink = sinks->second.find(sink_key); if (sink == sinks->second.end()) @@ -493,8 +491,8 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal view.net_threshold_adjustments.push_back(netThresholdAdjustment(database, arc.source_pin, arc.sink_pin)); } appendSparseElectricalEdges(snapshot, database, view); - STALOG.info(Loc::current(), "Sparse ", early ? "min" : "max", " edges collected: cell=", view.cell_edges.size(), - ", net=", view.net_edges.size(), ", slew_only=", view.slew_only_cell_edges.size(), "; phase ", monitor.getStatsInfo()); + STALOG.info(Loc::current(), "Sparse ", early ? "min" : "max", " edges collected: cell=", view.cell_edges.size(), ", net=", view.net_edges.size(), + ", slew_only=", view.slew_only_cell_edges.size(), "; phase ", monitor.getStatsInfo()); // Some output ports are represented as virtual timing endpoints with no DEF // net row. Their native path arrival is already the launch-side electrical @@ -509,8 +507,7 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal const auto constraint = database.sdc().get_port_constraint_map().find(state.pin); if (pin != database.network().pins().end() && pin->second.get_is_port() && state.source_type == 2 && constraint != database.sdc().get_port_constraint_map().end() - && (early ? constraint->second.get_has_output_delay_min() : constraint->second.get_has_output_delay_max()) - && !net_destinations.contains(state_id)) { + && (early ? constraint->second.get_has_output_delay_min() : constraint->second.get_has_output_delay_max()) && !net_destinations.contains(state_id)) { state.seed = state.valid; } } @@ -547,6 +544,10 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal continue; const auto& check = endpoint->check(); + if (check.type == TimingCheckType::kClockGatingSetup) { + view.unsupported.push_back("inferred clock-gating check requires a gate-window rule at " + state.pin); + continue; + } const std::string capture = endpoint->captureClock(); if (!domains.contains(capture)) { view.unsupported.push_back("unknown capture clock at " + state.pin); @@ -562,14 +563,13 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal if (arc != endpoint_arcs.end()) endpoint_arc = arc->second; if (endpoint_arc < 0) { - view.unsupported.push_back(selected_library_arc < 0 ? "missing selected max check LUT at " + state.pin - : "missing max check LUT at " + state.pin); + view.unsupported.push_back(selected_library_arc < 0 ? "missing selected max check LUT at " + state.pin : "missing max check LUT at " + state.pin); continue; } } - const auto resolved = matcher.resolve(path.get_start_point(), path.get_clock_name(), state.pin, capture, AnalysisType::kMax, - path.get_trans_type(), check.capture_transition, path.get_exception_state_list()); + const auto resolved = matcher.resolve(path.get_start_point(), path.get_clock_name(), state.pin, capture, AnalysisType::kMax, path.get_trans_type(), + check.capture_transition, path.get_exception_state_list()); SparseTimingCheck row; row.state = state_id; row.capture_domain = domains.at(capture); @@ -587,11 +587,10 @@ void compileSparse(TimingData& snapshot, bool force, bool structural_model, Anal row.constraint = static_cast(chosen - exceptions.data()); if (row.valid) { row.check_coefficient = check.capture_pin.empty() ? 0.0 : -1.0; - row.clock_slew_ps = check.capture_pin.empty() - ? 0.0 - : state_query.getClockSlew(check.capture_pin, capture, AnalysisType::kMax, check.capture_pin_transition, - check.capture_transition) - * snapshot.time_unit_ps; + row.clock_slew_ps = check.capture_pin.empty() ? 0.0 + : state_query.getClockSlew(check.capture_pin, capture, AnalysisType::kMax, check.capture_pin_transition, + check.capture_transition) + * snapshot.time_unit_ps; if (structural_model && !check.capture_pin.empty()) { const auto clock_pin = pin_ids.find(check.capture_pin); if (clock_pin != pin_ids.end()) { diff --git a/src/operation/iSTA/interface/TimingDataExporter.cpp b/src/operation/iSTA/interface/TimingDataExporter.cpp index 75ed1c57e..49d3ab606 100644 --- a/src/operation/iSTA/interface/TimingDataExporter.cpp +++ b/src/operation/iSTA/interface/TimingDataExporter.cpp @@ -434,8 +434,7 @@ TimingData TimingDataExporter::exportData(Database& database, const Config& conf continue; } const auto current_pin = database.network().pins().find(current->second); - if ((current_pin == database.network().pins().end() || current_pin->second.get_net_name().empty()) - && !sink->second.get_net_name().empty()) { + if ((current_pin == database.network().pins().end() || current_pin->second.get_net_name().empty()) && !sink->second.get_net_name().empty()) { current->second = arc.sink_pin; } } @@ -501,6 +500,19 @@ TimingData TimingDataExporter::exportData(Database& database, const Config& conf } } + // The external LUT-based schema has no inferred gate-check representation. + // Keep its endpoint identity and reject qualification explicitly instead of + // exporting a zero/missing Liberty LUT as a valid optimization constraint. + for (const auto& [endpoint, checks] : database.graph().clockGatingChecks()) { + addDiagnostic("inferred clock-gating check is not supported by the external timing model at " + endpoint); + auto pin = pin_ids.find(endpoint); + if (pin != pin_ids.end()) { + auto& qualification = snapshot.pins.at(pin->second).max_qualification; + qualification.valid = {false, false}; + qualification.reason = {TimingMaxQualificationReason::kUnsupportedConstraint, TimingMaxQualificationReason::kUnsupportedConstraint}; + } + } + if (used_non_finite_fallback) { addDiagnostic("non-finite timing values use finite AAT/RAT sentinels (-9e7/9e7 ps)"); } diff --git a/src/operation/iSTA/source/data_manager/Config.hpp b/src/operation/iSTA/source/data_manager/Config.hpp index 2f08b413c..696eada8b 100644 --- a/src/operation/iSTA/source/data_manager/Config.hpp +++ b/src/operation/iSTA/source/data_manager/Config.hpp @@ -37,6 +37,9 @@ class Config int32_t timing_path_limit = 20; int32_t min_slew_degradation = 0; int32_t adjust_driver_resistance = 1; + // Keep the existing PT2018-calibrated approximation available explicitly. + int32_t small_rc_fanout_reduction = 1; + int32_t check_constant_capacitance = 0; std::string timing_corner; bool is_path_report_number_specified = false; bool has_timing_report_slack_lesser_than = false; diff --git a/src/operation/iSTA/source/data_manager/DataManager.cpp b/src/operation/iSTA/source/data_manager/DataManager.cpp index 2250ab6e8..cf6df6333 100644 --- a/src/operation/iSTA/source/data_manager/DataManager.cpp +++ b/src/operation/iSTA/source/data_manager/DataManager.cpp @@ -72,6 +72,8 @@ void DataManager::readConfig(std::map& config_map) config.timing_path_limit = STAUTIL.getConfigValue(config_map, "-timing_path_limit", 20); config.min_slew_degradation = STAUTIL.getConfigValue(config_map, "-min_slew_degradation", 0); config.adjust_driver_resistance = STAUTIL.getConfigValue(config_map, "-adjust_driver_resistance", 1); + config.small_rc_fanout_reduction = STAUTIL.getConfigValue(config_map, "-small_rc_fanout_reduction", 1); + config.check_constant_capacitance = STAUTIL.getConfigValue(config_map, "-check_constant_capacitance", 0); config.timing_corner = STAUTIL.getConfigValue(config_map, "-timing_corner", ""); config.is_path_report_number_specified = STAUTIL.exist(config_map, std::string("-max_paths")) || STAUTIL.exist(config_map, std::string("-max_path")) || STAUTIL.exist(config_map, std::string("-path_report_number")); @@ -203,6 +205,8 @@ void DataManager::printConfig() STALOG.info(Loc::current(), STAUTIL.getSpaceByTabNum(2), _config.min_slew_degradation); STALOG.info(Loc::current(), STAUTIL.getSpaceByTabNum(1), "adjust_driver_resistance"); STALOG.info(Loc::current(), STAUTIL.getSpaceByTabNum(2), _config.adjust_driver_resistance); + STALOG.info(Loc::current(), " small_rc_fanout_reduction: ", _config.small_rc_fanout_reduction); + STALOG.info(Loc::current(), " check_constant_capacitance: ", _config.check_constant_capacitance); STALOG.info(Loc::current(), STAUTIL.getSpaceByTabNum(1), "timing_path_limit"); STALOG.info(Loc::current(), STAUTIL.getSpaceByTabNum(2), _config.timing_path_limit); // ********** STA ********** // diff --git a/src/operation/iSTA/source/data_manager/graph/Graph.hpp b/src/operation/iSTA/source/data_manager/graph/Graph.hpp index 2f1873b85..400cb736c 100644 --- a/src/operation/iSTA/source/data_manager/graph/Graph.hpp +++ b/src/operation/iSTA/source/data_manager/graph/Graph.hpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MulanPSL-2.0 #pragma once +#include #include #include #include @@ -12,6 +13,7 @@ #include "graph/Arc.hpp" #include "graph/SimValues.hpp" #include "graph/TimingPoint.hpp" +#include "liberty/TimingCheckArc.hpp" namespace ista { @@ -59,6 +61,8 @@ class Graph _incoming_arcs(other._incoming_arcs), _startpoints(other._startpoints), _endpoints(other._endpoints), + _clock_gating_checks(other._clock_gating_checks), + _clock_gating_endpoints(other._clock_gating_endpoints), _timing_order(other._timing_order), _points(other._points), _path_states_pruned(other._path_states_pruned), @@ -103,6 +107,23 @@ class Graph const std::vector& startpoints() const { return _startpoints; } std::vector& endpoints() { return _endpoints; } const std::vector& endpoints() const { return _endpoints; } + std::map>& clockGatingChecks() { return _clock_gating_checks; } + const std::map>& clockGatingChecks() const { return _clock_gating_checks; } + void addClockGatingCheck(TimingCheckArc check) + { + const auto& endpoint = check.get_data_port(); + if (std::find(_endpoints.begin(), _endpoints.end(), endpoint) == _endpoints.end()) { + _endpoints.push_back(endpoint); + _clock_gating_endpoints.insert(endpoint); + } + _clock_gating_checks[endpoint].push_back(std::move(check)); + } + void clearClockGatingChecks() + { + std::erase_if(_endpoints, [&](const auto& endpoint) { return _clock_gating_endpoints.contains(endpoint); }); + _clock_gating_checks.clear(); + _clock_gating_endpoints.clear(); + } std::vector& timingOrder() { return _timing_order; } const std::vector& timingOrder() const { return _timing_order; } std::map& points() { return _points; } @@ -138,6 +159,7 @@ class Graph // Clear propagated timing at both receiving and driving vertices. void clearTiming() { + clearClockGatingChecks(); _path_states_pruned = false; _capture_required_clocks.clear(); _exhaustive_required = false; @@ -168,6 +190,8 @@ class Graph std::map> _incoming_arcs; std::vector _startpoints; std::vector _endpoints; + std::map> _clock_gating_checks; + std::set _clock_gating_endpoints; std::vector _timing_order; std::map _points; bool _path_states_pruned = false; diff --git a/src/operation/iSTA/source/data_manager/graph/TimingStateQuery.cpp b/src/operation/iSTA/source/data_manager/graph/TimingStateQuery.cpp index b22ca291a..eeee2412c 100644 --- a/src/operation/iSTA/source/data_manager/graph/TimingStateQuery.cpp +++ b/src/operation/iSTA/source/data_manager/graph/TimingStateQuery.cpp @@ -297,7 +297,7 @@ ClockPinValues clockPinValues(const Database& database, const std::string& pin, } const ClockPinValues& ClockPinValuesQuery::values(const std::string& pin, std::string_view clock, AnalysisType analysis, TransType transition, - AnalysisType check, std::optional source) + AnalysisType check, std::optional source) { if (_mode == CacheMode::kNone) { _live = clockPinValues(_database, pin, clock, analysis, transition, check, source); @@ -306,8 +306,8 @@ const ClockPinValues& ClockPinValuesQuery::values(const std::string& pin, std::s const auto key = std::tuple{std::string_view(pin), clock, analysis, transition, check, source}; if (const auto found = _values.find(key); found != _values.end()) return found->second; - return _values.emplace(Key{pin, clock, analysis, transition, check, source}, - clockPinValues(_database, pin, clock, analysis, transition, check, source)).first->second; + return _values.emplace(Key{pin, clock, analysis, transition, check, source}, clockPinValues(_database, pin, clock, analysis, transition, check, source)) + .first->second; } double TimingStateQuery::clockPathBound(const std::string& pin_name, std::string_view clock_name, TransType transition, std::optional source) const @@ -400,9 +400,8 @@ bool TimingStateQuery::prunePathStates(TimingPoint& point, std::size_t remaining for (auto& [edge, states] : edges) { // A directly seeded state can carry a different slew. Its endpoint // check need not match the physical slew of arriving states. - if (!states.empty() && std::any_of(states.begin(), states.end(), [&](const auto& item) { - return item.second.get_slew() != states.begin()->second.get_slew(); - })) + if (!states.empty() + && std::any_of(states.begin(), states.end(), [&](const auto& item) { return item.second.get_slew() != states.begin()->second.get_slew(); })) continue; std::map witnesses; for (const auto& [tag, state] : states) { @@ -668,6 +667,8 @@ bool TimingStateQuery::isRegisterEndPoint(const std::string& end_point) bool TimingStateQuery::isTimingCheckEndPoint(const std::string& end_point) { + if (_database.graph().clockGatingChecks().contains(end_point)) + return true; Database& database = _database; Pin& pin = database.network().pins()[end_point]; if (pin.get_is_port() || database.network().instances().count(pin.get_instance_name()) == 0) { diff --git a/src/operation/iSTA/source/data_manager/liberty/TimingCheckArc.hpp b/src/operation/iSTA/source/data_manager/liberty/TimingCheckArc.hpp index 99dedbaa2..5166c4261 100644 --- a/src/operation/iSTA/source/data_manager/liberty/TimingCheckArc.hpp +++ b/src/operation/iSTA/source/data_manager/liberty/TimingCheckArc.hpp @@ -39,6 +39,9 @@ class TimingCheckArc const std::vector& get_timing_arc_list() const { return _timing_arc_list; } std::vector& get_timing_arc_list() { return _timing_arc_list; } TransType get_clock_trans_type() const { return _clock_trans_type; } + // Empty for linked Liberty checks; inferred checks belong to one analyzed + // clock cone and must not acquire other clocks present on the same pin. + const std::string& captureClock() const { return _capture_clock; } // setter void set_clock_port(const std::string& clock_port) { _clock_port = clock_port; } void set_data_port(const std::string& data_port) { _data_port = data_port; } @@ -46,6 +49,7 @@ class TimingCheckArc void set_check_time(const double check_time) { _check_time = check_time; } void set_timing_arc_list(const std::vector& timing_arc_list) { _timing_arc_list = timing_arc_list; } void set_clock_trans_type(const TransType& clock_trans_type) { _clock_trans_type = clock_trans_type; } + void setCaptureClock(const std::string& clock) { _capture_clock = clock; } // function private: @@ -55,6 +59,7 @@ class TimingCheckArc double _check_time = 0.0; std::vector _timing_arc_list; TransType _clock_trans_type = TransType::kRise; + std::string _capture_clock; }; } // namespace ista diff --git a/src/operation/iSTA/source/data_manager/liberty/TimingCheckType.hpp b/src/operation/iSTA/source/data_manager/liberty/TimingCheckType.hpp index f3e7f7743..b7d2de9cb 100644 --- a/src/operation/iSTA/source/data_manager/liberty/TimingCheckType.hpp +++ b/src/operation/iSTA/source/data_manager/liberty/TimingCheckType.hpp @@ -30,7 +30,9 @@ enum class TimingCheckType kRecovery, kRemoval, kWidth, - kPeriod + kPeriod, + kClockGatingSetup, + kClockGatingHold }; struct GetTimingCheckTypeName @@ -60,6 +62,12 @@ struct GetTimingCheckTypeName case TimingCheckType::kPeriod: timing_check_type_name = "period"; break; + case TimingCheckType::kClockGatingSetup: + timing_check_type_name = "clock_gating_setup"; + break; + case TimingCheckType::kClockGatingHold: + timing_check_type_name = "clock_gating_hold"; + break; default: STALOG.error(Loc::current(), "Unrecognized type!"); break; diff --git a/src/operation/iSTA/source/module/StructuralTimingStateBuilder.cpp b/src/operation/iSTA/source/module/StructuralTimingStateBuilder.cpp index 3a21f0002..48f4c57b0 100644 --- a/src/operation/iSTA/source/module/StructuralTimingStateBuilder.cpp +++ b/src/operation/iSTA/source/module/StructuralTimingStateBuilder.cpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MulanPSL-2.0 #include "StructuralTimingStateBuilder.hpp" -#include "StartpointInitializer.hpp" #include #include @@ -8,20 +7,22 @@ #include #include +#include "ClockGatingChecker.hpp" #include "Database.hpp" +#include "StartpointInitializer.hpp" #include "graph/Arc.hpp" #include "graph/Graph.hpp" #include "graph/TimingPoint.hpp" #include "graph/TimingStateQuery.hpp" +#include "graph_builder/GraphBuilder.hpp" #include "liberty/LibertyCell.hpp" #include "liberty/LibertyLibrary.hpp" #include "liberty/TimingArc.hpp" #include "liberty/TimingCellArc.hpp" -#include "graph_builder/GraphBuilder.hpp" -#include "timing_propagator/clock/ClockNetworkBuilder.hpp" -#include "timing_propagator/data/TransitionFilter.hpp" #include "network/Network.hpp" #include "sdc/ExceptionMatcher.hpp" +#include "timing_propagator/clock/ClockNetworkBuilder.hpp" +#include "timing_propagator/data/TransitionFilter.hpp" namespace ista { namespace { @@ -81,8 +82,7 @@ void propagate(Database& database, ExceptionThroughIndex& through_index) for (const auto analysis : {AnalysisType::kMax, AnalysisType::kMin}) { for (const auto source_type : {PathSourceType::kInput, PathSourceType::kRegister}) { for (const auto input_transition : {TransType::kRise, TransType::kFall}) { - if (!allowsTransition(database.network(), database.graph().simValues(), arc.get_source_pin(), input_transition, - arc.get_type() == ArcType::kNet)) + if (!allowsTransition(database.network(), database.graph().simValues(), arc.get_source_pin(), input_transition, arc.get_type() == ArcType::kNet)) continue; const auto source_analysis = source.get_path_state_map().find(analysis); if (source_analysis == source.get_path_state_map().end()) @@ -93,8 +93,8 @@ void propagate(Database& database, ExceptionThroughIndex& through_index) const auto path_it = source_type_it->second.find(input_transition); if (path_it == source_type_it->second.end()) continue; - const auto outputs = arc.get_type() == ArcType::kCell ? cellOutputTransitions(database, arc, input_transition) - : std::vector{input_transition}; + const auto outputs + = arc.get_type() == ArcType::kCell ? cellOutputTransitions(database, arc, input_transition) : std::vector{input_transition}; for (const auto& [source_tag, source_state] : path_it->second) { if (source_state.get_is_clock_source_path()) continue; @@ -104,9 +104,9 @@ void propagate(Database& database, ExceptionThroughIndex& through_index) continue; const auto progress = matcher.advanceState(through_index, source_state.get_exception_state_list(), arc.get_sink_pin(), output_transition, arc.get_type() == ArcType::kNet ? arc.get_source_pin() : ""); - const auto tag = matcher.makePathStateTag(source_state.get_clock_name(), source_state.get_launch_clock_trans_type(), progress, - source_state.get_is_clock_source_path(), source_state.crprPathTag(), - source_state.get_launch_clock_edge_index()); + const auto tag + = matcher.makePathStateTag(source_state.get_clock_name(), source_state.get_launch_clock_trans_type(), progress, + source_state.get_is_clock_source_path(), source_state.crprPathTag(), source_state.get_launch_clock_edge_index()); auto& target_map = sink.get_path_state_map()[analysis][source_type][output_transition]; if (target_map.contains(tag)) continue; @@ -143,6 +143,7 @@ void StructuralTimingStateBuilder::build(Database& database, DelayCalculator& de for (auto& [name, point] : database.graph().driverPoints()) for (auto& [clock, state] : point.get_clock_state_map()) state.is_structural = true; + ClockGatingChecker(database).infer(); ExceptionThroughIndex through_index(database.sdc(), database.network()); // Reuse the same boundary seeding as complete STA. In particular, min // input delays and ideal clock latency/transition must not become zeros. diff --git a/src/operation/iSTA/source/module/TimingAnalyzer.cpp b/src/operation/iSTA/source/module/TimingAnalyzer.cpp index 2c10f29a2..43728acb9 100644 --- a/src/operation/iSTA/source/module/TimingAnalyzer.cpp +++ b/src/operation/iSTA/source/module/TimingAnalyzer.cpp @@ -21,6 +21,7 @@ #include #include "ClockChecker.hpp" +#include "ClockGatingChecker.hpp" #include "ClockPropagator.hpp" #include "DataPropagator.hpp" #include "DelayCalculator.hpp" @@ -157,6 +158,7 @@ void TimingAnalyzer::analyze(bool reuse_structure) database.graph().clearTiming(); ClockPropagator(database, _delay).propagate(); + ClockGatingChecker(database).infer(); DataPropagator(database, _delay).propagate(); { @@ -166,7 +168,7 @@ void TimingAnalyzer::analyze(bool reuse_structure) PathAnalyzer(database, check).analyze(); } database.fanoutChecks() = FanoutChecker(database).check(); - database.electricalChecks() = ElectricalChecker(database, _delay).check(); + database.electricalChecks() = ElectricalChecker(database, _delay, _check_constant_capacitance).check(); database.clockChecks() = ClockChecker(database).check(); STALOG.info(Loc::current(), "Completed", monitor.getStatsInfo()); } diff --git a/src/operation/iSTA/source/module/TimingAnalyzer.hpp b/src/operation/iSTA/source/module/TimingAnalyzer.hpp index 6eb2382a5..ffbbb31da 100644 --- a/src/operation/iSTA/source/module/TimingAnalyzer.hpp +++ b/src/operation/iSTA/source/module/TimingAnalyzer.hpp @@ -24,7 +24,10 @@ class DelayCalculator; class TimingAnalyzer { public: - TimingAnalyzer(Database& database, DelayCalculator& delay) : _database(database), _delay(delay) {} + TimingAnalyzer(Database& database, DelayCalculator& delay, bool check_constant_capacitance = false) + : _database(database), _delay(delay), _check_constant_capacitance(check_constant_capacitance) + { + } void prepareStructure(); void analyze(bool reuse_structure = false); bool isStructureCurrent() const; @@ -32,5 +35,6 @@ class TimingAnalyzer private: Database& _database; DelayCalculator& _delay; + bool _check_constant_capacitance; }; } // namespace ista diff --git a/src/operation/iSTA/source/module/delay_calculator/arnoldi/ArnoldiDelayCalculator.cpp b/src/operation/iSTA/source/module/delay_calculator/arnoldi/ArnoldiDelayCalculator.cpp index ebc30e6f3..4e655bb0b 100644 --- a/src/operation/iSTA/source/module/delay_calculator/arnoldi/ArnoldiDelayCalculator.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/arnoldi/ArnoldiDelayCalculator.cpp @@ -206,6 +206,20 @@ std::optional ArnoldiDelayCalculator::calculate(TimingArc& t if (!pole_residue.get_is_valid()) { return std::nullopt; } + double waveform_resistance = drive_resistance; + auto update_waveform = [&](bool force = false) { + // Reuse the network response within 5% relative conductance change. + // G = 1/R: compare to the last actual rebuild, not the last driver fit, + // so successive small changes cannot accumulate beyond the tolerance. + if (force || std::abs(waveform_resistance / drive_resistance - 1.) > .05) { + auto updated = ArnoldiWaveformSolver::poleResidue(arnoldi_model, drive_resistance); + if (!updated.get_is_valid()) + return false; + pole_residue = std::move(updated); + waveform_resistance = drive_resistance; + } + return true; + }; double effective_capacitance = total_capacitance; bool request_adjustment = false; @@ -222,6 +236,7 @@ std::optional ArnoldiDelayCalculator::calculate(TimingArc& t auto fit_load = [&](bool match_crossing) { double lower_capacitance = 0.; double upper_capacitance = std::numeric_limits::infinity(); + double previous_residual = std::numeric_limits::infinity(); bool converged = false; // Fit the distributed source crossing for both ordinary and adjusted // two-load drivers. Charge matching remains the fallback for table data @@ -273,31 +288,50 @@ std::optional ArnoldiDelayCalculator::calculate(TimingArc& t if (next_fit) { driver_fit = next_fit; drive_resistance = next_fit->resistance; - pole_residue = ArnoldiWaveformSolver::poleResidue(arnoldi_model, drive_resistance); + const double previous_resistance = waveform_resistance; + if (!update_waveform()) + return false; + if (waveform_resistance != previous_resistance) { + // A changed response invalidates both the crossing and its bracket. + lower_capacitance = 0.; + upper_capacitance = previous_residual = std::numeric_limits::infinity(); + converged = false; + continue; + } } break; } // Solve matched(C) - C = 0. Nonlinear tables can make plain fixed-point // iteration oscillate around a valid solution. Retain the residual-sign // bracket and switch to bisection when iteration stops making progress. + // A joint fit may rebuild the response when C changes: keep contracting + // steps instead of jumping to a different network model. Fixed-R charge + // and adjusted fits retain periodic bisection, including slow contraction. if (capacitance > effective_capacitance) lower_capacitance = effective_capacitance; else upper_capacitance = effective_capacitance; - if (std::isfinite(upper_capacitance) && (iter >= 8 || capacitance <= lower_capacitance || capacitance >= upper_capacitance)) { + const double residual = std::abs(capacitance - effective_capacitance); + if (std::isfinite(upper_capacitance) + && ((iter >= 8 && (!driver_fit || residual >= previous_residual)) || capacitance <= lower_capacitance || capacitance >= upper_capacitance)) { effective_capacitance = lower_capacitance + .5 * (upper_capacitance - lower_capacitance); } else { effective_capacitance = capacitance; } + previous_residual = residual; if (!match_crossing && driver_fit) { driver_fit = effective_capacitance == capacitance ? next_fit : fit_table(effective_capacitance); if (!driver_fit) return false; drive_resistance = driver_fit->resistance; driver_ramp = driver_fit->ramp; - pole_residue = ArnoldiWaveformSolver::poleResidue(arnoldi_model, drive_resistance); - if (!pole_residue.get_is_valid()) + const double previous_resistance = waveform_resistance; + if (!update_waveform()) return false; + if (waveform_resistance != previous_resistance) { + lower_capacitance = 0.; + upper_capacitance = previous_residual = std::numeric_limits::infinity(); + } } else { driver_ramp = tableRamp(timing_arc, output_trans_type, input_slew, drive_resistance, effective_capacitance, lower_threshold, upper_threshold, voltage_log, slew_derate, min_slew_factor); @@ -316,8 +350,7 @@ std::optional ArnoldiDelayCalculator::calculate(TimingArc& t drive_resistance = charge_resistance; driver_ramp = charge_ramp; effective_capacitance = total_capacitance; - pole_residue = ArnoldiWaveformSolver::poleResidue(arnoldi_model, drive_resistance); - if (!pole_residue.get_is_valid()) + if (!update_waveform(true)) return std::nullopt; converged = fit_load(false); } @@ -333,8 +366,7 @@ std::optional ArnoldiDelayCalculator::calculate(TimingArc& t effective_capacitance = total_capacitance; driver_ramp = tableRamp(timing_arc, output_trans_type, input_slew, drive_resistance, total_capacitance, lower_threshold, upper_threshold, voltage_log, slew_derate, min_slew_factor); - pole_residue = ArnoldiWaveformSolver::poleResidue(arnoldi_model, drive_resistance); - if (!pole_residue.get_is_valid() || !(driver_ramp > 0.) || !std::isfinite(driver_ramp)) + if (!update_waveform(true) || !(driver_ramp > 0.) || !std::isfinite(driver_ramp)) return std::nullopt; // The adjusted driver matches its lumped table delay to the distributed // source crossing; receiver delay and slew use this same fitted waveform. diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/ArnoldiResponseCalculator.cpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/ArnoldiResponseCalculator.cpp index 382fbc43c..053d9cc61 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/ArnoldiResponseCalculator.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/ArnoldiResponseCalculator.cpp @@ -106,6 +106,8 @@ DriverResponse ArnoldiResponseCalculator::calculate(std::string& output_pin, Tim return timing_result; ParasiticNet& parasitic_net = database.parasitics().nets()[output_pin_data.get_net_name()]; + if (_rc.hasIncompleteNetwork(parasitic_net, output_pin)) + return timing_result; std::string source_node_name = _rc.nodeName(parasitic_net, output_pin); if (source_node_name.empty()) { return timing_result; diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/DmpResponseCalculator.cpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/DmpResponseCalculator.cpp index 6ffb15ed4..d5e1f5566 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/DmpResponseCalculator.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/DmpResponseCalculator.cpp @@ -49,6 +49,8 @@ DriverResponse DmpResponseCalculator::calculate(std::string& output_pin, TimingA return timing_result; ParasiticNet& parasitic_net = database.parasitics().nets()[output_pin_data.get_net_name()]; + if (_rc.hasIncompleteNetwork(parasitic_net, output_pin)) + return timing_result; std::string source_node_name = _rc.nodeName(parasitic_net, output_pin); if (source_node_name.empty()) { return timing_result; diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/InputPortResponseCalculator.cpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/InputPortResponseCalculator.cpp index 5cd6ee2cd..2c9c7b2c3 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/InputPortResponseCalculator.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/InputPortResponseCalculator.cpp @@ -51,6 +51,8 @@ DriverResponse InputPortResponseCalculator::calculate(ParasiticNet& parasitic_ne if (!database.network().pins().contains(source_pin_name) || !database.network().pins().at(source_pin_name).get_is_port()) { return result; } + if (_rc.hasIncompleteNetwork(parasitic_net, source_pin_name)) + return result; ParasiticArnoldiModel& model = _models.arnoldiModel(parasitic_net, source_node_name, analysis_type, trans_type); if (!model.get_is_valid() || model.get_order() <= 0) { return result; diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/LoadCalculator.cpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/LoadCalculator.cpp index 5f53a8408..f897602be 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/LoadCalculator.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/LoadCalculator.cpp @@ -61,6 +61,8 @@ double LoadCalculator::netLoad(Net& net, const std::string& driver_pin, Analysis if (database.parasitics().nets().count(net.get_net_name()) > 0) { ParasiticNet& parasitic_net = database.parasitics().nets()[net.get_net_name()]; + if (_rc.hasIncompleteNetwork(parasitic_net, driver_pin)) + return _rc.wireCapacitance(parasitic_net) + netPinLoad(net, driver_pin, analysis_type, output_trans_type); return parasiticLoad(driver_pin, parasitic_net, analysis_type, output_trans_type); } return netPinLoad(net, driver_pin, analysis_type, output_trans_type); diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/RcModelBuilder.cpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/RcModelBuilder.cpp index 483fe3abf..0bb7c4d5d 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/RcModelBuilder.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/RcModelBuilder.cpp @@ -277,9 +277,8 @@ ParasiticArnoldiModel RcModelBuilder::buildParasiticArnoldiModel(ParasiticNet& p wire_capacitance += node.get_capacitance(); const double resistance = _rc.totalResistance(parasitic_net) * 1e-3; const double count = static_cast(loads.size()); - const bool balanced = loads.size() <= 5; PinCapacitanceCalculator pin_capacitance(_database); - std::array constants; + std::array constants, reduced_constants; for (auto edge : {TransType::kRise, TransType::kFall}) { double pin_load = pin_capacitance.driverCapacitance(_rc.pinName(source_node_name), analysis_type, edge); for (auto pin : loads) @@ -290,9 +289,15 @@ ParasiticArnoldiModel RcModelBuilder::buildParasiticArnoldiModel(ParasiticNet& p // networks use total RC for the ordinary low-RC cutoff and response. // The enhancement also bounds the reduction to 10% of total RC. const double total_load = wire_capacitance + pin_load; - const double reduced_load = balanced ? std::max(wire_capacitance / (count + 1.) + pin_load / count, .1 * total_load) : total_load; - constants[edge == TransType::kRise ? 0 : 1] = resistance * reduced_load; + const double reduced_load = std::max(wire_capacitance / (count + 1.) + pin_load / count, .1 * total_load); + constants[edge == TransType::kRise ? 0 : 1] = resistance * total_load; + reduced_constants[edge == TransType::kRise ? 0 : 1] = resistance * reduced_load; } + // Ordinary total RC has priority. The enhanced model extends the small-net + // range; applying it inside the ordinary range understates every wire delay. + const bool balanced = _config.small_rc_fanout_reduction != 0 && std::max(constants[0], constants[1]) >= .002 && loads.size() <= 5; + if (balanced) + constants = reduced_constants; // Both low-RC models use a 2 ps cutoff. Select both edges together; // never replace a partially annotated // or multi-driver network with this approximation. diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.cpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.cpp index b690f4911..85f41f0d8 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.cpp @@ -26,6 +26,8 @@ #include #include +#include "Logger.hpp" + namespace ista { bool RcTreeAnalyzer::hasDisabledDriver(const std::string& name, const std::string& source) const @@ -40,19 +42,59 @@ bool RcTreeAnalyzer::hasDisabledDriver(const std::string& name, const std::strin double RcTreeAnalyzer::balancedDelay(ParasiticNet& net, const std::string& source, std::string& sink, AnalysisType analysis, TransType edge) { - const auto found = _database.network().nets().find(net.get_net_name()); - if (found == _database.network().nets().end()) - return 0.; - const auto& loads = found->second.get_load_pin_list(); - const auto count = std::count_if(loads.begin(), loads.end(), [&](const auto& pin) { return pin != source; }); - if (count == 0) - return 0.; - double wire_cap = 0.; + auto [entry, inserted] = _balanced_models.try_emplace({net.get_net_name(), source}); + auto& model = entry->second; + if (inserted) { + const auto found = _database.network().nets().find(net.get_net_name()); + if (found == _database.network().nets().end()) + return 0.; + const auto& loads = found->second.get_load_pin_list(); + const auto count = std::count_if(loads.begin(), loads.end(), [&](const auto& pin) { return pin != source; }); + if (count == 0) + return 0.; + double wire_cap = 0.; + for (const auto& [name, node] : net.get_node_map()) + wire_cap += node.get_capacitance(); + if (net.get_node_map().empty()) + wire_cap = net.get_lumped_capacitance(); + if (hasIncompleteNetwork(net, source)) + wire_cap = wireCapacitance(net); + model = {totalResistance(net) * 1e-3 / count, wire_cap / count}; + } + return model.resistance_per_load * (_pin_capacitance.pinCapacitance(sink, analysis, edge) + model.wire_capacitance_per_load); +} + +double RcTreeAnalyzer::wireCapacitance(ParasiticNet& net) const +{ + if (net.get_lumped_capacitance() > 0.) + return net.get_lumped_capacitance(); + double capacitance = 0.; for (const auto& [name, node] : net.get_node_map()) - wire_cap += node.get_capacitance(); - if (net.get_node_map().empty()) - wire_cap = net.get_lumped_capacitance(); - return totalResistance(net) * 1e-3 / count * (_pin_capacitance.pinCapacitance(sink, analysis, edge) + wire_cap / count); + capacitance += node.get_capacitance(); + return capacitance; +} + +bool RcTreeAnalyzer::hasIncompleteNetwork(ParasiticNet& net, const std::string& source) +{ + auto [entry, inserted] = _incomplete_networks.try_emplace({net.get_net_name(), source}, false); + if (!inserted) + return entry->second; + const auto logical = _database.network().nets().find(net.get_net_name()); + if (logical == _database.network().nets().end()) + return false; + auto root = nodeName(net, source); + const auto& tree = rootedTree(net, root); + const std::set reachable(tree.nodes.begin(), tree.nodes.end()); + for (const auto& pin : logical->second.get_pin_name_list()) { + const auto node = nodeName(net, pin); + if (node.empty() || !reachable.contains(node)) { + entry->second = true; + STALOG.warn(Loc::current(), "Incomplete SPEF connectivity on ", net.get_net_name(), ": ", pin, + "; using total wire R/C and all netlist loads in a balanced tree"); + break; + } + } + return entry->second; } double RcTreeAnalyzer::totalResistance(ParasiticNet& parasitic_net) diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.hpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.hpp index 8f56ae4e2..86c9fd7f1 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.hpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/RcTreeAnalyzer.hpp @@ -40,11 +40,15 @@ class RcTreeAnalyzer _parasitic_resistor_map_cache.clear(); _trees.clear(); _timing.clear(); + _incomplete_networks.clear(); + _balanced_models.clear(); } double load(const std::string& net, AnalysisType analysis, TransType trans, const std::string& node) const; std::optional delay(const std::string& net, AnalysisType analysis, TransType trans, const std::string& node) const; std::optional impulse(const std::string& net, AnalysisType analysis, TransType trans, const std::string& node) const; bool hasDisabledDriver(const std::string& net, const std::string& source) const; + bool hasIncompleteNetwork(ParasiticNet& net, const std::string& source); + double wireCapacitance(ParasiticNet& net) const; double balancedDelay(ParasiticNet& net, const std::string& source, std::string& sink, AnalysisType analysis, TransType edge); double totalResistance(ParasiticNet& parasitic_net); double nodeCapacitance(ParasiticNet& parasitic_net, std::string& pin_name); @@ -82,6 +86,13 @@ class RcTreeAnalyzer PinCapacitanceCalculator& _pin_capacitance; std::map>>> _parasitic_resistor_map_cache; std::map _trees; + std::map, bool> _incomplete_networks; + struct BalancedModel + { + double resistance_per_load = 0.; + double wire_capacitance_per_load = 0.; + }; + std::map, BalancedModel> _balanced_models; std::map>> _timing; }; } // namespace ista diff --git a/src/operation/iSTA/source/module/delay_calculator/interconnect/WireDelayCalculator.cpp b/src/operation/iSTA/source/module/delay_calculator/interconnect/WireDelayCalculator.cpp index cd2ef1f19..6df232183 100644 --- a/src/operation/iSTA/source/module/delay_calculator/interconnect/WireDelayCalculator.cpp +++ b/src/operation/iSTA/source/module/delay_calculator/interconnect/WireDelayCalculator.cpp @@ -60,6 +60,11 @@ double WireDelayCalculator::calcParasiticSlew(Arc& arc, AnalysisType analysis_ty { Database& database = _database; ParasiticNet& parasitic_net = database.parasitics().nets()[arc.get_owner_name()]; + if (_rc.hasIncompleteNetwork(parasitic_net, arc.get_source_pin())) { + double unused_delay = 0., slew = input_slew; + _threshold.adjustSignalLevel(arc.get_source_pin(), arc.get_sink_pin(), analysis_type, trans_type, unused_delay, slew); + return slew; + } std::string source_node_name = _rc.nodeName(parasitic_net, arc.get_source_pin()); std::string sink_node_name = _rc.nodeName(parasitic_net, arc.get_sink_pin()); if (source_node_name.empty() || sink_node_name.empty()) { @@ -140,7 +145,7 @@ double WireDelayCalculator::calcParasiticDelay(Arc& arc, AnalysisType analysis_t { Database& database = _database; ParasiticNet& parasitic_net = database.parasitics().nets()[arc.get_owner_name()]; - if (_rc.hasDisabledDriver(arc.get_owner_name(), arc.get_source_pin())) { + if (_rc.hasDisabledDriver(arc.get_owner_name(), arc.get_source_pin()) || _rc.hasIncompleteNetwork(parasitic_net, arc.get_source_pin())) { double delay = _rc.balancedDelay(parasitic_net, arc.get_source_pin(), arc.get_sink_pin(), analysis_type, trans_type); double slew = input_slew; _threshold.adjustSignalLevel(arc.get_source_pin(), arc.get_sink_pin(), analysis_type, trans_type, delay, slew); diff --git a/src/operation/iSTA/source/module/path_analyzer/PathGroupBuilder.cpp b/src/operation/iSTA/source/module/path_analyzer/PathGroupBuilder.cpp index e8194de58..d860bc7e6 100644 --- a/src/operation/iSTA/source/module/path_analyzer/PathGroupBuilder.cpp +++ b/src/operation/iSTA/source/module/path_analyzer/PathGroupBuilder.cpp @@ -40,6 +40,8 @@ void PathGroupBuilder::addPathEnd(std::vector& paths) std::string PathGroupBuilder::groupName(TimingPath& path) { + if (path.get_check_type() == TimingCheckType::kClockGatingSetup || path.get_check_type() == TimingCheckType::kClockGatingHold) + return "**clock_gating_default**"; if (path.get_check_type() == TimingCheckType::kRecovery || path.get_check_type() == TimingCheckType::kRemoval) { return "**async_default**"; } diff --git a/src/operation/iSTA/source/module/reader/ReadParasitics.cpp b/src/operation/iSTA/source/module/reader/ReadParasitics.cpp index 2ef9eebdd..3ae861aa0 100644 --- a/src/operation/iSTA/source/module/reader/ReadParasitics.cpp +++ b/src/operation/iSTA/source/module/reader/ReadParasitics.cpp @@ -7,8 +7,12 @@ #include #include #include +#include +#include +#include "Logger.hpp" #include "idm.h" +#include "network/Network.hpp" #include "parasitics/Parasitics.hpp" #include "spef/SpefParser.hh" @@ -34,8 +38,21 @@ void ReadParasitics::read() void ReadParasitics::readNet(spef::Net& spef_net) { + std::string net_name = spef_net.name; + if (!_network.nets().contains(net_name)) { + // A top-level assign may be linked under its internal wire name while + // extraction names the same connection after the port. Resolve only an + // actual port alias, then validate every extracted terminal below. + const auto port = _network.pins().find(net_name); + if (port != _network.pins().end() && port->second.get_is_port()) + net_name = port->second.get_net_name(); + } + // An ECO can leave the old extracted terminals in SPEF. Those nodes must + // not replace the loads of the linked netlist or become a fictitious RC tree. + if (!connectionsMatch(spef_net, net_name)) + return; ParasiticNet parasitic_net; - parasitic_net.set_net_name(spef_net.name); + parasitic_net.set_net_name(net_name); parasitic_net.set_lumped_capacitance(readCapacitance(spef_net.lcap)); for (spef::ConnEntry& spef_conn : spef_net.conns) { readConnection(parasitic_net, spef_conn); @@ -46,16 +63,51 @@ void ReadParasitics::readNet(spef::Net& spef_net) for (spef::ResCap& spef_cap : spef_net.caps) { readCapacitance(parasitic_net, spef_cap); } - // The default O-2018.06-SP1 model normalizes extracted nodes to at least 1 aF. + // The U-2022.12 model normalizes extracted nodes to at least 1 zF. // Apply this in pF after duplicate CAP entries have been summed, including // nodes present only in CONN/RES. Liberty pin capacitance is added later. - constexpr double minimum_node_capacitance = 1e-6; + constexpr double minimum_node_capacitance = 1e-9; for (auto& [name, node] : parasitic_net.get_node_map()) { node.set_capacitance(std::max(node.get_capacitance(), minimum_node_capacitance)); } _parasitics.nets()[parasitic_net.get_net_name()] = parasitic_net; } +bool ReadParasitics::connectionsMatch(const spef::Net& net, const std::string& net_name) const +{ + const auto found = _network.nets().find(net_name); + if (found == _network.nets().end()) { + STALOG.warn(Loc::current(), "Ignoring SPEF net absent from the timing network: ", net.name); + return false; + } + const auto& pins = found->second.get_pin_name_list(); + const std::unordered_set pin_names(pins.begin(), pins.end()); + for (const auto& connection : net.conns) { + if (connection.conn_direction == spef::ConnectionDirection::kInternal) + continue; // *N entries describe wire geometry, not netlist terminals. + const auto matches = [&](const std::string& name) { + if (pin_names.contains(name)) + return true; + const auto boundary = _network.hierarchyPins().find(name); + return boundary != _network.hierarchyPins().end() && boundary->second.error.empty() && boundary->second.net == net_name; + }; + std::string name = connection.pin_port_name; + bool matched = matches(name); + // Only replace an instance/pin separator, preserving hierarchy separators. + for (auto slash = name.rfind('/'); !matched && slash != std::string::npos; slash = slash == 0 ? std::string::npos : name.rfind('/', slash - 1)) { + name[slash] = ':'; + matched = matches(name); + name[slash] = '/'; + } + if (!matched) { + STALOG.warn(Loc::current(), "Ignoring stale SPEF net ", net.name, ": terminal ", connection.pin_port_name, + " is not connected in the timing network; retaining netlist pin/port loads"); + return false; + } + } + return true; +} + void ReadParasitics::readConnection(ParasiticNet& parasitic_net, spef::ConnEntry& spef_conn) { ParasiticNode& parasitic_node = ensureNode(parasitic_net, spef_conn.pin_port_name); @@ -100,7 +152,10 @@ double ReadParasitics::readResistance(double spef_resistance) { std::string spef_unit = _parasitics.get_resistance_unit(); std::string target_unit = "OHM"; - return spef_resistance * unitScale(spef_unit, target_unit); + // Normalize in physical ohms, independently of the SPEF R_UNIT. The + // U-2022.12 minimum also affects the total-RC choice of interconnect model. + constexpr double minimum_resistance = .01; + return std::max(spef_resistance * unitScale(spef_unit, target_unit), minimum_resistance); } double ReadParasitics::unitScale(std::string& spef_unit, std::string& target_unit) diff --git a/src/operation/iSTA/source/module/reader/ReadParasitics.hpp b/src/operation/iSTA/source/module/reader/ReadParasitics.hpp index 4478314ff..e3ae90094 100644 --- a/src/operation/iSTA/source/module/reader/ReadParasitics.hpp +++ b/src/operation/iSTA/source/module/reader/ReadParasitics.hpp @@ -29,14 +29,17 @@ class ParasiticNet; class ParasiticNode; class Parasitics; +class Network; class ReadParasitics { public: - explicit ReadParasitics(Parasitics& parasitics) : _parasitics(parasitics) {} + ReadParasitics(Parasitics& parasitics, const Network& network) : _parasitics(parasitics), _network(network) {} void read(); private: Parasitics& _parasitics; + const Network& _network; + bool connectionsMatch(const spef::Net& net, const std::string& net_name) const; void readNet(spef::Net& spef_net); void readConnection(ParasiticNet& parasitic_net, spef::ConnEntry& spef_conn); void readCapacitance(ParasiticNet& parasitic_net, spef::ResCap& spef_cap); diff --git a/src/operation/iSTA/source/module/sdc/SdcCommand.cpp b/src/operation/iSTA/source/module/sdc/SdcCommand.cpp index f538086d1..1e2160a6c 100644 --- a/src/operation/iSTA/source/module/sdc/SdcCommand.cpp +++ b/src/operation/iSTA/source/module/sdc/SdcCommand.cpp @@ -155,6 +155,17 @@ void SdcCommand::setIgnoreUnknownCommands(bool ignore) int SdcCommand::evalScriptFile(const std::string& file_name) { clearErrors(); + _ignored_commands.clear(); + setScreenVariables(_interp); + if (!_ignore_unknown_commands) { + // Preserve Tcl's file context for [info script], nested source, return, + // and file/line diagnostics. Evaluating individual strings loses it. + const int result = Tcl_EvalFile(_interp, file_name.c_str()); + if (result != TCL_OK) + addError(Tcl_GetErrorLine(_interp), Tcl_GetStringResult(_interp)); + return result; + } + std::ifstream script_file(file_name); if (!script_file.is_open()) { addError(0, "failed to open SDC file '" + file_name + "'"); @@ -162,7 +173,6 @@ int SdcCommand::evalScriptFile(const std::string& file_name) } const std::string script((std::istreambuf_iterator(script_file)), std::istreambuf_iterator()); - setScreenVariables(_interp); return evalScript(script); } diff --git a/src/operation/iSTA/source/module/sdc/SdcTclCmd.cpp b/src/operation/iSTA/source/module/sdc/SdcTclCmd.cpp index ced6e39e7..6ef33c83c 100644 --- a/src/operation/iSTA/source/module/sdc/SdcTclCmd.cpp +++ b/src/operation/iSTA/source/module/sdc/SdcTclCmd.cpp @@ -35,6 +35,29 @@ SdcTclCmd::SdcTclCmd(const char* cmd_name, ClientData client_data) : ecc::TclCmd { } +void SdcTclCmd::addOption(ecc::TclOption* option) +{ + ecc::TclCmd::addOption(option); + if (!option->is_arg()) + _named_options.emplace(option->get_option_name(), option); +} + +ecc::TclOption* SdcTclCmd::findOption(const std::string& name) const +{ + if (name.empty() || name.front() != '-') + return nullptr; + if (const auto exact = _named_options.find(name); exact != _named_options.end()) + return exact->second; + + ecc::TclOption* match = nullptr; + for (auto it = _named_options.lower_bound(name); it != _named_options.end() && it->first.starts_with(name); ++it) { + if (match != nullptr) + throw std::invalid_argument("ambiguous option '" + name + "': " + match->get_option_name() + " or " + it->first); + match = it->second; + } + return match; +} + int SdcTclCmd::execute(Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) { resetExecutionState(); @@ -53,7 +76,7 @@ int SdcTclCmd::execute(Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]) continue; } - ecc::TclOption* option = getOptionOrArg(value); + ecc::TclOption* option = findOption(value); if (option != nullptr) { current_option = option; if (option->isSwitchOption()) { diff --git a/src/operation/iSTA/source/module/sdc/SdcTclCmd.hpp b/src/operation/iSTA/source/module/sdc/SdcTclCmd.hpp index 8ee279554..2c2028c41 100644 --- a/src/operation/iSTA/source/module/sdc/SdcTclCmd.hpp +++ b/src/operation/iSTA/source/module/sdc/SdcTclCmd.hpp @@ -35,6 +35,7 @@ class SdcTclCmd : public ecc::TclCmd SdcTclCmd(const char* cmd_name, ClientData client_data); ~SdcTclCmd() override = default; + void addOption(ecc::TclOption* option); int execute(Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]); Tcl_Interp* interpreter() const { return _interp; } Tcl_Obj* getOptionObject(const std::string& name) const @@ -64,9 +65,12 @@ class SdcTclCmd : public ecc::TclCmd friend ObjectQueryOptions getObjectQueryOptions(SdcTclCmd& command); void resetExecutionState(); void setInterpreterError(Tcl_Interp* interp) const; + ecc::TclOption* findOption(const std::string& name) const; Tcl_Interp* _interp = nullptr; ClientData _client_data = nullptr; + // The base TclCmd owns these options; arguments are deliberately excluded. + std::map _named_options; std::string _error_message; std::string _result; std::vector _list_result; diff --git a/src/operation/iSTA/source/module/sdc/exception/SetFalsePath.cpp b/src/operation/iSTA/source/module/sdc/exception/SetFalsePath.cpp index 74f7042f7..9622023ef 100644 --- a/src/operation/iSTA/source/module/sdc/exception/SetFalsePath.cpp +++ b/src/operation/iSTA/source/module/sdc/exception/SetFalsePath.cpp @@ -21,6 +21,38 @@ namespace ista::sdc { +namespace { + +// A register output is traversed by a path, but is not its launch boundary. +// Keep this validation local to false paths: max/min delay intentionally allow +// internal pins to segment a path, and -through may name register outputs. +bool isRegisterOutput(const Database& database, const ExceptionObject& object) +{ + if (object.type != ExceptionObjectType::kPin) + return false; + const auto& pins = database.network().pins(); + auto pin = pins.find(object.name); + if (pin == pins.end() || pin->second.get_is_port()) + return false; + const auto& constraints = database.sdc().get_port_constraint_map(); + auto constraint = constraints.find(object.name); + if (constraint != constraints.end() && !constraint->second.get_input_delay_list().empty()) + return false; + const auto& instances = database.network().instances(); + auto instance = instances.find(pin->second.get_instance_name()); + if (instance == instances.end() || !instance->second.get_is_sequential()) + return false; + const auto& cells = database.liberty().get_cell_map(); + auto cell = cells.find(instance->second.get_cell_name()); + if (cell == cells.end()) + return false; + const auto& ports = cell->second.get_port_map(); + auto port = ports.find(pin->second.get_pin_name()); + return port != ports.end() && port->second.get_is_output() && !port->second.get_is_input(); +} + +} // namespace + TclSetFalsePath::TclSetFalsePath(const char* cmd_name, ClientData client_data) : TclPathException(cmd_name, client_data, true) { addOption(new ecc::TclSwitchOption("-setup")); @@ -34,6 +66,21 @@ unsigned TclSetFalsePath::exec() TimingException exception = parsePathSelector(database, true); if (hasEmptySelector()) return 1; + auto from = exception.get_from_objects(); + const bool explicit_from = !from.empty(); + for (auto it = from.begin(); it != from.end();) { + if (isRegisterOutput(database, *it)) { + STALOG.warn(Loc::current(), "set_false_path ignores non-startpoint register output ", it->name); + it = from.erase(it); + } else { + ++it; + } + } + // An empty stored selector means every startpoint. Never broaden an invalid + // command into a global exception, including when -reset_path was requested. + if (explicit_from && from.empty()) + return 1; + exception.set_from_objects(from); exception.set_type(TimingExceptionType::kFalsePath); applyAnalysisQualifiers(exception); if (getOptionOrArg("-reset_path")->is_set_val()) { diff --git a/src/operation/iSTA/source/module/timing_checker/CMakeLists.txt b/src/operation/iSTA/source/module/timing_checker/CMakeLists.txt index 5c7682c8f..b6e64c7fa 100644 --- a/src/operation/iSTA/source/module/timing_checker/CMakeLists.txt +++ b/src/operation/iSTA/source/module/timing_checker/CMakeLists.txt @@ -39,6 +39,6 @@ target_include_directories(ista_path_checker ${CMAKE_CURRENT_SOURCE_DIR} ) -add_library(ista_clock_checker ClockChecker.cpp) +add_library(ista_clock_checker ClockChecker.cpp ClockGatingChecker.cpp) target_include_directories(ista_clock_checker PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(ista_clock_checker PUBLIC ista_model PRIVATE ista_clock_reconvergence_calculator ista_graph_query) diff --git a/src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.cpp b/src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.cpp new file mode 100644 index 000000000..f73083e54 --- /dev/null +++ b/src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.cpp @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MulanPSL-2.0 +#include "ClockGatingChecker.hpp" + +#include +#include +#include +#include +#include + +#include "Logger.hpp" +#include "graph/TimingStateQuery.hpp" + +namespace ista { +void ClockGatingChecker::infer() +{ + auto& graph = _database.graph(); + const auto& network = _database.network(); + graph.clearClockGatingChecks(); + TimingStateQuery query(_database); + auto has_clock = [&](const std::string& pin) { + const auto point = graph.points().find(pin); + if (point != graph.points().end()) + for (const auto& [clock, state] : point->second.get_clock_state_map()) + if (!state.is_source_path_only && (state.is_structural || !state.waveforms.empty())) + return true; + return false; + }; + // A clock can also feed ordinary data logic. Trace clock sinks and exported + // clock outputs, keeping unrelated primary-clock roots as boundaries. + std::map> clock_fanin; + std::vector> pending; + auto seed = [&](const std::string& pin) { + const auto point = graph.points().find(pin); + if (point != graph.points().end()) + for (const auto& [clock, state] : point->second.get_clock_state_map()) + if (!state.is_source_path_only && (state.is_structural || !state.waveforms.empty())) + pending.emplace_back(pin, clock); + }; + for (const auto& [name, instance] : network.instances()) { + if (!instance.get_is_sequential() && !instance.get_is_clock_gating()) + continue; + seed(instance.get_clock_pin_name()); + for (const auto& check : instance.get_check_arc_list()) + seed(check.get_clock_port()); + } + for (const auto& [name, definition] : _database.sdc().get_clock_map()) + if (definition.get_is_generated()) + for (const auto& target : definition.get_source_list()) + pending.emplace_back(target, definition.get_master_clock_name()); + for (const auto& [name, pin] : network.pins()) { + if (!pin.get_is_port() || (pin.get_direction() != PinDirection::kOutput && pin.get_direction() != PinDirection::kInout)) + continue; + bool primary_root = false; + for (const auto& [clock, definition] : _database.sdc().get_clock_map()) + if (!definition.get_is_generated()) + for (const auto& source : definition.get_source_objects()) + primary_root |= source == name; + if (primary_root) + continue; + const auto incoming = graph.incomingArcs().find(name); + if (incoming == graph.incomingArcs().end()) + continue; + for (auto index : incoming->second) { + auto& arc = graph.arcs()[index]; + if (arc.get_type() != ArcType::kNet || query.isDisableArc(arc)) + continue; + const auto& point = graph.sourcePoint(arc); + for (const auto& [clock, state] : point.get_clock_state_map()) + if (!state.is_source_path_only && (state.is_structural || !state.waveforms.empty()) + && _database.sdc().clockAllowsNetArc(network, clock, arc.get_source_pin(), name)) + pending.emplace_back(arc.get_source_pin(), clock); + } + } + while (!pending.empty()) { + auto [pin, clock] = std::move(pending.back()); + pending.pop_back(); + if (!clock_fanin[pin].insert(clock).second) + continue; + const auto incoming = graph.incomingArcs().find(pin); + if (incoming == graph.incomingArcs().end()) + continue; + for (auto index : incoming->second) { + auto& arc = graph.arcs()[index]; + const auto* point = clockPoint(_database, arc.get_source_pin(), clock); + const auto* state = point ? point->find_clock_state(clock) : nullptr; + if (arc.get_is_clock_arc() || query.isDisableArc(arc) || !state || state->is_source_path_only || (!state->is_structural && state->waveforms.empty())) + continue; + if (arc.get_type() == ArcType::kNet && !_database.sdc().clockAllowsNetArc(network, clock, arc.get_source_pin(), arc.get_sink_pin())) + continue; + pending.emplace_back(arc.get_source_pin(), clock); + } + } + std::set> inferred; + for (const auto& [output, output_clocks] : clock_fanin) { + const auto pin = network.pins().find(output); + if (pin == network.pins().end() || pin->second.get_is_port()) + continue; + const auto& instance = network.instances().at(pin->second.get_instance_name()); + if (instance.get_is_sequential() || instance.get_is_clock_gating()) + continue; + const auto cell = _database.liberty().get_cell_map().find(instance.get_cell_name()); + if (cell == _database.liberty().get_cell_map().end()) + continue; + const auto port = cell->second.get_port_map().find(pin->second.get_pin_name()); + if (port == cell->second.get_port_map().end() || !port->second.get_is_output()) + continue; + const auto& function = port->second.get_function_expression(); + if (function.get_is_empty()) + continue; + std::map constants; + for (const auto& term : function.get_term_list()) { + if (term.get_operation_type() != LogicOperationType::kPort) + continue; + const auto value = graph.simValues().value(instance.get_instance_name() + ":" + term.get_port_name()); + if (value && (*value == TimingCaseValue::kZero || *value == TimingCaseValue::kOne)) + constants[term.get_port_name()] = *value == TimingCaseValue::kOne; + } + const auto incoming = graph.incomingArcs().find(output); + if (incoming == graph.incomingArcs().end()) + continue; + for (auto clock_index : incoming->second) { + auto& clock_arc = graph.arcs()[clock_index]; + const auto& clock_pin = clock_arc.get_source_pin(); + if (clock_arc.get_type() != ArcType::kCell || query.isDisableArc(clock_arc) || !clock_fanin.contains(clock_pin)) + continue; + const auto& clock_port = network.pins().at(clock_pin).get_pin_name(); + auto values = constants; + values[clock_port] = false; + const bool low_constant = function.evaluate_constant(values).has_value(); + values[clock_port] = true; + const bool high_constant = function.evaluate_constant(values).has_value(); + // One clock level must block every data change. A buffer is constant + // for both cofactors; XOR/mux functions generally have neither. + if (low_constant == high_constant) + continue; + const bool active_high = low_constant; + values[clock_port] = active_high; + for (auto enable_index : incoming->second) { + auto& enable_arc = graph.arcs()[enable_index]; + const auto& enable = enable_arc.get_source_pin(); + if (enable_arc.get_type() != ArcType::kCell || query.isDisableArc(enable_arc) || has_clock(enable)) + continue; + const auto& enable_port = network.pins().at(enable).get_pin_name(); + if (constants.contains(enable_port) || !function.is_sensitive_to(enable_port, values)) + continue; + // Explicit Liberty checks take precedence over inferred zero margins. + bool explicit_check = false; + for (const auto& check : instance.get_check_arc_list()) + explicit_check |= check.get_clock_port() == clock_pin && check.get_data_port() == enable; + if (explicit_check) + continue; + for (const auto& clock : clock_fanin.at(clock_pin)) + if (output_clocks.contains(clock) && inferred.emplace(enable, clock_pin, active_high, clock).second) + for (bool setup : {true, false}) { + TimingCheckArc check; + check.set_clock_port(clock_pin); + check.set_data_port(enable); + check.setCaptureClock(clock); + check.set_check_type(setup ? TimingCheckType::kClockGatingSetup : TimingCheckType::kClockGatingHold); + check.set_clock_trans_type(setup == active_high ? TransType::kRise : TransType::kFall); + graph.addClockGatingCheck(std::move(check)); + } + } + } + } + if (!graph.clockGatingChecks().empty()) + STALOG.info(Loc::current(), "Inferred combinational clock-gating endpoints: ", graph.clockGatingChecks().size()); +} +} // namespace ista diff --git a/src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.hpp b/src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.hpp new file mode 100644 index 000000000..9e22f8450 --- /dev/null +++ b/src/operation/iSTA/source/module/timing_checker/ClockGatingChecker.hpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MulanPSL-2.0 +#pragma once + +namespace ista { +class Database; + +// Infer zero-margin checks on combinational clock gates after clock propagation. +// These checks belong to the analyzed graph, not the linked Liberty/netlist. +class ClockGatingChecker +{ + public: + explicit ClockGatingChecker(Database& database) : _database(database) {} + void infer(); + + private: + Database& _database; +}; +} // namespace ista diff --git a/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.cpp b/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.cpp index 2a30e0601..ce9fdd663 100644 --- a/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.cpp +++ b/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.cpp @@ -25,7 +25,7 @@ double slewRange(const Library& library, TransType edge) const double upper = edge == TransType::kRise ? library.get_slew_upper_threshold_pct_rise() : library.get_slew_upper_threshold_pct_fall(); return (normalize(upper) - normalize(lower)) / library.get_slew_derate_from_library(); } -std::set dataClocks(TimingPoint& point, AnalysisType analysis, TransType edge) +std::set dataClocks(const TimingPoint& point, AnalysisType analysis, TransType edge) { std::set clocks; auto mode = point.get_path_state_map().find(analysis); @@ -118,12 +118,12 @@ std::vector ElectricalChecker::check() continue; const auto& pin = db.network().pins().at(driver); const bool separate_driver = db.graph().driverPoints().contains(driver); - if (!db.graph().simValues().allowsTransition(driver, TransType::kRise, separate_driver) + if (!_check_constant_capacitance && !db.graph().simValues().allowsTransition(driver, TransType::kRise, separate_driver) && !db.graph().simValues().allowsTransition(driver, TransType::kFall, separate_driver)) continue; - if (!db.graph().points().contains(driver) && !separate_driver) + const auto* point = db.graph().points().contains(driver) || separate_driver ? &db.graph().driverPoint(driver) : nullptr; + if (!point && !_check_constant_capacitance) continue; - auto& point = db.graph().driverPoint(driver); for (auto analysis : {AnalysisType::kMin, AnalysisType::kMax}) for (auto edge : {TransType::kRise, TransType::kFall}) { const double capacitance = _delay.pinLoad(driver, analysis, edge); @@ -142,16 +142,21 @@ std::vector ElectricalChecker::check() for (const auto& load : net.get_load_pin_list()) { const auto& receiver = db.network().pins().at(load); if (load == driver || receiver.get_is_port() || receiver.get_direction() != PinDirection::kInout - || db.graph().simValues().driverDisabled(load) == false || isIdealClockPin(db, load) || !db.graph().simValues().allowsTransition(load, edge)) + || db.graph().simValues().driverDisabled(load) == false || isIdealClockPin(db, load) + || (!_check_constant_capacitance && !db.graph().simValues().allowsTransition(load, edge))) continue; const auto forced = db.sdc().electricalLimits().forcedLimit(ElectricalRule::kCapacitance, load, false); record(ElectricalRule::kCapacitance, receiver, clock, clock_path, analysis, edge, capacitance, forced ? forced : maximum); } }; - for (const auto& clock : dataClocks(point, analysis, edge)) - evaluate(clock, false); - for (const auto& [clock, state] : point.get_clock_state_map()) - evaluate(clock, true); + if (point) { + for (const auto& clock : dataClocks(*point, analysis, edge)) + evaluate(clock, false); + for (const auto& [clock, state] : point->get_clock_state_map()) + evaluate(clock, true); + } else { + evaluate("", false); + } } } } diff --git a/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.hpp b/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.hpp index 0e73a08fc..46a33222c 100644 --- a/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.hpp +++ b/src/operation/iSTA/source/module/timing_checker/ElectricalChecker.hpp @@ -9,12 +9,16 @@ class DelayCalculator; class ElectricalChecker { public: - ElectricalChecker(Database& database, DelayCalculator& delay) : _database(database), _delay(delay) {} + ElectricalChecker(Database& database, DelayCalculator& delay, bool check_constant_capacitance = false) + : _database(database), _delay(delay), _check_constant_capacitance(check_constant_capacitance) + { + } std::vector check(); private: std::optional limit(ElectricalRule rule, const Pin& pin, const std::string& clock, bool clock_path, TransType edge); Database& _database; DelayCalculator& _delay; + bool _check_constant_capacitance; }; } // namespace ista diff --git a/src/operation/iSTA/source/module/timing_checker/FanoutChecker.cpp b/src/operation/iSTA/source/module/timing_checker/FanoutChecker.cpp index 8525e1bcf..ca058b2f7 100644 --- a/src/operation/iSTA/source/module/timing_checker/FanoutChecker.cpp +++ b/src/operation/iSTA/source/module/timing_checker/FanoutChecker.cpp @@ -42,6 +42,9 @@ std::vector FanoutChecker::check() const if (_sim_values.isDriverDisabled(driver) || isIdealClockPin(_database, driver)) { continue; } + const bool separate_driver = _database.graph().driverPoints().contains(driver); + if (!_sim_values.allowsTransition(driver, TransType::kRise, separate_driver) && !_sim_values.allowsTransition(driver, TransType::kFall, separate_driver)) + continue; double driver_fanout_load = fanout_load; if (!pin.get_is_port() && loads.contains(driver)) { const LibertyPort* port = _network.libertyPort(_liberty, pin); diff --git a/src/operation/iSTA/source/module/timing_checker/PathChecker.cpp b/src/operation/iSTA/source/module/timing_checker/PathChecker.cpp index 9b090dd25..5a509ef6c 100644 --- a/src/operation/iSTA/source/module/timing_checker/PathChecker.cpp +++ b/src/operation/iSTA/source/module/timing_checker/PathChecker.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -104,11 +105,16 @@ std::vector PathChecker::checkPathCandidates(const TimingPathState& pat if (!isMatchCheckTransType(*arc, path.get_trans_type())) continue; for (const auto& clock : getCaptureClockNames(arc->get_clock_port())) { + if (!arc->captureClock().empty() && arc->captureClock() != clock) + continue; const auto definition = _database.sdc().get_clock_map().find(clock); const size_t count = definition == _database.sdc().get_clock_map().end() ? 2 : definition->second.edgeCount(); for (auto source : clockSourceTransitions(_database, arc->get_clock_port(), clock, getCaptureAnalysisType(analysis), arc->get_clock_trans_type())) - for (size_t index = source == TransType::kFall ? 1 : 0; index < count; index += 2) - checks.push_back(evaluateRegister(path, *arc, analysis, clock, index, include_reconvergence)); + for (size_t index = source == TransType::kFall ? 1 : 0; index < count; index += 2) { + auto check = evaluateRegister(path, *arc, analysis, clock, index, include_reconvergence); + if (std::isfinite(check.capture_time)) + checks.push_back(std::move(check)); + } } } } @@ -147,8 +153,7 @@ std::vector PathChecker::checkPathCandidates(const TimingPathState& pat return candidates; } -std::optional PathChecker::checkPath(const TimingPathState& path, const std::string& end_point, AnalysisType analysis, - bool include_reconvergence) +std::optional PathChecker::checkPath(const TimingPathState& path, const std::string& end_point, AnalysisType analysis, bool include_reconvergence) { auto candidates = checkPathCandidates(path, end_point, analysis, include_reconvergence); if (candidates.empty()) @@ -189,11 +194,14 @@ PathCheck PathChecker::evaluateRegister(const TimingPathState& path, TimingCheck const double arrival = _query.getClockArrival(result.capture_pin, capture_clock, capture_mode, result.capture_pin_transition, result.capture_transition); const auto clock = _database.sdc().get_clock_map().find(std::string(capture_clock)); if (clock != _database.sdc().get_clock_map().end()) - result.source_latency = _clock_pin_values.values(result.capture_pin, capture_clock, capture_mode, result.capture_pin_transition, AnalysisType::kNone, - result.capture_transition) - .source_latency; + result.source_latency + = _clock_pin_values + .values(result.capture_pin, capture_clock, capture_mode, result.capture_pin_transition, AnalysisType::kNone, result.capture_transition) + .source_latency; result.network_delay = arrival - result.source_latency; - result.capture_time = getEndPointCaptureTime(path, capture_clock, result.capture_transition, analysis_type, capture_index) + arrival + result.reconvergence; + result.capture_time = getEndPointCaptureTime(path, capture_clock, result.capture_transition, analysis_type, capture_index, false, + result.type == TimingCheckType::kClockGatingHold) + + arrival + result.reconvergence; const double signed_check = analysis_type == AnalysisType::kMin ? result.check_time : -result.check_time; result.required_before_round = result.capture_time + signed_check + (analysis_type == AnalysisType::kMin ? result.uncertainty : -result.uncertainty); result.required = _query.roundTime(result.required_before_round); @@ -230,31 +238,41 @@ PathChecker::CheckTimeResult PathChecker::checkTime(const TimingPathState& path, std::optional result; for (double data_slew : data_slews) for (double clock_slew : clock_slews) { - // Keep the same candidate reduction as CheckDelayCalculator, but also - // retain the Liberty variant that produced the reduced value. The - // outer reduction over data/clock slew extrema is intentionally still - // max-of-check, matching the existing PathChecker semantics for both - // setup/recovery and hold/removal. - std::optional candidate_result; + // Both setup and hold must satisfy the largest applicable constraint. + // Minimum data arrival does not imply a minimum hold requirement. + // Compare after derating, which can reorder negative constraints. for (TimingArc& timing_arc : arc.get_timing_arc_list()) { + if (!isCheckVariantEnabled(arc, timing_arc)) + continue; const auto table = timing_arc.get_check_table_map().find(path.get_trans_type()); if (table == timing_arc.get_check_table_map().end()) continue; - const double nominal - = table->second.findValue(clock_slew * timing_arc.get_time_unit_scale(), data_slew * timing_arc.get_time_unit_scale()) - / timing_arc.get_time_unit_scale(); - if (!candidate_result - || (analysis == AnalysisType::kMin ? nominal < candidate_result->value : nominal > candidate_result->value)) { - candidate_result = CheckTimeResult{nominal, timing_arc.get_arc_idx()}; - } + const double nominal = table->second.findValue(clock_slew * timing_arc.get_time_unit_scale(), data_slew * timing_arc.get_time_unit_scale()) + / timing_arc.get_time_unit_scale(); + const double check = _query.deratedDelay(nominal, analysis, false, path.get_trans_type(), DerateDelay::kCheck, arc.get_data_port()); + if (!result || check > result->value) + result = CheckTimeResult{check, timing_arc.get_arc_idx()}; } - - const double nominal = candidate_result ? candidate_result->value : arc.get_check_time(); - const double check = _query.deratedDelay(nominal, analysis, false, path.get_trans_type(), DerateDelay::kCheck, arc.get_data_port()); - if (!result || check > result->value) - result = CheckTimeResult{check, candidate_result ? candidate_result->selected_arc_idx : -1}; } - return result.value_or(CheckTimeResult{_query.deratedDelay(arc.get_check_time(), analysis, false, path.get_trans_type(), DerateDelay::kCheck, arc.get_data_port()), -1}); + return result.value_or( + CheckTimeResult{_query.deratedDelay(arc.get_check_time(), analysis, false, path.get_trans_type(), DerateDelay::kCheck, arc.get_data_port()), -1}); +} + +bool PathChecker::isCheckVariantEnabled(const TimingCheckArc& arc, const TimingArc& variant) const +{ + const auto& condition = variant.get_when_expression(); + if (condition.get_is_empty()) + return true; + const auto& pin = _database.network().pins().at(arc.get_data_port()); + std::map values; + for (const auto& term : condition.get_term_list()) { + if (term.get_operation_type() != LogicOperationType::kPort) + continue; + const auto value = _database.graph().simValues().value(pin.get_instance_name() + ":" + term.get_port_name()); + if (value && (*value == TimingCaseValue::kZero || *value == TimingCaseValue::kOne)) + values[term.get_port_name()] = *value == TimingCaseValue::kOne; + } + return condition.evaluate_constant(values) != std::optional(false); } bool PathChecker::isMatchCheckTransType(TimingCheckArc& timing_check_arc, TransType data_trans_type) @@ -263,7 +281,7 @@ bool PathChecker::isMatchCheckTransType(TimingCheckArc& timing_check_arc, TransT return true; } for (TimingArc& timing_arc : timing_check_arc.get_timing_arc_list()) { - if (timing_arc.get_check_table_map().count(data_trans_type) > 0) { + if (timing_arc.get_check_table_map().count(data_trans_type) > 0 && isCheckVariantEnabled(timing_check_arc, timing_arc)) { return true; } } @@ -282,7 +300,7 @@ AnalysisType PathChecker::getCaptureAnalysisType(AnalysisType analysis_type) } double PathChecker::getEndPointCaptureTime(const TimingPathState& path, std::string_view capture_clock, TransType capture_transition, - AnalysisType analysis_type, std::size_t capture_index, bool level_sensitive) + AnalysisType analysis_type, std::size_t capture_index, bool level_sensitive, bool gating_hold) { const auto& clocks = _database.sdc().get_clock_map(); const auto launch = clocks.find(path.get_clock_name()); @@ -292,6 +310,22 @@ double PathChecker::getEndPointCaptureTime(const TimingPathState& path, std::str const double launch_edge = launch->second.edge(path.get_launch_clock_edge_index().value_or(path.get_launch_clock_trans_type() == TransType::kFall ? 1 : 0)).time; const double capture_edge = capture->second.edge(capture_index).time; + if (gating_hold) { + // Pair the launch with the preceding opening, then add the active width. + // The minimum launch offset after an opening repeats modulo gcd(periods), + // so this selects the worst hold window across unequal clock periods too. + // Ordinary hold against the trailing edge instead misses an active pulse. + const auto next_index = capture_index + 1; + const auto count = capture->second.edgeCount(); + const double opening = capture->second.edge((capture_index + count - 1) % count).time - (capture_index == 0 ? capture->second.get_period() : 0.); + const double next_open = capture->second.edge(next_index % count).time + (next_index / count) * capture->second.get_period(); + const double offset = -clockEdgeSeparation(launch->second.get_period(), capture->second.get_period(), launch_edge, opening, AnalysisType::kMin); + // A multi-pulse waveform can have no launch in this opening-to-opening + // interval. Its neighboring window owns that launch instead. + if (offset >= next_open - opening - capture->second.get_period() * 1e-10) + return std::numeric_limits::quiet_NaN(); + return launch_edge + capture_edge - opening - offset; + } return launch_edge + clockEdgeSeparation(launch->second.get_period(), capture->second.get_period(), launch_edge, capture_edge, analysis_type, level_sensitive); } @@ -409,9 +443,8 @@ double PathChecker::getClockReconvergencePessimism(const TimingPathState& path, return 0.0; if (launch_transition == TransType::kNone) launch_transition = TransType::kRise; - return _reconvergence.correction(launch_pin, launch_transition, path.get_clock_name(), check.capture_pin, - check.capture_pin_transition, check.capture_clock, analysis, check.common_pin, - path.get_launch_clock_trans_type(), check.capture_transition); + return _reconvergence.correction(launch_pin, launch_transition, path.get_clock_name(), check.capture_pin, check.capture_pin_transition, check.capture_clock, + analysis, check.common_pin, path.get_launch_clock_trans_type(), check.capture_transition); } std::vector PathChecker::getCheckArcs(const std::string& end_point, AnalysisType analysis) @@ -421,6 +454,11 @@ std::vector PathChecker::getCheckArcs(const std::string& end_po const auto instance = _database.network().instances().find(pin.get_instance_name()); if (pin.get_is_port() || instance == _database.network().instances().end()) return arcs; + const auto gating = _database.graph().clockGatingChecks().find(end_point); + if (gating != _database.graph().clockGatingChecks().end()) + for (auto& arc : gating->second) + if (isMatchCheckType(arc, analysis)) + arcs.push_back(&arc); for (auto& arc : instance->second.get_check_arc_list()) if (arc.get_data_port() == end_point && isMatchCheckType(arc, analysis)) arcs.push_back(&arc); @@ -430,13 +468,14 @@ std::vector PathChecker::getCheckArcs(const std::string& end_po bool PathChecker::isMatchCheckType(TimingCheckArc& timing_check_arc, AnalysisType analysis_type) { if (analysis_type == AnalysisType::kMin) { - return timing_check_arc.get_check_type() == TimingCheckType::kHold || timing_check_arc.get_check_type() == TimingCheckType::kRemoval; + return timing_check_arc.get_check_type() == TimingCheckType::kHold || timing_check_arc.get_check_type() == TimingCheckType::kRemoval + || timing_check_arc.get_check_type() == TimingCheckType::kClockGatingHold; } - return timing_check_arc.get_check_type() == TimingCheckType::kSetup || timing_check_arc.get_check_type() == TimingCheckType::kRecovery; + return timing_check_arc.get_check_type() == TimingCheckType::kSetup || timing_check_arc.get_check_type() == TimingCheckType::kRecovery + || timing_check_arc.get_check_type() == TimingCheckType::kClockGatingSetup; } -double PathChecker::calcPathRequiredTime(const std::string& end_point, const TimingPathState& end_path_state, AnalysisType analysis_type, - PathCheck& check) +double PathChecker::calcPathRequiredTime(const std::string& end_point, const TimingPathState& end_path_state, AnalysisType analysis_type, PathCheck& check) { const ResolvedTimingExceptions exceptions = ExceptionMatcher(_database.sdc(), _database.network()) @@ -464,10 +503,10 @@ double PathChecker::calcPathDelayRequiredTime(const std::string& end_point, cons = _database.network().pins().at(end_point).get_is_port() || check.capture_pin.empty() ? 0.0 : _query.getClockArrival(check.capture_pin, capture_clock, getCaptureAnalysisType(analysis_type), check.capture_pin_transition, capture_transition); - const double normal_capture_time - = getEndPointCaptureTime(end_path_state, capture_clock, capture_transition, analysis_type, - check.capture_edge_index.value_or(capture_transition == TransType::kFall ? 1 : 0), check.level_sensitive) - + capture_arrival; + const double normal_capture_time = getEndPointCaptureTime(end_path_state, capture_clock, capture_transition, analysis_type, + check.capture_edge_index.value_or(capture_transition == TransType::kFall ? 1 : 0), + check.level_sensitive, check.type == TimingCheckType::kClockGatingHold) + + capture_arrival; double endpoint_margin = check.required - normal_capture_time; if (database.network().pins().at(end_point).get_is_port() && !check.output_delay) endpoint_margin = 0.0; @@ -487,8 +526,7 @@ double PathChecker::calcPathDelayRequiredTime(const std::string& end_point, cons return _query.roundTime(anchor + exception.get_delay() + endpoint_margin); } -double PathChecker::multicycleAdjustment(const TimingPathState& path, AnalysisType analysis, const ResolvedTimingExceptions& exceptions, - const PathCheck& check) +double PathChecker::multicycleAdjustment(const TimingPathState& path, AnalysisType analysis, const ResolvedTimingExceptions& exceptions, const PathCheck& check) { double adjustment = 0.0; if (exceptions.setup_multicycle && exceptions.setup_multicycle->get_setup_multiplier()) { diff --git a/src/operation/iSTA/source/module/timing_checker/PathChecker.hpp b/src/operation/iSTA/source/module/timing_checker/PathChecker.hpp index dd5df7b40..10f448504 100644 --- a/src/operation/iSTA/source/module/timing_checker/PathChecker.hpp +++ b/src/operation/iSTA/source/module/timing_checker/PathChecker.hpp @@ -37,16 +37,18 @@ class PathChecker public: PathChecker(Database& database, DelayCalculator& delay_calculator, ClockReconvergenceCalculator::CacheMode clock_cache = ClockReconvergenceCalculator::CacheMode::kNone) - : _database(database), _delay_calculator(delay_calculator), _query(database), _clock_pin_values(database, clock_cache), _reconvergence(database, clock_cache) + : _database(database), + _delay_calculator(delay_calculator), + _query(database), + _clock_pin_values(database, clock_cache), + _reconvergence(database, clock_cache) { } // Return every legal capture/check candidate for this propagated path. The // winner-only checkPath() API remains the reduction used by setup analysis; // min/hold exporters use this API to preserve launch/capture context. - std::vector checkPathCandidates(const TimingPathState& path, const std::string& end_point, AnalysisType analysis, - bool include_reconvergence = true); - std::optional checkPath(const TimingPathState& path, const std::string& end_point, AnalysisType analysis, - bool include_reconvergence = true); + std::vector checkPathCandidates(const TimingPathState& path, const std::string& end_point, AnalysisType analysis, bool include_reconvergence = true); + std::optional checkPath(const TimingPathState& path, const std::string& end_point, AnalysisType analysis, bool include_reconvergence = true); double calcPathSlack(const TimingPathState& path, double required, AnalysisType analysis); private: @@ -63,21 +65,22 @@ class PathChecker ClockPinValuesQuery _clock_pin_values; ClockReconvergenceCalculator _reconvergence; - PathCheck evaluateRegister(const TimingPathState& path, TimingCheckArc& arc, AnalysisType analysis, std::string_view capture_clock, - std::size_t capture_index, bool include_reconvergence); + PathCheck evaluateRegister(const TimingPathState& path, TimingCheckArc& arc, AnalysisType analysis, std::string_view capture_clock, std::size_t capture_index, + bool include_reconvergence); std::vector evaluateOutput(const TimingPathState& path, const std::string& end_point, AnalysisType analysis, TransType data_transition, bool include_reconvergence); std::vector getCheckArcs(const std::string& end_point, AnalysisType analysis); std::vector getCaptureClockNames(const std::string& clock_pin); std::vector getOutputDelayList(const std::string& end_point, AnalysisType analysis, TransType transition); bool isMatchCheckTransType(TimingCheckArc& arc, TransType transition); + bool isCheckVariantEnabled(const TimingCheckArc& arc, const TimingArc& variant) const; bool isMatchCheckType(TimingCheckArc& arc, AnalysisType analysis); std::string_view getClockName(const std::string& pin_name); std::string_view getOutputClockName(const std::string& end_point); AnalysisType getCaptureAnalysisType(AnalysisType analysis); double getClockEdge(std::string_view clock_name, TransType transition); double getEndPointCaptureTime(const TimingPathState& path, std::string_view capture_clock, TransType capture_transition, AnalysisType analysis, - std::size_t capture_index, bool level_sensitive = false); + std::size_t capture_index, bool level_sensitive = false, bool gating_hold = false); double getClockUncertainty(std::string_view launch_clock, TransType launch_transition, std::string_view capture_clock, TransType capture_transition, AnalysisType analysis, const std::string& capture_pin, std::optional local = std::nullopt); double getClockReconvergencePessimism(const TimingPathState& path, PathCheck& check, AnalysisType analysis); diff --git a/src/operation/iSTA/source/module/timing_propagator/PathStatePruning.hpp b/src/operation/iSTA/source/module/timing_propagator/PathStatePruning.hpp index 4b767d320..4b05344c9 100644 --- a/src/operation/iSTA/source/module/timing_propagator/PathStatePruning.hpp +++ b/src/operation/iSTA/source/module/timing_propagator/PathStatePruning.hpp @@ -19,6 +19,8 @@ class PathStatePruning bool canPruneTo(const std::string& endpoint, const std::string& launch) const { const auto& graph = _database.graph(); + if (graph.clockGatingChecks().contains(endpoint)) + return false; for (const auto& [mode, sources] : graph.points().at(endpoint).get_path_state_map()) for (const auto& [source, edges] : sources) for (const auto& [edge, states] : edges) @@ -35,8 +37,7 @@ class PathStatePruning const auto definition = sdc.get_clock_map().find(clock); if (definition == sdc.get_clock_map().end()) return false; - const auto origins = clock_pin.empty() ? std::vector{local} - : clockSourceTransitions(_database, clock_pin, clock, capture_mode, local); + const auto origins = clock_pin.empty() ? std::vector{local} : clockSourceTransitions(_database, clock_pin, clock, capture_mode, local); for (auto origin : origins) { has_capture = true; // Empty exception progress deliberately evaluates only clock groups. @@ -95,8 +96,8 @@ class PathStatePruning } private: - bool hasPathDelay(const std::string& launch, const std::string& endpoint, const std::string& capture, AnalysisType mode, - TransType data_edge, TransType capture_edge, bool capture_known = true) const + bool hasPathDelay(const std::string& launch, const std::string& endpoint, const std::string& capture, AnalysisType mode, TransType data_edge, + TransType capture_edge, bool capture_known = true) const { for (const auto& exception : _database.sdc().get_path_exception_list()) { if (exception.get_type() != (mode == AnalysisType::kMax ? TimingExceptionType::kMaxDelay : TimingExceptionType::kMinDelay) @@ -109,8 +110,7 @@ class PathStatePruning && !from.contains({ExceptionObjectType::kClock, launch})) continue; const auto& to = exception.get_to_objects(); - if (!capture_known - && std::any_of(to.begin(), to.end(), [](const auto& object) { return object.type == ExceptionObjectType::kClock; })) + if (!capture_known && std::any_of(to.begin(), to.end(), [](const auto& object) { return object.type == ExceptionObjectType::kClock; })) return true; if (!_matcher.matchesEndpointSelector(to, endpoint, capture, false)) continue; diff --git a/src/operation/iSTA/source/module/timing_reporter/path/PathReportWriter.cpp b/src/operation/iSTA/source/module/timing_reporter/path/PathReportWriter.cpp index c733693c8..7935dcd4c 100644 --- a/src/operation/iSTA/source/module/timing_reporter/path/PathReportWriter.cpp +++ b/src/operation/iSTA/source/module/timing_reporter/path/PathReportWriter.cpp @@ -293,7 +293,8 @@ void PathReportWriter::outputRequiredClockInfo(std::ostream& report_file, Timing if (timing_path.get_check_type() != TimingCheckType::kNone) { const double check = (delay_type == DelayType::kMin || timing_path.get_check_type() == TimingCheckType::kRecovery ? 1 : -1) * timing_path.get_check_time(); path += check; - if (std::fabs(check) > ista::kTimingTolerance) + if (std::fabs(check) > ista::kTimingTolerance || timing_path.get_check_type() == TimingCheckType::kClockGatingSetup + || timing_path.get_check_type() == TimingCheckType::kClockGatingHold) outputTimingLine(report_file, _report_path_context.getLibraryCheckText(timing_path, delay_type), check, path, true, "", label_width); } else if (const auto delay = timing_path.get_output_delay()) { path -= *delay; diff --git a/src/operation/iSTA/source/module/timing_reporter/path/ReportPathContext.cpp b/src/operation/iSTA/source/module/timing_reporter/path/ReportPathContext.cpp index 509bfb4b4..921dec2a0 100644 --- a/src/operation/iSTA/source/module/timing_reporter/path/ReportPathContext.cpp +++ b/src/operation/iSTA/source/module/timing_reporter/path/ReportPathContext.cpp @@ -224,6 +224,8 @@ std::string ReportPathContext::getEndPointCheckText(TimingPath& path) { const bool falling = path.get_capture_clock_pin_transition() == TransType::kFall; const auto& clock = path.get_capture_clock_name(); + if (path.get_check_type() == TimingCheckType::kClockGatingSetup || path.get_check_type() == TimingCheckType::kClockGatingHold) + return std::string("(clock gating check against ") + (falling ? "falling-edge clock " : "rising-edge clock ") + clock + ")"; if (path.get_check_type() == TimingCheckType::kRecovery || path.get_check_type() == TimingCheckType::kRemoval) return std::string(path.get_check_type() == TimingCheckType::kRecovery ? "(recovery check against " : "(removal check against ") + (falling ? "falling-edge clock " : "rising-edge clock ") + clock + ")"; @@ -348,6 +350,10 @@ std::string ReportPathContext::getReportPinName(const std::string& pin_name) std::string ReportPathContext::getLibraryCheckText(TimingPath& timing_path, DelayType delay_type) { + if (timing_path.get_check_type() == TimingCheckType::kClockGatingSetup) + return "clock gating setup time"; + if (timing_path.get_check_type() == TimingCheckType::kClockGatingHold) + return "clock gating hold time"; if (timing_path.get_check_type() == TimingCheckType::kRecovery) { return "library recovery time"; } diff --git a/src/operation/iSTA/test/CMakeLists.txt b/src/operation/iSTA/test/CMakeLists.txt index 0021efafc..3afb6c646 100644 --- a/src/operation/iSTA/test/CMakeLists.txt +++ b/src/operation/iSTA/test/CMakeLists.txt @@ -265,7 +265,7 @@ add_test(NAME ista_named_clock_startpoint COMMAND test_ista_clock_propagation so add_executable(test_ista_sdc_identity EXCLUDE_FROM_ALL sdc_identity_test.cpp) add_executable(test_ista_sdc_limits EXCLUDE_FROM_ALL sdc_limits_test.cpp) target_link_libraries(test_ista_sdc_limits PRIVATE ista_sdc_writer ista_sdc_frontend ista_data_manager ista_graph_query) -foreach(case transition capacitance derate corners scoped check force hierarchy pulse) +foreach(case transition capacitance derate corners scoped check force hierarchy pulse abbreviation) add_test(NAME ista_sdc_limits_${case} COMMAND test_ista_sdc_limits ${case}) endforeach() target_link_libraries(test_ista_sdc_identity PRIVATE ista_sdc_writer ista_sdc_frontend ista_data_manager) @@ -345,7 +345,7 @@ add_test(NAME ista_clock_check_reader COMMAND test_ista_clock_check_reader ${CMA add_executable(test_ista_sdc_semantics EXCLUDE_FROM_ALL sdc_semantics_test.cpp) target_link_libraries(test_ista_sdc_semantics PRIVATE ista_sdc_writer ista_sdc_frontend ista_data_manager ista_graph_query) -foreach(case uncertainty reset groups generated waveform latency local_clock driving units status empty_collection writer_names removed_refs removed_source clock_stages) +foreach(case uncertainty reset groups generated waveform latency local_clock driving units status empty_collection writer_names removed_refs removed_source clock_stages false_startpoint) add_test(NAME ista_sdc_semantics_${case} COMMAND test_ista_sdc_semantics ${case}) endforeach() @@ -399,3 +399,8 @@ add_executable(test_ista_sdc_rebuild EXCLUDE_FROM_ALL sdc_rebuild_test.cpp) target_link_libraries(test_ista_sdc_rebuild PRIVATE ista_timing_analyzer ista_delay_calculator ista_sdc_writer ista_sdc_frontend) add_test(NAME ista_sdc_rebuild COMMAND test_ista_sdc_rebuild) set_tests_properties(ista_sdc_rebuild PROPERTIES TIMEOUT 60) + +add_executable(test_ista_clock_gating EXCLUDE_FROM_ALL clock_gating_test.cpp) +target_link_libraries(test_ista_clock_gating PRIVATE ista_clock_checker ista_path_checker ista_delay_calculator ista_sdc ista_interface ista_data_exporter) +add_test(NAME ista_clock_gating COMMAND test_ista_clock_gating) +set_tests_properties(ista_clock_gating PROPERTIES TIMEOUT 30) diff --git a/src/operation/iSTA/test/clock_gating_test.cpp b/src/operation/iSTA/test/clock_gating_test.cpp new file mode 100644 index 000000000..9e9fedf41 --- /dev/null +++ b/src/operation/iSTA/test/clock_gating_test.cpp @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: MulanPSL-2.0 +#include +#include +#include + +#include "ClockGatingChecker.hpp" +#include "DelayCalculator.hpp" +#include "MinTimingDataExporter.hpp" +#include "PathChecker.hpp" +#include "TimingDataExporter.hpp" +#include "sdc/ExceptionMatcher.hpp" +using namespace ista; +namespace { +void require(bool condition, const char* message) +{ + if (!condition) + throw std::runtime_error(message); +} +void near(double actual, double expected, const char* message) +{ + require(std::abs(actual - expected) < 1e-9, message); +} +Database design(bool high, bool invert_clock, bool invert_output) +{ + Database db; + auto& gate = db.network().instances()["gate"]; + gate.set_instance_name("gate"); + gate.set_cell_name("GATE"); + auto& ff = db.network().instances()["ff"]; + ff.set_instance_name("ff"); + ff.set_is_sequential(true); + ff.set_clock_pin_name("ff:CK"); + for (const auto& name : {"gate:C", "gate:E", "gate:Y", "ff:CK"}) { + const std::string full = name; + auto& pin = db.network().pins()[full]; + pin.set_full_name(full); + pin.set_instance_name(full.substr(0, full.find(':'))); + pin.set_pin_name(full.substr(full.find(':') + 1)); + db.graph().points()[full]; + } + db.network().pins()["in"].set_is_port(true); + auto& output = db.liberty().get_cell_map()["GATE"].get_port_map()["Y"]; + output.set_is_output(true); + LogicExpression expression; + auto term = [&](LogicOperationType op, const char* port = "") { + LogicExpressionTerm value; + value.set_operation_type(op); + value.set_port_name(port); + expression.get_term_list().push_back(value); + }; + term(LogicOperationType::kPort, "C"); + if (invert_clock) + term(LogicOperationType::kNot); + term(LogicOperationType::kPort, "E"); + term(LogicOperationType::kNot); // NAND2B-style inverted enable. + term(high ? LogicOperationType::kAnd : LogicOperationType::kOr); + if (invert_output) + term(LogicOperationType::kNot); + output.set_function_expression(expression); + auto arc = [&](const char* from, const char* to, ArcType type) { + Arc a; + a.set_source_pin(from); + a.set_sink_pin(to); + a.set_type(type); + auto i = db.graph().arcs().size(); + db.graph().arcs().push_back(a); + db.graph().incomingArcs()[to].push_back(i); + db.graph().outgoingArcs()[from].push_back(i); + }; + arc("gate:C", "gate:Y", ArcType::kCell); + arc("gate:E", "gate:Y", ArcType::kCell); + arc("gate:Y", "ff:CK", ArcType::kNet); + for (const auto* name : {"clk", "launch"}) { + auto& clock = db.sdc().get_clock_map()[name]; + clock.set_clock_name(name); + clock.set_period(10); + clock.set_fall_edge(3); + } + for (const auto* pin : {"gate:C", "gate:Y", "ff:CK"}) { + auto& point = db.graph().points()[pin]; + point.set_is_clock_point(true); + for (auto mode : {AnalysisType::kMin, AnalysisType::kMax}) + for (auto edge : {TransType::kRise, TransType::kFall}) { + auto& wave = point.get_clock_state("clk").waveforms[edge]; + wave.arrival_map[mode][edge] = 0.; + wave.slew_map[mode][edge] = .03; + } + } + db.graph().endpoints().push_back("ff:D"); // Existing structural endpoint must survive reset. + return db; +} +void checkGate(bool high, bool invert_clock, bool invert_output) +{ + auto db = design(high, invert_clock, invert_output); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().at("gate:E").size() == 2, "missing inferred setup/hold pair"); + require(TimingStateQuery(db).isConstrainedEndPoint("gate:E"), "inferred gate is not a constrained endpoint"); + Config config; + DelayCalculator delay(db, config); + PathChecker checker(db, delay); + TimingPathState path; + path.set_start_point("in"); + path.set_clock_name("launch"); + path.set_launch_clock_trans_type(TransType::kRise); + path.set_trans_type(TransType::kRise); + path.set_slew(.03); + const bool active_high = high != invert_clock; + for (double launch : {0., 3., 7.}) { + db.sdc().get_clock_map().at("launch").set_rise_edge(launch); + db.sdc().get_clock_map().at("launch").set_fall_edge(launch + 2.); + path.set_arrival(launch + .2); + path.set_launch_time(launch); + const auto hold = checker.checkPath(path, "gate:E", AnalysisType::kMin); + const auto setup = checker.checkPath(path, "gate:E", AnalysisType::kMax); + require(hold && setup, "clock gate disappeared from path checking"); + const double hold_edge = active_high ? 3. : launch < 3. ? 0. : 10.; + const double setup_edge = active_high ? 10. : launch < 3. ? 3. : 13.; + near(hold->required(), hold_edge, "wrong gate closing edge"); + near(setup->required(), setup_edge, "wrong gate opening edge"); + require(hold->check().type == TimingCheckType::kClockGatingHold, "gate lost its report identity"); + } + TimingException budget; + budget.set_type(TimingExceptionType::kMinDelay); + budget.set_delay(.4); + budget.set_to_objects({{ExceptionObjectType::kPin, "gate:E"}}); + db.sdc().get_path_exception_list().push_back(budget); + path.set_exception_state_list(ExceptionMatcher(db.sdc(), db.network()).initState("in", "launch", TransType::kRise, AnalysisType::kMin, TransType::kRise)); + near(checker.checkPath(path, "gate:E", AnalysisType::kMin)->required(), 7.4, "gate min-delay margin contains an extra clock cycle"); + db.sdc().get_path_exception_list().front().set_type(TimingExceptionType::kFalsePath); + require(!checker.checkPath(path, "gate:E", AnalysisType::kMin), "false path did not disable gate check"); + ClockGatingChecker(db).infer(); + require(std::count(db.graph().endpoints().begin(), db.graph().endpoints().end(), "gate:E") == 1, "reanalysis duplicated gate endpoint"); + Graph copied = db.graph(); + copied.clearTiming(); + require(copied.clockGatingChecks().empty() && copied.endpoints() == std::vector{"ff:D"}, "graph copy/reset retained derived endpoint"); + require(db.graph().clockGatingChecks().size() == 1, "graph copy reset mutated source"); + db.sdc().get_path_exception_list().clear(); + path.set_exception_state_list({}); + require(checker.checkPath(path, "gate:E", AnalysisType::kMin).has_value(), "export fixture has no native gate check"); + db.graph().points().at("gate:E").get_path_state_map()[AnalysisType::kMin][PathSourceType::kInput][TransType::kRise]["gate"] = path; + // Model preparation has clock topology but deliberately no numeric waveforms. + for (auto& [name, point] : db.graph().points()) + for (auto& [clock, state] : point.get_clock_state_map()) { + state.waveforms.clear(); + state.is_structural = true; + } + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().size() == 1, "structural model silently lost gate endpoints"); + auto snapshot = TimingDataExporter::exportData(db, config); + exportMinTimingModel(snapshot, db, config); + require(snapshot.min_contexts.empty(), "unknown gate check enum leaked into external hold schema"); + require(std::any_of(snapshot.diagnostics.begin(), snapshot.diagnostics.end(), + [](const auto& message) { return message.find("inferred clock-gating check") != std::string::npos; }), + "external model did not disclose unsupported gate check"); + auto exported_pin = std::find_if(snapshot.pins.begin(), snapshot.pins.end(), [](const auto& pin) { return pin.name == "gate:E"; }); + require(exported_pin != snapshot.pins.end() && !exported_pin->max_qualification.valid[0] && !exported_pin->min_qualification.valid[0] + && exported_pin->max_qualification.reason[0] == TimingMaxQualificationReason::kUnsupportedConstraint + && exported_pin->min_qualification.reason[0] == TimingMinQualificationReason::kUnsupportedConstraint, + "external model falsely qualified gate without a compatible check rule"); + db.graph().simValues().setValue("gate:E", TimingCaseValue::kZero); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().empty(), "constant enable kept a gate check"); + db.graph().simValues().clear(); + db.graph().arcs().back().set_is_disable_arc(true); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().empty(), "gate with no active sequential clock fanout was checked"); +} +} // namespace +int main() +{ + for (bool high : {true, false}) + for (bool inverted_clock : {false, true}) + for (bool inverted_output : {false, true}) + checkGate(high, inverted_clock, inverted_output); + { + auto db = design(true, false, false); + // Exported clocks qualify even without a generated-clock definition or a + // downstream register. Ordinary data fanout does not qualify. + db.network().instances().at("ff").set_is_sequential(false); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().empty(), "ordinary data fanout enabled gate checks"); + auto& output = db.network().pins().at("ff:CK"); + output.set_is_port(true); + output.set_direction(PinDirection::kOutput); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().contains("gate:E"), "exported clock lost gate checks without a generated clock"); + db.graph().arcs().back().set_is_disable_arc(true); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().empty(), "disabled output net enabled gate checks"); + db.graph().arcs().back().set_is_disable_arc(false); + auto& root = db.sdc().get_clock_map()["independent"]; + root.set_clock_name("independent"); + root.set_source_objects({"ff:CK"}); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().empty(), "output primary-clock boundary enabled upstream gate checks"); + } + { + auto db = design(true, false, false); + ClockGatingChecker(db).infer(); + Config config; + DelayCalculator delay(db, config); + PathChecker checker(db, delay); + TimingPathState path; + path.set_start_point("in"); + path.set_clock_name("launch"); + path.set_launch_clock_trans_type(TransType::kRise); + path.set_trans_type(TransType::kRise); + auto& launch = db.sdc().get_clock_map().at("launch"); + launch.set_period(6.); + for (const auto [phase, required] : {std::pair{0., 3.}, std::pair{1., 3.}, std::pair{3., 5.}}) { + launch.set_rise_edge(phase); + launch.set_fall_edge(phase + 2.); + near(checker.checkPath(path, "gate:E", AnalysisType::kMin)->required(), required, "different periods lost the worst gate hold window across cycles"); + } + launch.set_period(10.); + db.sdc().get_clock_map().at("clk").set_waveform({0., 2., 6., 8.}); + for (const auto [phase, required] : {std::pair{0., 2.}, std::pair{3., 2.}, std::pair{6., 8.}, std::pair{9., 8.}}) { + launch.set_rise_edge(phase); + launch.set_fall_edge(phase + .5); + near(checker.checkPath(path, "gate:E", AnalysisType::kMin)->required(), required, "multi-pulse clock paired gate hold with another active window"); + } + } + for (bool high : {true, false}) { + auto db = design(high, false, false); + auto& capture = db.sdc().get_clock_map().at("clk"); + capture.set_period(6.); + capture.set_fall_edge(2.); + ClockGatingChecker(db).infer(); + Config config; + DelayCalculator delay(db, config); + PathChecker checker(db, delay); + TimingPathState path; + path.set_start_point("in"); + path.set_clock_name("launch"); + path.set_launch_clock_trans_type(TransType::kRise); + path.set_trans_type(TransType::kRise); + for (double phase : {0., 1., 3.}) { + auto& launch = db.sdc().get_clock_map().at("launch"); + launch.set_rise_edge(phase); + launch.set_fall_edge(phase + .5); + const double required = high ? (phase < 3. ? 2. : 4.) : (phase < 3. ? 4. : 6.); + near(checker.checkPath(path, "gate:E", AnalysisType::kMin)->required(), required, "reverse period ratio selected a wrong hold cycle"); + } + } + { + auto db = design(true, false, false); + auto& state = db.graph().points().at("gate:C").get_clock_state("clk"); + for (auto source : {TransType::kRise, TransType::kFall}) { + auto& wave = state.waveforms[source]; + wave.arrival_map.clear(); + wave.slew_map.clear(); + for (auto mode : {AnalysisType::kMin, AnalysisType::kMax}) { + const auto local = source == TransType::kRise ? TransType::kFall : TransType::kRise; + wave.arrival_map[mode][local] = 0.; + wave.slew_map[mode][local] = .03; + } + } + ClockGatingChecker(db).infer(); + Config config; + DelayCalculator delay(db, config); + PathChecker checker(db, delay); + TimingPathState path; + path.set_start_point("in"); + path.set_clock_name("launch"); + path.set_launch_clock_trans_type(TransType::kRise); + path.set_trans_type(TransType::kRise); + auto check = checker.checkPath(path, "gate:E", AnalysisType::kMin); + require(check && check->check().capture_transition == TransType::kRise && check->check().capture_pin_transition == TransType::kFall, + "inverted clock lost local/source edge identity"); + near(check->required(), 0., "inverted clock closed on the wrong source edge"); + } + auto db = design(true, false, false); + auto& second = db.sdc().get_clock_map()["second"]; + second.set_clock_name("second"); + second.set_period(20); + second.set_fall_edge(9); + auto& input = db.graph().points().at("gate:C"); + input.get_clock_state("second") = input.get_clock_state("clk"); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().at("gate:E").size() == 2, "unreachable capture clock added a gate check"); + Config config; + DelayCalculator delay(db, config); + PathChecker checker(db, delay); + TimingPathState path; + path.set_start_point("in"); + path.set_clock_name("launch"); + path.set_launch_clock_trans_type(TransType::kRise); + path.set_trans_type(TransType::kRise); + auto checks = checker.checkPathCandidates(path, "gate:E", AnalysisType::kMin); + require(checks.size() == 1 && checks.front().captureClock() == "clk", "gate borrowed another clock on the same input pin"); + auto old = db.graph().points().at("ff:CK").get_clock_state("clk"); + db.graph().points().at("ff:CK").clearClockStates(); + db.graph().points().at("ff:CK").get_clock_state("second") = old; + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().empty(), "gate traversed a separate primary-clock boundary"); + db.graph().points().at("gate:Y").clearClockStates(); + db.graph().points().at("gate:Y").get_clock_state("second") = old; + input.get_clock_state_map().erase("second"); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().empty(), "gate traversed a new primary clock at its output"); + db.network().instances().at("ff").set_is_sequential(false); + auto& generated = db.sdc().get_clock_map()["generated"]; + generated.set_clock_name("generated"); + generated.set_master_clock_name("clk"); + generated.set_source_list({"gate:Y"}); + ClockGatingChecker(db).infer(); + require(db.graph().clockGatingChecks().contains("gate:E"), "exported generated clock lost upstream gate checks without register fanout"); +} diff --git a/src/operation/iSTA/test/driver_response_test.cpp b/src/operation/iSTA/test/driver_response_test.cpp index 1fbca721c..c2863b498 100644 --- a/src/operation/iSTA/test/driver_response_test.cpp +++ b/src/operation/iSTA/test/driver_response_test.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: MulanPSL-2.0 +#include #include +#include #include #include @@ -8,6 +10,7 @@ #include "ArnoldiWaveformSolver.hpp" #include "DmpResponseCalculator.hpp" #include "InputPortResponseCalculator.hpp" +#include "LoadCalculator.hpp" #include "PinCapacitanceCalculator.hpp" #include "WireDelayCalculator.hpp" @@ -17,6 +20,14 @@ void require(bool ok, const char* message) if (!ok) throw std::runtime_error(message); } + +double waveformMoment(const ista::ArnoldiDelayResult& result, std::size_t terminal = 0) +{ + double moment = 0.; + for (std::size_t i = 0; i < result.waveform.get_pole_list().size(); ++i) + moment += result.waveform.get_residue_list()[terminal][i] / result.waveform.get_pole_list()[i]; + return moment; +} void testSmallRcResponse() { using namespace ista; @@ -76,16 +87,13 @@ void testSmallRcResponse() // PT2018 small-network model: 60 fF wire C, 15 fF pin C, 90 Ohm // total R, three receivers. The balanced time constant is 1.8 ps; // source correction and receiver degradation must use the same model. - require(result.get_is_valid() && std::abs(result.get_effective_capacitance() - .075) < 1e-12, - "small RC source did not retain its total capacitive load"); - require(std::abs(result.get_gate_delay() - .4246257005224976) < 1e-9 - && std::abs(result.get_driver_slew() - .399084918347181) < 1e-9, + require(result.get_is_valid() && std::abs(result.get_effective_capacitance() - .075) < 1e-12, "small RC source did not retain its total capacitive load"); + require(std::abs(result.get_gate_delay() - .4246257005224976) < 1e-9 && std::abs(result.get_driver_slew() - .399084918347181) < 1e-9, "small RC source omitted the bounded delay/slew correction"); for (const auto& sink : net.get_load_pin_list()) { require(std::abs(result.get_wire_delay_map().at(sink) - .0012476649250079015) < 1e-9, "small RC receiver used a distributed delay instead of the selected lumped model"); - require(std::abs(result.get_load_slew_map().at(sink) - .399096574) < 1e-8, - "small RC receiver lost slew degradation"); + require(std::abs(result.get_load_slew_map().at(sink) - .399096574) < 1e-8, "small RC receiver lost slew degradation"); } } auto reset = [&]() { @@ -93,11 +101,27 @@ void testSmallRcResponse() models.clear(); rc.clear(); }; + config.small_rc_fanout_reduction = 0; + reset(); + require(!models.arnoldiModel(parasitic, source, AnalysisType::kMax, TransType::kRise).smallNetTimeConstants(), + "disabled fanout reduction still selected the enhanced small-RC model"); + config.small_rc_fanout_reduction = 1; + reset(); + // A four-load signoff probe uses ordinary total RC when it already fits + // below 2 ps. The enhanced fanout reduction only extends that range. + for (auto& resistor : parasitic.get_resistor_list()) + resistor.set_resistance(resistor.get_resistance() * .2); + reset(); + const auto& ordinary = calculator.response(source, timing, AnalysisType::kMax, TransType::kRise, .1); + require(std::abs(ordinary.get_gate_delay() - .425) < 1e-12 && std::abs(ordinary.get_wire_delay_map().at("a:A") - .00135 * std::log(2.)) < 1e-12, + "enhanced small-net reduction overrode an eligible ordinary total-RC model"); + for (auto& resistor : parasitic.get_resistor_list()) + resistor.set_resistance(resistor.get_resistance() / .2); + reset(); cell.get_port_map()["A"].get_trans_capacitance_map()[AnalysisType::kMax][TransType::kFall] = .004; reset(); const auto asymmetric = calculator.response(source, timing, AnalysisType::kMax, TransType::kFall, .1); - require(std::abs(asymmetric.get_gate_delay() - .4156444154963727) < 1e-9 - && std::abs(asymmetric.get_driver_slew() - .38708491831078184) < 1e-9 + require(std::abs(asymmetric.get_gate_delay() - .4156444154963727) < 1e-9 && std::abs(asymmetric.get_driver_slew() - .38708491831078184) < 1e-9 && std::abs(asymmetric.get_wire_delay_map().at("a:A") - .0011852816787575064) < 1e-9 && std::abs(asymmetric.get_load_slew_map().at("a:A") - .38709576462297646) < 1e-9, "small RC fall response lost its edge load or rise-based source-slew correction"); @@ -155,8 +179,7 @@ void testSmallRcResponse() parasitic.get_resistor_list().push_back(extra); if (receiver == 4 || receiver == 5) { reset(); - require(models.arnoldiModel(parasitic, source, AnalysisType::kMax, TransType::kRise).smallNetTimeConstants().has_value() - == (receiver == 4), + require(models.arnoldiModel(parasitic, source, AnalysisType::kMax, TransType::kRise).smallNetTimeConstants().has_value() == (receiver == 4), "balanced small RC selection must include five receivers and exclude six"); } } @@ -177,8 +200,7 @@ void testSmallRcResponse() // Ordinary low RC retains the table source response at 160 fF. Only // the receivers get the 1.6 ps RC degradation; the balanced source // correction belongs to the separate, limited-fanout model. - require(result.get_is_valid() && std::abs(result.get_gate_delay() - .68) < 1e-9 - && std::abs(result.get_driver_slew() - .74) < 1e-9, + require(result.get_is_valid() && std::abs(result.get_gate_delay() - .68) < 1e-9 && std::abs(result.get_driver_slew() - .74) < 1e-9, "ordinary low RC incorrectly corrected the table source response"); for (const auto& sink : net.get_load_pin_list()) { require(std::abs(result.get_wire_delay_map().at(sink) - .0011090354888959125) < 1e-9 @@ -292,8 +314,7 @@ void testDisabledMultidriverFallback() for (auto edge : {TransType::kRise, TransType::kFall}) { if (driving_cell) db.sdc().get_port_constraint_map()[source].set_driving_cell(mode, edge, driver); - require(wire.calcNetArcDelay(arc, mode, edge, 1.) == 0. - && wire.calcNetArcSlew(arc, mode, edge, 1.) == 1., + require(wire.calcNetArcDelay(arc, mode, edge, 1.) == 0. && wire.calcNetArcSlew(arc, mode, edge, 1.) == 1., "unextracted port-to-PAD connection changed the port waveform"); } } @@ -362,8 +383,8 @@ void testDistributedVoltageDomains() double low = 0., high = ramp + 40. * tau; for (int iteration = 0; iteration < 80; ++iteration) { const double time = .5 * (low + high); - const double voltage = time < ramp ? 1. - (time + tau * std::expm1(-time / tau)) / ramp - : tau / ramp * (-std::expm1(-ramp / tau)) * std::exp(-(time - ramp) / tau); + const double voltage + = time < ramp ? 1. - (time + tau * std::expm1(-time / tau)) / ramp : tau / ramp * (-std::expm1(-ramp / tau)) * std::exp(-(time - ramp) / tau); if (voltage > threshold) low = time; else @@ -372,10 +393,9 @@ void testDistributedVoltageDomains() return .5 * (low + high); }; const double ratio = 1.2 / 3.3; - const double expected_slew = edge == TransType::kRise ? (crossing(1. - .7 * ratio) - crossing(1. - .3 * ratio)) / .5 - : (crossing(.3 * ratio) - crossing(.7 * ratio)) / .5; - require(std::abs(response.get_load_slew_map().at(sink) - expected_slew) < 2e-6, - "distributed receiver slew disagrees with physical voltage crossings"); + const double expected_slew + = edge == TransType::kRise ? (crossing(1. - .7 * ratio) - crossing(1. - .3 * ratio)) / .5 : (crossing(.3 * ratio) - crossing(.7 * ratio)) / .5; + require(std::abs(response.get_load_slew_map().at(sink) - expected_slew) < 2e-6, "distributed receiver slew disagrees with physical voltage crossings"); const auto dmp_result = dmp.response(source, timing, mode, edge, .1, .05); require(dmp_result.get_is_valid(), "valid DMP fixture rejected"); const double dmp_delay = dmp_result.get_wire_delay_map().at(sink); @@ -426,9 +446,8 @@ void testDistributedVoltageDomains() for (auto mode : {AnalysisType::kMin, AnalysisType::kMax}) for (auto edge : {TransType::kRise, TransType::kFall}) { const auto mixed = arnoldi.response(source, timing, mode, edge, .1); - require(mixed.get_is_valid() && mixed.loadFailed(sink) && mixed.loadFailed(peer) - && !mixed.get_wire_delay_map().contains(peer) && !mixed.loadFailed(source) - && mixed.get_wire_delay_map().contains(source), + require(mixed.get_is_valid() && mixed.loadFailed(sink) && mixed.loadFailed(peer) && !mixed.get_wire_delay_map().contains(peer) + && !mixed.loadFailed(source) && mixed.get_wire_delay_map().contains(source), "failed shared receiver solve published a sibling's RC delay"); Arc peer_arc; peer_arc.set_type(ArcType::kNet); @@ -436,8 +455,7 @@ void testDistributedVoltageDomains() peer_arc.set_source_pin(source); peer_arc.set_sink_pin(peer); require(wire.calcNetArcDelay(peer_arc, mode, edge, mixed.get_driver_slew()) == 0. - && wire.calcNetArcSlew(peer_arc, mode, edge, mixed.get_driver_slew()) == mixed.get_driver_slew() - && peer_arc.rcFailed(mode, edge), + && wire.calcNetArcSlew(peer_arc, mode, edge, mixed.get_driver_slew()) == mixed.get_driver_slew() && peer_arc.rcFailed(mode, edge), "shared RC failure lost zero-delay/source-slew propagation or its diagnostic"); } // Only the fall window is unreachable; the rise solve and source response @@ -449,8 +467,7 @@ void testDistributedVoltageDomains() for (auto mode : {AnalysisType::kMin, AnalysisType::kMax}) { const auto fall = arnoldi.response(source, timing, mode, TransType::kFall, .1); const auto rise = arnoldi.response(source, timing, mode, TransType::kRise, .1); - require(fall.loadFailed(peer) && !rise.loadFailed(peer) && !rise.loadFailed(sink) - && rise.get_wire_delay_map().at(peer) > 0., + require(fall.loadFailed(peer) && !rise.loadFailed(peer) && !rise.loadFailed(sink) && rise.get_wire_delay_map().at(peer) > 0., "unreachable fall receiver contaminated the rise response"); } // Invalid library metadata is a separate failure from unreachable voltage. @@ -571,8 +588,7 @@ void testDriverFitNearRampEnd() delay.set_value_list({1.2116066217422 + offset, 1.2232328653336 + offset}); const auto result = ArnoldiDelayCalculator{}.calculate(arc, edge, .104165, model, false); require(result && !result->resistance_adjusted, "capacitive driver fit failed near the ramp end"); - require(std::abs(result->drive_resistance - 3.11792628705) < 1e-6 - && std::abs(result->driver_ramp - 1.83916621609) < 1e-6, + require(std::abs(result->drive_resistance - 3.11792628705) < 1e-6 && std::abs(result->driver_ramp - 1.83916621609) < 1e-6, "driver fit near ramp end selected a different waveform"); } } @@ -581,6 +597,7 @@ void testDriverFitNearRampEnd() void testOrdinaryDriverCrossing() { using namespace ista; + ArnoldiDelayCalculator calculator; Database db; Config config; PinCapacitanceCalculator caps(db); @@ -609,28 +626,107 @@ void testOrdinaryDriverCrossing() slew.set_axis_list({{0., 1.}}); slew.set_value_list({.3, 5.9}); const auto& model = models.arnoldiModel(net, source, AnalysisType::kMax, edge); - for (double offset : {.2, -1.2}) { - delay.set_value_list({offset, offset + 2.5}); + // Independent PT U-2022.12-SP3 runs of this two-node SPEF and the same + // synthetic library. Units: ns, pF, kohm. A translated delay table changes + // the stopping tolerance, so positive, near-zero and negative delays need + // separate references. Allow 1 ps for driver-fit/crossing numerics. + for (const auto& reference : + {std::array{.2, .280228, .892813, .127888, 2.067562, 2.089577}, std::array{-.7, .274874, -.011761, .128979, 2.059661, 2.081784}, + std::array{-1.2, .276057, -.510745, .128730, 2.061390, 2.083488}}) { + delay.set_value_list({reference[0], reference[0] + 2.5}); for (bool adjust : {false, true}) { - const auto result = ArnoldiDelayCalculator{}.calculate(arc, edge, .1, model, adjust); + const auto result = calculator.calculate(arc, edge, .1, model, adjust); require(result && !result->resistance_adjusted, "ordinary source spuriously adjusted its resistance"); - // Independent two-node conductance eigensolve, with a ramp fitted to - // D(C)=offset+2.5*C and physical slew=.15+2.8*C. The first midpoint - // update meets the delay residual tolerance; charge matching does not - // represent the same driver waveform. Translation preserves the RC. - require(std::abs(result->effective_capacitance - .280115399739051) < 1e-6 - && std::abs(result->drive_resistance - 3.55357132113458) < 1e-5, - "ordinary driver did not match the distributed source crossing"); - require(std::abs(result->delays[0] - (.698935842717261 + offset)) < 1e-6 - && std::abs(result->delays[1] - (.82772070301579 + offset)) < 1e-6 - && std::abs(result->slews[0] - 2.08341278546534) < 1e-6 - && std::abs(result->slews[1] - 2.10529621563007) < 1e-6, - "ordinary source and receiver do not share the accepted waveform"); + require(std::abs(result->effective_capacitance - reference[1]) < .0002, "ordinary driver did not retain the crossing-matched capacitance"); + require(std::abs(result->delays[0] - reference[2]) < .001 && std::abs(result->delays[1] - result->delays[0] - reference[3]) < .001 + && std::abs(result->slews[0] - reference[4]) < .001 && std::abs(result->slews[1] - reference[5]) < .001, + "ordinary driver differs from the independent distributed-RC reference"); + // The source's first moment is Rdriver * Ctotal, irrespective of the + // internal wire resistance. It exposes the resistance actually used + // by the retained waveform, independently of the reported fitted R. + const double response_resistance = waveformMoment(*result) / model.get_total_capacitance(); + require(std::abs(response_resistance - 3.51619887917337) < 1e-6, "small conductance change rebuilt the accepted network response"); + require(std::abs(response_resistance / result->drive_resistance - 1.) < .05, "retained network response exceeded the conductance tolerance"); } } + delay.set_value_list({.2, 2.7}); + // At 1450 ohm the fitted R rises by over 5%, but G changes by less than + // 5%: retain the initial response. At 1500 ohm, three individually small + // changes accumulate past 5% of the initial G and require a rebuild. + // Repeating the first case also checks that state is local to this call. + for (double resistance : {1450., 1500., 1450.}) { + net.get_resistor_list().front().set_resistance(resistance); + models.clear(); + rc.clear(); + const auto& changed_model = models.arnoldiModel(net, source, AnalysisType::kMax, edge); + const auto result = calculator.calculate(arc, edge, .1, changed_model); + require(result.has_value(), "conductance update lost a valid driver fit"); + const double initial_resistance = 3.51619887917337; + const double response_resistance = waveformMoment(*result) / .3; + if (resistance == 1450.) { + require(result->drive_resistance / initial_resistance - 1. > .05 && std::abs(response_resistance - initial_resistance) < 1e-6, + "waveform reuse tested relative resistance instead of conductance"); + } else { + require(response_resistance > initial_resistance / .95, "successive small conductance changes failed to rebuild the network response"); + } + require(std::abs(response_resistance / result->drive_resistance - 1.) <= .05, "retained response exceeded the conductance tolerance after a rebuild"); + // Independent Elmore moments of this two-node network, in ns. + require(std::abs(waveformMoment(*result, 1) - waveformMoment(*result) - resistance * 1e-3 * .25) < 1e-9, + "source and receiver responses use inconsistent network conductances"); + } + net.get_resistor_list().front().set_resistance(500.); + models.clear(); + rc.clear(); } } +void testSlowChargeConvergence() +{ + using namespace ista; + Database db; + Config config; + PinCapacitanceCalculator caps(db); + RcTreeAnalyzer rc(db, caps); + RcModelBuilder models(db, config, rc); + std::string source = "source", sink = "sink"; + db.network().pins()[source].set_is_port(true); + db.network().pins()[sink].set_is_port(true); + ParasiticNet net; + net.set_net_name("slow_charge"); + net.get_node_map()[source].set_capacitance(.05); + net.get_node_map()[sink].set_capacitance(.25); + ParasiticResistor wire; + wire.set_source_node(source); + wire.set_sink_node(sink); + wire.set_resistance(2000.); + net.get_resistor_list().push_back(wire); + TimingArc arc; + arc.set_slew_derate(1.); + // Use the charge model rather than the symmetric-threshold joint fit. + arc.set_output_threshold_pct_rise(.4); + auto& delay = arc.get_delay_table_map()[TransType::kRise]; + delay.set_variable_type1(TimingTableVariableType::kOutputCapacitance); + delay.set_axis_list({{0., 1.}}); + delay.set_value_list({.2, 1.2}); + auto& slew = arc.get_slew_table_map()[TransType::kRise]; + slew.set_variable_type1(TimingTableVariableType::kOutputCapacitance); + std::vector axis; + for (int i = 0; i <= 20; ++i) + axis.push_back(.05 + .0125 * i); + slew.set_axis_list({axis}); + // Two-node charge integrals give matched(C) = .8*C + .02 at these + // samples, with a fixed point at .1 pF. Residuals strictly decrease, but + // plain iteration cannot meet the 1e-7 tolerance within 64 trials. + slew.set_value_list({.043264782147582934, .059939520477666, .078949225234167142, .099569907430668431, .12183724609500696, .14597167269398151, + .17228386808234372, .20104382574501495, .23230038340054171, .26648174910911981, .30415230336692067, .346033301620566, + .39306814599964024, .4465215804514, .50813708971784999, .58039822357918647, .66698572746244289, .7736291803164822, + .90982274705580224, 1.0926456551786174, 1.3564747657165996}); + const auto& model = models.arnoldiModel(net, source, AnalysisType::kMax, TransType::kRise); + const auto result = ArnoldiDelayCalculator{}.calculate(arc, TransType::kRise, .1, model); + require(result && std::abs(result->effective_capacitance - .1) < 1e-7 && std::abs(result->drive_resistance - 1.) < 1e-12, + "slowly contracting charge fit exhausted its iteration budget"); +} + void testAdjustedDriver() { using namespace ista; @@ -669,16 +765,15 @@ void testAdjustedDriver() const auto& model = models.arnoldiModel(net, source, AnalysisType::kMin, edge); const auto adjusted = ArnoldiDelayCalculator{}.calculate(arc, edge, .1, model, true); require(adjusted && adjusted->resistance_adjusted, "network-limited source did not adjust its driver"); + require(std::abs(waveformMoment(*adjusted) / model.get_total_capacitance() - adjusted->drive_resistance) < 1e-9, + "adjusted driver retained the previous network conductance"); // Independent two-node C*dV/dt + G*V solve. Match the distributed source // midpoint and the lumped table slew; stop at the first delay residual // below 1% (0.25 ps floor). The fixed R follows the upper crossing. - require(std::abs(adjusted->drive_resistance - .7473402774234486) < 1e-10 - && std::abs(adjusted->effective_capacitance - .2671456181245039) < 1e-7, + require(std::abs(adjusted->drive_resistance - .7473402774234486) < 1e-10 && std::abs(adjusted->effective_capacitance - .2671456181245039) < 1e-7, "adjusted driver ignored the delay residual stopping criterion"); - require(std::abs(adjusted->delays[0] - .2621325282500496) < 1e-6 - && std::abs(adjusted->delays[1] - .9058311525795634) < 1e-6 - && std::abs(adjusted->slews[0] - .877862966512199) < 1e-6 - && std::abs(adjusted->slews[1] - 1.8626295004595543) < 1e-6, + require(std::abs(adjusted->delays[0] - .2621325282500496) < 1e-6 && std::abs(adjusted->delays[1] - .9058311525795634) < 1e-6 + && std::abs(adjusted->slews[0] - .877862966512199) < 1e-6 && std::abs(adjusted->slews[1] - 1.8626295004595543) < 1e-6, "adjusted cell and receiver do not share the fitted waveform"); // The same nodal network with translated delay tables exercises a negative // time origin and the absolute residual floor near zero cell delay. @@ -704,10 +799,8 @@ void testAdjustedDriver() unsupported.set_output_threshold_pct_fall(asymmetric ? .5 : .4); const auto ordinary = ArnoldiDelayCalculator{}.calculate(unsupported, edge, .1, model); const auto selected = ArnoldiDelayCalculator{}.calculate(unsupported, edge, .1, model, true); - require(ordinary && selected && !selected->resistance_adjusted, - "driver adjustment accepted an unsupported threshold convention"); - require(selected->delays == ordinary->delays && selected->slews == ordinary->slews, - "unsupported driver adjustment changed the ordinary waveform"); + require(ordinary && selected && !selected->resistance_adjusted, "driver adjustment accepted an unsupported threshold convention"); + require(selected->delays == ordinary->delays && selected->slews == ordinary->slews, "unsupported driver adjustment changed the ordinary waveform"); } } db.network().pins()[source].set_net_name("limited"); @@ -721,11 +814,10 @@ void testAdjustedDriver() const auto early = response.response(source, arc, AnalysisType::kMin, TransType::kRise, .1); const auto late = response.response(source, arc, AnalysisType::kMax, TransType::kRise, .1); require(early.get_is_valid() && late.get_is_valid(), "driver policy lost the physical waveform"); - require(std::abs(early.get_wire_delay_map().at(sink) - late.get_wire_delay_map().at(sink)) < 1e-10, - "minimum slew policy changed physical wire delay"); - require(adjust ? std::abs(early.get_load_slew_map().at(sink) - early.get_driver_slew()) < 1e-10 - : early.get_load_slew_map().at(sink) > early.get_driver_slew(), - "minimum slew rule did not follow the driver fit state"); + require(std::abs(early.get_wire_delay_map().at(sink) - late.get_wire_delay_map().at(sink)) < 1e-10, "minimum slew policy changed physical wire delay"); + require( + adjust ? std::abs(early.get_load_slew_map().at(sink) - early.get_driver_slew()) < 1e-10 : early.get_load_slew_map().at(sink) > early.get_driver_slew(), + "minimum slew rule did not follow the driver fit state"); config.min_slew_degradation = 1; response.clear(); const auto physical = response.response(source, arc, AnalysisType::kMin, TransType::kRise, .1); @@ -815,8 +907,7 @@ void testFlatDelayDriver() // 1.222281. A delay-slope-only fit gives .272889 and the wrong waveform. require(std::abs(result->drive_resistance - 44.527882) < .5 && std::abs(result->driver_ramp - 1.232865) < .03, "flat delay driver still fits resistance from delay slope alone"); - require(std::abs(result->slews[0] - 1.770038) < .02, - "joint driver fit lost the physical library transition"); + require(std::abs(result->slews[0] - 1.770038) < .02, "joint driver fit lost the physical library transition"); delay.set_value_list({.004, .004}); const auto constant_delay = ArnoldiDelayCalculator{}.calculate(arc, TransType::kRise, 0., model); require(constant_delay && constant_delay->drive_resistance > 40. && std::isfinite(constant_delay->delays[0]), @@ -837,8 +928,9 @@ void testFlatDelayDriver() rc.clear(); const auto& shielded_model = models.arnoldiModel(net, source, AnalysisType::kMax, TransType::kRise); const auto shielded = ArnoldiDelayCalculator{}.calculate(arc, TransType::kRise, 0., shielded_model); - require(shielded && shielded->effective_capacitance < .2, - "joint fit rejected a shielded load instead of retaining the charge solution"); + require(shielded && shielded->effective_capacitance < .2, "joint fit rejected a shielded load instead of retaining the charge solution"); + require(std::abs(waveformMoment(*shielded) / shielded_model.get_total_capacitance() - shielded->drive_resistance) < 1e-9, + "charge fallback retained the partial joint-fit network conductance"); // The network-limited decision belongs to each trial waveform, before // Ceff converges. This model crosses the decision boundary during shielding. @@ -1093,13 +1185,94 @@ void testPortReceiverThresholds() } } // namespace +void testIncompleteNetwork() +{ + using namespace ista; + Database db; + Config config; + std::string source = "in", net_name = "n"; + auto& net = db.network().nets()[net_name]; + net.set_net_name(net_name); + net.set_pin_name_list({source, "a", "b", "missing"}); + net.set_driver_pin_list({source}); + net.set_load_pin_list({"a", "b", "missing"}); + for (const auto& name : net.get_pin_name_list()) { + auto& pin = db.network().pins()[name]; + pin.set_is_port(true); + pin.set_full_name(name); + pin.set_net_name(net_name); + for (auto mode : {AnalysisType::kMin, AnalysisType::kMax}) + for (auto edge : {TransType::kRise, TransType::kFall}) + db.sdc().get_port_constraint_map()[name].set_load(mode, edge, name == "missing" ? .2 : 0., false); + } + auto& parasitic = db.parasitics().nets()[net_name]; + parasitic.set_net_name(net_name); + parasitic.set_lumped_capacitance(.041454); + for (const auto& name : {source, std::string("a"), std::string("b")}) + parasitic.get_node_map()[name].set_capacitance(.041454 / 3.); + for (const auto& name : {"a", "b"}) { + ParasiticResistor resistor; + resistor.set_source_node(source); + resistor.set_sink_node(name); + resistor.set_resistance(318.515 / 2.); + parasitic.get_resistor_list().push_back(resistor); + } + PinCapacitanceCalculator caps(db); + RcTreeAnalyzer rc(db, caps); + RcModelBuilder models(db, config, rc); + ReceiverThresholdCalculator thresholds(db); + ArnoldiResponseCalculator arnoldi(db, config, rc, models, thresholds); + DmpResponseCalculator dmp(db, rc, models, thresholds); + InputPortResponseCalculator input(db, rc, models, thresholds); + WireDelayCalculator wire(db, rc, dmp, arnoldi, input); + LoadCalculator load(db, caps, rc); + TimingArc timing; + for (auto edge : {TransType::kRise, TransType::kFall}) { + timing.get_delay_table_map()[edge].set_value_list({.2}); + timing.get_slew_table_map()[edge].set_value_list({.1}); + } + require(!arnoldi.response(source, timing, AnalysisType::kMax, TransType::kRise, .1).get_is_valid(), + "incomplete network entered the distributed driver solver"); + require(!dmp.response(source, timing, AnalysisType::kMax, TransType::kRise, .1, .241454).get_is_valid(), "incomplete network entered the pi driver solver"); + Arc arc; + arc.set_type(ArcType::kNet); + arc.set_owner_name(net_name); + arc.set_source_pin(source); + // PT U-2022.12-SP3 ord_000026: missing output-port annotation uses a + // balanced tree for every receiver, retaining its SDC capacitance. + for (auto mode : {AnalysisType::kMin, AnalysisType::kMax}) + for (auto edge : {TransType::kRise, TransType::kFall}) { + require(std::abs(load.pinLoad(source, mode, edge) - .241454) < 1e-12, "missing receiver lost its SDC load at the driver"); + for (const auto& sink : {"missing", "a"}) { + arc.set_sink_pin(sink); + double cap = std::string(sink) == "missing" ? .2 : 0.; + double expected = .318515 / 3. * (cap + .041454 / 3.); + require(std::abs(wire.calcNetArcDelay(arc, mode, edge, .1) - expected) < 1e-12, "incomplete RC did not use balanced total R/C for every load"); + require(wire.calcNetArcSlew(arc, mode, edge, .1) == .1, "incomplete RC manufactured slew degradation"); + } + } + // A named but electrically disconnected receiver is just as incomplete. + parasitic.get_node_map()["missing"].set_capacitance(0.); + rc.clear(); + require(rc.hasIncompleteNetwork(parasitic, source), "isolated receiver was treated as connected RC"); + ParasiticResistor repaired; + repaired.set_source_node(source); + repaired.set_sink_node("missing"); + repaired.set_resistance(1.); + parasitic.get_resistor_list().push_back(repaired); + rc.clear(); + require(!rc.hasIncompleteNetwork(parasitic, source), "RC invalidation retained an old connectivity failure"); +} + int main() { using namespace ista; testSmallRcResponse(); + testIncompleteNetwork(); testDisabledMultidriverFallback(); testDriverFitNearRampEnd(); testOrdinaryDriverCrossing(); + testSlowChargeConvergence(); testFlatDelayDriver(); testAdjustedDriver(); testZeroCapacitanceDriver(); diff --git a/src/operation/iSTA/test/electrical_check_test.cpp b/src/operation/iSTA/test/electrical_check_test.cpp index fb6178437..636bb0256 100644 --- a/src/operation/iSTA/test/electrical_check_test.cpp +++ b/src/operation/iSTA/test/electrical_check_test.cpp @@ -300,6 +300,12 @@ int main(int argc, char** argv) TimingAnalyzer(db, delay).analyze(); if (!db.electricalChecks().empty()) throw std::runtime_error("static net must not acquire switching electrical checks"); + const auto static_checks = ElectricalChecker(db, delay, true).check(); + require(static_checks.size() == 1 && static_checks.front().rule == ElectricalRule::kCapacitance && std::abs(static_checks.front().actual - .3) < 1e-12, + "explicit constant-net capacitance checking lost the SDC port load"); + TimingAnalyzer(db, delay, true).analyze(); + require(db.electricalChecks().size() == 1 && db.electricalChecks().front().rule == ElectricalRule::kCapacitance, + "analysis did not forward constant-net capacitance policy"); db.sdc().get_case_analysis_map().clear(); TimingAnalyzer(db, delay).analyze(); db.sdc().electricalLimits() = ElectricalLimits{}; diff --git a/src/operation/iSTA/test/path_check_test.cpp b/src/operation/iSTA/test/path_check_test.cpp index 47b5105a0..f578e27c7 100644 --- a/src/operation/iSTA/test/path_check_test.cpp +++ b/src/operation/iSTA/test/path_check_test.cpp @@ -106,10 +106,8 @@ static void registerChecks(bool reverse, bool split_tables, ista::AnalysisType m const auto budgeted = checker.checkPath(path, "ff:D", mode); // PT2018: -ignore_clock_latency also excludes clock uncertainty. The // endpoint setup/hold check remains part of the explicit data-path budget. - const double expected_budget = ignore ? (mode == AnalysisType::kMax ? 2. : 4.) - : (mode == AnalysisType::kMax ? 2.4 : 5.); - require(budgeted && std::abs(budgeted->required() - expected_budget) < 1e-8, - "path-delay budget applied the wrong clock uncertainty or latency"); + const double expected_budget = ignore ? (mode == AnalysisType::kMax ? 2. : 4.) : (mode == AnalysisType::kMax ? 2.4 : 5.); + require(budgeted && std::abs(budgeted->required() - expected_budget) < 1e-8, "path-delay budget applied the wrong clock uncertainty or latency"); } db.sdc().get_clock_map().at("b").set_setup_uncertainty(0.); db.sdc().get_clock_map().at("b").set_hold_uncertainty(0.); @@ -243,6 +241,52 @@ static void checkSlewRange() path.set_slew(.1); result = checker.checkPath(path, "ff:D", AnalysisType::kMin); require(result && std::abs(result->check().check_time - 1.1) < 1e-9, "hold must use the most restrictive check across physical slews"); + + // Both conditional Liberty variants remain possible. A min data path must + // still satisfy the largest hold/removal requirement (PT2022 CEB fixture). + for (auto type : {TimingCheckType::kSetup, TimingCheckType::kHold, TimingCheckType::kRecovery, TimingCheckType::kRemoval}) { + const auto mode = type == TimingCheckType::kSetup || type == TimingCheckType::kRecovery ? AnalysisType::kMax : AnalysisType::kMin; + auto& check_arc = instance.get_check_arc_list().front(); + check_arc.set_check_type(type); + for (bool negative : {false, true}) { + const double loose = negative ? -.03 : .1036; + const double strict = negative ? -.01 : .112; + TimingArc first, second; + first.set_arc_idx(17); + second.set_arc_idx(23); + first.get_check_table_map()[TransType::kRise].set_value_list({loose}); + second.get_check_table_map()[TransType::kRise].set_value_list({strict}); + for (bool reverse : {false, true}) { + check_arc.set_timing_arc_list(reverse ? std::vector{second, first} : std::vector{first, second}); + const auto checked = checker.checkPath(path, "ff:D", mode); + require(checked && std::abs(checked->check().check_time - strict) < 1e-12, "parallel constraint variants chose the easier timing check"); + require(checked->check().selected_check_arc_idx == 23, "strict check value lost its Liberty arc identity"); + } + if (negative) { + db.sdc().timingDerates().set(mode, false, TransType::kRise, DerateDelay::kCheck, 3.); + const auto derated = checker.checkPath(path, "ff:D", mode); + require(derated && std::abs(derated->check().check_time - .03) < 1e-12 && derated->check().selected_check_arc_idx == 17, + "negative check candidates were reduced before derating"); + db.sdc().timingDerates() = TimingDerates{}; + } + LogicExpressionTerm select; + select.set_operation_type(LogicOperationType::kPort); + select.set_port_name("SELECT"); + second.get_when_expression().set_term_list({select}); + check_arc.set_timing_arc_list({first, second}); + for (auto value : {TimingCaseValue::kZero, TimingCaseValue::kOne, TimingCaseValue::kNone}) { + db.graph().simValues().setValue("ff:SELECT", value); + const auto conditional = checker.checkPath(path, "ff:D", mode); + require(conditional && std::abs(conditional->check().check_time - (value == TimingCaseValue::kZero ? loose : strict)) < 1e-12, + "conditional timing check ignored known-false or unknown when condition"); + } + first.get_when_expression().set_term_list({select}); + check_arc.set_timing_arc_list({first, second}); + db.graph().simValues().setValue("ff:SELECT", TimingCaseValue::kZero); + require(!checker.checkPath(path, "ff:D", mode), "all-disabled variants fabricated an unconditional check"); + db.graph().simValues().clear(); + } + } } static void outputPathBudget() @@ -289,8 +333,7 @@ static void outputPathBudget() path.set_exception_state_list(ExceptionMatcher(db.sdc(), db.network()).initState("in", "clk", TransType::kRise, mode, TransType::kRise)); const auto result = checker.checkPath(path, "out", mode); const double expected = ignore ? 3.4 : mode == AnalysisType::kMax ? 3.2 : 3.8; - require(result && std::abs(result->required() - expected) < 1e-9, - "clock-independent output budget lost its I/O delay or retained a clock margin"); + require(result && std::abs(result->required() - expected) < 1e-9, "clock-independent output budget lost its I/O delay or retained a clock margin"); } } } diff --git a/src/operation/iSTA/test/read_config_test.cpp b/src/operation/iSTA/test/read_config_test.cpp index b51d2c799..f09e6a1a0 100644 --- a/src/operation/iSTA/test/read_config_test.cpp +++ b/src/operation/iSTA/test/read_config_test.cpp @@ -31,9 +31,10 @@ int main() require(options.empty(), "default lookup mutated input"); require(config.temp_directory_path == "./sta_temp_directory" && config.thread_number == 128, "default execution config"); require(config.path_report_number == 1 && config.endpoint_path_report_number == 1 && !config.is_path_report_number_specified, "default report limits"); - require(config.output_timing_reports == 1 && config.output_timing_features == 1 && config.timing_path_limit == 20 && config.min_slew_degradation == 0 && config.adjust_driver_resistance == 1 - && config.timing_corner.empty(), + require(config.output_timing_reports == 1 && config.output_timing_features == 1 && config.timing_path_limit == 20 && config.min_slew_degradation == 0 + && config.adjust_driver_resistance == 1 && config.timing_corner.empty(), "default export config"); + require(config.small_rc_fanout_reduction == 1 && config.check_constant_capacitance == 0, "default calculation/check policy changed"); options = {{"-nworst", int32_t{4}}, {"-thread_number", int32_t{0}}}; STADM.readConfig(options); require(config.path_report_number == 4 && !config.is_path_report_number_specified, "nworst did not raise implicit max_paths"); @@ -50,7 +51,10 @@ int main() {"-output_timing_reports", int32_t{0}}, {"-output_timing_features", int32_t{0}}, {"-timing_path_limit", int32_t{9}}, - {"-min_slew_degradation", int32_t{1}}, {"-adjust_driver_resistance", int32_t{0}}}; + {"-min_slew_degradation", int32_t{1}}, + {"-adjust_driver_resistance", int32_t{0}}, + {"-small_rc_fanout_reduction", int32_t{0}}, + {"-check_constant_capacitance", int32_t{1}}}; STADM.readConfig(options); require(config.path_report_number == 5 && config.endpoint_path_report_number == 8 && config.is_path_report_number_specified, "alias precedence or explicit limit changed"); @@ -58,10 +62,13 @@ int main() && config.timing_report_slack_greater_than == -0.75, "slack bounds changed"); require(config.temp_directory_path == "custom" && config.timing_corner == "slow" && omp_get_max_threads() == 2 && config.output_timing_reports == 0 - && config.output_timing_features == 0 && config.timing_path_limit == 9 && config.min_slew_degradation == 1 && config.adjust_driver_resistance == 0, + && config.output_timing_features == 0 && config.timing_path_limit == 9 && config.min_slew_degradation == 1 + && config.adjust_driver_resistance == 0, "explicit config changed"); + require(config.small_rc_fanout_reduction == 0 && config.check_constant_capacitance == 1, "explicit calculation/check policy ignored"); options.clear(); STADM.readConfig(options); + require(config.small_rc_fanout_reduction == 1 && config.check_constant_capacitance == 0, "calculation/check policy leaked across initialization"); require(!config.has_timing_report_slack_lesser_than && !config.has_timing_report_slack_greater_than && config.timing_report_slack_lesser_than == 0.25 && config.timing_report_slack_greater_than == -0.75, "repeat import must disable bounds while retaining inactive values"); diff --git a/src/operation/iSTA/test/read_parasitics_test.cpp b/src/operation/iSTA/test/read_parasitics_test.cpp index c6d849118..48928d1a2 100644 --- a/src/operation/iSTA/test/read_parasitics_test.cpp +++ b/src/operation/iSTA/test/read_parasitics_test.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MulanPSL-2.0 #include +#include #include #include #include @@ -9,6 +10,7 @@ #include "ReadParasitics.hpp" #include "idm.h" +#include "network/Network.hpp" #include "parasitics/Parasitics.hpp" namespace { @@ -18,11 +20,12 @@ void require(bool value, const char* message) throw std::runtime_error(message); } -void checkImport(const std::filesystem::path& file, const char* unit) +void checkImport(const std::filesystem::path& file, const char* unit, const char* resistance_unit, double resistance_scale = 1.) { // Both spellings use the same physical fF scale. Repeated CAP records must - // sum before the 1 aF node floor; RES-only nodes need the same treatment. - std::ofstream(file) << R"(*SPEF "IEEE 1481-1999" + // sum before the 1 zF node floor; RES-only nodes need the same treatment. + std::ofstream output(file); + output << R"(*SPEF "IEEE 1481-1999" *DESIGN "probe" *DATE "test" *VENDOR "test" @@ -33,49 +36,117 @@ void checkImport(const std::filesystem::path& file, const char* unit) *DELIMITER : *BUS_DELIMITER [ ] *T_UNIT 1 NS -*C_UNIT )" << unit << R"( -*R_UNIT 1 OHM +*C_UNIT )" + << unit << R"( +*R_UNIT )" + << resistance_unit << R"( *L_UNIT 1 HENRY -*D_NET n 2.001 +*D_NET n 2.0005 *CONN *P in I *I load:A I *CAP 1 in 0 -2 n:1 0.0005 +2 n:1 0.0000005 3 n:2 -0.001 -4 n:3 0.00075 -5 n:3 0.00075 +4 n:3 0.00000075 +5 n:3 0.00000075 6 n:4 2 7 load:A 0 +8 n:6 0.0005 *RES -1 in n:1 10 -2 n:1 n:2 10 -3 n:2 n:3 10 -4 n:3 n:4 10 -5 n:4 n:5 10 -6 n:5 load:A 10 -*END )"; + const std::array resistor_nodes{"in", "n:1", "n:2", "n:3", "n:4", "n:5", "n:6", "load:A"}; + const std::array resistances{-1., 0., .001, .009, .01, 10., .1}; + for (std::size_t index = 0; index < resistances.size(); ++index) + output << index + 1 << ' ' << resistor_nodes[index] << ' ' << resistor_nodes[index + 1] << ' ' << resistances[index] / resistance_scale << '\n'; + output << "*END\n"; + output.close(); require(dmInst->readSpef(file.string()), "SPEF fixture failed to parse"); ista::Parasitics parasitics; - ista::ReadParasitics reader(parasitics); + ista::Network network; + network.nets()["n"].set_pin_name_list({"in", "load:A"}); + ista::ReadParasitics reader(parasitics, network); for (int pass = 0; pass < 2; ++pass) { reader.read(); auto& net = parasitics.nets().at("n"); auto& nodes = net.get_node_map(); - require(nodes.size() == 7 && net.get_resistor_list().size() == 6, "import changed RC topology"); + require(nodes.size() == 8 && net.get_resistor_list().size() == 7, "import changed RC topology"); for (const auto* name : {"in", "n:1", "n:2", "n:5", "load:A"}) - require(std::abs(nodes.at(name).get_capacitance() - 1e-6) < 1e-15, - "missing, zero, negative or sub-aF node capacitance was not normalized"); - require(std::abs(nodes.at("n:3").get_capacitance() - 1.5e-6) < 1e-15, "node floor applied before summing duplicate CAP records"); + require(std::abs(nodes.at(name).get_capacitance() - 1e-9) < 1e-15, "missing, zero, negative or sub-zF node capacitance was not normalized"); + require(std::abs(nodes.at("n:3").get_capacitance() - 1.5e-9) < 1e-15, "node floor applied before summing duplicate CAP records"); require(std::abs(nodes.at("n:4").get_capacitance() - .002) < 1e-15, "positive extracted capacitance changed"); + require(std::abs(nodes.at("n:6").get_capacitance() - 5e-7) < 1e-15, "sub-aF positive capacitance was inflated"); double wire_cap = 0.; for (const auto& [name, node] : nodes) wire_cap += node.get_capacitance(); - require(std::abs(wire_cap - .0020065) < 1e-15, "incorrect imported wire capacitance or repeated-read accumulation"); + require(std::abs(wire_cap - .0020005065) < 1e-15, "incorrect imported wire capacitance or repeated-read accumulation"); + const std::array expected_resistances{.01, .01, .01, .01, .01, 10., .1}; + for (std::size_t index = 0; index < expected_resistances.size(); ++index) + require(std::abs(net.get_resistor_list()[index].get_resistance() - expected_resistances[index]) < 1e-12, + "extracted resistance floor was not applied in physical ohms"); } } + +void checkConnectivity(const std::filesystem::path& file) +{ + std::ofstream(file) << R"(*SPEF "IEEE 1481-1999" +*DESIGN "probe" +*DIVIDER / +*DELIMITER : +*BUS_DELIMITER [ ] +*T_UNIT 1 NS +*C_UNIT 1 PF +*R_UNIT 1 OHM +*L_UNIT 1 HENRY +*D_NET n 0.02 +*CONN +*P in I +*I removed:A I +*CAP +1 in 0.01 +2 removed:A 0.01 +*RES +1 in removed:A 20 +*END +)"; + require(dmInst->readSpef(file.string()), "stale SPEF fixture failed to parse"); + ista::Network network; + network.nets()["n"].set_pin_name_list({"in", "block/load:A"}); + network.pins()["in"].set_is_port(true); + network.pins()["block/load:A"].set_pin_name("A"); + ista::Parasitics parasitics; + ista::ReadParasitics reader(parasitics, network); + reader.read(); + require(parasitics.nets().empty(), "SPEF referencing a removed ECO pin was accepted"); + + auto text = [&] { + std::ifstream in(file); + return std::string(std::istreambuf_iterator(in), {}); + }(); + for (size_t pos = 0; (pos = text.find("removed:A", pos)) != std::string::npos; pos += 12) + text.replace(pos, 9, "block/load/A"); + std::ofstream(file) << text; + require(dmInst->readSpef(file.string()), "hierarchical SPEF fixture failed to parse"); + reader.read(); + require(parasitics.nets().contains("n"), "valid hierarchical pin separator was rejected"); + // Linking an assign can choose an internal net name while extraction keeps + // the top-level port alias as *D_NET. All terminals still have to match. + auto extracted = network.nets().extract("n"); + extracted.key() = "internal"; + network.nets().insert(std::move(extracted)); + text.replace(text.find("*D_NET n "), 9, "*D_NET in "); + std::ofstream(file) << text; + require(dmInst->readSpef(file.string()), "port-alias SPEF fixture failed to parse"); + network.pins()["in"].set_net_name("internal"); + reader.read(); + require(parasitics.nets().contains("internal") && !parasitics.nets().contains("in"), "top-level alias discarded a valid extracted RC network"); + require(parasitics.nets().at("internal").get_net_name() == "internal" && parasitics.nets().at("internal").get_resistor_list().size() == 1, + "alias annotation changed the RC topology or lookup identity"); + network.nets()["internal"].set_pin_name_list({"in"}); + reader.read(); + require(parasitics.nets().empty(), "SPEF pin moved to another net retained stale parasitics"); +} } // namespace int main() @@ -83,8 +154,10 @@ int main() const auto directory = std::filesystem::temp_directory_path() / ("ista-read-parasitics-" + std::to_string(getpid())); std::filesystem::create_directories(directory); try { - checkImport(directory / "net.spef", "1 FF"); - checkImport(directory / "net.spef", "0.001 PF"); + checkImport(directory / "net.spef", "1 FF", "1 OHM"); + checkImport(directory / "net.spef", "0.001 PF", "0.001 KOHM"); + checkImport(directory / "net.spef", "1 FF", "1 KOHM", 1000.); + checkConnectivity(directory / "stale.spef"); } catch (...) { std::filesystem::remove_all(directory); throw; diff --git a/src/operation/iSTA/test/sdc_core_test.cpp b/src/operation/iSTA/test/sdc_core_test.cpp index 054abab83..17e4aee65 100644 --- a/src/operation/iSTA/test/sdc_core_test.cpp +++ b/src/operation/iSTA/test/sdc_core_test.cpp @@ -83,6 +83,38 @@ void testFanout() require(FanoutChecker(other).check().empty(), "fanout crossed database lifetimes"); } +void testConstantFanout() +{ + Database db; + pin(db, "drv:Y", "drv", "Y"); + pin(db, "sink:A", "sink", "A"); + auto& net = db.network().nets()["n"]; + net.set_net_name("n"); + net.set_driver_pin("drv:Y"); + net.set_driver_pin_list({"drv:Y"}); + net.set_load_pin_list({"sink:A"}); + db.sdc().get_max_fanout() = .5; + const FanoutChecker checker(db); + require(checker.check().size() == 1 && checker.check().front().get_slack() == -.5, "switching fanout violation missing"); + for (auto value : {TimingCaseValue::kZero, TimingCaseValue::kOne, TimingCaseValue::kStatic}) { + db.graph().simValues().setValue("drv:Y", value); + require(checker.check().empty(), "constant driver acquired a fanout check"); + } + for (auto value : {TimingCaseValue::kRise, TimingCaseValue::kFall, TimingCaseValue::kNone}) { + db.graph().simValues().setValue("drv:Y", value); + require(checker.check().size() == 1, "edge-qualified driver lost fanout check"); + } + // An inout's resolved receiver value does not replace its independent driver. + db.network().pins().at("drv:Y").set_direction(PinDirection::kInout); + db.graph().driverPoints().try_emplace("drv:Y"); + db.graph().simValues().setValue("drv:Y", TimingCaseValue::kZero); + require(checker.check().size() == 1, "constant receiver suppressed unknown inout driver"); + db.graph().simValues().setValue("drv:Y", TimingCaseValue::kOne, true); + require(checker.check().empty(), "constant inout driver acquired fanout check"); + db.graph().simValues().setValue("drv:Y", TimingCaseValue::kFall, true); + require(checker.check().size() == 1, "edge-qualified inout driver lost fanout check"); +} + void testExceptions() { Database db; @@ -141,5 +173,6 @@ int main() { // Deliberately initialize no singleton, Tcl interpreter or path search. testFanout(); + testConstantFanout(); testExceptions(); } diff --git a/src/operation/iSTA/test/sdc_frontend_test.cpp b/src/operation/iSTA/test/sdc_frontend_test.cpp index 678723338..eaedbec87 100644 --- a/src/operation/iSTA/test/sdc_frontend_test.cpp +++ b/src/operation/iSTA/test/sdc_frontend_test.cpp @@ -1,17 +1,70 @@ // SPDX-License-Identifier: MulanPSL-2.0 // This consumer deliberately links only ista_sdc_frontend. +#include + +#include +#include +#include #include #include "SdcCommand.hpp" #include "SdcTclCmd.hpp" #include "query/ObjectQuery.hpp" -int main() +int main(int, char** argv) { + Tcl_FindExecutable(argv[0]); ista::SdcCommand interpreter; if (interpreter.evalString("expr {2 + 3}") != TCL_OK) return 1; if (std::string(Tcl_GetStringResult(interpreter.getInterp())) != "5") return 2; - return interpreter.getErrors().empty() ? 0 : 3; + if (!interpreter.getErrors().empty()) + return 3; + + const auto directory = std::filesystem::temp_directory_path() / ("ista-sdc source [literal] $-" + std::to_string(getpid())); + std::filesystem::create_directory(directory); + struct Cleanup + { + std::filesystem::path directory; + ~Cleanup() + { + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + } + } cleanup{directory}; + const auto source = directory / "constraints.sdc"; + const auto helper = directory / "io.tcl"; + { + std::ofstream(helper) << "set helper_script [info script]\nset output_load 0.2\n"; + std::ofstream(source) << "set constraint_script [info script]\n" + "source [file join [file dirname [info script]] io.tcl]\n" + "set resumed_script [info script]\n" + "return\n" + "error {commands after top-level return must not execute}\n"; + } + if (interpreter.evalScriptFile(source.string()) != TCL_OK) { + std::cerr << "SDC could not source its sibling helper: " << Tcl_GetStringResult(interpreter.getInterp()) << '\n'; + return 4; + } + const auto value = [&](const char* name) { + const char* result = Tcl_GetVar(interpreter.getInterp(), name, TCL_GLOBAL_ONLY); + return result ? std::string(result) : std::string(); + }; + if (value("constraint_script") != source.string() || value("helper_script") != helper.string() || value("resumed_script") != source.string() + || value("output_load") != "0.2") + return 5; + if (interpreter.evalString("info script") != TCL_OK || std::string(Tcl_GetStringResult(interpreter.getInterp())) != "") + return 6; + { + std::ofstream(source) << "# error line must be relative to this file\nset before_error 1\nerror {constraint failed}\n"; + } + if (interpreter.evalScriptFile(source.string()) != TCL_ERROR || interpreter.getErrors().size() != 1 || interpreter.getErrors()[0].line_number != 3 + || interpreter.getErrors()[0].message != "constraint failed" || value("errorInfo").find(source.string()) == std::string::npos) + return 7; + if (interpreter.evalString("info script") != TCL_OK || std::string(Tcl_GetStringResult(interpreter.getInterp())) != "") + return 8; + if (interpreter.evalScriptFile((directory / "missing.sdc").string()) != TCL_ERROR || interpreter.getErrors().empty()) + return 9; + return 0; } diff --git a/src/operation/iSTA/test/sdc_limits_test.cpp b/src/operation/iSTA/test/sdc_limits_test.cpp index 1ce3b584b..09c425037 100644 --- a/src/operation/iSTA/test/sdc_limits_test.cpp +++ b/src/operation/iSTA/test/sdc_limits_test.cpp @@ -91,6 +91,24 @@ int main(int argc, char** argv) "set_timing_derate -late -cell_check -cell_delay 1.1", "set_timing_derate -late -cell_check -net_delay 1.1"}) require(interp.evalString(invalid) == TCL_ERROR, "incompatible check derate options accepted"); expected = "set_timing_derate"; + } else if (mode == "abbreviation") { + eval("set_max_transition 0.5 -clock -r core"); + eval("set_max_transition 1.0 -data core"); + const auto& limits = db.sdc().electricalLimits(); + near(*limits.limit(ElectricalRule::kTransition, "", false, "", "core", true, TransType::kRise), .5, "abbreviated clock/rise selectors lost"); + near(*limits.limit(ElectricalRule::kTransition, "", false, "", "core", false, TransType::kFall), 1., "abbreviated data selector lost"); + require(!limits.limit(ElectricalRule::kTransition, "", false, "", "core", true, TransType::kFall), + "data/rise constraint leaked into falling clock transition"); + const auto count = limits.entries().size(); + require(interp.evalString("set_max_transition 0.3 -f core") == TCL_ERROR, "ambiguous -fall/-force prefix accepted"); + require(limits.entries().size() == count, "ambiguous option changed constraints"); + eval("set_timing_derate -late -data 1.2"); + eval("set_timing_derate -late -data -inc -0.1"); + near(cellDelay("cdc/buf"), 2.2, "exact option or negative abbreviated option value changed"); + eval("set_input_delay -clock core 0.25 [get_ports in]"); + require(db.sdc().get_port_constraint_map().at("in").get_input_delay_list().front().get_clock_trans_type() == TransType::kRise, + "exact -clock was confused with its longer -clock_fall option"); + expected = "-clock_path"; } else if (mode == "transition") { eval("set_max_transition 0.5 -clock_path -rise [get_clocks core]"); eval("set_max_transition 1.0 -data_path [get_clocks core]"); diff --git a/src/operation/iSTA/test/sdc_semantics_test.cpp b/src/operation/iSTA/test/sdc_semantics_test.cpp index c12719c38..99a795cce 100644 --- a/src/operation/iSTA/test/sdc_semantics_test.cpp +++ b/src/operation/iSTA/test/sdc_semantics_test.cpp @@ -16,14 +16,22 @@ static void testClockStageFiles() auto& interpreter = SdcCommand::getInst(); const auto directory = std::filesystem::temp_directory_path() / ("sta-clock-stages-" + std::to_string(getpid())); require(std::filesystem::create_directory(directory), "could not create stage test directory"); - struct Cleanup { + struct Cleanup + { std::filesystem::path directory; - ~Cleanup() { std::error_code ignored; std::filesystem::remove_all(directory, ignored); } + ~Cleanup() + { + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + } } cleanup{directory}; for (const auto* stage : {"pre_CTS", "post_CTS"}) std::filesystem::create_directory(directory / stage); const auto bad_file = directory / "invalid.tcl"; - { std::ofstream file(bad_file); file << "set_propagated_clock missing_clock\n"; } + { + std::ofstream file(bad_file); + file << "set_propagated_clock missing_clock\n"; + } for (int clocks = 0; clocks < 3; ++clocks) { const bool has_virtual = clocks != 1; @@ -67,8 +75,7 @@ static void testClockStageFiles() const bool physical = name == "clk"; require(clock.get_is_propagated() == (post_cts && physical), "incorrect pre/post-CTS clock mode"); require(clock.get_source_list().empty() == !physical, "virtual clock acquired a physical source"); - require(clock.get_source_latency(AnalysisType::kMin, ClockVariation::kEarly, TransType::kRise) == 0.5, - "propagation changed source latency"); + require(clock.get_source_latency(AnalysisType::kMin, ClockVariation::kEarly, TransType::kRise) == 0.5, "propagation changed source latency"); require(clock.get_network_latency(AnalysisType::kMax, TransType::kFall) == (post_cts && physical ? 0. : 1.25), "propagation lost virtual latency or retained physical network latency"); require(clock.get_setup_uncertainty() == 0.2 && clock.get_hold_uncertainty() == 0.1, "uncertainty changed"); @@ -205,10 +212,8 @@ int main(int argc, char** argv) require(sdc.get_clock_map().at("M").get_period() == period, "failed command changed the old clock"); } else if (mode == "empty_collection") { const std::vector commands = { - "set_false_path -to [get_pins -quiet no_such_pin]", - "set_max_delay 1 -to [get_pins -quiet no_such_pin]", - "set_min_delay 1 -through [get_pins -quiet no_such_pin]", - "set_multicycle_path 2 -setup -from [get_pins -quiet no_such_pin]", + "set_false_path -to [get_pins -quiet no_such_pin]", "set_max_delay 1 -to [get_pins -quiet no_such_pin]", + "set_min_delay 1 -through [get_pins -quiet no_such_pin]", "set_multicycle_path 2 -setup -from [get_pins -quiet no_such_pin]", "reset_path -to [get_pins -quiet no_such_pin]", }; for (const auto& command : commands) { @@ -379,6 +384,34 @@ int main(int argc, char** argv) sdc.get_path_exception_list().clear(); eval("set_false_path -rise_from A -to C; reset_path -from A -to C"); require(!sdc.get_path_exception_list().empty(), "unqualified selector removed edge-specific exception"); + } else if (mode == "false_startpoint") { + for (const auto* selector : {"-from", "-rise_from", "-fall_from"}) { + eval(std::string("set_false_path ") + selector + " [get_pins cdc/source_reg/Q] -to [get_pins cdc/sink_reg/D]"); + require(sdc.get_path_exception_list().empty(), "ordinary register output was accepted as false-path startpoint"); + } + eval("set_false_path -from [get_pins {cdc/source_reg/Q cdc/source_reg/CK}] -to [get_pins cdc/sink_reg/D]"); + require(sdc.get_path_exception_list().size() == 1 + && sdc.get_path_exception_list().front().get_from_objects() == ExceptionObjects{{ExceptionObjectType::kPin, "cdc/source_reg:CK"}}, + "invalid Q filter discarded or widened a valid clock-pin selector"); + eval("set_false_path -reset_path -from [get_pins cdc/source_reg/Q] -to [get_pins cdc/sink_reg/D]"); + require(sdc.get_path_exception_list().size() == 1, "empty invalid selector reset existing exceptions"); + sdc.get_path_exception_list().clear(); + eval("set_false_path -through [get_pins cdc/source_reg/Q]"); + require(sdc.get_path_exception_list().size() == 1, "Q ceased to be a legal through point"); + sdc.get_path_exception_list().clear(); + eval("set_false_path -to [get_pins cdc/sink_reg/D]"); + require(sdc.get_path_exception_list().size() == 1, "omitted from selector lost its wildcard meaning"); + sdc.get_path_exception_list().clear(); + for (const auto* cmd : {"set_max_delay 1", "set_min_delay 1"}) { + eval(std::string(cmd) + " -from [get_pins cdc/source_reg/Q] -to [get_pins cdc/sink_reg/D]"); + } + require(sdc.get_path_exception_list().size() == 2, "false-path validation leaked into segmented path-delay constraints"); + sdc.get_path_exception_list().clear(); + TimingIoDelay input_delay; + input_delay.set_clock_name("A"); + sdc.get_port_constraint_map()["cdc/source_reg:Q"].set_input_delays({input_delay}, false); + eval("set_false_path -from [get_pins cdc/source_reg/Q] -to [get_pins cdc/sink_reg/D]"); + require(sdc.get_path_exception_list().size() == 1, "explicitly constrained input-delay pin was rejected"); } else if (mode == "groups") { eval("set_clock_groups -asynchronous -group A -group B"); require(interp.evalString("set_clock_groups -asynchronous -allow_paths -group A -group B") == TCL_ERROR, "contradictory group relation accepted"); diff --git a/src/operation/iSTA/test/sta_interface_test.cpp b/src/operation/iSTA/test/sta_interface_test.cpp index cf12af9c6..5455892cc 100644 --- a/src/operation/iSTA/test/sta_interface_test.cpp +++ b/src/operation/iSTA/test/sta_interface_test.cpp @@ -27,9 +27,11 @@ static void require(bool value, const char* message) struct DesignFixture { - idb::IdbLayout layout; + idb::IdbLefService lef; + idb::IdbLayout& layout = *lef.get_layout(); idb::IdbDefService service{&layout}; idb::IdbDefService* saved_service = dmInst->get_idb_def_service(); + idb::IdbLefService* saved_lef = dmInst->get_idb_lef_service(); std::string saved_sdc = dmInst->get_config().get_sdc_path(); std::filesystem::path directory = std::filesystem::temp_directory_path() / ("ista-interface-" + std::to_string(getpid())); @@ -50,12 +52,14 @@ struct DesignFixture net->add_io_pin(pin); } dmInst->set_idb_def_service(&service); + dmInst->set_idb_lef_service(&lef); std::ofstream(sdc_file) << "set_input_transition 0.1 [get_ports in]\n"; dmInst->get_config().get_sdc_path() = sdc_file.string(); } ~DesignFixture() { dmInst->set_idb_def_service(saved_service); + dmInst->set_idb_lef_service(saved_lef); dmInst->get_config().get_sdc_path() = saved_sdc; std::filesystem::remove_all(directory); std::filesystem::remove(sdc_file); @@ -83,17 +87,20 @@ int main() sta.prepareTimingModel(); require(sta.isModelReady() && !sta.isTimingReady(), "structural model preparation published the wrong ready state"); const auto structural_model = sta.exportTimingModel(); - require(structural_model.phase_calls == std::map{ - {"structural_preparation", 1}, {"clock_network", 1}, {"clock_propagation", 0}, - {"data_slew", 0}, {"data_arrival", 0}, {"required", 0}, {"path_analysis", 0}, {"delay_graph_initialization", 0}}, + require(structural_model.phase_calls + == std::map{{"structural_preparation", 1}, + {"clock_network", 1}, + {"clock_propagation", 0}, + {"data_slew", 0}, + {"data_arrival", 0}, + {"required", 0}, + {"path_analysis", 0}, + {"delay_graph_initialization", 0}}, "model preparation executed a full numeric timing phase"); - require(structural_model.sparse.enabled && !structural_model.pins.empty(), - "model-only export did not publish the structural sparse model"); - require(structural_model.min_launches.size() == 2 && structural_model.min_contexts.size() == 4, - "model-only export lost the min launch/endpoint rules"); + require(structural_model.sparse.enabled && !structural_model.pins.empty(), "model-only export did not publish the structural sparse model"); + require(structural_model.min_launches.size() == 2 && structural_model.min_contexts.size() == 4, "model-only export lost the min launch/endpoint rules"); for (const auto& seed : structural_model.min_launches) - require(seed.start_point == "in" && seed.arrival_ps == structural_model.time_unit_ps, - "model-only min seed lost the SDC input delay"); + require(seed.start_point == "in" && seed.arrival_ps == structural_model.time_unit_ps, "model-only min seed lost the SDC input delay"); for (const auto& pin : structural_model.pins) { const double expected = pin.name == "out" ? 0.1 : pin.name == "other" ? 0.2 : 0.0; require(std::abs(pin.min_output_rise_load_pf - expected * structural_model.capacitance_unit_pf) < 1E-12 @@ -104,13 +111,18 @@ int main() sta.runSTA(); require(sta.isTimingReady(), "successful analysis is not ready"); const auto complete = sta.exportTimingData(); - require(complete.phase_calls == std::map{ - {"structural_preparation", 1}, {"clock_network", 2}, {"clock_propagation", 1}, - {"data_slew", 1}, {"data_arrival", 1}, {"required", 1}, {"path_analysis", 1}, {"delay_graph_initialization", 1}}, + require(complete.phase_calls + == std::map{{"structural_preparation", 1}, + {"clock_network", 2}, + {"clock_propagation", 1}, + {"data_slew", 1}, + {"data_arrival", 1}, + {"required", 1}, + {"path_analysis", 1}, + {"delay_graph_initialization", 1}}, "complete STA did not record its actual numeric phases"); require(!complete.pins.empty(), "successful export is empty"); - require(complete.min_launches.size() == structural_model.min_launches.size() - && complete.min_contexts.size() == structural_model.min_contexts.size(), + require(complete.min_launches.size() == structural_model.min_launches.size() && complete.min_contexts.size() == structural_model.min_contexts.size(), "model-only and complete preparation produced different min domains"); for (std::size_t index = 0; index < complete.min_contexts.size(); ++index) { const auto& expected = complete.min_contexts[index]; @@ -120,8 +132,7 @@ int main() "model-only and complete preparation produced different min required rules"); } - require(SdcCommand::getInst().evalString("get_ports out") == TCL_OK && sta.isTimingReady(), - "read-only SDC query invalidated timing"); + require(SdcCommand::getInst().evalString("get_ports out") == TCL_OK && sta.isTimingReady(), "read-only SDC query invalidated timing"); require(SdcCommand::getInst().evalString("set_load 0.25 [get_ports out]") == TCL_OK, "load edit failed"); require(!sta.isTimingReady(), "SDC edit left stale timing export ready"); bool stale_rejected = false; @@ -133,14 +144,18 @@ int main() require(stale_rejected, "SDC edit allowed export of old timing"); sta.runSTA(); require(sta.isTimingReady(), "SDC edit did not recover after reanalysis"); - require(SdcCommand::getInst().evalString("set_load 0.4 [get_ports out]; unknown_sdc_command") == TCL_ERROR, - "intended partial SDC failure did not occur"); + require(SdcCommand::getInst().evalString("set_load 0.4 [get_ports out]; unknown_sdc_command") == TCL_ERROR, "intended partial SDC failure did not occur"); require(!sta.isTimingReady(), "partially failed SDC script retained old timing validity"); sta.runSTA(); require(sta.isTimingReady(), "partial script changes could not be reanalyzed"); tcl::registerCmdSTA(); auto* engine = ecc::ScriptEngine::getOrCreateInstance(); + for (const auto* option : {"-small_rc_fanout_reduction", "-check_constant_capacitance"}) { + const std::string command = std::string("init_sta ") + option + " 2"; + require(engine->evalString(command.c_str()) == TCL_ERROR, "invalid calculation/check switch was accepted by Tcl"); + require(sta.isTimingReady(), "invalid switch mutated the live timing session"); + } const auto path = fixture.directory / "effective.sdc"; Tcl_SetVar(engine->get_interp(), "destination", path.c_str(), TCL_GLOBAL_ONLY); require(engine->evalString("write_sdc -nosplit $destination") == TCL_OK, "public write_sdc failed on initialized session"); @@ -201,6 +216,42 @@ int main() require(sta.isTimingReady(), "successful model extraction lost completed timing"); sta.destroySTA(); require(!sta.isTimingReady(), "destroy retained ready timing"); + const auto hierarchy = fixture.directory / "hierarchy.v"; + std::ofstream(hierarchy) << "module block(input a); endmodule\n" + "module interface_contract(input in, output out, output other);\n" + "block b(.a(in)); assign out=in; assign other=in; endmodule\n"; + const auto spef = fixture.directory / "hierarchy.spef"; + std::ofstream(spef) << R"(*SPEF "IEEE 1481-1999" +*DESIGN "interface_contract" +*DIVIDER / +*DELIMITER : +*BUS_DELIMITER [ ] +*T_UNIT 1 NS +*C_UNIT 1 PF +*R_UNIT 1 OHM +*L_UNIT 1 HENRY +*D_NET wire 0.03 +*CONN +*P in I +*P out O +*P other O +*I b:a I +*CAP +1 in 0.01 +2 out 0.01 +3 other 0.01 +*RES +1 in b:a 1 +2 b:a out 10 +3 b:a other 20 +*END +)"; + require(dmInst->readSpef(spef.string()), "hierarchy SPEF failed to parse"); + require(sta.initSTA({{"-temp_directory_path", fixture.directory.string()}, {"-thread_number", int32_t{1}}, {"-hierarchy_verilog", hierarchy.string()}}), + "hierarchy STA initialization failed"); + require(STADM.getDatabase().network().hierarchyPins().contains("b:a"), "hierarchy fixture did not create a boundary pin"); + require(STADM.getDatabase().parasitics().nets().contains("wire"), "parasitics were validated before hierarchy boundaries existed"); + sta.destroySTA(); STAInterface::destroyInst(); ecc::ScriptEngine::destroyInstance(); }