kicad:EasyEDA→KiCad 工程(Board1/Board2)、库、脚本与评审文档(IC 引脚、风扇、热、EDL 供电、布局规则)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,303 @@
|
||||
"""Generate the scoped four-wire prototype revision; retain a pre-change snapshot.
|
||||
|
||||
Only P6 and the unused U20.IO5 connection on P3 are changed. Existing migration
|
||||
helpers are deliberately not used to regenerate the rest of the project.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
from schematic_maintenance import ROOT, parse, child, children, properties
|
||||
|
||||
LIB = 'PhoneRail-Fan'
|
||||
KICAD = Path('D:/KiCad/share/kicad')
|
||||
PAGE = ROOT / 'PhoneRail-Board1/P6-FAN-EDL.kicad_sch'
|
||||
MARKER = 'FOUR-WIRE FAN PROTOTYPE R1'
|
||||
|
||||
|
||||
def uid(key):
|
||||
return str(uuid.uuid5(uuid.NAMESPACE_URL, 'phonerail/fan-r1/' + key))
|
||||
|
||||
|
||||
def quote(value):
|
||||
return json.dumps(str(value), ensure_ascii=False)
|
||||
|
||||
|
||||
def symbol_from(path, name, new_name):
|
||||
source = path.read_text(encoding='utf-8')
|
||||
node = next(n for n in children(parse(source), 'symbol') if n[1] == name)
|
||||
assert child(node, 'extends') is None
|
||||
raw = source[node.start:node.end]
|
||||
return raw.replace('"' + name, '"' + new_name)
|
||||
|
||||
|
||||
def prop(name, value, x, y, hidden=False):
|
||||
return (f'(property {quote(name)} {quote(value)} (at {x:g} {y:g} 0) '
|
||||
f'(effects (font (size 1.27 1.27)) {"(hide yes)" if hidden else ""}))')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--apply', action='store_true')
|
||||
args = ap.parse_args()
|
||||
old = PAGE.read_text(encoding='utf-8')
|
||||
if MARKER in old or 'SANYO FAN SELECTION R2' in old:
|
||||
print('Already revised; generation skipped. Edit the current schematic directly.')
|
||||
return
|
||||
assert 'LEGACY THREE-PIN CIRCUIT' in old
|
||||
root = parse(old)
|
||||
original = {properties(n)['Reference'][2]: n for n in children(root, 'symbol')}
|
||||
board = ROOT / 'PhoneRail-Board1'
|
||||
top_id = child(parse((board / 'PhoneRail-Board1.kicad_sch').read_text(encoding='utf-8')), 'uuid')[1]
|
||||
page_id = child(root, 'uuid')[1]
|
||||
sheet_path = child(child(child(original['Q6'], 'instances'), 'project'), 'path')[1]
|
||||
assert sheet_path.startswith('/' + top_id)
|
||||
shared = ROOT / 'lib/PhoneRail.kicad_sym'
|
||||
definitions = {
|
||||
'R': symbol_from(shared, '0402WGF1002TCE', 'R'),
|
||||
'NMOS': symbol_from(shared, 'AO3400A', 'NMOS'),
|
||||
'PMOS': symbol_from(KICAD / 'symbols/Transistor_FET.kicad_sym', 'TP0610T', 'PMOS'),
|
||||
'Zener': symbol_from(KICAD / 'symbols/Device.kicad_sym', 'D_Zener', 'Zener'),
|
||||
}
|
||||
# Gate/input and channel/passive follow KiCad's standard discrete MOS symbols.
|
||||
raw = definitions['NMOS']
|
||||
for unit in children(parse(raw), 'symbol'):
|
||||
for pin in sorted(children(unit, 'pin'), key=lambda p: p.start, reverse=True):
|
||||
kind = 'input' if child(pin, 'number')[1] == '1' else 'passive'
|
||||
raw = raw[:pin.start] + raw[pin.start:pin.end].replace('pin unspecified', 'pin ' + kind) + raw[pin.end:]
|
||||
definitions['NMOS'] = raw
|
||||
for ref, key in [('D2', 'Schottky'), ('C738', 'C')]:
|
||||
libid = child(original[ref], 'lib_id')[1].split(':')[1]
|
||||
definitions[key] = symbol_from(shared, libid, key)
|
||||
definitions['FanHeader'] = '''(symbol "FanHeader"
|
||||
(pin_names (offset 1.016)) (in_bom yes) (on_board yes)
|
||||
(property "Reference" "J" (at 0 5.08 0) (effects (font (size 1.27 1.27))))
|
||||
(property "Value" "47053-1000" (at 0 -10.16 0) (effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(symbol "FanHeader_0_1" (rectangle (start 0 2.54) (end 17.78 -10.16)
|
||||
(stroke (width 0.254) (type default)) (fill (type background))))
|
||||
(symbol "FanHeader_1_1"
|
||||
''' + ''.join(f'''(pin passive line (at -5.08 {-2.54*(i-1):g} 0) (length 5.08)
|
||||
(name "{name}" (effects (font (size 1.27 1.27))))
|
||||
(number "{i}" (effects (font (size 1.27 1.27)))))'''
|
||||
for i, name in enumerate(['GND', '+12V', 'TACH', 'PWM'], 1)) + '))'
|
||||
# These are generic drawing primitives. Do not retain the source part's
|
||||
# voltage rating, datasheet or footprint as inherited library metadata.
|
||||
for key, raw in definitions.items():
|
||||
edits = []
|
||||
for name, value in [('Value', key), ('Footprint', ''), ('Datasheet', ''),
|
||||
('Description', 'Four-wire prototype: see instance MPN and FAN_CIRCUIT.md')]:
|
||||
p = properties(parse(raw)).get(name)
|
||||
if p:
|
||||
edits.append((p.start, p.end, prop(name, value, 0, 0, name != 'Value')))
|
||||
for start, end, replacement in sorted(edits, reverse=True):
|
||||
raw = raw[:start] + replacement + raw[end:]
|
||||
definitions[key] = raw
|
||||
parts, pins, items = {}, {}, []
|
||||
|
||||
def add(ref, key, x, y, value, footprint, mpn, angle=0, manufacturer='', datasheet='', lcsc=''):
|
||||
assert ref not in parts
|
||||
sx = child(original[ref], 'uuid')[1] if ref in original else uid(ref)
|
||||
parts[ref] = sx
|
||||
node = parse(definitions[key])
|
||||
coords = {}
|
||||
for unit in children(node, 'symbol'):
|
||||
for pin in children(unit, 'pin'):
|
||||
px, py = map(float, child(pin, 'at')[1:3])
|
||||
t = math.radians(angle)
|
||||
coords[child(pin, 'number')[1]] = (round(x + px*math.cos(t) - py*math.sin(t), 4),
|
||||
round(y - px*math.sin(t) - py*math.cos(t), 4))
|
||||
pins[ref] = coords
|
||||
if key in {'NMOS', 'PMOS'}:
|
||||
tx, ty = x + (10.16 if angle == 0 else -12.7), y
|
||||
else:
|
||||
tx, ty = x, y - (5.08 if angle == 0 else 8.89)
|
||||
metadata = {'Footprint': footprint, 'MPN': mpn, 'Manufacturer': manufacturer,
|
||||
'Datasheet': datasheet, 'LCSC': lcsc}
|
||||
items.append(f'(symbol (lib_id "{LIB}:{key}") (at {x:g} {y:g} {angle}) (unit 1) '
|
||||
f'(in_bom yes) (on_board yes) (dnp no) (uuid "{sx}") '
|
||||
+ prop('Reference', ref, tx, ty - 2.54)
|
||||
+ prop('Value', value, tx, ty)
|
||||
+ ''.join(prop(k, v, x, y, True) for k, v in metadata.items())
|
||||
+ ''.join(f'(pin "{p}" (uuid "{uid(ref + "." + p)}"))' for p in coords)
|
||||
+ f'(instances (project "PhoneRail-Board1" (path "{sheet_path}" (reference "{ref}") (unit 1)))))')
|
||||
|
||||
def wire(a, b):
|
||||
assert a != b
|
||||
assert a[0] == b[0] or a[1] == b[1], (a, b)
|
||||
items.append(f'(wire (pts (xy {a[0]:g} {a[1]:g}) (xy {b[0]:g} {b[1]:g})) '
|
||||
f'(stroke (width 0) (type default)) (uuid "{uid(str((a,b)))}"))')
|
||||
|
||||
def join(*points):
|
||||
for a, b in zip(points, points[1:]):
|
||||
wire(a, b)
|
||||
|
||||
def label(net, pos, global_net=False):
|
||||
typ = 'global_label' if global_net or net in {'+12V', '+3V3', 'GND', 'FAN_EN', 'FAN_PWM', 'FAN_TACH_IN', 'FAN_TACH_OUT'} else 'label'
|
||||
extra = '(shape bidirectional)' if typ == 'global_label' else ''
|
||||
items.append(f'({typ} {quote(net)} {extra} (at {pos[0]:g} {pos[1]:g} 0) '
|
||||
f'(effects (font (size 1.27 1.27)) (justify left bottom)) (uuid "{uid(net+str(pos))}"))')
|
||||
|
||||
def pin(ref, number):
|
||||
return pins[ref][str(number)]
|
||||
|
||||
def terminal(ref, number, net, dx=0, dy=5.08):
|
||||
p = pin(ref, number)
|
||||
q = (round(p[0]+dx, 4), round(p[1]+dy, 4))
|
||||
wire(p, q)
|
||||
label(net, q)
|
||||
|
||||
def resistor(ref, value, x, y, angle=0):
|
||||
code = {1000: '1001', 10000: '1002', 100000: '1003'}[value]
|
||||
add(ref, 'R', x, y, f'{value//1000}k', 'PhoneRail:R0402', '0402WGF'+code+'TCE', angle, 'UNI-ROYAL')
|
||||
|
||||
def mos(ref, x, y, pm=False):
|
||||
mpn = 'AO3401A' if pm else 'AO3400A'
|
||||
add(ref, 'PMOS' if pm else 'NMOS', x, y, mpn,
|
||||
'PhoneRail:SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR', mpn,
|
||||
180 if pm else 0, 'Alpha & Omega Semiconductor',
|
||||
f'https://www.aosmd.com/sites/default/files/res/datasheets/{mpn}.pdf')
|
||||
|
||||
def note(x, y, text, size=1.27):
|
||||
items.append(f'(text {quote(text)} (at {x:g} {y:g} 0) '
|
||||
f'(effects (font (size {size} {size})) (justify left bottom)) (uuid "{uid(text)}"))')
|
||||
|
||||
note(15.24, 15.24, MARKER, 2.0)
|
||||
note(15.24, 22.86, '2x NF-A6x25 PWM / J26 intake / J27 exhaust. Vendor limits and prototype tests OPEN.')
|
||||
note(15.24, 30.48, 'HIGH-SIDE POWER: GPIO5 high = ON; hardware reset = OFF', 1.5)
|
||||
mos('Q7', 76.2, 55.88, True)
|
||||
terminal('Q7', 2, '+12V', dy=-15.24)
|
||||
terminal('Q7', 3, 'FAN_12V_SW')
|
||||
resistor('R740', 100000, 109.22, 40.64, 90)
|
||||
terminal('R740', 2, '+12V', dy=-5.08)
|
||||
join(pin('Q7', 1), (109.22, 55.88), pin('R740', 1))
|
||||
add('DZ3', 'Zener', 137.16, 40.64, 'BZT52C8V2-7-F', f'{LIB}:D_SOD-123', 'BZT52C8V2-7-F', 270, 'Diodes Incorporated', 'https://www.diodes.com/assets/Datasheets/ds18004.pdf')
|
||||
terminal('DZ3', 1, '+12V', dy=-5.08)
|
||||
join(pin('DZ3', 2), (137.16, 55.88), (109.22, 55.88))
|
||||
resistor('R741', 1000, 109.22, 76.2, 90)
|
||||
wire((109.22, 55.88), pin('R741', 2))
|
||||
mos('Q8', 106.68, 101.6)
|
||||
wire(pin('R741', 1), pin('Q8', 3))
|
||||
terminal('Q8', 2, 'GND')
|
||||
resistor('R742', 1000, 71.12, 101.6)
|
||||
terminal('R742', 1, 'FAN_EN', dx=-7.62, dy=0)
|
||||
join(pin('R742', 2), (91.44, 101.6), pin('Q8', 1))
|
||||
resistor('R743', 100000, 91.44, 119.38, 90)
|
||||
wire((91.44, 101.6), pin('R743', 2))
|
||||
terminal('R743', 1, 'GND')
|
||||
for ref, key, x, y in [('D2', 'Schottky', 27.94, 76.2), ('C738', 'C', 27.94, 114.3)]:
|
||||
p = properties(original[ref])
|
||||
add(ref, key, x, y, p['Value'][2], p['Footprint'][2], p['MPN'][2], 90,
|
||||
p['Manufacturer'][2], p['Datasheet'][2], p['LCSC'][2])
|
||||
# Imported diode: pin 1 cathode. Imported capacitor: pin 1 left.
|
||||
terminal(ref, 1, 'FAN_12V_SW', dx=-7.62, dy=0)
|
||||
terminal(ref, 2, 'GND', dx=7.62, dy=0)
|
||||
note(15.24, 142.24, 'Q7 body diode blocks +12V -> fan when OFF. No reverse isolation/current limiting.')
|
||||
note(15.24, 149.86, 'DZ3 clamps |Vgs|; validate clamp tolerance, startup SOA and short-circuit protection.')
|
||||
|
||||
note(15.24, 162.56, 'OPEN-DRAIN PWM: GPIO18 high pulls fan pin 4 LOW', 1.5)
|
||||
mos('Q6', 106.68, 193.04)
|
||||
resistor('R733', 1000, 71.12, 193.04)
|
||||
terminal('R733', 1, 'FAN_PWM', dx=-7.62, dy=0)
|
||||
join(pin('R733', 2), (91.44, 193.04), pin('Q6', 1))
|
||||
resistor('R734', 100000, 91.44, 213.36, 90)
|
||||
wire((91.44, 193.04), pin('R734', 2))
|
||||
terminal('R734', 1, 'GND')
|
||||
terminal('Q6', 2, 'GND')
|
||||
terminal('Q6', 3, 'FAN_PWM_OD', dy=-7.62)
|
||||
note(15.24, 238.76, '25kHz target; invert MCU duty: D_gpio = 1 - D_fan. Confirm vendor limits.')
|
||||
note(15.24, 246.38, 'Stop/reset: FAN_EN=0. PWM alone does not guarantee a stopped fan.')
|
||||
note(15.24, 254, 'Firmware: two counters, startup grace, per-fan stall fault and commanded-OFF mask.')
|
||||
note(15.24, 266.7, 'PROTOTYPE ONLY: see FAN_CIRCUIT.md. Not released for PCB fabrication.', 1.5)
|
||||
|
||||
for ref, x, raw in [('J26', 284.48, 'FAN_IN_TACH_RAW'), ('J27', 378.46, 'FAN_OUT_TACH_RAW')]:
|
||||
add(ref, 'FanHeader', x, 48.26, '47053-1000', f'{LIB}:FanPinHeader_1x04_P2.54mm_Vertical', '470531000', manufacturer='Molex')
|
||||
for p, net in enumerate(['GND', 'FAN_12V_SW', raw, 'FAN_PWM_OD'], 1):
|
||||
terminal(ref, p, net, dx=-7.62, dy=0)
|
||||
note(223.52, 76.2, 'Keyed PC fan header: 1 GND / 2 +12V / 3 TACH / 4 PWM.')
|
||||
note(223.52, 83.82, 'Confirm header drawing, key orientation and cable fit before layout.')
|
||||
note(223.52, 96.52, 'INDEPENDENT TACH: switched pullups avoid a 3V3 path into an unpowered fan', 1.5)
|
||||
for idx, y in enumerate([132.08, 215.9]):
|
||||
ref = ['Q9', 'Q10'][idx]
|
||||
raw = ['FAN_IN_TACH_RAW', 'FAN_OUT_TACH_RAW'][idx]
|
||||
output = ['FAN_TACH_IN', 'FAN_TACH_OUT'][idx]
|
||||
a, b, c, d = [f'R{744 + idx*4 + j}' for j in range(4)]
|
||||
mos(ref, 332.74, y)
|
||||
resistor(a, 10000, 246.38, y-17.78, 90)
|
||||
terminal(a, 2, 'FAN_12V_SW', dy=-5.08)
|
||||
label(raw, (246.38, y))
|
||||
wire(pin(a, 1), (246.38, y))
|
||||
resistor(b, 10000, 279.4, y)
|
||||
wire((246.38, y), pin(b, 1))
|
||||
join(pin(b, 2), (307.34, y), pin(ref, 1))
|
||||
resistor(c, 10000, 307.34, y+17.78, 90)
|
||||
wire((307.34, y), pin(c, 2))
|
||||
terminal(c, 1, 'GND')
|
||||
terminal(ref, 2, 'GND')
|
||||
resistor(d, 10000, 363.22, y-17.78, 90)
|
||||
terminal(d, 2, '+3V3', dy=-5.08)
|
||||
join(pin(ref, 3), (335.28, y-7.62), (363.22, y-7.62), pin(d, 1))
|
||||
terminal(d, 1, output, dx=7.62, dy=0)
|
||||
note(223.52, y+35.56, f'{output}: inverted RAW; MCU GPIO{16 if idx == 0 else 4}. Count one edge per pulse.')
|
||||
note(223.52, 264.16, 'RAW high: Vgate ~ Vfan/3. RAW low: Vgate ~ Vtach_low/2.')
|
||||
note(223.52, 271.78, 'Electrical limits, hot-plug/ESD, power sequencing and thermal validation remain open.')
|
||||
|
||||
# Explicit junctions for every drawn T connection, independent of label semantics.
|
||||
for x, y in [(109.22,55.88), (91.44,101.6), (91.44,193.04),
|
||||
(307.34,132.08), (307.34,215.9), (363.22,119.38), (363.22,203.2)]:
|
||||
items.append(f'(junction (at {x:g} {y:g}) (diameter 0) (color 0 0 0 0) (uuid "{uid("junction"+str((x,y)))}"))')
|
||||
|
||||
embedded = '\n'.join(raw.replace('(symbol "'+key+'"', '(symbol "'+LIB+':'+key+'"', 1) for key, raw in definitions.items())
|
||||
page = (f'(kicad_sch (version 20250610) (generator "eeschema") (uuid "{page_id}") (paper "A3") '
|
||||
'(title_block (title "P6-FAN-EDL / Four-wire fan prototype") (rev "FAN-R1") (date "2026-09-07")) '
|
||||
f'(lib_symbols {embedded})\n' + '\n'.join(items) + '\n(embedded_fonts no))\n')
|
||||
parse(page)
|
||||
existing_refs = set()
|
||||
for path in board.glob('*.kicad_sch'):
|
||||
if path != PAGE:
|
||||
existing_refs.update(properties(n)['Reference'][2] for n in children(parse(path.read_text(encoding='utf-8')), 'symbol'))
|
||||
assert not existing_refs.intersection(parts), existing_refs.intersection(parts)
|
||||
control = board / 'P3-CONTROL.kicad_sch'
|
||||
source = control.read_text(encoding='utf-8')
|
||||
control_root = parse(source)
|
||||
mcu = next(n for n in children(control_root, 'symbol') if properties(n)['Reference'][2] == 'U20')
|
||||
assert child(mcu, 'at')[1:] == ['64.77', '66.04', '0']
|
||||
io5 = (76.2, 27.94)
|
||||
assert 'FAN_EN' not in source
|
||||
for n in children(control_root, 'wire'):
|
||||
assert io5 not in [tuple(map(float, p[1:])) for p in children(child(n, 'pts'), 'xy')]
|
||||
extra = f'''(wire (pts (xy 76.2 27.94) (xy 91.44 27.94)) (stroke (width 0) (type default)) (uuid "{uid('io5-wire')}"))
|
||||
(global_label "FAN_EN" (shape input) (at 91.44 27.94 0) (effects (font (size 1.27 1.27)) (justify left)) (uuid "{uid('io5-label')}"))'''
|
||||
source = source[:control_root.end-1] + extra + source[control_root.end-1:]
|
||||
outputs = {PAGE: page, control: source,
|
||||
ROOT / f'lib/{LIB}.kicad_sym': '(kicad_symbol_lib (version 20241209) (generator "kicad_symbol_editor")\n' + '\n'.join(definitions.values()) + '\n)\n'}
|
||||
for name, category in [('FanPinHeader_1x04_P2.54mm_Vertical', 'Connector'), ('D_SOD-123', 'Diode_SMD')]:
|
||||
outputs[ROOT / f'lib/{LIB}.pretty/{name}.kicad_mod'] = (KICAD / f'footprints/{category}.pretty/{name}.kicad_mod').read_text(encoding='utf-8')
|
||||
for table, kind, uri in [('sym-lib-table', 'KiCad', f'${{KIPRJMOD}}/../lib/{LIB}.kicad_sym'),
|
||||
('fp-lib-table', 'KiCad', f'${{KIPRJMOD}}/../lib/{LIB}.pretty')]:
|
||||
path = board / table
|
||||
src = path.read_text(encoding='utf-8')
|
||||
end = parse(src).end - 1
|
||||
assert LIB not in src
|
||||
outputs[path] = src[:end] + f'(lib (name "{LIB}") (type "{kind}") (uri "{uri}") (options "") (descr "Four-wire prototype additions"))\n' + src[end:]
|
||||
print(f'P6: {len(parts)} parts; Board1 expected total {284 - 7 + len(parts)}')
|
||||
for path in outputs:
|
||||
print(path.relative_to(ROOT))
|
||||
if not args.apply:
|
||||
return
|
||||
backup = ROOT / 'out/before-four-wire'
|
||||
backup.mkdir(exist_ok=False)
|
||||
for path in [PAGE, control, board/'sym-lib-table', board/'fp-lib-table', ROOT/'out/Board1.xml', ROOT/'out/Board2.xml']:
|
||||
shutil.copy2(path, backup / path.name)
|
||||
for path, content in outputs.items():
|
||||
path.parent.mkdir(exist_ok=True)
|
||||
path.write_text(content, encoding='utf-8', newline='\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户