pcb_toolkit/differential/
broadside_coupled.rs1use crate::CalcError;
10use super::types::{DifferentialResult, kb_terminated};
11
12pub struct BroadsideCoupledInput {
14 pub width: f64,
16 pub separation: f64,
18 pub height_total: f64,
20 pub thickness: f64,
22 pub er: f64,
24 pub shielded: bool,
26}
27
28pub 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 #[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 #[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 #[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 #[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}