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