board
RITHESH LED Display
Public Unreviewedby Rithesh03
A 300 x 70 mm PCB that spells RITHESH with 107 WS2812B RGB LEDs, controlled over Wi-Fi.
main
Rithesh03
Update 3: PCB design completed - routed board, manufacturing package (prototype, do not order yet), WLED guide, final checklist, Hydrogen feedback
1ed072d
13d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
"""Pin audit: symbols vs datasheet pin tables, symbols vs footprints, and connection rules.
Usage: python3 check_pins.py <netlist.xml>
The DATASHEET tables below are transcribed from the official datasheets (sources in comments).
"""
import os
import sys
import xml.etree.ElementTree as ET
import sexp
HW = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FP_DIR = "/usr/share/kicad/footprints/"
# ---- datasheet pin tables (pin number -> function) ------------------------------
ESP = {"3": "3V3", "8": "EN", "5": "IO2", "6": "IO3", "12": "IO0", "13": "IO1", "16": "IO10",
"18": "IO4", "19": "IO5", "20": "IO6", "21": "IO7", "22": "IO8", "23": "IO9",
"26": "IO18", "27": "IO19", "30": "RXD0", "31": "TXD0"}
ESP.update({str(n): "GND" for n in [1, 2, 11, 14] + list(range(36, 54))})
ESP.update({str(n): "NC" for n in [4, 7, 9, 10, 15, 17, 24, 25, 28, 29, 32, 33, 34, 35]})
DATASHEET = {
# Espressif ESP32-C3-MINI-1 datasheet v2.2, Table 3-1 (EPAD = pin 49, GND)
"ESP32-C3-MINI-1": ESP,
# TI TPS2595 SLVSE57C, Pin Functions, TPS2595x0/1 DSG; exposed PAD = GND (footprint pad 9)
"TPS259531": {"1": "dVdt", "2": "EN/UVLO", "3": "IN", "4": "IN", "5": "OUT", "6": "FLT",
"7": "ILM", "8": "GND", "9": "GND"},
# TI TLV767 SLVSE84D, Figure 5-2 DRV (fixed); thermal pad = GND (footprint pad 7)
"TLV76733DRV": {"1": "OUT", "2": "SNS", "3": "GND", "4": "EN", "5": "GND", "6": "IN", "7": "GND"},
# XINGLIGHT XL-5050RGBC-2812B datasheet, pin table (page 11)
"XL-5050RGBC-2812B": {"1": "VDD", "2": "DOUT", "3": "GND", "4": "DIN"},
}
ALIASES = {"~{FLT}": "FLT", "PAD": "GND", "VSS": "GND", "DO": "DOUT", "DI": "DIN"}
def norm(name):
name = ALIASES.get(name, name)
return name.split("/")[0] if not name.startswith("EN/") else name
# ---- load netlist -----------------------------------------------------------------
root = ET.parse(sys.argv[1]).getroot()
pin_net, net_size = {}, {}
for net in root.iter("net"):
name = net.get("name").split("/")[-1]
nodes = list(net.iter("node"))
for n in nodes:
pin_net[(n.get("ref"), n.get("pin"))] = name
net_size[name] = net_size.get(name, 0) + len(nodes)
comps = {c.get("ref"): (c.findtext("value"), c.findtext("footprint") or "") for c in root.iter("comp")}
errors, notes = [], []
# ---- 1. custom symbols vs datasheet ----------------------------------------------
lib = sexp.parse(open(os.path.join(HW, "rithesh.kicad_sym")).read())
for sym in sexp.find(lib, "symbol"):
name = sym[1]
pins = {}
for unit in sexp.find(sym, "symbol"):
for p in sexp.find(unit, "pin"):
pins[sexp.find1(p, "number")[1]] = norm(sexp.find1(p, "name")[1])
ds = DATASHEET[name]
if set(pins) != set(ds):
errors.append(f"{name}: pin numbers differ from datasheet: extra {set(pins) - set(ds)} missing {set(ds) - set(pins)}")
for num, fn in ds.items():
if num in pins and pins[num] != fn:
errors.append(f"{name}: pin {num} is '{pins[num]}', datasheet says '{fn}'")
notes.append(f"{name}: {len(ds)} pins match the datasheet table")
# ---- 2. symbol pins vs footprint pads --------------------------------------------
fp_cache = {}
for ref, (val, fp) in sorted(comps.items()):
if not fp:
errors.append(f"{ref}: no footprint assigned")
continue
lib_name, fp_name = fp.split(":")
base = HW if lib_name == "rithesh_fp" else FP_DIR
path = os.path.join(base, lib_name + ".pretty", fp_name + ".kicad_mod")
if path not in fp_cache:
mod = sexp.parse(open(path).read())
fp_cache[path] = {str(p[1]) for p in sexp.find(mod, "pad") if str(p[1])}
pads = fp_cache[path]
sym_pins = {pin for (r, pin) in pin_net if r == ref}
if sym_pins - pads:
errors.append(f"{ref} ({fp_name}): symbol pins {sorted(sym_pins - pads)} have no footprint pad")
if pads - sym_pins:
errors.append(f"{ref} ({fp_name}): footprint pads {sorted(pads - sym_pins)} not in symbol")
notes.append("symbol pins = footprint pads for every part (incl. ESP32 and LED project footprints)")
# ---- 3. required connections -------------------------------------------------------
def want(ref, pin, net, why):
got = pin_net.get((ref, pin))
if got != net:
errors.append(f"{ref}.{pin} ({why}) is on '{got}', expected '{net}'")
# power and ground pins
for pin in ["3", "4"]:
want("U2", pin, "VBUS", "eFuse IN")
want("U2", "5", "+5V", "eFuse OUT")
want("U2", "8", "GND", "eFuse GND")
want("U2", "9", "GND", "eFuse exposed pad")
want("U3", "6", "+5V", "LDO IN")
want("U3", "4", "+5V", "LDO EN (always on)")
want("U3", "1", "+3V3", "LDO OUT")
want("U3", "2", "+3V3", "LDO SNS to OUT")
for pin, why in [("3", "LDO GND"), ("5", "LDO GND"), ("7", "LDO thermal pad")]:
want("U3", pin, "GND", why)
want("U4", "3", "+3V3", "ESP32 3V3")
for n in [1, 2, 11, 14] + list(range(36, 54)):
want("U4", str(n), "GND", "ESP32 GND/EPAD" if n == 49 else "ESP32 GND")
want("U5", "5", "+5V", "level shifter VCC")
want("U5", "3", "GND", "level shifter GND")
want("U5", "1", "GND", "level shifter OE (active low = enabled)")
for n in range(1, 108):
want(f"LED{n}", "1", "+5V", "LED VDD")
want(f"LED{n}", "3", "GND", "LED GND")
# USB-C connector: every electrical pin of the 16-pin receptacle
for pin in ["A4", "A9", "B4", "B9"]:
want("J1", pin, "VBUS", "USB-C VBUS")
for pin in ["A1", "A12", "B1", "B12", "S1"]:
want("J1", pin, "GND", "USB-C GND/shield")
want("J1", "A5", "CC1", "USB-C CC1")
want("J1", "B5", "CC2", "USB-C CC2")
for pin in ["A6", "B6"]:
want("J1", pin, "USB_DP", "USB-C D+ (both plug orientations)")
for pin in ["A7", "B7"]:
want("J1", pin, "USB_DN", "USB-C D- (both plug orientations)")
# USB protection chip (ST USBLC6-2SC6: 1/6 = I/O1, 3/4 = I/O2, 5 = VBUS, 2 = GND)
for pin in ["1", "6"]:
want("U1", pin, "USB_DP", "USBLC6 I/O1 on D+")
for pin in ["3", "4"]:
want("U1", pin, "USB_DN", "USBLC6 I/O2 on D-")
want("U1", "5", "VBUS", "USBLC6 VBUS reference")
want("U1", "2", "GND", "USBLC6 GND")
# D+ and D- reach the ESP32 on the right pins (IO19 = D+, IO18 = D-) via R14/R15
want("U4", "27", "USB_DP_MCU", "ESP32 IO19 = USB D+")
want("U4", "26", "USB_DN_MCU", "ESP32 IO18 = USB D-")
# surge diode: pin 1 = cathode (bar in symbol, band side/pad 1 of D_SMA) to VBUS
want("D1", "1", "VBUS", "TVS cathode")
want("D1", "2", "GND", "TVS anode")
# ---- 4. unused pins must be unconnected -------------------------------------------
def unused(ref, pin, why):
net = pin_net.get((ref, pin))
if net is None or net_size.get(net, 0) != 1:
errors.append(f"{ref}.{pin} ({why}) should be unconnected but is on '{net}'")
for pin, why in [("6", "IO3"), ("18", "IO4"), ("20", "IO6"), ("21", "IO7")]:
unused("U4", pin, f"ESP32 {why} spare")
for n in [4, 7, 9, 10, 15, 17, 24, 25, 28, 29, 32, 33, 34, 35]:
unused("U4", str(n), "ESP32 NC pad")
unused("J1", "A8", "USB-C SBU1")
unused("J1", "B8", "USB-C SBU2")
unused("LED107", "2", "last LED data out")
# every other pin must be on a net with at least one other pin
allowed_single = {("U4", p) for p in ["6", "18", "20", "21", "4", "7", "9", "10", "15", "17", "24", "25",
"28", "29", "32", "33", "34", "35"]} | {("J1", "A8"), ("J1", "B8"), ("LED107", "2")}
for key, net in pin_net.items():
if net_size[net] == 1 and key not in allowed_single:
errors.append(f"{key[0]}.{key[1]} is not connected to anything")
for n in notes:
print("OK " + n)
if errors:
print("PROBLEMS:")
for e in errors:
print(" " + e)
sys.exit(1)
print("OK power/ground pins, exposed pads, USB-C pins, USBLC6, surge diode and unused pins all as intended")