Skip to content

Commit 8f81485

Browse files
authored
SMUv3 example (#341)
Another version of the SMU board, that fixes problems with V2. Specifically: - Power stage - Design target output capability of 30V 3A - Bulk Vusb capacitor and input ferrite filter - Operates off 5v, by using gate drivers that are 4.5v capable and powered off the buck converter - Add comparator / latch protection on Vconv - Measurement / control stage - Use integrated current-sense amp, for greater accuracy and CMRR - Fixed +33v, -3v analog control rails, to reduce PSRR issues in measurement - Use clamping resistors as protection between voltage domains, as opposed to Zeners - Add REF3033 common reference that feeds all references - Add shielded (coax RX) test points of control and measurement signals - Use lower noise opamps, dualpack all the opamps - Add DUT IO (2 GPIOs) and Qwiic connector (on an independent I2C line) - Add 2.54mm output option - Add fan - Use larger OLED - Add current ranging (3A / 300mA ranges) - Move encoder to MCU pins... so it can actually be read by ESPHome Library improvements: - Capacitor improvements - Add CeramicCapacitor and AluminumCapacitor subtypes - Add JlcAluminumCapacitor - Make TableCapacitor standalone, where auto-parallel-ing and derating aren't required - Add InOut tag to DecouplingCapacitor - RF connector and test points - Add RfConnector base class and UflConnector - Add RfConnectorTestPoint, for sensitive signals where shielding is desired - Add AnalogClampResistor, as protection element for moving signals between different voltage domains. It changes the modeled voltage range, but not the signal range; the signal range must be within the output range. - Deprecate AnalogClampZenerDiode, it wasn't ever a good option without a clamping resistor anyways - Expand inductors footprints and JLC parsing to larger parts - Add DummyAnalogSource dummy block - Half Bridge - Move L/H (independent) and PWM drive to mixins on top of the base HalfBridgeDriver (gate driver) class and HalfBridge class. PWM enable is modeled separately via the Resettable mixin - Add independent and PWM versions of CustomSyncBuckBoostConverter - Add NCP3420 4.5v-compatible gate driver - Opamps - Support schematic-import of OpampFollower - Add OPA2171 dual-pack general-purpose 36v opamp - Add OPA2189 dual-pack prevision 36v opamp - Refactor OPA197/2197 to share common definitions - Add TLV9152 dual-pack low-noise 5v amp - MCP3561: support optional external Vref, generate correct part number based on used channels - No longer possible to export Vref - Add DG468 analog switch - Voltage converters - Fix input and output ripple limits not being propagated in some devices - Add LM2733 boost converter - Add LM2664 switched cap inverter - Misc logic elements - Add LMV311 comparator - Add SN74LVC1G74 flip-flop with asynchronous (pre)set and reset - Add Qwiic target connector, that acts as a I2C target (for connecting Qwiic devices downstream) - Add SMT JST-SH connector - Add AD8418A fixed-gain 20x current sense amp, it should be a lot more accurate and have less common-mode issues than the discrete diffamp circuit - Solid state relays: make the current ratings bidirectional - Shrink footprint courtyard for SKRH directional switch - DigitalLink: support the case when only SingleSource and Sink connected Infrastructural changes - Add `RangeExpr.cancel_multiply` and `.center`, to allow cancel-multiple in expression-land - Break out StandardFootprint._footprint_pinning_map to allow access to the pinmaps / supported footprints Resolves #86 Resolves #311
1 parent de4cb38 commit 8f81485

File tree

65 files changed

+122052
-72347
lines changed

Some content is hidden

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

65 files changed

+122052
-72347
lines changed

edg/BoardTop.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def refinements(self) -> Refinements:
8888
(Opamp, Lmv321),
8989
(SpiMemory, W25q), # 128M version is a basic part
9090
(TestPoint, Keystone5015), # this is larger, but is part of JLC's parts inventory
91+
(UflConnector, Bwipx_1_001e),
9192
],
9293
class_values=[ # realistically only RCs are going to likely be basic parts
9394
(JlcResistor, ['require_basic_part'], True),

edg_core/ConstraintExpr.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,15 @@ def _from_lit(cls, pb: edgir.ValueLit) -> Range:
358358
assert pb.HasField('range') and pb.range.minimum.HasField('floating') and pb.range.maximum.HasField('floating')
359359
return Range(pb.range.minimum.floating.val, pb.range.maximum.floating.val)
360360

361+
@classmethod
362+
def cancel_multiply(cls, input_side: RangeLike, output_side: RangeLike) -> RangeExpr:
363+
"""See Range.cancel_multiply"""
364+
input_expr = cls._to_expr_type(input_side)
365+
output_expr = cls._to_expr_type(output_side)
366+
lower = input_expr.upper() * output_expr.lower()
367+
upper = input_expr.lower() * output_expr.upper()
368+
return cls._to_expr_type((lower, upper)) # rely on internally to check for non-empty range
369+
361370
def __init__(self, initializer: Optional[RangeLike] = None) -> None:
362371
# must cast non-empty initializer type, because range supports wider initializers
363372
# TODO and must ignore initializers of self-type (because model weirdness) - remove model support!
@@ -396,6 +405,9 @@ def lower(self) -> FloatExpr:
396405
def upper(self) -> FloatExpr:
397406
return self._upper
398407

408+
def center(self) -> FloatExpr:
409+
return (self._lower + self._upper) / 2
410+
399411
@classmethod
400412
def _create_range_float_binary_op(cls,
401413
lhs: RangeExpr,

electronics_abstract_parts/AbstractCapacitor.py

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ def __init__(self, *args, **kwargs) -> None:
8484
self.neg = self.Port(Passive.empty())
8585

8686

87+
@abstract_block
88+
class CeramicCapacitor(Capacitor):
89+
"""Abstract base class for ceramic capacitors, which appear more ideal in terms of lower ESP"""
90+
pass
91+
92+
93+
@abstract_block
94+
class AluminumCapacitor(Capacitor):
95+
"""Abstract base class for aluminum electrolytic capacitors capacitors which provide compact bulk capacitance
96+
but at the cost of ESR"""
97+
pass
98+
99+
87100
@non_library
88101
class CapacitorStandardFootprint(Capacitor, StandardFootprint[Capacitor]):
89102
REFDES_PREFIX = 'C'
@@ -99,6 +112,56 @@ class CapacitorStandardFootprint(Capacitor, StandardFootprint[Capacitor]):
99112
'Capacitor_SMD:C_1210_3225Metric',
100113
'Capacitor_SMD:C_1812_4532Metric',
101114
'Capacitor_SMD:C_2512_6332Metric',
115+
116+
'Capacitor_SMD:CP_Elec_3x5.3',
117+
'Capacitor_SMD:CP_Elec_3x5.4',
118+
'Capacitor_SMD:CP_Elec_4x3',
119+
'Capacitor_SMD:CP_Elec_4x3.9',
120+
'Capacitor_SMD:CP_Elec_4x4.5',
121+
'Capacitor_SMD:CP_Elec_4x5.3',
122+
'Capacitor_SMD:CP_Elec_4x5.4',
123+
'Capacitor_SMD:CP_Elec_4x5.7',
124+
'Capacitor_SMD:CP_Elec_4x5.8',
125+
'Capacitor_SMD:CP_Elec_5x3',
126+
'Capacitor_SMD:CP_Elec_5x3.9',
127+
'Capacitor_SMD:CP_Elec_5x4.4',
128+
'Capacitor_SMD:CP_Elec_5x4.5',
129+
'Capacitor_SMD:CP_Elec_5x5.3',
130+
'Capacitor_SMD:CP_Elec_5x5.4',
131+
'Capacitor_SMD:CP_Elec_5x5.7',
132+
'Capacitor_SMD:CP_Elec_5x5.8',
133+
'Capacitor_SMD:CP_Elec_5x5.9',
134+
'Capacitor_SMD:CP_Elec_6.3x3',
135+
'Capacitor_SMD:CP_Elec_6.3x3.9',
136+
'Capacitor_SMD:CP_Elec_6.3x4.5',
137+
'Capacitor_SMD:CP_Elec_6.3x4.9',
138+
'Capacitor_SMD:CP_Elec_6.3x5.2',
139+
'Capacitor_SMD:CP_Elec_6.3x5.3',
140+
'Capacitor_SMD:CP_Elec_6.3x5.4',
141+
'Capacitor_SMD:CP_Elec_6.3x5.7',
142+
'Capacitor_SMD:CP_Elec_6.3x5.8',
143+
'Capacitor_SMD:CP_Elec_6.3x5.9',
144+
'Capacitor_SMD:CP_Elec_6.3x7.7',
145+
'Capacitor_SMD:CP_Elec_6.3x9.9',
146+
'Capacitor_SMD:CP_Elec_8x5.4',
147+
'Capacitor_SMD:CP_Elec_8x6.2',
148+
'Capacitor_SMD:CP_Elec_8x6.5',
149+
'Capacitor_SMD:CP_Elec_8x6.7',
150+
'Capacitor_SMD:CP_Elec_8x6.9',
151+
'Capacitor_SMD:CP_Elec_8x10',
152+
'Capacitor_SMD:CP_Elec_8x10.5',
153+
'Capacitor_SMD:CP_Elec_8x11.9',
154+
'Capacitor_SMD:CP_Elec_10x7.7',
155+
'Capacitor_SMD:CP_Elec_10x7.9',
156+
'Capacitor_SMD:CP_Elec_10x10',
157+
'Capacitor_SMD:CP_Elec_10x10.5',
158+
'Capacitor_SMD:CP_Elec_10x12.5',
159+
'Capacitor_SMD:CP_Elec_10x12.6',
160+
'Capacitor_SMD:CP_Elec_10x14.3',
161+
'Capacitor_SMD:CP_Elec_16x17.5',
162+
'Capacitor_SMD:CP_Elec_16x22',
163+
'Capacitor_SMD:CP_Elec_18x7.5',
164+
'Capacitor_SMD:CP_Elec_18x22',
102165
): lambda block: {
103166
'1': block.pos,
104167
'2': block.neg,
@@ -121,16 +184,34 @@ class CapacitorStandardFootprint(Capacitor, StandardFootprint[Capacitor]):
121184

122185

123186
@non_library
124-
class TableCapacitor(Capacitor):
125-
"""Abstract table-based capacitor, providing some interface column definitions.
126-
DO NOT USE DIRECTLY - this provides no selection logic implementation."""
187+
class TableCapacitor(CapacitorStandardFootprint, PartsTableFootprintSelector):
188+
"""Abstract table-based capacitor, providing some interface column definitions."""
127189
CAPACITANCE = PartsTableColumn(Range)
128190
NOMINAL_CAPACITANCE = PartsTableColumn(float) # nominal capacitance, even with asymmetrical tolerances
129191
VOLTAGE_RATING = PartsTableColumn(Range)
130192

193+
@init_in_parent
194+
def __init__(self, *args, **kwargs):
195+
super().__init__(*args, **kwargs)
196+
self.generator_param(self.capacitance, self.voltage, self.voltage_rating_derating, self.exact_capacitance)
197+
198+
def _row_generate(self, row: PartsTableRow) -> None:
199+
super()._row_generate(row)
200+
self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING])
201+
self.assign(self.actual_capacitance, row[self.CAPACITANCE])
202+
203+
def _row_filter(self, row: PartsTableRow) -> bool:
204+
derated_voltage = self.get(self.voltage) / self.get(self.voltage_rating_derating)
205+
return super()._row_filter(row) and \
206+
derated_voltage.fuzzy_in(row[self.VOLTAGE_RATING]) and \
207+
self._row_filter_capacitance(row)
208+
209+
def _row_filter_capacitance(self, row: PartsTableRow) -> bool:
210+
return row[self.CAPACITANCE].fuzzy_in(self.get(self.capacitance))
211+
131212

132213
@non_library
133-
class TableDeratingCapacitor(CapacitorStandardFootprint, TableCapacitor, PartsTableFootprintSelector):
214+
class TableDeratingCapacitor(TableCapacitor):
134215
"""Abstract table-based capacitor with derating based on a part-part voltage coefficient."""
135216
VOLTCO = PartsTableColumn(float)
136217
DERATED_CAPACITANCE = PartsTableColumn(Range)
@@ -151,16 +232,13 @@ class TableDeratingCapacitor(CapacitorStandardFootprint, TableCapacitor, PartsTa
151232
def __init__(self, *args, single_nominal_capacitance: RangeLike = (0, 22)*uFarad, **kwargs):
152233
super().__init__(*args, **kwargs)
153234
self.single_nominal_capacitance = self.ArgParameter(single_nominal_capacitance)
154-
self.generator_param(self.capacitance, self.voltage, self.single_nominal_capacitance,
155-
self.voltage_rating_derating, self.exact_capacitance)
235+
self.generator_param(self.single_nominal_capacitance)
156236

157237
self.actual_derated_capacitance = self.Parameter(RangeExpr())
158238

159-
def _row_filter(self, row: PartsTableRow) -> bool:
160-
derated_voltage = self.get(self.voltage) / self.get(self.voltage_rating_derating)
161-
return super()._row_filter(row) and \
162-
derated_voltage.fuzzy_in(row[self.VOLTAGE_RATING]) and \
163-
Range.exact(row[self.NOMINAL_CAPACITANCE]).fuzzy_in(self.get(self.single_nominal_capacitance))
239+
def _row_filter_capacitance(self, row: PartsTableRow) -> bool:
240+
# post-derating capacitance filtering is in _table_postprocess
241+
return Range.exact(row[self.NOMINAL_CAPACITANCE]).fuzzy_in(self.get(self.single_nominal_capacitance))
164242

165243
def _table_postprocess(self, table: PartsTable) -> PartsTable:
166244
def add_derated_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]:
@@ -201,8 +279,6 @@ def add_derated_row(row: PartsTableRow) -> Optional[Dict[PartsTableColumn, Any]]
201279
def _row_generate(self, row: PartsTableRow) -> None:
202280
if row[self.PARALLEL_COUNT] == 1:
203281
super()._row_generate(row) # creates the footprint
204-
self.assign(self.actual_voltage_rating, row[self.VOLTAGE_RATING])
205-
self.assign(self.actual_capacitance, row[self.CAPACITANCE])
206282
self.assign(self.actual_derated_capacitance, row[self.DERATED_CAPACITANCE])
207283
else:
208284
self.assign(self.actual_part, f"{row[self.PARALLEL_COUNT]}x {row[self.PART_NUMBER_COL]}")
@@ -256,7 +332,7 @@ def __init__(self, capacitance: RangeLike, *, exact_capacitance: BoolLike = Fals
256332
self.pwr = self.Export(self.cap.pos.adapt_to(VoltageSink(
257333
voltage_limits=(self.cap.actual_voltage_rating + self.gnd.link().voltage).hull(self.gnd.link().voltage),
258334
current_draw=0*Amp(tol=0)
259-
)), [Power])
335+
)), [Power, InOut])
260336

261337
self.assign(self.cap.voltage, self.pwr.link().voltage - self.gnd.link().voltage)
262338

electronics_abstract_parts/AbstractConnector.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,24 @@ def __init__(self) -> None:
1414
class BananaSafetyJack(BananaJack):
1515
"""Base class for a single terminal 4mm banana jack supporting a safety sheath,
1616
such as on multimeter leads."""
17+
18+
19+
@abstract_block
20+
class RfConnector(Connector):
21+
"""Base class for a RF connector, with a signal and ground. Signal is passive-typed."""
22+
def __init__(self) -> None:
23+
super().__init__()
24+
self.sig = self.Port(Passive.empty())
25+
self.gnd = self.Port(Ground(), [Common])
26+
27+
28+
class RfConnectorTestPoint(BlockInterfaceMixin[RfConnector]):
29+
"""Test point mixin that allows the footprint to take a name"""
30+
@init_in_parent
31+
def __init__(self, name: StringLike):
32+
super().__init__()
33+
self.tp_name = self.ArgParameter(name)
34+
35+
36+
class UflConnector(RfConnector):
37+
"""Base class for a U.FL / IPEX / UMCC connector, miniature RF connector."""

electronics_abstract_parts/AbstractDiodes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Dict
2+
from deprecated import deprecated
23

34
from electronics_model import *
45
from .DummyDevices import ForcedAnalogVoltage
@@ -183,6 +184,7 @@ def contents(self):
183184
self.connect(self.diode.anode.adapt_to(Ground()), self.gnd)
184185

185186

187+
@deprecated("Use AnalogClampResistor, which should be cheaper and cause less signal distortion")
186188
class AnalogClampZenerDiode(Protection, KiCadImportableBlock):
187189
"""Analog overvoltage protection diode to clamp the input voltage"""
188190
@init_in_parent

electronics_abstract_parts/AbstractInductor.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class InductorStandardFootprint(Inductor, StandardFootprint[Inductor]):
6464
'Inductor_SMD:L_Bourns-SRR1005',
6565
'Inductor_SMD:L_Bourns_SRR1210A',
6666
'Inductor_SMD:L_Bourns_SRR1260',
67+
'Inductor_SMD:L_Bourns_SRP1245A',
6768

6869
'Inductor_SMD:L_Sunlord_SWPA3010S',
6970
'Inductor_SMD:L_Sunlord_SWPA3012S',
@@ -105,6 +106,14 @@ class InductorStandardFootprint(Inductor, StandardFootprint[Inductor]):
105106
'Inductor_SMD:L_TDK_SLF12555',
106107
'Inductor_SMD:L_TDK_SLF12565',
107108
'Inductor_SMD:L_TDK_SLF12575',
109+
110+
'Inductor_SMD:L_Vishay_IHLP-1212',
111+
'Inductor_SMD:L_Vishay_IHLP-1616',
112+
'Inductor_SMD:L_Vishay_IHLP-2020',
113+
'Inductor_SMD:L_Vishay_IHLP-2525',
114+
'Inductor_SMD:L_Vishay_IHLP-4040',
115+
'Inductor_SMD:L_Vishay_IHLP-5050',
116+
'Inductor_SMD:L_Vishay_IHLP-6767',
108117
): lambda block: {
109118
'1': block.a,
110119
'2': block.b,

electronics_abstract_parts/AbstractPowerConverters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc
373373
efficiency: RangeLike = (0.8, 1.0), # from TI reference
374374
input_voltage_ripple: FloatLike = 75*mVolt,
375375
output_voltage_ripple: FloatLike = 25*mVolt,
376-
dutycycle_limit: RangeLike = (0.2, 0.85)): # arbitrary
376+
dutycycle_limit: RangeLike = (0.1, 0.9)): # arbitrary
377377
super().__init__()
378378

379379
self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models input cap and inductor power draw

electronics_abstract_parts/AbstractResistor.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import re
2-
from typing import Optional, cast, Mapping
2+
from typing import Optional, cast, Mapping, Dict
33

44
from electronics_model import *
55
from .PartsTable import PartsTableColumn, PartsTableRow
@@ -228,7 +228,7 @@ def __init__(self, resistance: RangeLike) -> None:
228228

229229
self.pwr_out = self.Port(VoltageSource.empty(), [Output]) # forward declaration
230230
self.pwr_in = self.Port(VoltageSink.empty(), [Power, Input]) # forward declaration
231-
current_draw = self.pwr_out.link().current_drawn
231+
current_draw = self.pwr_out.link().current_drawn.abs()
232232

233233
self.res = self.Block(Resistor(
234234
resistance=self.resistance,
@@ -293,3 +293,46 @@ def connected(self, pwr_in: Optional[Port[VoltageLink]] = None, pwr_out: Optiona
293293
if pwr_out is not None:
294294
cast(Block, builder.get_enclosing_block()).connect(pwr_out, self.pwr_out)
295295
return self
296+
297+
298+
class AnalogClampResistor(Protection, KiCadImportableBlock):
299+
"""Inline resistor that limits the current (to a parameterized amount) which works in concert
300+
with ESD diodes in the downstream device to clamp the signal voltage to allowable levels.
301+
302+
The protection voltage can be extended beyond the modeled range from the input signal,
303+
and can also be specified to allow zero output voltage (for when the downstream device
304+
is powered down)
305+
306+
TODO: clamp_target should be inferred from the target voltage_limits,
307+
but voltage_limits doesn't always get propagated"""
308+
@init_in_parent
309+
def __init__(self, clamp_target: RangeLike = (0, 3)*Volt, clamp_current: RangeLike = (0.25, 2.5)*mAmp,
310+
protection_voltage: RangeLike = (0, 0)*Volt, zero_out: BoolLike = False):
311+
super().__init__()
312+
313+
self.signal_in = self.Port(AnalogSink.empty(), [Input])
314+
self.signal_out = self.Port(AnalogSource.empty(), [Output])
315+
316+
self.clamp_target = self.ArgParameter(clamp_target)
317+
self.clamp_current = self.ArgParameter(clamp_current)
318+
self.protection_voltage = self.ArgParameter(protection_voltage)
319+
self.zero_out = self.ArgParameter(zero_out)
320+
321+
def contents(self):
322+
super().contents()
323+
324+
# TODO bidirectional clamping calcs?
325+
self.res = self.Block(Resistor(resistance=1/self.clamp_current * self.zero_out.then_else(
326+
self.signal_in.link().voltage.hull(self.protection_voltage).upper(),
327+
self.signal_in.link().voltage.hull(self.protection_voltage).upper() - self.clamp_target.upper(),
328+
)))
329+
self.connect(self.res.a.adapt_to(AnalogSink()), self.signal_in)
330+
self.connect(self.res.b.adapt_to(AnalogSource(
331+
voltage_out=self.signal_in.link().voltage.intersect(self.clamp_target),
332+
signal_out=self.signal_in.link().signal,
333+
impedance=self.signal_in.link().source_impedance + self.res.actual_resistance
334+
)), self.signal_out)
335+
336+
def symbol_pinning(self, symbol_name: str) -> Dict[str, Port]:
337+
assert symbol_name == 'Device:R'
338+
return {'1': self.signal_in, '2': self.signal_out}

electronics_abstract_parts/AbstractTestPoint.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from typing import cast, List
1+
from typing import cast
22

33
from electronics_model import *
44
from electronics_model.CanPort import CanLogicLink
55
from electronics_model.I2cPort import I2cLink
6+
from .AbstractConnector import RfConnector, RfConnectorTestPoint
7+
from .AbstractResistor import Resistor
68
from .Categories import *
79

810

@@ -32,6 +34,23 @@ def contents(self):
3234
self.assign(self.tp.tp_name, (self.tp_name == "").then_else(self.io.link().name(), self.tp_name))
3335

3436

37+
@non_library
38+
class BaseRfTestPoint(TypedTestPoint, Block):
39+
"""Base class with utility infrastructure for typed RF test points."""
40+
@init_in_parent
41+
def __init__(self, tp_name: StringLike = "") -> None:
42+
super().__init__()
43+
self.tp_name = self.ArgParameter(tp_name)
44+
self.conn = self.Block(RfConnector())
45+
self.gnd = self.Export(self.conn.gnd, [Common])
46+
self.io: Port
47+
48+
def contents(self):
49+
super().contents()
50+
conn_tp = self.conn.with_mixin(RfConnectorTestPoint(StringExpr()))
51+
self.assign(conn_tp.tp_name, (self.tp_name == "").then_else(self.io.link().name(), self.tp_name))
52+
53+
3554
class VoltageTestPoint(BaseTypedTestPoint, Block):
3655
"""Test point with a VoltageSink port."""
3756
def __init__(self, *args):
@@ -78,7 +97,7 @@ def generate(self):
7897

7998

8099
class AnalogTestPoint(BaseTypedTestPoint, Block):
81-
"""Test point with a AnalogSink port."""
100+
"""Test point with a AnalogSink port"""
82101
def __init__(self, *args):
83102
super().__init__(*args)
84103
self.io = self.Port(AnalogSink.empty(), [InOut])
@@ -89,6 +108,19 @@ def connected(self, io: Port[AnalogLink]) -> 'AnalogTestPoint':
89108
return self
90109

91110

111+
class AnalogRfTestPoint(BaseRfTestPoint, Block):
112+
"""Test point with a AnalogSink port and 50-ohm matching resistor."""
113+
def __init__(self, *args):
114+
super().__init__(*args)
115+
self.res = self.Block(Resistor(50*Ohm(tol=0.05)))
116+
self.io = self.Export(self.res.a.adapt_to(AnalogSink()), [InOut])
117+
self.connect(self.res.b, self.conn.sig)
118+
119+
def connected(self, io: Port[AnalogLink]) -> 'AnalogRfTestPoint':
120+
cast(Block, builder.get_enclosing_block()).connect(io, self.io)
121+
return self
122+
123+
92124
class I2cTestPoint(TypedTestPoint, Block):
93125
"""Two test points for I2C SDA and SCL"""
94126
@init_in_parent

0 commit comments

Comments
 (0)