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