Skip to main content

leodos_protocols/physical/modulator/
gmsk.rs

1//! Gaussian Minimum Shift Keying (GMSK) Modulation
2//!
3//! GMSK is a continuous-phase modulation with Gaussian pulse
4//! shaping and modulation index h = 0.5. The Gaussian filter
5//! smooths the frequency transitions, yielding excellent spectral
6//! containment and a nearly constant envelope.
7//!
8//! Used in Proximity-1 links (CCSDS 211.0, BT = 0.25) and other
9//! near-Earth LEO communication systems.
10//!
11//! # Modulation
12//!
13//! The instantaneous frequency deviation is the NRZ data convolved
14//! with a Gaussian pulse truncated to 4 symbol periods. The phase
15//! is the running integral of the frequency deviation.
16//!
17//! # Demodulation
18//!
19//! Soft-decision differential detection: computes the cross-product
20//! of consecutive symbol-spaced samples to approximate the phase
21//! difference, then scales by 1/σ² for LLR output.
22
23use super::clamp_i16;
24
25/// Maximum supported samples-per-symbol.
26const MAX_SPS: usize = 32;
27
28/// Pulse truncation length in symbol periods.
29const TRUNC: usize = 4;
30
31/// Maximum pulse filter length.
32const MAX_PULSE: usize = TRUNC * MAX_SPS + 1;
33
34/// Returns the number of output samples for a GMSK frame.
35///
36/// Each input bit produces `sps` I/Q sample pairs, plus a tail
37/// of `TRUNC / 2 * sps` samples for the pulse filter to ring
38/// down.
39pub fn output_len(n_bits: usize, sps: usize) -> usize {
40    n_bits * sps + TRUNC / 2 * sps
41}
42
43/// Computes the Gaussian frequency pulse shape.
44///
45/// Returns `(pulse, len)` where `pulse[0..len]` contains the
46/// sampled pulse, normalized so the total phase per bit is π/2
47/// (for h = 0.5).
48fn gaussian_pulse(bt: f32, sps: usize) -> ([f32; MAX_PULSE], usize) {
49    let pulse_len = TRUNC * sps + 1;
50    assert!(pulse_len <= MAX_PULSE);
51
52    let center = (pulse_len / 2) as f32;
53    let c = core::f32::consts::PI * bt
54        / libm::sqrtf(2.0 * libm::logf(2.0));
55
56    let mut pulse = [0f32; MAX_PULSE];
57    let mut sum = 0f32;
58
59    for m in 0..pulse_len {
60        let t = (m as f32 - center) / sps as f32;
61        let a1 = c * (2.0 * t - 1.0);
62        let a2 = c * (2.0 * t + 1.0);
63        pulse[m] = 0.25 * (libm::erff(a2) - libm::erff(a1));
64        sum += pulse[m];
65    }
66
67    // Normalize so total phase per bit = π/2
68    let norm = (sps as f32 * 0.5) / sum;
69    for m in 0..pulse_len {
70        pulse[m] *= norm;
71    }
72
73    (pulse, pulse_len)
74}
75
76/// Modulates packed bits using GMSK.
77///
78/// - `bt`: Gaussian bandwidth-time product (e.g. 0.25, 0.3, 0.5)
79/// - `sps`: samples per symbol (must be ≤ 32)
80///
81/// Writes `output_len(n_bits, sps)` samples to `out_i` and `out_q`.
82pub fn modulate_gmsk(
83    bits: &[u8],
84    n_bits: usize,
85    bt: f32,
86    sps: usize,
87    out_i: &mut [f32],
88    out_q: &mut [f32],
89) {
90    assert!(sps >= 1 && sps <= MAX_SPS);
91    assert!(bits.len() * 8 >= n_bits || n_bits == 0);
92
93    let (pulse, pulse_len) = gaussian_pulse(bt, sps);
94    let n_out = output_len(n_bits, sps);
95    assert!(out_i.len() >= n_out);
96    assert!(out_q.len() >= n_out);
97
98    let sps_f = sps as f32;
99    let mut phase = 0f32;
100
101    let half = (pulse_len / 2) as isize;
102
103    for n in 0..n_out {
104        // Compute filtered frequency deviation at sample n.
105        // The pulse for bit k is centered at sample k*sps, so
106        // tap = n - k*sps + pulse_len/2 maps the center of the
107        // stored pulse to the bit transition.
108        let mut freq = 0f32;
109
110        // Only iterate over bits whose pulse overlaps sample n.
111        let first_k = if (n as isize) >= half {
112            ((n as isize - half) as usize) / sps
113        } else {
114            0
115        };
116        let last_k = (((n as isize + half) as usize) / sps + 1)
117            .min(n_bits);
118
119        for k in first_k..last_k {
120            let tap = n as isize - (k * sps) as isize + half;
121            if tap >= 0 && (tap as usize) < pulse_len {
122                let bit =
123                    (bits[k / 8] >> (7 - (k % 8))) & 1;
124                let nrz = 1.0 - 2.0 * bit as f32;
125                freq += nrz * pulse[tap as usize];
126            }
127        }
128
129        phase += core::f32::consts::PI / sps_f * freq;
130        out_i[n] = libm::cosf(phase);
131        out_q[n] = libm::sinf(phase);
132    }
133}
134
135/// Demodulates GMSK using soft-decision differential detection.
136///
137/// Computes the cross-product of samples spaced `sps` apart to
138/// approximate `sin(Δφ)`, which is proportional to the transmitted
139/// bit. The result is scaled by `scale / σ²` and quantized to i16.
140///
141/// Produces one LLR per bit. Bits at the beginning (before the
142/// first full symbol delay) use the initial reference (1, 0).
143pub fn demodulate_gmsk(
144    in_i: &[f32],
145    in_q: &[f32],
146    n_bits: usize,
147    sps: usize,
148    noise_var: f32,
149    scale: f32,
150    llr: &mut [i16],
151) {
152    assert!(sps >= 1 && sps <= MAX_SPS);
153    assert!(llr.len() >= n_bits);
154
155    let n_out = output_len(n_bits, sps);
156    assert!(in_i.len() >= n_out);
157    assert!(in_q.len() >= n_out);
158
159    let factor = scale / noise_var;
160    let half_sps = sps / 2;
161
162    for k in 0..n_bits {
163        // Sample at the center of bit k
164        let n = k * sps + half_sps;
165
166        // Previous sample, one symbol period earlier
167        let (prev_i, prev_q) = if n >= sps {
168            (in_i[n - sps], in_q[n - sps])
169        } else {
170            (1.0, 0.0) // initial reference
171        };
172
173        // Cross-product ≈ sin(Δφ): I_prev · Q_curr - Q_prev · I_curr
174        let cross = prev_i * in_q[n] - prev_q * in_i[n];
175        llr[k] = clamp_i16(factor * cross);
176    }
177}
178
179/// GMSK modulator/demodulator with configurable parameters.
180pub struct Gmsk {
181    bt: f32,
182    sps: usize,
183    noise_var: f32,
184    scale: f32,
185}
186
187impl Gmsk {
188    /// Creates a GMSK modem.
189    ///
190    /// - `bt`: Gaussian bandwidth-time product (e.g. 0.25)
191    /// - `sps`: samples per symbol (≤ 32)
192    /// - `noise_var`: noise variance σ²
193    /// - `scale`: LLR quantization scale factor
194    pub fn new(bt: f32, sps: usize, noise_var: f32, scale: f32) -> Self {
195        Self { bt, sps, noise_var, scale }
196    }
197}
198
199impl super::Modulator for Gmsk {
200    fn modulate(
201        &self,
202        bits: &[u8],
203        n_bits: usize,
204        symbols: &mut [f32],
205    ) -> usize {
206        let n_out = output_len(n_bits, self.sps);
207        let (oi, oq) = symbols.split_at_mut(n_out);
208        modulate_gmsk(bits, n_bits, self.bt, self.sps, oi, oq);
209        n_out * 2
210    }
211}
212
213impl super::Demodulator for Gmsk {
214    fn demodulate_soft(
215        &self,
216        symbols: &[f32],
217        n_bits: usize,
218        llr: &mut [i16],
219    ) {
220        let n_out = output_len(n_bits, self.sps);
221        let (ii, iq) = symbols.split_at(n_out);
222        demodulate_gmsk(
223            ii, iq, n_bits, self.sps,
224            self.noise_var, self.scale, llr,
225        );
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn pulse_shape_normalized() {
235        for &bt in &[0.25f32, 0.3, 0.5] {
236            let sps = 8;
237            let (pulse, len) = gaussian_pulse(bt, sps);
238            let sum: f32 = pulse[..len].iter().sum();
239            let expected = sps as f32 * 0.5;
240            assert!(
241                (sum - expected).abs() < 0.01,
242                "bt={bt}: sum={sum}, expected={expected}"
243            );
244        }
245    }
246
247    #[test]
248    fn pulse_shape_symmetric() {
249        let (pulse, len) = gaussian_pulse(0.25, 8);
250        for i in 0..len / 2 {
251            let diff = (pulse[i] - pulse[len - 1 - i]).abs();
252            assert!(diff < 1e-6, "asymmetry at {i}: {diff}");
253        }
254    }
255
256    #[test]
257    fn constant_envelope() {
258        let bits = [0xA5u8, 0x5A];
259        let n_bits = 16;
260        let sps = 8;
261        let n_out = output_len(n_bits, sps);
262        let mut oi = [0f32; 256];
263        let mut oq = [0f32; 256];
264        modulate_gmsk(&bits, n_bits, 0.25, sps, &mut oi, &mut oq);
265
266        for n in 0..n_out {
267            let env = libm::sqrtf(oi[n] * oi[n] + oq[n] * oq[n]);
268            assert!(
269                (env - 1.0).abs() < 1e-5,
270                "envelope at {n} = {env}"
271            );
272        }
273    }
274
275    #[test]
276    fn roundtrip_no_noise() {
277        let bits = [0b10110100u8];
278        let n_bits = 8;
279        let sps = 8;
280        let mut oi = [0f32; 256];
281        let mut oq = [0f32; 256];
282        modulate_gmsk(&bits, n_bits, 0.5, sps, &mut oi, &mut oq);
283
284        let mut llr = [0i16; 8];
285        demodulate_gmsk(&oi, &oq, n_bits, sps, 0.01, 100.0, &mut llr);
286
287        let mut recovered = [0u8; 1];
288        for i in 0..8 {
289            if llr[i] < 0 {
290                recovered[0] |= 1 << (7 - i);
291            }
292        }
293        assert_eq!(recovered[0], bits[0]);
294    }
295
296    #[test]
297    fn roundtrip_multiple_bytes() {
298        let bits = [0xDE, 0xAD, 0xBE, 0xEF];
299        let n_bits = 32;
300        let sps = 8;
301        let mut oi = [0f32; 512];
302        let mut oq = [0f32; 512];
303        modulate_gmsk(&bits, n_bits, 0.3, sps, &mut oi, &mut oq);
304
305        let mut llr = [0i16; 32];
306        demodulate_gmsk(&oi, &oq, n_bits, sps, 0.01, 100.0, &mut llr);
307
308        let mut recovered = [0u8; 4];
309        for i in 0..32 {
310            if llr[i] < 0 {
311                recovered[i / 8] |= 1 << (7 - (i % 8));
312            }
313        }
314        assert_eq!(recovered, bits);
315    }
316
317    #[test]
318    fn all_zeros_positive_llr() {
319        let bits = [0x00u8];
320        let n_bits = 8;
321        let sps = 8;
322        let mut oi = [0f32; 256];
323        let mut oq = [0f32; 256];
324        modulate_gmsk(&bits, n_bits, 0.5, sps, &mut oi, &mut oq);
325
326        let mut llr = [0i16; 8];
327        demodulate_gmsk(&oi, &oq, n_bits, sps, 0.01, 100.0, &mut llr);
328
329        // All bits are 0, so LLRs should all be positive
330        for (i, &l) in llr.iter().enumerate() {
331            assert!(l > 0, "llr[{i}] = {l}, expected positive");
332        }
333    }
334
335    #[test]
336    fn output_len_calculation() {
337        assert_eq!(output_len(10, 8), 10 * 8 + 2 * 8);
338        assert_eq!(output_len(0, 8), 16);
339    }
340
341    #[test]
342    fn different_bt_products() {
343        // All BT values should produce valid roundtrips
344        for &bt in &[0.25f32, 0.3, 0.5] {
345            let bits = [0xC3u8];
346            let n_bits = 8;
347            let sps = 8;
348            let mut oi = [0f32; 256];
349            let mut oq = [0f32; 256];
350            modulate_gmsk(&bits, n_bits, bt, sps, &mut oi, &mut oq);
351
352            let mut llr = [0i16; 8];
353            demodulate_gmsk(
354                &oi, &oq, n_bits, sps, 0.01, 100.0, &mut llr,
355            );
356
357            let mut recovered = [0u8; 1];
358            for i in 0..8 {
359                if llr[i] < 0 {
360                    recovered[0] |= 1 << (7 - i);
361                }
362            }
363            assert_eq!(
364                recovered[0], bits[0],
365                "roundtrip failed for BT={bt}"
366            );
367        }
368    }
369}