board
Solar MPPT Power System
Public Unreviewedby TylerK
A 30 W solar battery charger with regulated 5 V and 3.3 V rails: an STM32G031 running perturb & observe MPPT and a CC/CV charge state machine, steering a TI LM5175 4-switch buck-boost through a single analog control input. Four-layer KiCad board, schematic and PCB generated from Python, with an electrothermal simulation run against the published board file.
← Commit history
Delete kicad/generator-scripts/route_stitch.py
1 file changed
−308
kicad/generator-scripts/route_stitch.pydeleted−308@@ -1,308 +0,0 @@-#!/usr/bin/env python3-"""Simulation-driven follow-up: the electrothermal report's remaining worst-case after the trace-widening fix is /VBATT from RS2.2 (charge-current shunt)-to J2.1 (battery connector) -- 307mV/123mOhm/769mW, on an already-wide 0.6mm-trace. Root cause identified in the report: the routed path is a routing--LENGTH problem, not a width problem (measured resistance implies a ~150mm-routed path vs a 93.8mm straight-line distance -- the multi-drop chain router-visits this net's many other pads in between rather than going direct).--Fix: add ONE additional direct 0.6mm path between these two exact points,-IN ADDITION to (not replacing) the existing chain route. Since both ends are-the same real copper net, the two paths run in parallel electrically,-lowering the effective end-to-end resistance without touching the existing-(already DRC-checked) routing at all.-"""-import re, math, heapq, uuid as U--def uid(): return str(U.uuid4())--PCB_PATH = "/mnt/c/Users/kirkt/Documents/mppt-power-system-mcu/mppt-power-system-mcu.kicad_pcb"-GRID = 0.15-LAYERS = ["F.Cu", "In2.Cu", "B.Cu"]-VIA_PENALTY = 10-SIGNAL_WIDTH = 0.6-VIA_SIZE, VIA_DRILL = 0.4, 0.2--LINKS = [- ("/VBATT", (71.963, 73.0), (165.0, 85.0)),-]---def split_blocks(text, tag):- out = []- i = 0- n = len(text)- needle = '(' + tag- while True:- i = text.find(needle, i)- if i == -1:- break- j = i + len(needle)- if j < n and (text[j].isalnum() or text[j] == '_'):- i = j- continue- depth = 0- k = i- ins = False- esc = False- while k < n:- ch = text[k]- if esc:- esc = False- elif ch == '\\':- esc = True- elif ch == '"':- ins = not ins- elif not ins:- if ch == '(':- depth += 1- elif ch == ')':- depth -= 1- if depth == 0:- k += 1- break- k += 1- out.append(text[i:k])- i = k- return out---text = open(PCB_PATH, errors='replace').read()--pads = []-for blk in split_blocks(text, 'footprint'):- m_at = re.search(- r'\(layer "[^"]*"\)\s*\n\s*\(uuid "[^"]*"\)\s*\n\s*\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', blk)- if not m_at:- continue- fx, fy, frot = float(m_at.group(1)), float(m_at.group(2)), float(m_at.group(3) or 0)- th = math.radians(frot)- cos_t, sin_t = math.cos(th), math.sin(th)- for pblk in split_blocks(blk, 'pad'):- mp = re.match(r'\(pad "(\w*)"\s+(\w+)\s+(\w+)', pblk)- if not mp or not mp.group(1):- continue- m_lat = re.search(r'\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', pblk)- if not m_lat:- continue- lx, ly = float(m_lat.group(1)), float(m_lat.group(2))- m_layers = re.search(r'\(layers ([^)]*)\)', pblk)- layers = m_layers.group(1) if m_layers else ''- any_layer = '*.Cu' in layers- m_net = re.search(r'\(net "([^"]*)"\)', pblk)- netname = m_net.group(1) if m_net else ""- m_size = re.search(r'\(size ([-\d.]+) ([-\d.]+)\)', pblk)- pw, ph = (float(m_size.group(1)), float(m_size.group(2))) if m_size else (0.5, 0.5)- wx = fx + lx * cos_t + ly * sin_t- wy = fy + ly * cos_t - lx * sin_t- pads.append({"x": wx, "y": wy, "net": netname, "any_layer": any_layer, "radius": max(pw, ph) / 2})--print(f"parsed {len(pads)} pads")---def to_grid(x, y):- return (round(x / GRID), round(y / GRID))--half = max(1, round((SIGNAL_WIDTH / 2 + 0.2) / GRID))-obstacle = {}--def build_obstacle(clearance_mm):- obstacle = {}- for p in pads:- gx, gy = to_grid(p["x"], p["y"])- layer_idxs = range(len(LAYERS)) if p["any_layer"] else [0]- pad_inflate = max(1, round((p["radius"] + clearance_mm) / GRID))- for li in layer_idxs:- for dx in range(-pad_inflate, pad_inflate + 1):- for dy in range(-pad_inflate, pad_inflate + 1):- obstacle[(li, gx + dx, gy + dy)] = p["net"] or "?"- return obstacle--obstacle = build_obstacle(0.15)--for sblk in split_blocks(text, 'segment'):- m_start = re.search(r'\(start ([-\d.]+) ([-\d.]+)\)', sblk)- m_end = re.search(r'\(end ([-\d.]+) ([-\d.]+)\)', sblk)- m_layer = re.search(r'\(layer "([^"]*)"\)', sblk)- m_net = re.search(r'\(net "([^"]*)"\)', sblk)- if not (m_start and m_end and m_layer and m_net):- continue- layer = m_layer.group(1)- if layer not in LAYERS:- continue- li = LAYERS.index(layer)- netname = m_net.group(1)- x1, y1 = float(m_start.group(1)), float(m_start.group(2))- x2, y2 = float(m_end.group(1)), float(m_end.group(2))- gx1, gy1 = to_grid(x1, y1)- gx2, gy2 = to_grid(x2, y2)- steps = max(abs(gx2 - gx1), abs(gy2 - gy1), 1)- for s in range(steps + 1):- gx = round(gx1 + (gx2 - gx1) * s / steps)- gy = round(gy1 + (gy2 - gy1) * s / steps)- for dx in range(-half, half + 1):- for dy in range(-half, half + 1):- key = (li, gx + dx, gy + dy)- if key not in obstacle:- obstacle[key] = netname--for vblk in split_blocks(text, 'via'):- m_at = re.search(r'\(at ([-\d.]+) ([-\d.]+)\)', vblk)- m_net = re.search(r'\(net "([^"]*)"\)', vblk)- if not (m_at and m_net):- continue- x, y = float(m_at.group(1)), float(m_at.group(2))- netname = m_net.group(1)- gx, gy = to_grid(x, y)- for li in range(len(LAYERS)):- for dx in range(-half, half + 1):- for dy in range(-half, half + 1):- key = (li, gx + dx, gy + dy)- if key not in obstacle:- obstacle[key] = netname--MOVES = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)]--def cell_free(li, gx, gy, net):- o = obstacle.get((li, gx, gy))- return o is None or o == net--def mark_occ(li, gx, gy, net):- for dx in range(-half, half + 1):- for dy in range(-half, half + 1):- obstacle[(li, gx + dx, gy + dy)] = net--def astar(src, dst, net, margin_cells):- sx, sy = to_grid(*src)- tx, ty = to_grid(*dst)- x0, x1 = min(sx, tx) - margin_cells, max(sx, tx) + margin_cells- y0, y1 = min(sy, ty) - margin_cells, max(sy, ty) + margin_cells- nlayers = len(LAYERS)-- def h(node):- li, gx, gy = node- return math.hypot(gx - tx, gy - ty)-- # start/end forced onto F.Cu (index 0) -- every endpoint here is a real- # SMD/THT pad's exact location, which always includes F.Cu- starts = [(0, sx, sy)]- goals = {(0, tx, ty)}- openq = []- for s in starts:- heapq.heappush(openq, (h(s), 0, s, None))- came, gscore, visited = {}, {s: 0 for s in starts}, set()- while openq:- f, g, node, parent = heapq.heappop(openq)- if node in visited:- continue- visited.add(node)- came[node] = parent- if node in goals:- path = []- cur = node- while cur is not None:- path.append(cur)- cur = came[cur]- path.reverse()- return path- li, gx, gy = node- if not (x0 <= gx <= x1 and y0 <= gy <= y1):- continue- for dxn, dyn in MOVES:- ngx, ngy = gx + dxn, gy + dyn- if not (x0 <= ngx <= x1 and y0 <= ngy <= y1) or not cell_free(li, ngx, ngy, net):- continue- nnode = (li, ngx, ngy)- ng = g + math.hypot(dxn, dyn)- if nnode not in gscore or ng < gscore[nnode]:- gscore[nnode] = ng- heapq.heappush(openq, (ng + h(nnode), ng, nnode, node))- for oli in range(nlayers):- if oli == li or not cell_free(oli, gx, gy, net):- continue- nnode = (oli, gx, gy)- ng = g + VIA_PENALTY- if nnode not in gscore or ng < gscore[nnode]:- gscore[nnode] = ng- heapq.heappush(openq, (ng + h(nnode), ng, nnode, node))- return None---def merge_collinear(pts):- if len(pts) < 3:- return pts- out = [pts[0]]- prev_dir = None- for i in range(1, len(pts)):- dx = pts[i][0] - out[-1][0]- dy = pts[i][1] - out[-1][1]- norm = math.hypot(dx, dy) or 1- d = (dx / norm, dy / norm)- if prev_dir is not None and abs(d[0] - prev_dir[0]) < 1e-6 and abs(d[1] - prev_dir[1]) < 1e-6:- out[-1] = pts[i]- else:- out.append(pts[i])- prev_dir = d- return out---def cell_xy(gx, gy):- return gx * GRID, gy * GRID---all_new = []-still_missing = []-for net_name, (ax, ay), (bx, by) in LINKS:- path = None- for margin in (120, 300, 800):- path = astar((ax, ay), (bx, by), net_name, margin)- if path:- break- if not path:- still_missing.append((net_name, (ax, ay), (bx, by)))- continue- for (li, gx, gy) in path:- mark_occ(li, gx, gy, net_name)- i = 0- n = len(path)- while i < n - 1:- li0 = path[i][0]- j = i- while j + 1 < n and path[j + 1][0] == li0:- j += 1- pts = [cell_xy(path[k][1], path[k][2]) for k in range(i, j + 1)]- if i == 0:- pts[0] = (ax, ay)- if j == n - 1:- pts[-1] = (bx, by)- pts = merge_collinear(pts)- for k in range(len(pts) - 1):- x1, y1 = pts[k]- x2, y2 = pts[k + 1]- if (x1, y1) == (x2, y2):- continue- all_new.append(- f'\t(segment\n\t\t(start {x1:.6f} {y1:.6f})\n\t\t(end {x2:.6f} {y2:.6f})\n'- f'\t\t(width {SIGNAL_WIDTH})\n\t\t(layer "{LAYERS[li0]}")\n\t\t(net "{net_name}")\n'- f'\t\t(uuid "{uid()}")\n\t)')- if j + 1 < n:- _, vx, vy = path[j]- x, y = cell_xy(vx, vy)- all_new.append(- f'\t(via\n\t\t(at {x:.6f} {y:.6f})\n\t\t(size {VIA_SIZE})\n\t\t(drill {VIA_DRILL})\n'- f'\t\t(layers "F.Cu" "B.Cu")\n\t\t(net "{net_name}")\n\t\t(uuid "{uid()}")\n\t)')- i = j + 1--print(f"polish: routed {len(LINKS) - len(still_missing)}/{len(LINKS)} links, "- f"{len(all_new)} new pcb objects")-if still_missing:- print("STILL MISSING:", still_missing)--if all_new:- insertion = '\n'.join(all_new) + '\n'- stripped = text.rstrip()- idx = stripped.rfind('\n')- new_text = stripped[:idx + 1] + insertion + stripped[idx + 1:] + '\n'- open(PCB_PATH, "w").write(new_text)- print("wrote polish routes back to", PCB_PATH)