Skip to main content

pcb_toolkit/
pdn.rs

1//! PDN (Power Delivery Network) impedance calculator.
2//!
3//! Computes target PDN impedance, plane capacitance, and capacitive reactance.
4
5use serde::{Deserialize, Serialize};
6
7use crate::CalcError;
8
9/// Inputs for a PDN impedance calculation.
10pub struct PdnInput {
11    /// DC supply voltage (V).
12    pub v_supply: f64,
13    /// Maximum load current (A).
14    pub i_max: f64,
15    /// Transient current step as percentage of i_max (%).
16    pub i_step_pct: f64,
17    /// Allowable voltage ripple as percentage of v_supply (%).
18    pub v_ripple_pct: f64,
19    /// Area of power/ground plane (sq.in).
20    pub area_sq_in: f64,
21    /// Substrate relative permittivity.
22    pub er: f64,
23    /// Distance between power and ground planes (mils).
24    pub d_mils: f64,
25    /// Frequency (MHz). 0 = DC only (skip Xc calculation).
26    pub freq_mhz: f64,
27}
28
29/// Result of a PDN impedance calculation.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct PdnResult {
32    /// Target PDN impedance (Ω).
33    pub z_target_ohms: f64,
34    /// Total plane capacitance (pF).
35    pub c_plane_pf: f64,
36    /// Capacitive reactance (Ω), None if DC.
37    pub xc_ohms: Option<f64>,
38}
39
40/// Parallel-plate capacitance constant (ε₀ in pF, imperial units).
41const EPSILON_0_IMPERIAL: f64 = 0.225;
42
43/// Calculate PDN impedance.
44pub fn calculate(input: &PdnInput) -> Result<PdnResult, CalcError> {
45    if input.v_supply <= 0.0 {
46        return Err(CalcError::OutOfRange {
47            name: "v_supply",
48            value: input.v_supply,
49            expected: "> 0",
50        });
51    }
52    if input.i_max <= 0.0 {
53        return Err(CalcError::OutOfRange {
54            name: "i_max",
55            value: input.i_max,
56            expected: "> 0",
57        });
58    }
59    if input.i_step_pct <= 0.0 {
60        return Err(CalcError::OutOfRange {
61            name: "i_step_pct",
62            value: input.i_step_pct,
63            expected: "> 0",
64        });
65    }
66    if input.v_ripple_pct <= 0.0 {
67        return Err(CalcError::OutOfRange {
68            name: "v_ripple_pct",
69            value: input.v_ripple_pct,
70            expected: "> 0",
71        });
72    }
73    if input.area_sq_in <= 0.0 {
74        return Err(CalcError::OutOfRange {
75            name: "area_sq_in",
76            value: input.area_sq_in,
77            expected: "> 0",
78        });
79    }
80    if input.er <= 0.0 {
81        return Err(CalcError::OutOfRange {
82            name: "er",
83            value: input.er,
84            expected: "> 0",
85        });
86    }
87    if input.d_mils <= 0.0 {
88        return Err(CalcError::OutOfRange {
89            name: "d_mils",
90            value: input.d_mils,
91            expected: "> 0",
92        });
93    }
94
95    // Z_target = (V * V_ripple%) / (I * I_step%)
96    let z_target_ohms = (input.v_supply * input.v_ripple_pct / 100.0)
97        / (input.i_max * input.i_step_pct / 100.0);
98
99    // C_plane = 0.225 * Er * A / (d_mils/1000)
100    let d_inches = input.d_mils / 1000.0;
101    let c_plane_pf = EPSILON_0_IMPERIAL * input.er * input.area_sq_in / d_inches;
102
103    // Xc = 1 / (2*pi*f*C) — skip if DC
104    let xc_ohms = if input.freq_mhz > 0.0 {
105        let f_hz = input.freq_mhz * 1e6;
106        let c_farads = c_plane_pf * 1e-12;
107        Some(1.0 / (2.0 * std::f64::consts::PI * f_hz * c_farads))
108    } else {
109        None
110    };
111
112    Ok(PdnResult { z_target_ohms, c_plane_pf, xc_ohms })
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use approx::assert_relative_eq;
119
120    #[test]
121    fn help_pdf_test_vector() {
122        let result = calculate(&PdnInput {
123            v_supply: 5.0,
124            i_max: 2.0,
125            i_step_pct: 50.0,
126            v_ripple_pct: 5.0,
127            area_sq_in: 5.0,
128            er: 4.6,
129            d_mils: 2.0,
130            freq_mhz: 1.0,
131        })
132        .unwrap();
133
134        assert_relative_eq!(result.z_target_ohms, 0.25, epsilon = 1e-10);
135        assert_relative_eq!(result.c_plane_pf, 2587.5, epsilon = 1e-10);
136        assert_relative_eq!(result.xc_ohms.unwrap(), 61.5092, epsilon = 0.001);
137    }
138
139    #[test]
140    fn dc_mode_no_xc() {
141        let result = calculate(&PdnInput {
142            v_supply: 3.3,
143            i_max: 1.0,
144            i_step_pct: 100.0,
145            v_ripple_pct: 10.0,
146            area_sq_in: 2.0,
147            er: 4.6,
148            d_mils: 4.0,
149            freq_mhz: 0.0,
150        })
151        .unwrap();
152
153        assert_relative_eq!(result.z_target_ohms, 0.33, epsilon = 1e-10);
154        assert!(result.xc_ohms.is_none());
155    }
156
157    #[test]
158    fn invalid_inputs() {
159        let base = PdnInput {
160            v_supply: 5.0,
161            i_max: 2.0,
162            i_step_pct: 50.0,
163            v_ripple_pct: 5.0,
164            area_sq_in: 5.0,
165            er: 4.6,
166            d_mils: 2.0,
167            freq_mhz: 1.0,
168        };
169        assert!(calculate(&PdnInput { v_supply: 0.0, ..base }).is_err());
170        assert!(calculate(&PdnInput { i_step_pct: 0.0, ..base }).is_err());
171        assert!(calculate(&PdnInput { d_mils: -1.0, ..base }).is_err());
172    }
173}