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}