#!/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()