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