dual_iir/
dual-iir.rs

1//! # Dual IIR
2//!
3//! The Dual IIR application exposes two configurable channels. Stabilizer samples input at a fixed
4//! rate, digitally filters the data, and then generates filtered output signals on the respective
5//! channel outputs.
6//!
7//! ## Features
8//! * Two indpenendent channels
9//! * up to 800 kHz rate, timed sampling
10//! * Run-time filter configuration
11//! * Input/Output data streaming
12//! * Down to 2 µs latency
13//! * f32 IIR math
14//! * Generic biquad (second order) IIR filter
15//! * Anti-windup
16//! * Derivative kick avoidance
17//!
18//! ## Settings
19//! Refer to the [DualIir] structure for documentation of run-time configurable settings for this
20//! application.
21//!
22//! ## Telemetry
23//! Refer to [stabilizer::net::telemetry::Telemetry] for information about telemetry reported by this application.
24//!
25//! ## Stream
26//! This application streams raw ADC and DAC data over UDP. Refer to
27//! [stabilizer::net::data_stream] for more information.
28#![no_std]
29#![no_main]
30
31use core::sync::atomic::{fence, Ordering};
32use miniconf::{Leaf, Tree};
33
34use rtic_monotonics::Monotonic;
35
36use fugit::ExtU32 as _;
37
38use idsp::iir;
39
40use serde::{Deserialize, Serialize};
41use stabilizer::{
42    hardware::{
43        self,
44        adc::{Adc0Input, Adc1Input, AdcCode},
45        afe::Gain,
46        dac::{Dac0Output, Dac1Output, DacCode},
47        hal,
48        signal_generator::{self, Source},
49        timers::SamplingTimer,
50        DigitalInput0, DigitalInput1, Pgia, SerialTerminal, SystemTimer,
51        Systick, UsbDevice,
52    },
53    net::{
54        data_stream::{FrameGenerator, StreamFormat, StreamTarget},
55        telemetry::TelemetryBuffer,
56        NetworkState, NetworkUsers,
57    },
58    settings::NetSettings,
59};
60
61// The number of cascaded IIR biquads per channel. Select 1 or 2!
62const IIR_CASCADE_LENGTH: usize = 1;
63
64// The number of samples in each batch process
65const BATCH_SIZE: usize = 8;
66
67// The logarithm of the number of 100MHz timer ticks between each sample. With a value of 2^7 =
68// 128, there is 1.28uS per sample, corresponding to a sampling frequency of 781.25 KHz.
69const SAMPLE_TICKS_LOG2: u8 = 7;
70const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2;
71const SAMPLE_PERIOD: f32 =
72    SAMPLE_TICKS as f32 * hardware::design_parameters::TIMER_PERIOD;
73
74#[derive(Clone, Debug, Tree)]
75pub struct Settings {
76    dual_iir: DualIir,
77    net: NetSettings,
78}
79
80impl stabilizer::settings::AppSettings for Settings {
81    fn new(net: NetSettings) -> Self {
82        Self {
83            net,
84            dual_iir: DualIir::default(),
85        }
86    }
87
88    fn net(&self) -> &NetSettings {
89        &self.net
90    }
91}
92
93impl serial_settings::Settings for Settings {
94    fn reset(&mut self) {
95        *self = Self {
96            dual_iir: DualIir::default(),
97            net: NetSettings::new(self.net.mac),
98        }
99    }
100}
101
102#[derive(Clone, Debug, Tree)]
103pub struct BiquadRepr {
104    /// Biquad parameters
105    #[tree(typ="Leaf<iir::BiquadReprDiscriminants>", rename="typ",
106        with(serialize=self.repr.tag_serialize, deserialize=self.repr.tag_deserialize),
107        deny(ref_any="deny", mut_any="deny"))]
108    _tag: (),
109    repr: iir::BiquadRepr<f32, f32>,
110}
111
112impl Default for BiquadRepr {
113    fn default() -> Self {
114        let mut i = iir::Biquad::IDENTITY;
115        i.set_min(-i16::MAX as _);
116        i.set_max(i16::MAX as _);
117        Self {
118            _tag: (),
119            repr: iir::BiquadRepr::Raw(Leaf(i)),
120        }
121    }
122}
123
124#[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)]
125pub enum Run {
126    #[default]
127    /// Run
128    Run,
129    /// Hold
130    Hold,
131    /// Hold controlled by corresponding digital input
132    External,
133}
134
135impl Run {
136    fn run(&self, di: bool) -> bool {
137        match self {
138            Self::Run => true,
139            Self::Hold => false,
140            Self::External => di,
141        }
142    }
143}
144
145#[derive(Clone, Debug, Tree, Default)]
146pub struct Channel {
147    /// Analog Front End (AFE) gain.
148    gain: Leaf<Gain>,
149    /// Biquad
150    biquad: [BiquadRepr; IIR_CASCADE_LENGTH],
151    /// Run/Hold behavior
152    run: Leaf<Run>,
153    /// Signal generator configuration to add to the DAC0/DAC1 outputs
154    source: signal_generator::Config,
155}
156
157impl Channel {
158    fn build(&self) -> Result<Active, signal_generator::Error> {
159        Ok(Active {
160            source: self
161                .source
162                .build(SAMPLE_PERIOD, DacCode::FULL_SCALE.recip())
163                .unwrap(),
164            state: Default::default(),
165            run: *self.run,
166            biquad: self.biquad.each_ref().map(|biquad| {
167                biquad.repr.build::<f32>(
168                    SAMPLE_PERIOD,
169                    1.0,
170                    DacCode::LSB_PER_VOLT,
171                )
172            }),
173        })
174    }
175}
176
177#[derive(Clone, Debug, Tree)]
178pub struct DualIir {
179    /// Channel configuration
180    ch: [Channel; 2],
181    /// Trigger both signal sources
182    trigger: Leaf<bool>,
183    /// Telemetry output period in seconds.
184    telemetry_period: Leaf<f32>,
185    /// Target IP and port for UDP streaming.
186    ///
187    /// Can be multicast.
188    stream: Leaf<StreamTarget>,
189}
190
191impl Default for DualIir {
192    fn default() -> Self {
193        Self {
194            telemetry_period: Leaf(10.0),
195            trigger: Leaf(false),
196            stream: Default::default(),
197            ch: Default::default(),
198        }
199    }
200}
201
202#[derive(Clone, Debug)]
203pub struct Active {
204    run: Run,
205    biquad: [iir::Biquad<f32>; IIR_CASCADE_LENGTH],
206    state: [[f32; 4]; IIR_CASCADE_LENGTH],
207    source: Source,
208}
209
210#[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, LTDC, SDMMC])]
211mod app {
212    use super::*;
213
214    #[shared]
215    struct Shared {
216        usb: UsbDevice,
217        network: NetworkUsers<DualIir, 8>,
218        settings: Settings,
219        active: [Active; 2],
220        telemetry: TelemetryBuffer,
221    }
222
223    #[local]
224    struct Local {
225        usb_terminal: SerialTerminal<Settings, 9>,
226        sampling_timer: SamplingTimer,
227        digital_inputs: (DigitalInput0, DigitalInput1),
228        afes: [Pgia; 2],
229        adcs: (Adc0Input, Adc1Input),
230        dacs: (Dac0Output, Dac1Output),
231        generator: FrameGenerator,
232        cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor,
233    }
234
235    #[init]
236    fn init(c: init::Context) -> (Shared, Local) {
237        let clock = SystemTimer::new(|| Systick::now().ticks());
238
239        // Configure the microcontroller
240        let (stabilizer, _pounder) = hardware::setup::setup::<Settings, 9>(
241            c.core,
242            c.device,
243            clock,
244            BATCH_SIZE,
245            SAMPLE_TICKS,
246        );
247
248        let mut network = NetworkUsers::new(
249            stabilizer.net.stack,
250            stabilizer.net.phy,
251            clock,
252            env!("CARGO_BIN_NAME"),
253            &stabilizer.settings.net,
254            stabilizer.metadata,
255        );
256
257        let generator = network.configure_streaming(StreamFormat::AdcDacData);
258
259        let shared = Shared {
260            usb: stabilizer.usb,
261            network,
262            active: stabilizer
263                .settings
264                .dual_iir
265                .ch
266                .each_ref()
267                .map(|a| a.build().unwrap()),
268            telemetry: TelemetryBuffer::default(),
269            settings: stabilizer.settings,
270        };
271
272        let mut local = Local {
273            usb_terminal: stabilizer.usb_serial,
274            sampling_timer: stabilizer.adc_dac_timer,
275            digital_inputs: stabilizer.digital_inputs,
276            afes: stabilizer.afes,
277            adcs: stabilizer.adcs,
278            dacs: stabilizer.dacs,
279            generator,
280            cpu_temp_sensor: stabilizer.temperature_sensor,
281        };
282
283        // Enable ADC/DAC events
284        local.adcs.0.start();
285        local.adcs.1.start();
286        local.dacs.0.start();
287        local.dacs.1.start();
288
289        // Spawn a settings update for default settings.
290        settings_update::spawn().unwrap();
291        telemetry::spawn().unwrap();
292        ethernet_link::spawn().unwrap();
293        usb::spawn().unwrap();
294        start::spawn().unwrap();
295
296        (shared, local)
297    }
298
299    #[task(priority = 1, local=[sampling_timer])]
300    async fn start(c: start::Context) {
301        Systick::delay(100.millis()).await;
302        // Start sampling ADCs and DACs.
303        c.local.sampling_timer.start();
304    }
305
306    /// Main DSP processing routine.
307    ///
308    /// # Note
309    /// Processing time for the DSP application code is bounded by the following constraints:
310    ///
311    /// DSP application code starts after the ADC has generated a batch of samples and must be
312    /// completed by the time the next batch of ADC samples has been acquired (plus the FIFO buffer
313    /// time). If this constraint is not met, firmware will panic due to an ADC input overrun.
314    ///
315    /// The DSP application code must also fill out the next DAC output buffer in time such that the
316    /// DAC can switch to it when it has completed the current buffer. If this constraint is not met
317    /// it's possible that old DAC codes will be generated on the output and the output samples will
318    /// be delayed by 1 batch.
319    ///
320    /// Because the ADC and DAC operate at the same rate, these two constraints actually implement
321    /// the same time bounds, meeting one also means the other is also met.
322    #[task(
323        binds=DMA1_STR4,
324        local=[digital_inputs, adcs, dacs, generator, source: [[i16; BATCH_SIZE]; 2] = [[0; BATCH_SIZE]; 2]],
325        shared=[active, telemetry],
326        priority=3)]
327    #[link_section = ".itcm.process"]
328    fn process(c: process::Context) {
329        let process::SharedResources {
330            active, telemetry, ..
331        } = c.shared;
332
333        let process::LocalResources {
334            digital_inputs,
335            adcs: (adc0, adc1),
336            dacs: (dac0, dac1),
337            generator,
338            source,
339            ..
340        } = c.local;
341
342        (active, telemetry).lock(|active, telemetry| {
343            (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| {
344                // Preserve instruction and data ordering w.r.t. DMA flag access before and after.
345                fence(Ordering::SeqCst);
346                let adc: [&[u16; BATCH_SIZE]; 2] = [
347                    (**adc0).try_into().unwrap(),
348                    (**adc1).try_into().unwrap(),
349                ];
350                let mut dac: [&mut [u16; BATCH_SIZE]; 2] =
351                    [(*dac0).try_into().unwrap(), (*dac1).try_into().unwrap()];
352
353                for ((((adc, dac), active), di), source) in adc
354                    .into_iter()
355                    .zip(dac.iter_mut())
356                    .zip(active.iter_mut())
357                    .zip(telemetry.digital_inputs)
358                    .zip(source.iter())
359                {
360                    for ((adc, dac), source) in
361                        adc.iter().zip(dac.iter_mut()).zip(source)
362                    {
363                        let x = f32::from(*adc as i16);
364                        let y = active
365                            .biquad
366                            .iter()
367                            .zip(active.state.iter_mut())
368                            .fold(x, |y, (ch, state)| {
369                                let filter = if active.run.run(di) {
370                                    ch
371                                } else {
372                                    &iir::Biquad::HOLD
373                                };
374                                filter.update(state, y)
375                            });
376
377                        // Note(unsafe): The filter limits must ensure that the value is in range.
378                        // The truncation introduces 1/2 LSB distortion.
379                        let y: i16 = unsafe { y.to_int_unchecked() };
380                        *dac = DacCode::from(y.saturating_add(*source)).0;
381                    }
382                }
383                telemetry.adcs = [AdcCode(adc[0][0]), AdcCode(adc[1][0])];
384                telemetry.dacs = [DacCode(dac[0][0]), DacCode(dac[1][0])];
385
386                const N: usize = BATCH_SIZE * size_of::<i16>();
387                generator.add(|buf| {
388                    [adc[0], adc[1], dac[0], dac[1]]
389                        .into_iter()
390                        .zip(buf.chunks_exact_mut(N))
391                        .map(|(data, buf)| {
392                            buf.copy_from_slice(bytemuck::cast_slice(data))
393                        })
394                        .count()
395                        * N
396                });
397
398                fence(Ordering::SeqCst);
399            });
400            *source = active.each_mut().map(|ch| {
401                core::array::from_fn(|_| (ch.source.next().unwrap() >> 16) as _)
402            });
403            telemetry.digital_inputs =
404                [digital_inputs.0.is_high(), digital_inputs.1.is_high()];
405        });
406    }
407
408    #[idle(shared=[network, settings, usb])]
409    fn idle(mut c: idle::Context) -> ! {
410        loop {
411            match (&mut c.shared.network, &mut c.shared.settings)
412                .lock(|net, settings| net.update(&mut settings.dual_iir))
413            {
414                NetworkState::SettingsChanged => {
415                    settings_update::spawn().unwrap();
416                }
417                NetworkState::Updated => {}
418                NetworkState::NoChange => {
419                    // We can't sleep if USB is not in suspend.
420                    if c.shared.usb.lock(|usb| {
421                        usb.state()
422                            == usb_device::device::UsbDeviceState::Suspend
423                    }) {
424                        cortex_m::asm::wfi();
425                    }
426                }
427            }
428        }
429    }
430
431    #[task(priority = 1, local=[afes], shared=[network, settings, active])]
432    async fn settings_update(mut c: settings_update::Context) {
433        c.shared.settings.lock(|settings| {
434            c.local.afes[0].set_gain(*settings.dual_iir.ch[0].gain);
435            c.local.afes[1].set_gain(*settings.dual_iir.ch[1].gain);
436
437            if *settings.dual_iir.trigger {
438                *settings.dual_iir.trigger = false;
439                let s = settings.dual_iir.ch.each_ref().map(|ch| {
440                    let s = ch
441                        .source
442                        .build(SAMPLE_PERIOD, DacCode::FULL_SCALE.recip());
443                    if let Err(err) = &s {
444                        log::error!("Failed to update source: {:?}", err);
445                    }
446                    s
447                });
448                c.shared.active.lock(|ch| {
449                    for (ch, s) in ch.iter_mut().zip(s) {
450                        if let Ok(s) = s {
451                            ch.source = s;
452                        }
453                    }
454                });
455            }
456            let b = settings.dual_iir.ch.each_ref().map(|ch| {
457                (
458                    *ch.run,
459                    ch.biquad.each_ref().map(|b| {
460                        b.repr.build::<f32>(
461                            SAMPLE_PERIOD,
462                            1.0,
463                            DacCode::LSB_PER_VOLT,
464                        )
465                    }),
466                )
467            });
468            c.shared.active.lock(|active| {
469                for (a, b) in active.iter_mut().zip(b) {
470                    (a.run, a.biquad) = b;
471                }
472            });
473            c.shared
474                .network
475                .lock(|net| net.direct_stream(*settings.dual_iir.stream));
476        });
477    }
478
479    #[task(priority = 1, shared=[network, settings, telemetry], local=[cpu_temp_sensor])]
480    async fn telemetry(mut c: telemetry::Context) {
481        loop {
482            let telemetry =
483                c.shared.telemetry.lock(|telemetry| telemetry.clone());
484
485            let (gains, telemetry_period) =
486                c.shared.settings.lock(|settings| {
487                    (
488                        settings.dual_iir.ch.each_ref().map(|ch| *ch.gain),
489                        *settings.dual_iir.telemetry_period,
490                    )
491                });
492
493            c.shared.network.lock(|net| {
494                net.telemetry.publish(&telemetry.finalize(
495                    gains[0],
496                    gains[1],
497                    c.local.cpu_temp_sensor.get_temperature().unwrap(),
498                ))
499            });
500
501            Systick::delay(((telemetry_period * 1000.0) as u32).millis()).await;
502        }
503    }
504
505    #[task(priority = 1, shared=[usb, settings], local=[usb_terminal])]
506    async fn usb(mut c: usb::Context) {
507        loop {
508            // Handle the USB serial terminal.
509            c.shared.usb.lock(|usb| {
510                usb.poll(&mut [c
511                    .local
512                    .usb_terminal
513                    .interface_mut()
514                    .inner_mut()]);
515            });
516
517            c.shared.settings.lock(|settings| {
518                if c.local.usb_terminal.poll(settings).unwrap() {
519                    settings_update::spawn().unwrap()
520                }
521            });
522
523            Systick::delay(10.millis()).await;
524        }
525    }
526
527    #[task(priority = 1, shared=[network])]
528    async fn ethernet_link(mut c: ethernet_link::Context) {
529        loop {
530            c.shared.network.lock(|net| net.processor.handle_link());
531            Systick::delay(1.secs()).await;
532        }
533    }
534
535    #[task(binds = ETH, priority = 1)]
536    fn eth(_: eth::Context) {
537        unsafe { hal::ethernet::interrupt_handler() }
538    }
539
540    #[task(binds = SPI2, priority = 4)]
541    fn spi2(_: spi2::Context) {
542        panic!("ADC0 SPI error");
543    }
544
545    #[task(binds = SPI3, priority = 4)]
546    fn spi3(_: spi3::Context) {
547        panic!("ADC1 SPI error");
548    }
549
550    #[task(binds = SPI4, priority = 4)]
551    fn spi4(_: spi4::Context) {
552        panic!("DAC0 SPI error");
553    }
554
555    #[task(binds = SPI5, priority = 4)]
556    fn spi5(_: spi5::Context) {
557        panic!("DAC1 SPI error");
558    }
559}