Skip to content

Commit 275b102

Browse files
authored
New library parts and example boards: SX1262 LoRa, PN7160 NFC, FLIR Lepton thermal camera sensor (#384)
Adds these libraries: - Bidirectional level shifter using two FETs , requires a directionality hint - A lot of RF library components / generators, including L networks, and a L network overlaid with a second-harmonic notch - Unit tests for these verified against online calculators - DiscreteRfWarning mixin, override-able assertion warning for discrete RF blocks - TLV757P high current SOT-23 LDOs - PN7160 NFC controller with analog frontend generator. Experimental, but tested working (though not necessary optimally tuned) - Unit tests for these verified against reference documents - SX1262 LoRa transceiver, based on the reference analog frontend architecture but with generators. The component values don't line up exactly but are in the right ballpark. Example boards: - Standing desk controller - flipped RX/TX as sent to fab, corrected in libraries and generator but not layout. Layout is still good enough since the level shifters are bidirectional. Level shifter resistor for the Neopixels also need to be 1k not 4.7k. - SX1262 LoRa transceiver / PN7160 NFC omnibus RF test board - missing the SX1262 BUSY pin connection, missing the PN7160 IRQ pin connection as sent to fab, corrected in libraries and generator but not layout. - FLIR Lepton test board - works with no modifications. Changes to existing libraries: - Add gnd to Antenna interface - Add RF connectors as antennas, including Amphenol901143 SMA-F connector - Add SOT-323 to BJTs, FETs - Un-comment DCR criteria for inductors - Fixes for new JlcParts parts selector libraries - Optional reset pull for OV2640, only generated if reset not externally connected - Expansion of SG8101 crystals to also 3.2x2.5mm devices - Add support for using strapping pins of ESP32-C3-WROOM module - More common default threshold for APX803s POR generator TODO - future work - Separate Shutdown-able and Resettable
1 parent 2080f8a commit 275b102

File tree

77 files changed

+106642
-221
lines changed

Some content is hidden

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

77 files changed

+106642
-221
lines changed

edg/abstract_parts/AbstractAntenna.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ def __init__(self, frequency: RangeLike, impedance: RangeLike = Range.all(), pow
1919
self.power = self.ArgParameter(power)
2020
self.actual_power_rating = self.Parameter(RangeExpr())
2121

22-
self.a = self.Port(Passive.empty())
22+
self.a = self.Port(Passive.empty(), [Input])
23+
self.gnd = self.Port(Ground.empty(), [Common])
2324

2425

2526
@non_library

edg/abstract_parts/AbstractBjt.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ class BjtStandardFootprint(StandardFootprint['Bjt']):
1111
REFDES_PREFIX = 'Q'
1212

1313
FOOTPRINT_PINNING_MAP = {
14-
'Package_TO_SOT_SMD:SOT-23': lambda block: {
14+
(
15+
'Package_TO_SOT_SMD:SOT-23',
16+
'Package_TO_SOT_SMD:SOT-323_SC-70',
17+
): lambda block: {
1518
'1': block.base,
1619
'2': block.emitter,
1720
'3': block.collector,
@@ -21,11 +24,6 @@ class BjtStandardFootprint(StandardFootprint['Bjt']):
2124
'2': block.collector,
2225
'3': block.emitter,
2326
},
24-
'Package_TO_SOT_SMD:SOT-323_SC-70': lambda block: {
25-
'1': block.base,
26-
'2': block.emitter,
27-
'3': block.collector,
28-
},
2927
}
3028

3129

edg/abstract_parts/AbstractConnector.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ..electronics_model import *
22
from .Categories import Connector
3+
from .AbstractAntenna import Antenna
34

45

56
@abstract_block
@@ -21,7 +22,7 @@ class RfConnector(Connector):
2122
"""Base class for a RF connector, with a signal and ground. Signal is passive-typed."""
2223
def __init__(self) -> None:
2324
super().__init__()
24-
self.sig = self.Port(Passive.empty())
25+
self.sig = self.Port(Passive.empty(), [Input])
2526
self.gnd = self.Port(Ground(), [Common])
2627

2728

@@ -33,5 +34,32 @@ def __init__(self, name: StringLike):
3334
self.tp_name = self.ArgParameter(name)
3435

3536

37+
class RfConnectorAntenna(Antenna):
38+
"""RF connector used as an antenna"""
39+
def __init__(self, *args, **kwargs):
40+
super().__init__(*args, **kwargs)
41+
self.conn = self.Block(RfConnector())
42+
self.connect(self.conn.sig, self.a)
43+
self.connect(self.conn.gnd, self.gnd)
44+
45+
46+
@abstract_block
3647
class UflConnector(RfConnector):
3748
"""Base class for a U.FL / IPEX / UMCC connector, miniature RF connector."""
49+
50+
51+
@abstract_block
52+
class SmaConnector(RfConnector):
53+
"""Base class for a SMA coax connector."""
54+
55+
56+
@abstract_block
57+
class SmaMConnector(SmaConnector):
58+
"""Base class for a SMA M connector, pin with internal threads.
59+
Typically used on the antenna itself."""
60+
61+
62+
@abstract_block
63+
class SmaFConnector(SmaConnector):
64+
"""Base class for a SMA F connector, socket with external threads.
65+
Typically used for an antenna connector for sub-2.4GHz applications; 2.4GHz uses RP-SMA."""

edg/abstract_parts/AbstractFets.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ class FetStandardFootprint(StandardFootprint['Fet']):
1111
REFDES_PREFIX = 'Q'
1212

1313
FOOTPRINT_PINNING_MAP = {
14-
'Package_TO_SOT_SMD:SOT-23': lambda block: {
14+
(
15+
'Package_TO_SOT_SMD:SOT-23',
16+
'Package_TO_SOT_SMD:SOT-323_SC-70',
17+
): lambda block: {
1518
'1': block.gate,
1619
'2': block.source,
1720
'3': block.drain,

edg/abstract_parts/AbstractInductor.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]:
9393
@init_in_parent
9494
def __init__(self, inductance: RangeLike,
9595
current: RangeLike = RangeExpr.ZERO,
96-
frequency: RangeLike = RangeExpr.ZERO) -> None:
96+
frequency: RangeLike = RangeExpr.ZERO,
97+
resistance_dc: RangeLike = (0, 1)*Ohm # generic sane choice?
98+
) -> None:
9799
super().__init__()
98100

99101
self.a = self.Port(Passive.empty())
@@ -102,12 +104,12 @@ def __init__(self, inductance: RangeLike,
102104
self.inductance = self.ArgParameter(inductance)
103105
self.current = self.ArgParameter(current) # defined as operating current range, non-directioned
104106
self.frequency = self.ArgParameter(frequency) # defined as operating frequency range
105-
# TODO: in the future, when we consider efficiency - for now, use current ratings
106-
# self.resistance_dc = self.Parameter(RangeExpr())
107+
self.resistance_dc = self.ArgParameter(resistance_dc)
107108

108109
self.actual_inductance = self.Parameter(RangeExpr())
109110
self.actual_current_rating = self.Parameter(RangeExpr())
110111
self.actual_frequency_rating = self.Parameter(RangeExpr())
112+
self.actual_resistance_dc = self.Parameter(RangeExpr())
111113

112114
def contents(self):
113115
super().contents()
@@ -118,7 +120,9 @@ def contents(self):
118120
"<b>current rating:</b> ", DescriptionString.FormatUnits(self.actual_current_rating, "A"),
119121
" <b>of operating:</b> ", DescriptionString.FormatUnits(self.current, "A"), "\n",
120122
"<b>frequency rating:</b> ", DescriptionString.FormatUnits(self.actual_frequency_rating, "Hz"),
121-
" <b>of operating:</b> ", DescriptionString.FormatUnits(self.frequency, "Hz")
123+
" <b>of operating:</b> ", DescriptionString.FormatUnits(self.frequency, "Hz"), "\n",
124+
"<b>dc resistance:</b> ", DescriptionString.FormatUnits(self.actual_resistance_dc, "Ω"),
125+
" <b>of spec:</b> ", DescriptionString.FormatUnits(self.resistance_dc, "Ω"),
122126
)
123127

124128

@@ -132,21 +136,22 @@ class TableInductor(PartsTableSelector, Inductor):
132136
@init_in_parent
133137
def __init__(self, *args, **kwargs) -> None:
134138
super().__init__(*args, **kwargs)
135-
self.generator_param(self.inductance, self.current, self.frequency)
139+
self.generator_param(self.inductance, self.current, self.frequency, self.resistance_dc)
136140

137141
def _row_filter(self, row: PartsTableRow) -> bool:
138142
# TODO eliminate arbitrary DCR limit in favor of exposing max DCR to upper levels
139143
return super()._row_filter(row) and \
140144
row[self.INDUCTANCE].fuzzy_in(self.get(self.inductance)) and \
141145
self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) and \
142-
row[self.DC_RESISTANCE].fuzzy_in(Range.zero_to_upper(1.0)) and \
146+
row[self.DC_RESISTANCE].fuzzy_in(self.get(self.resistance_dc)) and \
143147
self.get(self.frequency).fuzzy_in(row[self.FREQUENCY_RATING])
144148

145149
def _row_generate(self, row: PartsTableRow) -> None:
146150
super()._row_generate(row)
147151
self.assign(self.actual_inductance, row[self.INDUCTANCE])
148152
self.assign(self.actual_current_rating, row[self.CURRENT_RATING])
149153
self.assign(self.actual_frequency_rating, row[self.FREQUENCY_RATING])
154+
self.assign(self.actual_resistance_dc, row[self.DC_RESISTANCE])
150155

151156

152157
class SeriesPowerInductor(DiscreteApplication):

edg/abstract_parts/LevelShifter.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from ..electronics_model import *
2+
from .Categories import *
3+
from .AbstractResistor import PullupResistor
4+
from .AbstractFets import Fet
5+
from .DummyDevices import DummyVoltageSink
6+
7+
class BidirectionaLevelShifter(Interface, GeneratorBlock):
8+
"""Bidirectional level shifter for low(ish) frequency signals.
9+
Circuit design from Phillips AN97055, https://cdn-shop.adafruit.com/datasheets/an97055.pdf
10+
When both sides are floating or driving high, the FET is off and the pullups provide the high signal.
11+
When the LV side drives low, the FET source goes to ground, putting the FET into conduction and pulling HV low.
12+
When the HV side drives low, the body diode pulls the FET source low, then goes into conduction.
13+
14+
Use infinity resistance to not generate a resistor, for example if it is known there is already a resistor
15+
on that side.
16+
17+
src_hint = 'lv' | 'hv' | '' determines the 'source' side to help the electronics model resolve directionality
18+
and does not affect circuit generation or functionality.
19+
If empty, both sides are assumed to be able to drive the shifter and must have voltages and output thresholds
20+
modeled. TODO: this mode may be brittle
21+
"""
22+
@init_in_parent
23+
def __init__(self, lv_res: RangeLike = 4.7*kOhm(tol=0.05), hv_res: RangeLike = 4.7*kOhm(tol=0.05),
24+
src_hint: StringLike = '') -> None:
25+
super().__init__()
26+
self.lv_pwr = self.Port(VoltageSink.empty())
27+
self.lv_io = self.Port(DigitalBidir.empty())
28+
self.hv_pwr = self.Port(VoltageSink.empty())
29+
self.hv_io = self.Port(DigitalBidir.empty())
30+
31+
self.lv_res = self.ArgParameter(lv_res)
32+
self.hv_res = self.ArgParameter(hv_res)
33+
self.src_hint = self.ArgParameter(src_hint)
34+
self.generator_param(self.lv_res, self.hv_res, self.src_hint)
35+
36+
def generate(self) -> None:
37+
super().generate()
38+
39+
self.fet = self.Block(Fet.NFet(
40+
drain_voltage=self.hv_pwr.link().voltage.hull(self.hv_io.link().voltage),
41+
drain_current=self.lv_io.link().current_drawn.hull(self.hv_io.link().current_drawn),
42+
gate_voltage=self.lv_pwr.link().voltage - self.lv_io.link().voltage,
43+
rds_on=(0, 1)*Ohm # arbitrary
44+
))
45+
46+
if self.get(self.src_hint) == 'lv': # LV is source, HV model is incomplete
47+
lv_io_model = DigitalBidir(
48+
voltage_out=self.lv_pwr.link().voltage, # this is not driving, effectively only a pullup
49+
output_thresholds=self.lv_pwr.link().voltage.hull(-float('inf'))
50+
)
51+
else: # HV model is complete, can use its thresholds
52+
lv_io_model = DigitalBidir(
53+
voltage_out=self.lv_pwr.link().voltage.hull(self.hv_io.link().voltage.lower()),
54+
output_thresholds=self.lv_pwr.link().voltage.hull(self.hv_io.link().voltage.lower())
55+
)
56+
57+
if self.get(self.src_hint) == 'hv': # HV is source, LV model is incomplete
58+
hv_io_model = DigitalBidir(
59+
voltage_out=self.hv_pwr.link().voltage, # this is not driving, effectively only a pullup
60+
output_thresholds=self.hv_pwr.link().voltage.hull(-float('inf'))
61+
)
62+
else: # HV model is complete, can use its thresholds
63+
hv_io_model = DigitalBidir(
64+
voltage_out=self.hv_pwr.link().voltage.hull(self.lv_io.link().voltage.lower()),
65+
output_thresholds=self.hv_pwr.link().voltage.hull(self.lv_io.link().voltage.lower())
66+
)
67+
68+
self.connect(self.lv_io, self.fet.source.adapt_to(lv_io_model))
69+
self.connect(self.hv_io, self.fet.drain.adapt_to(hv_io_model))
70+
self.connect(self.lv_pwr, self.fet.gate.adapt_to(VoltageSink()))
71+
72+
if self.get(self.lv_res) != RangeExpr.INF:
73+
self.lv_pu = self.Block(PullupResistor(self.lv_res)).connected(self.lv_pwr, self.lv_io)
74+
if self.get(self.hv_res) != RangeExpr.INF:
75+
self.hv_pu = self.Block(PullupResistor(self.hv_res)).connected(self.hv_pwr, self.hv_io)
76+
else:
77+
self.dummy_hv = self.Block(DummyVoltageSink()) # must be connected
78+
self.connect(self.dummy_hv.pwr, self.hv_pwr)

0 commit comments

Comments
 (0)