"""KSZ9031RNX gigabit Ethernet PHY application circuit.
Section, table and figure numbers cite the KSZ9031RNX data sheet, DS00002117K,
linked from :attr:`~jitxexamples.components.ethernet_phys.microchip_KSZ9031RNX.KSZ9031RNX.datasheet`.
ANLAN206 is the Microchip application note Section 11.0 cites as the LDO
controller's design reference; its Figure 8 is the KSZ9031RNX circuit.
The circuit runs the PHY from one 3.3 V rail, for AVDDH and DVDDH. The PHY's
on-chip LDO controller makes the 1.2 V core (AVDDL, AVDDL_PLL and DVDDL) from it
through an external P-channel MOSFET. The circuit holds the parts the PHY needs
to run: that MOSFET and its capacitors, the 25 MHz crystal and its load
capacitors, the strap-in resistors, the LED series resistors, the ISET bias
resistor, the power-on reset network, the interrupt pull-up and the supply
decoupling. Its boundary is typed: one ``Power`` port for the 3.3 V rail,
``RGMII`` and ``MIIM`` toward the MAC, ``MDI1000BaseT`` toward the magnetics,
one ``Polarized`` port per LED output for the LED it drives, and one port each
for RESET_N and INT_N toward the host.
The MDIO pull-up is not here: it belongs to the circuit that composes the bus.
The exposed pad's thermal vias (V and EV of Figures 13-3, 13-6 and 13-9) are
left to layout.
"""
from jitx import Circuit, Net
from jitx.common import Polarized, Power
from jitx.net import Port, ShortTrace
from jitx.units import V, kohm, mW, nF, ohm, pct, uF
from jitxlib.parts import AtLeast, AtMost, Capacitor, Resistor
from jitxlib.protocols.ethernet.mdi.mdi1000base_t import MDI1000BaseT
from jitxlib.protocols.ethernet.mii.miim import MIIM
from jitxlib.protocols.ethernet.mii.rgmii import RGMII
from jitxexamples.components.crystals.seiko_epson_TSX_3225 import TSX3225
from jitxexamples.components.ethernet_phys.microchip_KSZ9031RNX import KSZ9031RNX
from jitxexamples.components.fets_p_ch.NTF6P02T3G import NTF6P02T3G
from .straps import DEFAULT_STRAPS, Strap, StrapConfig
# Section 9.0, Figure 9-1, and the text after it: a strap pin is pulled up with
# 10 kOhm to DVDDH or pulled down with 1.0 kOhm to ground. The data sheet gives
# these values for the LED strap pins only; Section 2.0 gives none for the
# RGMII and CLK125_NDO strap pins, so the circuit uses the same pair on all nine.
STRAP_PULL_UP = 10 * kohm
STRAP_PULL_DOWN = 1.0 * kohm
# Section 9.0, Figure 9-1: each LED is fed from DVDDH through 220 Ohm.
LED_SERIES = 220 * ohm
[docs]
class KSZ9031RNXCircuit(Circuit):
"""KSZ9031RNX with its core LDO, crystal, straps, bias, reset, interrupt
pull-up and decoupling.
Args:
straps: the configuration the strap resistors latch at power-up.
"""
vdd_3v3 = Power()
"""3.3 V supply for AVDDH and DVDDH (I/O at 3.3 V), and the source of the
1.2 V core."""
rgmii = RGMII()
"""RGMII toward the MAC. ``TX`` carries MAC to PHY, ``RX`` PHY to MAC."""
miim = MIIM()
"""Management interface. MDIO needs a pull-up in the composing circuit."""
mdi = MDI1000BaseT()
"""The four line pairs, A to D, toward the magnetics."""
led1 = Polarized()
"""LED driven by LED1 (pin 17): wire it anode to ``a``, cathode to ``c``."""
led2 = Polarized()
"""LED driven by LED2 (pin 15): wire it anode to ``a``, cathode to ``c``."""
reset_n = Port()
"""RESET_N (pin 42), active low, toward the host. The circuit holds the
power-on RC network; the host may pull the line low to reset the PHY and
otherwise leaves it undriven, as an open-drain output does."""
int_n = Port()
"""INT_N (pin 38), active low by default, toward the host. The circuit holds
its pull-up."""
def __init__(self, straps: StrapConfig = DEFAULT_STRAPS) -> None:
self.straps = straps
self.u1 = KSZ9031RNX()
u1 = self.u1
self.GND = Net([self.vdd_3v3.Vn, u1.VSS, u1.P_GND], name="GND")
self.V3V3 = Net([self.vdd_3v3.Vp, *u1.AVDDH, *u1.DVDDH], name="V3V3")
self.V1V2 = Net([*u1.AVDDL, u1.AVDDL_PLL, *u1.DVDDL], name="V1V2")
# Each net is stored in one field named for it, the name it exports
# when no parent circuit joins it under another name.
self.TXD = [m + p for m, p in zip(self.rgmii.TX.data, u1.TXD, strict=True)]
self.GTX_CLK = self.rgmii.TX.clk + u1.GTX_CLK
self.TX_EN = self.rgmii.TX.ctl + u1.TX_EN
self.RXD = [m + p for m, p in zip(self.rgmii.RX.data, u1.RXD, strict=True)]
self.RX_CLK = self.rgmii.RX.clk + u1.RX_CLK
self.RX_DV = self.rgmii.RX.ctl + u1.RX_DV
self.MDC = self.miim.mdc + u1.MDC
self.MDIO = self.miim.mdio + u1.MDIO
self.CLK125_NDO = Net([u1.CLK125_NDO])
line_pins = (
(u1.TXRXP_A, u1.TXRXM_A),
(u1.TXRXP_B, u1.TXRXM_B),
(u1.TXRXP_C, u1.TXRXM_C),
(u1.TXRXP_D, u1.TXRXM_D),
)
self.TXRX_P = [
pair.p + p for pair, (p, _) in zip(self.mdi.TP, line_pins, strict=True)
]
self.TXRX_N = [
pair.n + n for pair, (_, n) in zip(self.mdi.TP, line_pins, strict=True)
]
self._add_core_ldo()
self._add_decoupling()
self._add_crystal()
self._add_leds()
self._add_straps()
self._add_bias_and_reset()
self._add_interrupt()
def _add_core_ldo(self) -> None:
"""The 1.2 V core from 3.3 V through the on-chip LDO controller.
Table 2-1, pin 43: LDO_O drives the gate of a P-channel MOSFET whose
source is on 3.3 V and whose drain makes the 1.2 V core. Section 11.0
asks for a P-channel part rated 500 mA continuous whose VGS at 500 mA,
from a 3.3 V source, is -1.2 to -1.5 V, in saturation rather than near
threshold. ANLAN206 names the part on Microchip's evaluation board, the
FDT434P. When this part was chosen, Digi-Key listed the FDT434P as
obsolete and named the onsemi NTF6P02T3G as its substitute; this
circuit uses the NTF6P02T3G.
The NTF6P02T3G is rated -20 V and -10 A continuous at 25 C. Its
threshold at 250 uA is -0.7 V typical, -0.4 V and -1.0 V at its limits,
and its magnitude falls 2.6 mV per C as the part heats. Here VDS is
about -2.1 V (3.3 V - 1.2 V). At that VDS the typical on-region curve
(Figure 1, 25 C) carries 500 mA at about -1.18 V, read from the plot:
a typical part sits at the edge of Section 11.0's window. The data
sheet gives no curve for other parts. Moving the typical curve by the
threshold's spread and its temperature coefficient gives estimates of
about -1.48 V at 25 C and -1.55 V at 0 C for a part at the -1.0 V
limit, and about -0.88 V at 25 C and -0.62 V at a 125 C junction for
a part at the -0.4 V limit. So the part does not meet Section 11.0's window
across its spread: a part near the -1.0 V limit lands inside it at
25 C, a typical part at its edge, and a part near the -0.4 V limit
outside it. In every case VDS is beyond VGS - VGS(th), so the part
runs in saturation.
Table 6-11: LDO_O swings 0.85 to 2.8 V with AVDDH at 3.3 V, a VGS of
-2.45 to -0.5 V. At 500 mA the estimated VGS stays inside that range:
the largest, about -1.55 V, is about 0.9 V inside the -2.45 V limit,
and the smallest, about -0.62 V, about 0.12 V inside the -0.5 V end.
That covers 500 mA only; light load is below. Table 6-11 gives the
range at AVDDH 3.3 V only. At 3.465 V (3.3 V + 5%) the same gate
voltages give a VGS 0.165 V stronger, so if LDO_O still tops out at
2.8 V its weakest drive is about -0.67 V, and the margins at the weak
end shrink: the -0.62 V estimate would fall outside the range.
ANLAN206 asks for about 250 mA at 1.9 V VDS with a VGS below 2.5 V.
The typical curve gives 250 mA at about -1.10 V; by the same estimate
a part at the -1.0 V limit needs about -1.40 V at 25 C and -1.47 V at
0 C (read at -2.1 V VDS; the part is in saturation at either VDS).
ANLAN206's Figure 6 shows the FDT434P's working point at a -VGS below
1.5 V, which these estimates also stay under, by about 0.03 V at 0 C.
ANLAN206 also asks for an input capacitance below 2 nF; Ciss is
1200 pF at most (900 pF typical).
Light load is not verified, and the risk is an estimate. Table 3-12's
lightest core loads are 1.0 mA in software power-down and 7.1 mA at
10BASE-T link-up. At LDO_O's weakest drive, a 2.8 V gate (Table 6-11),
VGS is -0.5 V, and about -0.67 V from a 3.465 V AVDDH. A part at the
-0.4 V threshold limit already passes 250 uA at -0.4 V. Its threshold
magnitude falls as the part heats, by 2.6 mV per C, which makes
turn-off harder, not easier. So at -0.5 V it may not turn off far
enough to hold the drain to those loads, and the core could rise
above 1.2 V. The data sheet gives no curve at these currents, and the
KSZ9031RNX data sheet does not say whether LDO_O can pull the gate
above 2.8 V. The FDT434P on Microchip's evaluation board has the same
-0.4 V minimum threshold (onsemi FDT434P data sheet).
ANLAN206 Figure 8 gives the rest: 100 kOhm from gate to source, which
it adds to avoid a large inrush current from 3.3 V as the LDO powers up;
10 uF on the source; 10 uF, 1 uF and 100 nF on the drain; and a short
trace from the drain to AVDDL pin 4, which the controller senses as its
feedback (the 10 uF bulk capacitor sits on AVDDL pin 9). The drain
capacitors and the sense trace land on the drain tab, pin 4 of the
MOSFET, and the source capacitor on its source pin. Figure 8 feeds
DVDDL and AVDDL_PLL through ferrite beads. The example catalogue holds
no ferrite bead, so this circuit puts all three on one 1.2 V net.
AVDDL takes no bead in the figure: one there would degrade the LDO's
load regulation.
The MOSFET dissipates the core's whole drop. At Table 3-12's highest
core current, 221 mA at 1000BASE-T full duplex, that is
(3.3 V - 1.2 V) x 221 mA = 0.46 W. ANLAN206 sizes for typical current
plus 20% at the supply extremes, 0.62 W. The NTF6P02T3G's steady-state
junction-to-ambient resistance is 71.4 C/W on FR4 with a 1 inch drain
pad, 1.127 square inches of copper (Note 2). At 0.46 W that is a 33 C
rise: a 103 C junction at 70 C ambient, the top of the commercial
range, and 118 C at 85 C, the top of the industrial range. At 0.62 W
it is a 44 C rise: 114 C at 70 C and 129 C at 85 C. ANLAN206 asks for
at most 90 C/W in the commercial range and 65 C/W in the industrial
range, limits that put the junction near 125 C at 0.62 W. The
NTF6P02T3G meets the commercial limit and not the industrial one: at
0.62 W and 85 C its junction is about 4 C above 125 C, and 21 C under
its own 150 C maximum. Its 160 C/W rating on the minimum recommended
pad (Note 3) still assumes 0.412 square inches of copper, about 16
times the 16.6 square millimetres of this footprint's own pads, so the
layout must give the drain tab the Note 2 copper.
The core comes from AVDDH, so it cannot lead 3.3 V at power-up, which
meets Figure 7-5, Note 1 by construction. Figure 7-5, Note 3
recommends the reverse at power-down, the 1.2 V core down before the
3.3 V supplies. This circuit does not meet Note 3: the core is made
from 3.3 V, so it stays up until 3.3 V has fallen. That is how the
LDO controller scheme of Section 11.0 and ANLAN206 behaves.
"""
u1 = self.u1
self.q_ldo = NTF6P02T3G()
q = self.q_ldo
# The tab, the drain pad that carries the rated copper.
drain_tab = q.mosfet.D[1]
self.r_ldo_gate = Resistor(resistance=100 * kohm)
self.LDO_GATE = u1.LDO_O + q.g + self.r_ldo_gate.p1
self.V3V3 += Net([q.s, self.r_ldo_gate.p2])
self.V1V2 += q.d
self.c_ldo_in = _bulk_cap()
self.c_ldo_out = [
_bulk_cap(),
Capacitor(
capacitance=1 * uF,
rated_voltage_dc=AtLeast(10 * V),
temperature_coefficient_code="X5R",
),
_hf_cap(),
]
self.ldo_st = [
ShortTrace(self.c_ldo_in.p1, q.mosfet.S),
*(ShortTrace(c.p1, drain_tab) for c in self.c_ldo_out),
ShortTrace(drain_tab, u1.AVDDL[0]),
]
self.GND += Net([self.c_ldo_in.p2, *(c.p2 for c in self.c_ldo_out)])
def _add_decoupling(self) -> None:
"""Supply decoupling.
The data sheet gives no decoupling recommendation; ANLAN206 Figure 8
places one 100 nF per 1.2 V supply pin (9 pins) and 10 uF on AVDDL
pin 9, DVDDL and AVDDL_PLL. The 3.3 V pins follow the same pattern as
a decision: one 100 nF X7R per supply pin (5 pins, so 14 across both
rails), and one 10 uF X5R bulk capacitor beside a DVDDH pin. Each is
rated at least 10 V, derated for DC bias on rails of 3.3 V and below.
"""
u1 = self.u1
pins_3v3 = [*u1.AVDDH, *u1.DVDDH]
pins_1v2 = [*u1.AVDDL, u1.AVDDL_PLL, *u1.DVDDL]
self.c_3v3 = [_hf_cap() for _ in pins_3v3]
self.c_1v2 = [_hf_cap() for _ in pins_1v2]
self.c_3v3_st = [
ShortTrace(c.p1, pin) for c, pin in zip(self.c_3v3, pins_3v3, strict=True)
]
self.c_1v2_st = [
ShortTrace(c.p1, pin) for c, pin in zip(self.c_1v2, pins_1v2, strict=True)
]
bulk_pins = [u1.DVDDH[0], u1.AVDDL[1], u1.DVDDL[0], u1.AVDDL_PLL]
self.c_bulk = [_bulk_cap() for _ in bulk_pins]
self.bulk_st = [
ShortTrace(c.p1, pin) for c, pin in zip(self.c_bulk, bulk_pins, strict=True)
]
self.GND += Net(
[
*(c.p2 for c in self.c_3v3),
*(c.p2 for c in self.c_1v2),
*(c.p2 for c in self.c_bulk),
]
)
def _add_crystal(self) -> None:
"""25 MHz crystal on XI and XO.
Table 10-1 asks for 25 MHz, at most +/-50 ppm, about 40 Ohm series
resistance. X1E0000210132 is +/-10 ppm at +25 C and +/-10 ppm over
-20 to 75 C, 40 Ohm, with a 9 pF load: ``add_load_caps`` sizes the two
C0G load capacitors and joins each to its crystal pin through a
``ShortTrace``. Figure 10-1 shows 22 pF without naming the crystal's
load; the PHY does not constrain the load capacitance (Table 10-1).
"""
self.y1 = TSX3225(
frequency=25e6,
load_capacitance=9e-12,
frequency_tolerance=10e-6,
frequency_stability=10e-6,
temperature_range=(-20, 75),
)
self.XI = self.y1.OSC1 + self.u1.XI
self.XO = self.y1.OSC2 + self.u1.XO
self.GND += Net([self.y1.GND0, self.y1.GND1])
# The capacitors belong to this circuit already; storing them on self
# as well would register them twice.
self.y1.add_load_caps(self.GND)
def _add_straps(self) -> None:
"""One strap resistor per Table 2-2 strap pin, on the pin's own net.
Each resistor joins the net that already carries its pin, so the net
keeps its signal name.
A pull-down sits on a pin that drives once the PHY is out of reset (an
RGMII receive output, an LED output or CLK125_NDO), so it dissipates
up to (3.3 V)^2 / 1.0 kOhm = 10.9 mW while the pin is high. A pull-up
dissipates a tenth of that. Neither value needs a tolerance: each only
sets a logic level.
"""
nets = self._strap_nets()
self.r_strap = [_strap_resistor(self.straps.level(s)) for s in Strap]
for strap, r in zip(Strap, self.r_strap, strict=True):
nets[strap] += r.p1
if self.straps.level(strap):
self.V3V3 += r.p2
else:
self.GND += r.p2
def _add_leds(self) -> None:
"""LED drive on the LED1/PHYAD0 and LED2/PHYAD1 strap pins.
Section 9.0, Figure 9-1 gives one reference circuit per strap level at
3.3 V DVDDH. In both the LED's anode takes DVDDH through 220 Ohm and
its cathode sits on the LED pin, which sinks current to light it
(Table 3-9: L is on). The levels differ only in the strap resistor on
the pin, 10 kOhm to DVDDH or 1.0 kOhm to ground, which ``_add_straps``
places from the configuration. With a pull-down, the LED and 220 Ohm
also feed the pin while reset holds it undriven, and the strap reads
low only under the 1.3 V input-low maximum of Table 6-3. Figure 9-1 is
Microchip's circuit for this case; the jack's drawing gives the LED's
forward voltage only at 20 mA, so the pin voltage at the 1 mA or so
that flows here is not computed.
"""
u1 = self.u1
self.r_led = [Resistor(resistance=LED_SERIES) for _ in range(2)]
self.LED1_A = self.led1.a + self.r_led[0].p1
self.LED2_A = self.led2.a + self.r_led[1].p1
self.LED1 = self.led1.c + u1.LED1
self.LED2 = self.led2.c + u1.LED2
for r in self.r_led:
self.V3V3 += r.p2
def _add_bias_and_reset(self) -> None:
"""ISET bias resistor and power-on reset.
Table 2-1, pin 48: ISET takes a 12.1 kOhm 1% resistor to ground.
Section 8.0, Figure 8-1: RESET_N gets 10 kOhm to DVDDH and 10 uF to
ground. Table 7-4 asks for supplies stable at least 10 ms before reset
de-asserts (tSR). RESET_N's internal pull-up, 13 to 31 kOhm at 3.3 V
(Table 6-6), sits in parallel with the 10 kOhm, giving 5.7 to
7.6 kOhm. The 10 uF part the example catalogue resolves,
CC0402MRX5R6BB106 (X5R, 0402, +/-20%), keeps about 42% of its value at
3.3 V in its maker's simulated DC-bias curve, at least 3.3 uF. RESET_N
reaches the 2.0 V input-high level (Table 6-3) after at least 17 ms,
using that 3.3 V capacitance; the capacitor loses less below 2 V, which
makes it longer, about 35 to 70 ms on the maker's curve. In the
composed board, 3.3 V settles at most 2.9 ms after it starts to rise,
the maximum of its regulator's soft start, and the core follows it
through the LDO controller, for which the data sheet gives no start-up
time. Reset de-asserts at least 14 ms after 3.3 V settles, so tSR holds
while the core settles within 4 ms of it, and neither part takes a
tolerance filter. The figure's 1N4148 diode is
left off, because the reference library has no small-signal diode.
Without it the capacitor discharges only through the resistors when
DVDDH falls. A supply cycle that keeps Table 7-4's 150 ms off-to-on
time (tPC) still resets the PHY; a shorter dip may not pull RESET_N low
again, which the diode's fast discharge would. The reset capacitor is RC timing, not supply
decoupling.
The host joins RESET_N through ``reset_n``. Figure 8-2 puts a 1N4148
in series from the host's reset output, so that output can only pull
the line low and the RC network still sets power-on timing. With no
small-signal diode in the library the host connects directly, so it
drives the line open-drain: low to reset, released otherwise. Figure
7-5, Note 2: after reset de-asserts, the host waits at least 100 us
before MIIM access.
"""
u1 = self.u1
self.r_iset = Resistor(resistance=12.1 * kohm, tolerance=AtMost(1 * pct))
self.ISET = u1.ISET + self.r_iset.p1
self.GND += self.r_iset.p2
self.r_reset = Resistor(resistance=10 * kohm)
self.c_reset = Capacitor(
capacitance=10 * uF,
rated_voltage_dc=AtLeast(10 * V),
temperature_coefficient_code="X5R",
)
self.RESET_N = self.reset_n + u1.RESET_N + self.r_reset.p1 + self.c_reset.p1
self.V3V3 += self.r_reset.p2
self.GND += self.c_reset.p2
def _add_interrupt(self) -> None:
"""INT_N pull-up.
Table 2-1, pin 38: for its interrupt function, active low by default,
INT_N takes a 1.0 to 4.7 kOhm pull-up to DVDDH. The circuit uses
4.7 kOhm and brings the pin out to the host as ``int_n``.
"""
self.r_int = Resistor(resistance=4.7 * kohm)
self.INT_N = self.int_n + self.u1.INT_N + self.r_int.p1
self.V3V3 += self.r_int.p2
[docs]
def strap_pins(self) -> dict[Strap, Port]:
"""The KSZ9031RNX pin carrying each strap, per Table 2-2."""
u1 = self.u1
return {
Strap.PHYAD2: u1.RX_CLK,
Strap.PHYAD1: u1.LED2,
Strap.PHYAD0: u1.LED1,
Strap.MODE3: u1.RXD[3],
Strap.MODE2: u1.RXD[2],
Strap.MODE1: u1.RXD[1],
Strap.MODE0: u1.RXD[0],
Strap.CLK125_EN: u1.RX_DV,
Strap.LED_MODE: u1.CLK125_NDO,
}
def _strap_nets(self) -> dict[Strap, Net[Port]]:
"""The stored net carrying each strap pin of ``strap_pins``."""
nets = [
self.RX_CLK,
self.LED1,
self.LED2,
*self.RXD,
self.RX_DV,
self.CLK125_NDO,
]
return {
strap: next(n for n in nets if pin in list(n))
for strap, pin in self.strap_pins().items()
}
[docs]
def strap_resistor(self, strap: Strap) -> Resistor:
"""The resistor setting ``strap``."""
return self.r_strap[list(Strap).index(strap)]
def _hf_cap() -> Capacitor:
return Capacitor(
capacitance=100 * nF,
rated_voltage_dc=AtLeast(10 * V),
temperature_coefficient_code="X7R",
)
def _bulk_cap() -> Capacitor:
return Capacitor(
capacitance=10 * uF,
rated_voltage_dc=AtLeast(10 * V),
temperature_coefficient_code="X5R",
)
def _strap_resistor(pull_up: bool) -> Resistor:
if pull_up:
return Resistor(resistance=STRAP_PULL_UP)
return Resistor(resistance=STRAP_PULL_DOWN, max_power=AtLeast(11 * mW))