diff options
| author | Friedrich Beckmann <friedrich.beckmann@tha.de> | 2026-11-08 18:14:54 +0100 |
|---|---|---|
| committer | Friedrich Beckmann <friedrich.beckmann@tha.de> | 2026-11-08 18:14:54 +0100 |
| commit | 5129a5a5448012a53cb9ddae1349c2a2c8571339 (patch) | |
| tree | 41d60bbce7fe0de57cdd92b232682831759de1a8 /kicad/tools/gen_sch.py | |
| parent | 9e4911662b81bad61183cca74b8c1905f84e60e4 (diff) | |
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-x | kicad/tools/gen_sch.py | 1721 |
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") |
