From 1e048fa4600ceda19e62ec4b23657ba60adaa54c Mon Sep 17 00:00:00 2001 From: Harrison Liew Date: Wed, 8 Nov 2023 09:31:53 -0800 Subject: [PATCH] Clock & Delay constraint refinements (#818) * setup/hold delay constraints, optional period for generated clocks, clock inversion * pytest/mypy --- hammer/config/defaults.yml | 5 +-- hammer/synthesis/yosys/__init__.py | 4 ++- hammer/vlsi/constraints.py | 27 ++++++++++----- hammer/vlsi/hammer_tool.py | 11 +++++- hammer/vlsi/hammer_vlsi_impl.py | 11 ++++-- hammer/vlsi/vendor/openroad.py | 12 ++++--- tests/test_constraints.py | 54 +++++++++++++++++++++++++----- 7 files changed, 95 insertions(+), 29 deletions(-) diff --git a/hammer/config/defaults.yml b/hammer/config/defaults.yml index be439d0ee..a845fc103 100644 --- a/hammer/config/defaults.yml +++ b/hammer/config/defaults.yml @@ -181,9 +181,9 @@ vlsi.inputs: # period (TimeValue) - Clock port period. e.g. "1 ns", "5ns". Default units: ns # path (str) - Optional. If specified, this is the RTL path to the clock. Otherwise, the path is the same as the name. # uncertainty (TimeValue) - Optional. Clock uncertainty. e.g. "1 ns", "5ns". Default units: ns - # generated (bool) - Optional. If specified this clock is generated from another clock, must specify source_path and divisor + # generated (bool) - Optional. If specified this clock is generated from another clock. Must specify source_path and divisor, but period becomes Optional. # source_path (str) - Required if generated. The path of the clock that this clock is generated from. - # divisor (int) - Required if generated. The amount this generated clock is slowed from the source clock e.g. 2 => this clock will be two times slower than the source clock. + # divisor (int) - Required if generated. The amount this generated clock is slower/faster than the source clock e.g. 2 => this clock will be two times slower than the source clock. Negative values imply inversion (falling edge in line with the source's rising edge). # We are constrained to integers by SDC. # group (str) - Optional. The name of the clock group this clock belongs to. Clocks in the same group will not be marked as asynchronous. # Clocks with no group specified will all be placed in separate groups and thus marked as asynchronous to each other and all other groups. @@ -207,6 +207,7 @@ vlsi.inputs: # - "input" # - "output" # delay (TimeValue) - Delay applied to the I/O. + # corner (Optional[str]) - "setup" or "hold" will specify -max and -min flags. If empty, the same constraint will apply for setup and hold analysis. custom_sdc_constraints: [] # List of custom sdc constraints to use. (List[str]) # These are appended after all other generated constraints (clock, pin, delay, load, etc.). diff --git a/hammer/synthesis/yosys/__init__.py b/hammer/synthesis/yosys/__init__.py index 09193a216..fc344c9be 100644 --- a/hammer/synthesis/yosys/__init__.py +++ b/hammer/synthesis/yosys/__init__.py @@ -193,8 +193,10 @@ def init_environment(self) -> bool: clock_port = self.get_clock_ports()[0] self.clock_port_name = clock_port.name time_unit = "ps" # yosys requires time units in ps + assert clock_port.period is not None, "clock must have a period" + assert clock_port.uncertainty is not None, "clock must have an uncertainty" self.clock_period = int(clock_port.period.value_in_units(time_unit)) - self.clock_uncertainty = int(clock_port.period.value_in_units(time_unit)) + self.clock_uncertainty = int(clock_port.uncertainty.value_in_units(time_unit)) self.clock_transition = 0.15 # SYNTH_CLOCK_TRANSITION self.synth_cap_load = 33.5 # SYNTH_CAP_LOAD diff --git a/hammer/vlsi/constraints.py b/hammer/vlsi/constraints.py index 2e8650a6e..87830178d 100644 --- a/hammer/vlsi/constraints.py +++ b/hammer/vlsi/constraints.py @@ -370,7 +370,7 @@ def name_bump(self, definition: BumpsDefinition, assignment: BumpAssignment) -> ClockPort = NamedTuple('ClockPort', [ ('name', str), - ('period', TimeValue), + ('period', Optional[TimeValue]), ('path', Optional[str]), ('uncertainty', Optional[TimeValue]), ('generated', Optional[bool]), @@ -389,34 +389,43 @@ class DelayConstraint(NamedTuple('DelayConstraint', [ ('name', str), ('clock', str), ('direction', str), - ('delay', TimeValue) + ('delay', TimeValue), + ('corner', Optional[str]) ])): __slots__ = () - def __new__(cls, name: str, clock: str, direction: str, delay: TimeValue) -> "DelayConstraint": + def __new__(cls, name: str, clock: str, direction: str, delay: TimeValue, corner: Optional[str]) -> "DelayConstraint": if direction not in ("input", "output"): - raise ValueError("Invalid direction {direction}".format(direction=direction)) - return super().__new__(cls, name, clock, direction, delay) + raise ValueError(f"Invalid direction {direction} for a delay constraint") + if corner is not None: + if corner not in ("setup", "hold"): + raise ValueError(f"Invalid corner {corner} for a delay constraint") + return super().__new__(cls, name, clock, direction, delay, corner) @staticmethod def from_dict(delay_src: Dict[str, Any]) -> "DelayConstraint": direction = str(delay_src["direction"]) - if direction not in ("input", "output"): - raise ValueError("Invalid direction {direction}".format(direction=direction)) + corner = None # type: Optional[str] + if "corner" in delay_src: + corner = str(delay_src["corner"]) return DelayConstraint( name=str(delay_src["name"]), clock=str(delay_src["clock"]), direction=direction, - delay=TimeValue(delay_src["delay"]) + delay=TimeValue(delay_src["delay"]), + corner=corner ) def to_dict(self) -> dict: - return { + output = { "name": self.name, "clock": self.clock, "direction": self.direction, "delay": self.delay.str_value_in_units("ns", round_zeroes=False) } + if self.corner is not None: + output.update({"corner": self.corner}) + return output class DecapConstraint(NamedTuple('DecapConstraint', [ ('target', str), diff --git a/hammer/vlsi/hammer_tool.py b/hammer/vlsi/hammer_tool.py index b6c60a25a..b3f808802 100644 --- a/hammer/vlsi/hammer_tool.py +++ b/hammer/vlsi/hammer_tool.py @@ -1051,9 +1051,12 @@ def get_clock_ports(self) -> List[ClockPort]: output = [] # type: List[ClockPort] for clock_port in clocks: clock = ClockPort( - name=clock_port["name"], period=TimeValue(clock_port["period"]), + name=clock_port["name"], period=None, uncertainty=None, path=None, generated=None, source_path=None, divisor=None, group=None ) + period_assert = False + if "period" in clock_port: + clock = clock._replace(period=TimeValue(clock_port["period"])) if "path" in clock_port: clock = clock._replace(path=clock_port["path"]) if "uncertainty" in clock_port: @@ -1068,7 +1071,13 @@ def get_clock_ports(self) -> List[ClockPort]: source_path=clock_port["source_path"], divisor=int(clock_port["divisor"]) ) + else: + period_assert = True + else: + period_assert = True clock = clock._replace(generated=generated) + if period_assert: + assert clock.period is not None, f"Non-generated clock {clock.name} must have a period specified." output.append(clock) return output diff --git a/hammer/vlsi/hammer_vlsi_impl.py b/hammer/vlsi/hammer_vlsi_impl.py index 63c87223a..7c7088f47 100644 --- a/hammer/vlsi/hammer_vlsi_impl.py +++ b/hammer/vlsi/hammer_vlsi_impl.py @@ -2183,13 +2183,16 @@ def sdc_clock_constraints(self) -> str: if get_or_else(clock.generated, False): if any("hport" in p for p in [get_or_else(clock.path, ""), get_or_else(clock.source_path, "")]): self.logger.error(f"In clock constraints, hports are not supported by some tools. Consider using ports/pins/hpins instead. Offending clock name: ${clock.name}") - output.append("create_generated_clock -name {n} -source {m_path} -divide_by {div} {path}". - format(n=clock.name, m_path=clock.source_path, div=clock.divisor, path=clock.path)) + assert clock.divisor is not None, f"Generated clock {clock.name} must have a divisor" + output.append("create_generated_clock -name {n} -source {m_path} -divide_by {div} {invert} {path}". + format(n=clock.name, m_path=clock.source_path, div=abs(clock.divisor), invert="-invert" if clock.divisor < 0 else "", path=clock.path)) elif clock.path is not None: if "get_db hports" in clock.path: self.logger.error("get_db hports will cause some tools to crash. Consider querying hpins instead.") + assert clock.period is not None, f"Clock {clock.name} must have a period" output.append("create_clock {0} -name {1} -period {2}".format(clock.path, clock.name, clock.period.value_in_units(time_unit))) else: + assert clock.period is not None, f"Clock {clock.name} must have a period" output.append("create_clock {0} -name {0} -period {1}".format(clock.name, clock.period.value_in_units(time_unit))) if clock.uncertainty is not None: output.append("set_clock_uncertainty {1} [get_clocks {0}]".format(clock.name, clock.uncertainty.value_in_units(time_unit))) @@ -2232,10 +2235,12 @@ def sdc_pin_constraints(self) -> str: # Also specify delays for specific pins. for delay in self.get_delay_constraints(): - output.append("set_{direction}_delay {delay} -clock {clock} [get_port {name}]".format( + minmax = {None: "", "setup": "-max", "hold": "-min"} + output.append("set_{direction}_delay {delay} -clock {clock} {minmax} [get_port {name}]".format( delay=delay.delay.value_in_units(self.get_time_unit().value_prefix + self.get_time_unit().unit), clock=delay.clock, direction=delay.direction, + minmax=minmax[delay.corner], name=delay.name )) diff --git a/hammer/vlsi/vendor/openroad.py b/hammer/vlsi/vendor/openroad.py index b61199dfe..7aa404cf4 100644 --- a/hammer/vlsi/vendor/openroad.py +++ b/hammer/vlsi/vendor/openroad.py @@ -26,7 +26,7 @@ class OpenROADTool(HasSDCSupport, TCLTool, HammerTool): def env_vars(self) -> Dict[str, str]: """ Get the list of environment variables required for this tool. - Note to subclasses: remember to include variables from + Note to subclasses: remember to include variables from super().env_vars! """ list_of_vars = self.get_setting("openroad.extra_env_vars") @@ -36,7 +36,7 @@ def env_vars(self) -> Dict[str, str]: def validate_openroad_installation(self) -> None: """ make sure OPENROAD env-var is set, and klayout is in the path (since - klayout is not installed with OPENROAD as of version 1.1.0. this + klayout is not installed with OPENROAD as of version 1.1.0. this should be called in steps that actually run tools or touch filepaths """ if not shutil.which("openroad"): @@ -114,7 +114,7 @@ def version_number(self, version: str) -> int: def setup_openroad_rundir(self) -> bool: """ - OpenROAD expects several files/dirs in the current run_dir, so we + OpenROAD expects several files/dirs in the current run_dir, so we symlink them in from the OpenROAD-flow installation """ # TODO: for now, just symlink in the read-only OpenROAD stuff, since @@ -151,7 +151,9 @@ def _clock_period_value(self) -> str: """this string is used in the makefile fragment used by OpenROAD""" assert len(self.get_clock_ports()) == 1, "openroad only supports 1 root clock" - return str(self.get_clock_ports()[0].period.value_in_units("ns")) + period = self.get_clock_ports()[0].period + assert period is not None, "clock must have a period" + return str(period.value_in_units("ns")) def _floorplan_bbox(self) -> str: """this string is used in the makefile fragment used by OpenROAD""" @@ -187,7 +189,7 @@ def create_design_config(self) -> bool: abspath_input_files = list(map(lambda name: os.path.join(os.getcwd(), name), self.input_files)) - # Add any verilog_synth wrappers (which are needed in some + # Add any verilog_synth wrappers (which are needed in some # technologies e.g. for SRAMs) which need to be synthesized. abspath_input_files += self.technology.read_libs([ hammer_tech.filters.verilog_synth_filter diff --git a/tests/test_constraints.py b/tests/test_constraints.py index be1e2c0f6..f538f8c7a 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -192,7 +192,7 @@ def test_bump_naming(self) -> None: definition = BumpsDefinition(x=8421,y=8421, pitch_x=Decimal("1.23"), pitch_y=Decimal("3.14"), global_x_offset=Decimal('0'), global_y_offset=Decimal('0'), cell="bumpcell",assignments=assignments) assert BumpsPinNamingScheme.A1.name_bump(definition, assignments[0]) == "AAAA8421" - + def test_get_by_bump_dim_pitch(self) -> None: """ Test the extraction of x, y, pitches. @@ -208,7 +208,7 @@ def test_get_by_bump_dim_pitch(self) -> None: db = hammer_config.HammerDatabase() db.update_project([{"vlsi.inputs.bumps.pitch_x": 1}, {"vlsi.inputs.bumps.pitch": 2}]) tool.set_database(db) - + pitch_set = tool._get_by_bump_dim_pitch() assert pitch_set == {'x': 1, 'y': 2} @@ -243,7 +243,8 @@ def test_round_trip(self) -> None: name="mypin", clock="clock", direction="input", - delay=TimeValue("20 ns") + delay=TimeValue("20 ns"), + corner="setup" ) copied = DelayConstraint.from_dict(orig.to_dict()) assert orig == copied @@ -252,7 +253,8 @@ def test_round_trip(self) -> None: name="pin_2", clock="clock_20MHz", direction="output", - delay=TimeValue("0.3 ns") + delay=TimeValue("0.3 ns"), + corner="hold" ) copied = DelayConstraint.from_dict(orig.to_dict()) assert orig == copied @@ -266,7 +268,8 @@ def test_invalid_direction(self) -> None: name="mypin", clock="clock", direction="bad", - delay=TimeValue("20 ns") + delay=TimeValue("20 ns"), + corner=None ) with pytest.raises(ValueError): @@ -274,7 +277,8 @@ def test_invalid_direction(self) -> None: name="mypin", clock="clock", direction="inputt", - delay=TimeValue("20 ns") + delay=TimeValue("20 ns"), + corner=None ) with pytest.raises(ValueError): @@ -282,7 +286,8 @@ def test_invalid_direction(self) -> None: name="mypin", clock="clock", direction="inputoutput", - delay=TimeValue("20 ns") + delay=TimeValue("20 ns"), + corner=None ) with pytest.raises(ValueError): @@ -290,7 +295,8 @@ def test_invalid_direction(self) -> None: name="mypin", clock="clock", direction="", - delay=TimeValue("20 ns") + delay=TimeValue("20 ns"), + corner=None ) # Test that the error is raised with the dict as well. @@ -302,6 +308,38 @@ def test_invalid_direction(self) -> None: "delay": "20 ns" }) + def test_invalid_corner(self) -> None: + """ + Test that invalid corners are caught properly. + """ + with pytest.raises(ValueError): + DelayConstraint( + name="mypin", + clock="clock", + direction="input", + delay=TimeValue("20 ns"), + corner="typical" + ) + + with pytest.raises(ValueError): + DelayConstraint( + name="mypin", + clock="clock", + direction="input", + delay=TimeValue("20 ns"), + corner="" + ) + + # Test that the error is raised with the dict as well. + with pytest.raises(ValueError): + DelayConstraint.from_dict({ + "name": "mypin", + "clock": "clock", + "direction": "input", + "delay": "20 ns", + "corner": "typical" + }) + class TestDecapConstraint: def test_round_trip(self) -> None: