blob: c0b785876c68389dbfc9a6a5c95248f33f0e6202 [file] [log] [blame] [edit]
#!/usr/bin/env python3
import argparse
import os
import re
import json
import lxml.etree as ET
import sdf_timing.sdfparse
from sdf_timing.utils import get_scale_seconds
from termcolor import colored
DEBUG = 0
def log(ltype, message, outdesc=None):
"""Prints log messages.
Parameters
----------
ltype: str
Log type, can be INFO, WARNING, ERROR
message: str
Log message
"""
LOGLEVELS = ["INFO", "WARNING", "ERROR", "ALL"]
SUPPRESSBELOW = "ERROR"
if ltype not in LOGLEVELS[:-1]:
return
dat = {
"INFO": (0, "green"),
"WARNING": (1, "yellow"),
"ERROR": (2, "red"),
"ALL": (3, "black")
}
if dat[ltype][0] >= dat[SUPPRESSBELOW][0]:
print(colored("{}: {}".format(ltype, message), dat[ltype][1]))
if outdesc:
print(
colored("{}: {}".format(ltype, message), dat[ltype][1]),
file=outdesc
)
# =============================================================================
# RAM 2x1 ports, their widths and associated clocks.
# FIXME: Read that from the techfile or phy_db.pickle
RAM_2X1_PORTS = {
"input":
[
# RAM part 1, port 1
["WIDTH_SELECT1_0", 2, None],
["CLK1EN_0", 1, None], # "CLK1_0"],
["CS1_0", 1, None], # "CLK1_0"],
["A1_0", 11, "CLK1_0"],
["WD_0", 18, "CLK1_0"],
["WEN1_0", 2, "CLK1_0"],
["P1_0", 1, "CLK1_0"],
# RAM part 1, port 2
["WIDTH_SELECT2_0", 2, None],
["CLK2EN_0", 1, None], # "CLK2_0"],
["CS2_0", 1, None], # "CLK2_0"],
["A2_0", 11, "CLK2_0"],
["P2_0", 1, "CLK2_0"],
# RAM part 1, common
["CONCAT_EN_0", 1, None],
["PIPELINE_RD_0", 1, None],
["FIFO_EN_0", 1, None],
["DIR_0", 1, None],
["SYNC_FIFO_0", 1, None],
["ASYNC_FLUSH_0", 1, None],
# RAM part 2, port 1
["WIDTH_SELECT1_1", 2, None],
["CLK1EN_1", 1, None], # "CLK1_1"],
["CS1_1", 1, None], # "CLK1_1"],
["A1_1", 11, "CLK1_1"],
["WD_1", 18, "CLK1_1"],
["WEN1_1", 2, "CLK1_1"],
["P1_1", 1, "CLK1_1"],
# RAM part 2, port 2
["WIDTH_SELECT2_1", 2, None],
["CLK2EN_1", 1, None], # "CLK2_1"],
["CS2_1", 1, None], # "CLK2_1"],
["A2_1", 11, "CLK2_1"],
["P2_1", 1, "CLK2_1"],
# RAM part 2, common
["CONCAT_EN_1", 1, None],
["PIPELINE_RD_1", 1, None],
["FIFO_EN_1", 1, None],
["DIR_1", 1, None],
["SYNC_FIFO_1", 1, None],
["ASYNC_FLUSH_1", 1, None],
# Common, unknown
["DS", 1, None],
["DS_RB1", 1, None],
["LS", 1, None],
["LS_RB1", 1, None],
["SD", 1, None],
["SD_RB1", 1, None],
["RMA", 4, None],
["RMB", 4, None],
["RMEA", 1, None],
["RMEB", 1, None],
["TEST1A", 1, None],
["TEST1B", 1, None],
],
"clock":
[
# RAM part 1
["CLK1_0", 1, None],
["CLK2_0", 1, None],
# RAM part 2
["CLK1_1", 1, None],
["CLK2_1", 1, None],
],
"output":
[
# RAM part 1, port 1
["Almost_Full_0", 1, "CLK1_0"],
["PUSH_FLAG_0", 4, "CLK1_0"],
# RAM part 1, port 2
["Almost_Empty_0", 1, "CLK2_0"],
["POP_FLAG_0", 4, "CLK2_0"],
["RD_0", 18, "CLK2_0"],
# RAM part 2, port 1
["Almost_Full_1", 1, "CLK1_1"],
["PUSH_FLAG_1", 4, "CLK1_1"],
# RAM part 2, port 2
["Almost_Empty_1", 1, "CLK2_1"],
["POP_FLAG_1", 4, "CLK2_1"],
["RD_1", 18, "CLK2_1"],
],
}
# A list of non-routable ports
RAM_2X1_NON_ROUTABLE_PORTS = {
"input":
[
["CLK1S_0", 1, None],
["CLK2S_0", 1, None],
["CLK1S_1", 1, None],
["CLK2S_1", 1, None],
["ASYNC_FLUSH_S0", 1, None],
["ASYNC_FLUSH_S1", 1, None],
]
}
# A list of test ports
RAM_TEST_PORTS = [
"DS",
"DS_RB1",
"LS",
"LS_RB1",
"SD",
"SD_RB1",
"RMA",
"RMB",
"RMEA",
"RMEB",
"TEST1A",
"TEST1B",
]
# A list of test ports specific to a device
RAM_2X1_TEST_PORTS = {
"ql-eos-s3": RAM_TEST_PORTS,
"ql-pp3e": [],
}
# A list of ports relevant only in RAM mode
RAM_PORTS = [
"WEN1",
"WEN2",
"A1",
"A2",
]
# A list of ports relevant only in FIFO mode
FIFO_PORTS = [
"DIR",
"SYNC_FIFO",
"ASYNC_FLUSH",
"P1",
"P2",
"Almost_Full",
"Almost_Empty",
"PUSH_FLAG",
"POP_FLAG",
]
# A clock map for FIFO operating in DIR=1 mode
FIFO_CLOCK_MAP = {
"CLK1_0": "CLK2_0",
"CLK2_0": "CLK1_0",
"CLK1_1": "CLK2_1",
"CLK2_1": "CLK1_1",
"CLK1": "CLK2",
"CLK2": "CLK1",
}
# A list of non-splitable ports
NON_SPLITABLE_PORTS = [
"WIDTH_SELECT1_0",
"WIDTH_SELECT2_0",
"WIDTH_SELECT1_1",
"WIDTH_SELECT2_1",
"WIDTH_SELECT1",
"WIDTH_SELECT2",
]
# =============================================================================
def blacklist_ports(ports, blacklist):
"""
Removes port present on the blacklist
"""
new_ports = dict()
for key, ports in ports.items():
new_ports[key] = []
for name, width, assoc_clock in ports:
if name not in blacklist:
new_ports[key].append([name, width, assoc_clock])
return new_ports
# =============================================================================
def yield_ram_modes(ram_tree):
"""
Yields all possible combinations of ram modes. Generated strings contain
comma separated control signal conditions.
"""
def inner(d, cond_prefix=None):
for k, v in d.items():
if not cond_prefix:
cond = k
else:
cond = cond_prefix + "," + k
if v is None:
yield cond
else:
yield from inner(v, cond)
yield from inner(ram_tree)
def make_mode_name(cond):
"""
Formats a mode name given a condition set
"""
name = []
for c in cond.split(","):
sig, val = [s.strip() for s in c.strip().split("=")]
# If the signal name ends with a digit, replace it with a letter
# FIXME: Assuming start from 1
if sig[-1].isdigit():
n = int(sig[-1])
sig = sig[:-1] + "_" + chr(ord('A') + n - 1)
# Abbreviate the signal
fields = sig.split("_")
sig = "".join([f[0] for f in fields])
# Append the value
sig += val
name.append(sig)
return "_".join(name)
# =============================================================================
def filter_cells(
timings, cond, part=None, normalized_names=False, debug=False
):
"""
Filters SDF timings basing on conditions present in cell type names.
If a negated condition from the condition list is found in a cell type
then that cell type is rejected.
"""
# Make false condition strings
cond_strs = []
for c in cond.split(","):
sig, val = [s.strip() for s in c.strip().split("=")]
# There appears to be a single CONCAT_EN_0 condition for both cells.
# Handle it here separately
if sig == "CONCAT_EN":
nval = str(1 - int(val))
cond_strs.append(sig + "_0" + "_EQ_" + nval)
continue
# For a single part
if part is not None:
assert part in [0, 1], part
part_str = "_{}".format(int(part))
# Single bit
# "<param>_EQ_<!val>"
nval = str(1 - int(val))
cond_strs.append(sig + part_str + "_EQ_" + nval)
# Single bit of a multi-bit parameter
# "<param>_<bit_idx>_EQ_<!bit_val>"
# FIXME: Assuming 2-bit max !!
for b in range(4):
nval = str(int((int(val) & (1 << b)) == 0))
if normalized_names:
cond_strs.append(sig + part_str + str(b) + "_EQ_" + nval)
else:
cond_strs.append(
sig + part_str + "[{}]".format(b) + "_EQ_" + nval
)
# Reject the other part completely
other_part_str = "_{}".format(int(1 - part))
cond_strs.append(sig + other_part_str + "_EQ_0")
cond_strs.append(sig + other_part_str + "_EQ_1")
# FIXME: Assuming 2-bit max !!
for b in range(4):
if normalized_names:
cond_strs.append(sig + other_part_str + str(b) + "_EQ_0")
cond_strs.append(sig + other_part_str + str(b) + "_EQ_1")
else:
cond_strs.append(
sig + other_part_str + "[{}]".format(b) + "_EQ_0"
)
cond_strs.append(
sig + other_part_str + "[{}]".format(b) + "_EQ_1"
)
# For both parts
else:
# Assume that control parameters must be equal for both parts
nval = str(1 - int(val))
cond_strs.append(sig + "_0" + "_EQ_" + nval)
cond_strs.append(sig + "_1" + "_EQ_" + nval)
for b in range(4):
nval = str(int((int(val) & (1 << b)) == 0))
if normalized_names:
cond_strs.append(sig + "_0" + str(b) + "_EQ_" + nval)
cond_strs.append(sig + "_1" + str(b) + "_EQ_" + nval)
else:
cond_strs.append(
sig + "_0" + "[{}]".format(b) + "_EQ_" + nval
)
cond_strs.append(
sig + "_1" + "[{}]".format(b) + "_EQ_" + nval
)
# DEBUG
if debug:
print(cond)
for s in cond_strs:
print("", "!" + s)
# Filter
cells = {}
for cell_type, cell_data in timings.items():
if debug:
print("", "check", cell_type)
# If any of the false conditions is found in the cell_type then the
# cell is rejected
reject = False
for s in cond_strs:
if s in cell_type:
if debug:
print(" ", "reject", s)
reject = True
# The cell is ok
if not reject:
if debug:
print(" ", "OK")
assert cell_type not in cells
cells[cell_type] = cell_data
return cells
def filter_instances(timings, name):
"""
Filters a single cell instance from the given timing data
"""
cells = {}
for cell_type, cell_data in timings.items():
# Don't have that instance
if name not in cell_data:
continue
# Leave only the one that we are looking for
cells[cell_type] = {name: cell_data[name]}
return cells
def find_timings(timings, src_pin, dst_pin):
path_timings = {}
for cell_type, cell_data in timings.items():
# FIXME: Skip falling edges
if "FALLING" in cell_type:
continue
for inst_name, inst_data in cell_data.items():
for tname, tdata in inst_data.items():
# Got matching pins
if src_pin in tdata["from_pin"]:
if dst_pin in tdata["to_pin"]:
if tname in path_timings:
print("new", cell_type)
print("old", path_timings[tname][0])
# FIXME
continue
path_timings[tname] = (
cell_type,
inst_name,
tdata,
)
return path_timings
# =============================================================================
def make_single_ram(ports):
"""
Makes port definition for a single RAM cell (not 2x1)
"""
new_ports = {
"input": set(),
"clock": set(),
"output": set(),
}
for key in ["input", "clock", "output"]:
all_port_names = set([p[0] for p in ports[key]])
for name, width, assoc_clock in ports[key]:
# Make a generic associated clock name
if assoc_clock is not None:
if assoc_clock.endswith("_0"):
base_assoc_clock = assoc_clock[:-2]
if assoc_clock.endswith("_1"):
base_assoc_clock = assoc_clock[:-2]
else:
base_assoc_clock = None
# Make a generic port name
if name.endswith("_0"):
base = name[:-2]
if (base + "_1") in all_port_names:
new_ports[key].add((base, width, base_assoc_clock))
elif name.endswith("_1"):
base = name[:-2]
if (base + "_0") in all_port_names:
new_ports[key].add((base, width, base_assoc_clock))
# This is a common port for both RAMs, add it unchanged
else:
assert key == "input", (
key,
name,
width,
assoc_clock,
)
new_ports[key].add((
name,
width,
assoc_clock,
))
return new_ports
def remap_clocks(ports, clock_map):
"""
Remaps clock dependencies of all ports and returns a new port map.
Does not remap clocks for "P1" and "P2" ports.
"""
new_ports = {
"input": set(),
"clock": set(),
"output": set(),
}
for key in ["input", "clock", "output"]:
for name, width, assoc_clock in ports[key]:
# P1 and P2 are not subject to the remap
if "P1" not in name and "P2" not in name:
# Remap the associated clock
if assoc_clock in clock_map:
assoc_clock = clock_map[assoc_clock]
new_ports[key].add((
name,
width,
assoc_clock,
))
return new_ports
def filter_ports(ports, ports_to_filter):
"""
Removes timing relation from ports of a model
"""
# Make a copy
new_ports = dict()
for key in ["input", "clock", "output"]:
new_ports[key] = []
for name, width, assoc_clock in ports[key]:
if key in ["input", "output"]:
if name.endswith("_0") or name.endswith("_1"):
generic_name = name[:-2]
if generic_name in ports_to_filter:
assoc_clock = None
if name in ports_to_filter:
assoc_clock = None
new_ports[key].append((
name,
width,
assoc_clock,
))
return new_ports
# =============================================================================
def make_pin_name(port, index):
"""
Formats a pin name of a multi-bit port
"""
return "{}_b{}".format(port, index)
def split_port_bit_index(name):
"""
Extracts bit index from the port
"""
m = re.match(r"(?P<name>.*)(_b(?P<bit>[0-9]+))$", name)
if m is not None:
return m.group("name"), int(m.group("bit")),
return name, None
def parse_port_name(name):
"""
Parses a port name. Returns the base name, cell index and bit index.
>>> parse_port_name("A_PORT")
('A_PORT', None, None)
>>> parse_port_name("A_PORT_0")
('A_PORT', 0, None)
>>> parse_port_name("A_PORT_b31")
('A_PORT', None, 31)
>>> parse_port_name("A_PORT_0_b15")
('A_PORT', 0, 15)
"""
# A multi-bit port
m = re.match(r"(?P<name>.*)(_b(?P<bit>[0-9]+))$", name)
if m is not None:
port = m.group("name")
bit = int(m.group("bit"))
else:
port = name
bit = None
# A port of a sub-cell
m = re.match(r"(?P<name>.*)(_(?P<cell>[0-9]+))$", port)
if m is not None:
return m.group("name"), int(m.group("cell")), bit
return port, None, bit
def split_ports(ports):
"""
Splits multi-bit ports into single-bit
"""
split_ports = dict()
for key in ports.keys():
split_ports[key] = list()
for name, width, assoc_clock in ports[key]:
if width == 1 or name in NON_SPLITABLE_PORTS:
split_ports[key].append((
name,
width,
assoc_clock,
))
else:
for i in range(width):
split_ports[key].append(
(make_pin_name(name, i), 1, assoc_clock)
)
return split_ports
def make_model(model_name, ports):
"""
Makex a model XML given the port definition
"""
# The model root
xml_model = ET.Element("model", {
"name": model_name,
})
# Port lists
xml_ports = {
"input": ET.SubElement(xml_model, "input_ports"),
"output": ET.SubElement(xml_model, "output_ports"),
}
xml_ports["clock"] = xml_ports["input"]
# Emits a XML tag for a port
def add_port(xml_parent, name, width, assoc_clock):
# A clock
if key == "clock":
assert assoc_clock is None, (name, width, assoc_clock)
attrs = {"is_clock": "1"}
# An input / output
else:
if assoc_clock is not None:
attrs = {"clock": assoc_clock}
else:
attrs = dict()
ET.SubElement(xml_parent, "port", {"name": name, **attrs})
# Add ports
for key in ["clock", "input", "output"]:
for name, width, assoc_clock in ports[key]:
add_port(xml_ports[key], name, width, assoc_clock)
return xml_model
def make_pb_type(
pb_name,
ports,
model_name,
timings=None,
timescale=1.0,
normalized_names=False
):
stats = {
"total_timings": 0,
"missing_timings": 0,
}
# The pb_type tag
if model_name:
attrs = {"blif_model": ".subckt {}".format(model_name)}
else:
attrs = dict()
xml_pb = ET.Element("pb_type", {"name": pb_name, "num_pb": "1", **attrs})
# Ports
for key in ["clock", "input", "output"]:
for name, width, *data in ports[key]:
ET.SubElement(
xml_pb, key, {
"name": name,
"num_pins": str(width),
}
)
# fasm
if model_name:
xml_metadata = ET.SubElement(xml_pb, "metadata")
xml_fasmprefix = ET.SubElement(
xml_metadata, "meta", {"name": "fasm_prefix"}
)
xml_fasmprefix.text = "RAM.RAM"
xml_fasm = ET.SubElement(xml_metadata, "meta", {"name": "fasm_params"})
if "_0_" in pb_name:
xml_fasm.text = "\n\
INIT[9215:0] = INIT\n"
elif "_1_" in pb_name:
xml_fasm.text = "\n\
INIT[18431:9216] = INIT\n"
else:
xml_fasm.text = "\n\
INIT[18431:0] = INIT\n"
# Timings
if timings is not None:
# Setup and hold
for name, width, assoc_clock in ports["input"]:
if assoc_clock is None:
continue
# Split the port and its bit index (if any)
alias, bit = split_port_bit_index(name)
# Find all timing paths for that port
path_timings = find_timings(timings, assoc_clock, alias.upper())
# Bit index suffix
if bit is not None:
if normalized_names:
suffix = "{}".format(bit)
else:
suffix = "[{}]".format(bit)
else:
suffix = ""
# Setup
for key in path_timings:
if key.startswith("setup") and key.endswith(suffix):
tim = path_timings[key][2]
delay = tim["delay_paths"]["nominal"]["avg"] * timescale
break
if key.startswith("setuphold") and key.endswith(suffix):
tim = path_timings[key][2]
print("SETUPHOLD", tim)
exit(-1)
delay = tim["delay_paths"]["nominal"]["avg"] * timescale
break
else:
delay = 1e-10
stats["missing_timings"] += 1
log(
"WARNING",
"No setup timing for '{}'->'{}' for pb_type '{}'".format(
alias + suffix, assoc_clock, pb_name
)
)
stats["total_timings"] += 1
ET.SubElement(
xml_pb, "T_setup", {
"port": name,
"clock": assoc_clock,
"value": "{:+e}".format(delay)
}
)
# Hold
for key in path_timings:
if key.startswith("hold") and key.endswith(suffix):
tim = path_timings[key][2]
delay = tim["delay_paths"]["nominal"]["avg"] * timescale
break
if key.startswith("setuphold") and key.endswith(suffix):
tim = path_timings[key][2]
print("SETUPHOLD", tim)
exit(-1)
delay = tim["delay_paths"]["nominal"]["avg"] * timescale
break
else:
delay = 1e-10
stats["missing_timings"] += 1
log(
"WARNING",
"No hold timing for '{}'->'{}' for pb_type '{}'".format(
alias + suffix, assoc_clock, pb_name
)
)
stats["total_timings"] += 1
ET.SubElement(
xml_pb, "T_hold", {
"port": name,
"clock": assoc_clock,
"value": "{:+e}".format(delay)
}
)
# Clock to Q
for name, width, assoc_clock in ports["output"]:
if assoc_clock is None:
continue
# Split the port and its bit index (if any)
alias, bit = split_port_bit_index(name)
# Find all timing paths for that port
path_timings = find_timings(timings, assoc_clock, alias.upper())
# Index suffixes
# Bit index suffix
if bit is not None:
if normalized_names:
suffix = "{}".format(bit)
else:
suffix = "[{}]".format(bit)
else:
suffix = ""
# "Clock to Q"
for key in path_timings:
if key.startswith("iopath") and key.endswith(suffix):
tim = path_timings[key][2]
delay_min = tim["delay_paths"]["slow"]["min"] * timescale
delay_max = tim["delay_paths"]["slow"]["max"] * timescale
break
else:
delay_min = 1e-10
delay_max = 1e-10
stats["missing_timings"] += 1
log(
"WARNING",
"No \"clock to Q\" timing for '{}'->'{}' for pb_type '{}'".
format(assoc_clock, alias + suffix, pb_name)
)
stats["total_timings"] += 1
ET.SubElement(
xml_pb, "T_clock_to_Q", {
"port": name,
"clock": assoc_clock,
"min": "{:+e}".format(delay_min),
"max": "{:+e}".format(delay_max)
}
)
return xml_pb, stats
# =============================================================================
def auto_interconnect(pb_type):
"""
Automatically generates an interconnect that connects matching ports of
a child pb_type to its parent. Moreover, when a child pb_type is suffixed
with sth. like "_0" then parent ports with the same suffix are matched.
"""
def get_ports(pb, type):
"""
Yields pb_type ports of the given type
"""
for port in pb.findall(type):
yield port.attrib["name"], int(port.attrib["num_pins"]),
# Get parent for the interconnect (can be either "mode" or "pb_type")
assert pb_type.tag == "mode"
pb_parent = pb_type.getparent()
assert pb_parent.tag == "pb_type"
pb_children = list(pb_type.findall("pb_type"))
# Upstream ports
parent_name = pb_parent.attrib["name"]
parent_ports = set([(*p, "I") for p in get_ports(pb_parent, "input")])
parent_ports |= set([(*p, "I") for p in get_ports(pb_parent, "clock")])
parent_ports |= set([(*p, "O") for p in get_ports(pb_parent, "output")])
# Downstream ports
children = {}
for child in pb_children:
name = child.attrib["name"]
ports = set([(*p, "I") for p in get_ports(child, "input")])
ports |= set([(*p, "I") for p in get_ports(child, "clock")])
ports |= set([(*p, "O") for p in get_ports(child, "output")])
children[name] = ports
# Adds a connection
def add_connection(xml, iname, oname, reverse=False):
if reverse:
ET.SubElement(
xml, "direct", {
"name": "{}_to_{}".format(oname, iname),
"input": oname,
"output": iname
}
)
else:
ET.SubElement(
xml, "direct", {
"name": "{}_to_{}".format(iname, oname),
"input": iname,
"output": oname
}
)
# Create the interconnect
ic = ET.SubElement(pb_type, "interconnect")
for child in sorted(children.keys()):
child_ports = sorted(list(children[child]), key=lambda x: x[0])
# Get the child cell index
m = re.match(r"(.*)(_(?P<idx>[0-9]+))$", child)
if m is not None:
child_index = int(m.group("idx"))
else:
child_index = None
# Loop over all child ports
for dn_port, dn_width, dn_type in child_ports:
# Got a 1-to-1 match
if (dn_port, dn_width, dn_type) in parent_ports:
iname = "{}.{}".format(parent_name, dn_port)
oname = "{}.{}".format(child, dn_port)
add_connection(ic, iname, oname, dn_type == "O")
continue
# Parse the child port name
dn_alias, dn_index, dn_bit = parse_port_name(dn_port)
# Got a match with the child cell index
up_port = "{}_{}".format(dn_alias, child_index)
if (up_port, dn_width, dn_type) in parent_ports:
iname = "{}.{}".format(parent_name, up_port)
oname = "{}.{}".format(child, dn_port)
add_connection(ic, iname, oname, dn_type == "O")
continue
# The downstream port is split into bits
if dn_bit is not None:
# Try without the cell index
ports = [(*p, ) for p in parent_ports if p[0] == dn_alias]
assert len(ports) < 2, ports
if ports:
up_port, up_width, up_type = ports[0]
iname = "{}.{}[{}]".format(parent_name, up_port, dn_bit)
oname = "{}.{}".format(child, dn_port)
add_connection(ic, iname, oname, dn_type == "O")
continue
up_alias = "{}_{}".format(dn_alias, dn_index)
ports = [(*p, ) for p in parent_ports if p[0] == up_alias]
assert len(ports) < 2, ports
if ports:
up_port, up_width, up_type = ports[0]
iname = "{}.{}[{}]".format(parent_name, up_port, dn_bit)
oname = "{}.{}".format(child, dn_port)
add_connection(ic, iname, oname, dn_type == "O")
continue
# Couldn't find a matching port
print(
"ERROR: No matching parent port in '{}' for '{}.{}'".format(
parent_name, child, dn_port
)
)
return ic
# =============================================================================
def make_ports(ports, separator=",\n"):
verilog = ""
for key in ["clock", "input", "output"]:
if key == "clock":
type = "input"
else:
type = key
verilog += "\n"
for name, width, assoc_clock in ports[key]:
if width > 1:
verilog += " {:<6} [{:2d}:0] {}{}".format(
type, width - 1, name, separator
)
else:
verilog += " {:<6} {}{}".format(type, name, separator)
return verilog
def make_ram2x1_instance(device, ports, separator=",\n"):
verilog = ""
verilog += "\n ram8k_2x1_cell # (.INIT(INIT)) I1 ( \n"
for key in ["clock", "input", "output"]:
for name, width, assoc_clock in ports[key]:
if name not in RAM_2X1_TEST_PORTS[device]:
if name[:-3] not in RAM_2X1_TEST_PORTS[device]:
if name.find("_0") >= 0 or name.find("_1") >= 0:
verilog += " .{}({}){}".format(
name, name, separator
)
elif name.find("_b") >= 0:
verilog += " .{}({}){}".format(
name.replace("_b", "_0_b"), name, separator
)
else:
verilog += " .{}_0({}){}".format(
name, name, separator
)
verilog = verilog[:-2] + ");\n\n"
return verilog
def make_specify(ports, separator=";\n"):
verilog = ""
verilog += "\n specify\n"
for key in ["clock", "input", "output"]:
for name, width, assoc_clock in ports[key]:
if key == "input":
if assoc_clock is not None:
verilog += " $setup({}, posedge {}, \"\"){}".format(
name, assoc_clock, separator
)
verilog += " $hold(posedge {}, {}, \"\"){}".format(
assoc_clock, name, separator
)
elif key == "output":
if assoc_clock is not None:
verilog += " ({}*>{})=\"\"{}".format(
assoc_clock, name, separator
)
verilog += " endspecify\n\n"
return verilog
def make_blackbox(device, name, ports, specify_ports):
# Header
verilog = """
`timescale 1ns/10ps
(* blackbox *)
module {} (
""".format(name)
# Ports
verilog += make_ports(ports)
verilog = verilog[:-2] + "\n"
verilog += ");\n"
verilog += " parameter [18431:0] INIT = 18432'bx;\n"
# Specify
verilog += make_specify(specify_ports)
# RAM2x1 cell instance
verilog += make_ram2x1_instance(device, ports)
# Footer
verilog += "endmodule\n"
return verilog
def make_techmap(port_blacklist, conditions):
# The original cell name
cell_name = "ram8k_2x1_cell_macro"
# Header
verilog = "module {} (\n".format(cell_name)
# Ports
map_ports = {}
for key, ports in RAM_2X1_PORTS.items():
map_ports[key] = list(ports)
if key in RAM_2X1_NON_ROUTABLE_PORTS:
map_ports[key] += RAM_2X1_NON_ROUTABLE_PORTS[key]
verilog += make_ports(map_ports)
verilog = verilog[:-2] + "\n"
verilog += ");\n"
verilog += "\n"
# Gather significant control parameters
control_signals = set()
for condition in conditions:
for cond_str in condition.split(","):
sig, val = cond_str.split("=")
for part in [0, 1]:
part_sig = "{}_{}".format(sig, part)
control_signals.add(part_sig)
# Techmap special parameters
for sig in sorted(control_signals):
verilog += " parameter _TECHMAP_CONSTMSK_{}_ = 1'bx;\n".format(sig)
verilog += " parameter _TECHMAP_CONSTVAL_{}_ = 1'bx;\n".format(sig)
verilog += "\n"
verilog += " parameter [18431:0] INIT = 18432'bx;\n"
# Split condition strings for single and dual ram modes
sing_conditions = [c for c in conditions if "CONCAT_EN=0" in c]
dual_conditions = [c for c in conditions if "CONCAT_EN=1" in c]
# Split RAM mode
verilog_cond = []
verilog_cond.append("(_TECHMAP_CONSTVAL_CONCAT_EN_0_ == 1'b0)")
verilog_cond.append("(_TECHMAP_CONSTVAL_CONCAT_EN_1_ == 1'b0)")
verilog_cond = " && ".join(verilog_cond)
verilog += " // Split RAM\n"
verilog += " generate if({}) begin\n".format(verilog_cond)
# Each part is independent
model_ports = split_ports(blacklist_ports(RAM_2X1_PORTS, port_blacklist))
for part in [0, 1]:
verilog += " // RAM {}\n".format(part)
for i, condition in enumerate(sing_conditions):
# Case condition
verilog_cond = []
for condition_str in condition.split(","):
sig, val = condition_str.split("=")
if sig == "CONCAT_EN":
continue
cond_part = "(_TECHMAP_CONSTVAL_{}_{}_ == {})".format(
sig, part, val
)
verilog_cond.append(cond_part)
verilog_cond = " && ".join(verilog_cond)
if i == 0:
verilog += " if ({}) begin\n".format(verilog_cond)
else:
verilog += " end else if ({}) begin\n".format(verilog_cond)
# Instance
mode_name = make_mode_name(condition)
model_name = "RAM_" + mode_name + "_VPR"
verilog += " {} # (\n".format(model_name)
if part == 0:
verilog += " .INIT(INIT[9215:0]),\n"
else:
verilog += " .INIT(INIT[18431:9216]),\n"
verilog += " )"
verilog += " RAM_{} (\n".format(part)
# Ports mapped to the part
for key in ["clock", "input", "output"]:
for name, width, assoc_clock in model_ports[key]:
# Parse the port name
alias, index, bit = parse_port_name(name)
# Not for that part
if index is not None and part != index:
continue
if index is None:
pname = alias
else:
pname = "{}_{}".format(alias, index)
if bit is not None:
portspec = "{}_b{}".format(alias, bit)
sigspec = "{}[{}]".format(pname, bit)
else:
portspec = alias
sigspec = pname
verilog += " .{}({}),\n".format(portspec, sigspec)
verilog = verilog[:-2] + "\n"
verilog += " );\n"
# Error catcher
verilog += """
end else begin
wire _TECHMAP_FAIL_;
end
"""
# Non-split (concatenated) RAM mode
verilog_cond = []
# It appears that only CONCAT_EN_0 needs to be set
verilog_cond.append("(_TECHMAP_CONSTVAL_CONCAT_EN_0_ == 1'b1)")
verilog_cond = " && ".join(verilog_cond)
verilog += " // Concatenated RAM\n"
verilog += " end else if({}) begin\n".format(verilog_cond)
for i, condition in enumerate(dual_conditions):
# Case condition
verilog_cond = []
for condition_str in condition.split(","):
sig, val = condition_str.split("=")
if sig == "CONCAT_EN":
continue
cond_part = "(_TECHMAP_CONSTVAL_{}_0_ == {})".format(sig, val)
verilog_cond.append(cond_part)
verilog_cond = " && ".join(verilog_cond)
if i == 0:
verilog += " if ({}) begin\n".format(verilog_cond)
else:
verilog += " end else if ({}) begin\n".format(verilog_cond)
# Instance
mode_name = make_mode_name(condition)
model_name = "RAM_" + mode_name + "_VPR"
verilog += " {} # (\n".format(model_name)
verilog += " .INIT(INIT[18431:0]),\n"
verilog += " )"
verilog += " RAM (\n"
# Ports always mapped 1-to-1
for key in ["clock", "input", "output"]:
for name, width, assoc_clock in model_ports[key]:
# Parse the port name
alias, index, bit = parse_port_name(name)
if index is None:
pname = alias
else:
pname = "{}_{}".format(alias, index)
if bit is not None:
portspec = "{}_b{}".format(pname, bit)
sigspec = "{}[{}]".format(pname, bit)
else:
portspec = pname
sigspec = pname
verilog += " .{}({}),\n".format(portspec, sigspec)
verilog = verilog[:-2] + "\n"
verilog += " );\n"
# Error catcher
verilog += """
end else begin
wire _TECHMAP_FAIL_;
end
"""
# Error handler for unexpected configuration
verilog += """
end else begin
wire _TECHMAP_FAIL_;
end endgenerate
"""
# Footer
verilog += "endmodule\n"
return verilog
# =============================================================================
def main():
# Parse arguments
parser = argparse.ArgumentParser()
parser.add_argument(
"--device", type=str, choices=["ql-eos-s3", "ql-pp3e"], required=True
)
parser.add_argument(
"--mode-defs",
type=str,
default="ram_modes.json",
help="A JSON file defining RAM modes"
)
parser.add_argument(
"--sdf", type=str, required=True, help="An SDF file with timing data"
)
parser.add_argument(
"--xml-path", type=str, default="./", help="Output path for XML files"
)
parser.add_argument(
"--vlog-path",
type=str,
default="./",
help="Output path for Verilog files"
)
args = parser.parse_args()
# Prepare RAM port blacklist (containing test ports)
port_blacklist = set()
if not RAM_2X1_TEST_PORTS[args.device]:
port_blacklist = set(RAM_TEST_PORTS)
# Load the RAM mode tree definition
with open(args.mode_defs, "r") as fp:
ram_tree = json.load(fp)
# Load RAM timings
with open(args.sdf, "r") as fp:
ram_timings = sdf_timing.sdfparse.parse(fp.read())
timescale = get_scale_seconds(ram_timings["header"]["timescale"])
# Make port definition for a single ram
ram_ports_all = blacklist_ports(RAM_2X1_PORTS, port_blacklist)
ram_ports_sing = make_single_ram(ram_ports_all)
# Gather all RAM instances
instances = set()
for v in ram_timings["cells"].values():
instances |= set(v.keys())
# Use not normalized names in SDFs (i.e. with brackets)
# TODO: Make this an argument
normalized_names = False
xml_models = {}
# Generate a pb_type tree for each instance
for instance in instances:
print(instance)
# Initialize the top-level pb_type XML
xml_pb_root = make_pb_type("RAM", ram_ports_all, None)[0]
# Wrapper pb_type for split RAM (CONCAT_EN=0)
xml_mode = ET.SubElement(xml_pb_root, "mode", {"name": "SING"})
xml_sing = [
make_pb_type("RAM_0", ram_ports_sing, None)[0],
make_pb_type("RAM_1", ram_ports_sing, None)[0],
]
for x in xml_sing:
xml_mode.append(x)
ic = auto_interconnect(xml_mode)
xml_mode.append(ic)
# Wrapper pb_type for non-split RAM (CONCAT_EN=1)
xml_mode = ET.SubElement(xml_pb_root, "mode", {"name": "DUAL"})
xml_dual = make_pb_type("RAM_DUAL", ram_ports_all, None)[0]
xml_mode.append(xml_dual)
ic = auto_interconnect(xml_mode)
xml_mode.append(ic)
# Get timings for this instance
all_timings = filter_instances(ram_timings["cells"], instance)
total_timings = 0
missing_timings = 0
# Generate RAM modes
for cond in yield_ram_modes(ram_tree):
print("", cond)
mode_name = make_mode_name(cond)
model_name = "RAM_" + mode_name + "_VPR"
# CONCAT_EN=0 - the 2x1 RAM is split into two
if "CONCAT_EN=0" in cond:
if "FIFO_EN=0" in cond:
model_ports = filter_ports(ram_ports_sing, FIFO_PORTS)
else:
model_ports = filter_ports(ram_ports_sing, RAM_PORTS)
if "DIR=1" in cond:
model_ports = remap_clocks(model_ports, FIFO_CLOCK_MAP)
# For each part
for part in [0, 1]:
# Filter timings basing on the generated set of conditions
# and RAM part
timings = filter_cells(
all_timings,
cond,
part,
normalized_names=normalized_names
)
if DEBUG:
print(" RAM_" + str(part))
for cname, cdata in timings.items():
print(" ", cname)
for iname, idata in cdata.items():
for tname, tdata in idata.items():
print(
" ", tname, "src={}, dst={}".format(
tdata["from_pin"], tdata["to_pin"]
)
)
# Make the mode XML
xml_mode = ET.SubElement(
xml_sing[part], "mode", {"name": mode_name}
)
# Make the pb_type XML
pb_name = "RAM_{}_{}".format(part, mode_name)
xml_pb, stats = make_pb_type(
pb_name,
split_ports(model_ports),
model_name,
timings,
timescale,
normalized_names=normalized_names,
)
xml_mode.append(xml_pb)
total_timings += stats["total_timings"]
missing_timings += stats["missing_timings"]
# Make the interconnect
ic = auto_interconnect(xml_mode)
xml_mode.append(ic)
# Make the model XML
xml_model = make_model(model_name, split_ports(model_ports))
xml_models[model_name] = xml_model
# CONCAT_EN=1 - keep the 2x1 RAM as one
elif "CONCAT_EN=1" in cond:
if "FIFO_EN=0" in cond:
model_ports = filter_ports(ram_ports_all, FIFO_PORTS)
else:
model_ports = filter_ports(ram_ports_all, RAM_PORTS)
if "DIR=1" in cond:
model_ports = remap_clocks(model_ports, FIFO_CLOCK_MAP)
# Filter timings
timings = filter_cells(
all_timings, cond, None, normalized_names=normalized_names
)
if DEBUG:
print(" Dual RAM")
for cname, cdata in timings.items():
print(" ", cname)
for iname, idata in cdata.items():
for tname, tdata in idata.items():
print(
" ", tname, "src={}, dst={}".format(
tdata["from_pin"], tdata["to_pin"]
)
)
# Make the mode XML
xml_mode = ET.SubElement(xml_dual, "mode", {"name": mode_name})
# Make the pb_type XML
pb_name = "RAM_" + mode_name
xml_pb, stats = make_pb_type(
pb_name,
split_ports(model_ports),
model_name,
timings,
timescale,
normalized_names=normalized_names,
)
xml_mode.append(xml_pb)
total_timings += stats["total_timings"]
missing_timings += stats["missing_timings"]
# Make the interconnect
ic = auto_interconnect(xml_mode)
xml_mode.append(ic)
# Make the model XML
xml_model = make_model(model_name, split_ports(model_ports))
xml_models[model_name] = xml_model
# Should not happen
else:
assert False, cond
# Serialize the pb_type XML
fname = os.path.join(args.xml_path, instance.lower() + ".pb_type.xml")
ET.ElementTree(xml_pb_root).write(fname, pretty_print=True)
print("total timings : {}".format(total_timings))
print(
"missing timings: {} {:.2f}%".format(
missing_timings, 100.0 * missing_timings / total_timings
)
)
# Write models XML
xml_model_root = ET.Element("models")
for key in sorted(list(xml_models.keys())):
xml_model_root.append(xml_models[key])
fname = os.path.join(args.xml_path, "ram.model.xml")
ET.ElementTree(xml_model_root).write(fname, pretty_print=True)
# Make blackboxes
blackboxes = {}
for cond in yield_ram_modes(ram_tree):
mode_name = make_mode_name(cond)
model_name = "RAM_" + mode_name + "_VPR"
# CONCAT_EN=0 - the 2x1 RAM is split into two
if "CONCAT_EN=0" in cond:
if "DIR=1" in cond:
ports = remap_clocks(ram_ports_sing, FIFO_CLOCK_MAP)
else:
ports = ram_ports_sing
if "FIFO_EN=0" in cond:
model_ports = filter_ports(ram_ports_sing, FIFO_PORTS)
else:
model_ports = filter_ports(ram_ports_sing, RAM_PORTS)
if "DIR=1" in cond:
model_ports = remap_clocks(model_ports, FIFO_CLOCK_MAP)
verilog = make_blackbox(
args.device, model_name, split_ports(ports),
split_ports(model_ports)
)
blackboxes[model_name] = verilog
# CONCAT_EN=1 - keep the 2x1 RAM as one
elif "CONCAT_EN=1" in cond:
if "DIR=1" in cond:
ports = remap_clocks(ram_ports_all, FIFO_CLOCK_MAP)
else:
ports = ram_ports_all
if "FIFO_EN=0" in cond:
model_ports = filter_ports(ram_ports_all, FIFO_PORTS)
else:
model_ports = filter_ports(ram_ports_all, RAM_PORTS)
if "DIR=1" in cond:
model_ports = remap_clocks(model_ports, FIFO_CLOCK_MAP)
verilog = make_blackbox(
args.device, model_name, split_ports(ports),
split_ports(model_ports)
)
blackboxes[model_name] = verilog
# Write blackbox definitions
fname = os.path.join(args.vlog_path, "ram_sim.v")
with open(fname, "w") as fp:
for k, v in blackboxes.items():
fp.write(v)
# Make techmap
techmap = make_techmap(port_blacklist, list(yield_ram_modes(ram_tree)))
fname = os.path.join(args.vlog_path, "ram_map.v")
with open(fname, "w") as fp:
fp.write(techmap)
if __name__ == "__main__":
main()