board
Solar MPPT Power System
Public Unreviewedby TylerK
A 30 W solar battery charger with regulated 5 V and 3.3 V rails: an STM32G031 running perturb & observe MPPT and a CC/CV charge state machine, steering a TI LM5175 4-switch buck-boost through a single analog control input. Four-layer KiCad board, schematic and PCB generated from Python, with an electrothermal simulation run against the published board file.
main
TylerK
Replace discrete-analog board with MCU/IC redesign: STM32G031 P&O MPPT + CC/CV firmware, LM5175 buck-boost, INA180 curr…
e48ba30
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
#!/usr/bin/env python3
"""MCU/IC-based solar MPPT power system -- schematic generator.
Panel(6-24V) -> fuse -> ideal-diode reverse-polarity (LM74700+NFET) -> TVS ->
bulk caps -> LM5175 4-switch synchronous buck-boost (MCU-driven MPPT/CC/CV
FB reference) -> 12V SLA/AGM battery bus -> {TPS62130 5V/2A, TLV62569 3.3V/1A}
POL bucks. INA180 current-sense amps (panel Iin, battery Ichg) + Vin/Vbatt
dividers feed an STM32G031F8Px MCU running P&O MPPT + CC/CV charge firmware.
Unlike the old discrete-analog board's generator, every real connection here
is a drawn wire (schgen2.Sch.connect/net/power), not a net-name label.
"""
import sys, os
sys.path.insert(0, os.path.dirname(__file__))
from schgen2 import Sch, check_balance
from symlib import SymLib
PROJECT = "mppt-power-system-mcu"
SYM = "/mnt/c/Program Files/KiCad/10.0/share/kicad/symbols"
LIBNAMES = ['Device', 'Connector_Generic', 'power', 'Diode', 'Transistor_FET',
'Regulator_Switching', 'Regulator_Linear', 'Amplifier_Current',
'Power_Management', 'MCU_ST_STM32G0']
LIBS = [f"{SYM}/{n}.kicad_sym" for n in LIBNAMES]
lib = SymLib(LIBS)
S = Sch(lib, PROJECT)
DEV, CONN, PWR, DIO, FET = 'Device', 'Connector_Generic', 'power', 'Diode', 'Transistor_FET'
RSW, RLIN, AMPC, PWRM, MCU = ('Regulator_Switching', 'Regulator_Linear',
'Amplifier_Current', 'Power_Management', 'MCU_ST_STM32G0')
R_THT = 'Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal'
R_SMD = 'Resistor_SMD:R_0805_2012Metric'
C_SMD = 'Capacitor_SMD:C_0805_2012Metric'
C_SMD_1206 = 'Capacitor_SMD:C_1206_3216Metric'
C_ELEC = 'Capacitor_SMD:CP_Elec_6.3x5.8'
SOT23 = 'Package_TO_SOT_SMD:SOT-23'
SOT23_5 = 'Package_TO_SOT_SMD:SOT-23-5'
SOD123 = 'Diode_SMD:D_SOD-123'
SMB = 'Diode_SMD:D_SMB'
IND_SMD = 'Inductor_SMD:L_1210_3225Metric'
IND_PWR = 'Inductor_SMD:L_Sunlord_MWSA1040S'
FP_LM5165 = "Package_SON:Texas_S-PVSON-N10"
FP_LM5175 = "Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.75x6.2mm_ThermalVias"
FP_TLV62569 = "Package_TO_SOT_SMD:SOT-563"
FP_TPS62130 = "Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm"
FP_INA180 = "Package_TO_SOT_SMD:SOT-23-5"
FP_AMS1117 = "Package_TO_SOT_SMD:SOT-223-3_TabPin2"
FP_IND_22U = "Inductor_SMD:L_Sunlord_MWSA1005S"
def block_title(t, x, y):
S.text(t, x, y, size=3.2)
def sub(t, x, y):
S.text(t, x, y, size=1.8, bold=False)
# ============================================================================
# BLOCK 1 -- SOLAR INPUT & IDEAL-DIODE REVERSE-POLARITY PROTECTION
# ============================================================================
BX, BY = 20.0, 20.0
block_title("BLOCK 1: SOLAR INPUT PROTECTION", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 150, BY + 110)
S.place(CONN, 'Conn_01x02', 1, 'J1', 'SOLAR_IN', BX + 10, BY + 30,
footprint="TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-2-3.5-H_1x02_P3.50mm_Horizontal")
S.power('J1', '2', 'GND')
S.drive_rail('J1', '2', 'GND') # GND originates at the input connector -- one driver for the whole board's GND net
S.place(DEV, 'Fuse', 1, 'F1', '6.3A', BX + 35, BY + 20,
footprint="Fuse:Fuseholder_Clip-5x20mm_Keystone_3512_Inline_P23.62x7.27mm_D1.02x1.57mm_Horizontal")
S.connect('J1', '1', 'F1', '1')
# Ideal-diode reverse-polarity block: Q1 NFET in series, U1 LM74700 controller
S.place(FET, 'IRF3205', 1, 'Q1', 'IRF3205 (55V NFET)', BX + 65, BY + 25, rot=270,
footprint="Package_TO_SOT_THT:TO-220-3_Vertical")
S.connect('F1', '2', 'Q1', 'D')
S.drive_rail('Q1', 'D', 'PANEL_P_FUSED') # ANODE is a voltage-sense input mistyped power_in by the vendor symbol
S.place(PWRM, 'LM74700', 1, 'U1', 'LM74700', BX + 65, BY + 55,
footprint="Package_TO_SOT_SMD:SOT-23-6")
S.connect('U1', 'ANODE', 'F1', '2')
S.connect('U1', 'GATE', 'Q1', 'G')
S.connect('U1', 'CATHODE', 'Q1', 'S')
S.power('U1', 'GND', 'GND')
S.place(DEV, 'C', 1, 'C1', '10n', BX + 45, BY + 60, rot=90,
footprint=C_SMD)
S.connect('C1', '1', 'U1', 'VCAP', via=[(60.96,70.0),(80.0,70.0),(80.0,77.47)])
S.connect('C1', '2', 'U1', 'CATHODE')
S.place(DEV, 'R', 1, 'R1', '100k', BX + 45, BY + 45, rot=90, footprint=R_SMD)
S.connect('R1', '1', 'U1', 'ANODE', via=[(60.96,58.0),(77.47,58.0)])
S.connect('R1', '2', 'U1', 'EN', via=[(68.58,72.0),(90.0,72.0),(90.0,80.01)])
S.drive_rail('R1', '2', 'EN_PULLUP') # EN pulled high through R1; own net has no power_out pin (R1 breaks the graph)
S.place(DEV, 'D_Zener', 1, 'D1', 'BZT52C15 (15V)', BX + 90, BY + 25, rot=90,
footprint=SOD123)
S.connect('D1', 'K', 'Q1', 'D')
S.connect('D1', 'A', 'Q1', 'G')
# Downstream protected node PANEL_P_PROT = Q1 source
S.place(DEV, 'D_TVS', 1, 'D2', 'SMBJ33A (33V)', BX + 110, BY + 40, rot=90,
footprint=SMB)
S.connect('D2', 'A1', 'Q1', 'S')
S.power('D2', 'A2', 'GND')
S.drive_rail('D2', 'A1', 'PANEL_PROT')
S.place(DEV, 'C', 1, 'C2', '100u', BX + 125, BY + 25, footprint=C_ELEC)
S.place(DEV, 'C', 1, 'C3', '100u', BX + 135, BY + 25, footprint=C_ELEC)
S.place(DEV, 'C', 1, 'C4', '1u', BX + 145, BY + 25, footprint=C_SMD)
S.net([('D2', 'A1'), ('C2', '1'), ('C3', '1'), ('C4', '1')])
S.power('C2', '2', 'GND')
S.power('C3', '2', 'GND')
S.power('C4', '2', 'GND')
PANEL_PROT_REF, PANEL_PROT_PIN = 'D2', 'A1' # tap point for downstream blocks
# ============================================================================
# BLOCK 2 -- HOUSEKEEPING BIAS SUPPLY (self-starting, panel-referenced)
# ============================================================================
BX, BY = 20.0, 150.0
block_title("BLOCK 2: HOUSEKEEPING BIAS (5V then 3V3)", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 150, BY + 105)
S.place(RSW, 'LM5165', 1, 'U2', 'LM5165 (wide-Vin micro-buck)', BX + 45, BY + 30,
footprint=FP_LM5165)
# Long cross-block hop: routed through the clear right margin (x=180, past
# both blocks' boxes which end at x=170) then the clear gutter between the
# two block boxes (y=140, between block1's bottom at y=130 and block2's top
# at y=150), then straight down into VIN from above -- VIN is U2's topmost
# pin, so nothing else is in that final vertical approach either.
S.connect(PANEL_PROT_REF, PANEL_PROT_PIN, 'U2', 'VIN',
via=[(180.0, 63.5), (180.0, 140.0), (54.61, 140.0)])
S.place(DEV, 'C', 1, 'C5', '1u', BX + 25, BY + 30, rot=90, footprint=C_SMD)
S.connect('U2', 'VIN', 'C5', '1')
S.power('C5', '2', 'GND')
S.connect('U2', 'EN', 'U2', 'VIN', bend='v') # EN tied high -> always enabled
S.place(DEV, 'L', 1, 'L1', '22u', BX + 75, BY + 22, footprint=FP_IND_22U)
S.connect('U2', 'SW', 'L1', '1', bend='v') # avoid routing through L1's own pin column
S.place(DEV, 'C', 1, 'C6', '10u', BX + 100, BY + 26, footprint=C_SMD_1206) # off L1's own Y so C6.2 doesn't land on the rail
S.place(DEV, 'C', 1, 'C7', '100n', BX + 110, BY + 26, footprint=C_SMD)
S.net([('L1', '2'), ('C6', '1'), ('C7', '1')])
S.power('C6', '2', 'GND')
S.power('C7', '2', 'GND')
S.drive_rail('L1', '2', 'BIAS_5V')
S.place(DEV, 'R', 1, 'R2', '40.2k', BX + 100, BY + 45, rot=90, footprint=R_SMD)
S.place(DEV, 'R', 1, 'R3', '10k', BX + 100, BY + 60, rot=90, footprint=R_SMD)
S.connect('R2', '1', 'L1', '2')
S.connect('R2', '2', 'R3', '1')
S.connect('R3', '1', 'U2', 'FB', bend='v')
S.power('R3', '2', 'GND')
S.place(DEV, 'C', 1, 'C8', '1n', BX + 55, BY + 55, rot=90, footprint=C_SMD)
S.connect('C8', '1', 'U2', 'SS', bend='h')
S.power('C8', '2', 'GND')
S.place(DEV, 'R', 1, 'R6', '0', BX + 55, BY + 75, rot=90, footprint=R_SMD) # HYS->GND: min hysteresis
S.connect('U2', 'HYS', 'R6', '1', bend='v')
S.power('R6', '2', 'GND')
S.noconn('U2', 'RT') # RT floating = internal default switching freq
S.place(DEV, 'R', 1, 'R4', '4.99k', BX + 45, BY + 65, rot=90, footprint=R_SMD)
S.connect('R4', '1', 'L1', '2')
S.connect('R4', '2', 'U2', 'ILIM', bend='v')
S.place(DEV, 'R', 1, 'R5', '10k', BX + 30, BY + 45, rot=90, footprint=R_SMD) # PGOOD pull-up
S.connect('R5', '1', 'L1', '2')
S.connect('R5', '2', 'U2', 'PGOOD', bend='v')
S.label('BIAS_PGOOD', *S.pin_xy('U2', 'PGOOD')[:2], rot=180, just='right bottom')
S.power('U2', 'GND', 'GND')
S.power('U2', 'PAD', 'GND')
# ---- 3.3V logic rail from the 5V bias (AMS1117-3.3 LDO) -------------------
S.place(RLIN, 'AMS1117-3.3', 1, 'U3', 'AMS1117-3.3', BX + 125, BY + 40, footprint=FP_AMS1117)
S.connect('U3', 'VI', 'L1', '2')
S.place(DEV, 'C', 1, 'C9', '1u', BX + 115, BY + 25, rot=90, footprint=C_SMD)
S.connect('C9', '1', 'U3', 'VI')
S.power('C9', '2', 'GND')
S.place(DEV, 'C', 1, 'C10', '10u', BX + 140, BY + 40, footprint=C_SMD_1206)
S.connect('C10', '1', 'U3', 'VO')
S.power('C10', '2', 'GND')
S.power('U3', 'GND', 'GND')
BIAS5_REF, BIAS5_PIN = 'L1', '2'
BIAS33_REF, BIAS33_PIN = 'U3', 'VO'
# ============================================================================
# BLOCK 3 -- MPPT / CHARGE POWER STAGE: LM5175 4-switch synchronous buck-boost
# ============================================================================
BX, BY = 210.0, 20.0
block_title("BLOCK 3: MPPT/CHARGE POWER STAGE (LM5175, 4-switch buck-boost)", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 260, BY + 210)
S.place(RSW, 'LM5175PWP', 1, 'U4', 'LM5175 (4-switch sync buck-boost)', BX + 110, BY + 90,
footprint=FP_LM5175)
# ---- Input: current-sense resistor, then VIN/VISNS ------------------------
S.place(DEV, 'R', 1, 'RS1', '0.01 (2512, 1W)', BX + 30, BY + 40, footprint='Resistor_SMD:R_2512_6332Metric')
S.connect(PANEL_PROT_REF, PANEL_PROT_PIN, 'RS1', '1', via=[(180.0, 63.5), (180.0, 15.0), (BX + 30, 15.0)])
S.connect('RS1', '2', 'U4', 'VIN', bend='v')
S.drive_rail('RS1', '2', 'MPPT_VIN_SENSE') # RS1 breaks the net graph from PANEL_PROT
S.connect('U4', 'ISNS(+)', 'RS1', '1', via=[(334.0, 128.27), (334.0, 15.0)])
S.connect('U4', 'ISNS(-)', 'RS1', '2', via=[(332.0, 125.73), (332.0, 17.0)])
S.place(DEV, 'C', 1, 'C11', '10u', BX + 55, BY + 55, footprint=C_SMD_1206)
S.connect('C11', '1', 'RS1', '2')
S.power('C11', '2', 'GND')
S.place(DEV, 'R', 1, 'R7', '100', BX + 70, BY + 40, rot=90, footprint=R_SMD) # VISNS filter
S.connect('R7', '1', 'RS1', '2')
S.connect('R7', '2', 'U4', 'VISNS', bend='v')
S.place(DEV, 'C', 1, 'C12', '1n', BX + 80, BY + 45, rot=90, footprint=C_SMD)
S.connect('C12', '1', 'U4', 'VISNS')
S.power('C12', '2', 'GND')
# EN/UVLO divider from Vin
S.place(DEV, 'R', 1, 'R8', '100k', BX + 15, BY + 55, rot=90, footprint=R_SMD)
S.place(DEV, 'R', 1, 'R9', '10k', BX + 15, BY + 70, rot=90, footprint=R_SMD)
S.connect('R8', '1', 'RS1', '2', bend='v')
S.connect('R8', '2', 'R9', '1')
S.connect('R9', '1', 'U4', 'EN/UVLO', bend='v')
S.power('R9', '2', 'GND')
S.connect('U4', 'MODE', 'U4', 'AGND', bend='h') # 4-switch buck-boost mode select
S.connect('U4', 'DITH', 'U4', 'AGND', bend='h') # dither disabled
S.place(DEV, 'R', 1, 'R10', '150k', BX + 15, BY + 90, rot=90, footprint=R_SMD) # RT/SYNC freq-set
S.connect('R10', '1', 'U4', 'RT/SYNC', bend='v')
S.power('R10', '2', 'GND')
S.place(DEV, 'R', 1, 'R11', '20k', BX + 15, BY + 105, rot=90, footprint=R_SMD) # slope compensation
S.connect('R11', '1', 'U4', 'SLOPE', bend='v')
S.power('R11', '2', 'GND')
S.place(DEV, 'C', 1, 'C13', '10n', BX + 30, BY + 90, rot=90, footprint=C_SMD) # soft start
S.connect('C13', '1', 'U4', 'SS', bend='v')
S.power('C13', '2', 'GND')
S.place(DEV, 'R', 1, 'R12', '10k', BX + 30, BY + 105, rot=90, footprint=R_SMD) # comp network
S.place(DEV, 'C', 1, 'C14', '2.2n', BX + 30, BY + 120, rot=90, footprint=C_SMD)
S.connect('R12', '1', 'U4', 'COMP', bend='v')
S.connect('R12', '2', 'C14', '1')
S.power('C14', '2', 'GND')
S.connect('U4', 'CS', 'U4', 'CSG', bend='h') # secondary current-sense unused: tied together
S.connect('U4', 'CSG', 'U4', 'AGND', bend='v')
S.power('U4', 'AGND', 'GND')
S.power('U4', 'PGND', 'GND')
S.power('U4', 'PowerPAD', 'GND')
# ---- VCC/BIAS local bypass -------------------------------------------------
S.connect('U4', 'BIAS', BIAS5_REF, BIAS5_PIN,
via=[(330.0, 95.25), (330.0, BY + 4 - 15), (100.0, BY + 4 - 15)])
S.place(DEV, 'C', 1, 'C15', '1u', BX + 150, BY + 55, rot=90, footprint=C_SMD)
S.connect('C15', '1', 'U4', 'VCC', via=[(328.0, 92.71), (328.0, 74.93)])
S.drive_rail('C15', '1', 'MPPT_VCC') # VCC is the internal LDO's own output pin, decoupled locally
S.power('C15', '2', 'GND')
# ---- Switching leg 1 (chop VIN -> SW1) ------------------------------------
S.place(FET, 'IRF3205', 1, 'Q2', 'IRF3205 (leg1 HS)', BX + 160, BY + 30, rot=270,
footprint="Package_TO_SOT_THT:TO-220-3_Vertical")
S.connect('RS1', '2', 'Q2', 'D', via=[(BX + 35, BY + 15), (BX + 160, BY + 15)])
S.connect('U4', 'HDRV1', 'Q2', 'G', via=[(330.0, 100.33), (330.0, 40.0), (369.57, 40.0)])
S.place(DEV, 'C', 1, 'C16', '100n', BX + 145, BY + 20, rot=90, footprint=C_SMD) # BOOT1 cap
S.connect('C16', '1', 'U4', 'BOOT1', via=[(326.0, 102.87), (326.0, 10.0), (351.79, 10.0)])
S.place(DEV, 'D_Schottky', 1, 'D3', 'BAT54', BX + 135, BY + 15, rot=90, footprint=SOD123)
S.connect('D3', 'K', 'U4', 'BOOT1', via=[(326.0, 102.87)])
S.connect('D3', 'A', 'C15', '1', bend='h') # bootstrap fed from VCC
S.place(FET, 'IRF3205', 1, 'Q3', 'IRF3205 (leg1 LS)', BX + 160, BY + 70, rot=270,
footprint="Package_TO_SOT_THT:TO-220-3_Vertical")
S.connect('Q2', 'S', 'Q3', 'D', bend='v')
S.connect('C16', '2', 'Q2', 'S', bend='v')
S.connect('U4', 'LDRV1', 'Q3', 'G', via=[(327.0, 107.95), (327.0, 80.0), (369.57, 80.0)])
S.power('Q3', 'S', 'GND')
# ---- Main power inductor between SW1 and SW2 -------------------------------
S.place(DEV, 'L', 1, 'L2', '68u (Isat>=6A)', BX + 195, BY + 50, rot=90,
footprint='Inductor_SMD:L_Bourns-SRN1060')
S.connect('Q2', 'S', 'L2', '1', bend='h')
S.connect('U4', 'SW1', 'Q2', 'S', via=[(324.0, 105.41), (324.0, 52.07)])
# ---- Switching leg 2 (SW2 -> VBATT) ---------------------------------------
S.place(FET, 'IRF3205', 1, 'Q4', 'IRF3205 (leg2 HS)', BX + 235, BY + 30, rot=270,
footprint="Package_TO_SOT_THT:TO-220-3_Vertical")
S.connect('L2', '2', 'Q4', 'S', bend='v')
S.connect('U4', 'SW2', 'Q4', 'S', via=[(BX + 205, BY + 12), (BX + 235, BY + 12)])
S.connect('U4', 'HDRV2', 'Q4', 'G', via=[(324.0, 115.57), (324.0, 36.0), (444.5, 36.0)])
S.place(DEV, 'C', 1, 'C17', '100n', BX + 220, BY + 20, rot=90, footprint=C_SMD) # BOOT2 cap
S.connect('C17', '1', 'U4', 'BOOT2', via=[(322.0, 118.11), (322.0, 13.0), (426.72, 13.0)])
S.place(DEV, 'D_Schottky', 1, 'D4', 'BAT54', BX + 210, BY + 15, rot=90, footprint=SOD123)
S.connect('D4', 'K', 'U4', 'BOOT2', via=[(322.0, 118.11)])
S.connect('D4', 'A', 'C15', '1', via=[(BX + 205, BY + 8), (BX + 150, BY + 8)])
S.place(FET, 'IRF3205', 1, 'Q5', 'IRF3205 (leg2 LS)', BX + 235, BY + 70, rot=270,
footprint="Package_TO_SOT_THT:TO-220-3_Vertical")
S.connect('Q4', 'D', 'Q5', 'D', bend='v')
S.connect('C17', '2', 'Q4', 'S', bend='v')
S.connect('U4', 'LDRV2', 'Q5', 'G', via=[(321.0, 123.19), (321.0, 76.0), (444.5, 76.0)])
S.power('Q5', 'S', 'GND')
VBATT_REF, VBATT_PIN = 'Q4', 'D'
# ---- Output bulk caps + FB / VOSNS dividers off VBATT ----------------------
S.place(DEV, 'C', 1, 'C18', '100u', BX + 255, BY + 60, footprint=C_ELEC) # away from rail_y so C18.2(GND) has clearance
S.place(DEV, 'C', 1, 'C19', '100u', BX + 255, BY + 45, footprint=C_ELEC)
S.place(DEV, 'C', 1, 'C20', '1u', BX + 240, BY + 45, footprint=C_SMD)
S.net([(VBATT_REF, VBATT_PIN), ('C18', '1'), ('C19', '1'), ('C20', '1')])
S.power('C18', '2', 'GND')
S.power('C19', '2', 'GND')
S.power('C20', '2', 'GND')
# FB summing node (3 resistors into U4.FB, VFB = 1.0V typ per LM5175 datasheet):
# VBATT = VFB*(1 + R13/R14 + R13/R15) - (R13/R15)*V_MCU_MPPT_REF
# R13=120k, R14=10k, R15=68k -> VBATT ranges from ~14.76V (V_MCU_REF=0V,
# PWM duty=0%, the failsafe/idle ceiling if firmware is inactive) down to
# ~8.9V (V_MCU_REF=3.3V, PWM duty=100%) -- brackets the 14.2V CV target
# with the MCU trimming DOWN from the ceiling, plus headroom below for
# CC/MPPT throttling. (Original values here -- R13=46.4k/R15=10k -- gave
# only a 10.28V ceiling, physically unable to ever reach 14.2V; fixed
# during firmware bring-up once the transfer function was worked through.)
S.place(DEV, 'R', 1, 'R13', '120k', BX + 195, BY + 120, rot=90, footprint=R_SMD) # FB top
S.place(DEV, 'R', 1, 'R14', '10k', BX + 200, BY + 135, rot=90, footprint=R_SMD) # FB bottom (off R13's X so R14.1 doesn't land on R13.1)
S.connect('R13', '1', VBATT_REF, VBATT_PIN, via=[(BX + 195, BY + 100), (BX + 255, BY + 100)])
S.net([('R13', '2'), ('R14', '1'), ('U4', 'FB')])
S.power('R14', '2', 'GND')
S.place(DEV, 'R', 1, 'R15', '68k', BX + 170, BY + 135, rot=90, footprint=R_SMD) # MCU MPPT-ref summing
S.connect('R15', '1', 'R13', '2', bend='h')
S.label('MCU_MPPT_REF', *S.pin_xy('R15', '2')[:2], rot=0, just='left bottom', kind='local')
# TODO: VOSNS(OVP) trip point not independently verified against the real
# LM5175 datasheet OVP reference/threshold -- this ratio was carried over
# from R13/R14's ORIGINAL (since-corrected) values without its own
# derivation. Flagged for the documentation pass; does not block firmware.
S.place(DEV, 'R', 1, 'R16', '46.4k', BX + 220, BY + 120, rot=90, footprint=R_SMD) # VOSNS(OVP) top
S.place(DEV, 'R', 1, 'R17', '10k', BX + 220, BY + 135, rot=90, footprint=R_SMD)
S.connect('R16', '1', VBATT_REF, VBATT_PIN, via=[(BX + 220, BY + 95), (BX + 255, BY + 95)])
S.connect('R16', '2', 'R17', '1')
S.connect('R17', '1', 'U4', 'VOSNS', bend='v')
S.power('R17', '2', 'GND')
S.place(DEV, 'R', 1, 'R18', '10k', BX + 100, BY + 150, rot=90, footprint=R_SMD) # PGOOD pull-up
S.connect('R18', '1', BIAS5_REF, BIAS5_PIN, via=[(BX + 100, BY + 145), (100.0, BY + 145)])
S.connect('R18', '2', 'U4', 'PGOOD', bend='v')
S.label('MPPT_PGOOD', *S.pin_xy('U4', 'PGOOD')[:2], rot=180, just='right bottom')
# ============================================================================
# BLOCK 4 -- CURRENT / VOLTAGE SENSING (panel Iin, battery Ichg, Vbatt)
# ============================================================================
BX, BY = 210.0, 250.0
block_title("BLOCK 4: CURRENT/VOLTAGE SENSING (-> MCU ADC)", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 260, BY + 90)
# Panel input current: reuses RS1 (already in Block 3) -- one shunt read by
# both the LM5175's own average-current loop AND this MCU-facing amplifier.
S.place(AMPC, 'INA180A1', 1, 'U5', 'INA180A1 (Iin, x20V/V)', BX + 30, BY + 30,
footprint=FP_INA180)
S.connect('U5', '+', 'RS1', '1', via=[(BX + 10, 45.72), (BX + 10, BY + 22)])
S.connect('U5', '-', 'RS1', '2', via=[(BX + 5, 50.8), (BX + 5, BY + 27)])
S.connect('U5', 'V+', BIAS5_REF, BIAS5_PIN, via=[(BX + 30, BY - 5), (100.0, BY - 5)])
S.power('U5', 'GND', 'GND')
S.place(DEV, 'C', 1, 'C21', '100n', BX + 15, BY + 35, rot=90, footprint=C_SMD)
S.connect('C21', '1', 'U5', 'V+', bend='v')
S.power('C21', '2', 'GND')
S.label('IIN_SENSE', *S.pin_xy('U5', '1')[:2], rot=0, just='left bottom')
# Battery charge current: its own shunt RS2, in series between the power
# stage's raw output bulk-cap node and the actual battery bus.
S.place(DEV, 'R', 1, 'RS2', '0.02 (2512, 1W)', BX + 90, BY + 20, footprint='Resistor_SMD:R_2512_6332Metric')
S.connect(VBATT_REF, VBATT_PIN, 'RS2', '1', via=[(BX + 90, 60.0), (BX + 90, BY + 15)])
S.place(DEV, 'C', 1, 'C22', '100u', BX + 115, BY + 15, footprint=C_ELEC)
S.intend('RS2', '2', 'C22', '1') # direct path would cross RS2.1 (only 7.62mm away); jog around it
S.wire([S.pin_xy('C22', '1')[:2], (295.0, 261.62), (295.0, 274.32), S.pin_xy('RS2', '2')[:2]])
S.power('C22', '2', 'GND')
S.drive_rail('C22', '1', 'BATT_BUS')
BATT_BUS_REF, BATT_BUS_PIN = 'C22', '1' # same net as RS2.2, but physically clear of RS2's own tight pin spacing
S.place(AMPC, 'INA180A1', 1, 'U6', 'INA180A1 (Ichg, x20V/V)', BX + 130, BY + 40,
footprint=FP_INA180)
S.intend('U6', '+', 'RS2', '1') # direct path would cross RS2.2 (only 7.62mm away); jog around it
S.wire([S.pin_xy('U6', '+')[:2], (290.0, 287.02), (290.0, 266.7), S.pin_xy('RS2', '1')[:2]])
S.connect('U6', '-', 'RS2', '2', via=[(BX + 115, BY + 55), (BX + 115, BY + 37)])
S.connect('U6', 'V+', BIAS5_REF, BIAS5_PIN, via=[(BX + 130, BY - 5), (110.0, BY - 5)])
S.power('U6', 'GND', 'GND')
S.place(DEV, 'C', 1, 'C23', '100n', BX + 115, BY + 45, rot=90, footprint=C_SMD)
S.connect('C23', '1', 'U6', 'V+', bend='v')
S.power('C23', '2', 'GND')
S.label('ICHG_SENSE', *S.pin_xy('U6', '1')[:2], rot=0, just='left bottom')
# Battery bus voltage sense divider (14.2V CV target, same as the old board)
S.place(DEV, 'R', 1, 'R19', '46.4k', BX + 180, BY + 25, rot=90, footprint=R_SMD)
S.place(DEV, 'R', 1, 'R20', '10k', BX + 180, BY + 40, rot=90, footprint=R_SMD)
S.connect('R19', '1', BATT_BUS_REF, BATT_BUS_PIN, via=[(BX + 180, BY + 15), (BX + 116, BY + 15)])
S.connect('R19', '2', 'R20', '1')
S.power('R20', '2', 'GND')
S.label('VBATT_SENSE', *S.pin_xy('R19', '2')[:2], rot=90, just='left bottom')
# ============================================================================
# BLOCK 5 -- MCU (STM32G031F8Px): P&O MPPT + CC/CV charge firmware host
# ============================================================================
BX, BY = 210.0, 360.0
block_title("BLOCK 5: MCU (STM32G031F8Px) + SWD/UART/status", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 220, BY + 140)
S.place(MCU, 'STM32G031F8Px', 1, 'U7', 'STM32G031F8Px', BX + 110, BY + 70,
footprint="Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm")
S.place(DEV, 'C', 1, 'C24', '100n', BX + 80, BY + 40, rot=90, footprint=C_SMD)
S.connect('C24', '1', 'U7', 'VDD', bend='v')
S.power('C24', '2', 'GND')
S.place(DEV, 'C', 1, 'C25', '1u', BX + 90, BY + 40, rot=90, footprint=C_SMD)
S.connect('C25', '1', 'U7', 'VDD', bend='v')
S.power('C25', '2', 'GND')
S.connect(BIAS33_REF, BIAS33_PIN, 'U7', 'VDD', via=[(150.0, BY - 5), (BX + 110, BY - 5)])
S.power('U7', 'VSS', 'GND')
# ---- ADC sense inputs (real short stub + label -- MCU fan-out signals are
# the one place labels carry the connection: 6+ same-side pins wired
# straight across the page would be page-filling clutter, not clarity) ----
def adc_stub(pin, label_text):
x, y, dx, dy = S.pin_xy('U7', pin)
ex, ey = snap(x + dx * 6 * 1.27), y
S.wire([(x, y), (ex, ey)])
S.label(label_text, ex, ey, rot=(0 if dx > 0 else 180),
just=('left bottom' if dx > 0 else 'right bottom'))
from schgen2 import snap
adc_stub('PA0', 'VBATT_SENSE')
adc_stub('PA1', 'IIN_SENSE')
adc_stub('PA2', 'ICHG_SENSE')
adc_stub('PA8/PB0/PB1/PB2', 'MPPT_PGOOD')
adc_stub('PB3/PB4/PB5/PB6', 'BIAS_PGOOD')
# PA4: PWM output, RC-filtered into an analog reference, summed into LM5175 FB
S.place(DEV, 'R', 1, 'R22', '10k', BX + 40, BY + 90, rot=90, footprint=R_SMD)
S.place(DEV, 'C', 1, 'C26', '100n', BX + 55, BY + 100, rot=90, footprint=C_SMD)
S.connect('U7', 'PA4', 'R22', '1', bend='h')
S.connect('R22', '2', 'C26', '1')
S.power('C26', '2', 'GND')
S.label('MCU_MPPT_REF', *S.pin_xy('R22', '2')[:2], rot=0, just='left bottom')
# UART debug telemetry (USART1: PA9=TX, PA10=RX)
adc_stub('PA9/PA11', 'UART_TX')
adc_stub('PA10/PA12', 'UART_RX')
# Status LEDs
S.place(DEV, 'R', 1, 'R23', '1k', BX + 165, BY + 30, rot=90, footprint=R_SMD)
S.place(DEV, 'LED', 1, 'LED1', 'GREEN (charge)', BX + 165, BY + 45, rot=90, footprint='LED_SMD:LED_0805_2012Metric')
S.connect('U7', 'PA6', 'R23', '1', bend='h')
S.connect('R23', '2', 'LED1', 'A')
S.power('LED1', 'K', 'GND')
S.place(DEV, 'R', 1, 'R24', '1k', BX + 180, BY + 30, rot=90, footprint=R_SMD)
S.place(DEV, 'LED', 1, 'LED2', 'RED (fault)', BX + 180, BY + 45, rot=90, footprint='LED_SMD:LED_0805_2012Metric')
S.connect('U7', 'PA7', 'R24', '1', bend='h')
S.connect('R24', '2', 'LED2', 'A')
S.power('LED2', 'K', 'GND')
# SWD programming header
S.place(CONN, 'Conn_01x04', 1, 'J5', 'SWD', BX + 20, BY + 110,
footprint="Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical")
S.connect(BIAS33_REF, BIAS33_PIN, 'J5', '1', via=[(150.0, BY + 100), (BX + 20, BY + 100)])
S.connect('U7', 'PA13', 'J5', '2', via=[(BX + 140, BY + 115), (BX + 20, BY + 115)])
S.connect('U7', 'PA14/PA15', 'J5', '3', via=[(BX + 140, BY + 120), (BX + 20, BY + 120)])
S.power('J5', '4', 'GND')
# unused GPIO explicitly tied off (kept ERC-meaningful, matching the old
# board's own "no floating pins" convention)
for p in ['PB7/PB8', 'PB9/PC14', 'PC15', 'PF2', 'PA3', 'PA5']:
S.noconn('U7', p)
# ============================================================================
# BLOCK 6 -- 5V / 2A POL BUCK (TPS62130), fed from the battery bus
# ============================================================================
BX, BY = 460.0, 20.0
block_title("BLOCK 6: 5V / 2A POL BUCK (TPS62130)", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 140, BY + 100)
S.place(RSW, 'TPS62130', 1, 'U8', 'TPS62130 (5V/2A buck)', BX + 60, BY + 40, footprint=FP_TPS62130)
S.connect(BATT_BUS_REF, BATT_BUS_PIN, 'U8', 'VIN', via=[(340.0, 10.0), (BX + 45, 10.0)])
S.connect('U8', 'VIN', 'U8', '11', bend='h')
S.connect('U8', 'VIN', 'U8', '12', bend='h')
S.place(DEV, 'C', 1, 'C27', '10u', BX + 30, BY + 30, footprint=C_SMD_1206)
S.connect('C27', '1', 'U8', 'VIN', bend='v')
S.power('C27', '2', 'GND')
S.connect('U8', 'EN', 'U8', 'VIN', bend='v')
S.connect('U8', 'DEF', 'U8', 'GND', bend='v')
S.connect('U8', 'FSW', 'U8', 'GND', bend='v')
S.place(DEV, 'C', 1, 'C28', '10n', BX + 45, BY + 60, rot=90, footprint=C_SMD)
S.connect('C28', '1', 'U8', 'SS/TR', bend='v')
S.power('C28', '2', 'GND')
S.power('U8', 'GND', 'GND')
S.power('U8', '15', 'GND')
S.power('U8', '16', 'GND')
S.power('U8', '17', 'GND')
S.place(DEV, 'L', 1, 'L3', '4.7u (Isat>=3A)', BX + 90, BY + 22, footprint='Inductor_SMD:L_Bourns_SRN6045TA')
S.connect('U8', '1', 'L3', '1', bend='v')
S.connect('U8', '2', 'U8', '1', bend='h')
S.connect('U8', '3', 'U8', '1', bend='h')
S.place(DEV, 'C', 1, 'C29', '47u', BX + 115, BY + 35, footprint=C_SMD_1206)
S.place(DEV, 'C', 1, 'C30', '1u', BX + 125, BY + 35, footprint=C_SMD)
S.net([('L3', '2'), ('C29', '1'), ('C30', '1')])
S.power('C29', '2', 'GND')
S.power('C30', '2', 'GND')
V5V_REF, V5V_PIN = 'L3', '2'
S.drive_rail('L3', '2', 'V5V_RAIL')
S.place(DEV, 'R', 1, 'R25', '61.9k', BX + 95, BY + 55, rot=90, footprint=R_SMD) # FB top (5V)
S.place(DEV, 'R', 1, 'R26', '10k', BX + 95, BY + 70, rot=90, footprint=R_SMD) # FB bottom
S.connect('R25', '1', 'L3', '2', bend='v')
S.connect('R25', '2', 'R26', '1')
S.connect('R26', '1', 'U8', 'FB', bend='v')
S.power('R26', '2', 'GND')
S.connect('U8', 'VOS', 'L3', '2', bend='v')
S.place(DEV, 'R', 1, 'R27', '10k', BX + 70, BY + 55, rot=90, footprint=R_SMD) # PG pull-up
S.connect('R27', '1', 'L3', '2', bend='v')
S.connect('R27', '2', 'U8', 'PG', bend='v')
S.label('V5V_PGOOD', *S.pin_xy('U8', 'PG')[:2], rot=180, just='right bottom')
# ============================================================================
# BLOCK 7 -- 3.3V / 1A POL BUCK (TLV62569), fed from the battery bus
# ============================================================================
BX, BY = 460.0, 140.0
block_title("BLOCK 7: 3.3V / 1A POL BUCK (TLV62569)", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 140, BY + 90)
S.place(RSW, 'TLV62569DRL', 1, 'U9', 'TLV62569 (3.3V/1A buck)', BX + 60, BY + 35, footprint=FP_TLV62569)
S.connect(BATT_BUS_REF, BATT_BUS_PIN, 'U9', 'VIN', via=[(325.12, BY - 15.0), (BX + 45, BY - 15.0)])
S.place(DEV, 'C', 1, 'C31', '10u', BX + 30, BY + 25, footprint=C_SMD_1206)
S.connect('C31', '1', 'U9', 'VIN', bend='v')
S.power('C31', '2', 'GND')
S.connect('U9', 'EN', 'U9', 'VIN', bend='v')
S.power('U9', 'GND', 'GND')
S.noconn('U9', 'NC')
S.place(DEV, 'L', 1, 'L4', '2.2u (Isat>=1.5A)', BX + 90, BY + 20, footprint='Inductor_SMD:L_Bourns-SRN4018')
S.connect('U9', 'SW', 'L4', '1', bend='v')
S.place(DEV, 'C', 1, 'C32', '22u', BX + 115, BY + 30, footprint=C_SMD_1206)
S.place(DEV, 'C', 1, 'C33', '1u', BX + 125, BY + 30, footprint=C_SMD)
S.net([('L4', '2'), ('C32', '1'), ('C33', '1')])
S.power('C32', '2', 'GND')
S.power('C33', '2', 'GND')
V3V3_LOAD_REF, V3V3_LOAD_PIN = 'L4', '2'
S.drive_rail('L4', '2', 'V3V3_RAIL')
S.place(DEV, 'R', 1, 'R28', '20k', BX + 95, BY + 50, rot=90, footprint=R_SMD) # FB top (3.3V)
S.place(DEV, 'R', 1, 'R29', '10k', BX + 95, BY + 65, rot=90, footprint=R_SMD) # FB bottom
S.connect('R28', '1', 'L4', '2', bend='v')
S.connect('R28', '2', 'R29', '1')
S.connect('R29', '1', 'U9', 'FB', bend='v')
S.power('R29', '2', 'GND')
# ============================================================================
# BLOCK 8 -- OUTPUT PROTECTION / FILTERING (ferrite bead + decoupling)
# ============================================================================
BX, BY = 460.0, 250.0
block_title("BLOCK 8: OUTPUT FILTERING", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 140, BY + 70)
S.place(DEV, 'FerriteBead', 1, 'FB1', '600R@100MHz', BX + 30, BY + 25, rot=270, footprint=C_SMD)
# Fully explicit, individually-obstacle-verified path (connect()'s automatic
# stub-then-corner logic kept re-landing on FB1's own other pin or L3's own
# other pin in this dense area, then jogging straight onto a THIRD problem --
# whack-a-mole. Every segment below was checked with S._obstacles_on() first.
S.intend(V5V_REF, V5V_PIN, 'FB1', '1')
S.wire([S.pin_xy(V5V_REF, V5V_PIN)[:2], (545.0, 45.72), (545.0, 7.0), (500.0, 7.0),
(500.0, 275.59), S.pin_xy('FB1', '1')[:2]])
S.place(DEV, 'C', 1, 'C34', '100n', BX + 45, BY + 30, rot=90, footprint=C_SMD)
S.intend('FB1', '2', 'C34', '1')
S.wire([S.pin_xy('FB1', '2')[:2], (486.41, 283.0), (501.65, 283.0), S.pin_xy('C34', '1')[:2]])
S.power('C34', '2', 'GND')
V5V_OUT_REF, V5V_OUT_PIN = 'FB1', '2'
S.place(DEV, 'FerriteBead', 1, 'FB2', '600R@100MHz', BX + 90, BY + 25, rot=270, footprint=C_SMD)
S.intend(V3V3_LOAD_REF, V3V3_LOAD_PIN, 'FB2', '1')
S.wire([S.pin_xy(V3V3_LOAD_REF, V3V3_LOAD_PIN)[:2], (555.0, 163.83), (555.0, 160.0), (560.0, 160.0),
(560.0, 275.59), S.pin_xy('FB2', '1')[:2]])
S.place(DEV, 'C', 1, 'C35', '100n', BX + 105, BY + 30, rot=90, footprint=C_SMD)
S.intend('FB2', '2', 'C35', '1')
S.wire([S.pin_xy('FB2', '2')[:2], (546.1, 283.0), (561.34, 283.0), S.pin_xy('C35', '1')[:2]])
S.power('C35', '2', 'GND')
V3V3_OUT_REF, V3V3_OUT_PIN = 'FB2', '2'
# ============================================================================
# BLOCK 9 -- CONNECTORS + TEST POINTS
# ============================================================================
BX, BY = 460.0, 340.0
block_title("BLOCK 9: CONNECTORS + TEST POINTS", BX, BY)
S.rect_box(BX - 4, BY + 4, BX + 220, BY + 160)
S.place(CONN, 'Conn_01x02', 1, 'J2', 'BATTERY (12V SLA/AGM)', BX + 20, BY + 20,
footprint="TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-2-3.5-H_1x02_P3.50mm_Horizontal")
S.connect(BATT_BUS_REF, BATT_BUS_PIN, 'J2', '1', via=[(325.12, 300.0), (BX + 20, 300.0)])
S.power('J2', '2', 'GND')
S.place(CONN, 'Conn_01x02', 1, 'J3', '5V OUT / 2A', BX + 20, BY + 45,
footprint="TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-2-3.5-H_1x02_P3.50mm_Horizontal")
S.connect(V5V_OUT_REF, V5V_OUT_PIN, 'J3', '1', via=[(505.0, 285.0), (BX + 20, 285.0)])
S.power('J3', '2', 'GND')
S.place(CONN, 'Conn_01x02', 1, 'J4', '3.3V OUT / 1A', BX + 20, BY + 70,
footprint="TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-2-3.5-H_1x02_P3.50mm_Horizontal")
S.connect(V3V3_OUT_REF, V3V3_OUT_PIN, 'J4', '1', via=[(565.0, 285.0), (BX + 15, 285.0), (BX + 15, BY + 60)])
S.power('J4', '2', 'GND')
# UART debug header (GND, TX, RX)
S.place(CONN, 'Conn_01x03', 1, 'J6', 'UART_DEBUG', BX + 20, BY + 100,
footprint="Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical")
S.power('J6', '1', 'GND')
x, y, dx, dy = S.pin_xy('J6', '2')
S.wire([(x, y), (snap(x - 6 * 1.27), y)])
S.label('UART_TX', snap(x - 6 * 1.27), y, rot=180, just='right bottom')
x, y, dx, dy = S.pin_xy('J6', '3')
S.wire([(x, y), (snap(x - 6 * 1.27), y)])
S.label('UART_RX', snap(x - 6 * 1.27), y, rot=180, just='right bottom')
# Test points -- key nodes, mirroring the old board's TP coverage. Each tap
# is a real wire stub from the point itself out to a nearby label; a test
# point's whole purpose is "same net as X, accessible here", so a label is
# the standard, unambiguous way to name that -- the same reasoning as the
# MCU fan-out above, not a shortcut around the "real wires" requirement.
tp_sources = [
(PANEL_PROT_REF, PANEL_PROT_PIN, 'PANEL_PROT'),
(BATT_BUS_REF, BATT_BUS_PIN, 'VBATT'),
(V5V_OUT_REF, V5V_OUT_PIN, 'V5V_OUT'),
(V3V3_OUT_REF, V3V3_OUT_PIN, 'V3V3_OUT'),
]
for sref, spin, name in tp_sources:
x, y, dx, dy = S.pin_xy(sref, spin)
ex, ey = snap(x + dx * 4 * 1.27), snap(y + dy * 4 * 1.27)
S.wire([(x, y), (ex, ey)])
S.label(name, ex, ey, rot=(0 if dx >= 0 else 180),
just=('left bottom' if dx >= 0 else 'right bottom'))
for i, (ref, name) in enumerate([('TP1', 'PANEL_PROT'), ('TP2', 'VBATT'),
('TP3', 'V5V_OUT'), ('TP4', 'V3V3_OUT')]):
tx, ty = BX + 100 + 25 * i, BY + 130
S.place(CONN, 'Conn_01x01', 1, ref, f'TP_{name}', tx, ty, rot=90,
footprint="TestPoint:TestPoint_Pad_D1.0mm")
x, y, dx, dy = S.pin_xy(ref, '1')
ex, ey = snap(x + dx * 4 * 1.27), snap(y + dy * 4 * 1.27)
S.wire([(x, y), (ex, ey)])
S.label(name, ex, ey, rot=(0 if dx >= 0 else 180),
just=('left bottom' if dx >= 0 else 'right bottom'))
S.finalize_wires()
S.verify_intent(raise_on_error=False)
txt = S.render(650, 550, "MPPT Power System (MCU/IC) - BLOCKS 1-9", "v1", "Adom", "2026-09-16")
assert check_balance(txt)
OUT = "/mnt/c/Users/kirkt/Documents/mppt-power-system-mcu/mppt-power-system-mcu.kicad_sch"
open(OUT, 'w').write(txt)
print("Blocks 1-9 (full schematic) written:", len(txt), "bytes")