#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MosfetKind {
#[cfg_attr(feature = "serde", serde(rename = "nmos"))]
Nmos,
#[cfg_attr(feature = "serde", serde(rename = "pmos"))]
Pmos,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MosfetRegion {
Cutoff,
Triode,
Saturation,
}
impl MosfetRegion {
pub fn as_str(&self) -> &'static str {
match self {
MosfetRegion::Cutoff => "cutoff",
MosfetRegion::Triode => "triode",
MosfetRegion::Saturation => "saturation",
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct MosfetParams {
pub kp: f64,
pub vto: f64,
pub lambda: f64,
pub gamma: f64,
pub phi: f64,
}
impl MosfetParams {
pub fn default_nmos() -> Self {
Self {
kp: 2e-4, vto: 0.7, lambda: 0.02, gamma: 0.4, phi: 0.6, }
}
pub fn default_pmos() -> Self {
Self {
kp: 1e-4, vto: -0.7, lambda: 0.02,
gamma: 0.4,
phi: 0.6,
}
}
}
impl Default for MosfetParams {
fn default() -> Self {
Self::default_nmos()
}
}
pub struct MosfetCompanion {
pub id: f64,
pub gm: f64,
pub gds: f64,
pub gmb: f64,
pub region: MosfetRegion,
pub vth: f64,
}
pub fn threshold_voltage(vbs: f64, params: &MosfetParams) -> f64 {
let phi_minus_vbs = params.phi - vbs;
let sqrt_term = if phi_minus_vbs > 0.0 {
phi_minus_vbs.sqrt()
} else {
0.0
};
params.vto + params.gamma * (sqrt_term - params.phi.sqrt())
}
pub fn mosfet_companion(vgs: f64, vds: f64, vbs: f64, params: &MosfetParams) -> MosfetCompanion {
let vth = threshold_voltage(vbs, params);
let vov = vgs - vth;
if vov <= 0.0 {
return MosfetCompanion {
id: 0.0,
gm: 0.0,
gds: 0.0,
gmb: 0.0,
region: MosfetRegion::Cutoff,
vth,
};
}
let phi_minus_vbs = (params.phi - vbs).max(1e-6);
let dvth_dvbs = if params.gamma > 0.0 {
-params.gamma / (2.0 * phi_minus_vbs.sqrt())
} else {
0.0
};
if vds < vov {
let id = params.kp * ((vov * vds) - (vds * vds / 2.0)) * (1.0 + params.lambda * vds);
let gm = params.kp * vds * (1.0 + params.lambda * vds);
let gds = params.kp * (vov - vds) * (1.0 + params.lambda * vds)
+ params.kp * ((vov * vds) - (vds * vds / 2.0)) * params.lambda;
let gmb = -gm * dvth_dvbs;
MosfetCompanion {
id,
gm,
gds,
gmb,
region: MosfetRegion::Triode,
vth,
}
} else {
let id = (params.kp / 2.0) * vov * vov * (1.0 + params.lambda * vds);
let gm = params.kp * vov * (1.0 + params.lambda * vds);
let gds = (params.kp / 2.0) * vov * vov * params.lambda;
let gmb = -gm * dvth_dvbs;
MosfetCompanion {
id,
gm,
gds,
gmb,
region: MosfetRegion::Saturation,
vth,
}
}
}
pub fn detect_region(vgs: f64, vds: f64, vth: f64) -> MosfetRegion {
let vov = vgs - vth;
if vov <= 0.0 {
MosfetRegion::Cutoff
} else if vds < vov {
MosfetRegion::Triode
} else {
MosfetRegion::Saturation
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn nmos_cutoff() {
let params = MosfetParams::default_nmos();
let comp = mosfet_companion(0.0, 5.0, 0.0, ¶ms);
assert_eq!(comp.region, MosfetRegion::Cutoff);
assert_relative_eq!(comp.id, 0.0, epsilon = 1e-15);
assert_relative_eq!(comp.gm, 0.0, epsilon = 1e-15);
}
#[test]
fn nmos_saturation() {
let params = MosfetParams::default_nmos();
let comp = mosfet_companion(3.0, 5.0, 0.0, ¶ms);
assert_eq!(comp.region, MosfetRegion::Saturation);
let expected_id = (2e-4 / 2.0) * 2.3 * 2.3 * (1.0 + 0.02 * 5.0);
assert_relative_eq!(comp.id, expected_id, epsilon = 1e-10);
assert!(comp.gm > 0.0);
assert!(comp.gds > 0.0);
}
#[test]
fn nmos_triode() {
let params = MosfetParams::default_nmos();
let comp = mosfet_companion(3.0, 0.5, 0.0, ¶ms);
assert_eq!(comp.region, MosfetRegion::Triode);
let expected_id = 2e-4 * ((2.3 * 0.5) - (0.5 * 0.5 / 2.0)) * (1.0 + 0.02 * 0.5);
assert_relative_eq!(comp.id, expected_id, epsilon = 1e-10);
}
#[test]
fn body_effect_increases_vth() {
let params = MosfetParams::default_nmos();
let vth_no_body = threshold_voltage(0.0, ¶ms);
let vth_with_body = threshold_voltage(-2.0, ¶ms); assert!(
vth_with_body > vth_no_body,
"Body effect should increase Vth: {} vs {}",
vth_with_body,
vth_no_body
);
}
#[test]
fn body_effect_gmb_nonzero() {
let params = MosfetParams::default_nmos();
let comp = mosfet_companion(3.0, 5.0, -1.0, ¶ms);
assert!(
comp.gmb.abs() > 0.0,
"gmb should be nonzero with body effect"
);
}
#[test]
fn saturation_id_vs_vgs() {
let params = MosfetParams::default_nmos();
let comp1 = mosfet_companion(2.0, 5.0, 0.0, ¶ms);
let comp2 = mosfet_companion(3.0, 5.0, 0.0, ¶ms);
assert!(comp2.id > comp1.id);
}
#[test]
fn channel_length_modulation() {
let params = MosfetParams::default_nmos();
let comp1 = mosfet_companion(3.0, 2.5, 0.0, ¶ms);
let comp2 = mosfet_companion(3.0, 5.0, 0.0, ¶ms);
assert!(comp2.id > comp1.id, "CLM: Id should increase with Vds");
}
#[test]
fn pmos_with_sign_flip() {
let params = MosfetParams {
kp: 1e-4,
vto: 0.7, lambda: 0.02,
gamma: 0.4,
phi: 0.6,
};
let comp = mosfet_companion(3.0, 5.0, 0.0, ¶ms);
assert_eq!(comp.region, MosfetRegion::Saturation);
assert!(comp.id > 0.0);
}
#[test]
fn detect_region_matches_companion() {
let params = MosfetParams::default_nmos();
let vth = threshold_voltage(0.0, ¶ms);
assert_eq!(detect_region(0.0, 5.0, vth), MosfetRegion::Cutoff);
assert_eq!(detect_region(3.0, 5.0, vth), MosfetRegion::Saturation);
assert_eq!(detect_region(3.0, 0.5, vth), MosfetRegion::Triode);
}
}