"""工程封装库:从参考设计的 .kicad_pcb 按位号提取封装 / 从 KiCad 官方库拷贝。 **必须用 KiCad 自带的 python 跑**(要 import pcbnew):~/skills_env.md 的 KICAD_PYTHON。 from schgen.fplib import FpLib lib = FpLib(r"E:\\...\\lib\\myboard.pretty") lib.from_pcb(r"E:\\...\\SMOD_V1.1.kicad_pcb", [("WSON-10-0R5", "U703", "TPS7A9201")]) lib.from_kicad([("SOT-23", "Package_TO_SOT_SMD", "SOT-23", "Q")]) lib.report() 取封装的顺序(G-0033):① 项目已有参考设计 ② KiCad 官方库(本地与线上)③ 第三方 ④ 按 datasheet 自己画。厂商私有 case style 往往就是某个标准封装,先看 datasheet 末页。 pcbnew swig 的坑(G-0038):便捷函数 FootprintLoad() 不能用,要走 PCB_IO_MGR 插件; 对 BOARD Remove() 过 FOOTPRINT 之后 swig 类型注册会坏,所以这里只读板子、不改板子。 """ import os import pcbnew MIN_TXT_H = pcbnew.FromMM(0.8) # 丝印下限(嘉立创):字高 0.8 / 笔宽 0.15 MIN_TXT_W = pcbnew.FromMM(0.15) class FpLib: def __init__(self, pretty): self.out = pretty os.makedirs(pretty, exist_ok=True) self.io = pcbnew.PCB_IO_MGR.FindPlugin(pcbnew.PCB_IO_MGR.KICAD_SEXP) self.ok, self.miss = [], [] self._boards = {} @staticmethod def fixup(fp): # 无编号且孔环为 0 的"焊盘"其实是机械定位孔,改成 NPTH,否则 DRC 报 annular_width for pad in fp.Pads(): if pad.GetNumber(): continue d, s = pad.GetDrillSize(), pad.GetSize() if d.x and d.x >= s.x - 1 and d.y >= s.y - 1: pad.SetAttribute(pcbnew.PAD_ATTRIB_NPTH) pad.SetLayerSet(pcbnew.PAD.UnplatedHoleMask()) texts = [fp.Reference(), fp.Value()] for it in fp.GraphicalItems(): if it.GetClass() in ("PCB_TEXT", "FP_TEXT"): texts.append(it) for it in texts: if it.GetTextHeight() < MIN_TXT_H: it.SetTextSize(pcbnew.VECTOR2I(MIN_TXT_H, MIN_TXT_H)) if it.GetTextThickness() < MIN_TXT_W: it.SetTextThickness(MIN_TXT_W) def save(self, fp, name, fix=None): fp.SetOrientationDegrees(0) # 板上实例还原成库里的朝向 if fp.IsFlipped(): fp.Flip(fp.GetPosition(), pcbnew.FLIP_DIRECTION_TOP_BOTTOM if hasattr(pcbnew, "FLIP_DIRECTION_TOP_BOTTOM") else False) fp.SetPosition(pcbnew.VECTOR2I(0, 0)) fp.SetFPID(pcbnew.LIB_ID("", name)) fp.SetReference("REF**") fp.SetValue(name) fp.SetPath(pcbnew.KIID_PATH()) self.fixup(fp) if fix: fix(fp) self.io.FootprintSave(self.out, fp) return sorted({p.GetNumber() for p in fp.Pads()}, key=lambda s: (len(s), s)) def from_pcb(self, pcb, items, fix=None): """items: [(目标封装名, 参考板上的位号, 说明), ...]""" if pcb not in self._boards: print("载入 %s ..." % pcb) self._boards[pcb] = pcbnew.LoadBoard(pcb) brd = self._boards[pcb] for name, ref, note in items: fp = brd.FindFootprintByReference(ref) if fp is None: self.miss.append((name, "%s 里找不到位号 %s" % (os.path.basename(pcb), ref))) continue src = "%s:%s(%s)" % (os.path.basename(pcb), fp.GetFPID().GetUniStringLibItemName(), ref) self.ok.append((name, src, self.save(fp, name, fix), note)) def from_kicad(self, items, fp_dir=None, fix=None): """items: [(目标封装名, 库名, 库内封装名, 说明), ...];目标名给 None 表示沿用原名。""" from .env import kicad_footprint_dir fp_dir = fp_dir or kicad_footprint_dir() for name, lib, src, note in items: try: fp = self.io.FootprintLoad(os.path.join(fp_dir, lib + ".pretty"), src) if fp is None: raise RuntimeError("FootprintLoad 返回 None") self.ok.append((name or src, "KiCad:%s/%s" % (lib, src), self.save(fp, name or src, fix), note)) except Exception as e: self.miss.append((name or src, "%s: %s" % (type(e).__name__, e))) def add_courtyard(self, name, rect=None, margin=0.25, silk_rect=False): """给库里的封装补 F.CrtYd(参考设计提来的常常没有,DRC 就查不到它和别人重叠)。 rect=(x0, y0, x1, y1) 给实物外形(封装本地坐标,mm,如简牛护套);不给就按焊盘+丝印外框。 silk_rect=True 时同时在 F.SilkS 画出这个外形。已有 courtyard 的先清掉再画。""" fp = self.io.FootprintLoad(self.out, name) for g in list(fp.GraphicalItems()): if g.GetLayer() == pcbnew.F_CrtYd: fp.Remove(g) if rect is None: xs, ys = [], [] for p in fp.Pads(): bb = p.GetBoundingBox() xs += [bb.GetX(), bb.GetRight()]; ys += [bb.GetY(), bb.GetBottom()] for g in fp.GraphicalItems(): if g.GetLayer() in (pcbnew.F_SilkS, pcbnew.F_Fab) and g.GetClass() != "PCB_TEXT": bb = g.GetBoundingBox() xs += [bb.GetX(), bb.GetRight()]; ys += [bb.GetY(), bb.GetBottom()] rect = (pcbnew.ToMM(min(xs)), pcbnew.ToMM(min(ys)), pcbnew.ToMM(max(xs)), pcbnew.ToMM(max(ys))) x0, y0, x1, y1 = rect[0] - margin, rect[1] - margin, rect[2] + margin, rect[3] + margin layers = [pcbnew.F_CrtYd] + ([pcbnew.F_SilkS] if silk_rect else []) for lay in layers: s = pcbnew.PCB_SHAPE(fp) s.SetShape(pcbnew.SHAPE_T_RECT) s.SetStart(pcbnew.VECTOR2I(pcbnew.FromMM(x0 if lay == pcbnew.F_CrtYd else rect[0]), pcbnew.FromMM(y0 if lay == pcbnew.F_CrtYd else rect[1]))) s.SetEnd(pcbnew.VECTOR2I(pcbnew.FromMM(x1 if lay == pcbnew.F_CrtYd else rect[2]), pcbnew.FromMM(y1 if lay == pcbnew.F_CrtYd else rect[3]))) s.SetLayer(lay) s.SetWidth(pcbnew.FromMM(0.05 if lay == pcbnew.F_CrtYd else 0.15)) fp.Add(s) fp.Reference().SetTextAngleDegrees(0) # 参考设计里常是 180°,看着像镜像 self.io.FootprintSave(self.out, fp) print(" courtyard %-20s (%.2f,%.2f)-(%.2f,%.2f)" % (name, x0, y0, x1, y1)) def report(self): print("\n写出 %s" % self.out) for name, src, pads, note in self.ok: ps = ",".join(pads) if len(pads) <= 12 else "%s..%s (%d)" % (pads[0], pads[-1], len(pads)) print(" %-34s pads=%-22s <- %-40s %s" % (name, ps, src, note)) for name, why in self.miss: print(" !! %-31s %s" % (name, why)) return 0 if not self.miss else 1