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}