1
0
mirror of https://github.com/whowechina/popn_pico.git synced 2025-03-03 16:43:49 +01:00
popn_pico/PCB/agg-kicad/scripts/build_mod_ic.py
2022-08-22 21:51:48 +08:00

665 lines
20 KiB
Python

"""
build_mod_ic.py
Copyright 2015 Adam Greig
Licensed under the MIT licence, see LICENSE file for details.
Create DFN, DFP, QFN, QFP, and BGA footprints from descriptions.
"""
from __future__ import print_function, division
# Style constants =============================================================
# Courtyard clearance
# Use 0.25 for IPC nominal and 0.10 for IPC least.
ctyd_gap = 0.25
# Courtyard grid
ctyd_grid = 0.05
# Courtyard line width
ctyd_width = 0.01
# Internal silk clearance from pads
silk_pad_igap = 0.5
# External silk clearance from pads
silk_pad_egap = 0.4
# Silk line width
silk_width = 0.15
# Internal silk pin1 arc radius
silk_pin1_ir = 0.6
# External two-row silk pin1 arc radius
silk_pin1_er = 0.3
# Fab layer line width
fab_width = 0.01
# Fab layer pin1 circle radius
fab_pin1_r = 0.4
# Fab layer pin1 corner offset
fab_pin1_offset = 0.8
# Ref/Val font size (width x height)
font_size = (1.0, 1.0)
# Ref/Val font thickness
font_thickness = 0.15
# Ref/Val font spacing from centre to top/bottom edge
font_halfheight = 0.7
# End Constants ===============================================================
import os
import re
import sys
import time
import math
import subprocess
import argparse
import yaml
import fnmatch
from sexp import parse as sexp_parse, generate as sexp_generate
from kicad_mod import fp_line, fp_arc, fp_circle, fp_text, pad, draw_square, model
def pin_centres(conf):
"""
Compute the locations of pin centres, (x, y).
Generates centres for a 4-row chip, just ignore top/bottom rows if 2 rows.
Returns (leftrow, bottomrow, rightrow, toprow).
"""
# Handle non-square chips differently
if "pins_first_row" in conf:
v_pins_per_row = conf['pins_first_row']
h_pins_per_row = (conf['pins'] - 2*v_pins_per_row) // 2
hx = conf['row_pitch'][0] / 2.0
vx = conf['row_pitch'][1] / 2.0
else:
h_pins_per_row = v_pins_per_row = conf['pins'] // conf['rows']
hx = vx = conf['row_pitch'] / 2.0
h_row_length = (h_pins_per_row - 1) * conf['pin_pitch']
v_row_length = (v_pins_per_row - 1) * conf['pin_pitch']
left_row = []
bottom_row = []
right_row = []
top_row = []
y = -v_row_length / 2.0
for pin in range(v_pins_per_row):
left_row.append((-hx, y))
right_row.insert(0, (hx, y))
y += conf['pin_pitch']
y = -h_row_length / 2.0
for pin in range(h_pins_per_row):
top_row.insert(0, (y, -vx))
bottom_row.append((y, vx))
y += conf['pin_pitch']
return left_row, bottom_row, right_row, top_row
def expand_skips(conf, letters=None):
"""
Computes list of all pins to skip given a list of specifiers which
may be either specific pins or ranges of pins.
Optional letters gives expanded list of BGA pin letters in use.
Returned list always contains strings.
"""
if letters is None:
letters = []
skips = conf.get("skip_pins", [])
out = []
for skip in skips:
skip = str(skip)
if "-" not in skip:
out.append(skip)
continue
match = re.search(r"^([A-Z]+(?:-[A-Z]+)?)?([0-9]+(?:-[0-9]+)?)$", skip)
if not match:
raise ValueError("Unknown skip specifier {}".format(skip))
let, num = match.groups()
if "-" in num:
num_start, num_stop = [int(x) for x in num.split("-")]
nums = list(range(num_start, num_stop+1))
else:
nums = [num]
if "-" in let:
let_start, let_stop = let.split("-")
let_start_idx = letters.index(let_start)
let_stop_idx = letters.index(let_stop)
lets = letters[let_start_idx:let_stop_idx+1]
else:
lets = [let]
for let in lets:
for num in nums:
out.append(let + str(num))
return out
def bga_pin_centres(conf):
"""
Compute the location of pin centres for BGA parts,
including skipping any in `skip_pins`.
Rows are labelled with letters and columns with numbers.
Returns a list of (pin number, x, y) positions.
"""
default_letters = "ABCDEFGHJKLMNPRTUVWY"
letters = conf.get("letters", default_letters)
letters = list(letters) + [a+b for a in letters for b in letters]
skips = expand_skips(conf, letters)
pitch = float(conf["pin_pitch"])
out = []
rows = int(conf['rows'])
cols = int(conf['cols'])
for row in range(rows):
for col in range(cols):
rowid = letters[row]
colid = col + 1
padid = rowid + str(colid)
if padid in skips:
continue
x = (col * pitch) - ((cols-1)/2.0 * pitch)
y = (row * pitch) - ((rows-1)/2.0 * pitch)
out.append((padid, x, y))
return out
def inner_apertures(ep, apertures):
"""
Compute the position of apertures inside a pad,
with:
ep: (width, height) of pad
apertures: (width, height, w_gap, h_gap) of apertures
w_gap is the spacing in the x-axis between apertures.
Fits as many apertures inside the pad as possible.
Returns a list of (x,y) aperture centres.
"""
out = []
ep_w, ep_h = ep
a_w, a_h, a_wg, a_hg = apertures
n_x = int((ep_w - a_w) // (a_w + a_wg)) + 1
n_y = int((ep_h - a_h) // (a_h + a_hg)) + 1
x = -((n_x - 1)*(a_w + a_wg)/2.0)
for ix in range(n_x):
y = -((n_y - 1)*(a_h + a_hg)/2.0)
for iy in range(n_y):
out.append((x, y))
y += a_h + a_hg
x += a_w + a_wg
return out
def exposed_pad(conf):
"""
Compute pads for an exposed pad, which might have separate mask and paste
apertures.
"""
out = []
ep_shape = conf['ep_shape']
ep_layers = ["F.Cu"]
ep_m_mask = None
ep_m_paste = None
# Mask apertures
if "ep_mask_shape" not in conf:
ep_layers.append("F.Mask")
ep_m_mask = 0.001
else:
mask_shape = conf['ep_mask_shape']
apertures = inner_apertures(ep_shape, mask_shape)
layers = ["F.Mask"]
for ap in apertures:
out.append(pad("", "smd", "rect", ap, mask_shape[:2], layers,
m_mask=.001))
# Paste apertures
if "ep_paste_shape" not in conf:
ep_layers.append("F.Paste")
ep_m_paste = 0.001
else:
paste_shape = conf['ep_paste_shape']
apertures = inner_apertures(ep_shape, paste_shape)
layers = ["F.Paste"]
for ap in apertures:
out.append(
pad("", "smd", "rect", ap, paste_shape[:2], layers,
m_paste=.001))
# Vias
if "ep_vias" in conf:
v_d, v_s, v_g = conf['ep_vias']
centres = inner_apertures(ep_shape, (v_s, v_s, v_g, v_g))
layers = ["*.Cu"]
for c in centres:
p = pad("EP", "thru_hole", "circle", c, (v_s, v_s), layers,
drill=v_d)
p.append(("zone_connect", 2))
out.append(p)
out.append(
pad("EP", "smd", "rect", (0, 0),
ep_shape, ep_layers, m_mask=ep_m_mask, m_paste=ep_m_paste))
return out
def refs(conf):
"""Generate val and ref labels."""
out = []
if conf['rows'] == 2 or 'cols' in conf:
ctyd_h = conf['chip_shape'][1] + 2 * ctyd_gap
elif conf['rows'] == 4:
# Handle non-square chips differently
if isinstance(conf['row_pitch'], float):
ctyd_h = conf['row_pitch'] + conf['pad_shape'][0] + 2 * ctyd_gap
else:
ctyd_h = conf['row_pitch'][1] + conf['pad_shape'][0] + 2 * ctyd_gap
y = ctyd_h/2.0 + font_halfheight
out.append(fp_text("reference", "REF**", (0, -y),
"F.Fab", font_size, font_thickness))
out.append(fp_text("value", conf['name'], (0, y),
"F.Fab", font_size, font_thickness))
return out
def fab(conf):
"""
Generate a drawing of the chip on the Fab layer, including its outline,
the outline of the pins, and a pin 1 indicator.
"""
if 'rows' in conf and 'cols' in conf:
bga = True
else:
bga = False
chip_w, chip_h = conf['chip_shape']
if bga:
pin_r = conf['pin_shape']
else:
pin_w, pin_h = conf['pin_shape']
out = []
# Chip outline
nw, _, _, _, sq = draw_square(chip_w, chip_h, (0, 0), "F.Fab", fab_width)
out += sq
# Pin 1
centre = (nw[0] + fab_pin1_offset, nw[1] + fab_pin1_offset)
end = (centre[0], centre[1] + fab_pin1_r)
out.append(fp_circle(centre, end, "F.Fab", fab_width))
# Pins
if bga:
centres = bga_pin_centres(conf)
for _, x, y in centres:
out += [fp_circle((x, y), (x, y+pin_r//2), "F.Fab", fab_width)]
else:
leftr, bottomr, rightr, topr = pin_centres(conf)
skips = [int(x) for x in expand_skips(conf)]
idx = 1
for pin in leftr:
idx += 1
if idx - 1 in skips:
continue
xy = -(chip_w + pin_w) / 2.0, pin[1]
_, _, _, _, sq = draw_square(pin_w, pin_h, xy, "F.Fab", fab_width)
out += [sq[0], sq[2], sq[3]]
for pin in rightr:
idx += 1
if idx - 1 in skips:
continue
xy = (chip_w + pin_w) / 2.0, pin[1]
_, _, _, _, sq = draw_square(pin_w, pin_h, xy, "F.Fab", fab_width)
out += [sq[0], sq[1], sq[2]]
if conf['rows'] == 4:
for pin in topr:
idx += 1
if idx - 1 in skips:
continue
xy = pin[0], -(chip_h + pin_w) / 2.0
_, _, _, _, sq = draw_square(pin_h, pin_w, xy, "F.Fab", fab_width)
out += [sq[0], sq[1], sq[3]]
for pin in bottomr:
idx += 1
if idx - 1 in skips:
continue
xy = pin[0], (chip_h + pin_w) / 2.0
_, _, _, _, sq = draw_square(pin_h, pin_w, xy, "F.Fab", fab_width)
out += [sq[1], sq[2], sq[3]]
return out
def internal_silk(conf):
"""
Generate an internal silkscreen, with an outline of the part and a pin1
indicator.
"""
out = []
pins = conf['pins']
rows = conf['rows']
pin_pitch = conf['pin_pitch']
row_pitch = conf['row_pitch']
pad_shape = conf['pad_shape']
width = row_pitch - pad_shape[0] - 2 * silk_pad_igap
if rows == 2:
height = (((pins // rows) - 1) * pin_pitch)
elif rows == 4:
height = width
ir = silk_pin1_ir
if ir > width:
ir = width
c = (0, 0)
layer = "F.SilkS"
nw, ne, se, sw, sq = draw_square(width, height, c, layer, silk_width)
out.append(fp_line((nw[0] + ir, nw[1]), ne, layer, silk_width))
out.append(fp_line(ne, se, layer, silk_width))
out.append(fp_line(se, sw, layer, silk_width))
out.append(fp_line(sw, (nw[0], nw[1] + ir), layer, silk_width))
start = (nw[0], nw[1] + ir)
end = (nw[0] + ir, nw[1])
out.append(fp_line(start, end, "F.SilkS", silk_width))
# Old circular pin1 indicator:
# out += sq
# start = nw
# end = (start[0] + silk_pin1_ir, start[1])
# out.append(fp_arc(start, end, 90, "F.SilkS", silk_width))
return out
def external_silk(conf):
"""
Generate an external silkscreen.
For two row devices: Horizontal lines top and bottom, semicircle pin 1
For four row devices: Three sharp corners and a cut corner for pin 1
"""
out = []
rows = conf['rows']
chip_shape = conf['chip_shape']
w = silk_width
l = "F.SilkS"
x = chip_shape[0]/2.0
y = chip_shape[1]/2.0
bga = 'cols' in conf
if rows == 2 and not bga:
r = silk_pin1_er
out.append(fp_line((-x+2*r, -y), (x, -y), l, w))
out.append(fp_line((-x, y), (x, y), l, w))
out.append(fp_arc((-x+r, -y), (-x, -y), 180, l, w))
elif rows == 4 or bga:
if bga:
dx = x//2
dy = y//2
else:
if 'pins_first_row' in conf:
v_pins_per_row = conf['pins_first_row']
h_pins_per_row = (conf['pins'] - 2*v_pins_per_row) // 2
pin_x = ((h_pins_per_row - 1) * conf['pin_pitch']) / 2.0
pin_y = ((v_pins_per_row - 1) * conf['pin_pitch']) / 2.0
else:
pins_per_row = conf['pins'] // rows
pin_x = pin_y = ((pins_per_row - 1) * conf['pin_pitch']) / 2.0
dx = x - pin_x - silk_pad_egap
dy = y - pin_y - silk_pad_egap
# NW
if bga:
dp1 = 1.0
out.append(fp_line((-x, -y+dy), (-x, -y+dp1), l, w))
out.append(fp_line((-x, -y+dp1), (-x+dp1, -y), l, w))
out.append(fp_line((-x+dx, -y), (-x+dp1, -y), l, w))
else:
dp1 = min(dx, dy)
out.append(fp_line((-x, -y+dp1), (-x+dp1, -y), l, w))
# NE
out.append(fp_line((x-dx, -y), (x, -y), l, w))
out.append(fp_line((x, -y), (x, -y+dy), l, w))
# SE
out.append(fp_line((x-dx, y), (x, y), l, w))
out.append(fp_line((x, y), (x, y-dy), l, w))
# SW
out.append(fp_line((-x+dx, y), (-x, y), l, w))
out.append(fp_line((-x, y), (-x, y-dy), l, w))
return out
def silk(conf):
if 'ep_shape' in conf or 'cols' in conf:
default = 'external'
else:
default = 'internal'
silk = conf.get('silk', default)
if silk == 'external':
return external_silk(conf)
elif silk == 'internal':
return internal_silk(conf)
else:
return []
def ctyd(conf):
chip_w, chip_h = conf['chip_shape']
if 'cols' in conf:
width, height = chip_w + 2*ctyd_gap, chip_h + 2*ctyd_gap
elif conf['rows'] == 2:
pad_w, pad_h = conf['pad_shape']
row_pitch = conf['row_pitch']
width = row_pitch + pad_w + 2 * ctyd_gap
height = chip_h + 2 * ctyd_gap
elif conf['rows'] == 4:
pad_w, pad_h = conf['pad_shape']
row_pitch = conf['row_pitch']
# We need to handle non-square chips slightly differently,
# depending on whether row_pitch is given as (w, h) or just a scalar.
if isinstance(row_pitch, float):
height = width = row_pitch + pad_w + 2 * ctyd_gap
else:
width = row_pitch[0] + pad_w + 2 * ctyd_gap
height = row_pitch[1] + pad_w + 2 * ctyd_gap
# Ensure courtyard lies on a specified grid
# (double the grid since we halve the width/height)
grid = 2*ctyd_grid
width = grid * int(math.ceil(width / (2*ctyd_grid)))
height = grid * int(math.ceil(height / (2*ctyd_grid)))
_, _, _, _, sq = draw_square(width, height, (0, 0), "F.CrtYd", ctyd_width)
return sq
def pad_row(centres, num, idx, shape, size, layers, skip):
out = []
for centre in centres:
idx += 1
if idx - 1 in skip:
continue
out.append(pad(num, "smd", shape, centre, size, layers))
num += 1
return num, idx, out
def pads(conf):
out = []
layers = ["F.Cu", "F.Mask", "F.Paste"]
size_lr = conf['pad_shape']
size_tb = size_lr[1], size_lr[0]
shape = "rect"
skip = conf.get('skip_pins', [])
leftr, btmr, rightr, topr = pin_centres(conf)
num = 1
idx = 1
num, idx, pins = pad_row(leftr, num, idx, shape, size_lr, layers, skip)
out += pins
if conf['rows'] == 4:
num, idx, pins = pad_row(btmr, num, idx, shape, size_tb, layers, skip)
out += pins
num, idx, pins = pad_row(rightr, num, idx, shape, size_lr, layers, skip)
out += pins
if conf['rows'] == 4:
num, idx, pins = pad_row(topr, num, idx, shape, size_tb, layers, skip)
out += pins
# Exposed pad (potentially with separate mask/paste apertures)
if "ep_shape" in conf:
out += exposed_pad(conf)
return out
def bga_pads(conf):
out = []
layers = ["F.Cu", "F.Mask", "F.Paste"]
size = [conf['pad_shape']]*2
margin = (conf['mask_shape'] - conf['pad_shape']) / 2.0
for num, x, y in bga_pin_centres(conf):
p = pad(num, "smd", "circle", (x, y), size, layers)
p.append(["solder_mask_margin", margin])
out.append(p)
return out
def _3d(conf):
if "model" in conf:
return [model(**conf["model"])]
else:
return []
def footprint(conf):
tedit = format(int(time.time()), 'X')
sexp = ["module", conf['name'], ("layer", "F.Cu"), ("tedit", tedit)]
sexp += refs(conf)
sexp += fab(conf)
sexp += silk(conf)
sexp += ctyd(conf)
sexp += pads(conf)
sexp += _3d(conf)
return sexp_generate(sexp)
def bga_footprint(conf):
tedit = format(int(time.time()), 'X')
sexp = ["module", conf['name'], ("layer", "F.Cu"), ("tedit", tedit)]
sexp += refs(conf)
sexp += fab(conf)
sexp += silk(conf)
sexp += ctyd(conf)
sexp += bga_pads(conf)
sexp += _3d(conf)
return sexp_generate(sexp)
def git_version(libpath):
args = ["git", "describe", "--abbrev=8", "--dirty=-dirty", "--always"]
git = subprocess.Popen(args, cwd=libpath, stdout=subprocess.PIPE)
return git.stdout.read().decode().strip()
def load_items(modpath):
config = {}
for dirpath, dirnames, files in os.walk(modpath):
dirnames.sort()
files.sort()
for fn in fnmatch.filter(files, "*.yaml"):
path = os.path.join(dirpath, fn)
with open(path) as f:
item = yaml.safe_load(f)
item["path"] = dirpath
config[item["name"]] = item
return config
def main(prettypath, modpath, verify=False, verbose=False):
config = load_items(modpath)
for name, conf in config.items():
conf['name'] = name
if 'rows' in conf and 'pins' in conf:
assert conf['rows'] in (2, 4), \
"Must have either two or four rows"
assert conf['pins'] % conf['rows'] == 0, \
"Pins must equally divide among rows"
fp = footprint(conf)
elif 'rows' in conf and 'cols' in conf:
fp = bga_footprint(conf)
else:
raise ValueError("Must specify either rows+pins or rows+cols")
path = os.path.join(prettypath, name+".kicad_mod")
if verify and verbose:
print("Verifying", path)
# Check if an identical part already exists
if os.path.isfile(path):
with open(path) as f:
old = f.read()
old = [n for n in sexp_parse(old) if n[0] != "tedit"]
new = [n for n in sexp_parse(fp) if n[0] != "tedit"]
if new == old:
continue
# If not, either verification failed or we should output the new fp
if verify:
return False
else:
with open(path, "w") as f:
f.write(fp)
# If we finished and didn't return yet, verification has succeeded.
if verify:
return True
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("prettypath", type=str, help=
"Path to footprints to process")
parser.add_argument("modpath", type=str, help=
"Path to .yaml files defining footprints")
parser.add_argument("--verify", action="store_true", help=
"Verify libraries are up to date")
parser.add_argument("--verbose", action="store_true", help=
"Print out every library verified")
args = vars(parser.parse_args())
result = main(**args)
if args['verify']:
if result:
print("OK: all footprints up-to-date.")
sys.exit(0)
else:
print("Error: footprints not up-to-date.", file=sys.stderr)
sys.exit(1)