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