490 lines
18 KiB
Python
Executable File
490 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
from math import cos, radians, sin
|
|
from operator import itemgetter
|
|
from pathlib import Path
|
|
import csv
|
|
import re
|
|
|
|
COLUMNS = ("PAD", "Pin/Ball Function", "Bank", "Dual Function", "Differential", "High Speed", "DQS")
|
|
DEVICES = ("ecp5um25", "ecp5um45") # "ecp5um85" symbols ship with KiCad
|
|
EMPTY = ("",) * len(COLUMNS)
|
|
FOOTPRINTS = {
|
|
"CABGA381": ("Lattice*caBGA*17.0x17.0mm*Layout20x20*P0.8mm*", "Package_BGA:Lattice_caBGA-381_17.0x17.0mm_Layout20x20_P0.8mm_Ball0.4mm_Pad0.4mm_NSMD"),
|
|
"CABGA756": ("Lattice*caBGA*27.0x27.0mm*Layout32x32*P0.8mm*", "Package_BGA:Lattice_caBGA-756_27.0x27.0mm_Layout32x32_P0.8mm"),
|
|
}
|
|
PACKAGES = {
|
|
"CABGA381": "BG381",
|
|
"CABGA756": "BG756",
|
|
}
|
|
|
|
CONFIG_RE = re.compile(r"CCLK|CFG_[012]|DONE|INITN|PROGRAMN|RESERVED|TCK|TDI|TDO|TMS")
|
|
IO_RE = re.compile(r"P[BLRT]([1-9]\d{,2})[ABCD]|VCCIO\d")
|
|
POWER_RE = re.compile(r"GND|VCC(?!IO\d).*")
|
|
SERDES_RE = re.compile(r"HD[RT]X[NP]0_D[01]CH[01]|REFCLK[NP]_D[01]")
|
|
VCC_RE = re.compile(r"VCC[^\d]*")
|
|
|
|
def define_symbol(name, footprint_filter, description="", footprint=""):
|
|
return [
|
|
"symbol",
|
|
f"\"{name}\"",
|
|
["exclude_from_sim", "no"],
|
|
["in_bom", "yes"],
|
|
["on_board", "yes"],
|
|
[
|
|
"property",
|
|
"\"Reference\"",
|
|
"\"U\"",
|
|
["at", "22.86", "31.75", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["justify", "left"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Value\"",
|
|
f"\"{name}\"",
|
|
["at", "22.86", "29.21", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["justify", "left"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Footprint\"",
|
|
f"\"{footprint}\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Datasheet\"",
|
|
"\"https://www.latticesemi.com/view_document?document_id=50461\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Description\"",
|
|
f"\"{description}\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"ki_locked\"",
|
|
"\"\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"ki_keywords\"",
|
|
"\"FPGA programmable logic\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"ki_fp_filters\"",
|
|
f"\"{footprint_filter}\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
]
|
|
|
|
def define_alias(name, extends, footprint_filter, description="", footprint=""):
|
|
return [
|
|
"symbol",
|
|
f"\"{name}\"",
|
|
["extends", f"\"{extends}\""],
|
|
[
|
|
"property",
|
|
"\"Reference\"",
|
|
"\"U\"",
|
|
["at", "22.86", "31.75", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["justify", "left"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Value\"",
|
|
f"\"{name}\"",
|
|
["at", "22.86", "29.21", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["justify", "left"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Footprint\"",
|
|
f"\"{footprint}\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Datasheet\"",
|
|
"\"https://www.latticesemi.com/view_document?document_id=50461\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"Description\"",
|
|
f"\"{description}\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"ki_keywords\"",
|
|
"\"FPGA programmable logic\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
[
|
|
"property",
|
|
"\"ki_fp_filters\"",
|
|
f"\"{footprint_filter}\"",
|
|
["at", "0", "0", "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], ["hide", "yes"]],
|
|
],
|
|
]
|
|
|
|
def extract_index(t):
|
|
try:
|
|
return int(t[0].split()[-1])
|
|
except (IndexError, ValueError):
|
|
return -1
|
|
|
|
def rank_components(t):
|
|
description = t[0]
|
|
if description == "Power":
|
|
return 0
|
|
elif description.startswith("IO Bank "):
|
|
return 1
|
|
elif description == "Configuration":
|
|
return 2
|
|
elif description.startswith("Dual SERDES "):
|
|
return 3
|
|
return 4
|
|
|
|
def pad_index(f):
|
|
try:
|
|
index = IO_RE.fullmatch(f)[1]
|
|
return f.replace(index, index.rjust(3, "0"))
|
|
except TypeError:
|
|
return f
|
|
|
|
def dec(i):
|
|
s = str(i).rjust(3, "0")
|
|
return f"{s[:-2]}.{s[-2:]}"
|
|
|
|
def main():
|
|
library = [
|
|
"kicad_symbol_lib",
|
|
["version", "20231120"],
|
|
["generator", Path(__file__).name],
|
|
]
|
|
|
|
for device in DEVICES:
|
|
path = Path(__file__).parent / "pinouts" / f"{device}pinout.csv"
|
|
if not path.exists() or not path.is_file():
|
|
raise FileNotFoundError(f"File {path} not found")
|
|
|
|
packages = []
|
|
pins = {p: {} for p in PACKAGES}
|
|
with path.open() as f:
|
|
for row in csv.reader(f):
|
|
if len(packages) == 0:
|
|
if row[0].startswith("# ") or tuple(row[:len(COLUMNS)]) == EMPTY:
|
|
continue
|
|
if tuple(row[:len(COLUMNS)]) != COLUMNS:
|
|
raise ValueError("File deviates from expected format")
|
|
packages = row[len(COLUMNS):]
|
|
if len(packages) == 0:
|
|
raise ValueError("No packages defined")
|
|
continue
|
|
|
|
func = row[1]
|
|
bank = row[2]
|
|
ball = list(map(lambda s: s.strip("-"), row[len(COLUMNS):]))
|
|
|
|
if not any(ball):
|
|
continue
|
|
|
|
if CONFIG_RE.fullmatch(func):
|
|
for i, package in enumerate(packages):
|
|
if package not in PACKAGES:
|
|
continue
|
|
if ball[i] == "":
|
|
continue
|
|
|
|
component = "Configuration"
|
|
if component not in pins[package]:
|
|
pins[package][component] = []
|
|
pins[package][component].append((func, ball[i], {
|
|
"CFG_0": "input",
|
|
"CFG_1": "input",
|
|
"CFG_2": "input",
|
|
"CCLK": "bidirectional",
|
|
"DONE": "open_collector",
|
|
"INITN": "open_collector",
|
|
"PROGRAMN": "input",
|
|
"RESERVED": "no_connect",
|
|
"TCK": "input",
|
|
"TDI": "input",
|
|
"TDO": "output",
|
|
"TMS": "input",
|
|
}[func], "180" if func[0] in ("R", "T") else "0"))
|
|
elif IO_RE.fullmatch(func):
|
|
for i, package in enumerate(packages):
|
|
if package not in PACKAGES:
|
|
continue
|
|
if ball[i] == "":
|
|
continue
|
|
|
|
component = f"IO Bank {bank}"
|
|
if component not in pins[package]:
|
|
pins[package][component] = []
|
|
pins[package][component].append((func, ball[i], "power_in" if func.startswith("VCC") else "bidirectional", "270" if func.startswith("VCC") else "0"))
|
|
elif POWER_RE.fullmatch(func):
|
|
for i, package in enumerate(packages):
|
|
if package not in PACKAGES:
|
|
continue
|
|
if ball[i] == "":
|
|
continue
|
|
|
|
component = f"Power"
|
|
if component not in pins[package]:
|
|
pins[package][component] = []
|
|
pins[package][component].append((func, ball[i], "power_in", "90" if "GND" in func else "270"))
|
|
elif SERDES_RE.fullmatch(func):
|
|
for i, package in enumerate(packages):
|
|
if package not in PACKAGES:
|
|
continue
|
|
if ball[i] == "":
|
|
continue
|
|
|
|
component = f"Dual SERDES {bank[1]}"
|
|
if component not in pins[package]:
|
|
pins[package][component] = []
|
|
pins[package][component].append((func, ball[i], "input" if func.startswith("REFCLK") or "RX" in func else "output", "0"))
|
|
elif func == "NC":
|
|
pass
|
|
else:
|
|
raise RuntimeError(f"\"{func}\" did not match any category")
|
|
|
|
for package, components in pins.items():
|
|
if len(components) == 0:
|
|
continue
|
|
|
|
pin_count = {
|
|
"ecp5um25": "25F",
|
|
"ecp5um45": "45F",
|
|
"ecp5um85": "85F",
|
|
}[device]
|
|
name = f"LFE5UM5G-{pin_count}-8{PACKAGES[package]}x"
|
|
footprint_filter, footprint = FOOTPRINTS[package]
|
|
symbol = define_symbol(name, footprint_filter, footprint=footprint)
|
|
|
|
component_index = 1
|
|
for description, pins in sorted(sorted(components.items(), key=extract_index), key=rank_components):
|
|
if description == "Power":
|
|
height = 2032
|
|
|
|
vcc = set(map(itemgetter(0), pins))
|
|
vcc.remove("GND")
|
|
|
|
groups = {}
|
|
width = 0
|
|
for f in vcc:
|
|
group = VCC_RE.match(f)[0].removesuffix("RX").removesuffix("TX")
|
|
if group not in groups:
|
|
groups[group] = 0
|
|
width += 254
|
|
groups[group] += 1
|
|
width += 254
|
|
|
|
max_group = max(groups, key=lambda k: groups[k])
|
|
last_group = None
|
|
offset = 0
|
|
vcc_index = {}
|
|
for i, f in enumerate(sorted(sorted(vcc), key=lambda f: "" if (g := VCC_RE.match(f)[0].removesuffix("RX").removesuffix("TX")) == max_group else g)):
|
|
g = VCC_RE.match(f)[0].removesuffix("RX").removesuffix("TX")
|
|
if last_group and g != last_group:
|
|
offset += 1
|
|
last_group = g
|
|
|
|
vcc_index[f] = offset + i
|
|
|
|
width = max(width, (groups[max_group] * 254 + 508) * 2)
|
|
elif description == "Configuration":
|
|
height = 2794
|
|
width = 2032
|
|
elif description.startswith("Dual SERDES "):
|
|
height = 3810
|
|
width = 2032
|
|
else:
|
|
io = set(filter(lambda f: not f.startswith("VCCIO"), map(itemgetter(0), pins)))
|
|
io_index = {f: i for i, f in enumerate(sorted(io, key=pad_index))}
|
|
|
|
height = len(io) * 254 + 1270
|
|
width = 1016
|
|
|
|
component = [
|
|
"symbol",
|
|
f"\"{name}_{component_index}_1\"",
|
|
[
|
|
"rectangle",
|
|
["start", dec(-width // 2), dec(height // 2)], # left top
|
|
["end", dec(width // 2), dec(-height // 2)], # right bottom
|
|
["stroke", ["width", "0.3048"], ["type", "default"]],
|
|
["fill", ["type", "background"]],
|
|
],
|
|
[
|
|
"text",
|
|
f"\"{description}\"",
|
|
["at", dec(width // 2 - 127) if description == "Power" else "0", dec(-height // 2 + 127), "0"],
|
|
["effects", ["font", ["size", "1.27", "1.27"]], *([["justify", "right"]] if description == "Power" else [])],
|
|
],
|
|
]
|
|
|
|
positions = {}
|
|
for func, ball, type, orientation in pins:
|
|
attrs = []
|
|
if func in positions or type == "no_connect":
|
|
attrs.append("hide")
|
|
|
|
if func in positions:
|
|
type = "passive" if type != "no_connect" else type
|
|
x, y = positions[func]
|
|
else:
|
|
x, y = {
|
|
"CCLK": (-1524, 1016),
|
|
"CFG_0": (-1524, -508),
|
|
"CFG_1": (-1524, -762),
|
|
"CFG_2": (-1524, -1016),
|
|
"DONE": (-1524, 0),
|
|
"GND": (0, -1524),
|
|
"HDRXN0_D0CH0": (-1524, 762),
|
|
"HDRXN0_D0CH1": (-1524, -508),
|
|
"HDRXN0_D1CH0": (-1524, 762),
|
|
"HDRXN0_D1CH1": (-1524, -508),
|
|
"HDRXP0_D0CH0": (-1524, 1016),
|
|
"HDRXP0_D0CH1": (-1524, -254),
|
|
"HDRXP0_D1CH0": (-1524, 1016),
|
|
"HDRXP0_D1CH1": (-1524, -254),
|
|
"HDTXN0_D0CH0": (-1524, 1270),
|
|
"HDTXN0_D0CH1": (-1524, 0),
|
|
"HDTXN0_D1CH0": (-1524, 1270),
|
|
"HDTXN0_D1CH1": (-1524, 0),
|
|
"HDTXP0_D0CH0": (-1524, 1524),
|
|
"HDTXP0_D0CH1": (-1524, 254),
|
|
"HDTXP0_D1CH0": (-1524, 1524),
|
|
"HDTXP0_D1CH1": (-1524, 254),
|
|
"INITN": (-1524, 254),
|
|
"PROGRAMN": (-1524, 508),
|
|
"RESERVED": (1524, -254),
|
|
"REFCLKN_D0": (-1524, -1270),
|
|
"REFCLKN_D1": (-1524, -1270),
|
|
"REFCLKP_D0": (-1524, -1016),
|
|
"REFCLKP_D1": (-1524, -1016),
|
|
"TCK": (1524, 762),
|
|
"TDI": (1524, 508),
|
|
"TDO": (1524, 1016),
|
|
"TMS": (1524, 254),
|
|
}.get(func, (0, 0))
|
|
|
|
if (x, y) == (0, 0):
|
|
if func.startswith("VCCIO"):
|
|
x = 0
|
|
y = height // 2 + 508
|
|
elif "io_index" in locals() and func in io_index:
|
|
x = -width // 2 - 508
|
|
y = height // 2 - 1016 - io_index[func] * 254
|
|
else:
|
|
assert "vcc_index" in locals()
|
|
x = -width // 2 + 254 + vcc_index[func] * 254
|
|
y = height // 2 + 508
|
|
|
|
if type == "no_connect":
|
|
angle = radians(int(orientation))
|
|
dx = -round(cos(angle))
|
|
dy = -round(sin(angle))
|
|
x += dx * -508
|
|
y += dy * -508
|
|
|
|
positions[func] = (x, y)
|
|
|
|
pin = [
|
|
"pin",
|
|
type,
|
|
"line",
|
|
["at", dec(x), dec(y), orientation],
|
|
["length", "5.08"],
|
|
*attrs,
|
|
]
|
|
pin += [
|
|
[
|
|
"name",
|
|
f"\"{func}\"",
|
|
["effects", ["font", ["size", "1.27", "1.27"]]],
|
|
],
|
|
[
|
|
"number",
|
|
f"\"{ball}\"",
|
|
["effects", ["font", ["size", "1.27", "1.27"]]],
|
|
],
|
|
]
|
|
component.append(pin)
|
|
|
|
symbol.append(component)
|
|
component_index += 1
|
|
|
|
library.append(symbol)
|
|
|
|
for speed_grade in ("6", "7", "8"):
|
|
library.append(define_alias(f"LFE5UM-{pin_count}-{speed_grade}{PACKAGES[package]}x", name, footprint_filter, footprint=footprint))
|
|
|
|
with (Path(__file__).parent / "FPGA_Lattice_ECP5_SERDES.kicad_sym").open("w") as f:
|
|
stack = [library]
|
|
indent = [0]
|
|
multi_line = False
|
|
while len(stack) > 0:
|
|
if isinstance(stack[-1], list) and len(stack[-1]) > 0:
|
|
token = stack[-1]
|
|
multi_line = any(map(lambda e: isinstance(e, list), token))
|
|
|
|
while len(token) > 1:
|
|
stack.append(token.pop())
|
|
|
|
name = token.pop()
|
|
assert len(token) == 0
|
|
assert isinstance(name, str)
|
|
f.write(f"{' ' * indent[-1]}({name}")
|
|
|
|
if multi_line:
|
|
f.write("\n")
|
|
indent.append(indent[-1] + 2)
|
|
else:
|
|
indent.append(indent[-1])
|
|
elif isinstance(stack[-1], list):
|
|
assert len(indent) >= 2
|
|
multi_line = indent[-2] != indent[-1]
|
|
|
|
stack.pop()
|
|
indent.pop()
|
|
|
|
if multi_line:
|
|
f.write(f"{' ' * indent[-1]})\n")
|
|
else:
|
|
f.write(")\n")
|
|
else:
|
|
attr = stack.pop()
|
|
assert isinstance(attr, str)
|
|
|
|
assert len(indent) >= 2
|
|
|
|
if indent[-2] != indent[-1]:
|
|
f.write(f"{' ' * indent[-1]}{attr}\n")
|
|
else:
|
|
f.write(f" {attr}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|