Skip to main content

pcb_toolkit/differential/
edge_coupled_internal_asym.rs

1//! Edge-coupled internal asymmetric (offset) differential pair impedance calculator.
2//!
3//! Computes odd-mode, even-mode, and differential impedance for an offset stripline
4//! differential pair using the Wadell offset stripline formula with coupling correction.
5
6use crate::CalcError;
7use super::types::{DifferentialResult, kb_terminated};
8
9/// Inputs for edge-coupled internal asymmetric (offset) differential pair.
10pub struct EdgeCoupledInternalAsymInput {
11    /// Conductor width (mils).
12    pub width: f64,
13    /// Gap between traces (mils).
14    pub spacing: f64,
15    /// Dielectric height from trace to top ground plane (mils).
16    pub height1: f64,
17    /// Dielectric height from trace to bottom ground plane (mils).
18    pub height2: f64,
19    /// Conductor thickness (mils).
20    pub thickness: f64,
21    /// Substrate relative permittivity.
22    pub er: f64,
23}
24
25/// Compute differential impedance for an edge-coupled internal asymmetric (offset) pair.
26pub fn calculate(input: &EdgeCoupledInternalAsymInput) -> Result<DifferentialResult, CalcError> {
27    let EdgeCoupledInternalAsymInput { width, spacing, height1, height2, thickness, er } = *input;
28
29    if width <= 0.0 {
30        return Err(CalcError::NegativeDimension { name: "width", value: width });
31    }
32    if spacing <= 0.0 {
33        return Err(CalcError::NegativeDimension { name: "spacing", value: spacing });
34    }
35    if height1 <= 0.0 {
36        return Err(CalcError::NegativeDimension { name: "height1", value: height1 });
37    }
38    if height2 <= 0.0 {
39        return Err(CalcError::NegativeDimension { name: "height2", value: height2 });
40    }
41    if thickness <= 0.0 {
42        return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
43    }
44    if er < 1.0 {
45        return Err(CalcError::OutOfRange {
46            name: "er",
47            value: er,
48            expected: ">= 1.0",
49        });
50    }
51
52    let z0 = (60.0 / er.sqrt())
53        * (1.9 * (height1 + height2 + thickness) / (0.8 * width + thickness)).ln();
54
55    let h_ref = (height1 + height2) / 2.0;
56    let zodd = z0 * (1.0 - 0.48 * (-0.96 * spacing / h_ref).exp());
57    let zeven = z0 * z0 / zodd;
58    let zdiff = 2.0 * zodd;
59    let kb = (zeven - zodd) / (zeven + zodd);
60    let kb_db = 20.0 * kb.log10();
61    let kb_term = kb_terminated(kb);
62    let kb_term_db = 20.0 * kb_term.log10();
63
64    Ok(DifferentialResult {
65        zdiff,
66        zo: z0,
67        zodd,
68        zeven,
69        kb,
70        kb_db,
71        kb_term,
72        kb_term_db,
73    })
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use approx::assert_relative_eq;
80
81    fn input(
82        width: f64,
83        spacing: f64,
84        height1: f64,
85        height2: f64,
86        thickness: f64,
87        er: f64,
88    ) -> EdgeCoupledInternalAsymInput {
89        EdgeCoupledInternalAsymInput { width, spacing, height1, height2, thickness, er }
90    }
91
92    /// When H1 == H2 the asymmetric formula collapses to the symmetric offset stripline.
93    /// Z0 = (60/√Er) × ln(1.9 × (2H + T) / (0.8W + T))
94    #[test]
95    fn symmetric_case_matches_formula() {
96        let w = 10.0;
97        let s = 5.0;
98        let h = 10.0;
99        let t = 1.4;
100        let er = 4.6_f64;
101
102        let result = calculate(&input(w, s, h, h, t, er)).unwrap();
103
104        let z0_expected = (60.0 / er.sqrt()) * (1.9 * (2.0 * h + t) / (0.8 * w + t)).ln();
105        assert_relative_eq!(result.zo, z0_expected, max_relative = 1e-10);
106
107        let h_ref = h;
108        let zodd_expected = z0_expected * (1.0 - 0.48 * (-0.96 * s / h_ref).exp());
109        assert_relative_eq!(result.zodd, zodd_expected, max_relative = 1e-10);
110        assert_relative_eq!(result.zdiff, 2.0 * zodd_expected, max_relative = 1e-10);
111    }
112
113    /// Changing H1 alters the total dielectric span (H1+H2) and thus Z0.
114    #[test]
115    fn asymmetric_heights_change_z0() {
116        let baseline = calculate(&input(10.0, 5.0, 10.0, 10.0, 1.4, 4.6)).unwrap();
117        let taller   = calculate(&input(10.0, 5.0, 15.0, 10.0, 1.4, 4.6)).unwrap();
118
119        assert!(
120            taller.zo > baseline.zo,
121            "taller dielectric span Z0 {:.4} should exceed baseline Z0 {:.4}",
122            taller.zo,
123            baseline.zo
124        );
125    }
126
127    /// Wider spacing reduces inter-trace coupling.
128    #[test]
129    fn wider_spacing_reduces_coupling() {
130        let close = calculate(&input(10.0,  5.0, 10.0, 10.0, 1.4, 4.6)).unwrap();
131        let far   = calculate(&input(10.0, 20.0, 10.0, 10.0, 1.4, 4.6)).unwrap();
132
133        assert!(
134            far.kb.abs() < close.kb.abs(),
135            "wider spacing Kb {:.4} should be smaller than {:.4}",
136            far.kb,
137            close.kb
138        );
139    }
140
141    /// Higher substrate permittivity lowers single-ended impedance.
142    #[test]
143    fn higher_er_gives_lower_z0() {
144        let low_er  = calculate(&input(10.0, 5.0, 10.0, 10.0, 1.4, 2.2)).unwrap();
145        let high_er = calculate(&input(10.0, 5.0, 10.0, 10.0, 1.4, 4.6)).unwrap();
146
147        assert!(
148            high_er.zo < low_er.zo,
149            "higher Er Z0 {:.3} should be less than lower Er Z0 {:.3}",
150            high_er.zo,
151            low_er.zo
152        );
153    }
154
155    #[test]
156    fn rejects_negative_height1() {
157        let result = calculate(&input(10.0, 5.0, -1.0, 10.0, 1.4, 4.6));
158        assert!(result.is_err());
159    }
160
161    #[test]
162    fn rejects_er_below_one() {
163        let result = calculate(&input(10.0, 5.0, 10.0, 10.0, 1.4, 0.5));
164        assert!(result.is_err());
165    }
166}