Files
NeoECU-Hardware/Architecture/diagrams/build_circuits.py
T

177 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Rebuild documentation schematics with Schemdraw.
Run with ~/.venvs/codex-schematics/bin/python. Topology JSON is emitted beside
each figure. References identify documentation symbols, not CAD placements.
"""
from pathlib import Path
import json
import xml.etree.ElementTree as ET
import schemdraw
import schemdraw.elements as e
import schemdraw.logic as logic
import cairosvg
OUT = Path(__file__).resolve().parent
class Circuit:
def __init__(self,name,title,source):
self.name=name
self.d=schemdraw.Drawing(show=False)
self.d.config(unit=2.5,fontsize=12,font='DejaVu Sans',lw=1.5,color='#20394e',bgcolor='white')
self.top={'title':title,'source':source,'reference_scope':'documentation only','components':[],'nets':[],'no_connects':[]}
self.nets={}
def component(self,ref,kind,pins,value=''):
assert ref not in [c['ref'] for c in self.top['components']],ref
self.top['components'].append({'ref':ref,'kind':kind,'pins':list(pins),'value':value})
for pin,net in pins.items(): self.nets.setdefault(net,[]).append(ref+'.'+pin)
def label(self,xy,text,loc='top',size=12):
self.d.add(e.Label().at(xy).label(text,loc=loc,fontsize=size))
def wire(self,a,b): self.d.add(e.Line().at(a).to(b))
def dot(self,xy): self.d.add(e.Dot().at(xy))
def port(self,ref,xy,net,loc='left'):
self.component(ref,'port',{'P':net},net)
self.d.add(e.Dot(open=True).at(xy).label(net,loc=loc))
def res(self,ref,a,b,n1,n2,label=None):
self.component(ref,'resistor',{'1':n1,'2':n2},label or ref)
self.d.add(e.Resistor().at(a).to(b).label(label or ref))
def cap(self,ref,a,b,n1,n2,label=None):
self.component(ref,'capacitor',{'1':n1,'2':n2},label or ref)
self.d.add(e.Capacitor().at(a).to(b).label(label or ref))
def zener(self,ref,a,b,n1,n2):
self.component(ref,'zener',{'K':n1,'A':n2},'Low-C clamp\nVoltage TBD')
self.d.add(e.Zener().at(a).to(b).reverse().label('Low-C Zener\nVoltage TBD'))
def ground(self,ref,xy,net='SENSOR_GND'):
self.component(ref,'ground-port',{'P':net},net)
self.d.add(e.Ground().at(xy).label(net,loc='bottom'))
def block(self,ref,xy,label,ins,outs,w=3.5,h=2):
pins=[]
for side,ports in [('L',ins),('R',outs)]:
for i,(pin,net) in enumerate(ports):
pins.append(e.IcPin(name=pin,anchorname=pin,side=side,pos=(i+1)/(len(ports)+1)))
self.component(ref,'functional-block',dict(ins+outs),label)
return self.d.add(e.Ic(size=(w,h),pins=pins).at(xy).theta(0).label(label,loc='top'))
def save(self):
for net,ends in self.nets.items():
entry={'name':net,'connections':ends}
if len(ends)==1: entry.update(allow_single=True,note='Named boundary or supply shown in the documentation figure')
self.top['nets'].append(entry)
(OUT/(self.name+'.topology.json')).write_text(json.dumps(self.top,indent=2)+'\n')
self.d.save(str(OUT/(self.name+'.svg')))
svg_path = OUT/(self.name+'.svg')
ET.register_namespace('', 'http://www.w3.org/2000/svg')
tree = ET.parse(svg_path); root = tree.getroot()
x,y,w,h = map(float,root.get('viewBox').split())
root.set('viewBox', f'{x-16} {y-16} {w+32} {h+32}')
root.set('width', f'{w+32}pt'); root.set('height', f'{h+32}pt')
tree.write(svg_path, encoding='utf-8', xml_declaration=True)
cairosvg.svg2png(url=str(OUT/(self.name+'.svg')),write_to=str(OUT/(self.name+'.png')),background_color='white',output_width=1200)
# Shunt protection and ADC reservoir are deliberately drawn as branches.
c=Circuit('analog-input','Passive 05 V ADC channel','IO_MODULES/ANALOG_INPUTS.md: General 0--5 V channels')
c.port('J',(0,0),'Sensor signal')
c.wire((0,0),(2,0));c.dot((2,0))
c.component('CLAMP','transient-protection',{'SIG':'Sensor signal','RET':'SENSOR_GND'},'Connector clamp\nPart / rating TBD')
cl=c.d.add(e.Ic(size=(2,1),pins=[e.IcPin(name='SIG',side='T',anchorname='SIG'),e.IcPin(name='RET',side='B',anchorname='RET')]).at((1,-3)).theta(0).label('Transient clamp',loc='left'))
c.wire((2,0),cl.SIG);c.ground('GC',cl.RET)
c.res('R_TOP',(2,0),(6,0),'Sensor signal','DIVIDED')
c.dot((6,0));c.res('R_BOTTOM',(6,0),(6,-3),'DIVIDED','SENSOR_GND');c.ground('GR',(6,-3))
c.res('R_ISO',(6,0),(10,0),'DIVIDED','ADC_IN','Isolation R\nValue TBD')
c.dot((10,0));c.cap('C_HOLD',(10,0),(10,-3),'ADC_IN','SENSOR_GND');c.ground('GH',(10,-3))
c.wire((10,0),(13,0));c.port('ADC',(13,0),'ADC_IN',loc='right')
c.label((6,2),'Passive analogue input',size=18);c.save()
c=Circuit('thermistor','NTC excitation and acquisition','IO_MODULES/ANALOG_INPUTS.md: NTC temperature channels')
c.port('V',(0,0),'+5V_SENS');c.res('R_PULLUP',(0,0),(4,0),'+5V_SENS','NTC_NODE','R_PULLUP\nSelected for sensor curve');c.dot((4,0))
c.component('NTC','thermistor',{'1':'NTC_NODE','2':'SENSOR_GND'},'NTC')
c.d.add(e.Thermistor().at((4,0)).to((4,-3)).label('NTC'));c.ground('GN',(4,-3))
b=c.block('AFE',(7,-1),'Attenuation / local filter',[('IN','NTC_NODE')],[('OUT','ADC_NTC')],w=4)
c.wire((4,0),b.IN);c.wire(b.OUT,(14,0));c.port('ADC',(14,0),'ADC_NTC',loc='right')
c.port('VS',(0,-6),'+5V_SENS');b2=c.block('SENSE',(7,-7),'Matched divider / filter',[('IN','+5V_SENS')],[('OUT','ADC_5V_SENSE')],w=4)
c.wire((0,-6),b2.IN);c.wire(b2.OUT,(14,-6));c.port('ADCS',(14,-6),'ADC_5V_SENSE',loc='right')
c.label((7,2),'Thermistor + excitation-supply measurement',size=18)
c.label((7,-9),'Both ADC channels use +3V3_ANA as reference. Filter and protection details remain in the text.',size=11);c.save()
c=Circuit('trigger-input','Crank / cam Hall input','IO_MODULES/ENGINE_POSITION_INPUTS.md: Electrical interface')
c.port('J',(0,0),'TRIG_x');c.wire((0,0),(3,0));c.dot((3,0))
c.port('V',(3,4),'+12V_SENS',loc='top');c.res('R_PULLUP',(3,4),(3,0),'+12V_SENS','TRIG_x','R_PULLUP\n4.7 kΩ provision')
c.component('CLAMP','transient-protection',{'SIG':'TRIG_x','RET':'SENSOR_GND'},'Harness clamp / ratings TBD')
b=c.d.add(e.Ic(size=(2,1),pins=[e.IcPin(name='SIG',side='T',anchorname='SIG'),e.IcPin(name='RET',side='B',anchorname='RET')]).at((2,-3)).theta(0).label('Transient\nprotection',loc='left'))
c.wire((3,0),b.SIG);c.ground('GC',b.RET)
c.res('R_DIV_TOP',(3,0),(7,0),'TRIG_x','DIVIDED');c.dot((7,0))
c.res('R_DIV_BOTTOM',(7,0),(7,-3),'DIVIDED','SENSOR_GND');c.ground('GB',(7,-3))
c.wire((7,0),(10,0));c.dot((10,0));c.cap('C_FILTER',(10,0),(10,-3),'DIVIDED','SENSOR_GND','C_FILTER\n1 nF initial');c.ground('GF',(10,-3))
c.wire((10,0),(12,0));s=c.d.add(logic.Schmitt().at((12,0)).right().label('SN74LVC2G17-Q1\nSupply: +3V3_MAIN',loc='top'))
c.component('U','schmitt-buffer',{'IN':'DIVIDED','OUT':'TIMER_3V3'},'SN74LVC2G17-Q1')
c.wire(s.out,(16,0));c.port('MCU',(16,0),'TIMER_3V3',loc='right')
c.label((8,6),'Hall input · repeated for crank and cam',size=18)
c.label((8,-5.5),'Sensor connector: supply = +12V_SENS; return = SENSOR_GND; output = TRIG_x. Pin numbers unassigned.',size=11);c.save()
c=Circuit('ignition-command','Ignition timing and inhibit protection','IO_MODULES/IGNITION.md: Command and inhibit path')
for i,(net,out) in enumerate([('STM32 timing (3.3 V)','INP'),('SAFE_IGNITION_INHIBIT','EN')]):
y=-i*7
c.port('J'+str(i),(0,y),net)
if i==0:
b=c.block('BUFFER',(2,y-1),'Non-inverting\n3.3 V → 5 V',[('IN',net)],[('OUT','TIMING_5V')],w=3)
c.wire((0,y),b.IN);c.wire(b.OUT,(7,y));net2='TIMING_5V'
else:c.wire((0,y),(7,y));net2=net
c.res('RP'+str(i),(7,y),(11,y),net2,out,'1 kΩ')
c.dot((11,y));c.zener('Z'+str(i),(11,y),(11,y-3),out,'VBG_LOCAL_KELVIN');c.ground('G'+str(i),(11,y-3),'VBG_LOCAL_KELVIN')
c.wire((11,y),(14,y));c.port('U'+str(i),(14,y),out,loc='right')
c.label((7,2.5),'VBG08H-E command pins',size=18)
c.label((6,-12),'INP: high = dwell; falling edge = spark. EN: high = inhibit; low = permission.',size=11);c.save()
c=Circuit('injector-command','Injector command protection','IO_MODULES/INJECTON.md: Command path')
c.port('J',(0,0),'SAFE_INJECTOR_SIG');c.res('Rprot',(0,0),(4,0),'SAFE_INJECTOR_SIG','INPUT','Rprot · 1 kΩ');c.dot((4,0))
c.res('R_PD',(4,0),(4,-3),'INPUT','INJ_PGND','10 kΩ');c.ground('GR',(4,-3),'INJ_PGND')
c.wire((4,0),(8,0));c.dot((8,0));c.zener('Z',(8,0),(8,-3),'INPUT','INJ_PGND');c.ground('GZ',(8,-3),'INJ_PGND')
c.wire((8,0),(12,0));c.port('U',(12,0),'INPUT',loc='right');c.label((6,2),'VNL5050S5-E · active-high command',size=18);c.save()
c=Circuit('injector-status','Injector status acquisition','IO_MODULES/INJECTON.md: Status diagnostic')
c.port('U',(0,0),'STATUS');c.res('Rprot',(0,0),(5,0),'STATUS','MCU_STATUS','Rprot · 1 kΩ');c.dot((5,0))
c.port('V',(5,4),'+3V3_MAIN',loc='top');c.res('R_PULLUP',(5,4),(5,0),'+3V3_MAIN','MCU_STATUS','Pull-up\nValue TBD')
c.wire((5,0),(10,0));c.port('M',(10,0),'MCU_STATUS',loc='right');c.label((5,6),'VNL5050S5-E · open-drain diagnostic',size=18);c.save()
c=Circuit('logic-output','Protected 5 V logic output','IO_MODULES/DIGITAL_OUTPUTS.md: Protected 5 V logic outputs')
c.port('J',(0,0),'MCU GPIO');c.wire((0,0),(3,0));c.dot((3,0))
c.res('R_IN_PD',(3,0),(3,-3),'MCU GPIO','GND');c.ground('GI',(3,-3),'GND')
b=c.block('U',(6,-1),'TPS4H000-Q1\nVBB = +5V_AUX',[('INx','MCU GPIO')],[('OUTx','LOGIC_OUTx')],w=4)
c.wire((3,0),b.INx);c.wire(b.OUTx,(13,0));c.dot((13,0));c.res('R_OUT_PD',(13,0),(13,-3),'LOGIC_OUTx','GND');c.ground('GO',(13,-3),'GND')
c.wire((13,0),(17,0));c.port('O',(17,0),'LOGIC_OUTx',loc='right')
c.label((8,3),'5 V high-side logic output',size=18)
c.label((8,-5.5),'+5V_MAIN → eFuse / load switch → +5V_AUX. Connector TVS selection is specified in the text.',size=11);c.save()
c=Circuit('sink-command','Low-side command default states','IO_MODULES/DIGITAL_OUTPUTS.md: Protected low-side outputs')
for i,(net,pin) in enumerate([('MCU GPIO','INx'),('MCU enable','EN')]):
y=-i*6
c.port('J'+str(i),(0,y),net);c.wire((0,y),(4,y));c.dot((4,y))
c.res('R_IN_PD' if i==0 else 'R_EN_PD',(4,y),(4,y-2.5),net,'LOCAL_GND')
c.ground('G'+str(i),(4,y-2.5),'LOCAL_GND')
c.wire((4,y),(9,y));c.component('U'+str(i),'driver-pin',{pin:net},'TLE9104SH '+pin)
c.d.add(e.Dot(open=True).at((9,y)).label('TLE9104SH '+pin,loc='right'))
c.label((4,2),'TLE9104SH · default-low commands',size=18);c.save()
def equation_gate(c,ref,xy,a,b,out,invert_b=False):
x,y=xy
gate=c.d.add(logic.And(inputs=2,inputnots=[2] if invert_b else []).at(xy).right())
c.component(ref,'and-with-inverted-B' if invert_b else 'and',{'A':a,'B':b,'Y':out})
for idx,(anchor,net) in enumerate([(gate.in1,a),(gate.in2,b)]):
yy=y+1.4 if idx==0 else y-1.4
start=(x-5,yy); c.port(ref+'P'+str(idx),start,net)
c.wire(start,(x-1.2,yy));c.wire((x-1.2,yy),(x-1.2,anchor.y));c.wire((x-1.2,anchor.y),anchor)
c.wire(gate.out,(x+5,y));c.port(ref+'O',(x+5,y),out,loc='right')
c=Circuit('deadman-permit','Hardware engine-permit equations','IO_MODULES/DEAD_MAN.md: Logic equations')
equation_gate(c,'VALID',(0,0),'DEADMAN_A_HELD','DEADMAN_B_RELEASED','DEADMAN_OK',True)
equation_gate(c,'PERMIT',(0,-6),'DEADMAN_OK','MCU_RUN_PERMIT','ENGINE_PERMIT')
c.label((0,3),'Hardware permission · 5 V gate outputs',size=18)
c.label((0,-9),'Bubble inverts channel B. MCU_RUN_PERMIT is 3.3 V; gates must accept that input level.',size=11);c.save()
c=Circuit('deadman-outputs','Hardware permitted output commands','IO_MODULES/DEAD_MAN.md: Logic equations')
equation_gate(c,'INJECT',(0,0),'ENGINE_PERMIT','MCU_INJECTOR_SIG','SAFE_INJECTOR_SIG')
gate=c.d.add(logic.Not().at((0,-6)).right());c.component('INHIBIT','not',{'A':'ENGINE_PERMIT','Y':'SAFE_IGNITION_INHIBIT'})
c.port('I',(-5,-6),'ENGINE_PERMIT');c.wire((-5,-6),gate.in1);c.wire(gate.out,(5,-6));c.port('O',(5,-6),'SAFE_IGNITION_INHIBIT',loc='right')
c.label((0,3),'Injector permission and ignition inhibit',size=18)
c.label((0,-8),'SAFE_IGNITION_INHIBIT: high = inhibited. Supply-loss default states require the documented bias networks.',size=11);c.save()
print('Wrote 10 SVG circuits, previews and topology manifests.')