lfe5um-symbols/pinouts/generate_symbols.py

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 / 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.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()