Skip to main content

pcb_toolkit/differential/
edge_coupled_embedded.rs

1//! Edge-coupled embedded (buried) differential pair impedance calculator.
2//!
3//! An embedded differential pair is a surface microstrip pair covered by a
4//! dielectric overlay. The burial correction is applied to the single-ended
5//! base impedance first, then the coupling model is applied identically to
6//! the external edge-coupled case.
7
8use crate::CalcError;
9use crate::impedance::embedded::{self, EmbeddedMicrostripInput};
10use super::types::{DifferentialResult, kb_terminated};
11
12/// Inputs for edge-coupled embedded (buried) differential pair.
13pub struct EdgeCoupledEmbeddedInput {
14    /// Conductor width (mils).
15    pub width: f64,
16    /// Gap between traces (mils).
17    pub spacing: f64,
18    /// Dielectric height to ground plane (mils).
19    pub height: f64,
20    /// Conductor thickness (mils).
21    pub thickness: f64,
22    /// Substrate relative permittivity.
23    pub er: f64,
24    /// Cover height — dielectric above the trace (mils).
25    /// When 0, result equals the surface (external) edge-coupled result.
26    pub cover_height: f64,
27}
28
29/// Compute differential impedance for an edge-coupled embedded (buried) pair.
30pub fn calculate(input: &EdgeCoupledEmbeddedInput) -> Result<DifferentialResult, CalcError> {
31    let EdgeCoupledEmbeddedInput { width, spacing, height, thickness, er, cover_height } = *input;
32
33    if width <= 0.0 {
34        return Err(CalcError::NegativeDimension { name: "width", value: width });
35    }
36    if spacing <= 0.0 {
37        return Err(CalcError::NegativeDimension { name: "spacing", value: spacing });
38    }
39    if height <= 0.0 {
40        return Err(CalcError::NegativeDimension { name: "height", value: height });
41    }
42    if thickness <= 0.0 {
43        return Err(CalcError::NegativeDimension { name: "thickness", value: thickness });
44    }
45    if cover_height < 0.0 {
46        return Err(CalcError::NegativeDimension { name: "cover_height", value: cover_height });
47    }
48    if er < 1.0 {
49        return Err(CalcError::OutOfRange {
50            name: "er",
51            value: er,
52            expected: ">= 1.0",
53        });
54    }
55
56    let base = embedded::calculate(&EmbeddedMicrostripInput {
57        width,
58        height,
59        thickness,
60        er,
61        cover_height,
62        frequency: 0.0,
63    })?;
64
65    let z0 = base.zo;
66
67    let zodd = z0 * (1.0 - 0.48 * (-0.96 * spacing / height).exp());
68    let zeven = z0 * z0 / zodd;
69    let zdiff = 2.0 * zodd;
70    let kb = (zeven - zodd) / (zeven + zodd);
71    let kb_db = 20.0 * kb.log10();
72    let kb_term = kb_terminated(kb);
73    let kb_term_db = 20.0 * kb_term.log10();
74
75    Ok(DifferentialResult {
76        zdiff,
77        zo: z0,
78        zodd,
79        zeven,
80        kb,
81        kb_db,
82        kb_term,
83        kb_term_db,
84    })
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use approx::assert_relative_eq;
91
92    fn input(
93        width: f64,
94        spacing: f64,
95        height: f64,
96        thickness: f64,
97        er: f64,
98        cover_height: f64,
99    ) -> EdgeCoupledEmbeddedInput {
100        EdgeCoupledEmbeddedInput { width, spacing, height, thickness, er, cover_height }
101    }
102
103    /// With cover_height=0, embedded base Z0 equals the Hammerstad-Jensen surface
104    /// microstrip Z0 (~75.80 Ω). The external edge-coupled calculator uses the IPC-2141
105    /// approximation formula instead (~77.50 Ω), so the two paths diverge slightly.
106    /// At cover=0 with W=10, S=5, H=15, T=2.10, Er=4.6 the Zdiff is ~95.76.
107    #[test]
108    fn zero_cover_matches_external() {
109        let result = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6, 0.0)).unwrap();
110        assert_relative_eq!(result.zdiff, 95.76, max_relative = 0.005);
111    }
112
113    /// Deeper burial reduces the single-ended Z0 through the exp correction factor.
114    #[test]
115    fn deeper_burial_reduces_z0() {
116        let surface = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6, 0.0)).unwrap();
117        let buried = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6, 5.0)).unwrap();
118
119        assert!(
120            buried.zo < surface.zo,
121            "buried Zo {:.3} should be less than surface Zo {:.3}",
122            buried.zo,
123            surface.zo
124        );
125    }
126
127    /// Wider spacing reduces coupling magnitude.
128    #[test]
129    fn wider_spacing_reduces_coupling() {
130        let close = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6, 3.0)).unwrap();
131        let far = calculate(&input(10.0, 20.0, 15.0, 2.10, 4.6, 3.0)).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    #[test]
142    fn rejects_negative_width() {
143        let result = calculate(&input(-1.0, 5.0, 15.0, 2.10, 4.6, 0.0));
144        assert!(result.is_err());
145    }
146
147    #[test]
148    fn rejects_negative_cover_height() {
149        let result = calculate(&input(10.0, 5.0, 15.0, 2.10, 4.6, -1.0));
150        assert!(result.is_err());
151    }
152}