第18章-二次开发实战案例
第十八章:二次开发实战案例
18.1 案例一:自定义文件导出器
18.1.1 需求分析
创建一个将PCB设计导出为自定义格式的插件,用于与其他系统集成。
18.1.2 实现代码
import pcbnew
import json
import wx
class CustomExportPlugin(pcbnew.ActionPlugin):
def defaults(self):
self.name = "导出自定义格式"
self.category = "导出"
self.description = "将PCB导出为JSON格式"
self.show_toolbar_button = True
def Run(self):
board = pcbnew.GetBoard()
# 选择保存路径
dlg = wx.FileDialog(None, "保存导出文件", "", "",
"JSON files (*.json)|*.json",
wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
if dlg.ShowModal() == wx.ID_OK:
filepath = dlg.GetPath()
self.export_to_json(board, filepath)
wx.MessageBox(f"导出成功: {filepath}", "完成")
dlg.Destroy()
def export_to_json(self, board, filepath):
"""导出为JSON格式"""
data = {
'board_info': self.get_board_info(board),
'footprints': self.get_footprints(board),
'tracks': self.get_tracks(board),
'vias': self.get_vias(board),
'zones': self.get_zones(board),
'nets': self.get_nets(board)
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def get_board_info(self, board):
"""获取板信息"""
bbox = board.GetBoardEdgesBoundingBox()
return {
'filename': board.GetFileName(),
'width_mm': pcbnew.ToMM(bbox.GetWidth()),
'height_mm': pcbnew.ToMM(bbox.GetHeight()),
'layer_count': board.GetCopperLayerCount()
}
def get_footprints(self, board):
"""获取封装列表"""
footprints = []
for fp in board.GetFootprints():
pos = fp.GetPosition()
footprints.append({
'reference': fp.GetReference(),
'value': fp.GetValue(),
'footprint': str(fp.GetFPID().GetLibItemName()),
'x_mm': pcbnew.ToMM(pos.x),
'y_mm': pcbnew.ToMM(pos.y),
'rotation': fp.GetOrientationDegrees(),
'layer': 'top' if fp.GetLayer() == pcbnew.F_Cu else 'bottom',
'is_smd': bool(fp.GetAttributes() & pcbnew.FP_SMD)
})
return footprints
def get_tracks(self, board):
"""获取走线列表"""
tracks = []
for track in board.GetTracks():
if track.GetClass() == "PCB_TRACK":
start = track.GetStart()
end = track.GetEnd()
tracks.append({
'start_x': pcbnew.ToMM(start.x),
'start_y': pcbnew.ToMM(start.y),
'end_x': pcbnew.ToMM(end.x),
'end_y': pcbnew.ToMM(end.y),
'width_mm': pcbnew.ToMM(track.GetWidth()),
'layer': track.GetLayerName(),
'net': track.GetNetname()
})
return tracks
def get_vias(self, board):
"""获取过孔列表"""
vias = []
for track in board.GetTracks():
if track.GetClass() == "PCB_VIA":
pos = track.GetPosition()
vias.append({
'x_mm': pcbnew.ToMM(pos.x),
'y_mm': pcbnew.ToMM(pos.y),
'diameter_mm': pcbnew.ToMM(track.GetWidth()),
'drill_mm': pcbnew.ToMM(track.GetDrill()),
'net': track.GetNetname()
})
return vias
def get_zones(self, board):
"""获取覆铜区域"""
zones = []
for zone in board.Zones():
zones.append({
'net': zone.GetNetname(),
'layer': zone.GetLayerName(),
'priority': zone.GetPriority()
})
return zones
def get_nets(self, board):
"""获取网络列表"""
nets = []
for net in board.GetNetInfo().NetsByNetcode().values():
name = net.GetNetname()
if name:
nets.append({
'name': name,
'netclass': net.GetNetClassName()
})
return nets
CustomExportPlugin().register()
18.2 案例二:设计规则检查扩展
18.2.1 需求分析
创建自定义DRC检查,检测特定的设计问题。
18.2.2 实现代码
import pcbnew
import wx
class CustomDRCPlugin(pcbnew.ActionPlugin):
def defaults(self):
self.name = "自定义DRC检查"
self.category = "检查"
self.description = "执行额外的设计规则检查"
self.show_toolbar_button = True
def Run(self):
board = pcbnew.GetBoard()
violations = []
# 执行各项检查
violations.extend(self.check_power_track_width(board))
violations.extend(self.check_component_spacing(board))
violations.extend(self.check_via_placement(board))
violations.extend(self.check_silkscreen_on_pads(board))
# 显示结果
self.show_results(violations)
def check_power_track_width(self, board, min_width_mm=0.5):
"""检查电源走线宽度"""
violations = []
power_nets = ['VCC', 'VDD', '+5V', '+3.3V', '+12V', 'GND']
for track in board.GetTracks():
if track.GetClass() == "PCB_TRACK":
netname = track.GetNetname()
if any(pn in netname.upper() for pn in power_nets):
width_mm = pcbnew.ToMM(track.GetWidth())
if width_mm < min_width_mm:
start = track.GetStart()
violations.append({
'type': '电源线宽度不足',
'detail': f'{netname}: {width_mm:.3f}mm < {min_width_mm}mm',
'x': pcbnew.ToMM(start.x),
'y': pcbnew.ToMM(start.y)
})
return violations
def check_component_spacing(self, board, min_spacing_mm=0.5):
"""检查元器件间距"""
violations = []
footprints = list(board.GetFootprints())
for i, fp1 in enumerate(footprints):
bbox1 = fp1.GetBoundingBox()
for fp2 in footprints[i+1:]:
bbox2 = fp2.GetBoundingBox()
# 简化的距离检查
center1 = fp1.GetPosition()
center2 = fp2.GetPosition()
dist = ((center1.x - center2.x)**2 + (center1.y - center2.y)**2)**0.5
dist_mm = pcbnew.ToMM(dist)
# 如果边界框相交,检查间距
if bbox1.Intersects(bbox2):
violations.append({
'type': '元器件间距过近',
'detail': f'{fp1.GetReference()} 和 {fp2.GetReference()}',
'x': pcbnew.ToMM(center1.x),
'y': pcbnew.ToMM(center1.y)
})
return violations
def check_via_placement(self, board, min_dist_to_pad_mm=0.3):
"""检查过孔与焊盘距离"""
violations = []
# 收集所有焊盘位置
pads = []
for fp in board.GetFootprints():
for pad in fp.Pads():
pads.append(pad.GetPosition())
# 检查过孔
for track in board.GetTracks():
if track.GetClass() == "PCB_VIA":
via_pos = track.GetPosition()
for pad_pos in pads:
dist = ((via_pos.x - pad_pos.x)**2 +
(via_pos.y - pad_pos.y)**2)**0.5
dist_mm = pcbnew.ToMM(dist)
if dist_mm < min_dist_to_pad_mm:
violations.append({
'type': '过孔离焊盘过近',
'detail': f'距离: {dist_mm:.3f}mm',
'x': pcbnew.ToMM(via_pos.x),
'y': pcbnew.ToMM(via_pos.y)
})
return violations
def check_silkscreen_on_pads(self, board):
"""检查丝印是否覆盖焊盘"""
violations = []
# 这是一个简化的检查示例
# 实际实现需要更复杂的几何计算
return violations
def show_results(self, violations):
"""显示检查结果"""
if not violations:
wx.MessageBox("未发现问题", "自定义DRC检查",
wx.OK | wx.ICON_INFORMATION)
return
# 生成报告
report = f"发现 {len(violations)} 个问题:\n\n"
for v in violations:
report += f"• {v['type']}\n"
report += f" {v['detail']}\n"
report += f" 位置: ({v['x']:.2f}, {v['y']:.2f})\n\n"
# 显示在对话框中
dlg = wx.TextEntryDialog(None, report, "自定义DRC结果",
style=wx.TE_MULTILINE | wx.TE_READONLY)
dlg.SetSize((500, 400))
dlg.ShowModal()
dlg.Destroy()
CustomDRCPlugin().register()
18.3 案例三:自动布局辅助工具
18.3.1 需求分析
创建一个自动整理元器件布局的工具。
18.3.2 实现代码
import pcbnew
import wx
class AutoLayoutPlugin(pcbnew.ActionPlugin):
def defaults(self):
self.name = "自动布局辅助"
self.category = "布局"
self.description = "自动整理元器件布局"
self.show_toolbar_button = True
def Run(self):
dlg = LayoutOptionsDialog(None)
if dlg.ShowModal() == wx.ID_OK:
options = dlg.GetOptions()
self.auto_layout(options)
dlg.Destroy()
def auto_layout(self, options):
"""执行自动布局"""
board = pcbnew.GetBoard()
if options['type'] == 'grid':
self.layout_in_grid(board, options)
elif options['type'] == 'by_value':
self.layout_by_value(board, options)
elif options['type'] == 'by_prefix':
self.layout_by_prefix(board, options)
pcbnew.Refresh()
wx.MessageBox("布局完成", "自动布局")
def layout_in_grid(self, board, options):
"""网格布局"""
footprints = list(board.GetFootprints())
if not footprints:
return
start_x = options.get('start_x', 50)
start_y = options.get('start_y', 50)
spacing_x = options.get('spacing_x', 10)
spacing_y = options.get('spacing_y', 10)
cols = options.get('columns', 5)
for i, fp in enumerate(footprints):
row = i // cols
col = i % cols
x = pcbnew.FromMM(start_x + col * spacing_x)
y = pcbnew.FromMM(start_y + row * spacing_y)
fp.SetPosition(pcbnew.wxPoint(x, y))
def layout_by_value(self, board, options):
"""按值分组布局"""
footprints = list(board.GetFootprints())
# 按值分组
groups = {}
for fp in footprints:
value = fp.GetValue()
if value not in groups:
groups[value] = []
groups[value].append(fp)
# 布局各组
start_x = 50
start_y = 50
group_spacing = 30
item_spacing = 8
y_offset = 0
for value, fps in sorted(groups.items()):
for i, fp in enumerate(fps):
x = pcbnew.FromMM(start_x + i * item_spacing)
y = pcbnew.FromMM(start_y + y_offset)
fp.SetPosition(pcbnew.wxPoint(x, y))
y_offset += group_spacing
def layout_by_prefix(self, board, options):
"""按参考前缀分组布局"""
footprints = list(board.GetFootprints())
# 按前缀分组
groups = {}
for fp in footprints:
ref = fp.GetReference()
prefix = ''.join(c for c in ref if not c.isdigit())
if prefix not in groups:
groups[prefix] = []
groups[prefix].append(fp)
# 预定义区域
regions = {
'R': (50, 50), # 电阻区
'C': (50, 100), # 电容区
'U': (120, 50), # IC区
'Q': (120, 100), # 晶体管区
'D': (190, 50), # 二极管区
'J': (190, 100), # 连接器区
}
item_spacing = 8
for prefix, fps in groups.items():
start_x, start_y = regions.get(prefix, (250, 50))
for i, fp in enumerate(fps):
x = pcbnew.FromMM(start_x + i * item_spacing)
y = pcbnew.FromMM(start_y)
fp.SetPosition(pcbnew.wxPoint(x, y))
class LayoutOptionsDialog(wx.Dialog):
def __init__(self, parent):
super().__init__(parent, title="布局选项", size=(350, 300))
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
# 布局类型选择
self.layout_type = wx.RadioBox(
panel, choices=["网格布局", "按值分组", "按前缀分组"],
style=wx.RA_SPECIFY_COLS, majorDimension=1
)
vbox.Add(self.layout_type, 0, wx.ALL | wx.EXPAND, 10)
# 网格参数
grid_box = wx.StaticBox(panel, label="网格参数")
grid_sizer = wx.StaticBoxSizer(grid_box, wx.VERTICAL)
self.cols_ctrl = wx.SpinCtrl(panel, value="5", min=1, max=20)
self.spacing_ctrl = wx.SpinCtrl(panel, value="10", min=1, max=50)
grid_sizer.Add(wx.StaticText(panel, label="列数:"), 0, wx.ALL, 5)
grid_sizer.Add(self.cols_ctrl, 0, wx.ALL | wx.EXPAND, 5)
grid_sizer.Add(wx.StaticText(panel, label="间距(mm):"), 0, wx.ALL, 5)
grid_sizer.Add(self.spacing_ctrl, 0, wx.ALL | wx.EXPAND, 5)
vbox.Add(grid_sizer, 0, wx.ALL | wx.EXPAND, 10)
# 按钮
btn_sizer = wx.StdDialogButtonSizer()
ok_btn = wx.Button(panel, wx.ID_OK, "执行")
cancel_btn = wx.Button(panel, wx.ID_CANCEL, "取消")
btn_sizer.AddButton(ok_btn)
btn_sizer.AddButton(cancel_btn)
btn_sizer.Realize()
vbox.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 10)
panel.SetSizer(vbox)
def GetOptions(self):
types = ['grid', 'by_value', 'by_prefix']
return {
'type': types[self.layout_type.GetSelection()],
'columns': self.cols_ctrl.GetValue(),
'spacing_x': self.spacing_ctrl.GetValue(),
'spacing_y': self.spacing_ctrl.GetValue(),
'start_x': 50,
'start_y': 50
}
AutoLayoutPlugin().register()
18.4 案例四:BOM生成器增强
18.4.1 实现代码
import pcbnew
import wx
import csv
from collections import defaultdict
class EnhancedBOMPlugin(pcbnew.ActionPlugin):
def defaults(self):
self.name = "增强BOM生成"
self.category = "导出"
self.description = "生成带采购信息的BOM"
self.show_toolbar_button = True
def Run(self):
board = pcbnew.GetBoard()
# 收集元器件信息
components = self.collect_components(board)
# 合并相同元器件
merged = self.merge_components(components)
# 选择保存路径
dlg = wx.FileDialog(None, "保存BOM", "", "BOM.csv",
"CSV files (*.csv)|*.csv",
wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
if dlg.ShowModal() == wx.ID_OK:
filepath = dlg.GetPath()
self.export_bom(merged, filepath)
wx.MessageBox(f"BOM导出成功: {filepath}", "完成")
dlg.Destroy()
def collect_components(self, board):
"""收集元器件信息"""
components = []
for fp in board.GetFootprints():
# 跳过虚拟元器件
if fp.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
continue
# 获取基本信息
component = {
'reference': fp.GetReference(),
'value': fp.GetValue(),
'footprint': str(fp.GetFPID().GetLibItemName()),
'layer': 'Top' if fp.GetLayer() == pcbnew.F_Cu else 'Bottom'
}
# 尝试获取扩展字段
for field in ['Manufacturer', 'MPN', 'Supplier', 'SPN']:
try:
val = fp.GetFieldText(field)
component[field.lower()] = val if val else ''
except:
component[field.lower()] = ''
components.append(component)
return components
def merge_components(self, components):
"""合并相同元器件"""
merged = defaultdict(lambda: {
'refs': [],
'quantity': 0
})
for comp in components:
# 创建唯一键(基于值、封装、MPN)
key = (comp['value'], comp['footprint'],
comp.get('mpn', ''))
if merged[key]['quantity'] == 0:
merged[key].update(comp)
merged[key]['refs'].append(comp['reference'])
merged[key]['quantity'] += 1
# 转换为列表并排序参考标号
result = []
for key, data in merged.items():
data['refs'].sort(key=lambda x: (
''.join(c for c in x if not c.isdigit()),
int(''.join(c for c in x if c.isdigit()) or '0')
))
data['references'] = ', '.join(data['refs'])
result.append(data)
return result
def export_bom(self, components, filepath):
"""导出BOM到CSV"""
with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:
fieldnames = ['Item', 'Quantity', 'References', 'Value',
'Footprint', 'Manufacturer', 'MPN',
'Supplier', 'SPN']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for i, comp in enumerate(components, 1):
writer.writerow({
'Item': i,
'Quantity': comp['quantity'],
'References': comp['references'],
'Value': comp['value'],
'Footprint': comp['footprint'],
'Manufacturer': comp.get('manufacturer', ''),
'MPN': comp.get('mpn', ''),
'Supplier': comp.get('supplier', ''),
'SPN': comp.get('spn', '')
})
EnhancedBOMPlugin().register()
18.5 开发最佳实践
18.5.1 代码组织
# 推荐的插件项目结构
my_plugin/
├── __init__.py
├── plugin.py # 主插件代码
├── dialogs.py # GUI对话框
├── utils.py # 工具函数
├── resources/
│ └── icons/ # 图标文件
└── tests/
└── test_plugin.py
18.5.2 错误处理
def safe_operation(func):
"""装饰器:安全执行并捕获异常"""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
wx.MessageBox(f"错误: {str(e)}", "插件错误",
wx.OK | wx.ICON_ERROR)
import traceback
traceback.print_exc()
return wrapper
class SafePlugin(pcbnew.ActionPlugin):
@safe_operation
def Run(self):
# 插件代码
pass
18.5.3 配置持久化
import json
import os
class PluginConfig:
def __init__(self, plugin_name):
self.config_dir = os.path.expanduser("~/.config/kicad_plugins")
self.config_file = os.path.join(self.config_dir, f"{plugin_name}.json")
self.config = {}
self.load()
def load(self):
if os.path.exists(self.config_file):
with open(self.config_file, 'r') as f:
self.config = json.load(f)
def save(self):
os.makedirs(self.config_dir, exist_ok=True)
with open(self.config_file, 'w') as f:
json.dump(self.config, f, indent=2)
def get(self, key, default=None):
return self.config.get(key, default)
def set(self, key, value):
self.config[key] = value
self.save()
18.6 本章小结
本章通过四个实战案例展示了KiCad二次开发的应用:
- 自定义导出器:学会了将PCB数据导出为自定义格式。
- DRC扩展:掌握了创建自定义设计规则检查。
- 自动布局工具:实现了元器件自动整理功能。
- BOM增强:创建了功能更强的BOM生成器。
- 最佳实践:学会了代码组织、错误处理和配置管理。
通过这些案例,读者可以将所学知识应用到实际项目中,开发满足特定需求的KiCad扩展工具。

浙公网安备 33010602011771号