#!/usr/bin/env python3 """Generate KiCad 7 PCB for the standalone oscillator module (min board). Flow: J1(coil) -> TVS1 -> T1 -> Colpitts(Q1+C1/C2/Cc2) -> LM393(U1) -> F_OUT Layout: 46x28mm, VCC rail top (y=25.5), GND pour bottom, 2-layer. Routing: grid A* router (0.5mm), 0.25mm tracks, 0.2mm clearance. """ import os, math, heapq, glob import pcbnew from pcbnew import (VECTOR2I, FromMM, F_Cu, B_Cu, F_SilkS, F_CrtYd, Edge_Cuts, PAD_SHAPE_ROUNDRECT, PAD_ATTRIB_SMD) BASE = os.path.dirname(os.path.abspath(__file__)) PCB = os.path.join(BASE, "osc-module.kicad_pcb") FP = "/usr/share/kicad/footprints" MM = lambda v: int(FromMM(v)) PX = lambda x, y: VECTOR2I(MM(x), MM(y)) # ---------------- board ---------------- board = pcbnew.BOARD() ds = board.GetDesignSettings() ds.m_MinClearance = MM(0.2) ds.m_TrackMinWidth = MM(0.25) ds.m_MinThroughDrill = MM(0.4) ds.m_ViasMinSize = MM(0.8) BOARD_W, BOARD_H = 46.0, 28.0 # ---------------- nets ---------------- def make_net(name): n = pcbnew.NETINFO_ITEM(board, name) board.Add(n) return n NETS = {} for nm in ["COIL_A", "COIL_B", "BASE", "COLLECTOR", "EMITTER", "COMP_IN", "VREF", "F_OUT", "VCC", "GND"]: NETS[nm] = make_net(nm) def nc(nm): return NETS[nm].GetNetCode() # ---------------- footprints ---------------- def load_fp(lib, name): fp = pcbnew.FootprintLoad(os.path.join(FP, lib), name) if fp is None: raise RuntimeError(f"footprint not found: {lib}:{name}") return fp def place(fp, ref, value, x, y, rot=0): fp.SetReference(ref) fp.SetValue(value) fp.SetPosition(PX(x, y)) fp.SetOrientationDegrees(rot) board.Add(fp) return fp place(load_fp("Connector_PinHeader_2.54mm.pretty", "PinHeader_1x02_P2.54mm_Vertical"), "J1", "接线圈", 4.5, 21.5) place(load_fp("Diode_SMD.pretty", "D_SOD-123"), "TVS1", "TVS", 9.0, 21.5) place(load_fp("Capacitor_SMD.pretty", "C_0603_1608Metric"), "C1", "100~470p", 20.5, 20.0, 180) place(load_fp("Package_TO_SOT_SMD.pretty", "SOT-23-3"), "Q1", "S9018", 25.5, 21.5) place(load_fp("Capacitor_SMD.pretty", "C_0603_1608Metric"), "Cc2", "10p", 30.5, 20.0) place(load_fp("Capacitor_SMD.pretty", "C_0603_1608Metric"), "C2", "100p", 27.5, 23.5, 90) place(load_fp("Resistor_SMD.pretty", "R_0603_1608Metric"), "Re", "1k", 30.0, 23.5, 90) place(load_fp("Resistor_SMD.pretty", "R_0603_1608Metric"), "Rb1", "100k", 20.5, 16.5, 180) place(load_fp("Resistor_SMD.pretty", "R_0603_1608Metric"), "Rb2", "47k", 22.7, 16.5, 180) place(load_fp("Resistor_SMD.pretty", "R_0603_1608Metric"), "Rc", "2.2k", 29.5, 16.5, 180) place(load_fp("Package_SO.pretty", "SOIC-8_3.9x4.9mm_P1.27mm"), "U1", "LM393", 33.0, 6.0) place(load_fp("Resistor_SMD.pretty", "R_0603_1608Metric"), "R3", "10k", 27.5, 2.0, 90) place(load_fp("Resistor_SMD.pretty", "R_0603_1608Metric"), "R4", "10k", 29.0, 2.0, 90) place(load_fp("Resistor_SMD.pretty", "R_0603_1608Metric"), "R7", "10k上拉", 38.5, 4.5) place(load_fp("Connector_PinHeader_2.54mm.pretty", "PinHeader_1x03_P2.54mm_Vertical"), "J2", "VCC/GND/F_OUT", 40.5, 6.0) # custom T1: 4-pad SMD matching transformer t1 = pcbnew.FOOTPRINT(board) t1.SetReference("T1") t1.SetValue("XFMR 1:1") t1.SetPosition(PX(15.0, 21.5)) for i, (dx, dy) in enumerate([(-2.5, -1.5), (-2.5, 1.5), (2.5, -1.5), (2.5, 1.5)], 1): pad = pcbnew.PAD(t1) pad.SetName(str(i)) pad.SetShape(PAD_SHAPE_ROUNDRECT) pad.SetSize(PX(1.6, 1.6)) pad.SetPosition(PX(dx, dy)) pad.SetLayerSet(pcbnew.PAD_SMD) pad.SetAttribute(PAD_ATTRIB_SMD) pad.SetRoundRectRadiusRatio(0.25) t1.Add(pad) for layer, x1, y1, x2, y2 in [(F_CrtYd, -3.4, -2.4, 3.4, 2.4), (F_SilkS, -3.0, -2.0, 3.0, 2.0)]: s = pcbnew.FP_SHAPE(t1) s.SetShape(pcbnew.SHAPE_T_RECT) s.SetStart(PX(x1, y1)) s.SetEnd(PX(x2, y2)) s.SetLayer(layer) s.SetStrokeWidth(MM(0.15)) t1.Add(s) board.Add(t1) # mounting holes (2x, non-plated M3) holes = [(6.0, 4.0), (42.0, 22.0)] for hx, hy in holes: name = "MountingHole_3.2mm_M3" cand = glob.glob(os.path.join(FP, "MountingHole.pretty", name + ".kicad_mod")) if not cand: cand = glob.glob(os.path.join(FP, "MountingHole.pretty", "*3.2mm*.kicad_mod")) if not cand: raise RuntimeError("no mounting hole footprint") place(load_fp("MountingHole.pretty", os.path.basename(cand[0])[:-9]), "H", "M3", hx, hy) # ---------------- pad net map ---------------- PADMAP = { "J1": {"1": "COIL_A", "2": "COIL_B"}, "TVS1": {"1": "COIL_A", "2": "COIL_B"}, "T1": {"1": "COIL_A", "2": "COIL_B", "3": "BASE", "4": "GND"}, "C1": {"1": "BASE", "2": "GND"}, "Q1": {"1": "BASE", "2": "EMITTER", "3": "COLLECTOR"}, "Cc2": {"1": "COLLECTOR", "2": "COMP_IN"}, "C2": {"1": "EMITTER", "2": "GND"}, "Re": {"1": "EMITTER", "2": "GND"}, "Rb1": {"1": "BASE", "2": "VCC"}, "Rb2": {"1": "BASE", "2": "GND"}, "Rc": {"1": "VCC", "2": "COLLECTOR"}, "U1": {"1": "F_OUT", "2": "VREF", "3": "COMP_IN", "4": "GND", "5": "GND", "6": "GND", "7": "GND", "8": "VCC"}, "R3": {"1": "VREF", "2": "VCC"}, "R4": {"1": "GND", "2": "VREF"}, "R7": {"1": "F_OUT", "2": "VCC"}, "J2": {"1": "VCC", "2": "GND", "3": "F_OUT"}, } fps = {fp.GetReference(): fp for fp in board.GetFootprints()} net_pads = {nm: [] for nm in NETS} for ref, pm in PADMAP.items(): for pname, net in pm.items(): pad = fps[ref].FindPadByNumber(pname) pad.SetNetCode(nc(net)) pos = pad.GetPosition() net_pads[net].append((ref, pname, pos.x, pos.y)) def mmpos(p): return (p.x / 1e6, p.y / 1e6) # ---------------- grid A* router ---------------- GRID = 0.5 NX = int(BOARD_W / GRID) NY = int(BOARD_H / GRID) blocked = bytearray(NX * NY) def cell(x, y): return (int(round(x / GRID)), int(round(y / GRID))) def idx(i, j): return j * NX + i def inb(i, j): return 0 <= i < NX and 0 <= j < NY def block_circle(cx, cy, r): i0, j0 = cell(cx - r, cy - r) i1, j1 = cell(cx + r, cy + r) for j in range(max(0, j0), min(NY, j1 + 1)): for i in range(max(0, i0), min(NX, i1 + 1)): dx = i * GRID - cx dy = j * GRID - cy if dx * dx + dy * dy <= r * r: blocked[idx(i, j)] = 1 def block_rect(x0, y0, x1, y1): i0, j0 = cell(x0, y0) i1, j1 = cell(x1, y1) for j in range(max(0, j0), min(NY, j1 + 1)): for i in range(max(0, i0), min(NX, i1 + 1)): blocked[idx(i, j)] = 1 def dist_pt_seg(px, py, x0, y0, x1, y1): dx, dy = x1 - x0, y1 - y0 L2 = dx * dx + dy * dy if L2 == 0: return math.hypot(px - x0, py - y0) t = max(0.0, min(1.0, ((px - x0) * dx + (py - y0) * dy) / L2)) return math.hypot(px - (x0 + t * dx), py - (y0 + t * dy)) def block_segment(x0, y0, x1, y1, pad=0.4): i0, j0 = cell(min(x0, x1) - pad, min(y0, y1) - pad) i1, j1 = cell(max(x0, x1) + pad, max(y0, y1) + pad) for j in range(max(0, j0), min(NY, j1 + 1)): for i in range(max(0, i0), min(NX, i1 + 1)): if dist_pt_seg(i * GRID, j * GRID, x0, y0, x1, y1) <= pad: blocked[idx(i, j)] = 1 # block everything that already exists for fp in board.GetFootprints(): for pad in fp.Pads(): if not (pad.IsOnLayer(F_Cu) or pad.IsOnLayer(B_Cu)): continue bb = pad.GetBoundingBox() block_rect(bb.GetLeft() / 1e6 - 0.4, bb.GetTop() / 1e6 - 0.4, bb.GetRight() / 1e6 + 0.4, bb.GetBottom() / 1e6 + 0.4) # board edge margin block_rect(-5, -5, 1.2, BOARD_H + 5) block_rect(BOARD_W - 1.2, -5, BOARD_W + 5, BOARD_H + 5) block_rect(-5, -5, BOARD_W + 5, 1.2) block_rect(-5, BOARD_H - 1.2, BOARD_W + 5, BOARD_H + 5) for hx, hy in holes: block_circle(hx, hy, 2.0) tracks = [] def add_track(x0, y0, x1, y1, net): t = pcbnew.PCB_TRACK(board) t.SetStart(PX(x0, y0)) t.SetEnd(PX(x1, y1)) t.SetWidth(MM(0.25)) t.SetLayer(F_Cu) t.SetNetCode(nc(net)) board.Add(t) tracks.append(t) block_segment(x0, y0, x1, y1) def unblock_net(net, r=0.45): for (ref, pname, x, y) in net_pads[net]: block_circle_free(x / 1e6, y / 1e6, r) def block_circle_free(cx, cy, r): """unblock cells within r of (cx,cy)""" i0, j0 = cell(cx - r, cy - r) i1, j1 = cell(cx + r, cy + r) for j in range(max(0, j0), min(NY, j1 + 1)): for i in range(max(0, i0), min(NX, i1 + 1)): dx = i * GRID - cx dy = j * GRID - cy if dx * dx + dy * dy <= r * r: blocked[idx(i, j)] = 0 def astar(sx, sy, gx, gy): si, sj = cell(sx, sy) gi, gj = cell(gx, gy) if not inb(si, sj) or not inb(gi, gj): return None if blocked[idx(si, sj)] or blocked[idx(gi, gj)]: return None open_h = [(0.0, si, sj)] g = {(si, sj): 0.0} came = {} closed = set() while open_h: f, i, j = heapq.heappop(open_h) if (i, j) == (gi, gj): path = [(gi, gj)] while (i, j) in came: (i, j) = came[(i, j)] path.append((i, j)) path.reverse() return path if (i, j) in closed: continue closed.add((i, j)) for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)): ni, nj = i + di, j + dj if not inb(ni, nj) or blocked[idx(ni, nj)]: continue ng = g[(i, j)] + 1.0 if (ni, nj) not in g or ng < g[(ni, nj)]: g[(ni, nj)] = ng came[(ni, nj)] = (i, j) heapq.heappush(open_h, (ng + abs(ni - gi) + abs(nj - gj), ni, nj)) return None def path_to_tracks(path, net): # merge collinear runs runs = [] cur = [path[0]] for p in path[1:]: if cur and (p[0] == cur[-1][0] or p[1] == cur[-1][1]): cur.append(p) else: runs.append(cur) cur = [p] runs.append(cur) for run in runs: x0, y0 = run[0][0] * GRID, run[0][1] * GRID x1, y1 = run[-1][0] * GRID, run[-1][1] * GRID if (x0, y0) != (x1, y1): add_track(x0, y0, x1, y1, net) def route_net(net, order=None): pads = [(p[2] / 1e6, p[3] / 1e6) for p in net_pads[net]] if order: pads = [pads[i] for i in order] unblock_net(net) cur = pads[0] for nxt in pads[1:]: path = astar(cur[0], cur[1], nxt[0], nxt[1]) if path is None: print(f"!! A* FAILED: {net} {cur} -> {nxt}") return False path_to_tracks(path, net) cur = nxt return True # ---- VCC rail (top) ---- add_track(2.0, 25.5, 44.0, 25.5, "VCC") # ---- signal nets ---- ok = True ok &= route_net("COIL_A") ok &= route_net("COIL_B") ok &= route_net("EMITTER") ok &= route_net("COLLECTOR") ok &= route_net("BASE") ok &= route_net("COMP_IN") ok &= route_net("VREF") ok &= route_net("F_OUT") # ---- VCC drops (chain from rail) ---- vcc_pads = [(p[2] / 1e6, p[3] / 1e6) for p in net_pads["VCC"]] vcc_chain = [(43.0, 25.5)] + vcc_pads + [(3.0, 25.5)] unblock_net("VCC") cur = vcc_chain[0] for nxt in vcc_chain[1:]: path = astar(cur[0], cur[1], nxt[0], nxt[1]) if path is None: print(f"!! A* FAILED: VCC {cur} -> {nxt}") ok = False else: path_to_tracks(path, "VCC") cur = nxt # ---- GND vias + stubs ---- gnd_vias = [] for (ref, pname, x, y) in net_pads["GND"]: pad = fps[ref].FindPadByNumber(pname) if pad.GetAttribute() != PAD_ATTRIB_SMD: continue # THT connects to pour through hole px, py = x / 1e6, y / 1e6 # chain the three unused U1 comparator pads together first vx, vy = None, None for ox, oy in [(1.6, 0), (-1.6, 0), (0, 1.6), (0, -1.6)]: cx0, cy0 = px + ox, py + oy i0, j0 = cell(cx0, cy0) if not inb(i0, j0): continue free = True for j in range(max(0, j0 - 2), min(NY, j0 + 3)): for i in range(max(0, i0 - 2), min(NX, i0 + 3)): if blocked[idx(i, j)]: free = False break if not free: break if free: vx, vy = cx0, cy0 break if vx is None: print(f"!! no via spot for GND {ref}.{pname}") ok = False continue via = pcbnew.PCB_VIA(board) via.SetPosition(PX(vx, vy)) via.SetDrill(MM(0.4)) via.SetWidth(MM(0.8)) via.SetLayerPair(F_Cu, B_Cu) via.SetNetCode(nc("GND")) board.Add(via) block_circle(vx, vy, 0.7) # route stub pad -> via unblock_net("GND") path = astar(px, py, vx, vy) if path is None: print(f"!! A* FAILED: GND stub {ref}.{pname}") ok = False else: path_to_tracks(path, "GND") # ---- GND pour (bottom) ---- z = pcbnew.ZONE(board) z.SetLayer(B_Cu) z.SetNetCode(nc("GND")) z.SetIsFilled(False) ol = z.Outline() for (ox, oy) in [(1.0, 1.0), (BOARD_W - 1.0, 1.0), (BOARD_W - 1.0, BOARD_H - 1.0), (1.0, BOARD_H - 1.0), (1.0, 1.0)]: ol.Append(PX(ox, oy)) board.Add(z) # ---- edge cuts ---- e = pcbnew.PCB_SHAPE(board) e.SetShape(pcbnew.SHAPE_T_RECT) e.SetStart(PX(0, 0)) e.SetEnd(PX(BOARD_W, BOARD_H)) e.SetLayer(Edge_Cuts) board.Add(e) # ---- silk text ---- for txt, x, y, sz in [("OSC-MOD v1.0", 23.0, 27.2, 1.2), ("J1: COIL", 4.5, 26.2, 0.8), ("J2: 1=VCC 2=GND 3=F_OUT", 33.5, 12.5, 0.8)]: t = pcbnew.PCB_TEXT(board) t.SetText(txt) t.SetPosition(PX(x, y)) t.SetLayer(F_SilkS) t.SetTextSize(PX(sz, sz)) t.SetTextThickness(MM(0.15)) board.Add(t) board.Save(PCB) print("saved", PCB) print("nets routed OK" if ok else "!!! SOME NETS FAILED !!!") # ---- zone fill ---- filler = pcbnew.ZONE_FILLER(board) filler.Fill([z]) board.Save(PCB) print("zone filled & re-saved")