Skip to main content

pcb_toolkit/
via.rs

1//! Via electrical and thermal property calculator.
2//!
3//! Computes parasitic inductance, capacitance, impedance, resonant frequency,
4//! DC resistance, current capacity, and thermal resistance.
5//!
6//! Reference: Bert Simonovich (differential via modeling).
7
8use serde::{Deserialize, Serialize};
9
10use crate::CalcError;
11
12/// Input parameters for the via electrical property calculator.
13///
14/// All dimensions in mils.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct ViaInput {
17    /// Drilled hole diameter in mils.
18    pub hole_diameter_mils: f64,
19    /// Pad diameter on signal layers in mils.
20    pub pad_diameter_mils: f64,
21    /// Antipad (reference plane clearance opening) diameter in mils.
22    pub antipad_diameter_mils: f64,
23    /// Via barrel height (board thickness) in mils.
24    pub height_mils: f64,
25    /// Copper plating thickness in mils.
26    pub plating_thickness_mils: f64,
27    /// Relative permittivity of the board material.
28    pub er: f64,
29}
30
31/// Computed via electrical properties.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct ViaResult {
34    /// Parasitic capacitance in pF.
35    pub capacitance_pf: f64,
36    /// Parasitic inductance in nH.
37    pub inductance_nh: f64,
38    /// Characteristic impedance in Ohms.
39    pub impedance_ohms: f64,
40    /// Self-resonant frequency in MHz.
41    pub resonant_freq_mhz: f64,
42}
43
44/// Calculate via electrical properties.
45///
46/// # Arguments
47/// - `input` — via geometry and material parameters
48///
49/// # Errors
50/// Returns [`CalcError::NegativeDimension`] for non-positive dimensions, or
51/// [`CalcError::OutOfRange`] if the antipad diameter is not larger than the pad diameter.
52pub fn calculate(input: &ViaInput) -> Result<ViaResult, CalcError> {
53    if input.hole_diameter_mils <= 0.0 {
54        return Err(CalcError::NegativeDimension {
55            name: "hole_diameter_mils",
56            value: input.hole_diameter_mils,
57        });
58    }
59    if input.pad_diameter_mils <= 0.0 {
60        return Err(CalcError::NegativeDimension {
61            name: "pad_diameter_mils",
62            value: input.pad_diameter_mils,
63        });
64    }
65    if input.antipad_diameter_mils <= 0.0 {
66        return Err(CalcError::NegativeDimension {
67            name: "antipad_diameter_mils",
68            value: input.antipad_diameter_mils,
69        });
70    }
71    if input.height_mils <= 0.0 {
72        return Err(CalcError::NegativeDimension {
73            name: "height_mils",
74            value: input.height_mils,
75        });
76    }
77    if input.plating_thickness_mils <= 0.0 {
78        return Err(CalcError::NegativeDimension {
79            name: "plating_thickness_mils",
80            value: input.plating_thickness_mils,
81        });
82    }
83    if input.er <= 0.0 {
84        return Err(CalcError::OutOfRange {
85            name: "er",
86            value: input.er,
87            expected: "> 0",
88        });
89    }
90    if input.antipad_diameter_mils <= input.pad_diameter_mils {
91        return Err(CalcError::OutOfRange {
92            name: "antipad_diameter_mils",
93            value: input.antipad_diameter_mils,
94            expected: "> pad_diameter_mils",
95        });
96    }
97
98    // Convert mils to inches for formula application.
99    let d_hole = input.hole_diameter_mils / 1000.0;
100    let d_pad = input.pad_diameter_mils / 1000.0;
101    let d_antipad = input.antipad_diameter_mils / 1000.0;
102    let h = input.height_mils / 1000.0;
103
104    // Parasitic capacitance (pF).
105    //
106    // C = 1.41 × Er × h × D_pad / (D_antipad - D_pad)
107    let capacitance_pf =
108        1.41 * input.er * h * d_pad / (d_antipad - d_pad);
109
110    // Parasitic inductance (nH).
111    //
112    // L = 5.08 × h × (ln(4×h / d_hole) + 1)
113    let inductance_nh = 5.08 * h * ((4.0 * h / d_hole).ln() + 1.0);
114
115    // Characteristic impedance (Ω).
116    //
117    // Z = √(L / C)  with L in nH and C in pF → L/C already in Ω² (nH/pF = Ω²)
118    let impedance_ohms = (inductance_nh / capacitance_pf).sqrt() * 1000.0_f64.sqrt();
119
120    // Self-resonant frequency (MHz).
121    //
122    // f = 1 / (2π√(L×C))  with L in nH and C in pF
123    // √(nH × pF) = √(1e-9 × 1e-12) × √(L_val × C_val) = 1e-10.5 × √(L_val × C_val)
124    // f in Hz = 1 / (2π × 1e-10.5 × √(L_val × C_val))
125    //         = 1e10.5 / (2π × √(L_val × C_val))
126    // f in MHz = f_Hz / 1e6
127    let lc_product_nh_pf = inductance_nh * capacitance_pf;
128    // 1 nH × 1 pF = 1e-21 H·F; √(1e-21) = 1e-10.5 ≈ 3.16228e-11
129    let resonant_freq_mhz =
130        1.0 / (2.0 * std::f64::consts::PI * (lc_product_nh_pf * 1e-21).sqrt()) / 1e6;
131
132    Ok(ViaResult {
133        capacitance_pf,
134        inductance_nh,
135        impedance_ohms,
136        resonant_freq_mhz,
137    })
138}
139
140#[cfg(test)]
141mod tests {
142    use approx::assert_relative_eq;
143
144    use super::*;
145
146    // Saturn PCB Toolkit help PDF page 36:
147    //   hole=10 mils, pad=20 mils, antipad=40 mils, height=62 mils,
148    //   plating=1 mil, Er=4.6
149    //
150    //   C_via   = 0.4021 pF
151    //   L_via   = 1.3262 nH
152    //   Z_via   = 57.429 Ω
153    //   f_res   = 6891.661 MHz
154    #[test]
155    fn saturn_page36_via_vector() {
156        let input = ViaInput {
157            hole_diameter_mils: 10.0,
158            pad_diameter_mils: 20.0,
159            antipad_diameter_mils: 40.0,
160            height_mils: 62.0,
161            plating_thickness_mils: 1.0,
162            er: 4.6,
163        };
164        let result = calculate(&input).unwrap();
165        assert_relative_eq!(result.capacitance_pf, 0.4021, epsilon = 1e-3);
166        assert_relative_eq!(result.inductance_nh, 1.3262, epsilon = 1e-3);
167        assert_relative_eq!(result.impedance_ohms, 57.429, epsilon = 1e-2);
168        assert_relative_eq!(result.resonant_freq_mhz, 6891.661, epsilon = 1.0);
169    }
170
171    #[test]
172    fn error_on_zero_hole() {
173        let input = ViaInput {
174            hole_diameter_mils: 0.0,
175            pad_diameter_mils: 20.0,
176            antipad_diameter_mils: 40.0,
177            height_mils: 62.0,
178            plating_thickness_mils: 1.0,
179            er: 4.6,
180        };
181        assert!(calculate(&input).is_err());
182    }
183
184    #[test]
185    fn error_when_antipad_not_larger_than_pad() {
186        let input = ViaInput {
187            hole_diameter_mils: 10.0,
188            pad_diameter_mils: 40.0,
189            antipad_diameter_mils: 40.0,
190            height_mils: 62.0,
191            plating_thickness_mils: 1.0,
192            er: 4.6,
193        };
194        assert!(calculate(&input).is_err());
195    }
196}