1use serde::{Deserialize, Serialize};
22
23use crate::CalcError;
24use crate::constants::SPEED_OF_LIGHT_IN_NS;
25use crate::impedance::common;
26
27pub struct CrosstalkInput {
29 pub rise_time_ns: f64,
31 pub voltage: f64,
33 pub coupled_length_mils: f64,
35 pub spacing_mils: f64,
37 pub height_mils: f64,
39 pub er: f64,
41 pub trace_width_mils: f64,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct CrosstalkResult {
48 pub kb: f64,
50 pub crosstalk_db: f64,
52 pub coupled_voltage: f64,
54 pub next_coefficient: f64,
56 pub lsat_mils: f64,
58}
59
60pub fn calculate(input: &CrosstalkInput) -> Result<CrosstalkResult, CalcError> {
62 let CrosstalkInput {
63 rise_time_ns,
64 voltage,
65 coupled_length_mils,
66 spacing_mils,
67 height_mils,
68 er,
69 trace_width_mils,
70 } = *input;
71
72 if rise_time_ns <= 0.0 {
73 return Err(CalcError::OutOfRange {
74 name: "rise_time_ns",
75 value: rise_time_ns,
76 expected: "> 0",
77 });
78 }
79 if voltage <= 0.0 {
80 return Err(CalcError::OutOfRange {
81 name: "voltage",
82 value: voltage,
83 expected: "> 0",
84 });
85 }
86 if coupled_length_mils <= 0.0 {
87 return Err(CalcError::NegativeDimension {
88 name: "coupled_length_mils",
89 value: coupled_length_mils,
90 });
91 }
92 if spacing_mils <= 0.0 {
93 return Err(CalcError::NegativeDimension {
94 name: "spacing_mils",
95 value: spacing_mils,
96 });
97 }
98 if height_mils <= 0.0 {
99 return Err(CalcError::NegativeDimension {
100 name: "height_mils",
101 value: height_mils,
102 });
103 }
104 if er < 1.0 {
105 return Err(CalcError::OutOfRange {
106 name: "er",
107 value: er,
108 expected: ">= 1.0",
109 });
110 }
111 if trace_width_mils <= 0.0 {
112 return Err(CalcError::NegativeDimension {
113 name: "trace_width_mils",
114 value: trace_width_mils,
115 });
116 }
117
118 let s_over_h = spacing_mils / height_mils;
120 let kb = 1.0 / (4.0 * (1.0 + s_over_h * s_over_h));
121
122 let u = trace_width_mils / height_mils;
124 let er_eff = common::er_eff_static(u, er);
125 let v_prop = SPEED_OF_LIGHT_IN_NS / er_eff.sqrt();
127 let v_prop_mils_ns = v_prop * 1000.0;
129
130 let lsat_mils = rise_time_ns * v_prop_mils_ns / 2.0;
132
133 let length_ratio = coupled_length_mils / lsat_mils;
135 let next_coefficient = kb * length_ratio.min(1.0);
136
137 let coupled_voltage = next_coefficient * voltage;
139
140 let crosstalk_db = 20.0 * next_coefficient.log10();
142
143 Ok(CrosstalkResult {
144 kb,
145 crosstalk_db,
146 coupled_voltage,
147 next_coefficient,
148 lsat_mils,
149 })
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn basic_crosstalk() {
158 let result = calculate(&CrosstalkInput {
159 rise_time_ns: 1.0,
160 voltage: 5.0,
161 coupled_length_mils: 1000.0,
162 spacing_mils: 10.0,
163 height_mils: 5.0,
164 er: 4.6,
165 trace_width_mils: 10.0,
166 })
167 .unwrap();
168
169 assert!(result.kb > 0.0 && result.kb <= 0.25, "Kb = {}", result.kb);
171 assert!(
173 result.next_coefficient > 0.0 && result.next_coefficient <= result.kb,
174 "NEXT = {}",
175 result.next_coefficient
176 );
177 assert!(result.coupled_voltage > 0.0 && result.coupled_voltage < 5.0);
178 assert!(result.crosstalk_db < 0.0); assert!(result.lsat_mils > 0.0);
180 }
181
182 #[test]
183 fn wider_spacing_less_crosstalk() {
184 let close = calculate(&CrosstalkInput {
185 rise_time_ns: 1.0,
186 voltage: 5.0,
187 coupled_length_mils: 1000.0,
188 spacing_mils: 5.0,
189 height_mils: 5.0,
190 er: 4.6,
191 trace_width_mils: 10.0,
192 })
193 .unwrap();
194
195 let far = calculate(&CrosstalkInput {
196 rise_time_ns: 1.0,
197 voltage: 5.0,
198 coupled_length_mils: 1000.0,
199 spacing_mils: 20.0,
200 height_mils: 5.0,
201 er: 4.6,
202 trace_width_mils: 10.0,
203 })
204 .unwrap();
205
206 assert!(
207 close.kb > far.kb,
208 "close Kb {} should be > far Kb {}",
209 close.kb,
210 far.kb
211 );
212 }
213
214 #[test]
215 fn kb_max_at_zero_spacing_limit() {
216 let result = calculate(&CrosstalkInput {
218 rise_time_ns: 1.0,
219 voltage: 5.0,
220 coupled_length_mils: 1000.0,
221 spacing_mils: 0.01, height_mils: 5.0,
223 er: 4.6,
224 trace_width_mils: 10.0,
225 })
226 .unwrap();
227
228 assert!(result.kb > 0.24, "Kb at near-zero spacing = {}", result.kb);
229 }
230
231 #[test]
232 fn short_coupled_length_reduces_next() {
233 let long = calculate(&CrosstalkInput {
234 rise_time_ns: 1.0,
235 voltage: 5.0,
236 coupled_length_mils: 10000.0,
237 spacing_mils: 10.0,
238 height_mils: 5.0,
239 er: 4.6,
240 trace_width_mils: 10.0,
241 })
242 .unwrap();
243
244 let short = calculate(&CrosstalkInput {
245 rise_time_ns: 1.0,
246 voltage: 5.0,
247 coupled_length_mils: 100.0,
248 spacing_mils: 10.0,
249 height_mils: 5.0,
250 er: 4.6,
251 trace_width_mils: 10.0,
252 })
253 .unwrap();
254
255 assert!(
256 short.next_coefficient <= long.next_coefficient,
257 "short NEXT {} should be <= long NEXT {}",
258 short.next_coefficient,
259 long.next_coefficient
260 );
261 }
262
263 #[test]
264 fn rejects_negative_spacing() {
265 let result = calculate(&CrosstalkInput {
266 rise_time_ns: 1.0,
267 voltage: 5.0,
268 coupled_length_mils: 1000.0,
269 spacing_mils: -1.0,
270 height_mils: 5.0,
271 er: 4.6,
272 trace_width_mils: 10.0,
273 });
274 assert!(result.is_err());
275 }
276
277 #[test]
278 fn rejects_zero_rise_time() {
279 let result = calculate(&CrosstalkInput {
280 rise_time_ns: 0.0,
281 voltage: 5.0,
282 coupled_length_mils: 1000.0,
283 spacing_mils: 10.0,
284 height_mils: 5.0,
285 er: 4.6,
286 trace_width_mils: 10.0,
287 });
288 assert!(result.is_err());
289 }
290}