Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/sdf_toolkit/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Interconnect,
Iopath,
PathConstraint,
Period,
Port,
PortSpec,
Recovery,
Expand Down Expand Up @@ -55,6 +56,7 @@
"Interconnect",
"Iopath",
"PathConstraint",
"Period",
"Port",
"PortSpec",
"Recovery",
Expand Down
22 changes: 22 additions & 0 deletions src/sdf_toolkit/core/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Interconnect,
Iopath,
PathConstraint,
Period,
Port,
Recovery,
Removal,
Expand Down Expand Up @@ -639,6 +640,27 @@ def add_width(
cond_equation=cond_equation,
)

def add_period(
self,
pin: str,
delays: DelaysInput,
*,
pin_edge: EdgeType | None = None,
is_cond: bool = False,
cond_equation: str | None = None,
) -> "CellBuilder":
"""Add a period timing check entry."""
return self._add_timing_check(
Period,
pin,
pin,
delays,
from_pin_edge=pin_edge,
to_pin_edge=pin_edge,
is_cond=is_cond,
cond_equation=cond_equation,
)

def add_path_constraint(
self,
from_pin: str,
Expand Down
20 changes: 18 additions & 2 deletions src/sdf_toolkit/core/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class EntryType(StrEnum):
REMOVAL = "removal"
RECOVERY = "recovery"
WIDTH = "width"
PERIOD = "period"
SETUPHOLD = "setuphold"
PATHCONSTRAINT = "pathconstraint"

Expand Down Expand Up @@ -50,7 +51,9 @@ class DelayMetric(StrEnum):
MAX = "max"


DelayFieldLike = DelayField | Literal["nominal", "fast", "slow", "setup", "hold", "rise", "fall"]
DelayFieldLike = (
DelayField | Literal["nominal", "fast", "slow", "setup", "hold", "rise", "fall"]
)
DelayMetricLike = DelayMetric | Literal["min", "avg", "max"]


Expand Down Expand Up @@ -314,7 +317,11 @@ class DelayPaths:
rise: Values | None = None
fall: Values | None = None

def get_scalar(self, field: DelayFieldLike = DelayField.SLOW, metric: DelayMetricLike = DelayMetric.MAX) -> float | None:
def get_scalar(
self,
field: DelayFieldLike = DelayField.SLOW,
metric: DelayMetricLike = DelayMetric.MAX,
) -> float | None:
"""Extract a single float from a named field and metric.

Parameters
Expand Down Expand Up @@ -559,6 +566,15 @@ def __post_init__(self) -> None:
self.type = EntryType.WIDTH


@dataclass
class Period(TimingCheck):
"""Period timing check entry."""

def __post_init__(self) -> None:
"""Set entry type to PERIOD."""
self.type = EntryType.PERIOD


@dataclass
class SetupHold(TimingCheck):
"""SetupHold combined timing check entry."""
Expand Down
5 changes: 5 additions & 0 deletions src/sdf_toolkit/io/annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class SpecifyKind(StrEnum):
HOLD = "hold"
SETUPHOLD = "setuphold"
WIDTH = "width"
PERIOD = "period"
RECOVERY = "recovery"
REMOVAL = "removal"

Expand Down Expand Up @@ -368,6 +369,7 @@ def entries_to_specify(
EntryType.SETUP: SpecifyKind.SETUP,
EntryType.HOLD: SpecifyKind.HOLD,
EntryType.WIDTH: SpecifyKind.WIDTH,
EntryType.PERIOD: SpecifyKind.PERIOD,
EntryType.RECOVERY: SpecifyKind.RECOVERY,
EntryType.REMOVAL: SpecifyKind.REMOVAL,
}
Expand Down Expand Up @@ -473,6 +475,9 @@ def _format_specify_entry(entry: SpecifyEntry) -> str:
if entry.kind == SpecifyKind.WIDTH:
return f"{indent}$width({data_str}, {entry.rise_delay});"

if entry.kind == SpecifyKind.PERIOD:
return f"{indent}$period({data_str}, {entry.rise_delay});"

# hold, recovery, removal all share: $kind(ref, data, limit)
if entry.kind in (SpecifyKind.HOLD, SpecifyKind.RECOVERY, SpecifyKind.REMOVAL):
return f"{indent}${entry.kind}({ref_str}, {data_str}, {entry.rise_delay});"
Expand Down
2 changes: 1 addition & 1 deletion src/sdf_toolkit/io/templates/timingcheck.j2
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{%- set input_str = "(COND " ~ entry.cond_equation ~ " " ~ input_str ~ ")" -%}
{%- endif %}

{%- if entry.name.startswith('width') %}
{%- if entry.name.startswith('width') or entry.name.startswith('period') %}
{%- set output_str = "" -%}
{%- endif %}

Expand Down
4 changes: 3 additions & 1 deletion src/sdf_toolkit/parser/sdf.lark
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ timing_check: "(" "TIMINGCHECK" timing_check_list ")"

timing_check_list: t_check+

t_check: removal_check | recovery_check | hold_check | setup_check | width_check | setuphold_check
t_check: removal_check | recovery_check | hold_check | setup_check | width_check | period_check | setuphold_check

removal_check: "(" "REMOVAL" timing_port timing_port rvalue ")"

Expand All @@ -64,6 +64,8 @@ setup_check: "(" "SETUP" timing_port timing_port rvalue ")"

width_check: "(" "WIDTH" timing_port rvalue ")"

period_check: "(" "PERIOD" timing_port rvalue ")"

setuphold_check: "(" "SETUPHOLD" timing_port timing_port rvalue rvalue ")"

timing_port: port_spec | "(" "COND" equation port_spec ")"
Expand Down
7 changes: 7 additions & 0 deletions src/sdf_toolkit/parser/transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Interconnect,
Iopath,
PathConstraint,
Period,
Port,
PortSpec,
Recovery,
Expand Down Expand Up @@ -395,6 +396,12 @@ def width_check(self, port: TimingPortSpec, values: Values) -> Width:
paths = DelayPaths(nominal=values)
return self._make_timing_check(Width, port, port, paths)

@v_args(inline=True)
def period_check(self, port: TimingPortSpec, values: Values) -> Period:
"""Process period timing check."""
paths = DelayPaths(nominal=values)
return self._make_timing_check(Period, port, port, paths)

@v_args(inline=True)
def setuphold_check(
self,
Expand Down
27 changes: 27 additions & 0 deletions tests/test_annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Hold,
Interconnect,
Iopath,
Period,
Recovery,
SDFFile,
SDFHeader,
Expand Down Expand Up @@ -414,6 +415,21 @@ def test_width(self) -> None:
assert se.kind == "width"
assert se.from_edge == "posedge"

def test_period(self) -> None:
entries = [
Period(
name="period_CLK_CLK",
from_pin="CLK",
to_pin="CLK",
delay_paths=DelayPaths(nominal=Values(1.058, 1.058, 1.058)),
is_timing_check=True,
),
]
result = entries_to_specify(entries)
se = result[0]
assert se.kind == "period"
assert se.from_pin == "CLK"

def test_recovery(self) -> None:
entries = [
Recovery(
Expand Down Expand Up @@ -546,6 +562,17 @@ def test_width_check(self) -> None:
block = render_specify_block(entries)
assert "$width(posedge CP, 1:1:1);" in block

def test_period_check(self) -> None:
entries = [
SpecifyEntry(
kind="period",
from_pin="CLK",
rise_delay="1.058:1.058:1.058",
),
]
block = render_specify_block(entries)
assert "$period(CLK, 1.058:1.058:1.058);" in block

def test_recovery_check(self) -> None:
entries = [
SpecifyEntry(
Expand Down
3 changes: 2 additions & 1 deletion tests/test_new_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ def test_all_entry_types(self):
.add_recovery("RST", "CLK", delays)
.add_setuphold("D", "CLK", delays)
.add_width("CLK", delays)
.add_period("CLK", delays)
)
sdf = builder.build()
assert len(sdf.cells["TOP"]["top0"]) == 10
assert len(sdf.cells["TOP"]["top0"]) == 11


class TestValidate:
Expand Down
54 changes: 54 additions & 0 deletions tests/test_transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,57 @@ def test_invalid_port_spec_raises(self):
Token("ID", "CLK"),
Token("ID", "extra"),
)


class TestPeriodCheck:
PERIOD_SDF = """(DELAYFILE
(SDFVERSION "3.0")
(DESIGN "top")
(DIVIDER /)
(TIMESCALE 1.0 ns)
(CELL
(CELLTYPE "dff")
(INSTANCE ff0)
(TIMINGCHECK
(SETUP (posedge D) (posedge CLK) (0.118::0.118))
(WIDTH (posedge CLK) (0.495::0.495))
(PERIOD CLK (1.058::1.058))
(PERIOD (posedge CLK2) (2.0::2.5))
)
)
)"""

def test_period_check_parsed(self):
"""OpenSTA-style PERIOD checks parse into Period entries."""
result = parse_sdf(self.PERIOD_SDF)
entries = result.cells["dff"]["ff0"]
periods = [e for e in entries.values() if e.type == EntryType.PERIOD]
assert len(periods) == 2

plain = entries["period_CLK_CLK"]
assert plain.is_timing_check
assert plain.from_pin == "CLK"
assert plain.to_pin == "CLK"
assert plain.delay_paths.nominal.min == 1.058
assert plain.delay_paths.nominal.max == 1.058

def test_period_check_edge_qualified(self):
"""Edge-qualified PERIOD ports keep their edge."""
result = parse_sdf(self.PERIOD_SDF)
edged = result.cells["dff"]["ff0"]["period_CLK2_CLK2"]
assert edged.from_pin_edge is not None
assert edged.delay_paths.nominal.min == 2.0
assert edged.delay_paths.nominal.max == 2.5

def test_period_check_round_trip(self):
"""Emitting a parsed file reproduces the PERIOD check shape."""
from sdf_toolkit.io import emit

result = parse_sdf(self.PERIOD_SDF)
text = emit(result, timescale="1.0 ns")
assert "PERIOD" in text
# Re-parse the emitted text to prove the writer output is valid.
reparsed = parse_sdf(text)
entries = reparsed.cells["dff"]["ff0"]
periods = [e for e in entries.values() if e.type == EntryType.PERIOD]
assert len(periods) == 2