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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
"""Generate the RITHESH LED Display KiCad 7 schematic.
Run from anywhere: python3 hardware/tools/generate_schematic.py
Writes the .kicad_pro, .kicad_sch sheets, the project symbol library and
sym-lib-table into the hardware/ folder. Every connection is made with a short
wire from the pin to a net label or power symbol, so each pin's net is readable.
"""
import copy
import os
import uuid
import sexp
from sexp import Sym as S
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.dirname(HERE)
PROJECT = "rithesh-led-display"
KICAD_SYMBOLS = "/usr/share/kicad/symbols/"
DATE = "2026-09-23"
REV = "0.1 (prototype)"
NS = uuid.UUID("5f0e6a4e-6d2b-4d7e-9a57-7a1c3c2d9b10")
GRID = 1.27
def g(v):
"""Snap a coordinate to KiCad's 1.27 mm (50 mil) grid."""
return round(round(v / GRID) * GRID, 4)
def U(key):
"""Deterministic UUID so re-running the generator gives stable files."""
return str(uuid.uuid5(NS, key))
ROOT_UUID = U("root")
# ---------------------------------------------------------------- libraries
_lib_cache = {}
def _lib(lib):
if lib not in _lib_cache:
tree = sexp.parse(open(KICAD_SYMBOLS + lib + ".kicad_sym").read())
_lib_cache[lib] = {s[1]: s for s in sexp.find(tree, "symbol")}
return _lib_cache[lib]
def _rename_units(sym, old, new):
for x in sym:
if isinstance(x, list) and x and x[0] == "symbol" and x[1].startswith(old + "_"):
x[1] = new + x[1][len(old):]
def kicad_symbol(lib, name):
"""Copy a KiCad library symbol, flattening 'extends' (derived) symbols."""
src = _lib(lib)[name]
ext = sexp.find1(src, "extends")
if ext:
base = copy.deepcopy(_lib(lib)[ext[1]])
_rename_units(base, ext[1], name)
base[:] = [x for x in base if not (isinstance(x, list) and x and x[0] == "property")]
props = [copy.deepcopy(p) for p in sexp.find(src, "property")]
idx = next(i for i, x in enumerate(base) if isinstance(x, list) and x and x[0] == "symbol")
base[idx:idx] = props
sym = base
else:
sym = copy.deepcopy(src)
sym[1] = f"{lib}:{name}"
return sym
def _font(size=1.27):
return [S("effects"), [S("font"), [S("size"), size, size]]]
def _prop(key, val, x, y, hide=False, size=1.27, justify=None):
eff = _font(size)
if justify:
eff.append([S("justify")] + [S(j) for j in justify.split()])
if hide:
eff.append(S("hide"))
return [S("property"), key, val, [S("at"), x, y, 0], eff]
def custom_symbol(name, ref, value, body, pins, footprint, datasheet, desc, lcsc=""):
"""Build a simple rectangular symbol. pins: (num, name, type, x, y, angle, hidden)."""
x1, y1, x2, y2 = body
sym = [S("symbol"), name,
[S("pin_names"), [S("offset"), 1.016]],
[S("in_bom"), S("yes")], [S("on_board"), S("yes")],
_prop("Reference", ref, x1, y1 + 1.27, justify="left bottom"),
_prop("Value", value, x1, y2 - 1.27, justify="left top"),
_prop("Footprint", footprint, 0, 0, hide=True),
_prop("Datasheet", datasheet, 0, 0, hide=True),
_prop("Description", desc, 0, 0, hide=True)]
if lcsc:
sym.append(_prop("LCSC", lcsc, 0, 0, hide=True))
sym.append([S("symbol"), name + "_0_1",
[S("rectangle"), [S("start"), x1, y1], [S("end"), x2, y2],
[S("stroke"), [S("width"), 0.254], [S("type"), S("default")]],
[S("fill"), [S("type"), S("background")]]]])
unit = [S("symbol"), name + "_1_1"]
for num, pname, ptype, px, py, ang, hidden in pins:
p = [S("pin"), S(ptype), S("line"), [S("at"), px, py, ang], [S("length"), 2.54]]
if hidden:
p.append(S("hide"))
p += [[S("name"), pname, _font()], [S("number"), num, _font()]]
unit.append(p)
sym.append(unit)
return sym
def build_custom_symbols():
syms = {}
# TPS259531 eFuse, TI DSG (WSON-8) package. Pin numbers from TI datasheet SLVSE57C.
syms["TPS259531"] = custom_symbol(
"TPS259531", "U", "TPS259531DSGR", (-10.16, 7.62, 10.16, -7.62),
[("3", "IN", "power_in", -12.7, 5.08, 0, False),
("4", "IN", "passive", -12.7, 5.08, 0, True),
("2", "EN/UVLO", "input", -12.7, 0, 0, False),
("1", "dVdt", "passive", -12.7, -5.08, 0, False),
("5", "OUT", "power_out", 12.7, 5.08, 180, False),
("6", "~{FLT}", "open_collector", 12.7, 0, 180, False),
("7", "ILM", "passive", 12.7, -5.08, 180, False),
("8", "GND", "power_in", 0, -10.16, 90, False),
("9", "PAD", "passive", 0, -10.16, 90, True)],
"Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm",
"https://www.ti.com/lit/ds/symlink/tps2595.pdf",
"2.7-18V 4A eFuse, 5.7V overvoltage clamp, auto-retry", "C2155674")
# TLV76733 fixed 3.3 V LDO, TI DRV (WSON-6) package, fixed-output pinout.
syms["TLV76733DRV"] = custom_symbol(
"TLV76733DRV", "U", "TLV76733DRVR", (-7.62, 5.08, 7.62, -5.08),
[("6", "IN", "power_in", -10.16, 2.54, 0, False),
("4", "EN", "input", -10.16, -2.54, 0, False),
("1", "OUT", "power_out", 10.16, 2.54, 180, False),
("2", "SNS", "input", 10.16, -2.54, 180, False),
("3", "GND", "power_in", 0, -7.62, 90, False),
("5", "GND", "passive", 0, -7.62, 90, True),
("7", "PAD", "passive", 0, -7.62, 90, True)],
"Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm",
"https://www.ti.com/lit/ds/symlink/tlv767.pdf",
"1A low-dropout regulator, fixed 3.3V, 16V input", "C2848334")
# ESP32-C3-MINI-1 module. Pin numbers from Espressif datasheet Table 3-1.
esp = [("3", "3V3", "power_in", -17.78, 20.32, 0, False),
("8", "EN", "input", -17.78, 15.24, 0, False),
("12", "IO0", "bidirectional", -17.78, 10.16, 0, False),
("13", "IO1", "bidirectional", -17.78, 7.62, 0, False),
("5", "IO2", "bidirectional", -17.78, 2.54, 0, False),
("6", "IO3", "bidirectional", -17.78, 0, 0, False),
("18", "IO4", "bidirectional", -17.78, -2.54, 0, False),
("19", "IO5", "bidirectional", -17.78, -7.62, 0, False),
("20", "IO6", "bidirectional", 17.78, 20.32, 180, False),
("21", "IO7", "bidirectional", 17.78, 17.78, 180, False),
("22", "IO8", "bidirectional", 17.78, 12.7, 180, False),
("23", "IO9", "bidirectional", 17.78, 10.16, 180, False),
("16", "IO10", "bidirectional", 17.78, 5.08, 180, False),
("26", "IO18/USB_D-", "bidirectional", 17.78, 0, 180, False),
("27", "IO19/USB_D+", "bidirectional", 17.78, -2.54, 180, False),
("30", "RXD0/IO20", "bidirectional", 17.78, -7.62, 180, False),
("31", "TXD0/IO21", "bidirectional", 17.78, -10.16, 180, False),
("1", "GND", "power_in", 0, -25.4, 90, False)]
for n in [2, 11, 14] + list(range(36, 54)):
esp.append((str(n), "GND", "passive", 0, -25.4, 90, True))
# Module NC pads: hidden, each at its own spot so KiCad does not see them as joined.
for i, n in enumerate([4, 7, 9, 10, 15, 17, 24, 25, 28, 29, 32, 33, 34, 35]):
esp.append((str(n), "NC", "no_connect", -8.89 + i * 1.27, 25.4, 270, True))
syms["ESP32-C3-MINI-1"] = custom_symbol(
"ESP32-C3-MINI-1", "U", "ESP32-C3-MINI-1-N4", (-15.24, 22.86, 15.24, -22.86), esp,
"rithesh_fp:ESP32-C3-MINI-1",
"https://documentation.espressif.com/esp32-c3-mini-1_datasheet_en.pdf",
"ESP32-C3 Wi-Fi module, 4MB flash, PCB antenna (official Espressif footprint)",
"C2838502")
# XL-5050RGBC-2812B: same graphics and pin numbers as KiCad's WS2812B symbol.
xl = kicad_symbol("LED", "WS2812B")
_rename_units(xl, "WS2812B", "XL-5050RGBC-2812B")
xl[1] = "XL-5050RGBC-2812B"
for p in sexp.find(xl, "property"):
if p[1] == "Value":
p[2] = "XL-5050RGBC-2812B"
elif p[1] == "Footprint":
p[2] = "rithesh_fp:LED_XL-5050RGBC-2812B"
elif p[1] == "Datasheet":
p[2] = "https://jlcpcb.com/partdetail/C2843785"
elif p[1] == "Description":
p[2] = "Addressable RGB LED, 5050, 3.5-5.5V, 12mA/color (PROVISIONAL)"
elif p[1] == "Reference":
p[2] = "LED"
syms["XL-5050RGBC-2812B"] = xl
return syms
CUSTOM = build_custom_symbols()
def lib_symbol(lib_id):
lib, name = lib_id.split(":")
if lib == "rithesh":
s = copy.deepcopy(CUSTOM[name])
_rename_units(s, name, name)
s[1] = lib_id
return s
return kicad_symbol(lib, name)
def pin_table(symdef):
"""(number, x, y, angle, type, hidden) for unit 0/1, style 1 pins."""
out = []
for unit in sexp.find(symdef, "symbol"):
parts = unit[1].rsplit("_", 2)
if parts[1] not in ("0", "1") or parts[2] not in ("0", "1"):
continue
for p in sexp.find(unit, "pin"):
at = sexp.find1(p, "at")
out.append((sexp.find1(p, "number")[1], float(at[1]), float(at[2]),
int(float(at[3])), str(p[1]), S("hide") in p))
return out
# ---------------------------------------------------------------- sheets
_pwr_count = [0]
OUTWARD = {0: (-1, 0), 180: (1, 0), 90: (0, 1), 270: (0, -1)}
POWER_ROT_UP = {(0, -1): 0, (0, 1): 180, (-1, 0): 90, (1, 0): 270} # +5V style
POWER_ROT_DOWN = {(0, 1): 0, (0, -1): 180, (1, 0): 90, (-1, 0): 270} # GND style
POWER_LIB = {"GND": "power:GND", "+5V": "power:+5V", "+3V3": "power:+3V3", "VBUS": "power:VBUS"}
class Sheet:
def __init__(self, fname, title, paper, page, comments=()):
self.fname, self.title, self.paper, self.page = fname, title, paper, page
self.comments = comments
self.items, self.libs = [], {}
self.uuid = U("file:" + fname)
self.sheet_uuid = U("sheet:" + fname)
self.path = "/" if fname.startswith(PROJECT) else f"/{ROOT_UUID}/{self.sheet_uuid}"
self.inst_path = f"/{ROOT_UUID}" if self.path == "/" else self.path
self.nets = {}
# -- primitives
def wire(self, x1, y1, x2, y2):
self.items.append([S("wire"), [S("pts"), [S("xy"), x1, y1], [S("xy"), x2, y2]],
[S("stroke"), [S("width"), 0], [S("type"), S("default")]],
[S("uuid"), U(f"{self.fname}:w:{x1}:{y1}:{x2}:{y2}")]])
def text(self, s, x, y, size=1.8, bold=False):
eff = [S("effects"), [S("font"), [S("size"), size, size]] + ([S("bold")] if bold else []),
[S("justify"), S("left"), S("top")]]
self.items.append([S("text"), s, [S("at"), x, y, 0], eff,
[S("uuid"), U(f"{self.fname}:t:{x}:{y}:{s[:20]}")]])
def box(self, x1, y1, x2, y2, title=None):
self.items.append([S("rectangle"), [S("start"), x1, y1], [S("end"), x2, y2],
[S("stroke"), [S("width"), 0.3], [S("type"), S("dash")]],
[S("fill"), [S("type"), S("none")]],
[S("uuid"), U(f"{self.fname}:r:{x1}:{y1}")]])
if title:
self.text(title, x1 + 2, y1 + 2, size=2.5, bold=True)
def label(self, name, x, y, o, glob=False, shape="bidirectional"):
ang = {(1, 0): 0, (-1, 0): 180, (0, -1): 90, (0, 1): 270}[o]
just = "left" if ang in (0, 90) else "right"
key = f"{self.fname}:l:{name}:{x}:{y}"
if glob:
self.items.append([S("global_label"), name, [S("shape"), S(shape)],
[S("at"), x, y, ang], [S("fields_autoplaced")],
[S("effects"), [S("font"), [S("size"), 1.27, 1.27]], [S("justify"), S(just)]],
[S("uuid"), U(key)],
_prop("Intersheetrefs", "${INTERSHEET_REFS}", x, y, hide=True)])
else:
self.items.append([S("label"), name, [S("at"), x, y, ang], [S("fields_autoplaced")],
[S("effects"), [S("font"), [S("size"), 1.27, 1.27]],
[S("justify"), S(just), S("bottom")]],
[S("uuid"), U(key)]])
def no_connect(self, x, y):
self.items.append([S("no_connect"), [S("at"), x, y], [S("uuid"), U(f"{self.fname}:nc:{x}:{y}")]])
def _need(self, lib_id):
if lib_id not in self.libs:
self.libs[lib_id] = lib_symbol(lib_id)
return self.libs[lib_id]
def _instance(self, lib_id, ref, value, x, y, rot, props, pins, dnp=False, fp="",
ref_at=None, val_at=None, hide_ref=False, hide_val=False, in_bom=True):
key = f"{self.fname}:s:{ref}"
node = [S("symbol"), [S("lib_id"), lib_id], [S("at"), x, y, rot], [S("unit"), 1],
[S("in_bom"), S("yes" if in_bom else "no")], [S("on_board"), S("yes")],
[S("dnp"), S("yes" if dnp else "no")], [S("uuid"), U(key)]]
rx, ry = ref_at or (x + 2.54, y - 1.27)
vx, vy = val_at or (x + 2.54, y + 1.27)
node.append(_prop("Reference", ref, rx, ry, hide=hide_ref, justify="left"))
node.append(_prop("Value", value, vx, vy, hide=hide_val, justify="left"))
node.append(_prop("Footprint", fp, x, y, hide=True))
node.append(_prop("Datasheet", props.pop("Datasheet", "~"), x, y, hide=True))
for k, v in props.items():
node.append(_prop(k, v, x, y, hide=True))
for num in sorted({p[0] for p in pins}):
node.append([S("pin"), num, [S("uuid"), U(key + ":p:" + num)]])
node.append([S("instances"), [S("project"), PROJECT,
[S("path"), self.inst_path, [S("reference"), ref], [S("unit"), 1]]]])
self.items.append(node)
def power(self, net, x, y, o):
_pwr_count[0] += 1
ref = "#PWR%03d" % _pwr_count[0]
lib_id = POWER_LIB[net]
self._need(lib_id)
rot = (POWER_ROT_DOWN if net == "GND" else POWER_ROT_UP)[o]
tx, ty = x + 1.27 * o[0], y + 1.27 * o[1]
vx, vy = (x, y + 5.08) if (net == "GND" and o == (0, 1)) else (x + 2.54 * o[0] + 1.27, y + 3.81 * o[1] - 0.5)
self._instance(lib_id, ref, net, x, y, rot, {}, [("1",)], hide_ref=True,
ref_at=(tx, ty), val_at=(vx, vy), in_bom=False)
def pwr_flag(self, net, x, y):
"""PWR_FLAG tells ERC this net is driven (connector/pass-through supplies)."""
x, y = g(x), g(y)
_pwr_count[0] += 1
self._need("power:PWR_FLAG")
self._instance("power:PWR_FLAG", "#FLG%03d" % _pwr_count[0], "PWR_FLAG", x, y, 0, {}, [("1",)],
hide_ref=True, val_at=(x, y - 3.81), in_bom=False)
self.wire(x, y, x, y + 5.08)
self.power(net, x, y + 5.08, (0, 1) if net == "GND" else (0, -1))
# -- the main helper
def part(self, lib_id, ref, value, x, y, nets, fp="", props=None, dnp=False, stub=2.54,
ref_at=None, val_at=None, hide_val=False, in_bom=True):
x, y = g(x), g(y)
symdef = self._need(lib_id)
pins = pin_table(symdef)
props = dict(props or {})
seen = {}
for num, px, py, ang, ptype, hidden in pins:
spec = nets.get(num)
loc = (round(x + px, 3), round(y - py, 3))
if spec is None:
if ptype == "no_connect" or hidden:
continue
raise ValueError(f"{ref}: pin {num} has no net")
if loc in seen:
if seen[loc] != spec:
raise ValueError(f"{ref}: stacked pin {num} net mismatch")
continue
seen[loc] = spec
self.nets.setdefault(spec, []).append(f"{ref}.{num}")
if spec == "X": # connected by a wire drawn by the caller
continue
if spec == "NC":
self.no_connect(*loc)
continue
o = OUTWARD[ang]
ex, ey = round(loc[0] + o[0] * stub, 3), round(loc[1] + o[1] * stub, 3)
self.wire(loc[0], loc[1], ex, ey)
if spec in POWER_LIB:
if o[1] == 0: # sideways pin: bend so the power symbol stands upright
d = (0, 1) if spec == "GND" else (0, -1)
bx, by = ex, round(ey + d[1] * 2.54, 3)
self.wire(ex, ey, bx, by)
self.power(spec, bx, by, d)
else:
self.power(spec, ex, ey, o)
elif spec.startswith("G:"):
self.label(spec[2:], ex, ey, o, glob=True)
elif spec.startswith("L:"):
self.label(spec[2:], ex, ey, o)
else:
raise ValueError(spec)
self._instance(lib_id, ref, value, x, y, 0, props, pins, dnp=dnp, fp=fp,
ref_at=ref_at, val_at=val_at, hide_val=hide_val, in_bom=in_bom)
# -- output
def sheet_symbol(self, x, y, w, h, name):
return [S("sheet"), [S("at"), x, y], [S("size"), w, h], [S("fields_autoplaced")],
[S("stroke"), [S("width"), 0.1524], [S("type"), S("solid")]],
[S("fill"), [S("color"), 0, 0, 0, 0.0]],
[S("uuid"), self.sheet_uuid],
_prop("Sheetname", name, x, y - 0.7, justify="left bottom"),
_prop("Sheetfile", self.fname, x, y + h + 0.6, justify="left top"),
[S("instances"), [S("project"), PROJECT, [S("path"), f"/{ROOT_UUID}", [S("page"), str(self.page)]]]]]
def write(self, extra=()):
tb = [S("title_block"), [S("title"), self.title], [S("date"), DATE], [S("rev"), REV],
[S("company"), "RITHESH LED Display"]]
for i, c in enumerate(self.comments, 1):
tb.append([S("comment"), i, c])
doc = [S("kicad_sch"), [S("version"), 20230121], [S("generator"), S("eeschema")],
[S("uuid"), ROOT_UUID if self.path == "/" else self.uuid],
[S("paper"), self.paper], tb,
[S("lib_symbols")] + list(self.libs.values())]
doc += self.items + list(extra)
if self.path == "/":
doc.append([S("sheet_instances"), [S("path"), "/", [S("page"), "1"]]])
with open(os.path.join(OUT, self.fname), "w") as f:
f.write(sexp.dump(doc) + "\n")
# ---------------------------------------------------------------- footprints / values
R0402 = "Resistor_SMD:R_0402_1005Metric"
C0402 = "Capacitor_SMD:C_0402_1005Metric"
C0603 = "Capacitor_SMD:C_0603_1608Metric"
C0805 = "Capacitor_SMD:C_0805_2012Metric"
CP63 = "Capacitor_SMD:CP_Elec_6.3x7.7"
TP_FP = "TestPoint:TestPoint_Pad_D1.5mm"
SW_FP = "Button_Switch_SMD:SW_Push_1P1T_XKB_TS-1187A"
# JLCPCB/LCSC parts chosen in the component-placement step (in stock 2026-09-23).
LCSC = {
"5.1k 1%": "C25905", "100k 1%": "C25741", "100k": "C25741", "39k 1%": "C25783",
"750R 1%": "C25132", "10k": "C25744", "33R": "C25105", "22R": "C25092", "499R": "C4125",
"100nF": "C1525", "100nF 16V X7R": "C1525", "1uF 25V X5R": "C52923",
"4.7uF 16V X5R": "C19666", "10uF 25V X5R": "C15850",
}
def R(sh, ref, val, x, y, n1, n2, **kw):
props = dict(kw.pop("props", {}) or {})
props.setdefault("LCSC", LCSC[val])
sh.part("Device:R", ref, val, x, y, {"1": n1, "2": n2}, fp=R0402, props=props, **kw)
def C(sh, ref, val, x, y, n1, n2, fp=C0402, **kw):
props = dict(kw.pop("props", {}) or {})
if val in LCSC:
props.setdefault("LCSC", LCSC[val])
sh.part("Device:C", ref, val, x, y, {"1": n1, "2": n2}, fp=fp, props=props, **kw)
def TP(sh, ref, name, x, y, net):
"""Bare copper test pad: not a purchased part, so it is left out of the parts list."""
sh.part("Connector:TestPoint", ref, name, x, y, {"1": net}, fp=TP_FP,
ref_at=(x + 1.27, y - 3.81), val_at=(x + 1.27, y - 1.8), in_bom=False)
# ---------------------------------------------------------------- sheet 2: power
def power_sheet():
sh = Sheet("power.kicad_sch", "Power input, protection and 3.3 V supply", "A3", 2,
("USB-C in -> protection -> eFuse -> +5V -> 3.3 V regulator",
"Values: requirements.md (2026-09-23)"))
sh.box(15, 25, 135, 175, "1. USB-C power input")
sh.part("Connector:USB_C_Receptacle_USB2.0_16P", "J1", "USB-C (TYPE-C-31-M-12)", 45, 90,
{"A4": "VBUS", "A9": "VBUS", "B4": "VBUS", "B9": "VBUS",
"A1": "GND", "A12": "GND", "B1": "GND", "B12": "GND", "S1": "GND",
"A5": "G:CC1", "B5": "G:CC2", "A6": "G:USB_DP", "B6": "G:USB_DP",
"A7": "G:USB_DN", "B7": "G:USB_DN", "A8": "NC", "B8": "NC"},
fp="rithesh_fp:USB_C_Receptacle_HRO_TYPE-C-31-M-12",
props={"LCSC": "C165948", "Rating": "5 A power pins"},
ref_at=(33, 63.5), val_at=(33, 67.5))
R(sh, "R1", "5.1k 1%", 95, 125, "G:CC1", "GND")
R(sh, "R2", "5.1k 1%", 110, 125, "G:CC2", "GND")
sh.pwr_flag("VBUS", 100, 45)
sh.pwr_flag("GND", 120, 45)
sh.text("CC1/CC2: the two 5.1k resistors tell the charger\n"
"'send 5 V'. The same lines go to the ESP32 (GPIO0/1)\n"
"so charger strength can be measured later.\n"
"SBU pins are not used. Shield goes to GND.\n"
"PWR_FLAG symbols only tell the electrical check\n"
"that VBUS and GND are supplied by the connector.", 18, 145, size=1.5)
sh.box(140, 25, 225, 175, "2. Surge + static protection")
sh.part("Diode:SMAJ5.0A", "D1", "SMAJ5.0A", 165, 60, {"1": "VBUS", "2": "GND"},
fp="Diode_SMD:D_SMA", props={"LCSC": "C2925443"},
ref_at=(168, 56), val_at=(160, 66))
sh.part("Power_Protection:USBLC6-2SC6", "U1", "USBLC6-2SC6", 185, 100,
{"1": "G:USB_DP", "6": "G:USB_DP", "3": "G:USB_DN", "4": "G:USB_DN",
"5": "VBUS", "2": "GND"},
fp="Package_TO_SOT_SMD:SOT-23-6", props={"LCSC": "C7519"},
ref_at=(188, 92), val_at=(188, 112))
C(sh, "C1", "4.7uF 16V X5R", 160, 130, "VBUS", "GND", fp=C0603)
sh.text("D1 (surge diode): stays off at 5 V, clamps spikes\n"
"above ~6.4 V (up to ~9.2 V in a big surge).\n"
"Pin 1 = cathode (band) to VBUS.\n"
"U1: guards the USB data lines against static.\n"
"C1 4.7uF: the only capacitor the charger sees at\n"
"plug-in (~5 uF total; USB-C allows 1-10 uF).", 143, 145, size=1.5)
sh.box(230, 25, 330, 175, "3. eFuse: soft start, current limit, 5.7 V clamp")
sh.part("rithesh:TPS259531", "U2", "TPS259531DSGR", 280, 70,
{"3": "VBUS", "4": "VBUS", "2": "L:EN_UVLO", "1": "L:DVDT",
"5": "+5V", "6": "L:~{FLT}", "7": "L:ILM", "8": "GND", "9": "GND"},
fp=CUSTOM["TPS259531"][7][2], props={"LCSC": "C2155674"},
ref_at=(270, 60), val_at=(270, 80.5))
R(sh, "R4", "100k 1%", 245, 110, "VBUS", "L:EN_UVLO")
R(sh, "R5", "39k 1%", 258, 110, "L:EN_UVLO", "GND")
C(sh, "C2", "100nF 16V X7R", 271, 110, "L:DVDT", "GND")
R(sh, "R3", "750R 1%", 290, 110, "L:ILM", "GND")
R(sh, "R6", "100k", 305, 110, "+3V3", "L:~{FLT}")
sh.text("R4/R5: turn on above ~4.3 V, off below ~3.9 V;\n"
"keeps EN under its 7 V limit.\n"
"C2 100nF: output ramps ~0.42 V/ms (~12 ms),\n"
"plug-in current <= ~0.6 A.\n"
"R3 750R: current limit 2.49-2.90 A. ILM voltage\n"
"~0.21 V per amp (test pad TP8).\n"
"~FLT: low = fault (open drain, R6 pull-up).\n"
"Faults restart automatically every ~93 ms.", 233, 132, size=1.5)
sh.box(335, 25, 405, 175, "4. 3.3 V regulator")
sh.part("rithesh:TLV76733DRV", "U3", "TLV76733DRVR", 372, 70,
{"6": "+5V", "4": "+5V", "1": "+3V3", "2": "+3V3", "3": "GND", "5": "GND", "7": "GND"},
fp=CUSTOM["TLV76733DRV"][7][2], props={"LCSC": "C2848334"},
ref_at=(365, 62.5), val_at=(365, 78))
C(sh, "C7", "10uF 25V X5R", 352, 110, "+5V", "GND", fp=C0805)
C(sh, "C8", "10uF 25V X5R", 392, 110, "+3V3", "GND", fp=C0805)
sh.text("5 V in, 3.3 V out for the ESP32 (up to 1 A).\n"
"EN tied to +5V: always on.\n"
"SNS connects to the 3.3 V output.\n"
"C7/C8: 10uF ceramic in and out.", 338, 132, size=1.5)
sh.box(15, 185, 240, 250, "5. Test pads (for prototype measurements)")
tps = [("TP1", "5V_IN", "VBUS"), ("TP2", "5V_OUT", "+5V"), ("TP3", "3V3", "+3V3"),
("TP4", "GND", "GND"), ("TP5", "GND", "GND"), ("TP7", "FLT", "L:~{FLT}"),
("TP8", "ILM", "L:ILM")]
for i, (ref, name, net) in enumerate(tps):
TP(sh, ref, name, 30 + i * 30, 215, net)
sh.text("TP6 (LED data) is on the 'LED signal driver' sheet.", 18, 240, size=1.5)
return sh
# ---------------------------------------------------------------- sheet 3: ESP32
def mcu_sheet():
sh = Sheet("mcu.kicad_sch", "ESP32-C3 Wi-Fi controller and buttons", "A3", 3,
("ESP32-C3-MINI-1-N4 running WLED",
"U4 footprint: official Espressif land pattern"))
sh.box(140, 25, 290, 185, "1. ESP32-C3-MINI-1 module")
sh.part("rithesh:ESP32-C3-MINI-1", "U4", "ESP32-C3-MINI-1-N4", 215, 100,
{"3": "+3V3", "8": "L:EN", "12": "G:CC1", "13": "G:CC2", "5": "L:IO2_STRAP",
"6": "NC", "18": "NC", "19": "L:BTN_ONOFF", "20": "NC", "21": "NC",
"22": "L:IO8_STRAP", "23": "L:BOOT", "16": "G:LED_DATA_3V3",
"26": "L:USB_DN_MCU", "27": "L:USB_DP_MCU", "30": "L:U0RXD", "31": "L:U0TXD",
**{str(n): "GND" for n in [1, 2, 11, 14] + list(range(36, 54))}},
fp=CUSTOM["ESP32-C3-MINI-1"][7][2], props={"LCSC": "C2838502"},
ref_at=(200, 75), val_at=(200, 125))
sh.text("GPIO0/GPIO1: USB-C CC1/CC2 sensing (ADC1).\n"
"GPIO10: LED data (3.3 V) to the level shifter.\n"
"GPIO18/19: built-in USB, through R14/R15 (box 5).\n"
"GPIO20/21: serial debug pads (box 6).\n"
"GPIO3, 4, 6, 7: not used (X marks).\n"
"The antenna end of the module must sit at the board\n"
"edge with no copper underneath (layout rule).", 143, 150, size=1.5)
sh.box(15, 25, 135, 90, "2. 3.3 V decoupling")
C(sh, "C9", "10uF 25V X5R", 50, 55, "+3V3", "GND", fp=C0805)
C(sh, "C10", "100nF", 80, 55, "+3V3", "GND")
sh.text("Place C9/C10 right next to the module's 3V3 pin\n(Espressif reference design).", 18, 75, size=1.5)
sh.box(15, 95, 135, 185, "3. Enable delay + RESET button")
R(sh, "R7", "10k", 40, 120, "+3V3", "L:EN")
C(sh, "C11", "1uF 25V X5R", 60, 120, "L:EN", "GND")
sh.part("Switch:SW_Push", "SW1", "RESET", 95, 120, {"1": "L:EN", "2": "GND"}, fp=SW_FP, props={"LCSC": "C318884"},
ref_at=(90, 114), val_at=(90, 125))
sh.text("R7 + C11 (10k, 1uF): the chip starts only after\n"
"3.3 V is steady (Espressif recommendation).\n"
"SW1 RESET: pulls EN low to restart the chip.\n"
"Silkscreen label: RESET", 18, 150, size=1.5)
sh.box(295, 25, 405, 185, "4. BOOT, ON/OFF buttons + boot pins")
R(sh, "R8", "10k", 310, 55, "+3V3", "L:BOOT")
sh.part("Switch:SW_Push", "SW2", "BOOT", 345, 58, {"1": "L:BOOT", "2": "GND"}, fp=SW_FP, props={"LCSC": "C318884"},
ref_at=(340, 52), val_at=(340, 63))
R(sh, "R11", "10k", 310, 95, "+3V3", "L:BTN_ONOFF")
sh.part("Switch:SW_Push", "SW3", "ON/OFF", 345, 98, {"1": "L:BTN_ONOFF", "2": "GND"}, fp=SW_FP, props={"LCSC": "C318884"},
ref_at=(340, 92), val_at=(340, 103))
R(sh, "R9", "10k", 310, 130, "+3V3", "L:IO8_STRAP")
R(sh, "R10", "10k", 330, 130, "+3V3", "L:IO2_STRAP")
sh.box(15, 190, 200, 250, "5. USB data line resistors (close to U4)")
R(sh, "R14", "22R", 40, 215, "G:USB_DP", "L:USB_DP_MCU")
R(sh, "R15", "22R", 60, 215, "G:USB_DN", "L:USB_DN_MCU")
C(sh, "C13", "DNP", 85, 215, "L:USB_DP_MCU", "GND", dnp=True)
C(sh, "C14", "DNP", 105, 215, "L:USB_DN_MCU", "GND", dnp=True)
sh.text("R14/R15 22R: Espressif recommends reserving series\n"
"resistors (start at 22-33R) on USB D+/D-, near the chip.\n"
"C13/C14: spare capacitor spots to GND,\n"
"NOT FITTED (Espressif: initially unpopulated).\n"
"Change only if USB proves unreliable.", 120, 200, size=1.5)
sh.box(205, 190, 405, 250, "6. Serial debug pads (UART0)")
R(sh, "R16", "499R", 235, 215, "L:U0TXD", "L:U0TXD_TP")
TP(sh, "TP9", "IO20_RX", 260, 215, "L:U0RXD")
TP(sh, "TP10", "IO21_TX", 280, 215, "L:U0TXD_TP")
sh.text("Connect a 3.3 V USB-serial adapter here if USB\n"
"programming or WLED ever fails: adapter TX -> TP9,\n"
"adapter RX -> TP10, adapter GND -> a GND pad.\n"
"R16 499R on TX: Espressif recommendation\n"
"(suppresses harmonics). Never connect 5 V here.", 298, 200, size=1.5)
sh.text("SW2 BOOT (GPIO9): hold while pressing RESET to load\n"
"new software (rescue button). Label: BOOT\n"
"SW3 ON/OFF (GPIO5): WLED button 0, short press =\n"
"display on/off. Label: ON/OFF\n"
"R8/R11: keep the buttons reading 'not pressed'.\n"
"R9/R10: hold boot pins GPIO8/GPIO2 high so the\n"
"chip always starts normally.", 298, 150, size=1.5)
return sh
# ---------------------------------------------------------------- sheet 4: LED driver
def driver_sheet():
sh = Sheet("led_driver.kicad_sch", "LED signal driver (3.3 V -> 5 V)", "A4", 4,
("74AHCT1G125: 3.3 V -> 5 V LED data",))
sh.box(15, 25, 280, 150, "LED data level shifter")
sh.part("74xGxx:74AHCT1G125", "U5", "74AHCT1G125", 140, 85,
{"2": "G:LED_DATA_3V3", "1": "GND", "5": "+5V", "3": "GND", "4": "L:LED_DATA_5V"},
fp="Package_TO_SOT_SMD:SOT-23-5", props={"LCSC": "C7484", "MPN": "SN74AHCT1G125DBVR"},
ref_at=(143, 70), val_at=(143, 98))
R(sh, "R12", "10k", 100, 97, "G:LED_DATA_3V3", "GND")
C(sh, "C12", "100nF", 115, 60, "+5V", "GND")
R(sh, "R13", "33R", 175, 100, "L:LED_DATA_5V", "G:LED_DIN")
TP(sh, "TP6", "LED_DIN", 205, 88, "G:LED_DIN")
sh.text("U5 runs from +5V, so its output is a clean 5 V signal for the LEDs.\n"
"Pin 1 (~OE, enable) is tied to GND = always on.\n"
"R12 10k pull-down: holds the signal low while the ESP32 starts,\n"
"so the LEDs do not flash random colors at plug-in.\n"
"R13 33R: small series resistor that damps ringing on the data line\n"
"(standard practice, added in the schematic step).\n"
"C12 100nF: local decoupling for U5.\n"
"TP6: scope the LED timing here (timing pre-check / prototype test).",
18, 122, size=1.5)
return sh
# ---------------------------------------------------------------- sheet 5: LEDs
LETTERS = [("R", 18), ("I", 11), ("T", 11), ("H", 17), ("E", 18), ("S", 15), ("H", 17)]
def leds_sheet():
sh = Sheet("leds.kicad_sch", "107 LEDs spelling RITHESH", "A1", 5,
("XL-5050RGBC-2812B (C2843785) - PROVISIONAL",
"Data chain: LED_DIN -> LED1 -> ... -> LED107"))
n = 0
y = 55
for letter, count in LETTERS:
sh.text(f"Letter {letter}: LED{n + 1} to LED{n + count} ({count} LEDs)", 25, y - 22, size=2.2, bold=True)
for i in range(count):
n += 1
cx = g(40.64 + i * 40.64)
din = "G:LED_DIN" if n == 1 else ("L:CHAIN_%d" % (n - 1) if i == 0 else "X")
dout = "NC" if n == 107 else ("L:CHAIN_%d" % n if i == count - 1 else "X")
sh.part("rithesh:XL-5050RGBC-2812B", f"LED{n}", "XL-5050RGBC-2812B", cx, y,
{"4": din, "2": dout, "1": "+5V", "3": "GND"},
fp="rithesh_fp:LED_XL-5050RGBC-2812B",
props={"LCSC": "C2843785", "Status": "PROVISIONAL"},
ref_at=(cx + 2.54, y - 12.7), val_at=(cx + 2.54, y + 12.7), hide_val=True)
if i < count - 1:
sh.wire(cx + 7.62, g(y), g(40.64 + (i + 1) * 40.64) - 7.62, g(y))
y += 50
ytop = y - 10
sh.box(15, ytop, 790, ytop + 105, "Per-LED capacitors C101-C207 (100nF each)")
sh.text("C101 sits next to LED1, C102 next to LED2, ... C207 next to LED107 on the PCB.\n"
"Each one steadies its own LED's 5 V supply.", 25, ytop + 8, size=1.8)
for k in range(107):
row, col = divmod(k, 36)
C(sh, f"C{101 + k}", "100nF", 40 + col * 20, ytop + 35 + row * 30, "+5V", "GND",
ref_at=(40 + col * 20 + 1.5, ytop + 35 + row * 30 - 2), hide_val=True)
yb = ytop + 115
sh.box(15, yb, 420, yb + 70, "Bulk capacitors on the LED power rail (220uF 16V, C72496)")
spots = [("C301", "start of LED chain", False), ("C302", "one-third along", True),
("C303", "two-thirds along", True), ("C304", "far end", False)]
for i, (ref, where, dnp) in enumerate(spots):
x = 50 + i * 90
sh.part("Device:C_Polarized", ref, "220uF 16V" + (" DNP" if dnp else ""), x, yb + 38,
{"1": "+5V", "2": "GND"}, fp=CP63, props={"LCSC": "C72496"}, dnp=dnp)
sh.text(("NOT FITTED (DNP) - " if dnp else "FITTED - ") + where, x - 20, yb + 55, size=1.5)
sh.text("+ side to +5V. Print a '+' mark next to every footprint. Fit the two DNP\n"
"capacitors only if prototype testing shows rail dips or capacitor heating.",
430, yb + 10, size=1.8)
return sh
# ---------------------------------------------------------------- root
def root_sheet(children):
root = Sheet(PROJECT + ".kicad_sch", "RITHESH LED Display - overview", "A3", 1,
("107 RGB LEDs spelling RITHESH, Wi-Fi (WLED)",
"Prototype rev - see schematic-review.md",
"Made by tools/generate_schematic.py"))
root.text("RITHESH LED Display - schematic overview", 20, 20, size=4, bold=True)
root.text("How to read this schematic:\n"
"- Each box below is a sheet. Double-click it in KiCad (or turn the PDF page) to open it.\n"
"- Parts connect by NAME: two labels with the same text are the same wire.\n"
"- Flag-shaped labels (e.g. USB_DP) connect across sheets. Plain labels stay on one sheet.\n"
"- +5V, +3V3, VBUS and GND symbols connect everywhere.\n"
"- An X on a pin means 'intentionally not connected'.",
20, 32, size=2)
sheets = [("Power input, protection, 3.3 V", 20, 90), ("ESP32-C3 controller + buttons", 120, 90),
("LED signal driver", 220, 90), ("107 LEDs (RITHESH)", 320, 90)]
extra = []
for sh, (name, x, y) in zip(children, sheets):
extra.append(sh.sheet_symbol(x, y, 80, 30, name))
root.text("Power flow: USB-C (VBUS, 5 V) -> surge diode + ESD -> eFuse TPS259531 -> +5V\n"
" +5V -> 107 LEDs + level shifter; +5V -> TLV76733 -> +3V3 -> ESP32\n\n"
"Signal flow: ESP32 GPIO10 (LED_DATA_3V3) -> 74AHCT1G125 -> LED_DIN -> LED1 -> ... -> LED107\n\n"
"Nets that cross sheets: CC1, CC2, USB_DP, USB_DN, LED_DATA_3V3, LED_DIN\n\n"
"Open items (see schematic-review.md): LED part is PROVISIONAL until the timing pre-check.",
20, 140, size=2)
root.write(extra)
def project_files():
# Merge into an existing project file so board design rules saved by KiCad are kept.
import json
pro_path = os.path.join(OUT, PROJECT + ".kicad_pro")
pro = json.load(open(pro_path)) if os.path.exists(pro_path) else {}
pro.setdefault("meta", {}).update({"filename": PROJECT + ".kicad_pro", "version": 1})
pro["sheets"] = [[ROOT_UUID, ""]]
with open(pro_path, "w") as f:
json.dump(pro, f, indent=2)
f.write("\n")
lib = [S("kicad_symbol_lib"), [S("version"), 20220914], [S("generator"), S("rithesh_gen")]]
lib += [copy.deepcopy(s) for s in CUSTOM.values()]
with open(os.path.join(OUT, "rithesh.kicad_sym"), "w") as f:
f.write(sexp.dump(lib) + "\n")
with open(os.path.join(OUT, "sym-lib-table"), "w") as f:
f.write('(sym_lib_table\n (version 7)\n (lib (name "rithesh")(type "KiCad")'
'(uri "${KIPRJMOD}/rithesh.kicad_sym")(options "")(descr "RITHESH project symbols"))\n)\n')
def main():
children = [power_sheet(), mcu_sheet(), driver_sheet(), leds_sheet()]
for sh in children:
sh.write()
root_sheet(children)
project_files()
print("wrote", len(children) + 1, "sheets to", OUT)
if __name__ == "__main__":
main()