423 lines
14 KiB
Python
423 lines
14 KiB
Python
#!/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")
|