aboutsummaryrefslogtreecommitdiff
path: root/kicad/tools/gen_sch.py
diff options
context:
space:
mode:
authorFriedrich Beckmann <friedrich.beckmann@tha.de>2026-11-08 18:14:54 +0100
committerFriedrich Beckmann <friedrich.beckmann@tha.de>2026-11-08 18:14:54 +0100
commit5129a5a5448012a53cb9ddae1349c2a2c8571339 (patch)
tree41d60bbce7fe0de57cdd92b232682831759de1a8 /kicad/tools/gen_sch.py
parent9e4911662b81bad61183cca74b8c1905f84e60e4 (diff)
Add KiCad 9 schematic, project libraries and datasheetsHEADmaster
Schematic (kicad/), one sheet per function as on ULX3S: - power: TLV62569 bucks for +1V1 / +3V3, AP2112K LDO for +2V5 - usb: USB-C with PTC and TVS, CH347F for JTAG and UART - fpga_power, fpga_config, fpga_banks_01/23/67: ECP5 LFE5U-25F caBGA256 (pin compatible with the 45F), quad SPI flash, 50 MHz CLOCK_50, ball assignment as in PINPLAN.md - ethernet: RTL8211F gigabit PHY, HR911130A RJ45, shield via 1M || 1nF 2kV - sdram: 32 MB W9825G6KH-6 - hdmi: TMDS from the FPGA (LVCMOS33D), AC coupled, ESD protected - user_io: SW, KEY (debounced), LEDR, LEDG, HEX0-HEX3 with DE1 names - gpio: two DE1 compatible 2x20 headers (THDB-ADA on GPIO_0), Pmod A/B, ESD arrays - testpoints: bring-up pads, M3 mounting holes All parts have LCSC numbers, MPN and manufacturer for JLCPCB assembly; footprints are cross-checked against the EasyEDA data. Libraries: ECP5 symbols generated from the Lattice pinout CSVs (kipart), CH347F, RTL8211F, SM410561N symbols, HR911130A symbol and footprint from KiCad library merge requests, generator scripts in lib/tools/. Datasheets of all parts except resistors and capacitors in datasheets/ (manufacturer copyright, not covered by the project licenses).
Diffstat (limited to 'kicad/tools/gen_sch.py')
-rwxr-xr-xkicad/tools/gen_sch.py1721
1 files changed, 1721 insertions, 0 deletions
diff --git a/kicad/tools/gen_sch.py b/kicad/tools/gen_sch.py
new file mode 100755
index 0000000..084a764
--- /dev/null
+++ b/kicad/tools/gen_sch.py
@@ -0,0 +1,1721 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-3.0-or-later
+"""Generator of the first version of the eboard KiCad 9 schematic (historical record).
+
+Writes all sheets (eboard.kicad_sch and the sub-sheets) and the sheet list of
+eboard.kicad_pro, with stable UUIDs, from the code below:
+
+ python3 kicad/tools/gen_sch.py [KICAD_PROJECT_DIR] # default: kicad/
+
+WARNING: the schematic is now edited in KiCad. Running this script again
+OVERWRITES all sheets and discards every change made in KiCad since then.
+It is kept to document how the schematic was created (part choices, LCSC and
+MPN table, ball assignment of the FPGA banks), not for regular use.
+
+Needs the KiCad 9 symbol libraries (KICAD9_SYMBOL_DIR, default
+/usr/share/kicad/symbols) and the project libraries in lib/.
+"""
+import json
+import os
+import re
+import sys
+import uuid
+from collections import Counter
+
+OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+PROJECT = "eboard"
+SYMDIR = os.environ.get("KICAD9_SYMBOL_DIR", "/usr/share/kicad/symbols")
+PROJLIBS = {"eboard": os.path.join(OUT, "lib/eboard.kicad_sym"),
+ "eboard_fpga": os.path.join(OUT, "lib/eboard_fpga.kicad_sym")}
+NS = uuid.UUID("6b1c2a52-2f0e-4d8e-9c55-3a0e5b0e0b01")
+DATE = "2026-11-08"
+
+
+def uid(*parts):
+ return str(uuid.uuid5(NS, "/".join(map(str, parts))))
+
+
+ROOT_UUID = uid("root")
+
+
+def r(v):
+ return f"{round(v, 4):g}"
+
+
+def esc(s):
+ return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
+
+
+# ---------------------------------------------------------------- s-expr helpers
+def match_paren(t, i):
+ """Index after the parenthesis group starting at t[i] == '('."""
+ depth, j, n = 0, i, len(t)
+ while j < n:
+ c = t[j]
+ if c == '"':
+ j += 1
+ while t[j] != '"':
+ j += 2 if t[j] == "\\" else 1
+ elif c == "(":
+ depth += 1
+ elif c == ")":
+ depth -= 1
+ if depth == 0:
+ return j + 1
+ j += 1
+ raise ValueError("unbalanced")
+
+
+def children(block):
+ """Top-level child groups of an s-expression group."""
+ out, i = [], block.index("(", 1)
+ while True:
+ k = block.find("(", i)
+ if k < 0 or k >= len(block) - 1:
+ return out
+ e = match_paren(block, k)
+ out.append(block[k:e])
+ i = e
+
+
+_libcache = {}
+
+
+def lib_text(lib):
+ if lib not in _libcache:
+ path = PROJLIBS.get(lib, f"{SYMDIR}/{lib}.kicad_sym")
+ _libcache[lib] = open(path).read()
+ return _libcache[lib]
+
+
+def lib_block(lib, name):
+ t = lib_text(lib)
+ m = re.search(r'\(symbol "' + re.escape(name) + r'"\s', t)
+ assert m, (lib, name)
+ return t[m.start():match_paren(t, m.start())]
+
+
+def resolved(lib, name):
+ """(block, parent-or-same block) for lib:name."""
+ b = lib_block(lib, name)
+ m = re.search(r'\(extends "([^"]+)"\)', b)
+ return b, (lib_block(lib, m.group(1)) if m else b), (m.group(1) if m else name)
+
+
+def flat_symbol(lib, name):
+ b, parent, pname = resolved(lib, name)
+ props = [c for c in children(b) if c.startswith("(property ")]
+ rest = [c for c in children(parent)
+ if not c.startswith("(property ") and not c.startswith("(extends ")]
+ rest = [c.replace(f'(symbol "{pname}_', f'(symbol "{name}_') for c in rest]
+ return f'(symbol "{lib}:{name}"\n' + "\n".join(props + rest) + "\n)"
+
+
+def lib_pins(lib, name, unit=None):
+ _, b, pname = resolved(lib, name)
+ if unit is not None:
+ # only the pins of this unit (sub-symbols NAME_<unit>_x) and common pins (NAME_0_x)
+ b = "".join(c for c in children(b)
+ if re.match(r'\(symbol "' + re.escape(pname) + rf'_({unit}|0)_\d+"', c))
+ pins = {}
+ for p in re.finditer(r'\(pin \w+ \w+\s*\(at ([-\d.]+) ([-\d.]+) \d+\).*?'
+ r'\(name "([^"]*)".*?\(number "([^"]*)"', b, re.S):
+ pins[p.group(4)] = (float(p.group(1)), float(p.group(2)), p.group(3))
+ return pins
+
+
+def lib_props(lib, name):
+ b = lib_block(lib, name)
+ return dict(re.findall(r'\(property "([^"]+)" "((?:[^"\\]|\\.)*)"', b))
+
+
+# Manufacturer part numbers of all LCSC parts (from the JLCPCB parts list, 2026-11-08)
+PARTS = {
+ "C11702": ("0402WGF1001TCE", "UNI-ROYAL"),
+ "C135262": ("SMNR4020-2.2UH", "SXN"),
+ "C141368": ("SM410561N", "ARKLED"),
+ "C141836": ("TLV62569DBVR", "Texas Instruments"),
+ "C14709": ("BLM18PG121SN1D", "Murata Electronics"),
+ "C1525": ("CL05B104KO5NNNC", "Samsung Electro-Mechanics"),
+ "C1532": ("0402B223K500NT", "FH"),
+ "C1550762": ("LFE5U-25F-7BG256I", "Lattice"),
+ "C15850": ("CL21A106KAYNNNE", "Samsung Electro-Mechanics"),
+ "C165948": ("TYPE-C-31-M-12", "HRO"),
+ "C176945": ("AP2112K-2.5TRG1", "Diodes Incorporated"),
+ "C1779": ("CL21A475KAQNNNE", "Samsung Electro-Mechanics"),
+ "C18221627": ("CH347F", "WCH"),
+ "C187932": ("RTL8211F-CG", "Realtek Semicon"),
+ "C193402": ("SMF5.0A", "MDD"),
+ "C1972959": ("TPD2E2U06DRLR", "Texas Instruments"),
+ "C2286": ("KT-0603R", "Hubei KENTO Elec"),
+ "C22935": ("0603WAF1004T5E", "UNI-ROYAL"),
+ "C238127": ("5011", "Keystone"),
+ "C25092": ("0402WGF220JTCE", "UNI-ROYAL"),
+ "C25104": ("0402WGF3300TCE", "UNI-ROYAL"),
+ "C25105": ("0402WGF330JTCE", "UNI-ROYAL"),
+ "C25117": ("0402WGF4700TCE", "UNI-ROYAL"),
+ "C25123": ("0402WGF5100TCE", "UNI-ROYAL"),
+ "C25744": ("0402WGF1002TCE", "UNI-ROYAL"),
+ "C25792": ("0402WGF4702TCE", "UNI-ROYAL"),
+ "C25884": ("0402WGF2491TCE", "UNI-ROYAL"),
+ "C25900": ("0402WGF4701TCE", "UNI-ROYAL"),
+ "C25905": ("0402WGF5101TCE", "UNI-ROYAL"),
+ "C269102": ("SMD0805B020TF", "Brightking"),
+ "C269119": ("SMD1206B110TFT", "Brightking"),
+ "C2827646": ("TPD4E05U06DQAR", "TECH PUBLIC"),
+ "C2857673": ("SS-12D07-5", "ROCPU Switches"),
+ "C2901592": ("SX3M50.000M20F30TNN", "Shenzhen SCTF Elec"),
+ "C318884": ("TS-1187A-B-A-B", "XKB Connection"),
+ "C394957": ("CMH201209B2R2MT", "FH"),
+ "C428493": ("10029449-001RLF", "Amphenol ICC"),
+ "C45783": ("CL21A226MAQNNNE", "Samsung Electro-Mechanics"),
+ "C5200276": ("EH254V-12-40P", "XFCN"),
+ "C5333437": ("A2541HWR-2x6P", "CJT"),
+ "C54408": ("HR911130A", "HANRUN"),
+ "C5605": ("74HC14D,653", "Nexperia"),
+ "C57131": ("1C208000BC0R", "KDS Daishinku"),
+ "C62246": ("W9825G6KH-6", "Winbond Elec"),
+ "C8545": ("2N7002", "CJ (Jiangsu Changjing)"),
+ "C9006": ("X322525MOB4SI", "YXC Crystal Oscillators"),
+ "C9196": ("1206B102K202NT", "FH"),
+ "C965805": ("XL-1608SYGC-06", "XINGLIGHT"),
+ "C97521": ("W25Q128JVSIQ", "Winbond Elec"),
+ "C1546": ("0402CG101J500NT", "FH"),
+ "C52923": ("CL05A105KA5NQNC", "Samsung Electro-Mechanics"),
+ "C1549": ("0402CG180J500NT", "FH"),
+ "C25756": ("0402WGF1502TCE", "UNI-ROYAL"),
+ "C25752": ("0402WGF1202TCE", "UNI-ROYAL"),
+ "C25890": ("0402WGF3301TCE", "UNI-ROYAL"),
+}
+# Basic parts for passives that are placed without an LCSC number: (symbol, value, footprint)
+DEFAULT_LCSC = {
+ ("C", "100pF", "C_0402_1005Metric"): "C1546",
+ ("C", "1uF", "C_0402_1005Metric"): "C52923",
+ ("C", "18pF", "C_0402_1005Metric"): "C1549",
+ ("C", "100nF", "C_0402_1005Metric"): "C1525",
+ ("C", "22uF", "C_0805_2012Metric"): "C45783",
+ ("R", "15k", "R_0402_1005Metric"): "C25756",
+ ("R", "10k", "R_0402_1005Metric"): "C25744",
+ ("R", "12k", "R_0402_1005Metric"): "C25752",
+ ("R", "3.3k", "R_0402_1005Metric"): "C25890",
+ ("R", "4.7k", "R_0402_1005Metric"): "C25900",
+ ("R", "1k", "R_0402_1005Metric"): "C11702",
+}
+
+
+def part_fields(lib, name, value, fp, fields):
+ """Add LCSC (passive defaults), MPN and Manufacturer to the instance fields."""
+ f = dict(fields or {})
+ if "LCSC" not in f and lib == "Device" and name in ("R", "C") and fp:
+ key = (name, value, fp.split(":")[-1])
+ if key in DEFAULT_LCSC:
+ f["LCSC"] = DEFAULT_LCSC[key]
+ if f.get("LCSC") in PARTS:
+ mpn, mfr = PARTS[f["LCSC"]]
+ f["MPN"], f["Manufacturer"] = mpn, mfr
+ return f
+
+
+# ---------------------------------------------------------------- sheet model
+class Sheet:
+ def __init__(self, name, file_uuid, path, page, paper="A4"):
+ self.name, self.file_uuid, self.path, self.page = name, file_uuid, path, page
+ self.paper = paper
+ self.symbols, self.wires, self.texts, self.libs = [], [], [], {}
+ self.labels, self.ncs = [], []
+ self.points = Counter() # connection points for junction detection
+
+ def place(self, lib, name, ref, value, x, y, rot=0, fp=None, fields=None,
+ ref_at=None, val_at=None, hide_val=False, centered=False, dnp=False, unit=None,
+ mirror=False, datasheet=None, description=None, in_bom=True):
+ self.libs[f"{lib}:{name}"] = (lib, name)
+ props = lib_props(lib, name)
+ fields = part_fields(lib, name, value, fp if fp is not None else props.get("Footprint", ""), fields)
+ pins = lib_pins(lib, name, unit)
+ abs_pins = {}
+ for num, (px, py, pname) in pins.items():
+ if mirror: # mirror about the y axis (only used with rot 0)
+ px = -px
+ dx, dy = {0: (px, -py), 90: (-py, -px), 180: (-px, py), 270: (py, px)}[rot]
+ pt = (round(x + dx, 2), round(y + dy, 2))
+ abs_pins[num] = pt
+ abs_pins.setdefault(pname, pt)
+ for pt in set(abs_pins.values()): # stacked pins count once
+ self.points[pt] += 1
+ power = ref.startswith("#")
+ ref_at = ref_at or (x + 2.54, y - 1.27)
+ val_at = val_at or (x + 2.54, y + 1.27)
+ just = None if centered else "left"
+ f = [("Reference", ref, ref_at, power, just),
+ ("Value", value, val_at, hide_val, just),
+ ("Footprint", fp if fp is not None else props.get("Footprint", ""), (x, y), True, None),
+ ("Datasheet", datasheet or props.get("Datasheet", ""), (x, y), True, None),
+ ("Description", description or props.get("Description", ""), (x, y), True, None)]
+ for k, v in (fields or {}).items():
+ f.append((k, v, (x, y), True, None))
+ # further library fields (e.g. "Source") are copied as hidden fields
+ std = {"Reference", "Value", "Footprint", "Datasheet", "Description"} | set(fields or {})
+ for k, v in props.items():
+ if k not in std and not k.startswith("ki_"):
+ f.append((k, v, (x, y), True, None))
+ self.symbols.append(dict(lib_id=f"{lib}:{name}", ref=ref, x=x, y=y, rot=rot,
+ fields=f, fangle=rot if rot in (90, 270) else 0,
+ pins=sorted(set(pins)), dnp=dnp, unit=unit or 1, mirror=mirror, in_bom=in_bom,
+ uuid=uid(self.name, "sym", ref, unit, x, y)))
+ return abs_pins
+
+ def wire(self, *pts):
+ for a, b in zip(pts, pts[1:]):
+ a = (round(a[0], 2), round(a[1], 2))
+ b = (round(b[0], 2), round(b[1], 2))
+ self.wires.append((a, b))
+ self.points[a] += 1
+ self.points[b] += 1
+
+ def label(self, text, x, y, angle=0, glob=False, shape="bidirectional"):
+ self.labels.append((text, round(x, 2), round(y, 2), angle, glob, shape))
+ self.points[(round(x, 2), round(y, 2))] += 1
+
+ def pin_label(self, pin, text, angle=0, length=5.08, glob=True, shape="bidirectional"):
+ """Short wire from a pin, ending in a (global) label."""
+ x, y = pin
+ dx, dy = {0: (length, 0), 180: (-length, 0), 90: (0, -length), 270: (0, length)}[angle]
+ self.wire(pin, (x + dx, y + dy))
+ self.label(text, x + dx, y + dy, angle, glob, shape)
+
+ def nc(self, pin):
+ self.ncs.append(pin)
+
+ def text(self, s, x, y, size=1.27, bold=False):
+ self.texts.append((s, x, y, size, bold))
+
+ # -------------------------------------------------------------- output
+ def render(self, title, extra=""):
+ o = ["(kicad_sch\n\t(version 20250114)\n\t(generator \"eeschema\")\n"
+ "\t(generator_version \"9.0\")\n",
+ f'\t(uuid "{self.file_uuid}")\n\t(paper "{self.paper}")\n',
+ "\t(title_block\n"
+ f'\t\t(title "{title}")\n\t\t(date "{DATE}")\n'
+ '\t\t(rev "0.1")\n'
+ '\t\t(comment 1 "CERN-OHL-S-2.0")\n\t)\n',
+ "\t(lib_symbols\n"]
+ for key in sorted(self.libs):
+ o.append(flat_symbol(*self.libs[key]) + "\n")
+ o.append("\t)\n")
+ for (x, y), n in sorted(self.points.items()):
+ if n >= 3:
+ o.append(f"\t(junction\n\t\t(at {r(x)} {r(y)})\n\t\t(diameter 0)\n"
+ f"\t\t(color 0 0 0 0)\n\t\t(uuid \"{uid(self.name, 'j', x, y)}\")\n\t)\n")
+ for (x, y) in self.ncs:
+ o.append(f"\t(no_connect\n\t\t(at {r(x)} {r(y)})\n\t\t(uuid \"{uid(self.name, 'nc', x, y)}\")\n\t)\n")
+ for (a, b) in self.wires:
+ o.append(f"\t(wire\n\t\t(pts\n\t\t\t(xy {r(a[0])} {r(a[1])}) (xy {r(b[0])} {r(b[1])})\n\t\t)\n"
+ "\t\t(stroke\n\t\t\t(width 0)\n\t\t\t(type default)\n\t\t)\n"
+ f"\t\t(uuid \"{uid(self.name, 'w', a, b)}\")\n\t)\n")
+ for (s, x, y, size, bold) in self.texts:
+ o.append(f'\t(text "{esc(s)}"\n\t\t(exclude_from_sim no)\n\t\t(at {r(x)} {r(y)} 0)\n'
+ f"\t\t(effects\n\t\t\t(font\n\t\t\t\t(size {size} {size})\n"
+ + ("\t\t\t\t(bold yes)\n" if bold else "")
+ + "\t\t\t)\n\t\t\t(justify left top)\n\t\t)\n"
+ f"\t\t(uuid \"{uid(self.name, 't', s, x, y)}\")\n\t)\n")
+ for (s, x, y, a, glob, shape) in self.labels:
+ if glob:
+ just = "right" if a in (180, 270) else "left"
+ o.append(f'\t(global_label "{esc(s)}"\n\t\t(shape {shape})\n\t\t(at {r(x)} {r(y)} {a})\n'
+ "\t\t(fields_autoplaced yes)\n"
+ f"\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1.27 1.27)\n\t\t\t)\n\t\t\t(justify {just})\n\t\t)\n"
+ f"\t\t(uuid \"{uid(self.name, 'gl', s, x, y)}\")\n"
+ f"\t\t(property \"Intersheetrefs\" \"${{INTERSHEET_REFS}}\"\n\t\t\t(at {r(x)} {r(y)} {a})\n"
+ "\t\t\t(effects\n\t\t\t\t(font\n\t\t\t\t\t(size 1.27 1.27)\n\t\t\t\t)\n"
+ f"\t\t\t\t(justify {just})\n\t\t\t\t(hide yes)\n\t\t\t)\n\t\t)\n\t)\n")
+ else:
+ just = "right bottom" if a in (180, 270) else "left bottom"
+ o.append(f'\t(label "{esc(s)}"\n\t\t(at {r(x)} {r(y)} {a})\n'
+ "\t\t(fields_autoplaced yes)\n"
+ f"\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1.27 1.27)\n\t\t\t)\n\t\t\t(justify {just})\n\t\t)\n"
+ f"\t\t(uuid \"{uid(self.name, 'l', s, x, y)}\")\n\t)\n")
+ o.append(extra)
+ for s in self.symbols:
+ o.append(f"\t(symbol\n\t\t(lib_id \"{s['lib_id']}\")\n"
+ f"\t\t(at {r(s['x'])} {r(s['y'])} {s['rot']})\n"
+ + ("\t\t(mirror y)\n" if s["mirror"] else "")
+ + f"\t\t(unit {s['unit']})\n"
+ "\t\t(exclude_from_sim no)\n"
+ f"\t\t(in_bom {'yes' if s['in_bom'] else 'no'})\n\t\t(on_board yes)\n"
+ f"\t\t(dnp {'yes' if s['dnp'] else 'no'})\n\t\t(uuid \"{s['uuid']}\")\n")
+ for (k, v, (fx, fy), hide, just) in s["fields"]:
+ o.append(f"\t\t(property \"{k}\" \"{v}\"\n\t\t\t(at {r(fx)} {r(fy)} {s['fangle']})\n"
+ "\t\t\t(effects\n\t\t\t\t(font\n\t\t\t\t\t(size 1.27 1.27)\n\t\t\t\t)\n"
+ + (f"\t\t\t\t(justify {just})\n" if just else "")
+ + ("\t\t\t\t(hide yes)\n" if hide else "")
+ + "\t\t\t)\n\t\t)\n")
+ for p in s["pins"]:
+ o.append(f"\t\t(pin \"{p}\"\n\t\t\t(uuid \"{uid(s['uuid'], 'pin', p)}\")\n\t\t)\n")
+ o.append(f"\t\t(instances\n\t\t\t(project \"{PROJECT}\"\n"
+ f"\t\t\t\t(path \"{self.path}\"\n\t\t\t\t\t(reference \"{s['ref']}\")\n"
+ f"\t\t\t\t\t(unit {s['unit']})\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t)\n")
+ if self.page == "1":
+ o.append("\t(sheet_instances\n\t\t(path \"/\"\n\t\t\t(page \"1\")\n\t\t)\n\t)\n")
+ o.append("\t(embedded_fonts no)\n)\n")
+ return "".join(o)
+
+
+# ---------------------------------------------------------------- helpers
+C0805 = "Capacitor_SMD:C_0805_2012Metric"
+C0402 = "Capacitor_SMD:C_0402_1005Metric"
+R0402 = "Resistor_SMD:R_0402_1005Metric"
+LED0603 = "LED_SMD:LED_0603_1608Metric"
+
+cnt = Counter()
+
+
+def ref(prefix):
+ cnt[prefix] += 1
+ return f"{prefix}{cnt[prefix]}"
+
+
+TP_PAD = "TestPoint:TestPoint_Pad_D1.5mm"
+TP_LOOP = "TestPoint:TestPoint_Keystone_5010-5014_Multipurpose"
+
+
+def tp_pad(sh, net, x, y, kind="label", glob=False):
+ """Top-layer test pad (no part, not in the BOM) at pin (x, y).
+ kind: "rail" = power symbol above (pad hangs down), "gnd" = GND below,
+ "label" = (global) label below."""
+ if kind == "rail":
+ sh.place("Connector", "TestPoint", ref("TP"), net, x, y, rot=180, fp=TP_PAD, in_bom=False,
+ ref_at=(x, y + 5.08), val_at=(x, y + 7.62), centered=True)
+ pwr(sh, net, x, y)
+ return
+ sh.place("Connector", "TestPoint", ref("TP"), net, x, y, fp=TP_PAD, in_bom=False,
+ ref_at=(x + 1.27, y - 5.08), val_at=(x + 1.27, y - 2.54))
+ if kind == "gnd":
+ sh.wire((x, y), (x, y + 2.54))
+ pwr(sh, "GND", x, y + 2.54)
+ else:
+ sh.wire((x, y), (x, y + 5.08))
+ sh.label(net, x, y + 5.08, angle=270, glob=glob,
+ shape="bidirectional" if not glob else fpga_shape_tp(net))
+
+
+def fpga_shape_tp(net):
+ return "input" if net in ("CLOCK_50", "UART_RXD") else "output"
+
+
+def pwr(sh, name, x, y):
+ cnt["#PWR"] += 1
+ n = cnt["#PWR"]
+ if name == "GND":
+ return sh.place("power", name, f"#PWR{n:02d}", name, x, y,
+ ref_at=(x, y + 6.35), val_at=(x - 1.905, y + 4.445))
+ if name == "PWR_FLAG":
+ return sh.place("power", name, f"#FLG{n:02d}", name, x, y,
+ ref_at=(x, y - 1.905), val_at=(x - 2.54, y - 5.08))
+ return sh.place("power", name, f"#PWR{n:02d}", name, x, y,
+ ref_at=(x, y + 3.81), val_at=(x - 1.905, y - 4.445))
+
+
+def cap(sh, value, x, ytop, fp=C0805, fields=None):
+ """Vertical capacitor with pin 1 at (x, ytop)."""
+ return sh.place("Device", "C", ref("C"), value, x, ytop + 3.81, fp=fp, fields=fields,
+ ref_at=(x + 2.54, ytop + 2.54), val_at=(x + 2.54, ytop + 5.08))
+
+
+def res(sh, value, x, ytop, fields=None):
+ """Vertical resistor with pin 1 at (x, ytop)."""
+ return sh.place("Device", "R", ref("R"), value, x, ytop + 3.81, fp=R0402, fields=fields,
+ ref_at=(x + 2.54, ytop + 2.54), val_at=(x + 2.54, ytop + 5.08))
+
+
+# ---------------------------------------------------------------- power sheet
+POWER_SHEET_UUID = uid("sheet", "power")
+pw = Sheet("Power", uid("file", "power"), f"/{ROOT_UUID}/{POWER_SHEET_UUID}", "2")
+
+
+def buck(x0, y0, rail, ra, rb, vout_text, gnd_flag=False):
+ """TLV62569 buck converter as in ULX3S power.sch. U at (x0, y0)."""
+ yr = y0 - 2.54 # VIN / VOUT rail height
+ u = pw.place("Regulator_Switching", "TLV62569DBV", ref("U"), "TLV62569DBV",
+ x0, y0, ref_at=(x0 - 5.08, y0 - 6.35), val_at=(x0 + 1.27, y0 - 6.35),
+ fields={"LCSC": "C141836"})
+ xin = x0 - 25.4
+ pwr(pw, "+5V", xin, yr)
+ # 4.7 uF (TI recommendation): keeps the capacitance on VBUS <= 10 uF (USB inrush limit)
+ cap(pw, "4.7uF", x0 - 17.78, yr, fields={"LCSC": "C1779"})
+ pwr(pw, "GND", x0 - 17.78, yr + 7.62)
+ pw.wire((xin, yr), (x0 - 17.78, yr), (x0 - 10.16, yr), u["VIN"])
+ pw.wire(u["EN"], (x0 - 10.16, y0), (x0 - 10.16, yr))
+ pwr(pw, "GND", *u["GND"])
+ if gnd_flag:
+ f = (xin - 7.62, y0 + 7.62)
+ pwr(pw, "PWR_FLAG", *f)
+ pw.wire(f, (f[0], f[1] + 5.08))
+ pwr(pw, "GND", f[0], f[1] + 5.08)
+ xl = x0 + 19.05
+ lp = pw.place("Device", "L", ref("L"), "2.2uH", xl, yr, rot=90,
+ fp="Inductor_SMD:L_Sunlord_SWPA4020S",
+ ref_at=(xl, yr - 5.08), val_at=(xl, yr - 2.54), centered=True,
+ fields={"LCSC": "C135262", "MPN": "SXN SMNR4020-2.2UH"})
+ pw.wire(u["SW"], lp["1"])
+ xo = lp["2"][0]
+ xd = xo + 7.62
+ res(pw, ra, xd, yr)
+ res(pw, rb, xd, yr + 7.62)
+ pwr(pw, "GND", xd, yr + 15.24)
+ xff = xd + 10.16
+ cap(pw, "100pF", xff, yr, fp=C0402)
+ yfb = yr + 7.62
+ pw.wire(u["FB"], (x0 + 10.16, y0), (x0 + 10.16, yfb), (xd, yfb))
+ pw.wire((xd, yfb), (xff, yfb))
+ xc1, xc2 = xff + 12.7, xff + 22.86
+ cap(pw, "22uF", xc1, yr)
+ cap(pw, "22uF", xc2, yr)
+ pwr(pw, "GND", xc1, yr + 7.62)
+ pwr(pw, "GND", xc2, yr + 7.62)
+ xflag, xend = xc2 + 10.16, xc2 + 20.32
+ pw.wire(lp["2"], (xd, yr), (xff, yr), (xc1, yr), (xc2, yr), (xflag, yr), (xend, yr))
+ pwr(pw, "PWR_FLAG", xflag, yr)
+ pwr(pw, rail, xend, yr)
+ pw.text(vout_text, xend + 7.62, yr - 2.54)
+
+
+def ldo(x0, y0):
+ """VCCAUX 2.5 V: AP2112K-2.5 LDO fed from +3V3 (ICCAUX 17 mA typ. for LFE5U-45F)."""
+ yr = y0 - 2.54
+ u = pw.place("Regulator_Linear", "AP2112K-2.5", ref("U"), "AP2112K-2.5",
+ x0, y0, ref_at=(x0 - 5.08, y0 - 6.35), val_at=(x0 + 1.27, y0 - 6.35),
+ fields={"LCSC": "C176945"})
+ xin = x0 - 25.4
+ pwr(pw, "+3V3", xin, yr)
+ cap(pw, "1uF", x0 - 17.78, yr, fp=C0402)
+ pwr(pw, "GND", x0 - 17.78, yr + 7.62)
+ pw.wire((xin, yr), (x0 - 17.78, yr), (x0 - 10.16, yr), u["VIN"])
+ pw.wire(u["EN"], (x0 - 10.16, y0), (x0 - 10.16, yr))
+ pwr(pw, "GND", *u["GND"])
+ pw.nc(u["NC"])
+ xo = u["VOUT"][0]
+ xc, xend = xo + 12.7, xo + 35.56
+ cap(pw, "1uF", xc, yr, fp=C0402)
+ pwr(pw, "GND", xc, yr + 7.62)
+ pw.wire(u["VOUT"], (xc, yr), (xend, yr))
+ pwr(pw, "+2V5", xend, yr)
+ pw.text("VCCAUX 2.5 V (2.375-2.625 V)\nLDO from +3V3, ICCAUX 16 / 17 mA typ. (LFE5U-25F / 45F)\n"
+ "Ferrite + 10 uF + 100 nF per pin at the FPGA (TN-02038)", xend + 7.62, yr - 2.54)
+
+
+bx = 63.5
+buck(bx, 50.8, "+1V1", "10k", "12k",
+ "VCC FPGA core 1.1 V\nVout = 0.6 V * (1 + 10k/12k) = 1.10 V", gnd_flag=True)
+ldo(bx, 91.44)
+buck(bx, 132.08, "+3V3", "15k", "3.3k",
+ "VCCIO, SDRAM, Ethernet PHY, CH347 3.3 V\nVout = 0.6 V * (1 + 15k/3.3k) = 3.33 V")
+
+pw.text("Power supply", 25.4, 20.32, size=2.54, bold=True)
+pw.text("+5V comes from the USB-C connector on the USB sheet.\n"
+ "+1V1 and +3V3: buck converters TLV62569DBV (2 A, SOT-23-5) as on ULX3S. FB = 0.6 V.\n"
+ "Feedforward C2/C8 100 pF across the upper divider resistor (TI: optional, 6.8 pF with 200k):\n"
+ "zero at 159 kHz (+1V1, 10k) and 106 kHz (+3V3, 15k).\n"
+ "+2V5: LDO AP2112K-2.5 from +3V3, so VCCIO (3.3 V) comes up before VCCAUX.\n"
+ "L1, L2: 2.2 uH SXN SMNR4020-2.2UH, 4x4 mm, Isat 3.4 A (> 3 A switch current limit), 46 mOhm.\n"
+ "ECP5 power-up: no sequencing required, ramp rate 0.01-10 V/ms (DS1044).\n"
+ "USB inrush: only C1 + C7 = 9.4 uF sit directly on VBUS (USB limit 10 uF). The buck outputs\n"
+ "charge through the 800 us internal soft start. Keep extra capacitance off +5V.",
+ 25.4, 160.02)
+
+# ---------------------------------------------------------------- USB sheet
+USB_SHEET_UUID = uid("sheet", "usb")
+us = Sheet("USB", uid("file", "usb"), f"/{ROOT_UUID}/{USB_SHEET_UUID}", "3")
+
+# USB-C receptacle (USB 2.0, 16 pin)
+j = us.place("Connector", "USB_C_Receptacle_USB2.0_16P", ref("J"), "USB_C", 40.64, 88.9,
+ fp="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12",
+ ref_at=(30.48, 63.5), val_at=(30.48, 66.04), fields={"LCSC": "C165948"})
+pwr(us, "GND", *j["A1"])
+pwr(us, "GND", *j["S1"])
+# VBUS -> PTC fuse -> +5V
+vb = j["A4"]
+f1 = us.place("Device", "Polyfuse", ref("F"), "1.1A", 68.58, vb[1], rot=90,
+ fp="Fuse:Fuse_1206_3216Metric", ref_at=(68.58, vb[1] - 5.08),
+ val_at=(68.58, vb[1] - 2.54), centered=True,
+ fields={"LCSC": "C269119", "MPN": "SMD1206B110TFT"})
+xm = round(round((vb[0] + f1["1"][0]) / 2 / 1.27) * 1.27, 2)
+us.wire(vb, (xm, vb[1]), f1["1"])
+us.place("Connector", "TestPoint", ref("TP"), "VBUS", xm, vb[1], fp=TP_PAD, in_bom=False,
+ ref_at=(xm - 6.35, vb[1] - 5.08), val_at=(xm - 6.35, vb[1] - 2.54))
+us.wire(f1["2"], (83.82, vb[1]), (93.98, vb[1]))
+pwr(us, "PWR_FLAG", 83.82, vb[1])
+pwr(us, "+5V", 93.98, vb[1])
+# TVS behind the PTC: clamps ESD on VBUS; a permanent overvoltage trips F1
+xt = 99.06
+us.wire((93.98, vb[1]), (xt, vb[1]))
+tv = us.place("Device", "D_Zener", ref("D"), "SMF5.0A", xt, vb[1] + 3.81, rot=270,
+ fp="Diode_SMD:D_SOD-123F", ref_at=(xt + 6.35, vb[1] + 2.54), val_at=(xt + 8.89, vb[1] + 5.08),
+ centered=True,
+ fields={"LCSC": "C193402"}, description="TVS diode, unidirectional, 5 V stand-off, 9.2 V clamping, 200 W")
+pwr(us, "GND", *tv["2"])
+# CC pull-downs 5.1k (one per CC pin, sink / UFP)
+cc1, cc2 = j["A5"], j["B5"]
+us.wire(cc1, (86.36, cc1[1]))
+res(us, "5.1k", 86.36, cc1[1], fields={"LCSC": "C25905"})
+pwr(us, "GND", 86.36, cc1[1] + 7.62)
+us.wire(cc2, (78.74, cc2[1]))
+res(us, "5.1k", 78.74, cc2[1], fields={"LCSC": "C25905"})
+pwr(us, "GND", 78.74, cc2[1] + 7.62)
+# D- (A7, B7) and D+ (A6, B6)
+for a, b, name in (("A7", "B7", "USB_D-"), ("A6", "B6", "USB_D+")):
+ pa, pb = j[a], j[b]
+ us.wire(pb, (58.42, pb[1]), (58.42, pa[1]))
+ us.wire(pa, (58.42, pa[1]), (63.5, pa[1]))
+ us.label(name, 63.5, pa[1])
+us.nc(j["A8"])
+us.nc(j["B8"])
+
+# ESD protection, < 2 pF as required by WCH
+e = us.place("Power_Protection", "TPD2E2U06DRL", ref("U"), "TPD2E2U06DRL", 45.72, 137.16,
+ ref_at=(36.83, 127.0), val_at=(36.83, 129.54), fields={"LCSC": "C1972959"})
+us.pin_label(e["3"], "USB_D+", angle=180, glob=False)
+us.pin_label(e["5"], "USB_D-", angle=180, glob=False)
+pwr(us, "GND", *e["4"])
+us.nc(e["1"])
+us.nc(e["2"])
+
+# CH347F
+X, Y = 160.02, 101.6
+u = us.place("eboard", "CH347F", ref("U"), "CH347F", X, Y,
+ ref_at=(X + 7.62, Y - 33.02), val_at=(X + 7.62, Y - 30.48),
+ fields={"LCSC": "C18221627"})
+# supply: VCC and VIO on +3V3, 0.1 uF each
+yr = 60.96
+us.wire(u["21"], (u["21"][0], yr))
+us.wire(u["6"], (u["6"][0], yr))
+us.wire((129.54, yr), (137.16, yr), (147.32, yr), (u["21"][0], yr), (u["6"][0], yr))
+pwr(us, "+3V3", 129.54, yr)
+cap(us, "100nF", 137.16, yr, fp=C0402)
+cap(us, "100nF", 147.32, yr, fp=C0402)
+pwr(us, "GND", 137.16, yr + 7.62)
+pwr(us, "GND", 147.32, yr + 7.62)
+# ground (pin 20 and exposed pad = datasheet pin 0 = footprint pad 29)
+us.wire(u["29"], u["20"])
+pwr(us, "GND", *u["20"])
+# USB data
+us.pin_label(u["28"], "USB_D+", angle=180, length=7.62, glob=False)
+us.pin_label(u["27"], "USB_D-", angle=180, length=7.62, glob=False)
+# 8 MHz crystal (C57131, CL = 12 pF) with 2 x 18 pF: C = 2 * (CL - ~3 pF stray)
+xc, yc = 99.06, 121.92
+y1 = us.place("Device", "Crystal_GND24", ref("Y"), "8MHz", xc, yc,
+ fp="Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm",
+ ref_at=(xc, yc - 5.08), val_at=(xc, yc - 2.54), centered=True,
+ fields={"LCSC": "C57131", "MPN": "KDS 1C208000BC0R"})
+pwr(us, "GND", *y1["2"])
+xi, xo = y1["1"], y1["3"]
+xci, xco = xi[0] - 5.08, xo[0] + 5.08
+us.wire(xi, (xci, yc))
+us.wire(xo, (xco, yc))
+cap(us, "18pF", xci, yc, fp=C0402)
+cap(us, "18pF", xco, yc, fp=C0402)
+pwr(us, "GND", xci, yc + 7.62)
+pwr(us, "GND", xco, yc + 7.62)
+us.wire((xci, yc), (xci, u["1"][1]), u["1"])
+us.wire((xco, yc), (xco, u["2"][1]), u["2"])
+# reset: 4.7k pull-up to VIO, 100 nF to GND (optional parts in WCH reference)
+rst = u["3"]
+xr, xrc = 124.46, 81.28
+us.wire(rst, (xr, rst[1]), (xrc, rst[1]))
+res(us, "4.7k", xr, rst[1] - 7.62)
+pwr(us, "+3V3", xr, rst[1] - 7.62)
+cap(us, "100nF", xrc, rst[1], fp=C0402)
+pwr(us, "GND", xrc, rst[1] + 7.62)
+# ACT LED: on when USB is configured (ACT is active low)
+act = u["8"]
+yl = 137.16
+us.wire(act, (129.54, act[1]), (129.54, yl))
+led = us.place("Device", "LED", ref("D"), "green", 125.73, yl, rot=180, fp=LED0603,
+ ref_at=(125.73, yl + 5.08), val_at=(125.73, yl + 2.54), centered=True,
+ fields={"LCSC": "C965805"})
+rl = us.place("Device", "R", ref("R"), "1k", 118.11, yl, rot=90, fp=R0402,
+ ref_at=(118.11, yl + 5.08), val_at=(118.11, yl + 2.54), centered=True)
+pwr(us, "+3V3", *rl["1"])
+
+# signals to the FPGA (global labels, named like the LPF ports later)
+# DE1 convention: UART_TXD = FPGA transmits (-> CH347 RXD), UART_RXD = FPGA receives.
+# Only UART0 TXD/RXD go to the FPGA (pin budget, see kicad/PINPLAN.md).
+SIG = [("19", "UART_RXD", "output"), ("22", "UART_TXD", "input"),
+ ("23", "JTAG_TCK", "output"), ("26", "JTAG_TMS", "output"),
+ ("25", "JTAG_TDI", "output"), ("24", "JTAG_TDO", "input")]
+for num, name, shape in SIG:
+ us.pin_label(u[num], name, shape=shape)
+for num in ("17", "11", "12", "7", "9", "14", "13", "16", "15", # unused: CTS, I2C, SCS1, TRST, SPI
+ "18", "10", "4", "5"): # RTS0, DTR0, UART1
+ us.nc(u[num])
+
+# JTAG pull resistors, ECP5 hardware checklist TN-02038: 4.7k to VCCIO8 (= +3V3)
+x = 208.28
+for name, up in (("JTAG_TDI", True), ("JTAG_TMS", True), ("JTAG_TDO", True), ("JTAG_TCK", False)):
+ res(us, "4.7k", x, 147.32)
+ if up:
+ pwr(us, "+3V3", x, 147.32)
+ us.wire((x, 154.94), (x, 160.02))
+ us.label(name, x, 160.02, angle=270, glob=True)
+ else:
+ us.wire((x, 147.32), (x, 142.24))
+ us.label(name, x, 142.24, angle=90, glob=True)
+ pwr(us, "GND", x, 154.94)
+ x += 10.16
+
+# JTAG header for an external programmer (not fitted by default)
+h = us.place("Connector_Generic", "Conn_01x06", ref("J"), "JTAG", 264.16, 101.6,
+ fp="Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical",
+ ref_at=(261.62, 91.44), val_at=(261.62, 93.98), dnp=True)
+xh = 251.46
+us.wire(h["1"], (xh, h["1"][1]))
+pwr(us, "+3V3", xh, h["1"][1])
+for num, name in (("2", "JTAG_TCK"), ("3", "JTAG_TDI"), ("4", "JTAG_TDO"), ("5", "JTAG_TMS")):
+ us.pin_label(h[num], name, angle=180, length=7.62)
+us.wire(h["6"], (xh, h["6"][1]))
+pwr(us, "GND", xh, h["6"][1])
+
+us.text("USB-C, CH347F USB to JTAG / UART", 25.4, 20.32, size=2.54, bold=True)
+us.text("USB device (sink): CC1 and CC2 each 5.1k to GND -> 5 V from any USB-C host.\n"
+ "F1: PTC 1.1 A hold / 2.2 A trip protects host and board against shorts.\n"
+ "TVS SMF5.0A behind F1 clamps ESD on VBUS; a permanent overvoltage makes it conduct and trips F1.\n"
+ "No series resistors on D+/D- (built into CH347). ESD part < 2 pF (WCH).\n"
+ "CH347F works without mode straps; used here: UART0 (TXD/RXD) + JTAG. UART1, modem lines,\n"
+ "SPI and I2C are not connected (FPGA pin budget, see PINPLAN.md).\n"
+ "VIO = 3.3 V = VCCIO of the FPGA banks the signals go to (JTAG: VCCIO8).\n"
+ "Names as on the DE1, seen from the FPGA: UART_TXD = FPGA transmits, UART_RXD = FPGA receives.",
+ 25.4, 160.02)
+us.text("JTAG pull resistors\n(ECP5 TN-02038)", 208.28, 132.08)
+us.text("not fitted (DNP)", 254.0, 116.84)
+
+
+# ---------------------------------------------------------------- FPGA power sheet
+FPWR_SHEET_UUID = uid("sheet", "fpga_power")
+fpw = Sheet("FPGA power", uid("file", "fpga_power"), f"/{ROOT_UUID}/{FPWR_SHEET_UUID}", "4")
+FPGA = ("eboard_fpga", "LFE5U-25F-xBG256x")
+FPGA_REF = ref("U")
+C10U = {"LCSC": "C15850"}
+C100N = {"LCSC": "C1525"}
+
+
+def cap_row(sh, rail_y, xs, values):
+ """Capacitors hanging from a horizontal rail at rail_y, GND below."""
+ for x, v in zip(xs, values):
+ if v == "10uF":
+ cap(sh, v, x, rail_y, fp=C0805, fields=C10U)
+ elif v == "22uF":
+ cap(sh, v, x, rail_y, fp=C0805, fields={"LCSC": "C45783"})
+ else:
+ cap(sh, v, x, rail_y, fp=C0402, fields=C100N)
+ pwr(sh, "GND", x, rail_y + 7.62)
+
+
+# FPGA power unit: VCC (6 balls), VCCAUX (2 balls), GND (27 balls), stacked pins
+X, Y = 147.32, 104.14
+u = fpw.place(*FPGA, FPGA_REF, "LFE5U-25F-7BG256I", X, Y, unit=9,
+ ref_at=(X + 5.08, Y - 2.54), val_at=(X + 5.08, Y),
+ fields={"LCSC": "C1550762"})
+pwr(fpw, "GND", *u["GND"])
+
+# VCC 1.1 V: 3 x 10 uF + 100 nF per pin (6), TN-02038 Table 3.1
+yv = 63.5
+xs = [33.02 + 12.7 * i for i in range(9)]
+fpw.wire(u["VCC"], (u["VCC"][0], yv))
+fpw.wire((25.4, yv), *[(x, yv) for x in xs], (u["VCC"][0], yv))
+pwr(fpw, "+1V1", 25.4, yv)
+cap_row(fpw, yv, xs, ["10uF"] * 3 + ["100nF"] * 6)
+fpw.text("VCC 1.1 V core: 3 x 10 uF + 100 nF per VCC ball (G6 G7 G9 L8 L9 L10)", 33.02, yv - 8.89)
+
+# VCCAUX 2.5 V: 120 ohm ferrite + 10 uF + 100 nF per pin (2)
+ya = 76.2
+fpw.wire(u["VCCAUX"], (u["VCCAUX"][0], ya))
+xa = [162.56, 175.26, 187.96]
+fb = fpw.place("Device", "FerriteBead", ref("FB"), "120R@100MHz", 207.01, ya, rot=90,
+ fp="Inductor_SMD:L_0603_1608Metric",
+ ref_at=(207.01, ya - 5.08), val_at=(207.01, ya - 2.54), centered=True,
+ fields={"LCSC": "C14709", "MPN": "BLM18PG121SN1D"})
+fpw.wire((u["VCCAUX"][0], ya), (152.4, ya), *[(x, ya) for x in xa], (195.58, ya), fb["1"])
+fpw.label("FPGA_VCCAUX", 152.4, ya)
+pwr(fpw, "PWR_FLAG", 195.58, ya)
+fpw.wire(fb["2"], (218.44, ya))
+pwr(fpw, "+2V5", 218.44, ya)
+cap_row(fpw, ya, xa, ["10uF", "100nF", "100nF"])
+fpw.text("VCCAUX 2.5 V: ferrite + 10 uF\n+ 100 nF per VCCAUX ball (G11 L7)", 162.56, ya - 11.43)
+
+# VCCIO decoupling, all banks at 3.3 V. The VCCIO balls are on the bank units.
+def vccio_row(yio, banks):
+ x = 38.1
+ xs, vals = [], []
+ for bank, balls, n in banks:
+ fpw.text(f"Bank{bank}\n{balls}", x - 1.27, yio - 8.89)
+ for v in ["10uF"] + ["100nF"] * n:
+ xs.append(x)
+ vals.append(v)
+ x += 12.7
+ fpw.wire((25.4, yio), *[(xx, yio) for xx in xs])
+ pwr(fpw, "+3V3", 25.4, yio)
+ cap_row(fpw, yio, xs, vals)
+
+
+vccio_row(130.81, [("0", "F6 F7", 2), ("1", "F10 F11", 2), ("2", "H11 J11", 2), ("3", "K11 L11", 2)])
+vccio_row(163.83, [("6", "J6 J7", 2), ("7", "H6 H7", 2), ("8", "L6", 1)])
+
+fpw.text("FPGA power", 25.4, 20.32, size=2.54, bold=True)
+fpw.text("Decoupling per Lattice FPGA-TN-02038 (ECP5 hardware checklist) Table 3.1.\n"
+ "Place the 100 nF capacitors next to their balls, on the bottom side under the BGA.\n"
+ "VCCIO: 10 uF + 100 nF per ball for every bank; all banks run at 3.3 V\n"
+ "(VCCIO8 = 3.3 V also sets the JTAG and configuration levels).\n"
+ "POR needs VCC >= 0.9 V, VCCAUX >= 2.0 V, VCCIO8 >= 0.95 V.",
+ 25.4, 27.94)
+fpw.text("U6: LFE5U-25F-7BG256I (24K LUTs, JLCPCB C1550762).\n"
+ "Drop-in: LFE5U-45F-xBG256x (44K LUTs), same balls, banks and differential pairs;\n"
+ "swap with 'Change symbol'. The 45F has 2 extra PLLs (upper corners): their inputs\n"
+ "B1 B2 A2 B3 B14 A15 B15 B16 are plain I/O on the 25F -> no clocks on these balls.",
+ 152.4, 27.94)
+fpw.text("VCCIO decoupling (+3V3), 10 uF + 100 nF per ball", 25.4, 114.3)
+
+
+# ---------------------------------------------------------------- FPGA configuration sheet
+FCFG_SHEET_UUID = uid("sheet", "fpga_config")
+cf = Sheet("FPGA config", uid("file", "fpga_config"), f"/{ROOT_UUID}/{FCFG_SHEET_UUID}", "5")
+R4K7 = {"LCSC": "C25900"}
+R10K = {"LCSC": "C25744"}
+R1K = {"LCSC": "C11702"}
+R33 = {"LCSC": "C25105"}
+LED_GREEN = {"LCSC": "C965805"}
+
+
+def hres(sh, value, x, y, fields=None):
+ """Horizontal resistor centred at (x, y); returns pins."""
+ return sh.place("Device", "R", ref("R"), value, x, y, rot=90, fp=R0402, fields=fields,
+ ref_at=(x, y - 5.08), val_at=(x, y - 2.54), centered=True)
+
+
+# Config unit: dedicated configuration and JTAG pins
+X, Y = 76.2, 63.5
+c = cf.place(*FPGA, FPGA_REF, "LFE5U-25F-7BG256I", X, Y, unit=8,
+ ref_at=(X - 17.78, Y - 12.7), val_at=(X - 17.78, Y - 15.24),
+ fields={"LCSC": "C1550762"})
+for num, name in (("M10", "JTAG_TDO"), ("T10", "JTAG_TCK"), ("R11", "JTAG_TDI"), ("T11", "JTAG_TMS")):
+ cf.pin_label(c[num], name, shape="input" if name != "JTAG_TDO" else "output")
+for num, name in (("T9", "FPGA_INITN"), ("N9", "FPGA_MCLK"), ("R9", "FPGA_PROGRAMN"),
+ ("P9", "FPGA_DONE"), ("P10", "FPGA_CFG1")):
+ cf.pin_label(c[num], name, angle=180, length=10.16, glob=False)
+# CFG[2:0] = 010: master SPI (MSPI) boot from the flash
+cf.wire(c["R10"], (45.72, c["R10"][1]))
+cf.wire(c["N10"], (45.72, c["N10"][1]))
+cf.wire((45.72, c["R10"][1]), (45.72, c["N10"][1]), (45.72, c["N10"][1] + 5.08))
+pwr(cf, "GND", 45.72, c["N10"][1] + 5.08)
+
+# pull resistors to VCCIO8 = +3V3 (TN-02038 Table 6.2)
+pulls = [("FPGA_PROGRAMN", "4.7k", R4K7), ("FPGA_INITN", "4.7k", R4K7), ("FPGA_DONE", "4.7k", R4K7),
+ ("FPGA_CFG1", "4.7k", R4K7), ("FLASH_CS", "4.7k", R4K7), ("FLASH_CLK", "1k", R1K),
+ ("FLASH_IO2", "10k", R10K), ("FLASH_IO3", "10k", R10K)]
+for i, (name, val, f) in enumerate(pulls):
+ x = 129.54 + 12.7 * i
+ res(cf, val, x, 68.58, fields=f)
+ pwr(cf, "+3V3", x, 68.58)
+ cf.wire((x, 76.2), (x, 81.28))
+ cf.label(name, x, 81.28, angle=270)
+cf.text("Pull-ups to VCCIO8 (TN-02038 Table 6.2).\nFLASH_CS and FLASH_CLK pull-ups next to the flash.",
+ 129.54, 55.88)
+
+# MCLK series resistor next to the FPGA (22-80 ohm)
+rm = hres(cf, "33", 162.56, 45.72, fields=R33)
+cf.wire(rm["1"], (rm["1"][0] - 5.08, 45.72))
+cf.label("FPGA_MCLK", rm["1"][0] - 5.08, 45.72, angle=180)
+cf.wire(rm["2"], (rm["2"][0] + 5.08, 45.72))
+cf.label("FLASH_CLK", rm["2"][0] + 5.08, 45.72)
+cf.text("MCLK series R close to the FPGA ball N9", 147.32, 35.56)
+
+# Bank 8: shared configuration pins of the SPI flash (quad SPI capable)
+X, Y = 88.9, 116.84
+b8 = cf.place(*FPGA, FPGA_REF, "LFE5U-25F-7BG256I", X, Y, unit=7,
+ ref_at=(X + 5.08, Y - 22.86), val_at=(X + 5.08, Y - 20.32))
+pwr(cf, "+3V3", *b8["L6"])
+for num, name, ang in (("T8", "FLASH_IO0", 0), ("N8", "FLASH_CS", 0),
+ ("T7", "FLASH_IO1", 180), ("M7", "FLASH_IO2", 180), ("N7", "FLASH_IO3", 180)):
+ cf.pin_label(b8[num], name, angle=ang, length=7.62, glob=False)
+B8_FREE = {"T6": ("LEDG0", 180), "R7": ("LEDG1", 0), "R6": ("LEDG2", 180), "P7": ("LEDG3", 0),
+ "R8": ("LEDG4", 180), "P8": ("LEDG5", 0), "M8": ("HEX3_5", 180), "M9": ("HEX3_6", 0)}
+for num, (name, _) in B8_FREE.items():
+ side = 180 if b8[num][0] < X else 0
+ cf.pin_label(b8[num], name, angle=side, length=7.62 if side == 180 else 25.4, shape="output")
+cf.text("Bank 8 (VCCIO8 = 3.3 V): SPI flash; the other bank 8 I/O drive LEDG[5:0]\n"
+ "and HEX3[6:5] (they may toggle during configuration).",
+ 45.72, 139.7)
+
+# SPI flash, 16 MB: bitstream (25F ~0.7 MB, 45F ~1.2 MB) + firmware / data
+X, Y = 215.9, 116.84
+fl = cf.place("Memory_Flash", "W25Q128JVS", ref("U"), "W25Q128JVSIQ", X, Y,
+ ref_at=(X + 2.54, Y - 15.24), val_at=(X + 2.54, Y + 15.24),
+ fields={"LCSC": "C97521"})
+for num, name in (("1", "FLASH_CS"), ("6", "FLASH_CLK"), ("5", "FLASH_IO0"), ("2", "FLASH_IO1"),
+ ("3", "FLASH_IO2"), ("7", "FLASH_IO3")):
+ cf.pin_label(fl[num], name, angle=180, length=7.62, glob=False)
+P = (X, Y - 17.78)
+cf.wire(fl["8"], P, (X + 7.62, P[1]))
+pwr(cf, "+3V3", *P)
+cap(cf, "100nF", X + 7.62, P[1], fp=C0402, fields=C100N)
+pwr(cf, "GND", X + 7.62, P[1] + 7.62)
+pwr(cf, "GND", *fl["4"])
+
+# PROGRAM button: reloads the bitstream from the flash
+sw = cf.place("Switch", "SW_Push", ref("SW"), "TS-1187A", 40.64, 160.02, rot=90,
+ fp="Button_Switch_SMD:SW_Push_1P1T_XKB_TS-1187A",
+ ref_at=(46.99, 158.75), val_at=(53.34, 161.29), fields={"LCSC": "C318884", "MPN": "XKB TS-1187A-B-A-B"})
+top, bot = sorted((sw["1"], sw["2"]), key=lambda p: p[1])
+cf.wire(top, (top[0], top[1] - 5.08))
+cf.label("FPGA_PROGRAMN", top[0], top[1] - 5.08, angle=90)
+pwr(cf, "GND", *bot)
+cf.text("PROG button", 45.72, 163.83)
+
+# DONE LED: on when the FPGA is configured (DONE is open drain, high = done)
+qx, qy = 88.9, 172.72
+q = cf.place("Transistor_FET", "2N7002", ref("Q"), "2N7002", qx, qy,
+ ref_at=(qx + 5.08, qy - 1.27), val_at=(qx + 5.08, qy + 1.27), fields={"LCSC": "C8545"})
+cf.wire(q["G"], (qx - 10.16, qy))
+cf.label("FPGA_DONE", qx - 10.16, qy, angle=180)
+pwr(cf, "GND", *q["S"])
+xd = q["D"][0]
+cf.place("Device", "LED", ref("D"), "green", xd, q["D"][1] - 3.81, rot=90, fp=LED0603,
+ ref_at=(xd - 10.16, q["D"][1] - 5.08), val_at=(xd - 10.16, q["D"][1] - 2.54), fields=LED_GREEN)
+res(cf, "1k", xd, q["D"][1] - 15.24, fields=R1K)
+pwr(cf, "+3V3", xd, q["D"][1] - 15.24)
+cf.text("DONE LED", qx - 25.4, 154.94)
+
+# 50 MHz oscillator -> PCLK input (planned: M1 = PCLKT6_0, bank 6)
+ox, oy = 139.7, 160.02
+xo = cf.place("Oscillator", "ASE-xxxMHz", ref("X"), "SX3M50.000M20F30TNN", ox, oy,
+ fp="Oscillator:Oscillator_SMD_Abracon_ASE-4Pin_3.2x2.5mm",
+ ref_at=(ox + 3.81, oy - 8.89), val_at=(ox + 3.81, oy + 8.89),
+ fields={"LCSC": "C2901592", "MPN": "SX3M50.000M20F30TNN"})
+P = (ox, oy - 10.16)
+cf.wire(xo["4"], P, (ox - 10.16, P[1]), (ox - 17.78, P[1]))
+cf.wire((ox - 10.16, P[1]), (ox - 10.16, oy), xo["1"])
+cf.wire(P, (ox, P[1] - 2.54))
+pwr(cf, "+3V3", ox, P[1] - 2.54)
+cap(cf, "100nF", ox - 17.78, P[1], fp=C0402, fields=C100N)
+pwr(cf, "GND", ox - 17.78, P[1] + 7.62)
+pwr(cf, "GND", *xo["2"])
+ro = hres(cf, "33", ox + 12.7, oy, fields=R33)
+cf.wire(xo["3"], ro["1"])
+cf.pin_label(ro["2"], "CLOCK_50", length=7.62, shape="output")
+cf.text("50 MHz system clock (as on the DE1). Goes to a PCLK pin, not to\n"
+ "B1 B2 A2 B3 B14 A15 B15 B16 (PLL inputs only on the 45F).", 137.16, 135.89)
+
+cf.text("FPGA configuration", 25.4, 20.32, size=2.54, bold=True)
+for i, net in enumerate(("FPGA_PROGRAMN", "FPGA_INITN", "FPGA_DONE")):
+ tp_pad(cf, net, 205.74 + 15.24 * i, 40.64)
+cf.text("Test pads (bring-up)", 200.66, 27.94)
+cf.text("Boot: master SPI (MSPI), CFG[2:0] = 010, quad SPI capable; JTAG always available (CH347).\n"
+ "PROG button reloads the flash, DONE LED lights when the FPGA is configured.",
+ 25.4, 27.94)
+
+
+# ---------------------------------------------------------------- Ethernet sheet
+ETH_SHEET_UUID = uid("sheet", "ethernet")
+et = Sheet("Ethernet", uid("file", "ethernet"), f"/{ROOT_UUID}/{ETH_SHEET_UUID}", "6", paper="A3")
+R22 = {"LCSC": "C25092"}
+R510 = {"LCSC": "C25123"}
+R2K49 = {"LCSC": "C25884"}
+C22U = {"LCSC": "C45783"}
+
+
+def vres_pull(sh, value, x, ytop, net, up, fields=None, glob=False):
+ """Pull resistor: up = +3V3 on top, label below; else label on top, GND below."""
+ res(sh, value, x, ytop, fields=fields)
+ if up:
+ pwr(sh, "+3V3", x, ytop)
+ sh.wire((x, ytop + 7.62), (x, ytop + 12.7))
+ sh.label(net, x, ytop + 12.7, angle=270, glob=glob)
+ else:
+ sh.wire((x, ytop), (x, ytop - 5.08))
+ sh.label(net, x, ytop - 5.08, angle=90, glob=glob)
+ pwr(sh, "GND", x, ytop + 7.62)
+
+
+# --- PHY
+PX, PY = 190.5, 144.78
+ph = et.place("eboard", "RTL8211F-CG", ref("U"), "RTL8211F-CG", PX, PY,
+ ref_at=(PX + 25.4, PY - 36.83), val_at=(PX + 25.4, PY - 34.29),
+ fields={"LCSC": "C187932"})
+LEFT = [("20", "PHY_TXC"), ("19", "PHY_TXCTL"), ("18", "PHY_TXD0"), ("17", "PHY_TXD1"),
+ ("16", "PHY_TXD2"), ("15", "PHY_TXD3"), ("27", "PHY_RXC"), ("26", "PHY_RXCTL"),
+ ("25", "PHY_RXD0"), ("24", "PHY_RXD1"), ("23", "PHY_RXD2"), ("22", "PHY_RXD3")]
+for num, name in LEFT:
+ et.pin_label(ph[num], name, angle=180, length=10.16, glob=False)
+et.pin_label(ph["13"], "ENET_MDC", angle=180, length=10.16, shape="input")
+et.pin_label(ph["14"], "ENET_MDIO", angle=180, length=10.16)
+et.pin_label(ph["31"], "ENET_INT_N", angle=180, length=10.16, shape="output")
+et.pin_label(ph["12"], "ENET_RST_N", angle=180, length=10.16, shape="input")
+et.nc(ph["35"]) # CLKOUT unused
+RIGHT = [("1", "MDI0_P"), ("2", "MDI0_N"), ("4", "MDI1_P"), ("5", "MDI1_N"), ("6", "MDI2_P"),
+ ("7", "MDI2_N"), ("9", "MDI3_P"), ("10", "MDI3_N"), ("32", "PHY_LED0"),
+ ("33", "PHY_LED1"), ("34", "PHY_LED2"), ("36", "PHY_XTAL_IN"), ("37", "PHY_XTAL_OUT"),
+ ("39", "PHY_RSET"), ("30", "PHY_REG_OUT")]
+for num, name in RIGHT:
+ et.pin_label(ph[num], name, angle=0, length=10.16, glob=False)
+pwr(et, "GND", *ph["41"])
+# supply pins on top: DVDD33 + DVDD_RG -> +3V3, AVDD33 -> ETH_AVDD33, DVDD10 + AVDD10 -> ETH_1V0
+d33, drg, a33, d10, a10 = ph["29"], ph["28"], ph["11"], ph["21"], ph["3"]
+yj = d33[1] - 7.62
+et.wire(d33, (d33[0], yj))
+et.wire(drg, (drg[0], yj))
+et.wire((d33[0] - 10.16, yj), (d33[0], yj), (drg[0], yj))
+pwr(et, "+3V3", d33[0] - 10.16, yj)
+et.wire(a33, (a33[0], yj - 7.62))
+et.label("ETH_AVDD33", a33[0], yj - 7.62, angle=90)
+et.wire(d10, (d10[0], yj))
+et.wire(a10, (a10[0], yj))
+et.wire((d10[0], yj), (a10[0], yj), (a10[0] + 10.16, yj))
+et.label("ETH_1V0", a10[0] + 10.16, yj)
+
+# --- RGMII series resistors (22 ohm): TX close to the FPGA, RX close to the PHY
+RG = [("ENET_GTX_CLK", "PHY_TXC", "input"), ("ENET_TX_EN", "PHY_TXCTL", "input"),
+ ("ENET_TX_DATA0", "PHY_TXD0", "input"), ("ENET_TX_DATA1", "PHY_TXD1", "input"),
+ ("ENET_TX_DATA2", "PHY_TXD2", "input"), ("ENET_TX_DATA3", "PHY_TXD3", "input"),
+ ("ENET_RX_CLK", "PHY_RXC", "output"), ("ENET_RX_DV", "PHY_RXCTL", "output"),
+ ("ENET_RX_DATA0", "PHY_RXD0", "output"), ("ENET_RX_DATA1", "PHY_RXD1", "output"),
+ ("ENET_RX_DATA2", "PHY_RXD2", "output"), ("ENET_RX_DATA3", "PHY_RXD3", "output")]
+for i, (glob, loc, shape) in enumerate(RG):
+ y = 55.88 + 7.62 * i + (5.08 if i >= 6 else 0)
+ rr = hres(et, "22", 66.04, y, fields=R22)
+ et.pin_label(rr["1"], glob, angle=180, length=5.08, shape=shape)
+ et.pin_label(rr["2"], loc, angle=0, length=5.08, glob=False)
+et.text("RGMII series resistors 22R:\nTX (ENET_GTX_CLK ... ENET_TX_DATA3) close to the FPGA,\n"
+ "RX (ENET_RX_CLK ... ENET_RX_DATA3) close to the PHY.", 30.48, 38.1)
+
+# --- strap resistors 4.7k (sampled at power-up / when ENET_RST_N is released)
+STRAPS = [("PHY_RXD3", True, "PHYAD0 = 1"), ("PHY_RXC", False, "PHYAD1 = 0"),
+ ("PHY_RXCTL", False, "PHYAD2 = 0"), ("PHY_RXD2", False, "PLLOFF = 0"),
+ ("PHY_RXD1", True, "TXDLY = 1"), ("PHY_RXD0", True, "RXDLY = 1"),
+ ("PHY_LED0", True, "CFG_EXT = 1"), ("PHY_LED1", False, "CFG_LDO0 = 0"),
+ ("PHY_LED2", False, "CFG_LDO1 = 0")]
+for i, (net, up, txt) in enumerate(STRAPS):
+ x = 35.56 + 15.24 * i
+ vres_pull(et, "4.7k", x, 205.74, net, up, fields=R4K7)
+ et.text(txt, x - 5.08, 231.14 if up else 228.6)
+et.text("Strap resistors (RTL8211F datasheet 7.8): PHY address 1, 2 ns TXC and RXC delay inside the PHY,\n"
+ "RGMII I/O at 3.3 V from DVDD_RG (CFG_EXT = 1, CFG_LDO = 00), LED1/LED2 active high.",
+ 30.48, 182.88)
+
+# --- management and reset
+vres_pull(et, "4.7k", 177.8, 205.74, "ENET_MDIO", True, fields=R4K7, glob=True)
+vres_pull(et, "4.7k", 193.04, 205.74, "ENET_INT_N", True, fields=R4K7, glob=True)
+vres_pull(et, "10k", 208.28, 205.74, "ENET_RST_N", False, fields=R10K, glob=True)
+et.text("ENET_RST_N: 10k pull-down keeps the PHY in reset\nuntil the FPGA drives it high (>= 10 ms low,\n"
+ "wait 50 ms before MDIO access).", 172.72, 233.68)
+
+# --- 3.3 V and AVDD33 decoupling
+y3 = 50.8
+xs = [241.3, 251.46, 261.62]
+et.wire((233.68, y3), *[(x, y3) for x in xs])
+pwr(et, "+3V3", 233.68, y3)
+cap_row(et, y3, xs, ["10uF", "100nF", "100nF"])
+et.text("DVDD33, DVDD_RG", 241.3, y3 - 8.89)
+fb = et.place("Device", "FerriteBead", ref("FB"), "120R@100MHz", 280.67, y3, rot=90,
+ fp="Inductor_SMD:L_0603_1608Metric",
+ ref_at=(280.67, y3 - 5.08), val_at=(280.67, y3 - 2.54), centered=True,
+ fields={"LCSC": "C14709", "MPN": "BLM18PG121SN1D"})
+et.wire((261.62, y3), (271.78, y3), fb["1"])
+xa = [294.64, 304.8, 314.96]
+et.wire(fb["2"], *[(x, y3) for x in xa], (322.58, y3), (332.74, y3))
+et.label("ETH_AVDD33", 332.74, y3)
+pwr(et, "PWR_FLAG", 322.58, y3)
+cap_row(et, y3, xa, ["10uF", "100nF", "100nF"])
+et.text("AVDD33 (pins 11, 40)", 294.64, y3 - 8.89)
+
+# --- 1.0 V from the internal switching regulator (REG_OUT -> 2.2 uH -> DVDD10/AVDD10)
+y1 = 81.28
+lr = et.place("Device", "L", ref("L"), "2.2uH", 248.92, y1, rot=90,
+ fp="Inductor_SMD:L_0805_2012Metric",
+ ref_at=(248.92, y1 - 5.08), val_at=(248.92, y1 - 2.54), centered=True,
+ fields={"LCSC": "C394957", "MPN": "CMH201209B2R2MT"})
+et.wire((236.22, y1), lr["1"])
+et.label("PHY_REG_OUT", 236.22, y1, angle=180)
+x1 = [264.16, 274.32, 284.48, 294.64, 304.8]
+et.wire(lr["2"], *[(x, y1) for x in x1], (312.42, y1), (322.58, y1))
+et.label("ETH_1V0", 322.58, y1)
+tp_pad(et, "ETH_1V0", 350.52, y1 - 7.62)
+et.text("Test pad: PHY 1.0 V", 345.44, y1 - 20.32)
+pwr(et, "PWR_FLAG", 312.42, y1)
+cap_row(et, y1, x1, ["22uF", "100nF", "100nF", "100nF", "100nF"])
+et.text("1.0 V core (DVDD10 pin 21, AVDD10 pins 3, 8, 38): only for the PHY.\n"
+ "Inductor Isat > 600 mA, X5R/X7R capacitors.", 236.22, y1 + 15.24)
+
+# --- 25 MHz crystal (C9006, CL 12 pF -> 2 x 18 pF), RSET
+xc, yc = 254.0, 129.54
+xt = et.place("Device", "Crystal_GND24", ref("Y"), "25MHz", xc, yc,
+ fp="Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm",
+ ref_at=(xc, yc - 10.16), val_at=(xc, yc - 7.62), centered=True,
+ fields={"LCSC": "C9006", "MPN": "X322525MOB4SI"})
+pwr(et, "GND", *xt["2"])
+xi, xo = xt["1"], xt["3"]
+et.wire((xi[0] - 5.08, yc), xi)
+et.wire(xo, (xo[0] + 5.08, yc))
+cap(et, "18pF", xi[0] - 5.08, yc, fp=C0402)
+cap(et, "18pF", xo[0] + 5.08, yc, fp=C0402)
+pwr(et, "GND", xi[0] - 5.08, yc + 7.62)
+pwr(et, "GND", xo[0] + 5.08, yc + 7.62)
+et.wire((xi[0] - 5.08, yc), (xi[0] - 5.08, yc - 7.62))
+et.label("PHY_XTAL_IN", xi[0] - 5.08, yc - 7.62, angle=90)
+et.wire((xo[0] + 5.08, yc), (xo[0] + 5.08, yc - 7.62))
+et.label("PHY_XTAL_OUT", xo[0] + 5.08, yc - 7.62, angle=90)
+res(et, "2.49k 1%", 287.02, 124.46, fields=R2K49)
+et.wire((287.02, 124.46), (287.02, 119.38))
+et.label("PHY_RSET", 287.02, 119.38, angle=90)
+pwr(et, "GND", 287.02, 132.08)
+
+# --- RJ45 jack with magnetics (HanRun HR911130A, symbol and footprint from KiCad library MRs)
+JX, JY = 345.44, 157.48
+jk = et.place("eboard", "RJ45_Hanrun_HR911130A_Horizontal", ref("J"), "HR911130A", JX, JY, mirror=True,
+ fp="eboard:RJ45_Hanrun_HR911130A_Horizontal",
+ ref_at=(JX - 12.7, JY - 31.75), val_at=(JX - 12.7, JY - 29.21),
+ fields={"LCSC": "C54408"})
+for num, name in (("2", "MDI0_P"), ("3", "MDI0_N"), ("4", "MDI1_P"), ("7", "MDI1_N"),
+ ("5", "MDI2_P"), ("6", "MDI2_N"), ("8", "MDI3_P"), ("9", "MDI3_N"),
+ ("1", "MAG_CT")):
+ et.pin_label(jk[num], name, angle=180, length=10.16, glob=False)
+for num, name in (("11", "PHY_LED2"), ("12", "LEDG_K"), ("14", "PHY_LED1"), ("13", "LEDY_K")):
+ et.pin_label(jk[num], name, angle=90, length=7.62, glob=False)
+pwr(et, "GND", *jk["10"])
+# shield: 1M || 1 nF 2 kV to GND (keeps the 1500 V isolation with shielded cables, ESD / RF to GND)
+shp = jk["SH"]
+ysh = shp[1] + 2.54
+xr, xc = 325.12, 307.34
+et.wire(shp, (shp[0], ysh), (xr, ysh), (xc, ysh))
+et.label("ETH_SHIELD", shp[0], ysh, angle=180)
+et.place("Device", "R", ref("R"), "1M", xr, ysh + 3.81, fp="Resistor_SMD:R_0603_1608Metric",
+ ref_at=(xr + 2.54, ysh + 2.54), val_at=(xr + 2.54, ysh + 5.08), fields={"LCSC": "C22935"})
+pwr(et, "GND", xr, ysh + 7.62)
+et.place("Device", "C", ref("C"), "1nF 2kV", xc, ysh + 3.81, fp="Capacitor_SMD:C_1206_3216Metric",
+ ref_at=(xc + 2.54, ysh + 2.54), val_at=(xc + 2.54, ysh + 5.08),
+ fields={"LCSC": "C9196", "MPN": "FH 1206B102K202NT"})
+pwr(et, "GND", xc, ysh + 7.62)
+# LEDs: active high, LED pin -> LED -> 510R -> GND; transformer centre tap: 100 nF to GND
+vres_pull(et, "510", 378.46, 152.4, "LEDG_K", False, fields=R510)
+vres_pull(et, "510", 391.16, 152.4, "LEDY_K", False, fields=R510)
+cap(et, "100nF", 365.76, 152.4, fp=C0402, fields=C100N)
+et.wire((365.76, 152.4), (365.76, 147.32))
+et.label("MAG_CT", 365.76, 147.32, angle=90)
+pwr(et, "GND", 365.76, 160.02)
+et.text("RJ45 with 1000Base-T magnetics. Transformer centre tap via 100 nF to GND\n"
+ "(voltage-mode PHY, no external MDI termination needed).\n"
+ "LEDs: green = LED2 (1000 Mbit/s link/activity), yellow = LED1 (100 Mbit/s).\n"
+ "Shield ETH_SHIELD: 1M || 1 nF 2 kV to GND, keeps the 1500 V isolation of the magnetics\n"
+ "with shielded cables. Layout: own copper island under the jack shell, >= 2 mm gap to GND.",
+ 294.64, 198.12)
+
+et.text("Ethernet: RTL8211F gigabit PHY, RGMII to the FPGA bank 0", 25.4, 20.32, size=2.54, bold=True)
+et.text("Net names as on the DE2-115 (ENET_*), seen from the FPGA. RXC (ENET_RX_CLK) goes to a PCLK pin (A7).\n"
+ "3.3 V rise time must be >= 0.5 ms (TLV62569 soft start: 0.8 ms).",
+ 25.4, 27.94)
+
+
+# ---------------------------------------------------------------- SDRAM sheet
+SDRAM_SHEET_UUID = uid("sheet", "sdram")
+sd = Sheet("SDRAM", uid("file", "sdram"), f"/{ROOT_UUID}/{SDRAM_SHEET_UUID}", "7")
+SX, SY = 139.7, 101.6
+mem = sd.place("Memory_RAM", "MT48LC16M16A2TG", ref("U"), "W9825G6KH-6", SX, SY,
+ ref_at=(SX + 2.54, SY - 38.1), val_at=(SX + 2.54, SY - 35.56),
+ datasheet="https://www.winbond.com/hq/product/code-storage-flash-memory/sdram/?__locale=en&partNo=W9825G6KH",
+ description="256Mbit (4M x 16 x 4 banks) SDR SDRAM, 166 MHz, 3.3 V, TSOP-II-54; "
+ "pin compatible with MT48LC16M16A2TG",
+ fields={"LCSC": "C62246", "MPN": "W9825G6KH-6"})
+names = {}
+for num, (px, py, pname) in lib_pins("Memory_RAM", "MT48LC16M16A2TG").items():
+ names[num] = pname
+DE1 = {"DQML": "DRAM_LDQM", "DQMH": "DRAM_UDQM", "~{WE}": "DRAM_WE_N", "~{CAS}": "DRAM_CAS_N",
+ "~{RAS}": "DRAM_RAS_N", "~{CS}": "DRAM_CS_N", "BA0": "DRAM_BA_0", "BA1": "DRAM_BA_1",
+ "CKE": "DRAM_CKE"}
+supply = {"+3V3": set(), "GND": set()}
+for num, pname in sorted(names.items(), key=lambda kv: int(kv[0])):
+ pt = mem[num]
+ if pname in ("VDD", "VDDQ"):
+ supply["+3V3"].add(pt)
+ continue
+ if pname in ("VSS", "VSSQ"):
+ supply["GND"].add(pt)
+ continue
+ if pname == "NC":
+ sd.nc(pt)
+ continue
+ if pname == "CLK":
+ sd.pin_label(pt, "SDRAM_CLK", angle=180, length=27.94, glob=False)
+ continue
+ elif pname.startswith("DQ") and pname[2:].isdigit():
+ net, glob, shape = "DRAM_DQ" + pname[2:], True, "bidirectional"
+ elif pname.startswith("A") and pname[1:].isdigit():
+ net, glob, shape = "DRAM_ADDR" + pname[1:], True, "input"
+ else:
+ net, glob, shape = DE1[pname], True, "input"
+ sd.pin_label(pt, net, angle=180 if pt[0] < SX else 0, length=7.62, glob=glob, shape=shape)
+
+# stacked supply pins (VDD + VDDQ on top, VSS + VSSQ at the bottom): one symbol each
+for net, pts in supply.items():
+ pts = sorted(pts)
+ yv = pts[0][1] + (-5.08 if net == "+3V3" else 5.08)
+ for pt in pts:
+ sd.wire(pt, (pt[0], yv))
+ sd.wire(*[(pt[0], yv) for pt in pts])
+ pwr(sd, net, pts[0][0], yv)
+
+# SDRAM clock: 22R series resistor at the FPGA (source termination)
+rc = hres(sd, "22", 210.82, 139.7, fields=R22)
+sd.pin_label(rc["1"], "DRAM_CLK", angle=180, length=5.08, shape="input")
+sd.pin_label(rc["2"], "SDRAM_CLK", angle=0, length=5.08, glob=False)
+sd.text("DRAM_CLK series resistor\nclose to the FPGA", 200.66, 127.0)
+
+# decoupling: 10 uF + 100 nF per VDD / VDDQ pin (pins 1, 14, 27 and 3, 9, 43, 49)
+yd = 172.72
+xs = [38.1 + 12.7 * i for i in range(8)]
+sd.wire((27.94, yd), *[(x, yd) for x in xs])
+pwr(sd, "+3V3", 27.94, yd)
+cap_row(sd, yd, xs, ["10uF"] + ["100nF"] * 7)
+sd.text("Decoupling: 10 uF bulk, 100 nF at each VDD (1, 14, 27) and VDDQ (3, 9, 43, 49) pin.", 27.94, yd - 8.89)
+
+sd.text("SDRAM 32 MB: Winbond W9825G6KH-6 (JLCPCB C62246), 16 bit, 166 MHz", 25.4, 20.32,
+ size=2.54, bold=True)
+sd.text("Pin compatible with Micron MT48LC16M16A2TG (KiCad symbol, as on ULX3S). Net names as on the DE1\n"
+ "(DRAM_*), plus DRAM_ADDR12 for 32 MB. FPGA banks 7 + 6 (PINPLAN.md); short traces, no series\n"
+ "resistors on data/address (as on ULX3S).", 25.4, 27.94)
+
+
+# ---------------------------------------------------------------- HDMI sheet
+HDMI_SHEET_UUID = uid("sheet", "hdmi")
+hd = Sheet("HDMI", uid("file", "hdmi"), f"/{ROOT_UUID}/{HDMI_SHEET_UUID}", "8")
+HX, HY = 203.2, 104.14
+hc = hd.place("Connector", "HDMI_A", ref("J"), "HDMI_A", HX, HY,
+ fp="Connector_Video:HDMI_A_Amphenol_10029449-x01xLF_Horizontal",
+ ref_at=(HX + 12.7, HY - 22.86), val_at=(HX + 12.7, HY - 20.32),
+ fields={"LCSC": "C428493", "MPN": "10029449-001RLF"})
+TMDS = [("1", "D2_P"), ("3", "D2_N"), ("4", "D1_P"), ("6", "D1_N"),
+ ("7", "D0_P"), ("9", "D0_N"), ("10", "CLK_P"), ("12", "CLK_N")]
+for num, name in TMDS:
+ hd.pin_label(hc[num], "TMDS_" + name, angle=180, length=10.16, glob=False)
+for num in ("13", "14", "15", "16", "19"): # CEC, Utility, SCL, SDA, HPD not used
+ hd.nc(hc[num])
+# shields of the pairs, DDC/CEC ground and connector shell to GND
+gp = sorted({hc[n] for n in ("2", "5", "8", "11", "17", "SH")})
+yg = gp[0][1] + 5.08
+for pt in gp:
+ hd.wire(pt, (pt[0], yg))
+hd.wire(*[(pt[0], yg) for pt in gp])
+pwr(hd, "GND", gp[0][0], yg)
+# +5V for the monitor (pin 18), current limited (HDMI: max. 0.5 A)
+p18 = hc["18"]
+fz = hd.place("Device", "Polyfuse", ref("F"), "200mA", p18[0], p18[1] - 3.81,
+ fp="Fuse:Fuse_0805_2012Metric", ref_at=(p18[0] + 2.54, p18[1] - 5.08),
+ val_at=(p18[0] + 2.54, p18[1] - 2.54),
+ fields={"LCSC": "C269102", "MPN": "SMD0805B020TF"})
+pwr(hd, "+5V", *fz["1"])
+hd.wire(p18, (p18[0] - 7.62, p18[1]))
+pwr(hd, "PWR_FLAG", p18[0] - 7.62, p18[1])
+
+# AC coupling 22 nF per TMDS line (as on ULX3S): the sink terminates each line with
+# 50 ohm to 3.3 V, the capacitor blocks the DC current of the LVCMOS33D outputs
+for i, (num, name) in enumerate(TMDS):
+ y = 63.5 + 10.16 * i
+ cc = hd.place("Device", "C", ref("C"), "22nF", 101.6, y, rot=90, fp=C0402,
+ ref_at=(101.6, y - 6.35), val_at=(101.6, y - 3.81), centered=True,
+ fields={"LCSC": "C1532"})
+ hd.pin_label(cc["1"], "HDMI_" + name, angle=180, length=7.62, shape="output")
+ hd.pin_label(cc["2"], "TMDS_" + name, angle=0, length=7.62, glob=False)
+
+# ESD protection on the connector side of the coupling capacitors (flow-through, 0.3 pF)
+tl = ["TMDS_" + n for _, n in TMDS]
+for k, ex in ((0, 96.52), (4, 165.1)):
+ ey = 154.94
+ e4 = hd.place("Power_Protection", "TPD4E05U06DQA", ref("U"), "TPD4E05U06", ex, ey,
+ ref_at=(ex - 2.54, ey - 11.43), val_at=(ex - 2.54, ey - 8.89),
+ fields={"LCSC": "C2827646"})
+ for num, net in zip(("1", "2", "4", "5"), tl[k:k + 4]):
+ hd.pin_label(e4[num], net, angle=180 if num in ("1", "2") else 0, length=5.08, glob=False)
+ for num in ("6", "7", "9", "10"):
+ hd.nc(e4[num])
+ pwr(hd, "GND", *e4["3"])
+hd.text("ESD protection TPD4E05U06 (0.3 pF), at the connector,\n"
+ "between connector and coupling capacitors", 25.4, 172.72)
+
+hd.text("HDMI output (type A), TMDS driven directly by the FPGA", 25.4, 20.32, size=2.54, bold=True)
+hd.text("FPGA bank 0, each pair on an A/B ball pair, IO_TYPE=LVCMOS33D (as ULX3S GPDI). 640x480 and 720p work\n"
+ "with the ECP5 serializers (ODDRX1F/ODDRX2F). Route the pairs as 100 ohm differential, equal length.\n"
+ "Not connected (FPGA pin budget): DDC (SCL/SDA, no EDID), HPD, CEC, Utility.\n"
+ "ESD: two TPD4E05U06 on the TMDS lines at the connector (users plug HDMI cables in and out).",
+ 25.4, 27.94)
+hd.text("AC coupling capacitors 22 nF,\nclose to the connector", 88.9, 50.8)
+hd.text("+5V for the monitor, PTC 200 mA", HX + 5.08, HY - 45.72)
+
+
+# ---------------------------------------------------------------- user I/O sheet (DE1 names)
+UIO_SHEET_UUID = uid("sheet", "user_io")
+ui = Sheet("User IO", uid("file", "user_io"), f"/{ROOT_UUID}/{UIO_SHEET_UUID}", "9", paper="A3")
+R330 = {"LCSC": "C25104"}
+R470 = {"LCSC": "C25117"}
+R47K = {"LCSC": "C25792"}
+LED_RED = {"LCSC": "C2286", "MPN": "KT-0603R"}
+LED_YG = {"LCSC": "C965805", "MPN": "XL-1608SYGC-06"}
+HC14 = ("74xx", "74HC14")
+HC14_REF = ref("U")
+HC14_F = {"LCSC": "C5605", "MPN": "74HC14D"}
+HC14_FP = "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm"
+
+# --- KEY[3:0]: push button, RC, Schmitt trigger inverter -> active low, debounced (as on the DE1)
+GATES = [(1, "KEY0"), (2, "KEY1"), (3, "KEY2"), (4, "KEY3")]
+for i, (unit, name) in enumerate(GATES):
+ y = 55.88 + 22.86 * i
+ pwr(ui, "+3V3", 35.56, y)
+ sw = ui.place("Switch", "SW_Push", ref("SW"), "TS-1187A", 45.72, y,
+ fp="Button_Switch_SMD:SW_Push_1P1T_XKB_TS-1187A",
+ ref_at=(45.72, y - 5.08), val_at=(45.72, y - 2.54), centered=True,
+ fields={"LCSC": "C318884", "MPN": "XKB TS-1187A-B-A-B"})
+ ui.wire((35.56, y), sw["1"])
+ rr = hres(ui, "1k", 58.42, y, fields=R1K)
+ ui.wire(sw["2"], rr["1"])
+ g = ui.place(*HC14, HC14_REF, "74HC14D", 100.33, y, unit=unit, fp=HC14_FP,
+ ref_at=(100.33, y - 5.08), val_at=(100.33, y - 7.62), centered=True, fields=HC14_F)
+ gin, gout = sorted(set(g.values()))
+ ui.wire(rr["2"], (69.85, y), (80.01, y), gin)
+ cap(ui, "100nF", 69.85, y, fp=C0402, fields=C100N)
+ pwr(ui, "GND", 69.85, y + 7.62)
+ res(ui, "47k", 80.01, y, fields=R47K)
+ pwr(ui, "GND", 80.01, y + 7.62)
+ ui.pin_label(gout, name, length=7.62, shape="output")
+for unit, y in ((5, 152.4), (6, 167.64)): # unused gates: input to GND
+ g = ui.place(*HC14, HC14_REF, "74HC14D", 100.33, y, unit=unit, fp=HC14_FP,
+ ref_at=(100.33, y - 5.08), val_at=(100.33, y - 7.62), centered=True, fields=HC14_F)
+ gin, gout = sorted(set(g.values()))
+ ui.wire(gin, (gin[0] - 5.08, gin[1]))
+ pwr(ui, "GND", gin[0] - 5.08, gin[1])
+ ui.nc(gout)
+pu = ui.place(*HC14, HC14_REF, "74HC14D", 60.96, 162.56, unit=7, fp=HC14_FP,
+ ref_at=(63.5, 152.4), val_at=(63.5, 154.94), fields=HC14_F)
+pwr(ui, "+3V3", *pu["14"])
+pwr(ui, "GND", *pu["7"])
+cap(ui, "100nF", 43.18, 154.94, fp=C0402, fields=C100N)
+pwr(ui, "+3V3", 43.18, 154.94)
+pwr(ui, "GND", 43.18, 162.56)
+ui.text("KEY[3:0]: high when not pressed, low when pressed (as on the DE1).\n"
+ "Debounced: button charges 100 nF via 1k, 47k discharges it (~5 ms),\n"
+ "74HC14 Schmitt trigger inverts. Usable as clock input in lab designs.", 30.48, 38.1)
+
+# --- LEDR[9:0] (red) and LEDG[5:0] (yellow-green): active high, FPGA pin -> R -> LED -> GND
+LEDS = [(f"LEDR{i}", "red", "1k", R1K, LED_RED) for i in range(10)] + \
+ [(f"LEDG{i}", "green", "470", R470, LED_YG) for i in range(6)]
+for i, (name, colour, rv, rf, lf) in enumerate(LEDS):
+ y = 50.8 + 7.62 * i + (5.08 if i >= 10 else 0)
+ rl = hres(ui, rv, 172.72, y, fields=rf)
+ ui.pin_label(rl["1"], name, angle=180, length=5.08, shape="input")
+ led = ui.place("Device", "LED", ref("D"), colour, 186.69, y, rot=180, fp=LED0603,
+ ref_at=(186.69, y - 5.08), val_at=(186.69, y - 2.54), centered=True, fields=lf)
+ ui.wire(rl["2"], led["2"])
+ ui.wire(led["1"], (195.58, y))
+ pwr(ui, "GND", 195.58, y)
+ui.text("LEDR[9:0], LEDG[5:0]: active high (as on the DE1).\n"
+ "Red KT-0603R 1k (~1.3 mA), yellow-green XL-1608SYGC 470R (~2.3 mA).", 154.94, 38.1)
+
+# --- HEX0 ... HEX3: 7-segment displays, common anode, segments active low (as on the DE1)
+SEGS = "ABCDEFG"
+for d, (bx, by) in enumerate(((335.28, 132.08), (256.54, 132.08), (335.28, 50.8), (256.54, 50.8))):
+ for k, seg in enumerate(SEGS):
+ y = by + 7.62 * k
+ rs = hres(ui, "330", bx + 12.7, y, fields=R330)
+ ui.pin_label(rs["1"], f"HEX{d}_{k}", angle=180, length=5.08, shape="input")
+ ui.pin_label(rs["2"], f"H{d}_{seg}", angle=0, length=5.08, glob=False)
+ lp = lib_pins("eboard", "SM410561N")
+ ax = lp["7"][0] # x of the segment pins in the symbol
+ dx = bx + 48.26 - ax
+ dy = by + 30.48
+ disp = ui.place("eboard", "SM410561N", ref("U"), "SM410561N", dx, dy,
+ ref_at=(dx - 15.24, dy - 22.86), val_at=(dx - 15.24, dy - 20.32),
+ fields={"LCSC": "C141368"})
+ for num, seg in (("7", "A"), ("6", "B"), ("4", "C"), ("2", "D"), ("1", "E"), ("9", "F"), ("10", "G")):
+ ui.pin_label(disp[num], f"H{d}_{seg}", angle=180, length=7.62, glob=False)
+ ui.nc(disp["5"]) # DP not used (as on the DE1)
+ ca = sorted({disp["3"], disp["8"]})
+ yc = ca[0][1] - 5.08
+ for pt in ca:
+ ui.wire(pt, (pt[0], yc))
+ ui.wire(*[(pt[0], yc) for pt in ca])
+ pwr(ui, "+3V3", ca[0][0], yc)
+ ui.text(f"HEX{d}", bx + 30.48, by - 7.62, size=2.54, bold=True)
+ui.text("HEX0[6:0] ... HEX3[6:0]: segment 0 = a ... 6 = g, active low (as on the DE1).\n"
+ "Common anode on 3.3 V, 330R per segment (~4 mA). Decimal point not connected.",
+ 256.54, 38.1)
+
+# --- SW[9:0]: slide switches, common contact via 1k to the FPGA
+ys = 218.44
+for i in range(10):
+ x = 45.72 + 35.56 * i
+ ss = ui.place("Switch", "SW_SPDT", ref("SW"), "SS-12D07-5", x, ys,
+ fp="Button_Switch_THT:SW_Slide_SPDT_Straight_CK_OS102011MS2Q",
+ ref_at=(x - 2.54, ys + 7.62), val_at=(x - 2.54, ys + 10.16),
+ fields={"LCSC": "C2857673", "MPN": "ROCPU SS-12D07-5",
+ "Alternative": "C&K OS102011MS2QN1 (C221829), same footprint"})
+ rw = hres(ui, "1k", x - 10.16, ys, fields=R1K)
+ ui.wire(ss["2"], rw["2"])
+ ui.pin_label(rw["1"], f"SW{i}", angle=180, length=5.08, shape="input")
+ ui.wire(ss["1"], (x + 7.62, ss["1"][1]))
+ pwr(ui, "+3V3", x + 7.62, ss["1"][1])
+ ui.wire(ss["3"], (x + 7.62, ss["3"][1]))
+ pwr(ui, "GND", x + 7.62, ss["3"][1])
+ui.text("SW[9:0]: vertical slide switches ROCPU SS-12D07-5 (C&K OS102011 footprint; C&K OS102011MS2QN1 fits too),\n"
+ "through hole, not debounced (as on the DE1).\n"
+ "1k series resistor protects the FPGA pin if it is driven as an output by mistake.", 30.48, 198.12)
+
+ui.text("User I/O (DE1 compatible): SW, KEY, LEDR, LEDG, HEX", 25.4, 20.32, size=2.54, bold=True)
+ui.text("Net names = DE1 port names, see kicad/README.md. FPGA banks 3, 6 and 8 (PINPLAN.md).", 25.4, 27.94)
+
+
+# ---------------------------------------------------------------- GPIO headers, Pmods, ESD
+GPIO_SHEET_UUID = uid("sheet", "gpio")
+gp = Sheet("GPIO", uid("file", "gpio"), f"/{ROOT_UUID}/{GPIO_SHEET_UUID}", "10", paper="A3")
+
+
+def de1_index(pin):
+ """DE1 GPIO header pin -> GPIO index, or None for supply pins."""
+ if pin <= 10:
+ return pin - 1
+ if 13 <= pin <= 28:
+ return pin - 3
+ if 31 <= pin <= 40:
+ return pin - 5
+ return None
+
+
+def supply_pin(sh, pt, net, left):
+ """Supply pin of a connector: long wire past the labels, then the power symbol."""
+ x2 = pt[0] - 25.4 if left else pt[0] + 25.4
+ sh.wire(pt, (x2, pt[1]))
+ pwr(sh, net, x2, pt[1])
+
+
+ADA = {37: "ADA_POWER_ON", 38: "ADA_OEB", 40: "ADA_OEA"} # GPIO_0 pins fixed for the THDB-ADA
+for hdr, (hx, hy), ref_name in (("GPIO_0", (76.2, 81.28), "JP1"), ("GPIO_1", (190.5, 81.28), "JP2")):
+ j = gp.place("Connector_Generic", "Conn_02x20_Odd_Even", ref("J"), f"{hdr} ({ref_name})", hx, hy,
+ fp="Connector_IDC:IDC-Header_2x20_P2.54mm_Vertical",
+ ref_at=(hx - 2.54, hy - 27.94), val_at=(hx - 2.54, hy - 30.48),
+ fields={"LCSC": "C5200276", "MPN": "EH254V-12-40P"})
+ for pin in range(1, 41):
+ pt = j[str(pin)]
+ left = pin % 2 == 1
+ if pin in (11, 29): # DE1: 11 = 5 V, 29 = 3.3 V; eboard: both 3.3 V
+ supply_pin(gp, pt, "+3V3", left)
+ elif pin in (12, 30):
+ supply_pin(gp, pt, "GND", left)
+ elif hdr == "GPIO_0" and pin in ADA:
+ gp.pin_label(pt, ADA[pin], angle=180 if left else 0, length=25.4, glob=False)
+ elif hdr == "GPIO_0" and pin == 39:
+ gp.nc(pt)
+ else:
+ gp.pin_label(pt, f"{hdr}_{de1_index(pin)}", angle=180 if left else 0, length=7.62)
+
+# THDB-ADA control pins on GPIO_0 (see PINPLAN.md): fixed levels instead of FPGA pins
+for k, (net, up, txt) in enumerate((("ADA_POWER_ON", True, "pin 37 POWER_ON = 1"),
+ ("ADA_OEB", False, "pin 38 ADC_OEB = 0"),
+ ("ADA_OEA", False, "pin 40 ADC_OEA = 0"))):
+ x = 347.98 + 20.32 * k
+ vres_pull(gp, "10k", x, 66.04, net, up, fields=R10K)
+ gp.text(txt, x - 6.35, 96.52 + 2.54 * k)
+gp.text("THDB-ADA: GPIO_0 pins 37-40 are not FPGA pins.\nPOWER_ON pull-up (ADC and clock buffer on),\n"
+ "OEA/OEB pull-down (ADC outputs enabled), pin 39 unused.", 340.36, 43.18)
+
+# Pmod A / B (Digilent Pmod interface, 12 pin, right angle female): pin k (1-6, upper row) = pad 2k-1,
+# pin k (7-12, lower row) = pad 2(k-6); pins 5/11 GND, 6/12 3.3 V
+for name, (px, py), base in (("Pmod A", (292.1, 63.5), 0), ("Pmod B", (292.1, 101.6), 8)):
+ pm = gp.place("Connector_Generic", "Conn_02x06_Odd_Even", ref("J"), name, px, py,
+ fp="Connector_PinSocket_2.54mm:PinSocket_2x06_P2.54mm_Horizontal",
+ ref_at=(px - 2.54, py - 12.7), val_at=(px - 2.54, py - 15.24),
+ fields={"LCSC": "C5333437", "MPN": "A2541HWR-2x6P"})
+ for pmod_pin in range(1, 13):
+ pad = 2 * pmod_pin - 1 if pmod_pin <= 6 else 2 * (pmod_pin - 6)
+ pt = pm[str(pad)]
+ left = pad % 2 == 1
+ if pmod_pin in (5, 11):
+ supply_pin(gp, pt, "GND", left)
+ elif pmod_pin in (6, 12):
+ supply_pin(gp, pt, "+3V3", left)
+ else:
+ io = pmod_pin - 1 if pmod_pin <= 4 else pmod_pin - 3
+ gp.pin_label(pt, f"GPIO_0_{base + io}", angle=180 if left else 0, length=7.62)
+gp.text("Pmod A = GPIO_0_0..7, Pmod B = GPIO_0_8..15 (shared with GPIO_0:\n"
+ "use either a Pmod or a card on GPIO_0). Pmod pin 1-4 / 7-10 = IO 1-8.\n"
+ "Check pin 1 orientation against the Digilent Pmod specification in layout.", 248.92, 27.94)
+
+# ESD protection: TPD4E05U06 (4 lines, 0.3 pF, flow-through) on all 68 FPGA lines
+lines = [f"GPIO_0_{i}" for i in range(32)] + [f"GPIO_1_{i}" for i in range(36)]
+for k in range(0, len(lines), 4):
+ n = k // 4
+ ex, ey = 45.72 + 76.2 * (n % 5), 142.24 + 30.48 * (n // 5)
+ e4 = gp.place("Power_Protection", "TPD4E05U06DQA", ref("U"), "TPD4E05U06", ex, ey,
+ ref_at=(ex - 2.54, ey - 11.43), val_at=(ex - 2.54, ey - 8.89),
+ fields={"LCSC": "C2827646", "MPN": "TPD4E05U06DQAR"})
+ for num, net in zip(("1", "2", "4", "5"), lines[k:k + 4]):
+ gp.pin_label(e4[num], net, angle=180 if num in ("1", "2") else 0, length=5.08)
+ for num in ("6", "7", "9", "10"):
+ gp.nc(e4[num])
+ pwr(gp, "GND", *e4["3"])
+
+gp.text("GPIO expansion headers (DE1 compatible), Pmod A/B, ESD protection", 25.4, 20.32,
+ size=2.54, bold=True)
+gp.text("2x20 box headers at the DE1 positions (JP1 pin 1 at (125.603, 118.745) mm, JP2 20.32 mm to the right,\n"
+ "see PINPLAN.md). DE1 pinout: pins 1-10 = GPIO_x[0..9], 13-28 = [10..25], 31-40 = [26..35],\n"
+ "12/30 = GND. Pin 29 = 3.3 V; pin 11 = 3.3 V instead of 5 V on the DE1: no 5 V on the headers.\n"
+ "FPGA pins connected directly (as on ULX3S), ESD arrays close to the header pins.", 25.4, 27.94)
+for i, where in enumerate(("GPIO headers", "Pmods")):
+ lx = 355.6 + 25.4 * i
+ gp.place("Connector", "TestPoint", ref("TP"), "GND", lx, 114.3, fp=TP_LOOP,
+ ref_at=(lx + 2.54, 110.49), val_at=(lx + 2.54, 113.03),
+ fields={"LCSC": "C238127", "MPN": "Keystone 5011"})
+ gp.wire((lx, 114.3), (lx, 116.84))
+ pwr(gp, "GND", lx, 116.84)
+gp.text("Scope ground loops (Keystone 5011, black) for the\n"
+ "crocodile clips: one at the GPIO headers, one at the Pmods.", 345.44, 104.14)
+gp.text("ESD protection TPD4E05U06 (DFN 2.5x1 mm, flow-through), one per 4 lines, placed at the headers", 25.4, 119.38)
+
+
+# ---------------------------------------------------------------- FPGA bank sheets
+# Ball assignment (preliminary, PINPLAN.md): swap within a bank during layout.
+BALLS = {}
+# bank 0: Ethernet RGMII (RXC on PCLKT0_0 = A7) + HDMI (A/B pairs, A = P)
+BALLS.update({
+ "HDMI_D2_P": "A2", "HDMI_D2_N": "B3", "HDMI_D1_P": "A3", "HDMI_D1_N": "A4",
+ "HDMI_D0_P": "C4", "HDMI_D0_N": "B4", "HDMI_CLK_P": "A5", "HDMI_CLK_N": "A6",
+ "ENET_RX_CLK": "A7", "ENET_RX_DV": "A8", "ENET_RX_DATA0": "B7", "ENET_RX_DATA1": "C7",
+ "ENET_RX_DATA2": "B6", "ENET_RX_DATA3": "C6",
+ "ENET_GTX_CLK": "D7", "ENET_TX_EN": "E7", "ENET_TX_DATA0": "D6", "ENET_TX_DATA1": "E6",
+ "ENET_TX_DATA2": "D5", "ENET_TX_DATA3": "E5",
+ "ENET_MDC": "D4", "ENET_MDIO": "E4", "ENET_RST_N": "B5", "ENET_INT_N": "C5"})
+# bank 1: GPIO_0[31:0], [0] = C8 (PCLKT1_0), [2] = E8 (PCLKT1_1); (true, complement) pairs
+b1 = [("C8", "B8"), ("E8", "D8"), ("B9", "C9"), ("D9", "E9"), ("A9", "A10"), ("B10", "C10"),
+ ("D10", "E10"), ("B11", "C11"), ("D11", "E11"), ("A11", "A12"), ("B12", "C12"),
+ ("D12", "E12"), ("B13", "C13"), ("D13", "E13"), ("A13", "A14"), ("B14", "A15")]
+# bank 2: GPIO_1[31:0], [0] = K16 (PCLKT2_0), [2] = J16 (PCLKT2_1)
+b2 = [("K16", "K15"), ("J16", "J15"), ("J14", "K14"), ("G16", "H15"), ("J13", "J12"),
+ ("H12", "H13"), ("G14", "H14"), ("F16", "G15"), ("F15", "E16"), ("G12", "G13"),
+ ("F13", "F12"), ("D16", "E15"), ("E14", "F14"), ("C16", "C15"), ("C14", "D14"),
+ ("B16", "B15")]
+for i, (t, c) in enumerate(b1):
+ BALLS[f"GPIO_0_{2 * i}"], BALLS[f"GPIO_0_{2 * i + 1}"] = t, c
+for i, (t, c) in enumerate(b2):
+ BALLS[f"GPIO_1_{2 * i}"], BALLS[f"GPIO_1_{2 * i + 1}"] = t, c
+# bank 3: GPIO_1[35:32] (2 pairs next to bank 2), UART, HEX0-HEX2, HEX3[4:0]
+BALLS.update({"GPIO_1_32": "L16", "GPIO_1_33": "L15", "GPIO_1_34": "M16", "GPIO_1_35": "M15",
+ "UART_TXD": "K12", "UART_RXD": "K13"})
+b3 = ["L13", "L12", "L14", "M14", "N16", "P15", "M13", "N14", "P16", "R16", "N13", "T15",
+ "R15", "P14", "P13", "T14", "R13", "R14", "M12", "N12", "R12", "T13", "M11", "P12",
+ "N11", "P11"]
+hex_ = [f"HEX{d}_{s}" for d in range(4) for s in range(7)]
+for n, b in zip(hex_[:26], b3):
+ BALLS[n] = b
+# bank 6: CLOCK_50 on M1 (PCLKT6_0), SDRAM control next to bank 7, SW, KEY, LEDR
+BALLS["CLOCK_50"] = "M1"
+for n, b in zip(["DRAM_BA_0", "DRAM_BA_1", "DRAM_CS_N", "DRAM_RAS_N", "DRAM_CAS_N", "DRAM_WE_N",
+ "DRAM_CKE"], ["K4", "K5", "L4", "L5", "L1", "L2", "M2"]):
+ BALLS[n] = b
+b6 = ["L3", "M3", "N1", "P2", "M4", "P1", "R1", "N3", "N4", "R2", "P3", "T2", "P4", "R4",
+ "R3", "T3", "R5", "M5", "T4", "N5", "M6", "N6", "P6", "P5"]
+for n, b in zip([f"LEDR{i}" for i in range(10)] + [f"SW{i}" for i in range(10)]
+ + [f"KEY{i}" for i in range(4)], b6):
+ BALLS[n] = b
+# bank 7: SDRAM DQ, DQM, CLK, ADDR
+b7 = ["B1", "B2", "C3", "D3", "C1", "E3", "C2", "F3", "D1", "F4", "E2", "F5", "G5", "F2",
+ "G4", "E1", "F1", "G3", "G2", "H3", "H5", "J4", "H4", "J5", "G1", "J3", "H2", "K3",
+ "J1", "K1", "J2", "K2"]
+for n, b in zip([f"DRAM_DQ{i}" for i in range(8)] + ["DRAM_LDQM"]
+ + [f"DRAM_DQ{i}" for i in range(8, 16)] + ["DRAM_UDQM", "DRAM_CLK"]
+ + [f"DRAM_ADDR{i}" for i in range(13)], b7):
+ BALLS[n] = b
+# bank 8 (free pins after the SPI flash): LEDG, HEX3[6:5]
+B8_BALLS = {"LEDG0": "T6", "LEDG1": "R7", "LEDG2": "R6", "LEDG3": "P7", "LEDG4": "R8",
+ "LEDG5": "P8", "HEX3_5": "M8", "HEX3_6": "M9"}
+BALLS.update(B8_BALLS)
+assert len(BALLS) == 192 and len(set(BALLS.values())) == 192, (len(BALLS), len(set(BALLS.values())))
+BALL_NET = {b: n for n, b in BALLS.items()}
+
+
+def fpga_dir(n):
+ """Global label shape, seen from the FPGA."""
+ if n.startswith(("GPIO_", "DRAM_DQ")) or n == "ENET_MDIO":
+ return "bidirectional"
+ if n.startswith(("SW", "KEY", "ENET_RX", "CLOCK_50", "UART_RXD", "ENET_INT")):
+ return "input"
+ return "output"
+
+
+BANK_UNIT = {0: 1, 1: 2, 2: 3, 3: 4, 6: 5, 7: 6}
+
+
+def bank_unit(sh, bank, x, y, note):
+ u = sh.place(*FPGA, FPGA_REF, "LFE5U-25F-7BG256I", x, y, unit=BANK_UNIT[bank],
+ ref_at=(x - 30.48, y - 45.72), val_at=(x - 30.48, y - 43.18))
+ pins = lib_pins(*FPGA, BANK_UNIT[bank])
+ for num, (px, py, name) in pins.items():
+ if name.startswith("VCCIO"):
+ continue
+ net = BALL_NET[num]
+ sh.pin_label(u[num], net, angle=180 if px < 0 else 0, length=2.54, shape=fpga_dir(net))
+ for num, (px, py, name) in pins.items():
+ if name.startswith("VCCIO"):
+ pwr(sh, "+3V3", *u[num])
+ break
+ sh.text(note, x - 30.48, y + 33.02)
+
+
+fb = []
+for key, title, banks, notes in (
+ ("fpga_banks_01", "FPGA banks 0 1", (0, 1),
+ ("Bank 0 (top left): Ethernet RGMII, ENET_RX_CLK on PCLKT0_0 (A7);\n"
+ "HDMI pairs LVCMOS33D on A/B ball pairs (A = P).",
+ "Bank 1 (top right): GPIO_0[31:0] incl. Pmod A/B.\n"
+ "GPIO_0[0] / [2] on PCLKT1_0 C8 / PCLKT1_1 E8; GPIO_0[2k] / [2k+1]\n"
+ "on the true / complement ball of a pair (header neighbours).")),
+ ("fpga_banks_23", "FPGA banks 2 3", (2, 3),
+ ("Bank 2 (right upper): GPIO_1[31:0].\n"
+ "GPIO_1[0] / [2] on PCLKT2_0 K16 / PCLKT2_1 J16; GPIO_1[2k] / [2k+1]\n"
+ "on true LVDS pairs (A/B, C/D).",
+ "Bank 3 (right lower): GPIO_1[35:32] (pairs PR26),\n"
+ "UART_TXD / UART_RXD, HEX0-HEX2, HEX3[4:0].")),
+ ("fpga_banks_67", "FPGA banks 6 7", (6, 7),
+ ("Bank 6 (left lower): CLOCK_50 on PCLKT6_0 (M1),\n"
+ "SDRAM BA/CS/RAS/CAS/WE/CKE, LEDR, SW, KEY.",
+ "Bank 7 (left upper): SDRAM DQ[15:0], LDQM/UDQM, CLK, ADDR[12:0]."))):
+ sid = uid("sheet", key)
+ sh = Sheet(title, uid("file", key), f"/{ROOT_UUID}/{sid}", str(11 + len(fb)))
+ bank_unit(sh, banks[0], 78.74, 101.6, notes[0])
+ bank_unit(sh, banks[1], 213.36, 101.6, notes[1])
+ sh.text(title.replace("FPGA banks", "FPGA banks").replace(" 0 1", " 0 and 1")
+ .replace(" 2 3", " 2 and 3").replace(" 6 7", " 6 and 7"), 25.4, 20.32, size=2.54, bold=True)
+ sh.text("All banks VCCIO = 3.3 V. Ball assignment preliminary (PINPLAN.md): pins may be\n"
+ "swapped within a bank during layout; the LPF follows the schematic.\n"
+ "Balls B1 B2 A2 B3 B14 A15 B15 B16 (PLL inputs on the 45F) carry no clocks.",
+ 25.4, 27.94)
+ fb.append((sid, title, key, sh))
+
+
+# ---------------------------------------------------------------- test points sheet
+TP_SHEET_UUID = uid("sheet", "testpoints")
+tps = Sheet("Test points", uid("file", "testpoints"), f"/{ROOT_UUID}/{TP_SHEET_UUID}", "14")
+tps.text("Test points and mounting holes", 25.4, 20.32, size=2.54, bold=True)
+tps.text("Round top-layer pads (1.5 mm, TestPoint_Pad_D1.5mm) for probes and test needles. No part:\n"
+ "not in the BOM, not in the placement file. Further test pads on their sheets: VBUS (USB),\n"
+ "FPGA_PROGRAMN / FPGA_INITN / FPGA_DONE (FPGA config), ETH_1V0 (Ethernet).\n"
+ "JTAG: the 1x6 header footprint J2 (USB sheet, not fitted) serves as test access.\n"
+ "Scope ground loops for the lab (Keystone 5011): GPIO sheet.", 25.4, 27.94)
+tps.text("Supply rails", 25.4, 55.88, bold=True)
+for i, net in enumerate(("+5V", "+3V3", "+2V5", "+1V1")):
+ tp_pad(tps, net, 38.1 + 25.4 * i, 71.12, kind="rail")
+tps.text("GND, spread over the board (next to the rail pads, FPGA, Ethernet, GPIO)", 25.4, 93.98, bold=True)
+for i in range(4):
+ tp_pad(tps, "GND", 38.1 + 25.4 * i, 109.22, kind="gnd")
+tps.text("Signals", 25.4, 132.08, bold=True)
+for i, net in enumerate(("CLOCK_50", "UART_TXD", "UART_RXD")):
+ tp_pad(tps, net, 38.1 + 25.4 * i, 147.32, glob=True)
+
+# mounting holes at the DE1 positions (PINPLAN.md), plated, connected to GND
+tps.text("Mounting holes (M3, DE1 positions)", 152.4, 55.88, bold=True)
+MH = [(4.191, 4.191), (4.191, 32.385), (4.191, 147.955),
+ (147.955, 4.191), (147.955, 32.385), (147.955, 147.955)]
+for i, (bx, by) in enumerate(MH):
+ hx, hy = 160.02 + 20.32 * (i % 3), 71.12 + 27.94 * (i // 3)
+ h = tps.place("Mechanical", "MountingHole_Pad", ref("H"), "M3", hx, hy,
+ fp="MountingHole:MountingHole_3.2mm_M3_Pad_Via", in_bom=False,
+ ref_at=(hx + 3.81, hy - 2.54), val_at=(hx + 3.81, hy))
+ tps.wire(h["1"], (h["1"][0], h["1"][1] + 2.54))
+ pwr(tps, "GND", h["1"][0], h["1"][1] + 2.54)
+ tps.text(f"({bx:g}, {by:g})", hx - 5.08, hy + 12.7)
+tps.text("Board coordinates in mm, origin bottom left, y up (DE1 outline).\n"
+ "Plated M3 holes with via ring, connected to GND: the metal\n"
+ "standoffs ground the board to the lab bench / case.", 152.4, 129.54)
+
+
+# ---------------------------------------------------------------- root sheet
+root = Sheet("Top", ROOT_UUID, f"/{ROOT_UUID}", "1")
+
+
+def sheet_block(sid, name, fname, x, y, page):
+ return f"""\t(sheet
+\t\t(at {r(x)} {r(y)})
+\t\t(size 38.1 20.32)
+\t\t(exclude_from_sim no)
+\t\t(in_bom yes)
+\t\t(on_board yes)
+\t\t(dnp no)
+\t\t(stroke
+\t\t\t(width 0.1524)
+\t\t\t(type solid)
+\t\t)
+\t\t(fill
+\t\t\t(color 0 0 0 0.0000)
+\t\t)
+\t\t(uuid "{sid}")
+\t\t(property "Sheetname" "{name}"
+\t\t\t(at {r(x)} {r(y - 0.7116)} 0)
+\t\t\t(effects
+\t\t\t\t(font
+\t\t\t\t\t(size 1.27 1.27)
+\t\t\t\t)
+\t\t\t\t(justify left bottom)
+\t\t\t)
+\t\t)
+\t\t(property "Sheetfile" "{fname}"
+\t\t\t(at {r(x)} {r(y + 20.9046)} 0)
+\t\t\t(effects
+\t\t\t\t(font
+\t\t\t\t\t(size 1.27 1.27)
+\t\t\t\t)
+\t\t\t\t(justify left top)
+\t\t\t)
+\t\t)
+\t\t(instances
+\t\t\t(project "{PROJECT}"
+\t\t\t\t(path "/{ROOT_UUID}"
+\t\t\t\t\t(page "{page}")
+\t\t\t\t)
+\t\t\t)
+\t\t)
+\t)
+"""
+
+
+blocks = (sheet_block(POWER_SHEET_UUID, "Power", "power.kicad_sch", 50.8, 50.8, "2")
+ + sheet_block(USB_SHEET_UUID, "USB", "usb.kicad_sch", 101.6, 50.8, "3")
+ + sheet_block(FPWR_SHEET_UUID, "FPGA power", "fpga_power.kicad_sch", 152.4, 50.8, "4")
+ + sheet_block(FCFG_SHEET_UUID, "FPGA config", "fpga_config.kicad_sch", 203.2, 50.8, "5")
+ + sheet_block(ETH_SHEET_UUID, "Ethernet", "ethernet.kicad_sch", 50.8, 88.9, "6")
+ + sheet_block(SDRAM_SHEET_UUID, "SDRAM", "sdram.kicad_sch", 101.6, 88.9, "7")
+ + sheet_block(HDMI_SHEET_UUID, "HDMI", "hdmi.kicad_sch", 152.4, 88.9, "8")
+ + sheet_block(UIO_SHEET_UUID, "User IO", "user_io.kicad_sch", 203.2, 88.9, "9")
+ + sheet_block(GPIO_SHEET_UUID, "GPIO", "gpio.kicad_sch", 50.8, 127.0, "10")
+ + "".join(sheet_block(sid, t, k + ".kicad_sch", 101.6 + 50.8 * i, 127.0, str(11 + i))
+ for i, (sid, t, k, _) in enumerate(fb))
+ + sheet_block(TP_SHEET_UUID, "Test points", "testpoints.kicad_sch", 101.6, 165.1, "14"))
+root.text("eboard - ECP5 SoC board with GbE Ethernet", 25.4, 20.32, size=2.54, bold=True)
+root.text("Supply rails are global power symbols (+5V, +3V3, +2V5, +1V1, GND).\n"
+ "Signals between sheets are global labels, as on ULX3S.", 25.4, 30.48)
+
+os.makedirs(OUT, exist_ok=True)
+open(f"{OUT}/power.kicad_sch", "w").write(pw.render("eboard - Power"))
+open(f"{OUT}/usb.kicad_sch", "w").write(us.render("eboard - USB"))
+open(f"{OUT}/fpga_power.kicad_sch", "w").write(fpw.render("eboard - FPGA power"))
+open(f"{OUT}/fpga_config.kicad_sch", "w").write(cf.render("eboard - FPGA config"))
+open(f"{OUT}/ethernet.kicad_sch", "w").write(et.render("eboard - Ethernet"))
+open(f"{OUT}/sdram.kicad_sch", "w").write(sd.render("eboard - SDRAM"))
+open(f"{OUT}/hdmi.kicad_sch", "w").write(hd.render("eboard - HDMI"))
+open(f"{OUT}/user_io.kicad_sch", "w").write(ui.render("eboard - User IO"))
+open(f"{OUT}/gpio.kicad_sch", "w").write(gp.render("eboard - GPIO"))
+open(f"{OUT}/testpoints.kicad_sch", "w").write(tps.render("eboard - Test points"))
+for sid, t, k, sh in fb:
+ open(f"{OUT}/{k}.kicad_sch", "w").write(sh.render("eboard - " + t))
+open(f"{OUT}/{PROJECT}.kicad_sch", "w").write(root.render("eboard", extra=blocks))
+
+pro_path = f"{OUT}/{PROJECT}.kicad_pro"
+pro = json.load(open(pro_path)) if os.path.exists(pro_path) else {
+ "meta": {"filename": f"{PROJECT}.kicad_pro", "version": 3}, "text_variables": {}}
+pro["sheets"] = [[ROOT_UUID, "Root"], [POWER_SHEET_UUID, "Power"], [USB_SHEET_UUID, "USB"],
+ [FPWR_SHEET_UUID, "FPGA power"], [FCFG_SHEET_UUID, "FPGA config"],
+ [ETH_SHEET_UUID, "Ethernet"], [SDRAM_SHEET_UUID, "SDRAM"],
+ [HDMI_SHEET_UUID, "HDMI"], [UIO_SHEET_UUID, "User IO"],
+ [GPIO_SHEET_UUID, "GPIO"]] + [[sid, t] for sid, t, k, _ in fb] + [[TP_SHEET_UUID, "Test points"]]
+json.dump(pro, open(pro_path, "w"), indent=2)
+print("ok")