Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions edg/abstract_parts/AbstractFets.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ class Fet(KiCadImportableBlock, DiscreteSemiconductor, HasStandardFootprint):
"""Base class for untyped MOSFETs
Drain voltage, drain current, and gate voltages are positive (absolute).

The gate voltage is only checked against maximum ratings.
Optionally, the gate threshold voltage can also be specified.

The actual gate drive voltage is specified as (threshold voltage, gate drive voltage), where the top end of that
is either the voltage at Rds,on or the specified driving voltage level.

MOSFET equations
- https://inst.eecs.berkeley.edu/~ee105/fa05/handouts/discussions/Discussion5.pdf (cutoff/linear/saturation regions)

Expand Down Expand Up @@ -81,7 +87,8 @@ def PFet(*args, **kwargs) -> 'Fet':

@init_in_parent
def __init__(self, drain_voltage: RangeLike, drain_current: RangeLike, *,
gate_voltage: RangeLike = (0, 0), rds_on: RangeLike = Range.all(),
gate_voltage: RangeLike = (0, 0), gate_threshold_voltage: RangeLike = Range.all(),
rds_on: RangeLike = Range.all(),
gate_charge: RangeLike = Range.all(), power: RangeLike = Range.exact(0),
channel: StringLike = StringExpr()) -> None:
super().__init__()
Expand All @@ -93,6 +100,7 @@ def __init__(self, drain_voltage: RangeLike, drain_current: RangeLike, *,
self.drain_voltage = self.ArgParameter(drain_voltage)
self.drain_current = self.ArgParameter(drain_current)
self.gate_voltage = self.ArgParameter(gate_voltage)
self.gate_threshold_voltage = self.ArgParameter(gate_threshold_voltage)
self.rds_on = self.ArgParameter(rds_on)
self.gate_charge = self.ArgParameter(gate_charge)
self.power = self.ArgParameter(power)
Expand Down Expand Up @@ -142,15 +150,16 @@ class TableFet(PartsTableSelector, BaseTableFet):
@init_in_parent
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.generator_param(self.drain_voltage, self.drain_current, self.gate_voltage, self.rds_on, self.gate_charge,
self.power, self.channel)
self.generator_param(self.drain_voltage, self.drain_current, self.gate_voltage, self.gate_threshold_voltage,
self.rds_on, self.gate_charge, self.power, self.channel)

def _row_filter(self, row: PartsTableRow) -> bool:
return super()._row_filter(row) and \
row[self.CHANNEL] == self.get(self.channel) and \
self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) and \
self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) and \
self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) and \
(row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) and \
row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) and \
row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) and \
self.get(self.power).fuzzy_in(row[self.POWER_RATING])
Expand Down Expand Up @@ -184,7 +193,7 @@ def PFet(*args, **kwargs):


@init_in_parent
def __init__(self, frequency: RangeLike, drive_current: RangeLike, **kwargs) -> None:
def __init__(self, *, frequency: RangeLike = 0*Hertz(tol=0), drive_current: RangeLike = Range.all(), **kwargs) -> None:
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from positional to keyword-only arguments for frequency and drive_current may break existing code that calls this constructor with positional arguments. This is a breaking API change that should be documented or handled with backward compatibility.

Suggested change
def __init__(self, *, frequency: RangeLike = 0*Hertz(tol=0), drive_current: RangeLike = Range.all(), **kwargs) -> None:
def __init__(self, frequency: RangeLike = 0*Hertz(tol=0), drive_current: RangeLike = Range.all(), **kwargs) -> None:

Copilot uses AI. Check for mistakes.
super().__init__(**kwargs)

self.frequency = self.ArgParameter(frequency)
Expand All @@ -200,8 +209,9 @@ class TableSwitchFet(PartsTableSelector, SwitchFet, BaseTableFet):
@init_in_parent
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.generator_param(self.frequency, self.drain_voltage, self.drain_current, self.gate_voltage, self.rds_on,
self.gate_charge, self.power, self.channel, self.drive_current)
self.generator_param(self.frequency, self.drain_voltage, self.drain_current,
self.gate_voltage, self.gate_threshold_voltage,
self.rds_on, self.gate_charge, self.power, self.channel, self.drive_current)

self.actual_static_power = self.Parameter(RangeExpr())
self.actual_switching_power = self.Parameter(RangeExpr())
Expand All @@ -213,6 +223,7 @@ def _row_filter(self, row: PartsTableRow) -> bool: # here this is just a pre-fi
self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) and \
self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) and \
self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) and \
(row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) and \
row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) and \
row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) and \
self.get(self.power).fuzzy_in(row[self.POWER_RATING])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ..abstract_parts import *
from ..electronics_model import *
from .AbstractDiodes import Diode


class CustomDiode(Diode, FootprintBlock, GeneratorBlock):
Expand Down
5 changes: 3 additions & 2 deletions edg/parts/CustomFet.py → edg/abstract_parts/CustomFet.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from ..abstract_parts import *
from ..electronics_model import *
from .AbstractFets import SwitchFet


class CustomFet(Fet, FootprintBlock, GeneratorBlock):
class CustomFet(SwitchFet, FootprintBlock, GeneratorBlock):
@init_in_parent
def __init__(self, *args, footprint_spec: StringLike = "",
manufacturer_spec: StringLike = "", part_spec: StringLike = "", **kwargs):
Expand Down
12 changes: 12 additions & 0 deletions edg/abstract_parts/DummyDevices.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL,
self.current_limits = self.Parameter(RangeExpr(self.pwr.link().current_limits))


class DummyDigitalSource(DummyDevice):
@init_in_parent
def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO,
current_limits: RangeLike = RangeExpr.ALL) -> None:
super().__init__()

self.io = self.Port(DigitalSource(
voltage_out=voltage_out,
current_limits=current_limits
), [InOut])


class DummyDigitalSink(DummyDevice):
@init_in_parent
def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from typing import NamedTuple, Dict, Optional
import math

from ..abstract_parts import *
from ..electronics_model import *
from .AbstractCapacitor import Capacitor, DummyCapacitorFootprint
from .SelectorArea import SelectorArea
from .ESeriesUtil import ESeriesUtil


class GenericMlcc(Capacitor, SelectorArea, FootprintBlock, GeneratorBlock):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import List, Tuple

from ..abstract_parts import *
from ..electronics_model import *
from .AbstractResistor import Resistor
from .ESeriesUtil import ESeriesUtil
from .SelectorArea import SelectorArea


@non_library
Expand Down
110 changes: 109 additions & 1 deletion edg/abstract_parts/PowerCircuits.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from ..electronics_model import *
from .Resettable import Resettable
from .AbstractResistor import Resistor
from .AbstractFets import SwitchFet
from .AbstractFets import SwitchFet, Fet
from .AbstractCapacitor import Capacitor
from .GateDrivers import HalfBridgeDriver, HalfBridgeDriverIndependent, HalfBridgeDriverPwm
from .ResistiveDivider import VoltageDivider, ResistiveDivider
from .Categories import PowerConditioner


Expand Down Expand Up @@ -118,3 +120,109 @@ def generate(self):
self.connect(self.pwm_ctl, self.driver.with_mixin(HalfBridgeDriverPwm()).pwm_in)
if self.get(self.reset.is_connected()):
self.connect(self.reset, self.driver.with_mixin(Resettable()).reset)


class RampLimiter(KiCadSchematicBlock):
"""PMOS-based ramp limiter that roughly targets a constant-dV/dt ramp.
The cgd should be specified to swamp (10x+) the parasitic Cgd of the FET to get more controlled parameters.
The target ramp rate is in volts/second, and for a capacitive load this can be calculated from a target current with
I = C * dV/dt => dV/dt = I / C
The actual ramp rate will vary substantially, the values calculated are based on many assertions.

A target Vgs can also be specified, this is the final Vgs of the FET after the ramp completes.
The FET will be constrained to have a Vgs,th below the minimum of this range and a Vgs,max above the maximum.

A capacitive divider with Cgs will be generated so the target initial Vgs at less than half the FET Vgs,th
(targeting half Vgs,th at Vin,max).

TODO: allow control to be optional, eliminating the NMOS with a short

HOW THIS WORKS:
When the input voltage rises, the capacitive divider of Cgs, Cgd brings the gate to a subthreshold voltage.
The gate voltage charges via the divider until it gets to the threshold voltage.
At around the threshold voltage, the FET begins to turn on, with current flowing into (and charging) the output.
As the output rises, Cgd causes the gate to be pulled up with the output, keeping Vgs roughly constant.
(this also keeps the current roughly constant, mostly regardless of transconductance)
During this stage, if we assume Vgs is constant, then Cgs is constant and can be disregarded.
For the output to rise, Vgd must rise, which means Cgd must charge, and the current must go through the divider.
Assuming a constant Vgs (and absolute gate voltage), the current into the divider is constant,
and this is how the voltage ramp rate is controlled.
Once the output gets close to the input voltage, Cgd stops charging and Vgs rises, turning the FET fully on.

Note that Vgs,th is an approximate parameter and the ramp current is likely larger than the Vgs,th current.
Vgs also may rise during the ramp, meaning some current goes into charging Cgs.

References: https://www.ti.com/lit/an/slva156/slva156.pdf, https://www.ti.com/lit/an/slyt096/slyt096.pdf,
https://youtu.be/bOka13RtOXM

Additional more complex circuits
https://electronics.stackexchange.com/questions/294061/p-channel-mosfet-inrush-current-limiting
"""
@init_in_parent
def __init__(self, *, cgd: RangeLike = 10*nFarad(tol=0.5), target_ramp: RangeLike = 1000*Volt(tol=0.25),
target_vgs: RangeLike = (4, 10)*Volt, max_rds: FloatLike = 1*Ohm,
_cdiv_vgs_factor: RangeLike = (0.05, 0.75)):
super().__init__()

self.gnd = self.Port(Ground.empty(), [Common])
self.pwr_in = self.Port(VoltageSink.empty(), [Input])
self.pwr_out = self.Port(VoltageSource.empty(), [Output])
self.control = self.Port(DigitalSink.empty())

self.cgd = self.ArgParameter(cgd)
self.target_ramp = self.ArgParameter(target_ramp)
self.target_vgs = self.ArgParameter(target_vgs)
self.max_rds = self.ArgParameter(max_rds)
self._cdiv_vgs_factor = self.ArgParameter(_cdiv_vgs_factor)

def contents(self):
super().contents()

pwr_voltage = self.pwr_in.link().voltage
self.drv = self.Block(SwitchFet.PFet(
drain_voltage=pwr_voltage,
drain_current=self.pwr_out.link().current_drawn,
gate_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.upper()),
gate_threshold_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.lower()),
rds_on=(0, self.max_rds)
))

self.cap_gd = self.Block(Capacitor(
capacitance=self.cgd,
voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage)
))
# treat Cgs and Cgd as a capacitive divider with Cgs on the bottom
self.cap_gs = self.Block(Capacitor(
capacitance=(
(1/(self.drv.actual_gate_drive.lower()*self._cdiv_vgs_factor)).shrink_multiply(self.pwr_in.link().voltage) - 1
).shrink_multiply(
self.cap_gd.actual_capacitance
),
voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage)
))
Comment on lines +196 to +202
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This complex calculation for capacitance should be extracted into a helper method with clear documentation explaining the formula derivation. The current inline calculation is difficult to understand and maintain.

Suggested change
capacitance=(
(1/(self.drv.actual_gate_drive.lower()*self._cdiv_vgs_factor)).shrink_multiply(self.pwr_in.link().voltage) - 1
).shrink_multiply(
self.cap_gd.actual_capacitance
),
voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage)
))
capacitance=self._calculate_gs_capacitance(
self.drv.actual_gate_drive.lower(),
self._cdiv_vgs_factor,
self.pwr_in.link().voltage,
self.cap_gd.actual_capacitance
),
voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage)
))
def _calculate_gs_capacitance(self, actual_gate_drive_lower, cdiv_vgs_factor, pwr_in_voltage, cap_gd_actual_capacitance):
"""
Calculates the gate-source capacitance (Cgs) for the capacitive divider.
Formula derivation:
Treats Cgs and Cgd as a capacitive divider with Cgs on the bottom.
The calculation is:
Cgs = [ (1 / (actual_gate_drive_lower * cdiv_vgs_factor)) * pwr_in_voltage - 1 ] * cap_gd_actual_capacitance
where:
- actual_gate_drive_lower: The lower bound of the gate drive voltage.
- cdiv_vgs_factor: A scaling factor for the divider.
- pwr_in_voltage: The input power rail voltage.
- cap_gd_actual_capacitance: The actual gate-drain capacitance.
Returns the calculated Cgs value.
"""
divider = (1 / (actual_gate_drive_lower * cdiv_vgs_factor))
result = (divider.shrink_multiply(pwr_in_voltage) - 1).shrink_multiply(cap_gd_actual_capacitance)
return result

Copilot uses AI. Check for mistakes.
# dV/dt over a capacitor is I / C => I = Cgd * dV/dt
# then calculate to get the target I: Vgs,th = I * Reff => Reff = Vgs,th / I = Vgs,th / (Cgd * dV/dt)
# we assume Vgs,th is exact, and only contributing sources come from elsewhere
self.div = self.Block(ResistiveDivider(ratio=self.target_vgs.shrink_multiply(1/self.pwr_in.link().voltage),
impedance=(1 / self.target_ramp).shrink_multiply(self.drv.actual_gate_drive.lower() / (self.cap_gd.actual_capacitance))
))
Comment on lines +206 to +208
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The impedance calculation is complex and spans multiple lines. Consider extracting this calculation into a helper method with documentation explaining the relationship between target_ramp, gate_drive, and capacitance.

Copilot uses AI. Check for mistakes.
div_current_draw = (self.pwr_in.link().voltage/self.div.actual_impedance).hull(0)
self.ctl_fet = self.Block(SwitchFet.NFet(
drain_voltage=pwr_voltage,
drain_current=div_current_draw,
gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper())
))

self.import_kicad(
self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"),
conversions={
'pwr_in': VoltageSink(
current_draw=self.pwr_out.link().current_drawn + div_current_draw
),
'pwr_out': VoltageSource(
voltage_out=self.pwr_in.link().voltage
),
'control': DigitalSink(),
'gnd': Ground(),
})

18 changes: 15 additions & 3 deletions edg/abstract_parts/ResistiveDivider.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from math import log10, ceil
from typing import List, Tuple
from typing import List, Tuple, Mapping

from ..electronics_model import *
from . import Analog, Resistor
Expand Down Expand Up @@ -55,8 +55,12 @@ def intersects(self, spec: 'DividerValues') -> bool:
self.parallel_impedance.intersects(spec.parallel_impedance)


class ResistiveDivider(InternalSubcircuit, GeneratorBlock):
class ResistiveDivider(InternalSubcircuit, KiCadImportableBlock, GeneratorBlock):
"""Abstract, untyped (Passive) resistive divider, that takes in a ratio and parallel impedance spec."""
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
assert symbol_name == 'Device:VoltageDivider'
return {'1': self.top, '2': self.center, '3': self.bottom}

@classmethod
def divider_ratio(cls, rtop: RangeExpr, rbot: RangeExpr) -> RangeExpr:
"""Calculates the output voltage of a resistive divider given the input voltages and resistances."""
Expand Down Expand Up @@ -131,12 +135,16 @@ def generate(self) -> None:


@non_library
class BaseVoltageDivider(Block):
class BaseVoltageDivider(KiCadImportableBlock):
"""Base class that defines a resistive divider that takes in a voltage source and ground, and outputs
an analog constant-voltage signal.
The actual output voltage is defined as a ratio of the input voltage, and the divider is specified by
ratio and impedance.
Subclasses should define the ratio and impedance spec."""
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
assert symbol_name == 'Device:VoltageDivider'
return {'1': self.input, '2': self.output, '3': self.gnd}

@init_in_parent
def __init__(self, impedance: RangeLike) -> None:
super().__init__()
Expand Down Expand Up @@ -218,6 +226,10 @@ def contents(self) -> None:

class SignalDivider(Analog, Block):
"""Specialization of ResistiveDivider for Analog signals"""
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
assert symbol_name == 'Device:VoltageDivider'
return {'1': self.input, '2': self.output, '3': self.gnd}

@init_in_parent
def __init__(self, ratio: RangeLike, impedance: RangeLike) -> None:
super().__init__()
Expand Down
11 changes: 8 additions & 3 deletions edg/abstract_parts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
from .AbstractPowerConverters import BuckConverter, DiscreteBuckConverter, BoostConverter, DiscreteBoostConverter
from .AbstractPowerConverters import BuckConverterPowerPath, BoostConverterPowerPath, BuckBoostConverterPowerPath
from .PowerCircuits import HalfBridge, FetHalfBridge, HalfBridgeIndependent, HalfBridgePwm, FetHalfBridgeIndependent,\
FetHalfBridgePwmReset
FetHalfBridgePwmReset, RampLimiter
from .AbstractLedDriver import LedDriver, LedDriverPwm, LedDriverSwitchingConverter
from .AbstractFuse import Fuse, SeriesPowerFuse, PptcFuse, FuseStandardFootprint, TableFuse, SeriesPowerPptcFuse
from .AbstractCrystal import Crystal, TableCrystal, OscillatorReference, CeramicResonator
Expand Down Expand Up @@ -106,8 +106,13 @@
from .PinMappable import PinResource, PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource
from .VariantPinRemapper import VariantPinRemapper

from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSink, \
DummyAnalogSource, DummyAnalogSink
from .CustomDiode import CustomDiode
from .CustomFet import CustomFet
from .GenericResistor import ESeriesResistor, GenericChipResistor, GenericAxialResistor, GenericAxialVerticalResistor
from .GenericCapacitor import GenericMlcc

from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSource, \
DummyDigitalSink, DummyAnalogSource, DummyAnalogSink
from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltageCurrentLimit, ForcedVoltage, ForcedVoltageCurrent, \
ForcedAnalogVoltage, ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw
from .MergedBlocks import MergedVoltageSource, MergedDigitalSource, MergedAnalogSource, MergedSpiController
Expand Down
Loading