fls/
fls.rs

1#![cfg_attr(target_os = "none", no_std)]
2#![cfg_attr(target_os = "none", no_main)]
3
4//! Patent pending: DE102021112017A1
5//!
6//! # Algorithm description
7//!
8//! This application can be understood as a universal phase (frequency)
9//! signal processor. It determines the phase (we will drop frequency
10//! from now on as in a phase-aware system frequency is merely the
11//! difference between successive phases) of an RF input signal and
12//! emits an RF output signal with a phase that depends on the input
13//! phase. The transfer function between input and output phase is a
14//! sequence of various types of filters (analog RC, digital FIR, IIR,
15//! unwrapping, scaling, clipping) designed to implement either
16//! high-quality phase measurements or a certain constrained and
17//! somewhat exotic phase locked loop that is highly applicable
18//! to the task of stabilizing the arm length of an optical Michelson
19//! interferometer which in turn occurs when stabilizing the effective
20//! path length of an optical frequency transmission system.
21//!
22//! The sequence of processing steps is as follows. Analyzing it's
23//! application in the context of optical path length stabilization including
24//! laser sources, optical modulators, and photodetectors optical is left as
25//! an exercise for the user.
26//!
27//! ## PLL path
28//!
29//! * DDS locks its sysclk (500 MHz) to XO or external ref
30//! * DDS emits SYNC signal at sysclk/4
31//! * Prescaler 1/4 (in CPU)
32//! * Drives CPU timer counter
33//! * Counter is captured once per batch (based on CPU clock).
34//!   See [stabilizer::hardware::pounder::timestamp].
35//! * Digital PLL reconstructs SYNC frequency and phase (thus sysclk)
36//!   w.r.t. batch and sample frequency and phase.
37//!   This determines the relation of the CPU 8 MHz crystal (thus CPU
38//!   clock and timers) to the DDS clock (derived from an external reference
39//!   frequency or internal XCO). See [idsp::PLL].
40//!
41//! ## Signal path
42//!
43//! * RF signal enters Pounder at Pounder IN0
44//! * Adjustable attenuation `demod_att`.
45//! * 30 dB gain block
46//! * Mixing with DDS at `demod_freq`
47//! * RC lowpass and amplification to reject unwanted demodulation products and
48//!   harmonics
49//! * IF signal enters Stabilizer and is available at ADC0 for analog monitoring
50//! * 2x PGIA and AA filter on Stabilizer
51//! * ADC digitization at 1/1.28 µs interval
52//! * Data processing in batches of 8 samples
53//! * Digital mixing with the reconstructed sample phase (PLL path). See [idsp::Lockin].
54//! * Lowpass filtering with a second order (12 dB/octave)
55//!   IIR lowpass with an additional double zero at Nyquist. Adjustable corner frequency.
56//!   See [idsp::Lowpass]
57//! * Full rate baseband demodulated data (quadrature only) on DAC0
58//! * Lowpass filtering with a batch-size boxcar FIR filter (zeros at n/4 Nyquist)
59//! * Computation of signal power and phase. See [idsp::ComplexExt].
60//! * Fractional rescaling (`phase_scale`) and unwrapping of the phase with 32 bit turn range.
61//! * Scaling and clamping.
62//! * Filtering by a second order (biquad) IIR filter (supporting e.g. II, I, P
63//!   action). See [idsp::iir].
64//! * Clamping, output offset, and anti-windup. See [idsp::iir].
65//! * Feedback onto a frequency offset of the modulation DDS at `mod_freq`
66//! * Additional feedback path from the phase before unwrapping onto the
67//!   modulation DDS phase offset with an adjustable gain `pow_gain`
68//! * Adjustable DDS output amplitude and blanking on digital input
69//! * Adjustable modulation attenuation `mod_att`
70//! * Modulation output at Pounder OUT0
71//!
72//! # Telemetry
73//! Data is regularly published via MQTT. See [Telemetry].
74//!
75//! # Streaming
76//! Full-rate ADC and DAC data is available via configurable UDP data streaming.
77//! See [stream]. To view and analyze noise spectra the graphical application
78//! [`stabilizer-stream`](https://github.com/quartiq/stabilizer-stream) can be used.
79
80use ad9959::Acr;
81use arbitrary_int::{u14, u24};
82use idsp::{
83    Accu, Complex, ComplexExt, Filter, Lockin, Lowpass, PLL, Unwrapper, iir,
84};
85use miniconf::Tree;
86use platform::NetSettings;
87use serde::{Deserialize, Serialize};
88use stabilizer::{
89    convert::{DacCode, Gain},
90    statistics,
91};
92
93/// Sample and batch period configuration.
94/// Note that both `SAMPLE_TICKS_LOG2` and `BATCH_SIZE_LOG2` are implicitly used in the
95/// lockin harmonic computation below. Do not change them without accounting for that.
96const SAMPLE_TICKS_LOG2: u32 = 7;
97const BATCH_SIZE_LOG2: usize = 3;
98
99/// ADC and DAC sample rate in timer cycles. One timer cycle at 100 MHz is 10 ns.
100const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2; // 1.28 µs
101
102/// ADC/DAC Samples per batch. The [app::process] routine is invoked once per batch period
103/// and has access to the two (both channels) filled buffers of ADC samples from the
104/// previous batch period and to the two to-be-filled buffers of DAC samples that will
105/// be emitted in the next batch period.
106const BATCH_SIZE: usize = 1 << BATCH_SIZE_LOG2;
107
108// Delta FTW between the two DDS: DF
109// Timestamp counter wrap period in DDS clock cycles:
110// 1 << (2 (dds SYNC prescaler) + 2 (timer prescaler) + 16 (timer counter width))
111// Lockin demodulation period in DDS clock cycles: (1 << 32) / DF
112// Counter capture period in samples (also batch size): 1 << 3
113//
114// DDS clock interval t_dds = 2 ns
115// Lockin period
116// t_lo = 1/(f_b - f_a) = (1 << 32)*t_dds/DF
117// SYNC interval
118// t_sync = t_dds*psc_dds*psc_tim
119// CPU timer clock interval:
120// t_cpu = 10 ns
121// Batch interval:
122// t_batch = t_cpu*128*8
123// Timestamper increment:
124// dt_sync = t_batch/t_sync = t_cpu*128*8/(t_dds*4*4) = t_cpu/t_dds*64
125// Sample interval
126// t_sample = t_batch/n_batch = dt_sync*t_sync/n_batch = dt_sync*t_dds*2
127// Sample phase increment
128// dp_sample = t_sample/t_lo*(1 << 32) = dt_sync*2*DF
129// Ratio between sample phase increment and timestamper increment
130// harmonic_sample = dp_sample/dt_sync = DF << 1
131
132// Scaling factor (harmonic) to convert PLL frequency to lockin LO frequency.
133const MULT_SHIFT: u32 = 2 + 2 + 14 - BATCH_SIZE_LOG2 as u32;
134
135// Phase scale for fine phase offset, such that 1 is one DDS LSB.
136const PHASE_SCALE_SHIFT: u32 = 12;
137
138// Default modulation/demodulation frequency for characterization.
139// High CTZ has fewest DDS phase truncation spurs. Near 160 MHz.
140const F_DEMOD: u32 = 0x5200_0000;
141
142#[derive(Clone, Debug, Tree)]
143pub struct BiquadRepr<T>
144where
145    T: idsp::Coefficient
146        + num_traits::AsPrimitive<f32>
147        + num_traits::AsPrimitive<T>,
148    f32: num_traits::AsPrimitive<T>,
149{
150    // Order matters
151    /// Biquad representation type
152    #[tree(rename="typ", typ="&str", with=miniconf::str_leaf, defer=self.repr)]
153    _typ: (),
154    /// Biquad parameters
155    /// Biquad representation subtree access
156    repr: iir::BiquadRepr<f32, T>,
157    /// Update trigger. TODO: Needs explicit trigger for serial-settings
158    #[tree(rename="update", with=biquad_update, defer=*self)]
159    _update: (),
160    /// Built raw IIR
161    #[tree(skip)]
162    iir: iir::Biquad<T>,
163    #[tree(skip)]
164    period: f32,
165    #[tree(skip)]
166    b_scale: f32,
167    #[tree(skip)]
168    y_scale: f32,
169}
170
171mod biquad_update {
172    use super::BiquadRepr;
173    use miniconf::{Keys, SerdeError, leaf};
174    pub use miniconf::{
175        deny::{mut_any_by_key, ref_any_by_key},
176        leaf::SCHEMA,
177    };
178    use serde::{Deserialize, Deserializer, Serializer};
179
180    pub fn serialize_by_key<S, T>(
181        _value: &BiquadRepr<T>,
182        keys: impl Keys,
183        ser: S,
184    ) -> Result<S::Ok, SerdeError<S::Error>>
185    where
186        S: Serializer,
187        T: idsp::Coefficient
188            + num_traits::AsPrimitive<f32>
189            + num_traits::AsPrimitive<T>,
190        f32: num_traits::AsPrimitive<T>,
191    {
192        leaf::serialize_by_key(&(), keys, ser)
193    }
194
195    pub fn deserialize_by_key<'de, D, T>(
196        value: &mut BiquadRepr<T>,
197        keys: impl Keys,
198        de: D,
199    ) -> Result<(), SerdeError<D::Error>>
200    where
201        D: Deserializer<'de>,
202        T: idsp::Coefficient
203            + num_traits::AsPrimitive<f32>
204            + num_traits::AsPrimitive<T>,
205        f32: num_traits::AsPrimitive<T>,
206    {
207        leaf::deserialize_by_key(&mut (), keys, de)?;
208        value.iir =
209            value
210                .repr
211                .build::<f32>(value.period, value.b_scale, value.y_scale);
212        Ok(())
213    }
214
215    #[allow(clippy::extra_unused_type_parameters)]
216    pub fn probe_by_key<'de, T, D>(
217        keys: impl Keys,
218        de: D,
219    ) -> Result<(), SerdeError<D::Error>>
220    where
221        T: Deserialize<'de>,
222        D: Deserializer<'de>,
223    {
224        leaf::probe_by_key::<'_, T, _>(keys, de)
225    }
226}
227
228impl<T> Default for BiquadRepr<T>
229where
230    T: idsp::Coefficient
231        + num_traits::AsPrimitive<f32>
232        + num_traits::AsPrimitive<T>,
233    f32: num_traits::AsPrimitive<T>,
234{
235    fn default() -> Self {
236        Self {
237            _typ: (),
238            repr: iir::BiquadRepr::Raw(iir::Biquad::IDENTITY),
239            _update: (),
240            iir: iir::Biquad::IDENTITY,
241            period: 1.0,
242            b_scale: 1.0,
243            y_scale: 1.0,
244        }
245    }
246}
247
248#[derive(Clone, Debug, Deserialize, Serialize, Tree)]
249struct DdsSettings {
250    /// RF output (modulation) or input (demodulation) offset frequency tuning word.
251    /// The DDS sample clock is nominally 500 MHz.
252    ///
253    /// # Value
254    /// Modulation/demodulation frequency tuning word (32 bit).
255    /// Range [0, 0xffff_ffff]
256    ///
257    /// # Default
258    /// A `0x5200_0000` tuning word corresponds to close to 160 MHz.
259    freq: u32,
260    /// Modulation/demodulation RF attenuation.
261    ///
262    /// # Value
263    /// Attenuation in dB, Range [0, 31.5]
264    ///
265    /// # Default
266    /// 6 dB output attenuation, 31.5 dB input attenuation
267    #[tree(with=validate_att)]
268    att: f32,
269    /// Modulation/demodulation phase offset.
270    ///
271    /// # Value
272    /// Phase offset in machine units (16 bit).
273    ///
274    /// # Default
275    /// 0
276    #[tree(with=miniconf::leaf)]
277    phase: u14,
278}
279
280mod validate_att {
281    use miniconf::ValueError;
282    pub use miniconf::{
283        Keys, SerdeError,
284        deny::mut_any_by_key,
285        leaf::{self, SCHEMA, probe_by_key, ref_any_by_key, serialize_by_key},
286    };
287    use serde::Deserializer;
288
289    pub fn deserialize_by_key<'de, D: Deserializer<'de>>(
290        value: &mut f32,
291        keys: impl Keys,
292        de: D,
293    ) -> Result<(), SerdeError<D::Error>> {
294        let mut att = *value;
295        leaf::deserialize_by_key(&mut att, keys, de)?;
296        if !stabilizer::convert::att_is_valid(att) {
297            Err(ValueError::Access("Attenuation out of range (0..=31.5 dB)")
298                .into())
299        } else {
300            *value = att;
301            Ok(())
302        }
303    }
304}
305
306type LockinLowpass = Lowpass<2>;
307
308#[derive(Clone, Debug, Tree)]
309struct ChannelSettings {
310    /// Input (demodulation) DDS settings
311    /// Feedback to stabilize the RF input phase is applied to the RF output
312    /// on top of the output frequency and phase.
313    ///
314    /// For the demodulation this is the total (DDS **minus** Lockin, i.e. lower sideband)
315    /// demodulation frequency. If the modulation AOM is passed twice at +1 order,
316    /// `input/freq` should be twice `output/freq`.
317    input: DdsSettings,
318    /// Output (modulation) DDS settings
319    output: DdsSettings,
320    /// Demodulation amplitude control register.
321    ///
322    /// # Value
323    /// AD9959 amplitude control register (24 bits, see datasheet)
324    ///
325    /// # Default
326    /// 0 for full scale amplitude and multiplier disable
327    #[tree(with=miniconf::leaf)]
328    amp: u24,
329    /// Lockin lowpass time constant. The lowpass is a cascade of one second order IIR
330    /// filters, 12 dB/octave.
331    /// This needs to be high enough to suppress the unwanted demodulation components
332    /// and harmonics but as low as possible to maximize bandwidth. Many demodulation
333    /// components and harmonics are also suppressed by the zeros of the batch size
334    /// moving average FIR filter and judicious choice of `lockin_freq`.
335    ///
336    /// TODO: settle pll and lockin settings into design after confirming optimal choice
337    ///
338    /// # Default
339    /// `lockin_k = [0x200_0000, -0x2000_0000]`
340    #[tree(with=miniconf::leaf)]
341    lockin_k: <LockinLowpass as Filter>::Config,
342    /// Minimum demodulated signal power to enable feedback.
343    /// Note that this is RMS and that the signal peak must not clip.
344    ///
345    /// # Value
346    /// `log2` of the signal power relative to full scale. Range: `[-63..0]`
347    ///
348    /// # Default
349    /// `min_power = -24` corresponding to about -69 dBFS.
350    min_power: i32,
351    /// Clear the phase unwrap tracking counters once.
352    /// To make this setting edge-sensitive, after setting it to `true`,
353    /// it must be reset to `false` by the user before setting any other settings.
354    clear: bool,
355    /// Scaling factor of the unwrapped phase and fine rational offset.
356    /// The phase scaling is located after the phase unwrapping before the feedback
357    /// IIR filter.
358    ///
359    /// FIXME: doc rational offset
360    ///
361    /// # Value
362    /// `[[phase_factor, phase_shr], [time_factor, time_shr]]`
363    ///
364    /// # Default
365    /// `phase_scale = [[1, 16], [0, 0]]`:
366    /// clamped range: ±33 k turn (tracked range is ±2 G turn)
367    /// quantization: 1.5 µ turn, 0.1 Hz
368    #[tree(with=phase_scale, defer=*self)]
369    phase_scale: [[i32; 2]; 2],
370    /// Feedback IIR filter settings. The filter input is phase, the output is frequency.
371    ///
372    /// # Default
373    /// A proportional gain=-1 filter.
374    iir: BiquadRepr<i32>,
375    /// Phase offset feedback gain.
376    /// Phase feedback is a proportional bypass of the unwrapper, the IIR
377    /// (including its input and output scaling) and the frequency feedback path.
378    /// The phase offset gain is `pow_gain/(1 << 13) rad/rad`.
379    ///
380    /// # Value
381    /// Integer scaled phase feedback gain. Range: `[-0x2000, 0x2000]`
382    ///
383    /// # Default
384    /// 0 for no phase feedback
385    pow_gain: i16,
386    /// Allow digital input to hold
387    hold_en: bool,
388    /// Amplitude IIR filter. The filter input is squared magnitude, the output is DDS amplitude.
389    ///
390    /// # Default
391    /// No feedback
392    iir_amp: BiquadRepr<f32>,
393}
394
395const DDS_LSB_PER_HZ: f32 = (1i64 << 32) as f32
396    / stabilizer::design_parameters::DDS_SYSTEM_CLK.to_Hz() as f32;
397
398impl Default for ChannelSettings {
399    fn default() -> Self {
400        let mut iir_prop = iir::Biquad::IDENTITY;
401        iir_prop.ba_mut()[0] *= -1;
402        iir_prop.set_min(-0x4_0000);
403        iir_prop.set_max(0x4_0000);
404        let mut iir_amp = iir::Biquad::default();
405        iir_amp.set_u(0x3ff as _);
406        iir_amp.set_min(0.0);
407        iir_amp.set_max(0x3ff as _);
408        let mut s = Self {
409            input: DdsSettings {
410                freq: F_DEMOD,
411                att: 31.5,
412                phase: u14::new(0),
413            },
414            output: DdsSettings {
415                freq: F_DEMOD,
416                att: 6.0,
417                phase: u14::new(0),
418            },
419            lockin_k: [-(i32::MIN >> 6), i32::MIN >> 2],
420            amp: u24::new(0),
421            min_power: -24,
422            clear: true,
423            phase_scale: [[1, 16], [0, 0]],
424            iir: BiquadRepr {
425                repr: iir::BiquadRepr::Raw(iir_prop.clone()),
426                iir: iir_prop,
427                period: stabilizer::design_parameters::TIMER_PERIOD
428                    * (SAMPLE_TICKS * BATCH_SIZE as u32) as f32,
429                y_scale: DDS_LSB_PER_HZ,
430                ..Default::default()
431            },
432            pow_gain: 0,
433            hold_en: false,
434            iir_amp: BiquadRepr {
435                repr: iir::BiquadRepr::Raw(iir_amp.clone()),
436                iir: iir_amp.clone(),
437                period: 10e-3,
438                b_scale: iir_amp.max(),
439                y_scale: iir_amp.max(),
440                ..Default::default()
441            },
442        };
443        s.update_phase_scale();
444        s
445    }
446}
447
448mod phase_scale {
449    use super::ChannelSettings;
450    pub use miniconf::{
451        Keys, SerdeError,
452        deny::{mut_any_by_key, ref_any_by_key},
453        leaf::{self, SCHEMA},
454    };
455    use serde::{Deserialize, Deserializer, Serializer};
456
457    pub fn serialize_by_key<S: Serializer>(
458        value: &ChannelSettings,
459        keys: impl Keys,
460        ser: S,
461    ) -> Result<S::Ok, SerdeError<S::Error>> {
462        leaf::serialize_by_key(&value.phase_scale, keys, ser)
463    }
464
465    pub fn deserialize_by_key<'de, D: Deserializer<'de>>(
466        value: &mut ChannelSettings,
467        keys: impl Keys,
468        de: D,
469    ) -> Result<(), SerdeError<D::Error>> {
470        leaf::deserialize_by_key(&mut value.phase_scale, keys, de)?;
471        value.update_phase_scale();
472        Ok(())
473    }
474
475    pub fn probe_by_key<'de, T: Deserialize<'de>, D: Deserializer<'de>>(
476        keys: impl Keys,
477        de: D,
478    ) -> Result<(), SerdeError<D::Error>> {
479        leaf::probe_by_key::<'de, T, _>(keys, de)
480    }
481}
482
483impl ChannelSettings {
484    fn update_phase_scale(&mut self) {
485        // Units: [x] = turns, [y] = Hz
486        // TODO: verify
487        let phase_lsb_per_turn =
488            (self.phase_scale[0][0] << (32 - self.phase_scale[0][1])) as f32;
489        self.iir.b_scale = DDS_LSB_PER_HZ / phase_lsb_per_turn;
490    }
491}
492
493/// Settings structure for the application.
494/// All fields in this structure are available through MQTT and can be configured at runtime.
495#[derive(Clone, Debug, Tree)]
496pub struct Fls {
497    /// Channel-specific settings.
498    ch: [ChannelSettings; 2],
499    /// External reference
500    ///
501    /// # Value
502    /// `true` for external 100 MHz reference input selected,
503    /// `false` for internal 100 MHz XO enabled and selected
504    ///
505    /// # Default
506    /// `false`
507    ext_clk: bool,
508    /// Lockin local oscillator frequency tuning word. Common to both demodulation/input
509    /// channels.
510    ///
511    /// The demodulation DDS frequency tuning word
512    /// is `/ch/+/input/freq + lockin_freq*0x8000` (lower sideband).
513    ///
514    /// TODO: settle pll and lockin settings into design after confirming optimal choice
515    ///
516    /// # Default
517    /// `0x40` corresponding to 244 kHz. 5/8 Nyquist.
518    lockin_freq: u32,
519    /// Lockin demodulation oscillator PLL bandwidth.
520    /// This PLL reconstructs the DDS SYNC clock output on the CPU clock timescale.
521    ///
522    /// TODO: settle pll and lockin settings into design after confirming optimal choice
523    ///
524    /// # Default
525    /// `/pll_k = 0x4_0000` corresponds to to a time constant of about 0.4 s.
526    pll_k: i32,
527    /// Telemetry output period in seconds
528    ///
529    /// # Default
530    /// 2 second interval
531    telemetry_period: u16,
532    /// Target for data streaming
533    ///
534    /// # Default
535    /// Streaming disabled
536    #[tree(with=miniconf::leaf)]
537    stream: stream::Target,
538}
539
540impl Default for Fls {
541    fn default() -> Self {
542        Self {
543            ch: Default::default(),
544            ext_clk: false,
545            lockin_freq: 0x40,
546            pll_k: 0x4_0000,
547            telemetry_period: 10,
548            stream: Default::default(),
549        }
550    }
551}
552
553#[derive(Clone, Debug, Tree, Default)]
554pub struct Settings {
555    pub fls: Fls,
556
557    pub net: NetSettings,
558}
559
560impl platform::AppSettings for Settings {
561    fn new(net: NetSettings) -> Self {
562        Self {
563            net,
564            fls: Default::default(),
565        }
566    }
567
568    fn net(&self) -> &NetSettings {
569        &self.net
570    }
571}
572
573impl serial_settings::Settings for Settings {
574    fn reset(&mut self) {
575        *self = Self {
576            fls: Default::default(),
577            net: NetSettings::new(self.net.mac),
578        }
579    }
580}
581
582/// Stream data format.
583#[derive(
584    Clone, Copy, Debug, Default, Serialize, bytemuck::Zeroable, bytemuck::Pod,
585)]
586#[repr(C)]
587struct Stream {
588    /// Demodulated signal.  `-1 << 31` corresponds to negative full scale.
589    demod: Complex<i32>,
590    /// Current number of phase wraps. In units of turns.
591    phase: [i32; 2],
592    /// Current frequency tuning word added to the configured modulation
593    /// offset `mod_freq`.
594    delta_ftw: i32,
595    /// Current phase offset word applied to the modulation DDS.
596    delta_pow: i16,
597    /// Modulation DDS amplitude word
598    mod_amp: u16,
599    /// PLL time
600    pll: u32,
601}
602
603/// Channel Telemetry
604#[derive(Default, Clone, Serialize)]
605struct ChannelTelemetry {
606    /// Current phase. Offset and scaled.
607    phase: i64,
608    /// Power estimate, `|demod|²` re full scale.
609    power_log: i32,
610    ///
611    // power: i32,
612    /// Auxiliary front panel ADC input values, undersmpled
613    aux_adc: f32,
614    mod_amp: u16,
615    /// Number of sampler where digital input signal was high.
616    holds: u32,
617    /// Number of potential phase slips where the absolute
618    /// phase difference between successive samples is larger than π/2.
619    slips: u32,
620    /// Counter for the number of samples with low power.
621    blanks: u32,
622}
623
624#[derive(Default, Clone)]
625pub struct Telemetry {
626    pll_time: i64,
627    ch: [ChannelTelemetry; 2],
628    stats: [statistics::State; 2],
629}
630
631/// Telemetry structure.
632/// This structure is published via MQTT at the `telemetry_interval` configured in
633/// [Settings].
634/// There is no dedicated AA filtering for telemetry data (except for `stats`),
635/// it is just decimated by the telemetry interval. Use streaming for full
636/// bandwidth data.
637#[derive(Default, Clone, Serialize)]
638pub struct CookedTelemetry {
639    /// PLL time
640    /// DDS PLL time as seen by CPU (sample) clock.
641    /// Settles increments of approximately `0x140_0000`.
642    pll_time: i64,
643    /// Statistics of scaled (settings.phase_scale) phase including wraps.
644    /// Phase statistics state. Each message corresponds to the statistics of the
645    /// phase data since the last message.
646    phase: [statistics::ScaledStatistics; 2],
647    /// RF power in dBm as reported by the RF detector and ADC. Functionality
648    /// limited. <https://github.com/sinara-hw/Pounder/issues/95>
649    rf_power: [f32; 2],
650    /// Raw (binary) channel telemetry, mostly "stateful"
651    raw: [ChannelTelemetry; 2],
652    /// Channel frequency estimate (PI counter between telemetry messages)
653    /// TODO: deprecate
654    ch_freq: [f64; 2],
655    /// Pounder board temperature
656    temp: f32,
657}
658
659#[derive(Clone, Default)]
660pub struct ChannelState {
661    lockin: Lockin<LockinLowpass>,
662    x0: i32,
663    t0: i32,
664    t: i64,
665    y: i64,
666    unwrapper: Unwrapper<i64>,
667    iir: [i32; 5],
668    iir_amp: [f32; 4],
669    hold: bool,
670}
671
672#[cfg(not(target_os = "none"))]
673fn main() {
674    use miniconf::{json::to_json_value, json_schema::TreeJsonSchema};
675    let s = Settings::default();
676    println!(
677        "{}",
678        serde_json::to_string_pretty(&to_json_value(&s).unwrap()).unwrap()
679    );
680    let mut schema = TreeJsonSchema::new(Some(&s)).unwrap();
681    schema
682        .root
683        .insert("title".to_string(), "Stabilizer fls".into());
684    println!("{}", serde_json::to_string_pretty(&schema.root).unwrap());
685}
686
687#[cfg(target_os = "none")]
688#[cfg_attr(target_os = "none", rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, LTDC, SDMMC]))]
689mod app {
690    use arbitrary_int::u10;
691    use core::sync::atomic::{Ordering, fence};
692    use fugit::ExtU32 as _;
693    use rtic_monotonics::Monotonic;
694
695    use stabilizer::hardware::{
696        self,
697        DigitalInput0,
698        DigitalInput1,
699        SerialTerminal,
700        SystemTimer,
701        Systick,
702        UsbDevice,
703        adc::{Adc0Input, Adc1Input},
704        dac::{Dac0Output, Dac1Output},
705        hal,
706        net::{NetworkState, NetworkUsers},
707        // afe::Gain,
708        pounder::{
709            Channel, PounderDevices, dds_output::DdsOutput,
710            timestamp::Timestamper,
711        },
712        timers::SamplingTimer,
713    };
714
715    use stream::FrameGenerator;
716
717    use super::*;
718
719    #[shared]
720    struct Shared {
721        usb: UsbDevice,
722        network: NetworkUsers<Fls>,
723        active_settings: Fls,
724        settings: Settings,
725        telemetry: Telemetry,
726        dds_output: DdsOutput,
727        pounder: PounderDevices,
728        state: [ChannelState; 2],
729    }
730
731    #[local]
732    struct Local {
733        usb_terminal: SerialTerminal<Settings>,
734        sampling_timer: SamplingTimer,
735        digital_inputs: (DigitalInput0, DigitalInput1),
736        adcs: (Adc0Input, Adc1Input),
737        dacs: (Dac0Output, Dac1Output),
738        generator: FrameGenerator,
739        timestamper: Timestamper,
740        stream: [Stream; 2],
741        tele_state: [i64; 3],
742        pll: PLL,
743    }
744
745    #[init]
746    fn init(c: init::Context) -> (Shared, Local) {
747        let clock = SystemTimer::new(|| Systick::now().ticks());
748
749        // Configure the microcontroller
750        let (mut carrier, mezzanine, _eem) = hardware::setup::setup::<Settings>(
751            c.core,
752            c.device,
753            clock,
754            BATCH_SIZE,
755            SAMPLE_TICKS,
756        );
757
758        let mut network = NetworkUsers::new(
759            carrier.network_devices.stack,
760            carrier.network_devices.phy,
761            clock,
762            env!("CARGO_BIN_NAME"),
763            &carrier.settings.net,
764            carrier.metadata,
765        );
766
767        let generator = network.configure_streaming(stream::Format::Fls);
768
769        // ADC0 full scale 5V
770        carrier.afes[0].set_gain(Gain::G2);
771        carrier.afes[1].set_gain(Gain::G2);
772
773        let hardware::setup::Mezzanine::Pounder(mut pounder) = mezzanine else {
774            panic!("Missing Pounder Mezzanine");
775        };
776        pounder.timestamper.start();
777
778        // Enable ADC/DAC events
779        carrier.adcs.0.start();
780        carrier.adcs.1.start();
781        carrier.dacs.0.start();
782        carrier.dacs.1.start();
783
784        let shared = Shared {
785            usb: carrier.usb,
786            network,
787            telemetry: Telemetry::default(),
788            active_settings: carrier.settings.fls.clone(),
789            settings: carrier.settings,
790            dds_output: pounder.dds_output,
791            pounder: pounder.pounder,
792            state: Default::default(),
793        };
794
795        let local = Local {
796            usb_terminal: carrier.usb_serial,
797            sampling_timer: carrier.sampling_timer,
798            digital_inputs: carrier.digital_inputs,
799            adcs: carrier.adcs,
800            dacs: carrier.dacs,
801            generator,
802            timestamper: pounder.timestamper,
803            stream: Default::default(),
804            tele_state: [0; 3],
805            pll: PLL::default(),
806        };
807
808        settings_update::spawn().unwrap();
809        telemetry::spawn().unwrap();
810        aux_adc::spawn().unwrap();
811        usb::spawn().unwrap();
812        ethernet_link::spawn().unwrap();
813        start::spawn().unwrap();
814
815        (shared, local)
816    }
817
818    #[task(priority = 1, local = [sampling_timer])]
819    async fn start(c: start::Context) {
820        Systick::delay(200.millis()).await;
821        c.local.sampling_timer.start();
822    }
823
824    /// Main DSP processing routine.
825    ///
826    /// See `dual-iir` for general notes on processing time and timing.
827    ///
828    /// This is an implementation of fiber length stabilization using super-heterodyne
829    /// (pounder + lockin) and digital feedback to a DDS.
830    #[task(binds = DMA1_STR4, local=[timestamper, adcs, dacs, generator, digital_inputs, stream, pll], shared = [active_settings, state, telemetry, dds_output], priority = 3)]
831    #[unsafe(link_section = ".itcm.process")]
832    fn process(c: process::Context) {
833        let process::LocalResources {
834            adcs: (adc0, adc1),
835            dacs: (dac0, dac1),
836            digital_inputs,
837            timestamper,
838            generator,
839            stream,
840            pll,
841            ..
842        } = c.local;
843
844        // A counter running at a fourth of the DDS SYNC interval is captured by
845        // the overflow of a timer synchronized to the sampling timer (locked to the
846        // CPU clock and the other CPU timer clocks).
847        // Captured timestamps are about 0x140 counts apart between batches.
848        // They determine the phase and period of the DDS clock (driving the counter)
849        // in terms of the CPU clock (driving the capture).
850        // Discard double captures (overcaptures) and extrapolate.
851        // Extrapolate on no capture (undercapture).
852        let timestamp = timestamper
853            .latest_timestamp()
854            .unwrap_or(None)
855            .map(|t| ((t as u32) << 16) as i32);
856
857        (
858            c.shared.state,
859            c.shared.active_settings,
860            c.shared.dds_output,
861            c.shared.telemetry,
862        )
863            .lock(|state, settings, dds_output, telemetry| {
864                // Reconstruct frequency and phase using a lowpass that is aware of phase and frequency
865                // wraps.
866                pll.update(timestamp, settings.pll_k);
867                // TODO: implement clear
868                stream[0].pll = pll.frequency() as _;
869                stream[1].pll = pll.phase() as _;
870                telemetry.pll_time =
871                    telemetry.pll_time.wrapping_add(pll.frequency() as _);
872
873                let mut demod = [Complex::<i32>::default(); BATCH_SIZE];
874                // TODO: fixed lockin_freq, const 5/16 frequency table (80 entries), then rotate each by pll phase
875                for (d, p) in demod.iter_mut().zip(Accu::new(
876                    (pll.phase() << BATCH_SIZE_LOG2)
877                        .wrapping_mul(settings.lockin_freq as _),
878                    pll.frequency().wrapping_mul(settings.lockin_freq as _),
879                )) {
880                    *d = Complex::from_angle(p);
881                }
882
883                (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| {
884                    fence(Ordering::SeqCst);
885                    let adc: [&[u16; BATCH_SIZE]; 2] = [
886                        (**adc0).try_into().unwrap(),
887                        (**adc1).try_into().unwrap(),
888                    ];
889                    let dac: [&mut [u16; BATCH_SIZE]; 2] = [
890                        (*dac0).try_into().unwrap(),
891                        (*dac1).try_into().unwrap(),
892                    ];
893                    // Perform lockin demodulation of the ADC samples in the batch.
894                    for ((((adc, dac), state), settings), stream) in adc
895                        .into_iter()
896                        .zip(dac.into_iter())
897                        .zip(state.iter_mut())
898                        .zip(settings.ch.iter())
899                        .zip(stream.iter_mut())
900                    {
901                        stream.demod = adc
902                            .iter()
903                            .zip(dac.iter_mut())
904                            .zip(demod.iter())
905                            .map(|((a, d), p)| {
906                                // Demodulate the ADC sample `a0` with the sample's phase `p` and
907                                // filter it with the lowpass.
908                                // zero(s) at fs/2 (Nyquist) by lowpass
909                                let y = state.lockin.update_iq(
910                                    // 3 bit headroom for coeff sum minus one bit gain for filter
911                                    (*a as i16 as i32) << 14,
912                                    *p,
913                                    &settings.lockin_k,
914                                );
915                                // Convert quadrature demodulated output to DAC data for monitoring
916                                *d = DacCode::from((y.im >> 13) as i16).0;
917                                y
918                            })
919                            // Add more zeros at fs/2, fs/4, and fs/8 by rectangular window.
920                            // Sum up all demodulated samples in the batch. Corresponds to a boxcar
921                            // averager with sinc frequency response. The first 15 lockin harmonics end up
922                            // in zeros of the filter.
923                            .sum();
924                    }
925                    fence(Ordering::SeqCst);
926                });
927                let di =
928                    [digital_inputs.0.is_high(), digital_inputs.1.is_high()];
929                // TODO: pll.frequency()?
930                let time = pll.phase() & (-1 << PHASE_SCALE_SHIFT);
931                let dtime = time.wrapping_sub(state[0].t0) >> PHASE_SCALE_SHIFT;
932                state[0].t0 = time;
933                state[1].t0 = time;
934
935                let mut builder = dds_output.builder();
936                for (
937                    (((((idx, di), settings), state), telemetry), stream),
938                    stats,
939                ) in [Channel::Out0, Channel::Out1]
940                    .into_iter()
941                    .zip(di)
942                    .zip(settings.ch.iter_mut())
943                    .zip(state.iter_mut())
944                    .zip(telemetry.ch.iter_mut())
945                    .zip(stream.iter_mut())
946                    .zip(telemetry.stats.iter_mut())
947                {
948                    state.hold = settings.hold_en && di;
949                    if state.hold {
950                        telemetry.holds = telemetry.holds.wrapping_add(1);
951                    }
952
953                    if settings.clear {
954                        state.unwrapper = Unwrapper::default();
955                        state.t = 0;
956                        state.y = 0;
957                        settings.clear = false;
958                    }
959
960                    let power = stream.demod.log2();
961                    telemetry.power_log = power;
962                    let blank = power < settings.min_power;
963
964                    if blank {
965                        telemetry.blanks = telemetry.blanks.wrapping_add(1);
966                    }
967
968                    // Perform unwrapping, phase scaling, IIR filtering and FTW scaling.
969                    let (delta_ftw, delta_pow) = if blank || state.hold {
970                        // TODO: Unclear whether feeding zero error into the IIR or holding its output
971                        // is more correct. Also unclear what the frequency and phase should do.
972                        // telemetry.ch[0].dphase = 0;
973                        (0, stream.delta_pow)
974                    } else {
975                        let phase = stream.demod.arg();
976                        let dphase = phase.wrapping_sub(state.x0);
977                        state.x0 = phase;
978
979                        // |dphi| > pi/2 indicates a possible undetected phase slip.
980                        // Neither a necessary nor a sufficient condition though.
981                        if dphase.wrapping_add(1 << 30) < 0 {
982                            telemetry.slips = telemetry.slips.wrapping_add(1);
983                        }
984
985                        // Scale, offset and unwrap phase
986                        state.t = state.t.wrapping_add(
987                            dtime as i64 * settings.phase_scale[1][0] as i64,
988                        );
989                        state.y = state.y.wrapping_add(
990                            dphase as i64 * settings.phase_scale[0][0] as i64,
991                        );
992                        state.unwrapper.update(
993                            ((state.y >> settings.phase_scale[0][1]) as i32)
994                                .wrapping_add(
995                                    (state.t >> settings.phase_scale[1][1])
996                                        as i32,
997                                ),
998                        );
999
1000                        stream.phase = bytemuck::cast(state.unwrapper.y());
1001                        telemetry.phase = state.unwrapper.y();
1002                        let phase_err = state
1003                            .unwrapper
1004                            .y()
1005                            .clamp(-i32::MAX as _, i32::MAX as _)
1006                            as _;
1007
1008                        stats.update(phase_err);
1009                        // TODO; unchecked_shr
1010                        let delta_ftw =
1011                            settings.iir.iir.update(&mut state.iir, phase_err);
1012                        let delta_pow = ((phase_err >> 16)
1013                            .wrapping_mul(settings.pow_gain as _)
1014                            >> 16) as _;
1015                        (delta_ftw, delta_pow)
1016                    };
1017                    stream.delta_ftw = delta_ftw;
1018                    stream.delta_pow = delta_pow; // note the u14 wrap below
1019
1020                    // let power = (((stream.demod.re as i64).pow(2)
1021                    //     + (stream.demod.im as i64).pow(2))
1022                    //     >> 32) as i32;
1023
1024                    builder.push(
1025                        idx.into(),
1026                        Some(settings.output.freq.wrapping_add(delta_ftw as _)),
1027                        Some(u14::new(
1028                            settings
1029                                .output
1030                                .phase
1031                                .value()
1032                                .wrapping_add(delta_pow as _)
1033                                & u14::MASK,
1034                        )),
1035                        Some(
1036                            Acr::DEFAULT
1037                                .with_asf(u10::new(
1038                                    telemetry.mod_amp.clamp(0, 0x3ff),
1039                                ))
1040                                .with_multiplier(true),
1041                        ),
1042                    );
1043                }
1044                dds_output.write(builder);
1045
1046                const N: usize = core::mem::size_of::<[Stream; 2]>();
1047                generator.add(|buf| {
1048                    buf[..N].copy_from_slice(bytemuck::cast_slice(stream));
1049                    N
1050                });
1051            });
1052    }
1053
1054    #[idle(shared=[network, usb, settings])]
1055    fn idle(mut c: idle::Context) -> ! {
1056        loop {
1057            match (&mut c.shared.network, &mut c.shared.settings)
1058                .lock(|net, settings| net.update(&mut settings.fls))
1059            {
1060                NetworkState::SettingsChanged => {
1061                    settings_update::spawn().unwrap()
1062                }
1063                NetworkState::Updated => {}
1064                NetworkState::NoChange => {
1065                    // We can't sleep if USB is not in suspend.
1066                    if c.shared.usb.lock(|usb| {
1067                        usb.state()
1068                            == usb_device::device::UsbDeviceState::Suspend
1069                    }) {
1070                        cortex_m::asm::wfi();
1071                    }
1072                }
1073            }
1074        }
1075    }
1076
1077    #[task(priority = 1, shared=[network, settings, dds_output, pounder, active_settings])]
1078    async fn settings_update(mut c: settings_update::Context) {
1079        c.shared.settings.lock(|settings| {
1080            c.shared.pounder.lock(|p| {
1081                for (ch, att) in [
1082                    (Channel::In0, settings.fls.ch[0].input.att),
1083                    (Channel::Out0, settings.fls.ch[0].output.att),
1084                    (Channel::In1, settings.fls.ch[1].input.att),
1085                    (Channel::Out1, settings.fls.ch[1].output.att),
1086                ]
1087                .into_iter()
1088                {
1089                    p.set_attenuation(ch, att).unwrap();
1090                }
1091                p.set_ext_clk(settings.fls.ext_clk).unwrap();
1092            });
1093
1094            c.shared.dds_output.lock(|dds_output| {
1095                let mut builder = dds_output.builder();
1096                builder.push(
1097                    Channel::In0.into(),
1098                    Some(
1099                        settings.fls.ch[0].input.freq.wrapping_add(
1100                            settings.fls.lockin_freq << MULT_SHIFT,
1101                        ),
1102                    ),
1103                    Some(settings.fls.ch[0].input.phase),
1104                    Some(Acr::new_with_raw_value(settings.fls.ch[0].amp)),
1105                );
1106                builder.push(
1107                    Channel::In1.into(),
1108                    Some(
1109                        settings.fls.ch[1].input.freq.wrapping_add(
1110                            settings.fls.lockin_freq << MULT_SHIFT,
1111                        ),
1112                    ),
1113                    Some(settings.fls.ch[1].input.phase),
1114                    Some(Acr::new_with_raw_value(settings.fls.ch[1].amp)),
1115                );
1116                dds_output.write(builder);
1117            });
1118            c.shared
1119                .network
1120                .lock(|net| net.direct_stream(settings.fls.stream));
1121            c.shared
1122                .active_settings
1123                .lock(|current| *current = settings.fls.clone());
1124        });
1125    }
1126
1127    #[task(priority = 1, shared=[pounder, telemetry, settings, state])]
1128    async fn aux_adc(mut c: aux_adc::Context) -> ! {
1129        loop {
1130            let aux_adc::SharedResources {
1131                settings,
1132                state,
1133                telemetry,
1134                pounder,
1135                ..
1136            } = &mut c.shared;
1137            let x = pounder.lock(|p| {
1138                [
1139                    p.sample_aux_adc(Channel::In0).unwrap(),
1140                    p.sample_aux_adc(Channel::In1).unwrap(),
1141                ]
1142            });
1143            let mut y = [0; 2];
1144            (settings, state).lock(|s, c| {
1145                for (((s, c), x), y) in s
1146                    .fls
1147                    .ch
1148                    .iter()
1149                    .zip(c.iter_mut())
1150                    .zip(x.iter())
1151                    .zip(y.iter_mut())
1152                {
1153                    *y = if c.hold {
1154                        iir::Biquad::HOLD.update(&mut c.iir_amp, *x);
1155                        0
1156                    } else {
1157                        s.iir_amp.iir.update(&mut c.iir_amp, *x) as u16
1158                    };
1159                }
1160            });
1161            telemetry.lock(|t| {
1162                t.ch[0].aux_adc = x[0];
1163                t.ch[0].mod_amp = y[0];
1164                t.ch[1].aux_adc = x[1];
1165                t.ch[1].mod_amp = y[1];
1166            });
1167            Systick::delay(10.millis()).await;
1168        }
1169    }
1170
1171    #[task(priority = 1, local=[tele_state], shared=[network, settings, telemetry, pounder])]
1172    async fn telemetry(mut c: telemetry::Context) -> ! {
1173        loop {
1174            let (raw, stats) = c
1175                .shared
1176                .telemetry
1177                .lock(|t| (t.clone(), core::mem::take(&mut t.stats)));
1178
1179            let (phase_scale, freq) = c.shared.settings.lock(|s| {
1180                (
1181                    [s.fls.ch[0].phase_scale, s.fls.ch[1].phase_scale],
1182                    [s.fls.ch[0].input.freq, s.fls.ch[1].input.freq],
1183                )
1184            });
1185            let scales = [
1186                [
1187                    (1i64 << phase_scale[0][0][1]) as f64
1188                        / phase_scale[0][0][0] as f64,
1189                    phase_scale[0][1][0] as f64
1190                        / ((1 << PHASE_SCALE_SHIFT) as f64
1191                            * (1i64 << phase_scale[0][1][1]) as f64),
1192                ],
1193                [
1194                    (1i64 << phase_scale[1][0][1]) as f64
1195                        / phase_scale[1][0][0] as f64,
1196                    phase_scale[1][1][0] as f64
1197                        / ((1 << PHASE_SCALE_SHIFT) as f64
1198                            * (1i64 << phase_scale[1][1][1]) as f64),
1199                ],
1200            ];
1201            const FDDS: f64 = 1.0 / DDS_LSB_PER_HZ as f64;
1202            const FLI: f64 = 0x1000 as f64 * FDDS;
1203            let pll_tau =
1204                1.0 / raw.pll_time.wrapping_sub(c.local.tele_state[2]) as f64;
1205            *c.local.tele_state =
1206                [raw.ch[0].phase, raw.ch[1].phase, raw.pll_time];
1207
1208            let mut tele = CookedTelemetry {
1209                pll_time: raw.pll_time,
1210                phase: [
1211                    stats[0]
1212                        .get_scaled(scales[0][0] as f32 / (1i64 << 32) as f32),
1213                    stats[1]
1214                        .get_scaled(scales[1][0] as f32 / (1i64 << 32) as f32),
1215                ],
1216                ch_freq: [
1217                    freq[0] as f64 * FDDS
1218                        + (raw.ch[0].phase.wrapping_sub(c.local.tele_state[0])
1219                            as f64
1220                            * pll_tau
1221                            - scales[0][1])
1222                            * scales[0][0]
1223                            * FLI,
1224                    freq[1] as f64 * FDDS
1225                        + (raw.ch[1].phase.wrapping_sub(c.local.tele_state[1])
1226                            as f64
1227                            * pll_tau
1228                            - scales[1][1])
1229                            * scales[1][0]
1230                            * FLI,
1231                ],
1232                raw: raw.ch,
1233                ..Default::default()
1234            };
1235
1236            c.shared.pounder.lock(|p| {
1237                tele.rf_power = [
1238                    p.measure_power(Channel::In0).unwrap(),
1239                    p.measure_power(Channel::In1).unwrap(),
1240                ];
1241                tele.temp = p.temperature().unwrap();
1242            });
1243            c.shared.network.lock(|net| {
1244                net.telemetry.publish_telemetry("/telemetry", &tele)
1245            });
1246
1247            let telemetry_period =
1248                c.shared.settings.lock(|s| s.fls.telemetry_period);
1249            // Schedule the telemetry task in the future.
1250            Systick::delay((telemetry_period as u32).secs()).await;
1251        }
1252    }
1253
1254    #[task(priority = 1, shared=[usb, settings], local=[usb_terminal])]
1255    async fn usb(mut c: usb::Context) -> ! {
1256        loop {
1257            // Handle the USB serial terminal.
1258            c.shared.usb.lock(|usb| {
1259                usb.poll(&mut [c
1260                    .local
1261                    .usb_terminal
1262                    .interface_mut()
1263                    .inner_mut()]);
1264            });
1265
1266            c.shared
1267                .settings
1268                .lock(|settings| c.local.usb_terminal.poll(settings).unwrap());
1269
1270            // Schedule to run this task every 10 milliseconds.
1271            Systick::delay(10.millis()).await;
1272        }
1273    }
1274
1275    #[task(priority = 1, shared=[network])]
1276    async fn ethernet_link(mut c: ethernet_link::Context) -> ! {
1277        loop {
1278            c.shared.network.lock(|net| net.processor.handle_link());
1279            Systick::delay(1.secs()).await;
1280        }
1281    }
1282
1283    #[task(binds = ETH, priority = 1)]
1284    fn eth(_: eth::Context) {
1285        unsafe { hal::ethernet::interrupt_handler() }
1286    }
1287}