idsp/iir/biquad.rs
1//! Biquad IIR
2
3use crate::Clamp;
4use core::ops::{Add, Div, Mul};
5use dsp_fixedpoint::Q;
6use dsp_process::{SplitInplace, SplitProcess};
7
8#[cfg(not(feature = "std"))]
9#[allow(unused_imports)]
10use num_traits::float::FloatCore as _;
11use num_traits::{AsPrimitive, clamp};
12
13/// Biquad IIR (second order section)
14///
15/// A biquadratic IIR filter supports up to two zeros and two poles in the transfer function.
16///
17/// The Biquad performs the following operation to compute a new output sample `y0` from a new
18/// input sample `x0` given its configuration and previous samples:
19///
20/// `y0 = b0*x0 + b1*x1 + b2*x2 + a1*y1 + a2*y2`
21///
22/// This implementation here saves storage and improves caching opportunities by decoupling
23/// filter configuration (coefficients, limits and offset) from filter state
24/// and thus supports both (a) sharing a single filter between multiple states ("channels") and (b)
25/// rapid switching of filters (tuning, transfer) for a given state without copying either
26/// state of configuration.
27///
28/// # Filter architecture
29///
30/// Direct Form 1 (DF1) and Direct Form 2 transposed (DF2T) are the only IIR filter
31/// structures with an (effective in the case of TDF2) single summing junction
32/// this allows clamping of the output before feedback.
33///
34/// DF1 allows atomic coefficient change because only inputs and outputs are stored.
35/// The summing junction pipelining of TDF2 would require incremental
36/// coefficient changes and is thus less amenable to online tuning.
37///
38/// DF2T needs less state storage (2 instead of 4). This is in addition to the coefficient
39/// storage (5 plus 2 limits plus 1 offset)
40///
41/// DF2T is less efficient and less accurate for fixed-point architectures as quantization
42/// happens at each intermediate summing junction in addition to the output quantization. This is
43/// especially true for common `i64 + i32 * i32 -> i64` MACC architectures.
44/// One could use wide state storage for fixed point DF2T but that would negate the storage
45/// and processing advantages.
46///
47/// # Coefficients
48///
49/// `ba: [T; 5] = [b0, b1, b2, a1, a2]` is the coefficients type.
50/// To represent the IIR coefficients, this contains the feed-forward
51/// coefficients `b0, b1, b2` followed by the feed-back coefficients
52/// `a1, a2`, all five normalized such that `a0 = 1`.
53///
54/// The summing junction of the [`BiquadClamp`] filter also receives an offset `u`
55/// and applies clamping such that `min <= y <= max`.
56///
57/// See [`crate::iir::coefficients::Filter`] and [`crate::iir::pid::Builder`] for ways to generate coefficients.
58///
59/// # Fixed point
60///
61/// Coefficient scaling for fixed point (i.e. integer) processing relies on [`dsp_fixedpoint::Q`].
62///
63/// Choose the number of fractional bits to meet coefficient range (e.g. potentially `a1 = 2`
64/// for a double integrator) and guard bits.
65///
66/// # PID controller
67///
68/// The IIR coefficients can be mapped to other transfer function
69/// representations, for example PID controllers as described in
70/// <https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw> and
71/// <https://arxiv.org/abs/1508.06319>.
72///
73/// Using a Biquad as a template for a PID controller achieves several important properties:
74///
75/// * Its transfer function is universal in the sense that any biquadratic
76/// transfer function can be implemented (high-passes, gain limits, second
77/// order integrators with inherent anti-windup, notches etc) without code
78/// changes preserving all features.
79/// * It inherits a universal implementation of "integrator anti-windup", also
80/// and especially in the presence of set-point changes and in the presence
81/// of proportional or derivative gain without any back-off that would reduce
82/// steady-state output range.
83/// * It has universal derivative-kick (undesired, unlimited, and un-physical
84/// amplification of set-point changes by the derivative term) avoidance.
85/// * An offset at the input of an IIR filter (a.k.a. "set-point") is
86/// equivalent to an offset at the summing junction (in output units).
87/// They are related by the overall (DC feed-forward) gain of the filter.
88/// * It stores only previous outputs and inputs. These have direct and
89/// invariant interpretation (independent of coefficients and offset).
90/// Therefore it can trivially implement bump-less transfer between any
91/// coefficients/offset sets.
92/// * Cascading multiple IIR filters allows stable and robust
93/// implementation of transfer functions beyond biquadratic terms.
94#[derive(Clone, Debug, Default, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
95pub struct Biquad<C> {
96 /// Coefficients
97 ///
98 /// `[b0, b1, b2, a1, a2]`
99 ///
100 /// Such that
101 /// `y0 = (b0*x0 + b1*x1 + b2*x2 + a1*y1 + a2*y2)/(1 << F)`
102 /// where `x0, x1, x2` are current, delayed, and doubly delayed inputs and
103 /// `y0, y1, y2` are current, delayed, and doubly delayed outputs.
104 ///
105 /// Note the a1, a2 sign. The transfer function is:
106 /// `H(z) = (b0 + b1*z^-1 + b2*z^-2)/((1 << F) - a2*z^-1 - a2*z^-2)`
107 pub ba: [C; 5],
108}
109
110/// Second-order-section with offset and clamp
111#[derive(Clone, Debug, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
112pub struct BiquadClamp<C, T = C> {
113 /// Coefficients
114 pub coeff: Biquad<C>,
115
116 /// Summing junction offset
117 ///
118 /// ```
119 /// # use idsp::iir::*;
120 /// # use dsp_process::SplitProcess;
121 /// let mut i = BiquadClamp::<f32>::default();
122 /// i.u = 5.0;
123 /// assert_eq!(i.process(&mut DirectForm1::default(), 0.0), 5.0);
124 /// ```
125 pub u: T,
126
127 /// Summing junction lower limit
128 ///
129 /// ```
130 /// # use idsp::iir::*;
131 /// # use dsp_process::SplitProcess;
132 /// let mut i = BiquadClamp::<f32>::default();
133 /// i.min = 5.0;
134 /// assert_eq!(i.process(&mut DirectForm1::default(), 0.0), 5.0);
135 /// ```
136 pub min: T,
137
138 /// Summing junction upper limit
139 ///
140 /// ```
141 /// # use idsp::iir::*;
142 /// # use dsp_process::SplitProcess;
143 /// let mut i = BiquadClamp::<f32>::default();
144 /// i.max = -5.0;
145 /// assert_eq!(i.process(&mut DirectForm1::default(), 0.0), -5.0);
146 /// ```
147 pub max: T,
148}
149
150impl<C, T: Clamp> Default for BiquadClamp<C, T>
151where
152 Biquad<C>: Default,
153{
154 fn default() -> Self {
155 Self {
156 coeff: Biquad::default(),
157 u: T::ZERO,
158 min: T::MIN,
159 max: T::MAX,
160 }
161 }
162}
163
164impl<C: Clamp + Copy> Biquad<C> {
165 /// A unity gain filter
166 ///
167 /// ```
168 /// # use idsp::iir::*;
169 /// # use dsp_process::SplitProcess;
170 /// let x0 = 3.0;
171 /// let y0 = Biquad::<f32>::IDENTITY.process(&mut DirectForm1::default(), x0);
172 /// assert_eq!(y0, x0);
173 /// ```
174 pub const IDENTITY: Self = Self::proportional(C::ONE);
175
176 /// A filter with the given proportional gain at all frequencies
177 ///
178 /// ```
179 /// # use idsp::iir::*;
180 /// # use dsp_process::SplitProcess;
181 /// let x0 = 3.0;
182 /// let y0 = Biquad::<f32>::proportional(2.0).process(&mut DirectForm1::default(), x0);
183 /// assert_eq!(y0, 2.0 * x0);
184 /// ```
185 pub const fn proportional(k: C) -> Self {
186 Self {
187 ba: [k, C::ZERO, C::ZERO, C::ZERO, C::ZERO],
188 }
189 }
190 /// A "hold" filter that ingests input and maintains output
191 ///
192 /// ```
193 /// # use idsp::iir::*;
194 /// # use dsp_process::SplitProcess;
195 /// let mut state = DirectForm1::<f32>::default();
196 /// state.xy[2] = 2.0;
197 /// let x0 = 7.0;
198 /// let y0 = Biquad::<f32>::HOLD.process(&mut state, x0);
199 /// assert_eq!(y0, 2.0);
200 /// assert_eq!(state.xy, [x0, 0.0, y0, y0]);
201 /// ```
202 pub const HOLD: Self = Self {
203 ba: [C::ZERO, C::ZERO, C::ZERO, C::ONE, C::ZERO],
204 };
205}
206
207impl<C: Copy + Add<Output = C>> Biquad<C> {
208 /// DC forward gain fro input to summing junction
209 ///
210 /// ```
211 /// # use idsp::iir::*;
212 /// assert_eq!(Biquad::proportional(3.0).forward_gain(), 3.0);
213 /// ```
214 pub fn forward_gain(&self) -> C {
215 self.ba[0] + self.ba[1] + self.ba[2]
216 }
217}
218
219impl<C: Copy + Add<Output = C>, T: Copy + Div<C, Output = T> + Mul<C, Output = T>>
220 BiquadClamp<C, T>
221{
222 /// Summing junction offset referred to input
223 ///
224 /// ```
225 /// # use idsp::iir::*;
226 /// let mut i = BiquadClamp::from(Biquad::proportional(3.0));
227 /// i.u = 6.0;
228 /// assert_eq!(i.input_offset(), 2.0);
229 /// ```
230 pub fn input_offset(&self) -> T {
231 self.u / self.coeff.forward_gain()
232 }
233
234 /// Summing junction offset referred to input
235 ///
236 /// ```
237 /// # use idsp::iir::*;
238 /// let mut i = BiquadClamp::from(Biquad::proportional(3.0));
239 /// i.set_input_offset(2.0);
240 /// assert_eq!(i.u, 6.0);
241 /// ```
242 pub fn set_input_offset(&mut self, i: T) {
243 self.u = i * self.coeff.forward_gain();
244 }
245}
246
247/// SOS state
248#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
249pub struct DirectForm1<T> {
250 /// X,Y state
251 ///
252 /// Contents before `process()`: `[x1, x2, y1, y2]`
253 pub xy: [T; 4],
254}
255
256/// ```
257/// # use dsp_process::SplitProcess;
258/// # use idsp::iir::*;
259/// let mut state = DirectForm1 {
260/// xy: [0.0, 1.0, 2.0, 3.0],
261/// };
262/// let x0 = 4.0;
263/// let y0 = Biquad::<f32>::IDENTITY.process(&mut state, x0);
264/// assert_eq!(y0, x0);
265/// assert_eq!(state.xy, [x0, 0.0, y0, 2.0]);
266/// ```
267impl<T: 'static + Copy, C: Copy + Mul<T, Output = A>, A: Add<Output = A> + AsPrimitive<T>>
268 SplitProcess<T, T, DirectForm1<T>> for Biquad<C>
269{
270 fn process(&self, state: &mut DirectForm1<T>, x0: T) -> T {
271 let xy = &mut state.xy;
272 let ba = &self.ba;
273 let y0 = (ba[0] * x0 + ba[1] * xy[0] + ba[2] * xy[1] + ba[3] * xy[2] + ba[4] * xy[3]).as_();
274 *xy = [x0, xy[0], y0, xy[2]];
275 y0
276 }
277}
278
279/// ```
280/// use dsp_process::SplitProcess;
281/// use idsp::iir::*;
282/// let biquad = BiquadClamp::<f32, f32>::from(Biquad::IDENTITY);
283/// let mut state = DirectForm2Transposed::default();
284/// let x = 3.0f32;
285/// let y = biquad.process(&mut state, x);
286/// assert_eq!(x, y);
287/// ```
288impl<T: Copy + Add<Output = T> + PartialOrd, C> SplitProcess<T, T, DirectForm1<T>>
289 for BiquadClamp<C, T>
290where
291 Biquad<C>: SplitProcess<T, T, DirectForm1<T>>,
292{
293 fn process(&self, state: &mut DirectForm1<T>, x0: T) -> T {
294 let y0 = clamp(self.coeff.process(state, x0) + self.u, self.min, self.max);
295 state.xy[2] = y0; // overwrite
296 y0
297 }
298}
299
300/// Direct form 2 transposed SOS state
301#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
302pub struct DirectForm2Transposed<T> {
303 /// internal state
304 ///
305 /// `[u1, u2]`
306 pub u: [T; 2],
307}
308
309/// ```
310/// use dsp_process::SplitProcess;
311/// use idsp::iir::*;
312/// let biquad = Biquad::<f32>::IDENTITY;
313/// let mut state = DirectForm2Transposed::default();
314/// let x = 3.0f32;
315/// let y = biquad.process(&mut state, x);
316/// assert_eq!(x, y);
317/// ```
318impl<T: Copy + Mul<Output = T> + Add<Output = T>> SplitProcess<T, T, DirectForm2Transposed<T>>
319 for Biquad<T>
320{
321 fn process(&self, state: &mut DirectForm2Transposed<T>, x0: T) -> T {
322 let u = &mut state.u;
323 let ba = &self.ba;
324 let y0 = u[0] + ba[0] * x0;
325 u[0] = u[1] + ba[1] * x0 + ba[3] * y0;
326 u[1] = ba[2] * x0 + ba[4] * y0;
327 y0
328 }
329}
330
331impl<T: Copy + Add<Output = T> + Mul<Output = T> + PartialOrd>
332 SplitProcess<T, T, DirectForm2Transposed<T>> for BiquadClamp<T, T>
333{
334 fn process(&self, state: &mut DirectForm2Transposed<T>, x0: T) -> T {
335 let u = &mut state.u;
336 let ba = &self.coeff.ba;
337 let y0 = clamp(u[0] + ba[0] * x0 + self.u, self.min, self.max);
338 u[0] = u[1] + ba[1] * x0 + ba[3] * y0;
339 u[1] = ba[2] * x0 + ba[4] * y0;
340 y0
341 }
342}
343
344/// SOS state with wide Y
345#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
346pub struct DirectForm1Wide {
347 /// X state
348 ///
349 /// `[x1, x2]`
350 pub x: [i32; 2],
351 /// Y state
352 ///
353 /// `[y1, y2]`
354 pub y: [i64; 2],
355}
356
357impl<const F: i8> SplitProcess<i32, i32, DirectForm1Wide> for Biquad<Q<i32, i64, F>> {
358 fn process(&self, state: &mut DirectForm1Wide, x0: i32) -> i32 {
359 let x = &mut state.x;
360 let y = &mut state.y;
361 let ba = &self.ba;
362 let mut acc = (ba[0] * x0 + ba[1] * x[0] + ba[2] * x[1]).inner;
363 *x = [x0, x[0]];
364 acc += (y[0] as u32 as i64 * ba[3].inner as i64) >> 32;
365 acc += (y[0] >> 32) as i32 as i64 * ba[3].inner as i64;
366 acc += (y[1] as u32 as i64 * ba[4].inner as i64) >> 32;
367 acc += (y[1] >> 32) as i32 as i64 * ba[4].inner as i64;
368 acc <<= 32 - F;
369 *y = [acc, y[0]];
370 (acc >> 32) as _
371 }
372}
373
374impl<const F: i8> SplitProcess<i32, i32, DirectForm1Wide> for BiquadClamp<Q<i32, i64, F>, i32> {
375 fn process(&self, state: &mut DirectForm1Wide, x0: i32) -> i32 {
376 let x = &mut state.x;
377 let y = &mut state.y;
378 let ba = &self.coeff.ba;
379 let mut acc = (ba[0] * x0 + ba[1] * x[0] + ba[2] * x[1]).inner;
380 *x = [x0, x[0]];
381 acc += (y[0] as u32 as i64 * ba[3].inner as i64) >> 32;
382 acc += (y[0] >> 32) as i32 as i64 * ba[3].inner as i64;
383 acc += (y[1] as u32 as i64 * ba[4].inner as i64) >> 32;
384 acc += (y[1] >> 32) as i32 as i64 * ba[4].inner as i64;
385 acc <<= 32 - F;
386 let y0 = Ord::clamp((acc >> 32) as i32 + self.u, self.min, self.max);
387 *y = [((y0 as i64) << 32) | acc as u32 as i64, y[0]];
388 y0
389 }
390}
391
392/// SOS state with first order error feedback
393#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
394pub struct DirectForm1Dither {
395 /// X,Y state
396 ///
397 /// `[x1, x2, y1, y2]`
398 pub xy: [i32; 4],
399 /// Error feedback
400 pub e: u32,
401}
402
403/// ```
404/// # use dsp_process::SplitProcess;
405/// # use dsp_fixedpoint::Q32;
406/// # use idsp::iir::*;
407/// let mut state = DirectForm1Dither {
408/// xy: [1, 2, 3, 4],
409/// e: 5,
410/// };
411/// let x0 = 6;
412/// let y0 = Biquad::<Q32<30>>::IDENTITY.process(&mut state, x0);
413/// assert_eq!(y0, x0);
414/// assert_eq!(state.xy, [x0, 1, y0, 3]);
415/// assert_eq!(state.e, 20);
416/// ```
417impl<const F: i8> SplitProcess<i32, i32, DirectForm1Dither> for Biquad<Q<i32, i64, F>> {
418 fn process(&self, state: &mut DirectForm1Dither, x0: i32) -> i32 {
419 let xy = &mut state.xy;
420 let e = &mut state.e;
421 let ba = &self.ba;
422 let mut acc = *e as i64
423 + (ba[0] * x0 + ba[1] * xy[0] + ba[2] * xy[1] + ba[3] * xy[2] + ba[4] * xy[3]).inner;
424 acc <<= 32 - F;
425 *e = acc as _;
426 let y0 = (acc >> 32) as _;
427 *xy = [x0, xy[0], y0, xy[2]];
428 y0
429 }
430}
431
432impl<const F: i8> SplitProcess<i32, i32, DirectForm1Dither> for BiquadClamp<Q<i32, i64, F>, i32> {
433 fn process(&self, state: &mut DirectForm1Dither, x0: i32) -> i32 {
434 let xy = &mut state.xy;
435 let e = &mut state.e;
436 let ba = &self.coeff.ba;
437 let mut acc = *e as i64
438 + (ba[0] * x0 + ba[1] * xy[0] + ba[2] * xy[1] + ba[3] * xy[2] + ba[4] * xy[3]).inner;
439 acc <<= 32 - F;
440 *e = acc as _;
441 let y0 = Ord::clamp((acc >> 32) as i32 + self.u, self.min, self.max);
442 *xy = [x0, xy[0], y0, xy[2]];
443 y0
444 }
445}
446
447impl<C, T: Copy, S> SplitInplace<T, S> for Biquad<C> where Self: SplitProcess<T, T, S> {}
448impl<C, T: Copy, S> SplitInplace<T, S> for BiquadClamp<C, T> where Self: SplitProcess<T, T, S> {}
449
450/// `[[b0, b1, b2], [a0, a1, a2]]` coefficients with the literature sign of a1/a2
451macro_rules! impl_from_float {
452 ($ty:ident) => {
453 impl<C> From<[[$ty; 3]; 2]> for Biquad<C>
454 where
455 [$ty; 5]: Into<Biquad<C>>,
456 {
457 fn from(ba: [[$ty; 3]; 2]) -> Self {
458 let a0 = 1.0 / ba[1][0];
459 [
460 ba[0][0] * a0,
461 ba[0][1] * a0,
462 ba[0][2] * a0,
463 -ba[1][1] * a0,
464 -ba[1][2] * a0,
465 ]
466 .into()
467 }
468 }
469 };
470}
471impl_from_float!(f32);
472impl_from_float!(f64);
473
474/// Normalized and sign-flipped coefficients
475/// `[b0, b1, b2, a1, a2]`
476impl<C: Copy + 'static, T> From<[T; 5]> for Biquad<C>
477where
478 T: AsPrimitive<C>,
479{
480 fn from(ba: [T; 5]) -> Self {
481 Self {
482 ba: ba.map(AsPrimitive::as_),
483 }
484 }
485}
486
487impl<C, T, F> From<F> for BiquadClamp<C, T>
488where
489 F: Into<Biquad<C>>,
490 Self: Default,
491{
492 fn from(coeff: F) -> Self {
493 Self {
494 coeff: coeff.into(),
495 ..Default::default()
496 }
497 }
498}
499
500#[cfg(test)]
501mod test {
502 #![allow(dead_code)]
503 use super::*;
504 use dsp_fixedpoint::Q32;
505 use dsp_process::SplitInplace;
506 // No manual tuning needed here.
507 // Compiler knows best how and when:
508 // unroll loops
509 // cache on stack
510 // handle alignment
511 // register allocate variables
512 // manage pipeline and insn issue
513
514 // cargo asm idsp::iir::biquad::test::pnm -p idsp --rust --target thumbv7em-none-eabihf --lib --target-cpu cortex-m7 --color --mca -M=-iterations=1 -M=-timeline -M=-skip-unsupported-instructions=lack-sched | less -R
515
516 pub fn pnm(
517 config: &[Biquad<Q32<29>>; 4],
518 state: &mut [DirectForm1<i32>; 4],
519 xy0: &mut [i32; 1 << 3],
520 ) {
521 config.inplace(state, xy0);
522 }
523
524 // ~20 cycles/sample/sos on skylake, >200 MS/s
525 #[test]
526 #[ignore]
527 fn sos_insn() {
528 let cfg = [
529 [[1., 3., 5.], [19., -9., 9.]],
530 [[3., 3., 5.], [21., -11., 11.]],
531 [[1., 3., 5.], [55., -17., 17.]],
532 [[1., 8., 5.], [77., -7., 7.]],
533 ]
534 .map(|c| Biquad::from(c));
535 let mut state = Default::default();
536 let mut x = [977371917; 1 << 7];
537 for _ in 0..1 << 20 {
538 x[9] = x[63];
539 let (x, []) = x.as_chunks_mut() else {
540 unreachable!()
541 };
542 for x in x {
543 pnm(&cfg, &mut state, x);
544 }
545 }
546 }
547}