lockin/
lockin.rs

1//! # Lockin
2//!
3//! The `lockin` application implements a lock-in amplifier using either an external or internally
4//! generated reference.
5//!
6//! ## Features
7//! * Up to 800 kHz sampling
8//! * Up to 400 kHz modulation frequency
9//! * Supports internal and external reference sources:
10//!     1. Internal: Generate reference internally and output on one of the channel outputs
11//!     2. External: Reciprocal PLL, reference input applied to DI0.
12//! * Adjustable PLL and locking time constants
13//! * Adjustable phase offset and harmonic index
14//! * Run-time configurable output modes (in-phase, quadrature, magnitude, log2 power, phase, frequency)
15//! * Input/output data streamng via UDP
16//!
17//! ## Settings
18//! Refer to the [Lockin] structure for documentation of run-time configurable settings
19//! for this application.
20//!
21//! ## Telemetry
22//! Refer to [stabilizer::net::telemetry::Telemetry] for information about telemetry reported by this application.
23//!
24//! ## Stream
25//! This application streams raw ADC and DAC data over UDP. Refer to
26//! [stabilizer::net::data_stream] for more information.
27#![no_std]
28#![no_main]
29
30use core::{
31    iter,
32    mem::MaybeUninit,
33    sync::atomic::{fence, Ordering},
34};
35
36use miniconf::{Leaf, Tree};
37use rtic_monotonics::Monotonic;
38
39use fugit::ExtU32;
40
41use idsp::{Accu, Complex, ComplexExt, Filter, Lowpass, Repeat, RPLL};
42
43use stabilizer::{
44    hardware::{
45        self,
46        adc::{Adc0Input, Adc1Input, AdcCode},
47        afe::Gain,
48        dac::{Dac0Output, Dac1Output, DacCode},
49        hal,
50        input_stamper::InputStamper,
51        timers::SamplingTimer,
52        DigitalInput0, DigitalInput1, Pgia, SerialTerminal, SystemTimer,
53        Systick, UsbDevice,
54    },
55    net::{
56        data_stream::{FrameGenerator, StreamFormat, StreamTarget},
57        serde::{Deserialize, Serialize},
58        telemetry::TelemetryBuffer,
59        NetworkState, NetworkUsers,
60    },
61    settings::NetSettings,
62};
63
64// The logarithm of the number of samples in each batch process. This corresponds with 2^3 samples
65// per batch = 8 samples
66const BATCH_SIZE_LOG2: u32 = 3;
67const BATCH_SIZE: usize = 1 << BATCH_SIZE_LOG2;
68
69// The logarithm of the number of 100MHz timer ticks between each sample. This corresponds with a
70// sampling period of 2^7 = 128 ticks. At 100MHz, 10ns per tick, this corresponds to a sampling
71// period of 1.28 uS or 781.25 KHz.
72const SAMPLE_TICKS_LOG2: u32 = 7;
73const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2;
74
75#[derive(Clone, Debug, Tree)]
76pub struct Settings {
77    lockin: Lockin,
78    net: NetSettings,
79}
80
81impl stabilizer::settings::AppSettings for Settings {
82    fn new(net: NetSettings) -> Self {
83        Self {
84            net,
85            lockin: Lockin::default(),
86        }
87    }
88
89    fn net(&self) -> &NetSettings {
90        &self.net
91    }
92}
93
94impl serial_settings::Settings for Settings {
95    fn reset(&mut self) {
96        *self = Self {
97            lockin: Lockin::default(),
98            net: NetSettings::new(self.net.mac),
99        }
100    }
101}
102
103#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
104enum Conf {
105    /// Output the lockin magnitude.
106    Magnitude,
107    /// Output the phase of the lockin
108    Phase,
109    /// Output the lockin reference frequency as a sinusoid
110    ReferenceFrequency,
111    /// Output the logarithmic power of the lockin
112    LogPower,
113    /// Output the in-phase component of the lockin signal.
114    InPhase,
115    /// Output the quadrature component of the lockin signal.
116    Quadrature,
117    /// Output the lockin internal modulation frequency as a sinusoid
118    Modulation,
119}
120
121#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
122enum LockinMode {
123    /// Utilize an internally generated reference for demodulation
124    Internal,
125    /// Utilize an external modulation signal supplied to DI0
126    External,
127}
128
129#[derive(Clone, Debug, Tree)]
130pub struct Lockin {
131    /// Configure the Analog Front End (AFE) gain.
132    ///
133    /// # Path
134    /// `afe/<n>`
135    ///
136    /// * `<n>` specifies which channel to configure. `<n>` := [0, 1]
137    ///
138    /// # Value
139    /// Any of the variants of [Gain] enclosed in double quotes.
140    afe: [Leaf<Gain>; 2],
141
142    /// Specifies the operational mode of the lockin.
143    ///
144    /// # Path
145    /// `lockin_mode`
146    ///
147    /// # Value
148    /// One of the variants of [LockinMode] enclosed in double quotes.
149    lockin_mode: Leaf<LockinMode>,
150
151    /// Specifis the PLL time constant.
152    ///
153    /// # Path
154    /// `pll_tc/<n>`
155    ///
156    /// * `<n>` specifies which channel to configure. `<n>` := [0, 1]
157    ///
158    /// # Value
159    /// The PLL time constant exponent (1-31).
160    pll_tc: [Leaf<u32>; 2],
161
162    /// Specifies the lockin lowpass gains.
163    ///
164    /// # Path
165    /// `lockin_k`
166    ///
167    /// # Value
168    /// The lockin low-pass coefficients. See [`idsp::Lowpass`] for determining them.
169    lockin_k: Leaf<<Lowpass<2> as Filter>::Config>,
170
171    /// Specifies which harmonic to use for the lockin.
172    ///
173    /// # Path
174    /// `lockin_harmonic`
175    ///
176    /// # Value
177    /// Harmonic index of the LO. -1 to _de_modulate the fundamental (complex conjugate)
178    lockin_harmonic: Leaf<i32>,
179
180    /// Specifies the LO phase offset.
181    ///
182    /// # Path
183    /// `lockin_phase`
184    ///
185    /// # Value
186    /// Demodulation LO phase offset. Units are in terms of i32, where [i32::MIN] is equivalent to
187    /// -pi and [i32::MAX] is equivalent to +pi.
188    lockin_phase: Leaf<i32>,
189
190    /// Specifies DAC output mode.
191    ///
192    /// # Path
193    /// `output_conf/<n>`
194    ///
195    /// * `<n>` specifies which channel to configure. `<n>` := [0, 1]
196    ///
197    /// # Value
198    /// One of the variants of [Conf] enclosed in double quotes.
199    output_conf: [Leaf<Conf>; 2],
200
201    /// Specifies the telemetry output period in seconds.
202    ///
203    /// # Path
204    /// `telemetry_period`
205    ///
206    /// # Value
207    /// Any non-zero value less than 65536.
208    telemetry_period: Leaf<u16>,
209
210    /// Specifies the target for data streaming.
211    ///
212    /// # Path
213    /// `stream`
214    ///
215    /// # Value
216    /// See [StreamTarget#miniconf]
217    stream: Leaf<StreamTarget>,
218}
219
220impl Default for Lockin {
221    fn default() -> Self {
222        Self {
223            afe: [Gain::G1.into(); 2],
224
225            lockin_mode: LockinMode::External.into(),
226
227            pll_tc: [21.into(), 21.into()], // frequency and phase settling time (log2 counter cycles)
228
229            lockin_k: [0x8_0000, -0x400_0000].into(), // lockin lowpass gains
230            lockin_harmonic: (-1).into(), // Harmonic index of the LO: -1 to _de_modulate the fundamental (complex conjugate)
231            lockin_phase: 0.into(),       // Demodulation LO phase offset
232
233            output_conf: [Conf::InPhase.into(), Conf::Quadrature.into()],
234            // The default telemetry period in seconds.
235            telemetry_period: 10.into(),
236
237            stream: Default::default(),
238        }
239    }
240}
241
242#[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, SDMMC])]
243mod app {
244    use super::*;
245
246    #[shared]
247    struct Shared {
248        usb: UsbDevice,
249        network: NetworkUsers<Lockin, 2>,
250        settings: Settings,
251        active_settings: Lockin,
252        telemetry: TelemetryBuffer,
253    }
254
255    #[local]
256    struct Local {
257        usb_terminal: SerialTerminal<Settings, 3>,
258        sampling_timer: SamplingTimer,
259        digital_inputs: (DigitalInput0, DigitalInput1),
260        timestamper: InputStamper,
261        afes: [Pgia; 2],
262        adcs: (Adc0Input, Adc1Input),
263        dacs: (Dac0Output, Dac1Output),
264        pll: RPLL,
265        lockin: idsp::Lockin<Repeat<2, Lowpass<2>>>,
266        source: idsp::AccuOsc<iter::Repeat<i64>>,
267        generator: FrameGenerator,
268        cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor,
269    }
270
271    #[init]
272    fn init(c: init::Context) -> (Shared, Local) {
273        let clock = SystemTimer::new(|| Systick::now().ticks());
274
275        // Configure the microcontroller
276        let (mut stabilizer, _pounder) = hardware::setup::setup::<Settings, 3>(
277            c.core,
278            c.device,
279            clock,
280            BATCH_SIZE,
281            SAMPLE_TICKS,
282        );
283
284        let mut network = NetworkUsers::new(
285            stabilizer.net.stack,
286            stabilizer.net.phy,
287            clock,
288            env!("CARGO_BIN_NAME"),
289            &stabilizer.settings.net,
290            stabilizer.metadata,
291        );
292
293        let generator = network.configure_streaming(StreamFormat::AdcDacData);
294
295        let shared = Shared {
296            network,
297            usb: stabilizer.usb,
298            telemetry: TelemetryBuffer::default(),
299            active_settings: stabilizer.settings.lockin.clone(),
300            settings: stabilizer.settings,
301        };
302
303        let source =
304            idsp::AccuOsc::new(iter::repeat(1i64 << (64 - BATCH_SIZE_LOG2)));
305
306        let mut local = Local {
307            usb_terminal: stabilizer.usb_serial,
308            sampling_timer: stabilizer.adc_dac_timer,
309            digital_inputs: stabilizer.digital_inputs,
310            afes: stabilizer.afes,
311            adcs: stabilizer.adcs,
312            dacs: stabilizer.dacs,
313            timestamper: stabilizer.timestamper,
314
315            pll: RPLL::new(SAMPLE_TICKS_LOG2 + BATCH_SIZE_LOG2),
316            lockin: idsp::Lockin::default(),
317            source,
318
319            generator,
320            cpu_temp_sensor: stabilizer.temperature_sensor,
321        };
322
323        // Enable ADC/DAC events
324        local.adcs.0.start();
325        local.adcs.1.start();
326        local.dacs.0.start();
327        local.dacs.1.start();
328
329        // Spawn a settings and telemetry update for default settings.
330        settings_update::spawn().unwrap();
331        telemetry::spawn().unwrap();
332        ethernet_link::spawn().unwrap();
333        start::spawn().unwrap();
334        usb::spawn().unwrap();
335
336        // Start recording digital input timestamps.
337        stabilizer.timestamp_timer.start();
338
339        // Enable the timestamper.
340        local.timestamper.start();
341
342        (shared, local)
343    }
344
345    #[task(priority = 1, local=[sampling_timer])]
346    async fn start(c: start::Context) {
347        Systick::delay(100.millis()).await;
348        // Start sampling ADCs and DACs.
349        c.local.sampling_timer.start();
350    }
351
352    /// Main DSP processing routine.
353    ///
354    /// See `dual-iir` for general notes on processing time and timing.
355    ///
356    /// This is an implementation of a externally (DI0) referenced PLL lockin on the ADC0 signal.
357    /// It outputs either I/Q or power/phase on DAC0/DAC1. Data is normalized to full scale.
358    /// PLL bandwidth, filter bandwidth, slope, and x/y or power/phase post-filters are available.
359    #[task(binds=DMA1_STR4, shared=[active_settings, telemetry], local=[adcs, dacs, lockin, timestamper, pll, generator, source], priority=3)]
360    #[link_section = ".itcm.process"]
361    fn process(c: process::Context) {
362        let process::SharedResources {
363            active_settings,
364            telemetry,
365            ..
366        } = c.shared;
367
368        let process::LocalResources {
369            timestamper,
370            adcs: (adc0, adc1),
371            dacs: (dac0, dac1),
372            pll,
373            lockin,
374            source,
375            generator,
376            ..
377        } = c.local;
378
379        (active_settings, telemetry).lock(|settings, telemetry| {
380            let (reference_phase, reference_frequency) =
381                match *settings.lockin_mode {
382                    LockinMode::External => {
383                        let timestamp =
384                            timestamper.latest_timestamp().unwrap_or(None); // Ignore data from timer capture overflows.
385                        let (pll_phase, pll_frequency) = pll.update(
386                            timestamp.map(|t| t as i32),
387                            *settings.pll_tc[0],
388                            *settings.pll_tc[1],
389                        );
390                        (pll_phase, (pll_frequency >> BATCH_SIZE_LOG2) as i32)
391                    }
392                    LockinMode::Internal => {
393                        // Reference phase and frequency are known.
394                        (1i32 << 30, 1i32 << (32 - BATCH_SIZE_LOG2))
395                    }
396                };
397
398            let sample_frequency =
399                reference_frequency.wrapping_mul(*settings.lockin_harmonic);
400            let sample_phase = settings.lockin_phase.wrapping_add(
401                reference_phase.wrapping_mul(*settings.lockin_harmonic),
402            );
403
404            (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| {
405                let adc_samples = [adc0, adc1];
406                let mut dac_samples = [dac0, dac1];
407
408                // Preserve instruction and data ordering w.r.t. DMA flag access.
409                fence(Ordering::SeqCst);
410
411                let output: Complex<i32> = adc_samples[0]
412                    .iter()
413                    // Zip in the LO phase.
414                    .zip(Accu::new(sample_phase, sample_frequency))
415                    // Convert to signed, MSB align the ADC sample, update the Lockin (demodulate, filter)
416                    .map(|(&sample, phase)| {
417                        let s = (sample as i16 as i32) << 16;
418                        lockin.update(s, phase, &settings.lockin_k)
419                    })
420                    // Decimate
421                    .last()
422                    .unwrap()
423                    * 2; // Full scale assuming the 2f component is gone.
424
425                // Convert to DAC data.
426                for (channel, samples) in dac_samples.iter_mut().enumerate() {
427                    for sample in samples.iter_mut() {
428                        let value = match *settings.output_conf[channel] {
429                            Conf::Magnitude => output.abs_sqr() as i32 >> 16,
430                            Conf::Phase => output.arg() >> 16,
431                            Conf::LogPower => output.log2() << 8,
432                            Conf::ReferenceFrequency => {
433                                reference_frequency >> 16
434                            }
435                            Conf::InPhase => output.re >> 16,
436                            Conf::Quadrature => output.im >> 16,
437
438                            Conf::Modulation => source.next().unwrap().re,
439                        };
440
441                        *sample = DacCode::from(value as i16).0;
442                    }
443                }
444
445                // Stream the data.
446                const N: usize = BATCH_SIZE * size_of::<i16>()
447                    / size_of::<MaybeUninit<u8>>();
448                generator.add(|buf| {
449                    for (data, buf) in adc_samples
450                        .iter()
451                        .chain(dac_samples.iter())
452                        .zip(buf.chunks_exact_mut(N))
453                    {
454                        let data = unsafe {
455                            core::slice::from_raw_parts(
456                                data.as_ptr() as *const MaybeUninit<u8>,
457                                N,
458                            )
459                        };
460                        buf.copy_from_slice(data)
461                    }
462                    N * 4
463                });
464
465                // Update telemetry measurements.
466                telemetry.adcs =
467                    [AdcCode(adc_samples[0][0]), AdcCode(adc_samples[1][0])];
468
469                telemetry.dacs =
470                    [DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])];
471
472                // Preserve instruction and data ordering w.r.t. DMA flag access.
473                fence(Ordering::SeqCst);
474            });
475        });
476    }
477
478    #[idle(shared=[settings, network, usb])]
479    fn idle(mut c: idle::Context) -> ! {
480        loop {
481            match (&mut c.shared.network, &mut c.shared.settings)
482                .lock(|net, settings| net.update(&mut settings.lockin))
483            {
484                NetworkState::SettingsChanged => {
485                    settings_update::spawn().unwrap()
486                }
487                NetworkState::Updated => {}
488                NetworkState::NoChange => {
489                    // We can't sleep if USB is not in suspend.
490                    if c.shared.usb.lock(|usb| {
491                        usb.state()
492                            == usb_device::device::UsbDeviceState::Suspend
493                    }) {
494                        cortex_m::asm::wfi();
495                    }
496                }
497            }
498        }
499    }
500
501    #[task(priority = 1, local=[afes], shared=[network, settings, active_settings])]
502    async fn settings_update(mut c: settings_update::Context) {
503        c.shared.settings.lock(|settings| {
504            c.local.afes[0].set_gain(*settings.lockin.afe[0]);
505            c.local.afes[1].set_gain(*settings.lockin.afe[1]);
506
507            c.shared
508                .network
509                .lock(|net| net.direct_stream(*settings.lockin.stream));
510
511            c.shared
512                .active_settings
513                .lock(|current| *current = settings.lockin.clone());
514        });
515    }
516
517    #[task(priority = 1, local=[digital_inputs, cpu_temp_sensor], shared=[network, settings, telemetry])]
518    async fn telemetry(mut c: telemetry::Context) {
519        loop {
520            let mut telemetry =
521                c.shared.telemetry.lock(|telemetry| telemetry.clone());
522
523            telemetry.digital_inputs = [
524                c.local.digital_inputs.0.is_high(),
525                c.local.digital_inputs.1.is_high(),
526            ];
527
528            let (gains, telemetry_period) =
529                c.shared.settings.lock(|settings| {
530                    (settings.lockin.afe, *settings.lockin.telemetry_period)
531                });
532
533            c.shared.network.lock(|net| {
534                net.telemetry.publish(&telemetry.finalize(
535                    *gains[0],
536                    *gains[1],
537                    c.local.cpu_temp_sensor.get_temperature().unwrap(),
538                ))
539            });
540
541            // Schedule the telemetry task in the future.
542            Systick::delay((telemetry_period as u32).secs()).await;
543        }
544    }
545
546    #[task(priority = 1, shared=[usb, settings], local=[usb_terminal])]
547    async fn usb(mut c: usb::Context) {
548        loop {
549            // Handle the USB serial terminal.
550            c.shared.usb.lock(|usb| {
551                usb.poll(&mut [c
552                    .local
553                    .usb_terminal
554                    .interface_mut()
555                    .inner_mut()]);
556            });
557
558            c.shared.settings.lock(|settings| {
559                if c.local.usb_terminal.poll(settings).unwrap() {
560                    settings_update::spawn().unwrap()
561                }
562            });
563
564            Systick::delay(10.millis()).await;
565        }
566    }
567
568    #[task(priority = 1, shared=[network])]
569    async fn ethernet_link(mut c: ethernet_link::Context) {
570        loop {
571            c.shared.network.lock(|net| net.processor.handle_link());
572            Systick::delay(1.secs()).await;
573        }
574    }
575
576    #[task(binds = ETH, priority = 1)]
577    fn eth(_: eth::Context) {
578        unsafe { hal::ethernet::interrupt_handler() }
579    }
580}