Skip to content

Commit c510ae1

Browse files
ducky64DC37
andauthored
SMU v3.1 (#397)
SMUv3.1 and associated infrastructural changes. Major improvements to reliability, stability, and (hopefully) resolution (noise floor). Resolves #351 Changes: - Change emitter follower to be able to independently control gate on status on high and low sides - Refactor to use positive gate voltage instead of power - Rename ErrorAmplifier -> GatedSummingAmplifier to better reflect the standard topology - Improved docs - Add optional fine target input - Add optional sense output, upstream of the gating / resistive output element (to detect current limiting mode) rails, for headroom on analog switches - Make gate clamping voltage (zener) a parameter - Add JFET current clamp and simulation - Refactor SMU control schematic: - for readability - to be self-contained - change current sense amplifier to use inamps - better output protection with a R-C snubber and low-leakage TVS diodes - Add S-R inverted latch as SMU only block - SMU top changes - Use TO-220 for pass FETs, which should allow much better heatsinking - Add blowy fuse to the USB input - Use buck converter for direct Vbus -> 3.3v digital - Add INA219 power sense on Vbus input and Vconv in - Run Vcontrol- off analog rail, with ferrite filter - Add ramp limiter - Add current limit trip sense direct to MCU - Use RC for OLED reset, because we're out of MCU pins - Add another IO expander for low-speed controls, because we're _really_ out of MCU pins - Use SR with priority for converter OVP - should be more robust - Add I2S speaker - Simplify HDL using some new libraries - Add experimental block diagram grouping directive - Move SMU schematics into its folder - Rebaseline netlists - Delete calibration worksheet for old device Libraries added: - Add AnalogCapacitor as a cap to be sprinkled on analog lines. Apparently this is a thing in analog design. - Add VoltageComparator, a comparator against a set (absolute) voltage. - Add base SeriesPowerFuse, for blowy fuses - Add SeriesResistor generator, that breaks a resistor into a series combination, to increase power and voltage ratings. Applies derating for worst-case tolerance stackup for individual voltage and power ratings. - Add summing amplifier ratio calculator for n-ported noninverting summing amplifier, with unit test. No block yet. - Add differential-RC filter - Add more KiCad symbols to analog signal chain parts: voltage dividers - Add Nano2 / 154 series fuseholder, with no modeling. - Add INA826 in-amp - Add SN74LVC2G02 dual NOR gate - Add TLP170AM low(er)-cost SSR - Add TO-220 FETs - Add Sn74lvc1g3157 analog switch, as the NLAS4157 is obsolete. Higher on-state resistance. - Add Ws2812c_2020 0808-size Neopixel LED, which is 3v3 logic compatible. Adds decoupling capacitors, which will be standard for Neopixels going forward. Libraries modified: - Add mclkin for MCP3561, so we can overclock those ADCs - Add addr lsb selection on INA219 - Expand Cf target tolerance for Lm2733, to give more flexibility in capacitance - Deprecate NLAS4157 (obsoleted part) - Change Mcp4728 to use the -T part, which is more common. - Shrink courtyards on non-connector footprints, both for optimization and to standardize to KiCad's library guidelines. Fixes to JlcParts: - Fix require_basic_part in default refinements - Fix diode parsing - Expand diode footprint parsing rules Core changes: - Add range-float subtraction (in addition to existing float-range subtraction). Range-range subtraction is still undefined because the tolerancing is non-intuitive. - Add Ratio (like kOhm, and nFarad, but for unitless), which cleans up a lot of cases, resolves #391 - Add as_voltage_source adapter for AnalogSource - Add KiCad instantiation to SeriesPowerResistor --------- Co-authored-by: dc37 <[email protected]>
1 parent a152283 commit c510ae1

File tree

60 files changed

+175967
-132157
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+175967
-132157
lines changed

edg/abstract_parts/AbstractAnalogSwitch.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def __init__(self) -> None:
2222
self.pwr = self.Port(VoltageSink.empty(), [Power])
2323
self.gnd = self.Port(Ground.empty(), [Common])
2424

25+
self.control_gnd = self.Port(Ground.empty(), optional=True) # optional separate ground for control signal
2526
self.control = self.Port(Vector(DigitalSink.empty())) # length source
2627

2728
self.com = self.Port(Passive.empty())
@@ -40,7 +41,7 @@ class AnalogSwitchTree(AnalogSwitch, GeneratorBlock):
4041
def __init__(self, switch_size: IntLike = 0):
4142
super().__init__()
4243
self.switch_size = self.ArgParameter(switch_size)
43-
self.generator_param(self.switch_size, self.inputs.requested())
44+
self.generator_param(self.switch_size, self.inputs.requested(), self.control_gnd.is_connected())
4445

4546
def generate(self):
4647
import math
@@ -72,6 +73,8 @@ def generate(self):
7273
all_switches.append(sw)
7374
self.connect(sw.pwr, self.pwr)
7475
self.connect(sw.gnd, self.gnd)
76+
if self.get(self.control_gnd.is_connected()):
77+
self.connect(sw.control_gnd, self.control_gnd)
7578

7679
for sw_port_i in range(switch_size):
7780
port_i = sw_i * switch_size + sw_port_i
@@ -115,6 +118,8 @@ def __init__(self) -> None:
115118
self.device = self.Block(AnalogSwitch())
116119
self.pwr = self.Export(self.device.pwr, [Power])
117120
self.gnd = self.Export(self.device.gnd, [Common])
121+
122+
self.control_gnd = self.Port(Ground.empty(), optional=True) # optional separate ground for control signal
118123
self.control = self.Export(self.device.control)
119124

120125
self.inputs = self.Port(Vector(AnalogSink.empty()))
@@ -125,7 +130,7 @@ def __init__(self) -> None:
125130
impedance=self.device.analog_on_resistance + self.inputs.hull(lambda x: x.link().source_impedance)
126131
)))
127132

128-
self.generator_param(self.inputs.requested())
133+
self.generator_param(self.inputs.requested(), self.control_gnd.is_connected())
129134

130135
def generate(self):
131136
super().generate()
@@ -138,6 +143,8 @@ def generate(self):
138143
current_draw=self.out.link().current_drawn,
139144
impedance=self.out.link().sink_impedance + self.device.analog_on_resistance
140145
)))
146+
if self.get(self.control_gnd.is_connected()):
147+
self.connect(self.control_gnd, self.device.control_gnd)
141148

142149
def mux_to(self, inputs: Optional[List[Port[AnalogLink]]] = None,
143150
output: Optional[Port[AnalogLink]] = None) -> 'AnalogMuxer':

edg/abstract_parts/AbstractCapacitor.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,33 @@ def connected(self, gnd: Optional[Port[GroundLink]] = None, pwr: Optional[Port[V
336336
return self
337337

338338

339-
class CombinedCapacitorElement(Capacitor):
339+
class AnalogCapacitor(DiscreteApplication, KiCadImportableBlock):
340+
"""Capacitor attached to an analog line, that presents as an open model-wise.
341+
"""
342+
def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]:
343+
assert symbol_name in ('Device:C', 'Device:C_Small', 'Device:C_Polarized', 'Device:C_Polarized_Small')
344+
return {'1': self.io, '2': self.gnd}
345+
346+
def __init__(self, capacitance: RangeLike, *, exact_capacitance: BoolLike = False) -> None:
347+
super().__init__()
348+
349+
self.cap = self.Block(Capacitor(capacitance, voltage=RangeExpr(), exact_capacitance=exact_capacitance))
350+
self.gnd = self.Export(self.cap.neg.adapt_to(Ground()), [Common])
351+
self.io = self.Export(self.cap.pos.adapt_to(AnalogSink()), [InOut]) # ideal open port
352+
353+
self.assign(self.cap.voltage, self.io.link().voltage - self.gnd.link().voltage)
354+
355+
def connected(self, gnd: Optional[Port[GroundLink]] = None, io: Optional[Port[AnalogLink]] = None) -> \
356+
'AnalogCapacitor':
357+
"""Convenience function to connect both ports, returning this object so it can still be given a name."""
358+
if gnd is not None:
359+
cast(Block, builder.get_enclosing_block()).connect(gnd, self.gnd)
360+
if io is not None:
361+
cast(Block, builder.get_enclosing_block()).connect(io, self.io)
362+
return self
363+
364+
365+
class CombinedCapacitorElement(Capacitor): # to avoid an abstract part error
340366
def contents(self):
341367
super().contents()
342368
self.assign(self.actual_capacitance, self.capacitance) # fake it, since a combined capacitance is handwavey

edg/abstract_parts/AbstractComparator.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Mapping
22

3+
from .ResistiveDivider import FeedbackVoltageDivider, VoltageDivider
34
from ..abstract_parts import Analog
45
from ..electronics_model import *
56

@@ -22,3 +23,65 @@ def __init__(self) -> None:
2223
self.inn = self.Port(AnalogSink.empty())
2324
self.inp = self.Port(AnalogSink.empty())
2425
self.out = self.Port(DigitalSource.empty())
26+
27+
28+
class VoltageComparator(GeneratorBlock):
29+
"""A comparator subcircuit that compares an input voltage rail against some reference, either
30+
internally generated from the power lines or an external analog signals.
31+
Accounts for tolerance stackup on the reference input - so make sure the trip
32+
tolerance is specified wide enough.
33+
The output is logic high when the input exceeds the trip voltage by default,
34+
this can be inverted with the invert parameter.
35+
Optionally this can take a reference voltage input, otherwise this generates a divider.
36+
37+
TODO: maybe a version that takes an input analog signal?
38+
"""
39+
def __init__(self, trip_voltage: RangeLike, *, invert: BoolLike = False,
40+
input_impedance: RangeLike=(4.7, 47)*kOhm,
41+
trip_ref: RangeLike=1.65*Volt(tol=0.10)):
42+
super().__init__()
43+
self.comp = self.Block(Comparator())
44+
self.gnd = self.Export(self.comp.gnd, [Common])
45+
self.pwr = self.Export(self.comp.pwr, [Power])
46+
self.input = self.Port(AnalogSink.empty(), [Input])
47+
self.output = self.Export(self.comp.out, [Output])
48+
self.ref = self.Port(AnalogSink.empty(), optional=True)
49+
50+
self.trip_voltage = self.ArgParameter(trip_voltage)
51+
self.invert = self.ArgParameter(invert)
52+
self.trip_ref = self.ArgParameter(trip_ref) # only used if self.ref disconnected
53+
self.input_impedance = self.ArgParameter(input_impedance)
54+
self.generator_param(self.ref.is_connected(), self.invert)
55+
56+
self.actual_trip_voltage = self.Parameter(RangeExpr())
57+
58+
def generate(self):
59+
super().generate()
60+
61+
if self.get(self.ref.is_connected()):
62+
ref_pin: Port[AnalogLink] = self.ref
63+
ref_voltage = self.ref.link().signal
64+
else:
65+
self.ref_div = self.Block(VoltageDivider(
66+
output_voltage=self.trip_ref,
67+
impedance=self.input_impedance,
68+
))
69+
self.connect(self.ref_div.input, self.pwr)
70+
self.connect(self.ref_div.gnd, self.gnd)
71+
ref_pin = self.ref_div.output
72+
ref_voltage = self.ref_div.output.link().signal
73+
74+
self.comp_div = self.Block(FeedbackVoltageDivider(
75+
impedance=self.input_impedance,
76+
output_voltage=ref_voltage,
77+
assumed_input_voltage=self.trip_voltage
78+
))
79+
self.assign(self.actual_trip_voltage, self.comp_div.actual_input_voltage)
80+
self.connect(self.comp_div.input, self.input.as_voltage_source())
81+
self.connect(self.comp_div.gnd, self.gnd)
82+
if not self.get(self.invert): # positive connection
83+
self.connect(self.comp.inp, self.comp_div.output)
84+
self.connect(self.comp.inn, ref_pin)
85+
else: # negative connection
86+
self.connect(self.comp.inn, self.comp_div.output)
87+
self.connect(self.comp.inp, ref_pin)

edg/abstract_parts/AbstractFets.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ class FetStandardFootprint(StandardFootprint['Fet']):
4848
'4': block.gate,
4949
'5': block.drain,
5050
},
51+
(
52+
'Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp',
53+
'Package_TO_SOT_THT:TO-220-3_Horizontal_TabDown',
54+
'Package_TO_SOT_THT:TO-220-3_Vertical',
55+
): lambda block: {
56+
'1': block.gate,
57+
'2': block.drain,
58+
'3': block.source,
59+
},
5160
}
5261

5362

edg/abstract_parts/AbstractResistor.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,53 @@ def _row_sort_by(cls, row: PartsTableRow) -> Any:
125125
super()._row_sort_by(row))
126126

127127

128+
class SeriesResistor(Resistor, GeneratorBlock):
129+
"""Splits a resistor into equal resistors in series. Improves power and voltage ratings
130+
by distributing the load across multiple devices.
131+
132+
Generally used as a refinement to break up a single (logical) resistor that is dissipating too much power
133+
or has an excessive voltage across it. Accounts for tolerance stackup for power and voltage distribution
134+
using specified (not actual) resistor tolerance - is a pessimistic calculation."""
135+
def __init__(self, *args, count: IntLike = 2, **kwargs):
136+
super().__init__(*args, **kwargs)
137+
self.count = self.ArgParameter(count)
138+
self.generator_param(self.count, self.resistance)
139+
140+
def generate(self):
141+
super().generate()
142+
count = self.get(self.count)
143+
last_port = self.a
144+
cumu_resistance: RangeLike = Range.exact(0)
145+
cumu_power_rating: RangeLike = Range.exact(0)
146+
cumu_voltage_rating: RangeLike = Range.exact(0)
147+
self.res = ElementDict[Resistor]()
148+
149+
# calculate tolerance stackup effects on R for worst-case power and voltage
150+
resistance_range = self.get(self.resistance)
151+
resistance_tol = (resistance_range.upper - resistance_range.lower) / 2 / resistance_range.center()
152+
resistance_tol = min(0.05, resistance_tol) # in practice there should be no >5% resistors
153+
resistance_ratio_range = Range((1 - resistance_tol) / (count + resistance_tol * (count - 2)),
154+
(1 + resistance_tol) / (count - resistance_tol * (count - 2)))
155+
156+
elt_resistance = resistance_range / count
157+
elt_power = self.power * resistance_ratio_range
158+
elt_voltage = self.voltage * resistance_ratio_range
159+
160+
for i in range(count):
161+
self.res[i] = res = self.Block(Resistor(resistance=elt_resistance,
162+
power=elt_power,
163+
voltage=elt_voltage))
164+
self.connect(last_port, res.a)
165+
cumu_resistance = cumu_resistance + res.actual_resistance
166+
cumu_power_rating = cumu_power_rating + res.actual_power_rating
167+
cumu_voltage_rating = cumu_voltage_rating + res.actual_voltage_rating
168+
last_port = res.b
169+
self.connect(last_port, self.b)
170+
self.assign(self.actual_resistance, cumu_resistance)
171+
self.assign(self.actual_power_rating, cumu_power_rating)
172+
self.assign(self.actual_voltage_rating, cumu_voltage_rating)
173+
174+
128175
class PullupResistor(DiscreteApplication):
129176
"""Pull-up resistor with an VoltageSink for automatic implicit connect to a Power line."""
130177
def __init__(self, resistance: RangeLike) -> None:
@@ -205,8 +252,12 @@ def generate(self):
205252
self.connect(self.io.append_elt(DigitalSource.empty(), requested), res.io)
206253

207254

208-
class SeriesPowerResistor(DiscreteApplication):
255+
class SeriesPowerResistor(DiscreteApplication, KiCadImportableBlock):
209256
"""Series resistor for power applications"""
257+
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
258+
assert symbol_name in ('Device:R', 'Device:R_Small')
259+
return {'1': self.pwr_in, '2': self.pwr_out}
260+
210261
def __init__(self, resistance: RangeLike) -> None:
211262
super().__init__()
212263

@@ -324,3 +375,44 @@ def contents(self):
324375
def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]:
325376
assert symbol_name == 'Device:R'
326377
return {'1': self.signal_in, '2': self.signal_out}
378+
379+
380+
class DigitalClampResistor(Protection, KiCadImportableBlock):
381+
"""Inline resistor that limits the current (to a parameterized amount) which works in concert
382+
with ESD diodes in the downstream device to clamp the signal voltage to allowable levels.
383+
384+
The protection voltage can be extended beyond the modeled range from the input signal,
385+
and can also be specified to allow zero output voltage (for when the downstream device
386+
is powered down)
387+
388+
TODO: clamp_target should be inferred from the target voltage_limits,
389+
but voltage_limits doesn't always get propagated."""
390+
def __init__(self, clamp_target: RangeLike = (0, 3)*Volt, clamp_current: RangeLike = (1.0, 10)*mAmp,
391+
protection_voltage: RangeLike = (0, 0)*Volt, zero_out: BoolLike = False):
392+
super().__init__()
393+
394+
self.signal_in = self.Port(DigitalSink.empty(), [Input])
395+
self.signal_out = self.Port(DigitalSource.empty(), [Output])
396+
397+
self.clamp_target = self.ArgParameter(clamp_target)
398+
self.clamp_current = self.ArgParameter(clamp_current)
399+
self.protection_voltage = self.ArgParameter(protection_voltage)
400+
self.zero_out = self.ArgParameter(zero_out)
401+
402+
def contents(self):
403+
super().contents()
404+
405+
# TODO bidirectional clamping calcs?
406+
self.res = self.Block(Resistor(resistance=1/self.clamp_current * self.zero_out.then_else(
407+
self.signal_in.link().voltage.hull(self.protection_voltage).upper(),
408+
self.signal_in.link().voltage.hull(self.protection_voltage).upper() - self.clamp_target.upper(),
409+
)))
410+
self.connect(self.res.a.adapt_to(DigitalSink(current_draw=self.signal_out.link().current_drawn)), self.signal_in)
411+
self.connect(self.res.b.adapt_to(DigitalSource(
412+
voltage_out=self.signal_in.link().voltage.intersect(self.clamp_target),
413+
output_thresholds=self.signal_in.link().output_thresholds
414+
)), self.signal_out)
415+
416+
def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]:
417+
assert symbol_name == 'Device:R'
418+
return {'1': self.signal_in, '2': self.signal_out}

edg/abstract_parts/AbstractTestPoint.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,15 @@ def connected(self, io: Port[AnalogLink]) -> 'AnalogTestPoint':
116116
return self
117117

118118

119-
class AnalogRfTestPoint(BaseRfTestPoint, Block):
120-
"""Test point with a AnalogSink port and 50-ohm matching resistor."""
119+
class AnalogCoaxTestPoint(BaseRfTestPoint, Block):
120+
"""Test point with a AnalogSink port and using a coax connector with shielding connected to gnd.
121+
No impedance matching, this is intended for lower frequency signals where the wavelength would be
122+
much longer than the test lead length"""
121123
def __init__(self, *args):
122124
super().__init__(*args)
123-
self.res = self.Block(Resistor(50*Ohm(tol=0.05)))
124-
self.io = self.Export(self.res.a.adapt_to(AnalogSink()), [InOut])
125-
self.connect(self.res.b, self.conn.sig)
125+
self.io = self.Export(self.conn.sig.adapt_to(AnalogSink()), [InOut])
126126

127-
def connected(self, io: Port[AnalogLink]) -> 'AnalogRfTestPoint':
127+
def connected(self, io: Port[AnalogLink]) -> 'AnalogCoaxTestPoint':
128128
cast(Block, builder.get_enclosing_block()).connect(io, self.io)
129129
return self
130130

edg/abstract_parts/DigitalAmplifiers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ class HighSideSwitch(PowerSwitch, KiCadSchematicBlock, GeneratorBlock):
1414
1515
TODO: clamp_voltage should be compared against the actual voltage so the clamp is automatically generated,
1616
but generators don't support link terms (yet?)"""
17-
def __init__(self, pull_resistance: RangeLike = 10000*Ohm(tol=0.05), max_rds: FloatLike = 1*Ohm,
17+
def __init__(self, pull_resistance: RangeLike = 10*kOhm(tol=0.05), max_rds: FloatLike = 1*Ohm,
1818
frequency: RangeLike = RangeExpr.ZERO, *,
1919
clamp_voltage: RangeLike = RangeExpr.ZERO, clamp_resistance_ratio: FloatLike = 10) -> None:
2020
super().__init__()
2121

2222
self.pwr = self.Port(VoltageSink.empty(), [Power]) # amplifier voltage
2323
self.gnd = self.Port(Ground.empty(), [Common])
2424

25-
self.control = self.Port(DigitalSink.empty(), [Input])
26-
self.output = self.Port(VoltageSource.empty(), [Output])
25+
self.control = self.Port(DigitalSink.empty())
26+
self.output = self.Port(VoltageSource.empty())
2727

2828
self.pull_resistance = self.ArgParameter(pull_resistance)
2929
self.max_rds = self.ArgParameter(max_rds)

edg/abstract_parts/OpampCircuits.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,26 @@ def contents(self) -> None:
378378
})
379379

380380
self.assign(self.actual_factor, 1 / self.r.actual_resistance / self.c.actual_capacitance)
381+
382+
383+
class SummingAmplifier(OpampApplication):
384+
@classmethod
385+
def calculate_ratio(cls, resistances: List[Range]) -> List[Range]:
386+
"""Calculates each input's contribution to the output, for 1x gain.
387+
Non-inverting summing amplifier topology.
388+
Based on https://www.electronicshub.org/summing-amplifier/, which calculates the voltage of each input
389+
and uses superposition to combine them."""
390+
output = []
391+
for i, resistance in enumerate(resistances):
392+
# compute the two tolerance corners
393+
others = resistances[:i] + resistances[i+1:]
394+
other_lowest_parallel = 1 / sum([1 / other.lower for other in others])
395+
other_highest_parallel = 1 / sum([1 / other.upper for other in others])
396+
# ratio is lowest when this resistance is highest and other is lowest
397+
ratio_lowest = other_lowest_parallel / (resistance.upper + other_lowest_parallel)
398+
# ratio is highest when this resistance is lowest and other is highest
399+
ratio_highest = other_highest_parallel / (resistance.lower + other_highest_parallel)
400+
401+
output.append(Range(ratio_lowest, ratio_highest))
402+
403+
return output

0 commit comments

Comments
 (0)