Skip to main content

pcb_toolkit/differential/
broadside_coupled.rs

1//! Broadside-coupled differential pair impedance calculator.
2//!
3//! Supports shielded (between two ground planes, Wadell stripline model) and
4//! non-shielded (single ground plane below, microstrip-style model) configurations.
5//!
6//! # TODO
7//! - Validate against Saturn when test vector available (confidence: LOW — no RE data)
8
9use crate::CalcError;
10use super::types::{DifferentialResult, kb_terminated};
11
12/// Inputs for broadside-coupled differential pair.
13pub struct BroadsideCoupledInput {
14    /// Strip width (mils).
15    pub width: f64,
16    /// Vertical separation between the two strips (mils).
17    pub separation: f64,
18    /// Ground-to-ground spacing (mils). Only used in shielded mode.
19    pub height_total: f64,
20    /// Conductor thickness (mils).
21    pub thickness: f64,
22    /// Substrate relative permittivity.
23    pub er: f64,
24    /// true = shielded (between two ground planes), false = non-shielded
25    pub shielded: bool,
26}
27
28/// Compute differential impedance for a broadside-coupled differential pair.
29pub fn calculate(input: &BroadsideCoupledInput) -> Result<DifferentialResult, CalcError> {
30    let BroadsideCoupledInput { width, separation, height_total, thickness, er, shielded } = *input;
31
32    if width <= 0.0 {
33        return Err(CalcError::NegativeDimension { name: "width", value: width });
34    }
35    if separation <= 0.0 {
36        return Err(CalcError::NegativeDimension { name: "separation", value: separation });
37    }
38    if height_total <= 0.0 {
39        return Err(CalcError::NegativeDimension { name: "height_total", value: height_total });
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 = if shielded {
53        (60.0 / er.sqrt())
54            * (1.9 * height_total / (0.8 * width + thickness)).ln()
55    } else {
56        (87.0 / (er + 1.41_f64).sqrt())
57            * (5.98 * (separation + height_total) / (0.8 * width + thickness)).ln()
58    };
59
60    let cf = 1.0 - 1.0 / (std::f64::consts::PI * width / (2.0 * separation)).cosh();
61
62    let zodd  = z0 * (1.0 - cf);
63    let zeven = z0 * (1.0 + cf);
64    let zdiff = 2.0 * zodd;
65    let kb = (zeven - zodd) / (zeven + zodd);
66    let kb_db = 20.0 * kb.log10();
67    let kb_term = kb_terminated(kb);
68    let kb_term_db = 20.0 * kb_term.log10();
69
70    Ok(DifferentialResult {
71        zdiff,
72        zo: z0,
73        zodd,
74        zeven,
75        kb,
76        kb_db,
77        kb_term,
78        kb_term_db,
79    })
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use approx::assert_relative_eq;
86
87    fn shielded(width: f64, separation: f64, height_total: f64, thickness: f64, er: f64) -> BroadsideCoupledInput {
88        BroadsideCoupledInput { width, separation, height_total, thickness, er, shielded: true }
89    }
90
91    fn unshielded(width: f64, separation: f64, height_total: f64, thickness: f64, er: f64) -> BroadsideCoupledInput {
92        BroadsideCoupledInput { width, separation, height_total, thickness, er, shielded: false }
93    }
94
95    /// Wider strip lowers Z0 (stripline: denominator 0.8*W+T grows).
96    #[test]
97    fn shielded_wider_strip_lowers_z0() {
98        let narrow = calculate(&shielded(5.0,  5.0, 30.0, 2.0, 4.5)).unwrap();
99        let wide   = calculate(&shielded(20.0, 5.0, 30.0, 2.0, 4.5)).unwrap();
100
101        assert!(
102            narrow.zo > wide.zo,
103            "narrow Z0 {:.3} should exceed wide Z0 {:.3}",
104            narrow.zo,
105            wide.zo
106        );
107    }
108
109    /// Larger separation reduces coupling coefficient Kb.
110    #[test]
111    fn shielded_larger_separation_reduces_coupling() {
112        let close = calculate(&shielded(10.0, 5.0,  30.0, 2.0, 4.5)).unwrap();
113        let far   = calculate(&shielded(10.0, 20.0, 30.0, 2.0, 4.5)).unwrap();
114
115        assert!(
116            far.kb < close.kb,
117            "wider separation Kb {:.4} should be less than {:.4}",
118            far.kb,
119            close.kb
120        );
121    }
122
123    /// Zdiff = 2 * Zodd is an exact identity.
124    #[test]
125    fn shielded_zdiff_equals_two_zodd() {
126        let r = calculate(&shielded(10.0, 5.0, 30.0, 2.0, 4.5)).unwrap();
127        assert_relative_eq!(r.zdiff, 2.0 * r.zodd, max_relative = 1e-12);
128    }
129
130    /// Non-shielded should produce a Z0 in the 20–200 Ohm range.
131    #[test]
132    fn unshielded_reasonable_impedance() {
133        let r = calculate(&unshielded(10.0, 5.0, 15.0, 2.0, 4.5)).unwrap();
134
135        assert!(
136            r.zo >= 20.0 && r.zo <= 200.0,
137            "Z0 {:.3} should be in [20, 200] Ohm range",
138            r.zo
139        );
140    }
141
142    #[test]
143    fn rejects_negative_width() {
144        let result = calculate(&shielded(-1.0, 5.0, 30.0, 2.0, 4.5));
145        assert!(result.is_err());
146    }
147
148    #[test]
149    fn rejects_er_below_one() {
150        let result = calculate(&shielded(10.0, 5.0, 30.0, 2.0, 0.5));
151        assert!(result.is_err());
152    }
153}