Skip to main content

pcb_toolkit/differential/
edge_coupled_external.rs

1//! Edge-coupled external (surface) differential pair impedance calculator.
2//!
3//! Computes odd-mode, even-mode, and differential impedance for a surface
4//! microstrip differential pair using the IPC-2141 approximation formula.
5
6use crate::CalcError;
7use super::types::{DifferentialResult, kb_terminated};
8
9/// Inputs for edge-coupled external (surface) differential pair.
10pub struct EdgeCoupledExternalInput {
11    /// Conductor width (mils).
12    pub width: f64,
13    /// Gap between traces (mils).
14    pub spacing: f64,
15    /// Dielectric height to ground plane (mils).
16    pub height: f64,
17    /// Conductor thickness (mils).
18    pub thickness: f64,
19    /// Substrate relative permittivity.
20    pub er: f64,
21}
22
23/// Compute differential impedance for an edge-coupled external (surface) pair.
24pub fn calculate(input: &EdgeCoupledExternalInput) -> Result<DifferentialResult, CalcError> {
25    let EdgeCoupledExternalInput { width, spacing, height, thickness, er } = *input;
26
27    if width <= 0.0 {
28        return Err(CalcError::NegativeDimension { name: "width", value: width });
29    }
30    if spacing <= 0.0 {
31        return Err(CalcError::NegativeDimension { name: "spacing", value: spacing });
32    }
33    if height <= 0.0 {
34        return Err(CalcError::NegativeDimension { name: "height", value: height });
35    }
36    if thickness <= 0.0 {
37        return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
38    }
39    if er < 1.0 {
40        return Err(CalcError::OutOfRange {
41            name: "er",
42            value: er,
43            expected: ">= 1.0",
44        });
45    }
46
47    let z0 = (87.0 / (er + 1.41_f64).sqrt())
48        * (5.98 * height / (0.8 * width + thickness)).ln();
49
50    let zodd = z0 * (1.0 - 0.48 * (-0.96 * spacing / height).exp());
51    let zeven = z0 * z0 / zodd;
52    let zdiff = 2.0 * zodd;
53    let kb = (zeven - zodd) / (zeven + zodd);
54    let kb_db = 20.0 * kb.log10();
55    let kb_term = kb_terminated(kb);
56    let kb_term_db = 20.0 * kb_term.log10();
57
58    Ok(DifferentialResult {
59        zdiff,
60        zo: z0,
61        zodd,
62        zeven,
63        kb,
64        kb_db,
65        kb_term,
66        kb_term_db,
67    })
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use approx::assert_relative_eq;
74
75    fn input(width: f64, spacing: f64, height: f64, thickness: f64, er: f64) -> EdgeCoupledExternalInput {
76        EdgeCoupledExternalInput { width, spacing, height, thickness, er }
77    }
78
79    /// Saturn PDF page 11: W=10, S=5, H=15, Er=4.6, T=2.10
80    #[test]
81    fn saturn_pdf_page11() {
82        let result = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6)).unwrap();
83
84        assert_relative_eq!(result.zo,    77.504,  max_relative = 0.002);
85        assert_relative_eq!(result.zodd,  50.490,  max_relative = 0.002);
86        assert_relative_eq!(result.zeven, 118.971, max_relative = 0.002);
87        assert_relative_eq!(result.zdiff, 100.979, max_relative = 0.002);
88        assert_relative_eq!(result.kb,      0.4041,  max_relative = 0.002);
89        assert_relative_eq!(result.kb_db,   -7.870,  max_relative = 0.002);
90        assert_relative_eq!(result.kb_term, 0.2111,  max_relative = 0.005);
91        assert_relative_eq!(result.kb_term_db, -13.512, max_relative = 0.005);
92    }
93
94    /// Wider spacing reduces coupling magnitude.
95    #[test]
96    fn wider_spacing_reduces_coupling() {
97        let close = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6)).unwrap();
98        let far   = calculate(&input(10.0, 20.0, 15.0, 2.10, 4.6)).unwrap();
99
100        assert!(
101            far.kb.abs() < close.kb.abs(),
102            "wider spacing Kb {:.4} should be smaller than {:.4}",
103            far.kb,
104            close.kb
105        );
106    }
107
108    /// Higher Er gives lower Z0.
109    #[test]
110    fn higher_er_gives_lower_z0() {
111        let low_er  = calculate(&input(10.0, 5.0, 15.0, 2.10, 3.0)).unwrap();
112        let high_er = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6)).unwrap();
113
114        assert!(
115            high_er.zo < low_er.zo,
116            "higher Er Zo {:.3} should be less than {:.3}",
117            high_er.zo,
118            low_er.zo
119        );
120    }
121
122    #[test]
123    fn rejects_negative_width() {
124        let result = calculate(&input(-1.0, 5.0, 15.0, 2.10, 4.6));
125        assert!(result.is_err());
126    }
127
128    #[test]
129    fn rejects_er_below_one() {
130        let result = calculate(&input(10.0, 5.0, 15.0, 2.10, 0.5));
131        assert!(result.is_err());
132    }
133}