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