Skip to content

Commit 3d23412

Browse files
authored
Refactor speaker signal chain, add analog series caps and resistors (#467)
Post-analog-compositional-passive cleanup. Adds AnalogSeriesCapacitor and AnalogSeriesResistor library parts to eliminate the adhoc .adapt_to in the speaker drivers. Removes the impedance check for the speakers, since it breaks with the modeling of the series analog resistor. Removes the pull port form the solid-state relay which also needed an adhoc .adapt_to and broke levels of abstraction. This is a breaking change with no deprecation path. Updates netlists, since AnalogSeriesCapacitor/Reisstor has inner parts now. Resolves #466, resolves #464
1 parent cfa9dab commit 3d23412

18 files changed

Lines changed: 212 additions & 105 deletions

edg/abstract_parts/AbstractCapacitor.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,63 @@ def connected(
421421
return self
422422

423423

424+
class AnalogSeriesCapacitor(DiscreteApplication, KiCadImportableBlock):
425+
"""Series DC-blocking capacitor for an analog signal."""
426+
427+
@override
428+
def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]:
429+
assert symbol_name in ("Device:C", "Device:C_Small", "Device:C_Polarized", "Device:C_Polarized_Small")
430+
return {"1": self.input, "2": self.output}
431+
432+
def __init__(self, capacitance: RangeLike, output_bias: RangeLike, *, exact_capacitance: BoolLike = False) -> None:
433+
super().__init__()
434+
435+
self.output_bias = self.ArgParameter(output_bias)
436+
437+
self.input = self.Port(AnalogSink(impedance=RangeExpr()), [Input])
438+
self.output = self.Port(
439+
AnalogSource(voltage_out=RangeExpr(), signal_out=RangeExpr(), impedance=self.input.link().source_impedance),
440+
[Output],
441+
)
442+
443+
self.cap = self.Block(
444+
Capacitor(
445+
capacitance,
446+
voltage=self.input.link().voltage - self.output.link().voltage,
447+
exact_capacitance=exact_capacitance,
448+
)
449+
)
450+
451+
@override
452+
def contents(self) -> None:
453+
super().contents()
454+
455+
self.connect(self.input.net, self.cap.neg)
456+
self.connect(self.output.net, self.cap.pos)
457+
458+
self.assign(self.input.impedance, self.output.link().sink_impedance) # assumed high frequency
459+
voltage_halfspan = (self.input.link().voltage.upper() - self.input.link().voltage.lower()) / 2
460+
self.assign(
461+
self.output.voltage_out,
462+
(self.output_bias.lower() - voltage_halfspan, self.output_bias.upper() + voltage_halfspan),
463+
)
464+
signal_halfspan = (self.input.link().signal.upper() - self.input.link().signal.lower()) / 2
465+
self.assign(
466+
self.output.signal_out,
467+
(self.output_bias.lower() - signal_halfspan, self.output_bias.upper() + signal_halfspan),
468+
)
469+
470+
def connected(
471+
self, input: Optional[Port[AnalogLink]] = None, output: Optional[Port[AnalogLink]] = None
472+
) -> "AnalogSeriesCapacitor":
473+
"""Convenience function to connect both ports, returning this object so it can still be given a name."""
474+
if input is not None:
475+
cast(Block, builder.get_enclosing_block()).connect(input, self.input)
476+
if output is not None:
477+
cast(Block, builder.get_enclosing_block()).connect(output, self.output)
478+
return self
479+
480+
424481
class CombinedCapacitorElement(Capacitor): # to avoid an abstract part error
425482
@override
426483
def contents(self) -> None:

edg/abstract_parts/AbstractResistor.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,49 @@ def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]:
372372
return {"1": self.pwr_in, "2": self.pwr_out, "sense_in": self.sense_in, "sense_out": self.sense_out}
373373

374374

375+
class AnalogSeriesResistor(DiscreteApplication, KiCadImportableBlock):
376+
"""Analog passthrough series resistor"""
377+
378+
@override
379+
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
380+
assert symbol_name in ("Device:R", "Device:R_Small")
381+
return {"1": self.input, "2": self.output}
382+
383+
def __init__(self, resistance: RangeLike) -> None:
384+
super().__init__()
385+
386+
self.res = self.Block(Resistor(resistance=resistance, power=RangeExpr()))
387+
self.actual_resistance = self.Parameter(RangeExpr(self.res.actual_resistance))
388+
389+
self.input = self.Port(AnalogSink(impedance=RangeExpr()), [Input])
390+
self.output = self.Port(
391+
AnalogSource(
392+
voltage_out=self.input.link().voltage,
393+
signal_out=self.input.link().signal,
394+
impedance=self.input.link().source_impedance + self.res.actual_resistance,
395+
),
396+
[Output],
397+
)
398+
399+
self.assign(self.input.impedance, self.output.link().sink_impedance + self.res.actual_resistance)
400+
401+
self.assign(
402+
self.res.power, self.input.link().current_drawn * self.input.link().current_drawn * self.res.resistance
403+
)
404+
self.connect(self.input.net, self.res.a)
405+
self.connect(self.output.net, self.res.b)
406+
407+
def connected(
408+
self, input: Optional[Port[AnalogLink]] = None, output: Optional[Port[AnalogLink]] = None
409+
) -> "AnalogSeriesResistor":
410+
"""Convenience function to connect both ports, returning this object so it can still be given a name."""
411+
if input is not None:
412+
cast(Block, builder.get_enclosing_block()).connect(input, self.input)
413+
if output is not None:
414+
cast(Block, builder.get_enclosing_block()).connect(output, self.output)
415+
return self
416+
417+
375418
class AnalogClampResistor(Protection, KiCadImportableBlock):
376419
"""Inline resistor that limits the current (to a parameterized amount) which works in concert
377420
with ESD diodes in the downstream device to clamp the signal voltage to allowable levels.

edg/abstract_parts/AbstractSolidStateRelay.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,14 @@ class AnalogIsolatedSwitch(Interface, KiCadImportableBlock, Block):
9999
The ports are not tagged with Input/Output/InOut, because of potential for confusion between
100100
the digital side and the analog side.
101101
102-
A separate output-side pull port allows modeling the output switch standoff voltage
103-
when the switch is off.
102+
The output is modeled as if the switch is closed. A weak pull-up (or down) could be modeled by
103+
combining this device's output with a MergedAnalogSource.
104104
"""
105105

106106
@override
107107
def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]:
108108
assert symbol_name == "edg_importable:AnalogIsolatedSwitch"
109-
return {"in": self.signal, "gnd": self.gnd, "ain": self.ain, "apull": self.apull, "aout": self.aout}
109+
return {"in": self.signal, "gnd": self.gnd, "ain": self.ain, "aout": self.aout}
110110

111111
def __init__(self) -> None:
112112
super().__init__()
@@ -116,15 +116,27 @@ def __init__(self) -> None:
116116
self.signal = self.Port(DigitalSink.empty())
117117
self.gnd = self.Port(Ground.empty(), [Common])
118118

119-
self.apull = self.Port(AnalogSink.empty())
120119
self.ain = self.Port(
121120
AnalogSink(
122121
voltage_limits=RangeExpr(),
123122
impedance=RangeExpr(),
124123
)
125124
)
126-
self.aout = self.Port(AnalogSource.empty())
127-
self.assign(self.ain.voltage_limits, self.apull.link().voltage + self.ic.load_voltage_limit)
125+
self.aout = self.Port(
126+
AnalogSource(
127+
voltage_out=self.ain.link().voltage,
128+
signal_out=self.ain.link().signal,
129+
current_limits=self.ic.load_current_limit,
130+
impedance=self.ain.link().source_impedance + self.ic.load_resistance,
131+
)
132+
)
133+
self.assign(
134+
self.ain.voltage_limits,
135+
(
136+
self.aout.link().voltage.lower() + self.ic.load_voltage_limit.upper(),
137+
self.aout.link().voltage.upper() - self.ic.load_voltage_limit.lower(),
138+
),
139+
)
128140
self.assign(self.ain.impedance, self.aout.link().sink_impedance + self.ic.load_resistance)
129141

130142
self.res = self.Block(
@@ -143,15 +155,4 @@ def __init__(self) -> None:
143155
self.connect(self.res.b.adapt_to(Ground()), self.gnd)
144156

145157
self.connect(self.ain.net, self.ic.feta)
146-
self.pull_merge = self.Block(MergedAnalogSource()).connected_from(
147-
self.apull,
148-
self.ic.fetb.adapt_to(
149-
AnalogSource(
150-
voltage_out=self.ain.link().voltage,
151-
signal_out=self.ain.link().signal,
152-
current_limits=self.ic.load_current_limit,
153-
impedance=self.ain.link().source_impedance + self.ic.load_resistance,
154-
)
155-
),
156-
)
157-
self.connect(self.pull_merge.output, self.aout)
158+
self.connect(self.aout.net, self.ic.fetb)

edg/abstract_parts/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
SmaMConnector,
5353
SmaFConnector,
5454
)
55-
from .AbstractResistor import Resistor, ResistorStandardFootprint, TableResistor, SeriesResistor
55+
from .AbstractResistor import Resistor, ResistorStandardFootprint, TableResistor, SeriesResistor, AnalogSeriesResistor
5656
from .AbstractResistor import PulldownResistor, PullupResistor, PulldownResistorArray, PullupResistorArray
5757
from .AbstractResistor import SeriesPowerResistor, CurrentSenseResistor, AnalogClampResistor, DigitalClampResistor
5858
from .AbstractResistorArray import ResistorArray, ResistorArrayStandardFootprint, TableResistorArray
@@ -65,7 +65,13 @@
6565
TableCapacitor,
6666
TableDeratingCapacitor,
6767
)
68-
from .AbstractCapacitor import DummyCapacitorFootprint, DecouplingCapacitor, AnalogCapacitor, CombinedCapacitor
68+
from .AbstractCapacitor import (
69+
DummyCapacitorFootprint,
70+
DecouplingCapacitor,
71+
AnalogCapacitor,
72+
AnalogSeriesCapacitor,
73+
CombinedCapacitor,
74+
)
6975
from .AbstractInductor import Inductor, TableInductor, SeriesPowerInductor
7076
from .AbstractFerriteBead import FerriteBead, FerriteBeadStandardFootprint, TableFerriteBead, SeriesPowerFerriteBead
7177
from .ResistiveDivider import ResistiveDivider, VoltageDivider, VoltageSenseDivider

edg/parts/SpeakerDriver_Analog.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ def __init__(self) -> None:
112112
self.pwr,
113113
voltage_limit_tolerance=(-0.3, 0.3) * Volt,
114114
signal_limit_bound=(0.5 * Volt, -0.8 * Volt),
115-
impedance=(142, 158) * kOhm,
115+
# TODO this is too low for the input resistor and the analog link assertion, but signal attenuation is fine in practice
116+
# impedance=(142, 158) * kOhm,
116117
)
117118
self.inp = self.Port(input_port)
118119
self.inn = self.Port(input_port)
@@ -151,15 +152,15 @@ class Tpa2005d1(SpeakerDriver, Block):
151152
"""TPA2005D1 configured in single-ended input mode.
152153
Possible semi-pin-compatible with PAM8302AASCR (C113367), but which has internal resistor."""
153154

154-
def __init__(self, gain: RangeLike = Range.from_tolerance(20, 0.2)):
155+
def __init__(self, gain: RangeLike = 20 * Ratio(tol=0.2)):
155156
super().__init__()
156157
# TODO should be a SpeakerDriver abstract part
157158

158159
self.ic = self.Block(Tpa2005d1_Device())
159160
self.pwr = self.Export(self.ic.pwr, [Power])
160161
self.gnd = self.Export(self.ic.gnd, [Common])
161162

162-
self.sig = self.Port(AnalogSink(), [Input])
163+
self.sig = self.Port(AnalogSink.empty(), [Input])
163164
self.spk = self.Export(self.ic.vo, [Output])
164165

165166
self.gain = self.ArgParameter(gain)
@@ -185,34 +186,31 @@ def contents(self) -> None:
185186

186187
# Note, gain = 2 * (142k to 158k)/Ri, recommended gain < 20V/V
187188
res_value = (1 / self.gain).shrink_multiply(2 * Range(142e3, 158e3))
188-
in_res_model = Resistor(res_value)
189+
in_res_model = AnalogSeriesResistor(res_value)
189190
fc = (1, 20) * Hertz # for highpass filter, arbitrary, 20Hz right on the edge of audio frequency
191+
in_bias_voltage = self.pwr.link().voltage / 2 # assumed input bias point
190192

191193
self.inp_res = self.Block(in_res_model)
192194
self.inp_cap = self.Block(
193-
Capacitor(
195+
AnalogSeriesCapacitor(
194196
capacitance=(1 / (2 * math.pi * fc))
195197
.shrink_multiply(1 / self.inp_res.actual_resistance)
196198
.intersect((1 * 0.8, float("inf")) * uFarad),
197-
voltage=self.sig.link().voltage,
199+
output_bias=in_bias_voltage,
198200
)
199201
)
200-
self.connect(self.sig.net, self.inp_cap.neg)
201-
self.connect(self.inp_cap.pos, self.inp_res.a)
202-
self.connect(self.inp_res.b.adapt_to(AnalogSource()), self.ic.inp)
202+
self.chain(self.sig, self.inp_cap, self.inp_res, self.ic.inp)
203203

204204
self.inn_res = self.Block(in_res_model)
205205
self.inn_cap = self.Block(
206-
Capacitor(
206+
AnalogSeriesCapacitor(
207207
capacitance=(1 / (2 * math.pi * fc))
208208
.shrink_multiply(1 / self.inn_res.actual_resistance)
209209
.intersect((1 * 0.8, float("inf")) * uFarad),
210-
voltage=self.sig.link().voltage,
210+
output_bias=in_bias_voltage,
211211
)
212212
)
213-
self.connect(self.gnd, self.inn_cap.neg.adapt_to(Ground()))
214-
self.connect(self.inn_cap.pos, self.inn_res.a)
215-
self.connect(self.inn_res.b.adapt_to(AnalogSource()), self.ic.inn)
213+
self.chain(self.gnd.as_analog_source(), self.inn_cap, self.inn_res, self.ic.inn)
216214

217215

218216
class Pam8302a_Device(InternalSubcircuit, JlcPart, FootprintBlock):
@@ -291,10 +289,8 @@ def contents(self) -> None:
291289
)
292290
).connected(self.gnd, self.pwr)
293291

294-
in_cap_model = Capacitor(capacitance=0.1 * uFarad(tol=0.2), voltage=self.sig.link().voltage)
295-
self.inp_cap = self.Block(in_cap_model)
296-
self.connect(self.sig, self.inp_cap.neg.adapt_to(AnalogSink()))
297-
self.connect(self.inp_cap.pos.adapt_to(AnalogSource()), self.ic.inp)
298-
self.inn_cap = self.Block(in_cap_model)
299-
self.connect(self.gnd, self.inn_cap.neg.adapt_to(Ground()))
300-
self.connect(self.inn_cap.pos.adapt_to(AnalogSource()), self.ic.inn)
292+
input_cap_model = AnalogSeriesCapacitor(
293+
capacitance=0.1 * uFarad(tol=0.2), output_bias=self.pwr.link().voltage / 2
294+
)
295+
self.inp_cap = self.Block(input_cap_model).connected(self.sig, self.ic.inp)
296+
self.inn_cap = self.Block(input_cap_model).connected(self.gnd.as_analog_source(), self.ic.inn)

examples/DeskController/DeskController.net

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@
653653
(footprint "Resistor_SMD:R_0603_1608Metric")
654654
(property (name "Sheetname") (value "spk_drv"))
655655
(property (name "Sheetfile") (value "edg.parts.SpeakerDriver_Analog.Tpa2005d1"))
656-
(property (name "edg_path") (value "spk_drv.inp_res"))
656+
(property (name "edg_path") (value "spk_drv.inp_res.res"))
657657
(property (name "edg_short_path") (value "spk_drv.inp_res"))
658658
(property (name "edg_refdes") (value "DR10"))
659659
(property (name "edg_part") (value "0603WAF6802T5E (UNI-ROYAL(Uniroyal Elec))"))
@@ -665,7 +665,7 @@
665665
(footprint "Capacitor_SMD:C_0603_1608Metric")
666666
(property (name "Sheetname") (value "spk_drv"))
667667
(property (name "Sheetfile") (value "edg.parts.SpeakerDriver_Analog.Tpa2005d1"))
668-
(property (name "edg_path") (value "spk_drv.inp_cap"))
668+
(property (name "edg_path") (value "spk_drv.inp_cap.cap"))
669669
(property (name "edg_short_path") (value "spk_drv.inp_cap"))
670670
(property (name "edg_refdes") (value "DC15"))
671671
(property (name "edg_part") (value "CL10A105KB8NNNC (Samsung Electro-Mechanics)"))
@@ -677,7 +677,7 @@
677677
(footprint "Resistor_SMD:R_0603_1608Metric")
678678
(property (name "Sheetname") (value "spk_drv"))
679679
(property (name "Sheetfile") (value "edg.parts.SpeakerDriver_Analog.Tpa2005d1"))
680-
(property (name "edg_path") (value "spk_drv.inn_res"))
680+
(property (name "edg_path") (value "spk_drv.inn_res.res"))
681681
(property (name "edg_short_path") (value "spk_drv.inn_res"))
682682
(property (name "edg_refdes") (value "DR11"))
683683
(property (name "edg_part") (value "0603WAF6802T5E (UNI-ROYAL(Uniroyal Elec))"))
@@ -689,7 +689,7 @@
689689
(footprint "Capacitor_SMD:C_0603_1608Metric")
690690
(property (name "Sheetname") (value "spk_drv"))
691691
(property (name "Sheetfile") (value "edg.parts.SpeakerDriver_Analog.Tpa2005d1"))
692-
(property (name "edg_path") (value "spk_drv.inn_cap"))
692+
(property (name "edg_path") (value "spk_drv.inn_cap.cap"))
693693
(property (name "edg_short_path") (value "spk_drv.inn_cap"))
694694
(property (name "edg_refdes") (value "DC16"))
695695
(property (name "edg_part") (value "CL10A105KB8NNNC (Samsung Electro-Mechanics)"))
@@ -1115,10 +1115,10 @@
11151115
(net (code 40) (name "Dspk_drv.ic.inn")
11161116
(node (ref DU4) (pin 3))
11171117
(node (ref DR11) (pin 2)))
1118-
(net (code 41) (name "Dspk_drv.inp_res.a")
1118+
(net (code 41) (name "Dspk_drv.inp_res.input")
11191119
(node (ref DR10) (pin 1))
11201120
(node (ref DC15) (pin 1)))
1121-
(net (code 42) (name "Dspk_drv.inn_res.a")
1121+
(net (code 42) (name "Dspk_drv.inn_res.input")
11221122
(node (ref DR11) (pin 1))
11231123
(node (ref DC16) (pin 1)))
11241124
(net (code 43) (name "Dnpx_shift.lv_io")

examples/DeskController/DeskController.svgpcb.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,22 +211,22 @@ const DC14 = board.add(C_0805_2012Metric, {
211211
translate: pt(0.352, 3.279), rotate: 0,
212212
id: 'DC14'
213213
})
214-
// spk_drv.inp_res
214+
// spk_drv.inp_res.res
215215
const DR10 = board.add(R_0603_1608Metric, {
216216
translate: pt(0.214, 3.446), rotate: 0,
217217
id: 'DR10'
218218
})
219-
// spk_drv.inp_cap
219+
// spk_drv.inp_cap.cap
220220
const DC15 = board.add(C_0603_1608Metric, {
221221
translate: pt(0.370, 3.446), rotate: 0,
222222
id: 'DC15'
223223
})
224-
// spk_drv.inn_res
224+
// spk_drv.inn_res.res
225225
const DR11 = board.add(R_0603_1608Metric, {
226226
translate: pt(0.058, 3.543), rotate: 0,
227227
id: 'DR11'
228228
})
229-
// spk_drv.inn_cap
229+
// spk_drv.inn_cap.cap
230230
const DC16 = board.add(C_0603_1608Metric, {
231231
translate: pt(0.214, 3.543), rotate: 0,
232232
id: 'DC16'
@@ -353,8 +353,8 @@ board.setNetlist([
353353
{name: "Doled.device.c2n", pads: [["DJ3", "28"], ["DC7", "2"]]},
354354
{name: "Dspk_drv.ic.inp", pads: [["DU4", "4"], ["DR10", "2"]]},
355355
{name: "Dspk_drv.ic.inn", pads: [["DU4", "3"], ["DR11", "2"]]},
356-
{name: "Dspk_drv.inp_res.a", pads: [["DR10", "1"], ["DC15", "1"]]},
357-
{name: "Dspk_drv.inn_res.a", pads: [["DR11", "1"], ["DC16", "1"]]},
356+
{name: "Dspk_drv.inp_res.input", pads: [["DR10", "1"], ["DC15", "1"]]},
357+
{name: "Dspk_drv.inn_res.input", pads: [["DR11", "1"], ["DC16", "1"]]},
358358
{name: "Dnpx_shift.lv_io", pads: [["DU2", "6"], ["DQ3", "2"]]},
359359
{name: "Dnpx_shift.hv_io", pads: [["DQ3", "3"], ["DR12", "2"], ["DTP5", "1"], ["DD9", "4"]]},
360360
{name: "Dnpx.dout", pads: [["DD14", "2"]]},

examples/IotKnob/IotKnob.net

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,7 @@
15771577
(footprint "Capacitor_SMD:C_0603_1608Metric")
15781578
(property (name "Sheetname") (value "spk_drv"))
15791579
(property (name "Sheetfile") (value "edg.parts.SpeakerDriver_Analog.Pam8302a"))
1580-
(property (name "edg_path") (value "spk_drv.inp_cap"))
1580+
(property (name "edg_path") (value "spk_drv.inp_cap.cap"))
15811581
(property (name "edg_short_path") (value "spk_drv.inp_cap"))
15821582
(property (name "edg_refdes") (value "KC55"))
15831583
(property (name "edg_part") (value "CC0603KRX7R9BB104 (YAGEO)"))
@@ -1589,7 +1589,7 @@
15891589
(footprint "Capacitor_SMD:C_0603_1608Metric")
15901590
(property (name "Sheetname") (value "spk_drv"))
15911591
(property (name "Sheetfile") (value "edg.parts.SpeakerDriver_Analog.Pam8302a"))
1592-
(property (name "edg_path") (value "spk_drv.inn_cap"))
1592+
(property (name "edg_path") (value "spk_drv.inn_cap.cap"))
15931593
(property (name "edg_short_path") (value "spk_drv.inn_cap"))
15941594
(property (name "edg_refdes") (value "KC56"))
15951595
(property (name "edg_part") (value "CC0603KRX7R9BB104 (YAGEO)"))

0 commit comments

Comments
 (0)