KiCad PCB 文件格式详解

本文档详细说明 KiCad PCB 文件(.kicad_pcb)的文件结构,包含的信息类型,以及如何解析这些数据。

目录

  1. 文件格式概述
  2. 基本语法
  3. 顶层结构
  4. 各部分详解
  5. 数据映射
  6. 解析实现

文件格式概述

格式类型

KiCad PCB 文件 使用 S-expression (S表达式) 格式存储。

特点:

  • 文本格式,人类可读
  • 嵌套括号结构
  • 类似 Lisp 语法
  • 版本控制友好

文件扩展名: .kicad_pcb

KiCad 版本: KiCad 9.0+ (当前代码支持版本 20241229)


基本语法

S-表达式语法

(keyword value)
(keyword "string value")
(keyword (nested data))
(keyword
  (child1 data)
  (child2 data)
)

规则

  1. 所有内容都是列表: 用括号 () 包围
  2. 列表第一个元素是关键字: 标识数据类型
  3. 字符串用双引号: "string value"
  4. 数字不带引号: 123, 45.67
  5. 注释: 使用 ; 开头(很少使用)
  6. 空白字符: 空格、换行、制表符用于格式化

示例对比

Lisp 风格:

(define x 10)

KiCad PCB 格式:

(net 1 "GND")
(segment (start 10.5 20.3) (end 30.7 40.9))

顶层结构

完整文件结构

(kicad_pcb
  (version 20241229)
  (generator "pcbnew")
  (generator_version "9.0")

  (general
    (thickness 1.6002)
    (legacy_teardrops no)
  )

  (paper "A4")

  (title_block
    (title "Demo")
    (rev "2.C")
    (company "Kicad")
  )

  (layers ...)

  (setup ...)

  (properties ...)

  (net 0 "")
  (net 1 "GND")
  (net 2 "+3V3")
  ...

  (footprint "lib:FPName" ...)
  (footprint "lib:FPName" ...)
  ...

  (gr_line ...)
  (gr_arc ...)
  (gr_circle ...)

  (segment ...)
  (segment ...)
  ...

  (via ...)
  (via ...)
  ...

  (zone ...)
  (zone ...)
  ...
)

顶层元素说明

元素用途必需
kicad_pcb文件根标识✅
version文件格式版本✅
generator生成器名称✅
generator_versionKiCad 版本✅
general通用设置⚠️
paper图纸尺寸⚠️
title_block标题栏信息⚠️
layers层定义✅
setup设置和层叠⚠️
properties文件属性❌
net网络定义✅
footprint封装实例⚠️
segment轨道段⚠️
via过孔⚠️
zone填充区域❌
gr_*图形对象❌

各部分详解

1. 文件头 (Header)

格式
(kicad_pcb
  (version 20241229)
  (generator "pcbnew")
  (generator_version "9.0")
)
解析代码
# kicad_parser.py 不需要显式解析这些
# 它们在文件开头,用于版本验证
说明
  • version: KiCad 文件格式版本号(日期格式)
  • generator: 生成器名称(通常是 “pcbnew”)
  • generator_version: KiCad 版本号(如 “9.0”)

2. 通用设置 (General)

格式
(general
  (thickness 1.6002)              ; 板厚度 (mm)
  (legacy_teardrops no)            ; 是否使用旧式泪滴
  (suppress  connect)              ; 连接点显示
)
解析代码
# 当前 routing 工具不使用这些信息
# 但可以提取板厚度用于阻抗计算
thickness_match = re.search(r'\(thickness\s+([\d.]+)\)', content)
if thickness_match:
    board_thickness = float(thickness_match.group(1))

3. 层定义 (Layers)

格式
(layers
  (0 "F.Cu" signal "top_copper")                    ; 顶层铜
  (1 "In1.Cu" signal "internal1")                    ; 内层1铜
  (2 "In2.Cu" signal "internal2")                    ; 内层2铜
  (31 "B.Cu" signal "bottom_copper")                ; 底层铜

  (9 "F.Adhes" user "F.Adhesive")                   ; 顶层粘合剂
  (11 "B.Adhes" user "B.Adhesive")                   ; 底层粘合剂

  (13 "F.Paste" user)                               ; 顶层锡膏
  (15 "B.Paste" user)                               ; 底层锡膏

  (5 "F.SilkS" user "F.Silkscreen")                 ; 顶层丝印
  (7 "B.SilkS" user "B.Silkscreen")                 ; 底层丝印

  (1 "F.Mask" user)                                ; 顶层阻焊
  (3 "B.Mask" user)                                ; 底层阻焊

  (17 "Dwgs.User" user "User.Drawings")             ; 用户绘图
  (19 "Cmts.User" user "User.Comments")             ; 用户注释

  (21 "Eco1.User" user "User.Eco1")                 ; 用户 Eco1
  (23 "Eco2.User" user "User.Eco2")                 ; 用户 Eco2

  (25 "Edge.Cuts" user)                             ; 板边界
  (27 "Margin" user)                                ; 边缘

  (29 "B.CrtYd" user "B.Courtyard")                 ; 底层装配禁区
  (31 "F.CrtYd" user "F.Courtyard")                 ; 顶层装配禁区

  (33 "B.Fab" user)                                 ; 底层 Fab
  (35 "F.Fab" user)                                 ; 顶层 Fab
)
解析代码
def extract_layers(content: str) -> BoardInfo:
    """Extract layer information from PCB file."""
    layers = {}
    copper_layers = []

    # 查找 layers 部分
    layers_match = re.search(r'\(layers\s*((?:\([^)]+\)\s*)+)\)', content, re.DOTALL)
    if layers_match:
        layers_text = layers_match.group(1)

        # 解析每个层条目: (0 "F.Cu" signal)
        layer_pattern = r'\((\d+)\s+"([^"]+)"\s+(\w+)'
        for m in re.finditer(layer_pattern, layers_text):
            layer_id = int(m.group(1))
            layer_name = m.group(2)
            layer_type = m.group(3)
            layers[layer_id] = layer_name

            # 只收集铜层
            if layer_type == 'signal' and '.Cu' in layer_name:
                copper_layers.append(layer_name)

    return BoardInfo(layers=layers, copper_layers=copper_layers, ...)
层类型
层名后缀类型用途
.Cusignal铜层(用于布线)
.SilkSuser丝印层
.Maskuser阻焊层
.Pasteuser锡膏层
.Adhesuser粘合剂层
.CrtYduser装配禁区
.FabuserFab 层
Useruser用户自定义层
在路由中的用途
  • copper_layers: 确定可布线的层
  • layer_id: 内部层引用(0, 1, 2…)
  • layer_name: 显示名称(“F.Cu”, “B.Cu”…)

4. 设置和层叠 (Setup & Stackup)

格式
(setup
  (stackup
    (layer "F.SilkS"
      (type "Top Silk Screen")
      (color "White")
    )

    (layer "F.Mask"
      (type "Top Solder Mask")
      (color "Green")
      (thickness 0.01)            ; mm
    )

    (layer "F.Cu"
      (type "copper")
      (thickness 0.035)           ; mm: 铜厚
    )

    (layer "dielectric 1"
      (type "core")                ; 核心
      (thickness 1.51)             ; mm
      (material "FR4")              ; 材料
      (epsilon_r 4.5)               ; 介电常数
      (loss_tangent 0.02)           ; 损耗角正切
    )

    (layer "B.Cu"
      (type "copper")
      (thickness 0.035)
    )

    (copper_finish "None")
    (dielectric_constraints no)
  )

  (pad_to_mask_clearance 0)
  (allow_soldermask_bridges_in_footprints no)
  (tenting front back)              ; 阻焊开口
  (aux_axis_origin 74.93 140.97)    ; 辅助坐标原点

  (pcbplotparams
    (layerselection 0x00000000_00000000_000010f0_ffffffff)
    (disableapertmacros no)
    (usegerberextensions no)
    (usegerberattributes yes)
    (svgprecision 6)
    (plotframeref no)
    (mode 1)
    (useauxorigin no)
  )

  (net_class_pattern ...)           ; 网络类定义
)
解析代码
def extract_stackup(content: str) -> List[StackupLayer]:
    """Extract board stackup information."""
    stackup = []

    # 查找 stackup 部分
    stackup_match = re.search(r'\(stackup\s+(.*?)\n\s*\(copper_finish',
                             content, re.DOTALL)
    if not stackup_match:
        return stackup

    stackup_text = stackup_match.group(1)

    # 解析每一层
    layer_blocks = re.findall(
        r'\(layer\s+"([^"]+)"(.*?)(?=\(layer\s+"|$)',
        stackup_text,
        re.DOTALL
    )

    for layer_name, layer_content in layer_blocks:
        # 提取类型
        type_match = re.search(r'\(type\s+"([^"]+)"\)', layer_content)
        layer_type = type_match.group(1) if type_match else 'unknown'

        # 提取厚度 (mm)
        thickness_match = re.search(r'\(thickness\s+([\d.]+)\)', layer_content)
        thickness = float(thickness_match.group(1)) if thickness_match else 0.0

        # 提取介电常数
        epsilon_match = re.search(r'\(epsilon_r\s+([\d.]+)\)', layer_content)
        epsilon_r = float(epsilon_match.group(1)) if epsilon_match else 0.0

        # 提取损耗角正切
        loss_match = re.search(r'\(loss_tangent\s+([\d.]+)\)', layer_content)
        loss_tangent = float(loss_match.group(1)) if loss_match else 0.0

        # 提取材料名称
        material_match = re.search(r'\(material\s+"([^"]+)"\)', layer_content)
        material = material_match.group(1) if material_match else ""

        # 只包含铜层和介质层
        if layer_type in ('copper', 'core', 'prepreg'):
            stackup.append(StackupLayer(
                name=layer_name,
                layer_type=layer_type,
                thickness=thickness,
                epsilon_r=epsilon_r,
                loss_tangent=loss_tangent,
                material=material
            ))

    return stackup
在路由中的用途
信息用途
layer thicknessVia barrel 长度计算(长度匹配)
epsilon_r阻抗控制
layer type区分铜层、core、prepreg
material材料信息

5. 网络定义 (Nets)

格式
(net 0 "")                              ; 空网络(未连接)

(net 1 "GND")                          ; 电源网络
  (pin_reference "U1" 4)             ; 可选:器件引用

(net 2 "+3V3")                         ; 电源网络

(net 3 "/RESET")                       ; 信号网络

(net 4 "Net-(U1-Pad5)")                 ; 自动生成的网络名

(net 5 "unconnected-(U9-TMS-PadB5)")   ; 未连接的网络
解析代码
def extract_nets(content: str) -> Dict[int, Net]:
    """Extract all net definitions."""
    nets = {}

    # 匹配: (net 123 "net_name")
    net_pattern = r'\(net\s+(\d+)\s+"([^"]*)"\)'

    for m in re.finditer(net_pattern, content):
        net_id = int(m.group(1))
        net_name = m.group(2)
        nets[net_id] = Net(net_id=net_id, name=net_name)

    return nets
网络命名规则
模式示例说明
简单名称"GND", "VCC"电源网络
斜杠前缀"/RESET", "/CS"信号网络
连接点"Net-(U1-Pad5)"自动生成(器件-焊盘)
未连接"unconnected-(U9-PadB5)"未连接的焊盘
差分对"_P", "_N" 后缀人工命名(如 USB_P, USB_N)
在路由中的用途
  1. 网络选择: 用户选择要路由的网络
  2. 差分对检测: 识别 _P/_N 后缀
  3. 电源网络识别: 识别 GND, VCC 等
  4. 进度显示: 显示正在路由的网络名

6. 封装和焊盘 (Footprints & Pads)

格式
(footprint "lib:FP_Name"
  (layer "F.Cu")
  (uuid "00000000-0000-0000-0000-0000322d32a0")

  (at 171.958 42.037)              ; X, Y 位置 (mm)
  (descr "Description text...")

  (tags "Resistor Axial ...")         ; 搜索标签

  (property "Reference" "R5"
    (at 3.81 -2.31 0)
    (layer "F.SilkS")
    (uuid "...")
    (effects
      (font (size 1 1) (thickness 0.15))
    )
  )

  (property "Value" "330"           ; 器件值
    (at 3.81 2.31 0)
    (layer "F.Fab")
  )

  (fp_text reference "R5"            ; 参考标识符
    (at 0 0 0)
    (layer "F.SilkS")
    (effects (font (size 1 1)(thickness 0.15)))
  )

  (fp_text value "330"                ; 值标识符
    (at 0 0 0)
    (layer "F.Fab")
  )

  (pad "1" thru_hole circle           ; 焊盘定义
    (at 10.5 20.3)
    (size 1.5 1.5)                   ; 尺寸
    (drill 0.8)                       ; 钻孔
    (layers "*.Cu")
    (remove_unused_layers no)
    (net 5 "Net-(R5-Pad1)")
    (uuid "...")
    (pinfunction "1")
    (pintype "passive")
  )

  (pad "2" thru_hole circle
    (at 10.5 27.7)
    (size 1.5 1.5)
    (drill 0.8)
    (layers "*.Cu")
    (net 5 "Net-(R5-Pad2)")
    (uuid "...")
  )

  (model "..." ...)                   ; 3D 模型
  (zone "..." ...)                    ; 局部区域
)
焊盘类型
类型关键字说明
通孔圆焊盘thru_hole circle圆形通孔
通孔方焊盘thru_hole rect方形通孔
通孔圆角矩形thru_hole roundrect圆角矩形通孔
SMD 圆形smd circle表贴圆形
SMD 矩形smd rect表贴矩形
SMD 圆角矩形smd roundrect表贴圆角矩形
SMD 椭圆smd oval表贴椭圆
边缘连接器connect板边缘连接器
解析代码
def extract_footprints_and_pads(content: str, nets: Dict[int, Net]):
    """Extract footprints and their pads with global coordinates."""
    footprints = {}
    pads_by_net: Dict[int, List[Pad]] = {}

    # 查找所有 footprint - 需要处理嵌套括号
    footprint_starts = [m.start() for m in re.finditer(r'\(footprint\s+"', content)]

    for start in footprint_starts:
        # 查找匹配的右括号
        depth = 0
        end = start
        for i, char in enumerate(content[start:], start):
            if char == '(':
                depth += 1
            elif char == ')':
                depth -= 1
                if depth == 0:
                    end = i + 1
                    break

        fp_text = content[start:end]

        # 提取封装名称
        fp_name_match = re.search(r'\(footprint\s+"([^"]+)"', fp_text)
        fp_name = fp_name_match.group(1)

        # 提取位置和旋转
        at_match = re.search(r'\(at\s+([\d.-]+)\s+([\d.-]+)(?:\s+([\d.-]+))?\)', fp_text)
        fp_x = float(at_match.group(1))
        fp_y = float(at_match.group(2))
        fp_rotation = float(at_match.group(3)) if at_match.group(3) else 0.0

        # 提取层
        layer_match = re.search(r'\(layer\s+"([^"]+)"\)', fp_text)
        fp_layer = layer_match.group(1) if layer_match else "F.Cu"

        # 提取 Reference
        ref_match = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', fp_text)
        reference = ref_match.group(1) if ref_match else "?"

        # 提取 Value
        value_match = re.search(r'\(property\s+"Value"\s+"([^"]+)"', fp_text)
        value = value_match.group(1) if value_match else ""

        footprint = Footprint(
            reference=reference,
            footprint_name=fp_name,
            x=fp_x,
            y=fp_y,
            rotation=fp_rotation,
            layer=fp_layer,
            value=value
        )

        # 提取焊盘
        for pad_match in re.finditer(r'\(pad\s+"([^"]*)"\s+(\w+)\s+(\w+)', fp_text):
            pad_num = pad_match.group(1)
            pad_type = pad_match.group(2)  # thru_hole, smd, etc.
            pad_shape = pad_match.group(3)  # circle, rect, etc.

            # 查找焊盘块的结束
            pad_start = pad_match.start()
            pad_text = fp_text[pad_start:find_matching_paren(pad_start)]

            # 提取焊盘属性
            at_match = re.search(r'\(at\s+([\d.-]+)\s+([\d.-]+)\)', pad_text)
            local_x = float(at_match.group(1))
            local_y = float(at_match.group(2))

            size_match = re.search(r'\(size\s+([\d.-]+)\s+([\d.-]+)\)', pad_text)
            size_x = float(size_match.group(1))
            size_y = float(size_match.group(2))

            drill_match = re.search(r'\(drill\s+([\d.-]+)\)', pad_text)
            drill = float(drill_match.group(1)) if drill_match else 0.0

            layers_match = re.search(r'\(layers\s+"([^"]*)"\)', pad_text)
            layers = layers_match.group(1).split() if layers_match else ["F.Cu"]

            net_match = re.search(r'\(net\s+(\d+)\)', pad_text)
            net_id = int(net_match.group(1)) if net_match else 0

            net_name = nets[net_id].name if net_id in nets else ""

            # 转换到全局坐标
            global_x, global_y = local_to_global(fp_x, fp_y, fp_rotation, local_x, local_y)

            pad = Pad(
                component_ref=reference,
                pad_number=pad_num,
                global_x=global_x,
                global_y=global_y,
                local_x=local_x,
                local_y=local_y,
                size_x=size_x,
                size_y=size_y,
                shape=pad_shape,
                layers=layers,
                net_id=net_id,
                net_name=net_name,
                rotation=fp_rotation,  # 使用封装旋转
                pinfunction="",
                pintype="",
                drill=drill
            )

            footprint.pads.append(pad)

            # 添加到按网络分组的焊盘
            if net_id not in pads_by_net:
                pads_by_net[net_id] = []
            pads_by_net[net_id].append(pad)

        footprints[reference] = footprint

    return footprints, pads_by_net
坐标变换
def local_to_global(fp_x, fp_y, fp_rotation_deg, pad_local_x, pad_local_y):
    """
    将焊盘局部坐标转换为全局坐标

    关键:必须取反旋转角度!
    KiCad 的旋转约定要求在使用标准旋转矩阵公式时取反角度。
    """
    rad = math.radians(-fp_rotation_deg)  # 关键:取反角度
    cos_r = math.cos(rad)
    sin_r = math.sin(rad)

    global_x = fp_x + (pad_local_x * cos_r - pad_local_y * sin_r)
    global_y = fp_y + (pad_local_x * sin_r + pad_local_y * cos_r)

    return global_x, global_y
在路由中的用途
焊盘属性用途
global_x, global_y路由起点/终点
net_id网络连接
layers确定起始层
drill > 0通孔焊盘阻挡所有层
component_refBGA 检测、目标交换
pad_number引脚识别
pinfunction差分对检测(如 /CS)

7. 轨道段 (Segments)

格式
(segment
  (start 123.456 78.901)            ; 起点 X, Y (mm)
  (end 150.234 95.678)              ; 终点 X, Y (mm)
  (width 0.25)                       ; 轨道宽度 (mm)
  (layer "F.Cu")                    ; 层
  (net 3)                           ; 网络 ID
  (uuid "00000000-0000-0000-0000-00004abc1234")
)
解析代码
def extract_segments(content: str) -> List[Segment]:
    """Extract all track segments from PCB file."""
    segments = []

    # 匹配段块
    segment_pattern = r'\(segment\s+\(start\s+([\d.-]+)\s+([\d.-]+)\)\s+\(end\s+([\d.-]+)\s+([\d.-]+)\)\s+\(width\s+([\d.-]+)\)\s+\(layer\s+"([^"]+)"\)\s+\(net\s+(\d+)\)\s+\(uuid\s+"([^"]+)"\)'

    for m in re.finditer(segment_pattern, content, re.DOTALL):
        segment = Segment(
            start_x=float(m.group(1)),
            start_y=float(m.group(2)),
            end_x=float(m.group(3)),
            end_y=float(m.group(4)),
            width=float(m.group(5)),
            layer=m.group(6),
            net_id=int(m.group(7)),
            uuid=m.group(8),
            # 存储原始字符串用于精确文件匹配
            start_x_str=m.group(1),
            start_y_str=m.group(2),
            end_x_str=m.group(3),
            end_y_str=m.group(4)
        )
        segments.append(segment)

    return segments
在路由中的用途
属性用途
start_x, start_y, end_x, end_y障碍物位置、连通性检查
width障碍物宽度
layer障碍物层
net_id网络识别(同网络可穿过)
uuid唯一标识符(用于精确匹配)

8. 过孔 (Vias)

格式
(via
  (at 100.5 200.3)                  ; X, Y 位置 (mm)
  (size 0.6)                         ; 外径 (mm)
  (drill 0.3)                        ; 钻孔直径 (mm)
  (layers "F.Cu" "B.Cu")             ; 连接的层
  (free yes)                         ; 自由 via(KiCad 不会自动分配网络)
  (net 5)                            ; 网络 ID
  (uuid "00000000-0000-0000-0000-00004def5678")
)
解析代码
def extract_vias(content: str) -> List[Via]:
    """Extract all vias from PCB file."""
    vias = []

    # 匹配 via 块
    # 注意:free 属性是可选的,可以在 layers 和 net 之间
    via_pattern = r'\(via\s+\(at\s+([\d.-]+)\s+([\d.-]+)\)\s+\(size\s+([\d.-]+)\)\s+\(drill\s+([\d.-]+)\)\s+\(layers\s+"([^"]+)"\s+"([^"]+)"\)\s+(?:\(free\s+(yes|no)\)\s+)?\(net\s+(\d+)\)\s+\(uuid\s+"([^"]+)"\)'

    for m in re.finditer(via_pattern, content, re.DOTALL):
        free_value = m.group(7)  # "yes", "no", 或 None
        via = Via(
            x=float(m.group(1)),
            y=float(m.group(2)),
            size=float(m.group(3)),
            drill=float(m.group(4)),
            layers=[m.group(5), m.group(6)],
            net_id=int(m.group(8)),
            uuid=m.group(9),
            free=(free_value == "yes")
        )
        vias.append(via)

    return vias
在路由中的用途
属性用途
x, y障碍物位置
size障碍物尺寸
layers阻挡所有连接的层
net_id网络识别
drill钻孔信息
free是否为自由 via

9. 填充区域 (Zones)

格式
	(zone
		(net 1)                           ; 网络 ID
		(net_name "GND")                  ; 网络名称
		(layer "B.Cu")                    ; 层
		(uuid "00000000-0000-0000-0000-0000abcd1234")
		(pts
			(xy 110.5 50.2)
			(xy 110.5 80.7)
			(xy 150.8 80.7)
			(xy 150.8 50.2)
		)
		(filled_areas ...)                ; 可选:填充区域
		(fill ...)                         ; 可选:填充模式
		(zone_connect ...)                ; 可选:连接方式
	)
解析代码
def extract_zones(content: str) -> List[Zone]:
    """Extract all filled zones from PCB file."""
    zones = []

    # 查找每个 zone 块的开始
    zone_start_pattern = r'\r?\n\t\(zone\s*\r?\n'

    for start_match in re.finditer(zone_start_pattern, content):
        # 查找匹配的右括号
        start_pos = start_match.start() + len(start_match.group()) - 1
        paren_count = 1
        pos = start_match.end()
        zone_end = None

        while pos < len(content) and paren_count > 0:
            char = content[pos]
            if char == '(':
                paren_count += 1
            elif char == ')':
                paren_count -= 1
                if paren_count == 0:
                    zone_end = pos
            pos += 1

        if zone_end is None:
            continue

        zone_content = content[start_match.end():zone_end]

        # 提取网络 ID
        net_match = re.search(r'\(net\s+(\d+)\)', zone_content)
        net_id = int(net_match.group(1)) if net_match else 0

        # 提取网络名称
        net_name_match = re.search(r'\(net_name\s+"([^"]*)"\)', zone_content)
        net_name = net_name_match.group(1) if net_name_match else ""

        # 提取层
        layer_match = re.search(r'\(layer\s+"([^"]+)"\)', zone_content)
        layer = layer_match.group(1) if layer_match else ""

        # 提取 UUID
        uuid_match = re.search(r'\(uuid\s+"([^"]+)"\)', zone_content)
        uuid = uuid_match.group(1) if uuid_match else ""

        # 提取多边形点 - 查找 (pts ...) 并提取 xy 坐标
        pts_start = zone_content.find('(pts')
        if pts_start < 0:
            continue

        # 查找 (pts 的匹配右括号
        paren_count = 0
        pts_end = pts_start
        for i in range(pts_start, len(zone_content)):
            if zone_content[i] == '(':
                paren_count += 1
            elif zone_content[i] == ')':
                paren_count -= 1
                if paren_count == 0:
                    pts_end = i
                    break

        pts_content = zone_content[pts_start:pts_end + 1]

        # 解析所有 (xy x y) 点
        xy_pattern = r'\(xy\s+([\d.-]+)\s+([\d.-]+)\)'
        polygon = [(float(m.group(1)), float(m.group(2)))
                   for m in re.finditer(xy_pattern, pts_content)]

        if not polygon:
            continue

        zone = Zone(
            net_id=net_id,
            net_name=net_name,
            layer=layer,
            polygon=polygon,
            uuid=uuid
        )
        zones.append(zone)

    return zones
在路由中的用途
属性用途
net_id, net_name确定网络
layer所在层
polygon区域边界(多边形)
uuid唯一标识符

10. 图形对象 (Graphics)

类型
类型关键字用途
直线gr_line图形、标注
圆弧gr_arc圆弧
圆gr_circle圆
文本gr_text文本标注
矩形gr_rect矩形
多边形gr_poly多边形
格式示例
(gr_line
  (start 100 100)
  (end 200 200)
  (angle 90)
  (layer "Dwgs.User")
  (width 0.12)
  (uuid "...")
)

(gr_circle
  (center 150 150)
  (end 145 150)
  (layer "Dwgs.User")
  (width 0.12)
  (uuid "...")
)

(gr_text
  (at 150 150)
  (text "Label")
  (layer "F.SilkS")
  (effects
    (font
      (size 1.27)
      (thickness 0.15)
    )
  )
  (uuid "...")
)
解析代码
# 当前 routing 工具不解析图形对象
# 它们用于文档和标注,不影响路由

数据映射

PCB 文件 → PCBData

.kicad_pcb 文件
    ↓
kicad_parser.py::parse_kicad_pcb()
    ↓
PCBData 对象
    ├── board_info: BoardInfo
    │   ├── layers: Dict[int, str]              # {0: "F.Cu", 1: "B.Cu"}
    │   ├── copper_layers: List[str]            # ["F.Cu", "B.Cu"]
    │   ├── board_bounds: Tuple                # (min_x, min_y, max_x, max_y)
    │   ├── stackup: List[StackupLayer]         # 层叠信息
    │   └── board_outline: List[Tuple[float,float]]
    │
    ├── nets: Dict[int, Net]
    │   └── Net(net_id, name, pads)
    │
    ├── footprints: Dict[str, Footprint]
    │   └── Footprint(reference, footprint_name, x, y, rotation, layer, value, pads)
    │
    ├── vias: List[Via]
    │   └── Via(x, y, size, drill, layers, net_id, uuid, free)
    │
    ├── segments: List[Segment]
    │   └── Segment(start_x, start_y, end_x, end_y, width, layer, net_id, uuid)
    │
    ├── pads_by_net: Dict[int, List[Pad]]
    │   └── [Pad(...), Pad(...), ...]
    │
    └── zones: List[Zone]
        └── Zone(net_id, net_name, layer, polygon, uuid)

完整示例

示例 PCB 文件片段

(kicad_pcb
  (version 20241229)
  (generator "pcbnew")
  (generator_version "9.0")

  (general
    (thickness 1.6)
    (legacy_teardrops no)
  )

  (paper "A4")

  (layers
    (0 "F.Cu" signal)
    (31 "B.Cu" signal)
    (25 "Edge.Cuts" user)
  )

  (setup
    (stackup
      (layer "F.Cu"
        (type "copper")
        (thickness 0.035)
      )
      (layer "dielectric 1"
        (type "core")
        (thickness 1.51)
        (material "FR4")
        (epsilon_r 4.5)
        (loss_tangent 0.02)
      )
      (layer "B.Cu"
        (type "copper")
        (thickness 0.035)
      )
      (copper_finish "None")
    )
  )

  (net 0 "")
  (net 1 "GND")
  (net 2 "+5V")
  (net 3 "/RESET")
  (net 4 "Net-(U1-Pad5)")

  (footprint "Resistor_SMD:R_0805_2012
    (layer "F.Cu")
    (uuid "...")
    (at 100.5 80.3)
    (descr "Resistor, 0805")
    (tags "Resistor SMD")
    (property "Reference" "R1"
      (at 0 0 0)
      (layer "F.SilkS")
    )
    (property "Value" "10k"
      (at 0 0 0)
      (layer "F.Fab")
    )
    (pad "1" smd roundrect
      (at -0.95 0)
      (size 1.2 0.7)
      (layers "F.Cu")
      (net 4 "Net-(R1-Pad1)")
      (uuid "...")
    )
    (pad "2" smd roundrect
      (at 0.95 0)
      (size 1.2 0.7)
      (layers "F.Cu")
      (net 3 "Net-(R1-Pad2)")
      (uuid "...")
    )
  )

  (segment
    (start 100.5 80.3)
    (end 120.7 100.5)
    (width 0.25)
    (layer "F.Cu")
    (net 3)
    (uuid "...")
  )

  (via
    (at 110.5 90.4)
    (size 0.6)
    (drill 0.3)
    (layers "F.Cu" "B.Cu")
    (net 1)
    (uuid "...")
  )

  (zone
    (net 1)
    (net_name "GND")
    (layer "B.Cu")
    (uuid "...")
    (pts
      (xy 50 50)
      (xy 50 150)
      (xy 200 150)
      (xy 200 50)
    )
    (connect_patchesyes)
  )
)

解析实现细节

正则表达式模式

网络模式
net_pattern = r'\(net\s+(\d+)\s+"([^"]*)"\)'
# 匹配: (net 1 "GND")
# Group 1: 网络 ID
# Group 2: 网络名称
焊盘模式
pad_pattern = r'\(pad\s+"([^"]*)"\s+(\w+)\s+(\w+)'
# 匹配: (pad "1" thru_hole circle
# Group 1: 焊盘编号
# Group 2: 焊盘类型 (thru_hole, smd, etc.)
# Group 3: 焊盘形状 (circle, rect, etc.)
轨道段模式
segment_pattern = r'\(segment\s+\(start\s+([\d.-]+)\s+([\d.-]+)\)\s+\(end\s+([\d.-]+)\s+([\d.-]+)\)\s+\(width\s+([\d.-]+)\)\s+\(layer\s+"([^"]+)"\)\s+\(net\s+(\d+)\)\s+\(uuid\s+"([^"]+)"\)'
过孔模式
via_pattern = r'\(via\s+\(at\s+([\d.-]+)\s+([\d.-]+)\)\s+\(size\s+([\d.-]+)\)\s+\(drill\s+([\d.-]+)\)\s+\(layers\s+"([^"]+)"\s+"([^"]+)"\)\s+(?:\(free\s+(yes|no)\)\s+)?\(net\s+(\d+)\)\s+\(uuid\s+"([^"]+)"\)'

嵌套括号处理

由于 S-表达式是嵌套的,需要平衡括号计数:

def find_matching_paren(text, start):
    """查找从 start 开始的匹配右括号"""
    depth = 0
    for i in range(start, len(text)):
        if text[i] == '(':
            depth += 1
        elif text[i] == ')':
            depth -= 1
            if depth == 0:
                return i
    return -1

精度处理

# 位置精度
POSITION_DECIMALS = 3  # 所有坐标保留3位小数

global_x = round(global_x, POSITION_DECIMALS)
global_y = round(global_y, POSITION_DECIMALS)

文件大小和性能

典型文件大小

设计类型元器件网络数文件大小
简单50-100100-500100-500 KB
中等500-10001000-50001-5 MB
复杂2000+5000+10-50 MB

解析性能

# 典型解析时间(中等复杂度 PCB)
# 文件大小: 5 MB
# 解析时间: ~0.5-2 秒

def parse_kicad_pcb(filepath: str) -> PCBData:
    # 使用正则表达式,快速扫描
    # 避免完整的 S-expression 解析树
    # 针对关键字段进行优化

版本兼容性

KiCad 版本

KiCad 版本文件格式版本兼容性
7.0.x20210608⚠️ 部分兼容
8.0.x20220525⚠️ 部分兼容
9.0.x20241229✅ 完全兼容
未来未来版本❌ 不兼容

主要变化

  • KiCad 7.0+ 引入了新的层命名约定
  • KiCad 8.0+ 改进了网络类管理
  • KiCad 9.0+ 更新了层叠格式

与其他格式的对比

格式扩展名复杂度可读性
KiCad PCB.kicad_pcb中等✅ 高

最佳实践

读取 PCB 文件

from kicad_parser import parse_kicad_pcb

# 解析 PCB 文件
pcb_data = parse_kicad_pcb("my_board.kicad_pcb")

# 访问数据
print(f"层数: {len(pcb_data.board_info.copper_layers)}")
print(f"网络数: {len(pcb_data.nets)}")
print(f"封装数: {len(pcb_data.footprints)}")
print(f"轨道段: {len(pcb_data.segments)}")
print(f"过孔数: {len(pcb_data.vias)}")

写入 PCB 文件

from kicad_writer import write_routed_output

# 写入路由结果
write_routed_output(
    output_file="my_board_routed.kicad_pcb",
    pcb_data=pcb_data,
    new_segments=new_segments,
    new_vias=new_vias
)

调试技巧

检查文件内容

# 查看网络定义
grep "^    (net " my_board.kicad_pcb | head -20

# 查看封装列表
grep "^    (footprint " my_board.kicad_pcb | head -10

# 查看轨道段
grep "^    (segment " my_board.kicad_pcb | wc -l

# 查看过孔
grep "^    (via " my_board.kicad_pcb | wc -l

验证解析结果

# 验证网络解析
for net_id, net in pcb_data.nets.items():
    print(f"Net {net_id}: {net.name} ({len(net.pads)} pads)")

# 验证焊盘解析
for ref, fp in pcb_data.footprints.items():
    print(f"{ref}: {len(fp.pads)} pads at ({fp.x}, {fp.y})")

常见问题

Q: 如何处理未连接的焊盘?

A: 未连接的焊盘会被分配自动生成的网络名:

(net 123 "unconnected-(U9-PadB5)")

这些网络在路由时会被识别为 stub 端点。

Q: 如何识别差分对?

A: 通过网络命名约定:

  • xxx_P, xxx_N 后缀
  • xxx+, xxx- 后缀
  • 相同的器件编号和引脚位置
# kicad_parser.py 中的识别
from net_queries import find_differential_pairs

diff_pairs = find_differential_pairs(pcb_data, ["*"])

Q: 如何获取板的边界?

A: 从 Edge.Cuts 层计算:

def extract_board_bounds(content: str):
    """Extract board bounds from Edge.Cuts graphics."""
    bounds_match = re.search(
        r'\(layer\s+"Edge\.Cuts".*?\(width\s+([\d.]+).*?\(height\s+([\d.]+)',
        content, re.DOTALL
    )
    if bounds_match:
        width = float(bounds_match.group(1))
        height = float(bounds_match.group(2))
        return (0, 0, width, height)

    # 或从图形多边形计算
    # ...

总结

KiCad PCB 文件包含的主要信息

  1. 网络定义: 所有电网络的列表
  2. 封装实例: 所有器件的放置和方向
  3. 焊盘: 所有焊盘的精确位置和属性
  4. 轨道段: 所有布线轨道
  5. 过孔: 所有层间连接
  6. 填充区域: 电源平面和其他填充区域
  7. 层定义: 所有PCB层的定义
  8. 设置和层叠: 板参数和材料属性
  9. 图形对象: 文档和标注

数据提取方式

  • 正则表达式: 快速扫描和匹配关键字段
  • 括号平衡: 处理嵌套的 S-表达式
  • 坐标变换: 局部坐标 → 全局坐标
  • 索引构建: 快速查找结构

路由工具的使用

KiCad Routing Tools 从 PCB 文件中提取:

  • ✅ 路由端点(未连接的焊盘)
  • ✅ 障碍物(已布线、过孔、焊盘)
  • ✅ 层信息(用于多层布线)
  • ✅ 层叠信息(用于阻抗控制)
  • ✅ 封装信息(用于 BGA 扇出)

这些信息构成了路由算法的基础输入。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐