Merge branch 'master' into feature/digital-input-stamp

master
Ryan Summers 2020-12-07 16:12:55 +01:00
commit 93ab3b7dfd
26 changed files with 2867 additions and 803 deletions

View File

@ -1,6 +1,16 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "gdb-multiarch -q -x openocd.gdb"
rustflags = ["-C", "link-arg=-Tlink.x"]
rustflags = [
"-C", "link-arg=-Tlink.x",
# The target (below) defaults to cortex-m4
# There currently are two different options to go beyond that:
# 1. cortex-m7 has the right flags and instructions (FPU) but no instruction schedule yet
"-C", "target-cpu=cortex-m7",
# 2. cortex-m4 with the additional fpv5 instructions and a potentially
# better-than-nothing instruction schedule
"-C", "target-feature=+fp-armv8d16",
# When combined they are equivalent to (1) alone
]
[build]
target = "thumbv7em-none-eabihf"

View File

@ -17,7 +17,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: nightly
override: true
components: rustfmt
- name: cargo fmt --check
@ -33,15 +33,14 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: nightly
target: thumbv7em-none-eabihf
override: true
components: clippy
- name: cargo clippy
uses: actions-rs/cargo@v1
- uses: actions-rs/clippy-check@v1
continue-on-error: true
with:
command: clippy
token: ${{ secrets.GITHUB_TOKEN }}
compile:
runs-on: ubuntu-latest
@ -78,6 +77,25 @@ jobs:
command: build
args: --release --features semihosting
test:
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- beta
steps:
- uses: actions/checkout@v2
- name: Install Rust ${{ matrix.toolchain }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
- name: cargo test
uses: actions-rs/cargo@v1
with:
command: test
args: --package dsp --target=x86_64-unknown-linux-gnu
# Tell bors about it
# https://github.com/rtic-rs/cortex-m-rtic/blob/8a4f9c6b8ae91bebeea0791680f89375a78bffc6/.github/workflows/build.yml#L566-L603
ci-success:

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/dsp/target
.gdb_history

25
Cargo.lock generated
View File

@ -185,6 +185,14 @@ dependencies = [
"cortex-m",
]
[[package]]
name = "dsp"
version = "0.1.0"
dependencies = [
"libm",
"serde",
]
[[package]]
name = "embedded-dma"
version = "0.1.2"
@ -290,6 +298,12 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "libm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]]
name = "log"
version = "0.4.11"
@ -336,9 +350,9 @@ checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812"
[[package]]
name = "panic-semihosting"
version = "0.5.4"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aed16eb761d0ee9161dd1319cb38c8007813b20f9720a5a682b283e7b8cdfe58"
checksum = "c3d55dedd501dfd02514646e0af4d7016ce36bc12ae177ef52056989966a1eec"
dependencies = [
"cortex-m",
"cortex-m-semihosting",
@ -346,9 +360,9 @@ dependencies = [
[[package]]
name = "paste"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba7ae1a2180ed02ddfdb5ab70c70d596a26dd642e097bb6fe78b1bde8588ed97"
checksum = "7151b083b0664ed58ed669fcdd92f01c3d2fdbf10af4931a301474950b52bfa9"
[[package]]
name = "proc-macro2"
@ -466,6 +480,7 @@ dependencies = [
"cortex-m-log",
"cortex-m-rt",
"cortex-m-rtic",
"dsp",
"embedded-hal",
"enum-iterator",
"heapless",
@ -474,6 +489,7 @@ dependencies = [
"nb 1.0.0",
"panic-halt",
"panic-semihosting",
"paste",
"serde",
"serde-json-core",
"smoltcp",
@ -501,6 +517,7 @@ dependencies = [
[[package]]
name = "stm32h7xx-hal"
version = "0.8.0"
source = "git+https://github.com/stm32-rs/stm32h7xx-hal?branch=dma#0bfeeca4ce120c1b7c6d140a7da73a4372b874d8"
dependencies = [
"bare-metal 1.0.0",
"cast",

View File

@ -40,6 +40,8 @@ embedded-hal = "0.2.4"
nb = "1.0.0"
asm-delay = "0.9.0"
enum-iterator = "0.6.0"
paste = "1"
dsp = { path = "dsp" }
[dependencies.mcp23017]
git = "https://github.com/mrd0ll4r/mcp23017.git"
@ -54,14 +56,13 @@ path = "ad9959"
[dependencies.stm32h7xx-hal]
features = ["stm32h743v", "rt", "unproven", "ethernet", "quadspi"]
# git = "https://github.com/quartiq/stm32h7xx-hal"
# branch = "feature/dma-rtic-example"
path = "../stm32h7xx-hal"
git = "https://github.com/stm32-rs/stm32h7xx-hal"
branch = "dma"
[features]
semihosting = ["panic-semihosting", "cortex-m-log/semihosting"]
bkpt = [ ]
nightly = ["cortex-m/inline-asm"]
nightly = ["cortex-m/inline-asm", "dsp/nightly"]
[profile.dev]
codegen-units = 1

View File

@ -1,4 +1,4 @@
![Continuous Integration](https://github.com/quartiq/stabilizer/workflows/Continuous%20Integration/badge.svg)
[![QUARTIQ Matrix Chat](https://img.shields.io/matrix/quartiq:matrix.org)](https://matrix.to/#/#quartiq:matrix.org)
# Stabilizer Firmware

View File

@ -118,27 +118,24 @@ impl<I: Interface> Ad9959<I> {
};
// Reset the AD9959
reset_pin.set_high().or_else(|_| Err(Error::Pin))?;
reset_pin.set_high().or(Err(Error::Pin))?;
io_update.set_low().or_else(|_| Err(Error::Pin))?;
// Delay for a clock cycle to allow the device to reset.
delay.delay_ms((1000.0 / clock_frequency as f32) as u8);
reset_pin.set_low().or_else(|_| Err(Error::Pin))?;
reset_pin.set_low().or(Err(Error::Pin))?;
ad9959
.interface
.configure_mode(Mode::SingleBitTwoWire)
.map_err(|_| Error::Interface)?;
.or(Err(Error::Interface))?;
// Program the interface configuration in the AD9959. Default to all channels enabled.
let mut csr: [u8; 1] = [0xF0];
csr[0].set_bits(1..3, desired_mode as u8);
ad9959
.interface
.write(Register::CSR as u8, &csr)
.map_err(|_| Error::Interface)?;
ad9959.write(Register::CSR, &csr)?;
// Latch the new interface configuration.
io_update.set_high().or_else(|_| Err(Error::Pin))?;
@ -149,14 +146,11 @@ impl<I: Interface> Ad9959<I> {
ad9959
.interface
.configure_mode(desired_mode)
.map_err(|_| Error::Interface)?;
.or(Err(Error::Interface))?;
// Read back the CSR to ensure it specifies the mode correctly.
let mut updated_csr: [u8; 1] = [0];
ad9959
.interface
.read(Register::CSR as u8, &mut updated_csr)
.map_err(|_| Error::Interface)?;
ad9959.read(Register::CSR, &mut updated_csr)?;
if updated_csr[0] != csr[0] {
return Err(Error::Check);
}
@ -166,6 +160,18 @@ impl<I: Interface> Ad9959<I> {
Ok(ad9959)
}
fn read(&mut self, reg: Register, data: &mut [u8]) -> Result<(), Error> {
self.interface
.read(reg as u8, data)
.or(Err(Error::Interface))
}
fn write(&mut self, reg: Register, data: &[u8]) -> Result<(), Error> {
self.interface
.write(reg as u8, data)
.or(Err(Error::Interface))
}
/// Configure the internal system clock of the chip.
///
/// Arguments:
@ -181,7 +187,7 @@ impl<I: Interface> Ad9959<I> {
) -> Result<f32, Error> {
self.reference_clock_frequency = reference_clock_frequency;
if multiplier != 1 && (multiplier > 20 || multiplier < 4) {
if multiplier != 1 && !(4..=20).contains(&multiplier) {
return Err(Error::Bounds);
}
@ -193,17 +199,13 @@ impl<I: Interface> Ad9959<I> {
// TODO: Update / disable any enabled channels?
let mut fr1: [u8; 3] = [0, 0, 0];
self.interface
.read(Register::FR1 as u8, &mut fr1)
.map_err(|_| Error::Interface)?;
self.read(Register::FR1, &mut fr1)?;
fr1[0].set_bits(2..=6, multiplier);
let vco_range = frequency > 255e6;
fr1[0].set_bit(7, vco_range);
self.interface
.write(Register::FR1 as u8, &fr1)
.map_err(|_| Error::Interface)?;
self.write(Register::FR1, &fr1)?;
self.system_clock_multiplier = multiplier;
Ok(self.system_clock_frequency())
@ -217,9 +219,7 @@ impl<I: Interface> Ad9959<I> {
/// Get the current reference clock multiplier.
pub fn get_reference_clock_multiplier(&mut self) -> Result<u8, Error> {
let mut fr1: [u8; 3] = [0, 0, 0];
self.interface
.read(Register::FR1 as u8, &mut fr1)
.map_err(|_| Error::Interface)?;
self.read(Register::FR1, &mut fr1)?;
Ok(fr1[0].get_bits(2..=6) as u8)
}
@ -233,46 +233,34 @@ impl<I: Interface> Ad9959<I> {
/// True if the self test succeeded. False otherwise.
pub fn self_test(&mut self) -> Result<bool, Error> {
let mut csr: [u8; 1] = [0];
self.interface
.read(Register::CSR as u8, &mut csr)
.map_err(|_| Error::Interface)?;
self.read(Register::CSR, &mut csr)?;
let old_csr = csr[0];
// Enable all channels.
csr[0].set_bits(4..8, 0xF);
self.interface
.write(Register::CSR as u8, &csr)
.map_err(|_| Error::Interface)?;
self.write(Register::CSR, &csr)?;
// Read back the enable.
csr[0] = 0;
self.interface
.read(Register::CSR as u8, &mut csr)
.map_err(|_| Error::Interface)?;
self.read(Register::CSR, &mut csr)?;
if csr[0].get_bits(4..8) != 0xF {
return Ok(false);
}
// Clear all channel enables.
csr[0].set_bits(4..8, 0x0);
self.interface
.write(Register::CSR as u8, &csr)
.map_err(|_| Error::Interface)?;
self.write(Register::CSR, &csr)?;
// Read back the enable.
csr[0] = 0xFF;
self.interface
.read(Register::CSR as u8, &mut csr)
.map_err(|_| Error::Interface)?;
self.read(Register::CSR, &mut csr)?;
if csr[0].get_bits(4..8) != 0 {
return Ok(false);
}
// Restore the CSR.
csr[0] = old_csr;
self.interface
.write(Register::CSR as u8, &csr)
.map_err(|_| Error::Interface)?;
self.write(Register::CSR, &csr)?;
Ok(true)
}
@ -283,6 +271,34 @@ impl<I: Interface> Ad9959<I> {
* self.reference_clock_frequency as f32
}
/// Enable an output channel.
pub fn enable_channel(&mut self, channel: Channel) -> Result<(), Error> {
let mut csr: [u8; 1] = [0];
self.read(Register::CSR, &mut csr)?;
csr[0].set_bit(channel as usize + 4, true);
self.write(Register::CSR, &csr)?;
Ok(())
}
/// Disable an output channel.
pub fn disable_channel(&mut self, channel: Channel) -> Result<(), Error> {
let mut csr: [u8; 1] = [0];
self.read(Register::CSR, &mut csr)?;
csr[0].set_bit(channel as usize + 4, false);
self.write(Register::CSR, &csr)?;
Ok(())
}
/// Determine if an output channel is enabled.
pub fn is_enabled(&mut self, channel: Channel) -> Result<bool, Error> {
let mut csr: [u8; 1] = [0; 1];
self.read(Register::CSR, &mut csr)?;
Ok(csr[0].get_bit(channel as usize + 4))
}
/// Update an output channel configuration register.
///
/// Args:
@ -297,17 +313,16 @@ impl<I: Interface> Ad9959<I> {
) -> Result<(), Error> {
// Disable all other outputs so that we can update the configuration register of only the
// specified channel.
let csr: u8 = *0x00_u8
.set_bits(1..=2, self.communication_mode as u8)
.set_bit(4 + channel as usize, true);
let mut csr: [u8; 1] = [0];
self.read(Register::CSR, &mut csr)?;
self.interface
.write(Register::CSR as u8, &[csr])
.map_err(|_| Error::Interface)?;
let mut new_csr = csr;
new_csr[0].set_bits(4..8, 0);
new_csr[0].set_bit(4 + channel as usize, true);
self.interface
.write(register as u8, &data)
.map_err(|_| Error::Interface)?;
self.write(Register::CSR, &new_csr)?;
self.write(register, &data)?;
Ok(())
}
@ -327,27 +342,18 @@ impl<I: Interface> Ad9959<I> {
// Disable all other channels in the CSR so that we can read the configuration register of
// only the desired channel.
let mut csr: [u8; 1] = [0];
self.interface
.read(Register::CSR as u8, &mut csr)
.map_err(|_| Error::Interface)?;
self.read(Register::CSR, &mut csr)?;
let mut new_csr = csr;
new_csr[0].set_bits(4..8, 0);
new_csr[0].set_bit(4 + channel as usize, true);
self.interface
.write(Register::CSR as u8, &new_csr)
.map_err(|_| Error::Interface)?;
self.interface
.read(register as u8, &mut data)
.map_err(|_| Error::Interface)?;
self.write(Register::CSR, &new_csr)?;
self.read(register, &mut data)?;
// Restore the previous CSR. Note that the re-enable of the channel happens immediately, so
// the CSR update does not need to be latched.
self.interface
.write(Register::CSR as u8, &csr)
.map_err(|_| Error::Interface)?;
self.write(Register::CSR, &csr)?;
Ok(())
}
@ -406,7 +412,7 @@ impl<I: Interface> Ad9959<I> {
channel: Channel,
amplitude: f32,
) -> Result<f32, Error> {
if amplitude < 0.0 || amplitude > 1.0 {
if !(0.0..=1.0).contains(&amplitude) {
return Err(Error::Bounds);
}

70
dsp/Cargo.lock generated Normal file
View File

@ -0,0 +1,70 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "dsp"
version = "0.1.0"
dependencies = [
"libm",
"serde",
]
[[package]]
name = "libm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]]
name = "proc-macro2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443b4178719c5a851e1bde36ce12da21d74a0e60b4d982ec3385a933c812f0f6"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"

12
dsp/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "dsp"
version = "0.1.0"
authors = ["Robert Jördens <rj@quartiq.de>"]
edition = "2018"
[dependencies]
libm = "0.2.1"
serde = { version = "1.0", features = ["derive"], default-features = false }
[features]
nightly = []

205
dsp/src/iir.rs Normal file
View File

@ -0,0 +1,205 @@
use core::ops::{Add, Mul, Neg};
use serde::{Deserialize, Serialize};
use core::f32;
// These are implemented here because core::f32 doesn't have them (yet).
// They are naive and don't handle inf/nan.
// `compiler-intrinsics`/llvm should have better (robust, universal, and
// faster) implementations.
fn abs<T>(x: T) -> T
where
T: PartialOrd + Default + Neg<Output = T>,
{
if x >= T::default() {
x
} else {
-x
}
}
fn copysign<T>(x: T, y: T) -> T
where
T: PartialOrd + Default + Neg<Output = T>,
{
if (x >= T::default() && y >= T::default())
|| (x <= T::default() && y <= T::default())
{
x
} else {
-x
}
}
#[cfg(not(feature = "nightly"))]
fn max<T>(x: T, y: T) -> T
where
T: PartialOrd,
{
if x > y {
x
} else {
y
}
}
#[cfg(not(feature = "nightly"))]
fn min<T>(x: T, y: T) -> T
where
T: PartialOrd,
{
if x < y {
x
} else {
y
}
}
#[cfg(feature = "nightly")]
fn max(x: f32, y: f32) -> f32 {
core::intrinsics::maxnumf32(x, y)
}
#[cfg(feature = "nightly")]
fn min(x: f32, y: f32) -> f32 {
core::intrinsics::minnumf32(x, y)
}
// Multiply-accumulate vectors `x` and `a`.
//
// A.k.a. dot product.
// Rust/LLVM optimize this nicely.
fn macc<T>(y0: T, x: &[T], a: &[T]) -> T
where
T: Add<Output = T> + Mul<Output = T> + Copy,
{
x.iter()
.zip(a)
.map(|(x, a)| *x * *a)
.fold(y0, |y, xa| y + xa)
}
/// IIR state and coefficients type.
///
/// To represent the IIR state (input and output memory) during the filter update
/// this contains the three inputs (x0, x1, x2) and the two outputs (y1, y2)
/// concatenated. Lower indices correspond to more recent samples.
/// To represent the IIR coefficients, this contains the feed-forward
/// coefficients (b0, b1, b2) followd by the negated feed-back coefficients
/// (-a1, -a2), all five normalized such that a0 = 1.
pub type IIRState = [f32; 5];
/// IIR configuration.
///
/// Contains the coeeficients `ba`, the output offset `y_offset`, and the
/// output limits `y_min` and `y_max`.
///
/// This implementation achieves several important properties:
///
/// * Its transfer function is universal in the sense that any biquadratic
/// transfer function can be implemented (high-passes, gain limits, second
/// order integrators with inherent anti-windup, notches etc) without code
/// changes preserving all features.
/// * It inherits a universal implementation of "integrator anti-windup", also
/// and especially in the presence of set-point changes and in the presence
/// of proportional or derivative gain without any back-off that would reduce
/// steady-state output range.
/// * It has universal derivative-kick (undesired, unlimited, and un-physical
/// amplification of set-point changes by the derivative term) avoidance.
/// * An offset at the input of an IIR filter (a.k.a. "set-point") is
/// equivalent to an offset at the output. They are related by the
/// overall (DC feed-forward) gain of the filter.
/// * It stores only previous outputs and inputs. These have direct and
/// invariant interpretation (independent of gains and offsets).
/// Therefore it can trivially implement bump-less transfer.
/// * Cascading multiple IIR filters allows stable and robust
/// implementation of transfer functions beyond bequadratic terms.
#[derive(Copy, Clone, Deserialize, Serialize)]
pub struct IIR {
pub ba: IIRState,
pub y_offset: f32,
pub y_min: f32,
pub y_max: f32,
}
impl IIR {
/// Configures IIR filter coefficients for proportional-integral behavior
/// with gain limit.
///
/// # Arguments
///
/// * `kp` - Proportional gain. Also defines gain sign.
/// * `ki` - Integral gain at Nyquist. Sign taken from `kp`.
/// * `g` - Gain limit.
pub fn set_pi(&mut self, kp: f32, ki: f32, g: f32) -> Result<(), &str> {
let ki = copysign(ki, kp);
let g = copysign(g, kp);
let (a1, b0, b1) = if abs(ki) < f32::EPSILON {
(0., kp, 0.)
} else {
let c = if abs(g) < f32::EPSILON {
1.
} else {
1. / (1. + ki / g)
};
let a1 = 2. * c - 1.;
let b0 = ki * c + kp;
let b1 = ki * c - a1 * kp;
if abs(b0 + b1) < f32::EPSILON {
return Err("low integrator gain and/or gain limit");
}
(a1, b0, b1)
};
self.ba.copy_from_slice(&[b0, b1, 0., a1, 0.]);
Ok(())
}
/// Compute the overall (DC feed-forward) gain.
pub fn get_k(&self) -> f32 {
self.ba[..3].iter().sum()
}
/// Compute input-referred (`x`) offset from output (`y`) offset.
pub fn get_x_offset(&self) -> Result<f32, &str> {
let k = self.get_k();
if abs(k) < f32::EPSILON {
Err("k is zero")
} else {
Ok(self.y_offset / k)
}
}
/// Convert input (`x`) offset to equivalent output (`y`) offset and apply.
///
/// # Arguments
/// * `xo`: Input (`x`) offset.
pub fn set_x_offset(&mut self, xo: f32) {
self.y_offset = xo * self.get_k();
}
/// Feed a new input value into the filter, update the filter state, and
/// return the new output. Only the state `xy` is modified.
///
/// # Arguments
/// * `xy` - Current filter state.
/// * `x0` - New input.
pub fn update(&self, xy: &mut IIRState, x0: f32) -> f32 {
let n = self.ba.len();
debug_assert!(xy.len() == n);
// `xy` contains x0 x1 y0 y1 y2
// Increment time x1 x2 y1 y2 y3
// Shift x1 x1 x2 y1 y2
// This unrolls better than xy.rotate_right(1)
xy.copy_within(0..n - 1, 1);
// Store x0 x0 x1 x2 y1 y2
xy[0] = x0;
// Compute y0 by multiply-accumulate
let y0 = macc(self.y_offset, xy, &self.ba);
// Limit y0
let y0 = max(self.y_min, min(self.y_max, y0));
// Store y0 x0 x1 y0 y1 y2
xy[n / 2] = y0;
y0
}
}

11
dsp/src/lib.rs Normal file
View File

@ -0,0 +1,11 @@
#![cfg_attr(not(test), no_std)]
#![cfg_attr(feature = "nightly", feature(asm, core_intrinsics))]
pub type Complex<T> = (T, T);
pub mod iir;
pub mod lockin;
pub mod pll;
pub mod unwrap;
#[cfg(test)]
mod testing;

518
dsp/src/lockin.rs Normal file
View File

@ -0,0 +1,518 @@
//! Lock-in amplifier.
//!
//! Lock-in processing is performed through a combination of the
//! following modular processing blocks: demodulation, filtering,
//! decimation and computing the magnitude and phase from a complex
//! signal. These processing blocks are mutually independent.
//!
//! # Terminology
//!
//! * _demodulation signal_ - A copy of the reference signal that is
//! optionally frequency scaled and phase shifted. This is a complex
//! signal. The demodulation signals are used to demodulate the ADC
//! sampled signal.
//! * _internal clock_ - A fast internal clock used to increment a
//! counter for determining the 0-phase points of a reference signal.
//! * _reference signal_ - A constant-frequency signal used to derive
//! the demodulation signal.
//! * _timestamp_ - Timestamps record the timing of the reference
//! signal's 0-phase points. For instance, if a reference signal is
//! provided externally, a fast internal clock increments a
//! counter. When the external reference reaches the 0-phase point
//! (e.g., a positive edge), the value of the counter is recorded as a
//! timestamp. These timestamps are used to determine the frequency
//! and phase of the reference signal.
//!
//! # Usage
//!
//! The first step is to initialize a `Lockin` instance with
//! `Lockin::new()`. This provides the lock-in algorithms with
//! necessary information about the demodulation and filtering steps,
//! such as whether to demodulate with a harmonic of the reference
//! signal and the IIR biquad filter to use. There are then 4
//! different processing steps that can be used:
//!
//! * `demodulate` - Computes the phase of the demodulation signal
//! corresponding to each ADC sample, uses this phase to compute the
//! demodulation signal, and multiplies this demodulation signal by
//! the ADC-sampled signal. This is a method of `Lockin` since it
//! requires information about how to modify the reference signal for
//! demodulation.
//! * `filter` - Performs IIR biquad filtering of a complex
//! signals. This is commonly performed on the signal provided by the
//! demodulation step, but can be performed at any other point in the
//! processing chain or omitted entirely. `filter` is a method of
//! `Lockin` since it must hold onto the filter configuration and
//! state.
//! * `decimate` - This decimates a signal to reduce the load on the
//! DAC output. It does not require any state information and is
//! therefore a normal function.
//! * `magnitude_phase` - Computes the magnitude and phase of the
//! component of the ADC-sampled signal whose frequency is equal to
//! the demodulation frequency. This does not require any state
//! information and is therefore a normal function.
use super::iir::{IIRState, IIR};
use super::Complex;
use core::f32::consts::PI;
/// The number of ADC samples in one batch.
pub const ADC_SAMPLE_BUFFER_SIZE: usize = 16;
/// The number of outputs sent to the DAC for each ADC batch.
pub const DECIMATED_BUFFER_SIZE: usize = 1;
/// Treat the 2-element array as a FIFO. This allows new elements to
/// be pushed into the array, existing elements to shift back in the
/// array, and the last element to fall off the array.
trait Fifo2<T> {
fn push(&mut self, new_element: Option<T>);
}
impl<T: Copy> Fifo2<T> for [Option<T>; 2] {
/// Push a new element into the array. The existing elements move
/// backward in the array by one location, and the current last
/// element is discarded.
///
/// # Arguments
///
/// * `new_element` - New element pushed into the front of the
/// array.
fn push(&mut self, new_element: Option<T>) {
// For array sizes greater than 2 it would be preferable to
// use a rotating index to avoid unnecessary data
// copying. However, this would somewhat complicate the use of
// iterators and for 2 elements, shifting is inexpensive.
self[1] = self[0];
self[0] = new_element;
}
}
/// Performs lock-in amplifier processing of a signal.
pub struct Lockin {
phase_offset: f32,
sample_period: u32,
harmonic: u32,
timestamps: [Option<i32>; 2],
iir: IIR,
iirstate: [IIRState; 2],
}
impl Lockin {
/// Initialize a new `Lockin` instance.
///
/// # Arguments
///
/// * `phase_offset` - Phase offset (in radians) applied to the
/// demodulation signal.
/// * `sample_period` - ADC sampling period in terms of the
/// internal clock period.
/// * `harmonic` - Integer scaling factor used to adjust the
/// demodulation frequency. E.g., 2 would demodulate with the
/// first harmonic.
/// * `iir` - IIR biquad filter.
///
/// # Returns
///
/// New `Lockin` instance.
pub fn new(
phase_offset: f32,
sample_period: u32,
harmonic: u32,
iir: IIR,
) -> Self {
Lockin {
phase_offset: phase_offset,
sample_period: sample_period,
harmonic: harmonic,
timestamps: [None, None],
iir: iir,
iirstate: [[0.; 5]; 2],
}
}
/// Demodulate an input signal with the complex reference signal.
///
/// # Arguments
///
/// * `adc_samples` - One batch of ADC samples.
/// * `timestamps` - Counter values corresponding to the edges of
/// an external reference signal. The counter is incremented by a
/// fast internal clock.
///
/// # Returns
///
/// The demodulated complex signal as a `Result`. When there are
/// an insufficient number of timestamps to perform processing,
/// `Err` is returned.
///
/// # Assumptions
///
/// `demodulate` expects that the timestamp counter value is equal
/// to 0 when the ADC samples its first input in a batch. This can
/// be achieved by configuring the timestamp counter to overflow
/// at the end of the ADC batch sampling period.
pub fn demodulate(
&mut self,
adc_samples: &[i16],
timestamps: &[u16],
) -> Result<[Complex<f32>; ADC_SAMPLE_BUFFER_SIZE], &str> {
let sample_period = self.sample_period as i32;
// update old timestamps for new ADC batch
self.timestamps.iter_mut().for_each(|t| match *t {
Some(timestamp) => {
// Existing timestamps have aged by one ADC batch
// period since the last ADC batch.
*t = Some(
timestamp - ADC_SAMPLE_BUFFER_SIZE as i32 * sample_period,
);
}
None => (),
});
// return prematurely if there aren't enough timestamps for
// processing
let old_timestamp_count =
self.timestamps.iter().filter(|t| t.is_some()).count();
if old_timestamp_count + timestamps.len() < 2 {
return Err("insufficient timestamps");
}
let mut signal = [(0., 0.); ADC_SAMPLE_BUFFER_SIZE];
// if we have not yet recorded any timestamps, the first
// reference period must be computed from the first and
// second timestamps in the array
let mut timestamp_index: usize =
if old_timestamp_count == 0 { 1 } else { 0 };
// compute ADC sample phases, sines/cosines and demodulate
signal
.iter_mut()
.zip(adc_samples.iter())
.enumerate()
.for_each(|(i, (s, sample))| {
let adc_sample_count = i as i32 * sample_period;
// index of the closest timestamp that occurred after
// the current ADC sample
let closest_timestamp_after_index: i32 = if timestamps.len() > 0
{
// Linear search is fast because both the timestamps
// and ADC sample counts are sorted. Because of this,
// we only need to check timestamps that were also
// greater than the last ADC sample count.
while timestamp_index < timestamps.len() - 1
&& (timestamps[timestamp_index] as i32)
< adc_sample_count
{
timestamp_index += 1;
}
timestamp_index as i32
} else {
-1
};
// closest timestamp that occurred before the current
// ADC sample
let closest_timestamp_before: i32;
let reference_period = if closest_timestamp_after_index < 0 {
closest_timestamp_before = self.timestamps[0].unwrap();
closest_timestamp_before - self.timestamps[1].unwrap()
} else if closest_timestamp_after_index == 0 {
closest_timestamp_before = self.timestamps[0].unwrap();
timestamps[0] as i32 - closest_timestamp_before
} else {
closest_timestamp_before = timestamps
[(closest_timestamp_after_index - 1) as usize]
as i32;
timestamps[closest_timestamp_after_index as usize] as i32
- closest_timestamp_before
};
let integer_phase: i32 = (adc_sample_count
- closest_timestamp_before)
* self.harmonic as i32;
let phase = self.phase_offset
+ 2. * PI * integer_phase as f32 / reference_period as f32;
let (sine, cosine) = libm::sincosf(phase);
let sample = *sample as f32;
s.0 = sine * sample;
s.1 = cosine * sample;
});
// record new timestamps
let start_index: usize = if timestamps.len() < 2 {
0
} else {
timestamps.len() - 2
};
timestamps[start_index..]
.iter()
.for_each(|t| self.timestamps.push(Some(*t as i32)));
Ok(signal)
}
/// Filter the complex signal using the supplied biquad IIR. The
/// signal array is modified in place.
///
/// # Arguments
///
/// * `signal` - Complex signal to filter.
pub fn filter(&mut self, signal: &mut [Complex<f32>]) {
signal.iter_mut().for_each(|s| {
s.0 = self.iir.update(&mut self.iirstate[0], s.0);
s.1 = self.iir.update(&mut self.iirstate[1], s.1);
});
}
}
/// Decimate the complex signal to `DECIMATED_BUFFER_SIZE`. The ratio
/// of `ADC_SAMPLE_BUFFER_SIZE` to `DECIMATED_BUFFER_SIZE` must be a
/// power of 2.
///
/// # Arguments
///
/// * `signal` - Complex signal to decimate.
///
/// # Returns
///
/// The decimated signal.
pub fn decimate(
signal: [Complex<f32>; ADC_SAMPLE_BUFFER_SIZE],
) -> [Complex<f32>; DECIMATED_BUFFER_SIZE] {
let n_k = ADC_SAMPLE_BUFFER_SIZE / DECIMATED_BUFFER_SIZE;
debug_assert!(
ADC_SAMPLE_BUFFER_SIZE == DECIMATED_BUFFER_SIZE || n_k % 2 == 0
);
let mut signal_decimated = [(0_f32, 0_f32); DECIMATED_BUFFER_SIZE];
signal_decimated
.iter_mut()
.zip(signal.iter().step_by(n_k))
.for_each(|(s_d, s)| {
s_d.0 = s.0;
s_d.1 = s.1;
});
signal_decimated
}
/// Compute the magnitude and phase from the complex signal. The
/// signal array is modified in place.
///
/// # Arguments
///
/// * `signal` - Complex signal to decimate.
pub fn magnitude_phase(signal: &mut [Complex<f32>]) {
signal.iter_mut().for_each(|s| {
let new_i = libm::sqrtf([s.0, s.1].iter().map(|i| i * i).sum());
let new_q = libm::atan2f(s.1, s.0);
s.0 = new_i;
s.1 = new_q;
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::complex_allclose;
#[test]
fn array_push() {
let mut arr: [Option<u32>; 2] = [None, None];
arr.push(Some(1));
assert_eq!(arr, [Some(1), None]);
arr.push(Some(2));
assert_eq!(arr, [Some(2), Some(1)]);
arr.push(Some(10));
assert_eq!(arr, [Some(10), Some(2)]);
}
#[test]
fn magnitude_phase_length_1_quadrant_1() {
let mut signal: [Complex<f32>; 1] = [(1., 1.)];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[(2_f32.sqrt(), PI / 4.)],
f32::EPSILON,
0.
));
signal = [(3_f32.sqrt() / 2., 1. / 2.)];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[(1., PI / 6.)],
f32::EPSILON,
0.
));
}
#[test]
fn magnitude_phase_length_1_quadrant_2() {
let mut signal = [(-1., 1.)];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[(2_f32.sqrt(), 3. * PI / 4.)],
f32::EPSILON,
0.
));
signal = [(-1. / 2., 3_f32.sqrt() / 2.)];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[(1_f32, 2. * PI / 3.)],
f32::EPSILON,
0.
));
}
#[test]
fn magnitude_phase_length_1_quadrant_3() {
let mut signal = [(-1. / 2_f32.sqrt(), -1. / 2_f32.sqrt())];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[(1_f32.sqrt(), -3. * PI / 4.)],
f32::EPSILON,
0.
));
signal = [(-1. / 2., -2_f32.sqrt())];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[((3. / 2.) as f32, -1.91063323625 as f32)],
f32::EPSILON,
0.
));
}
#[test]
fn magnitude_phase_length_1_quadrant_4() {
let mut signal = [(1. / 2_f32.sqrt(), -1. / 2_f32.sqrt())];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[(1_f32.sqrt(), -1. * PI / 4.)],
f32::EPSILON,
0.
));
signal = [(3_f32.sqrt() / 2., -1. / 2.)];
magnitude_phase(&mut signal);
assert!(complex_allclose(
&signal,
&[(1_f32, -PI / 6.)],
f32::EPSILON,
0.
));
}
#[test]
fn decimate_sample_16_decimated_1() {
let signal: [Complex<f32>; ADC_SAMPLE_BUFFER_SIZE] = [
(0.0, 1.6),
(0.1, 1.7),
(0.2, 1.8),
(0.3, 1.9),
(0.4, 2.0),
(0.5, 2.1),
(0.6, 2.2),
(0.7, 2.3),
(0.8, 2.4),
(0.9, 2.5),
(1.0, 2.6),
(1.1, 2.7),
(1.2, 2.8),
(1.3, 2.9),
(1.4, 3.0),
(1.5, 3.1),
];
assert_eq!(decimate(signal), [(0.0, 1.6)]);
}
#[test]
fn lockin_demodulate_valid_0() {
let mut lockin = Lockin::new(
0.,
200,
1,
IIR {
ba: [0_f32; 5],
y_offset: 0.,
y_min: -(1 << 15) as f32,
y_max: (1 << 15) as f32 - 1.,
},
);
assert_eq!(
lockin.demodulate(&[0; ADC_SAMPLE_BUFFER_SIZE], &[]),
Err("insufficient timestamps")
);
}
#[test]
fn lockin_demodulate_valid_1() {
let mut lockin = Lockin::new(
0.,
200,
1,
IIR {
ba: [0_f32; 5],
y_offset: 0.,
y_min: -(1 << 15) as f32,
y_max: (1 << 15) as f32 - 1.,
},
);
assert_eq!(
lockin.demodulate(&[0; ADC_SAMPLE_BUFFER_SIZE], &[0],),
Err("insufficient timestamps")
);
}
#[test]
fn lockin_demodulate_valid_2() {
let adc_period: u32 = 200;
let mut lockin = Lockin::new(
0.,
adc_period,
1,
IIR {
ba: [0_f32; 5],
y_offset: 0.,
y_min: -(1 << 15) as f32,
y_max: (1 << 15) as f32 - 1.,
},
);
let adc_samples: [i16; ADC_SAMPLE_BUFFER_SIZE] =
[-8, 7, -7, 6, -6, 5, -5, 4, -4, 3, -3, 2, -2, -1, 1, 0];
let reference_period: u16 = 2800;
let initial_phase_integer: u16 = 200;
let timestamps: &[u16] = &[
initial_phase_integer,
initial_phase_integer + reference_period,
];
let initial_phase: f32 =
-(initial_phase_integer as f32) / reference_period as f32 * 2. * PI;
let phase_increment: f32 =
adc_period as f32 / reference_period as f32 * 2. * PI;
let mut signal = [(0., 0.); ADC_SAMPLE_BUFFER_SIZE];
for (n, s) in signal.iter_mut().enumerate() {
let adc_phase = initial_phase + n as f32 * phase_increment;
let sine = adc_phase.sin();
let cosine = adc_phase.cos();
s.0 = sine * adc_samples[n] as f32;
s.1 = cosine * adc_samples[n] as f32;
}
let result = lockin.demodulate(&adc_samples, timestamps).unwrap();
assert!(
complex_allclose(&result, &signal, 0., 1e-5),
"\nsignal computed: {:?},\nsignal expected: {:?}",
result,
signal
);
}
}

97
dsp/src/pll.rs Normal file
View File

@ -0,0 +1,97 @@
use serde::{Deserialize, Serialize};
/// Type-II, sampled phase, discrete time PLL
///
/// This PLL tracks the frequency and phase of an input signal with respect to the sampling clock.
/// The transfer function is I^2,I from input phase to output phase and P,I from input phase to
/// output frequency.
///
/// The PLL locks to any frequency (i.e. it locks to the alias in the first Nyquist zone) and is
/// stable for any gain (1 <= shift <= 30). It has a single parameter that determines the loop
/// bandwidth in octave steps. The gain can be changed freely between updates.
///
/// The frequency and phase settling time constants for an (any) frequency jump are `1 << shift`
/// update cycles. The loop bandwidth is about `1/(2*pi*(1 << shift))` in units of the sample rate.
///
/// All math is naturally wrapping 32 bit integer. Phase and frequency are understood modulo that
/// overflow in the first Nyquist zone. Expressing the IIR equations in other ways (e.g. single
/// (T)-DF-{I,II} biquad/IIR) would break on overflow.
///
/// There are no floating point rounding errors here. But there is integer quantization/truncation
/// error of the `shift` lowest bits leading to a phase offset for very low gains. Truncation
/// bias is applied. Rounding is "half up". The phase truncation error can be removed very
/// efficiently by dithering.
///
/// This PLL does not unwrap phase slips during lock acquisition. This can and should be
/// implemented elsewhere by (down) scaling and then unwrapping the input phase and (up) scaling
/// and wrapping output phase and frequency. This affects dynamic range accordingly.
///
/// The extension to I^3,I^2,I behavior to track chirps phase-accurately or to i64 data to
/// increase resolution for extremely narrowband applications is obvious.
#[derive(Copy, Clone, Default, Deserialize, Serialize)]
pub struct PLL {
// last input phase
x: i32,
// filtered frequency
f: i32,
// filtered output phase
y: i32,
}
impl PLL {
/// Update the PLL with a new phase sample.
///
/// Args:
/// * `input`: New input phase sample.
/// * `shift`: Error scaling. The frequency gain per update is `1/(1 << shift)`. The phase gain
/// is always twice the frequency gain.
///
/// Returns:
/// A tuple of instantaneous phase and frequency (the current phase increment).
pub fn update(&mut self, x: i32, shift: u8) -> (i32, i32) {
debug_assert!((1..=30).contains(&shift));
let bias = 1i32 << shift;
let e = x.wrapping_sub(self.f);
self.f = self.f.wrapping_add(
(bias >> 1).wrapping_add(e).wrapping_sub(self.x) >> shift,
);
self.x = x;
let f = self.f.wrapping_add(
bias.wrapping_add(e).wrapping_sub(self.y) >> (shift - 1),
);
self.y = self.y.wrapping_add(f);
(self.y, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mini() {
let mut p = PLL::default();
let (y, f) = p.update(0x10000, 10);
assert_eq!(y, 0xc2);
assert_eq!(f, y);
}
#[test]
fn converge() {
let mut p = PLL::default();
let f0 = 0x71f63049_i32;
let shift = 10;
let n = 31 << shift + 2;
let mut x = 0i32;
for i in 0..n {
x = x.wrapping_add(f0);
let (y, f) = p.update(x, shift);
if i > n / 4 {
assert_eq!(f.wrapping_sub(f0).abs() <= 1, true);
}
if i > n / 2 {
// The remaining error is removed by dithering.
assert_eq!(y.wrapping_sub(x).abs() < 1 << 18, true);
}
}
}
}

27
dsp/src/testing.rs Normal file
View File

@ -0,0 +1,27 @@
use super::Complex;
pub fn isclose(a: f32, b: f32, rtol: f32, atol: f32) -> bool {
(a - b).abs() <= a.abs().max(b.abs()) * rtol + atol
}
pub fn complex_isclose(
a: Complex<f32>,
b: Complex<f32>,
rtol: f32,
atol: f32,
) -> bool {
isclose(a.0, b.0, rtol, atol) && isclose(a.1, b.1, rtol, atol)
}
pub fn complex_allclose(
a: &[Complex<f32>],
b: &[Complex<f32>],
rtol: f32,
atol: f32,
) -> bool {
let mut result: bool = true;
a.iter().zip(b.iter()).for_each(|(i, j)| {
result &= complex_isclose(*i, *j, rtol, atol);
});
result
}

83
dsp/src/unwrap.rs Normal file
View File

@ -0,0 +1,83 @@
use serde::{Deserialize, Serialize};
/// Subtract `y - x` with signed overflow.
///
/// This is very similar to `i32::overflowing_sub(y, x)` except that the
/// overflow indicator is not a boolean but the signum of the overflow.
/// Additionally it's typically faster.
///
/// Returns:
/// A tuple containg the (wrapped) difference `y - x` and the signum of the
/// overflow.
#[inline(always)]
pub fn overflowing_sub(y: i32, x: i32) -> (i32, i8) {
let delta = y.wrapping_sub(x);
let wrap = (delta >= 0) as i8 - (y >= x) as i8;
(delta, wrap)
}
/// Overflow unwrapper.
///
/// This is unwrapping as in the phase unwrapping context, not unwrapping as
/// in the `Result`/`Option` context.
#[derive(Copy, Clone, Default, Deserialize, Serialize)]
pub struct Unwrapper {
// last input
x: i32,
// last wraps
v: i32,
}
impl Unwrapper {
/// Unwrap a new sample from a sequence and update the unwrapper state.
///
/// Args:
/// * `x`: New sample
///
/// Returns:
/// A tuple containing the (wrapped) difference `x - x_old` and the signed
/// number of wraps accumulated by `x`.
pub fn update(&mut self, x: i32) -> (i32, i32) {
let (dx, v) = overflowing_sub(x, self.x);
self.x = x;
self.v = self.v.saturating_add(v as i32);
(dx, self.v)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mini() {
for (x0, x1, v) in [
(0i32, 0i32, 0i8),
(0, 1, 0),
(0, -1, 0),
(1, 0, 0),
(-1, 0, 0),
(0, 0x7fff_ffff, 0),
(-1, 0x7fff_ffff, -1),
(-2, 0x7fff_ffff, -1),
(-1, -0x8000_0000, 0),
(0, -0x8000_0000, 0),
(1, -0x8000_0000, 1),
(-0x6000_0000, 0x6000_0000, -1),
(0x6000_0000, -0x6000_0000, 1),
(-0x4000_0000, 0x3fff_ffff, 0),
(-0x4000_0000, 0x4000_0000, -1),
(-0x4000_0000, 0x4000_0001, -1),
(0x4000_0000, -0x3fff_ffff, 0),
(0x4000_0000, -0x4000_0000, 0),
(0x4000_0000, -0x4000_0001, 1),
]
.iter()
{
let (dx, w) = overflowing_sub(*x1, *x0);
assert_eq!(*v, w, " = overflowing_sub({:#x}, {:#x})", *x0, *x1);
let (dx0, w0) = x1.overflowing_sub(*x0);
assert_eq!(w0, w != 0);
assert_eq!(dx, dx0);
}
}
}

1137
dsp/tests/lockin_low_pass.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ SECTIONS {
*(.itcm .itcm.*);
. = ALIGN(8);
} > ITCM
.axisram : ALIGN(8) {
.axisram (NOLOAD) : ALIGN(8) {
*(.axisram .axisram.*);
. = ALIGN(8);
} > AXISRAM

View File

@ -15,12 +15,9 @@
///! busy-waiting because the transfers should complete at approximately the same time.
use super::{
hal, sampling_timer, DMAReq, DmaConfig, MemoryToPeripheral,
PeripheralToMemory, Priority, TargetAddress, Transfer,
PeripheralToMemory, Priority, TargetAddress, Transfer, SAMPLE_BUFFER_SIZE,
};
// The desired ADC input buffer size. This is use configurable.
const INPUT_BUFFER_SIZE: usize = 1;
// The following data is written by the timer ADC sample trigger into each of the SPI TXFIFOs. Note
// that because the SPI MOSI line is not connected, this data is dont-care. Data in AXI SRAM is not
// initialized on boot, so the contents are random.
@ -30,333 +27,184 @@ static mut SPI_START: [u16; 1] = [0x00];
// The following global buffers are used for the ADC sample DMA transfers. Two buffers are used for
// each transfer in a ping-pong buffer configuration (one is being acquired while the other is being
// processed). Note that the contents of AXI SRAM is uninitialized, so the buffer contents on
// startup are undefined.
// startup are undefined. The dimensions are `ADC_BUF[adc_index][ping_pong_index][sample_index]`.
#[link_section = ".axisram.buffers"]
static mut ADC0_BUF0: [u16; INPUT_BUFFER_SIZE] = [0; INPUT_BUFFER_SIZE];
static mut ADC_BUF: [[[u16; SAMPLE_BUFFER_SIZE]; 2]; 2] =
[[[0; SAMPLE_BUFFER_SIZE]; 2]; 2];
#[link_section = ".axisram.buffers"]
static mut ADC0_BUF1: [u16; INPUT_BUFFER_SIZE] = [0; INPUT_BUFFER_SIZE];
#[link_section = ".axisram.buffers"]
static mut ADC1_BUF0: [u16; INPUT_BUFFER_SIZE] = [0; INPUT_BUFFER_SIZE];
#[link_section = ".axisram.buffers"]
static mut ADC1_BUF1: [u16; INPUT_BUFFER_SIZE] = [0; INPUT_BUFFER_SIZE];
/// SPI2 is used as a ZST (zero-sized type) for indicating a DMA transfer into the SPI2 TX FIFO
/// whenever the tim2 update dma request occurs.
struct SPI2 {}
impl SPI2 {
pub fn new() -> Self {
Self {}
}
}
unsafe impl TargetAddress<MemoryToPeripheral> for SPI2 {
/// SPI2 is configured to operate using 16-bit transfer words.
type MemSize = u16;
/// SPI2 DMA requests are generated whenever TIM2 CH1 comparison occurs.
const REQUEST_LINE: Option<u8> = Some(DMAReq::TIM2_CH1 as u8);
/// Whenever the DMA request occurs, it should write into SPI2's TX FIFO to start a DMA
/// transfer.
fn address(&self) -> u32 {
let regs = unsafe { &*hal::stm32::SPI2::ptr() };
&regs.txdr as *const _ as u32
}
}
/// SPI3 is used as a ZST (zero-sized type) for indicating a DMA transfer into the SPI3 TX FIFO
/// whenever the tim2 update dma request occurs.
struct SPI3 {}
impl SPI3 {
pub fn new() -> Self {
Self {}
}
}
unsafe impl TargetAddress<MemoryToPeripheral> for SPI3 {
/// SPI3 is configured to operate using 16-bit transfer words.
type MemSize = u16;
/// SPI3 DMA requests are generated whenever TIM2 CH2 comparison occurs.
const REQUEST_LINE: Option<u8> = Some(DMAReq::TIM2_CH2 as u8);
/// Whenever the DMA request occurs, it should write into SPI3's TX FIFO to start a DMA
/// transfer.
fn address(&self) -> u32 {
let regs = unsafe { &*hal::stm32::SPI3::ptr() };
&regs.txdr as *const _ as u32
}
}
/// Represents both ADC input channels.
pub struct AdcInputs {
adc0: Adc0Input,
adc1: Adc1Input,
}
impl AdcInputs {
/// Construct the ADC inputs.
pub fn new(adc0: Adc0Input, adc1: Adc1Input) -> Self {
Self { adc0, adc1 }
}
/// Interrupt handler to handle when the sample collection DMA transfer completes.
///
/// # Returns
/// (adc0, adc1) where adcN is a reference to the collected ADC samples. Two array references
/// are returned - one for each ADC sample stream.
pub fn transfer_complete_handler(
&mut self,
) -> (&[u16; INPUT_BUFFER_SIZE], &[u16; INPUT_BUFFER_SIZE]) {
let adc0_buffer = self.adc0.transfer_complete_handler();
let adc1_buffer = self.adc1.transfer_complete_handler();
(adc0_buffer, adc1_buffer)
}
}
/// Represents data associated with ADC0.
pub struct Adc0Input {
next_buffer: Option<&'static mut [u16; INPUT_BUFFER_SIZE]>,
transfer: Transfer<
hal::dma::dma::Stream1<hal::stm32::DMA1>,
hal::spi::Spi<hal::stm32::SPI2, hal::spi::Disabled, u16>,
PeripheralToMemory,
&'static mut [u16; INPUT_BUFFER_SIZE],
>,
_trigger_transfer: Transfer<
hal::dma::dma::Stream0<hal::stm32::DMA1>,
SPI2,
MemoryToPeripheral,
&'static mut [u16; 1],
>,
}
impl Adc0Input {
/// Construct the ADC0 input channel.
///
/// # Args
/// * `spi` - The SPI interface used to communicate with the ADC.
/// * `trigger_stream` - The DMA stream used to trigger each ADC transfer by writing a word into
/// the SPI TX FIFO.
/// * `data_stream` - The DMA stream used to read samples received over SPI into a data buffer.
/// * `_trigger_channel` - The ADC sampling timer output compare channel for read triggers.
pub fn new(
spi: hal::spi::Spi<hal::stm32::SPI2, hal::spi::Enabled, u16>,
trigger_stream: hal::dma::dma::Stream0<hal::stm32::DMA1>,
data_stream: hal::dma::dma::Stream1<hal::stm32::DMA1>,
trigger_channel: sampling_timer::Timer2Channel1,
) -> Self {
// Generate DMA events when an output compare of the timer hitting zero (timer roll over)
// occurs.
trigger_channel.listen_dma();
trigger_channel.to_output_compare(0);
// The trigger stream constantly writes to the TX FIFO using a static word (dont-care
// contents). Thus, neither the memory or peripheral address ever change. This is run in
// circular mode to be completed at every DMA request.
let trigger_config = DmaConfig::default()
.memory_increment(false)
.peripheral_increment(false)
.priority(Priority::High)
.circular_buffer(true);
// Construct the trigger stream to write from memory to the peripheral.
let mut trigger_transfer: Transfer<_, _, MemoryToPeripheral, _> =
Transfer::init(
trigger_stream,
SPI2::new(),
unsafe { &mut SPI_START },
None,
trigger_config,
);
// The data stream constantly reads from the SPI RX FIFO into a RAM buffer. The peripheral
// stalls reads of the SPI RX FIFO until data is available, so the DMA transfer completes
// after the requested number of samples have been collected. Note that only ADC1's data
// stream is used to trigger a transfer completion interrupt.
let data_config = DmaConfig::default()
.memory_increment(true)
.priority(Priority::VeryHigh)
.peripheral_increment(false);
// A SPI peripheral error interrupt is used to determine if the RX FIFO overflows. This
// indicates that samples were dropped due to excessive processing time in the main
// application (e.g. a second DMA transfer completes before the first was done with
// processing). This is used as a flow control indicator to guarantee that no ADC samples
// are lost.
let mut spi = spi.disable();
spi.listen(hal::spi::Event::Error);
// The data transfer is always a transfer of data from the peripheral to a RAM buffer.
let mut data_transfer: Transfer<_, _, PeripheralToMemory, _> =
Transfer::init(
data_stream,
spi,
unsafe { &mut ADC0_BUF0 },
None,
data_config,
);
data_transfer.start(|spi| {
// Allow the SPI FIFOs to operate using only DMA data channels.
spi.enable_dma_rx();
spi.enable_dma_tx();
// Enable SPI and start it in infinite transaction mode.
spi.inner().cr1.modify(|_, w| w.spe().set_bit());
spi.inner().cr1.modify(|_, w| w.cstart().started());
});
trigger_transfer.start(|_| {});
Self {
next_buffer: unsafe { Some(&mut ADC0_BUF1) },
transfer: data_transfer,
_trigger_transfer: trigger_transfer,
macro_rules! adc_input {
($name:ident, $index:literal, $trigger_stream:ident, $data_stream:ident,
$spi:ident, $trigger_channel:ident, $dma_req:ident) => {
/// $spi is used as a type for indicating a DMA transfer into the SPI TX FIFO
/// whenever the tim2 update dma request occurs.
struct $spi {
_channel: sampling_timer::tim2::$trigger_channel,
}
}
/// Handle a transfer completion.
///
/// # Returns
/// A reference to the underlying buffer that has been filled with ADC samples.
pub fn transfer_complete_handler(&mut self) -> &[u16; INPUT_BUFFER_SIZE] {
let next_buffer = self.next_buffer.take().unwrap();
// Wait for the transfer to fully complete before continuing.
while self.transfer.get_transfer_complete_flag() == false {}
// Start the next transfer.
self.transfer.clear_interrupts();
let (prev_buffer, _, _) =
self.transfer.next_transfer(next_buffer).unwrap();
self.next_buffer.replace(prev_buffer);
self.next_buffer.as_ref().unwrap()
}
}
/// Represents the data input stream from ADC1
pub struct Adc1Input {
next_buffer: Option<&'static mut [u16; INPUT_BUFFER_SIZE]>,
transfer: Transfer<
hal::dma::dma::Stream3<hal::stm32::DMA1>,
hal::spi::Spi<hal::stm32::SPI3, hal::spi::Disabled, u16>,
PeripheralToMemory,
&'static mut [u16; INPUT_BUFFER_SIZE],
>,
_trigger_transfer: Transfer<
hal::dma::dma::Stream2<hal::stm32::DMA1>,
SPI3,
MemoryToPeripheral,
&'static mut [u16; 1],
>,
}
impl Adc1Input {
/// Construct a new ADC1 input data stream.
///
/// # Args
/// * `spi` - The SPI interface connected to ADC1.
/// * `trigger_stream` - The DMA stream used to trigger ADC conversions on the SPI interface.
/// * `data_stream` - The DMA stream used to read ADC samples from the SPI RX FIFO.
/// * `trigger_channel` - The ADC sampling timer output compare channel for read triggers.
pub fn new(
spi: hal::spi::Spi<hal::stm32::SPI3, hal::spi::Enabled, u16>,
trigger_stream: hal::dma::dma::Stream2<hal::stm32::DMA1>,
data_stream: hal::dma::dma::Stream3<hal::stm32::DMA1>,
trigger_channel: sampling_timer::Timer2Channel2,
) -> Self {
// Generate DMA events when an output compare of the timer hitting zero (timer roll over)
// occurs.
trigger_channel.listen_dma();
trigger_channel.to_output_compare(0);
// The trigger stream constantly writes to the TX FIFO using a static word (dont-care
// contents). Thus, neither the memory or peripheral address ever change. This is run in
// circular mode to be completed at every DMA request.
let trigger_config = DmaConfig::default()
.memory_increment(false)
.peripheral_increment(false)
.priority(Priority::High)
.circular_buffer(true);
// Construct the trigger stream to write from memory to the peripheral.
let mut trigger_transfer: Transfer<_, _, MemoryToPeripheral, _> =
Transfer::init(
trigger_stream,
SPI3::new(),
unsafe { &mut SPI_START },
None,
trigger_config,
);
// The data stream constantly reads from the SPI RX FIFO into a RAM buffer. The peripheral
// stalls reads of the SPI RX FIFO until data is available, so the DMA transfer completes
// after the requested number of samples have been collected. Note that only ADC1's data
// stream is used to trigger a transfer completion interrupt.
let data_config = DmaConfig::default()
.memory_increment(true)
.transfer_complete_interrupt(true)
.priority(Priority::VeryHigh)
.peripheral_increment(false);
// A SPI peripheral error interrupt is used to determine if the RX FIFO overflows. This
// indicates that samples were dropped due to excessive processing time in the main
// application (e.g. a second DMA transfer completes before the first was done with
// processing). This is used as a flow control indicator to guarantee that no ADC samples
// are lost.
let mut spi = spi.disable();
spi.listen(hal::spi::Event::Error);
// The data transfer is always a transfer of data from the peripheral to a RAM buffer.
let mut data_transfer: Transfer<_, _, PeripheralToMemory, _> =
Transfer::init(
data_stream,
spi,
unsafe { &mut ADC1_BUF0 },
None,
data_config,
);
data_transfer.start(|spi| {
// Allow the SPI FIFOs to operate using only DMA data channels.
spi.enable_dma_rx();
spi.enable_dma_tx();
// Enable SPI and start it in infinite transaction mode.
spi.inner().cr1.modify(|_, w| w.spe().set_bit());
spi.inner().cr1.modify(|_, w| w.cstart().started());
});
trigger_transfer.start(|_| {});
Self {
next_buffer: unsafe { Some(&mut ADC1_BUF1) },
transfer: data_transfer,
_trigger_transfer: trigger_transfer,
impl $spi {
pub fn new(
_channel: sampling_timer::tim2::$trigger_channel,
) -> Self {
Self { _channel }
}
}
}
/// Handle a transfer completion.
///
/// # Returns
/// A reference to the underlying buffer that has been filled with ADC samples.
pub fn transfer_complete_handler(&mut self) -> &[u16; INPUT_BUFFER_SIZE] {
let next_buffer = self.next_buffer.take().unwrap();
// Note(unsafe): This structure is only safe to instantiate once. The DMA request is hard-coded and
// may only be used if ownership of the timer2 $trigger_channel compare channel is assured, which is
// ensured by maintaining ownership of the channel.
unsafe impl TargetAddress<MemoryToPeripheral> for $spi {
/// SPI is configured to operate using 16-bit transfer words.
type MemSize = u16;
// Wait for the transfer to fully complete before continuing.
while self.transfer.get_transfer_complete_flag() == false {}
/// SPI DMA requests are generated whenever TIM2 CHx ($dma_req) comparison occurs.
const REQUEST_LINE: Option<u8> = Some(DMAReq::$dma_req as u8);
// Start the next transfer.
self.transfer.clear_interrupts();
let (prev_buffer, _, _) =
self.transfer.next_transfer(next_buffer).unwrap();
/// Whenever the DMA request occurs, it should write into SPI's TX FIFO to start a DMA
/// transfer.
fn address(&self) -> u32 {
// Note(unsafe): It is assumed that SPI is owned by another DMA transfer and this DMA is
// only used for the transmit-half of DMA.
let regs = unsafe { &*hal::stm32::$spi::ptr() };
&regs.txdr as *const _ as u32
}
}
self.next_buffer.replace(prev_buffer);
self.next_buffer.as_ref().unwrap()
}
/// Represents data associated with ADC.
pub struct $name {
next_buffer: Option<&'static mut [u16; SAMPLE_BUFFER_SIZE]>,
transfer: Transfer<
hal::dma::dma::$data_stream<hal::stm32::DMA1>,
hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>,
PeripheralToMemory,
&'static mut [u16; SAMPLE_BUFFER_SIZE],
>,
_trigger_transfer: Transfer<
hal::dma::dma::$trigger_stream<hal::stm32::DMA1>,
$spi,
MemoryToPeripheral,
&'static mut [u16; 1],
>,
}
impl $name {
/// Construct the ADC input channel.
///
/// # Args
/// * `spi` - The SPI interface used to communicate with the ADC.
/// * `trigger_stream` - The DMA stream used to trigger each ADC transfer by writing a word into
/// the SPI TX FIFO.
/// * `data_stream` - The DMA stream used to read samples received over SPI into a data buffer.
/// * `_trigger_channel` - The ADC sampling timer output compare channel for read triggers.
pub fn new(
spi: hal::spi::Spi<hal::stm32::$spi, hal::spi::Enabled, u16>,
trigger_stream: hal::dma::dma::$trigger_stream<
hal::stm32::DMA1,
>,
data_stream: hal::dma::dma::$data_stream<hal::stm32::DMA1>,
trigger_channel: sampling_timer::tim2::$trigger_channel,
) -> Self {
// Generate DMA events when an output compare of the timer hitting zero (timer roll over)
// occurs.
trigger_channel.listen_dma();
trigger_channel.to_output_compare(0);
// The trigger stream constantly writes to the TX FIFO using a static word (dont-care
// contents). Thus, neither the memory or peripheral address ever change. This is run in
// circular mode to be completed at every DMA request.
let trigger_config = DmaConfig::default()
.priority(Priority::High)
.circular_buffer(true);
// Construct the trigger stream to write from memory to the peripheral.
let mut trigger_transfer: Transfer<
_,
_,
MemoryToPeripheral,
_,
> = Transfer::init(
trigger_stream,
$spi::new(trigger_channel),
// Note(unsafe): Because this is a Memory->Peripheral transfer, this data is never
// actually modified. It technically only needs to be immutably borrowed, but the
// current HAL API only supports mutable borrows.
unsafe { &mut SPI_START },
None,
trigger_config,
);
// The data stream constantly reads from the SPI RX FIFO into a RAM buffer. The peripheral
// stalls reads of the SPI RX FIFO until data is available, so the DMA transfer completes
// after the requested number of samples have been collected. Note that only ADC1's (sic!)
// data stream is used to trigger a transfer completion interrupt.
let data_config = DmaConfig::default()
.memory_increment(true)
.transfer_complete_interrupt($index == 1)
.priority(Priority::VeryHigh);
// A SPI peripheral error interrupt is used to determine if the RX FIFO overflows. This
// indicates that samples were dropped due to excessive processing time in the main
// application (e.g. a second DMA transfer completes before the first was done with
// processing). This is used as a flow control indicator to guarantee that no ADC samples
// are lost.
let mut spi = spi.disable();
spi.listen(hal::spi::Event::Error);
// The data transfer is always a transfer of data from the peripheral to a RAM buffer.
let mut data_transfer: Transfer<_, _, PeripheralToMemory, _> =
Transfer::init(
data_stream,
spi,
// Note(unsafe): The ADC_BUF[$index][0] is "owned" by this peripheral.
// It shall not be used anywhere else in the module.
unsafe { &mut ADC_BUF[$index][0] },
None,
data_config,
);
data_transfer.start(|spi| {
// Allow the SPI FIFOs to operate using only DMA data channels.
spi.enable_dma_rx();
spi.enable_dma_tx();
// Enable SPI and start it in infinite transaction mode.
spi.inner().cr1.modify(|_, w| w.spe().set_bit());
spi.inner().cr1.modify(|_, w| w.cstart().started());
});
trigger_transfer.start(|_| {});
Self {
// Note(unsafe): The ADC_BUF[$index][1] is "owned" by this peripheral. It shall not be used
// anywhere else in the module.
next_buffer: unsafe { Some(&mut ADC_BUF[$index][1]) },
transfer: data_transfer,
_trigger_transfer: trigger_transfer,
}
}
/// Obtain a buffer filled with ADC samples.
///
/// # Returns
/// A reference to the underlying buffer that has been filled with ADC samples.
pub fn acquire_buffer(&mut self) -> &[u16; SAMPLE_BUFFER_SIZE] {
// Wait for the transfer to fully complete before continuing.
// Note: If a device hangs up, check that this conditional is passing correctly, as there is
// no time-out checks here in the interest of execution speed.
while !self.transfer.get_transfer_complete_flag() {}
let next_buffer = self.next_buffer.take().unwrap();
// Start the next transfer.
self.transfer.clear_interrupts();
let (prev_buffer, _) =
self.transfer.next_transfer(next_buffer).unwrap();
self.next_buffer.replace(prev_buffer); // .unwrap_none() https://github.com/rust-lang/rust/issues/62633
self.next_buffer.as_ref().unwrap()
}
}
};
}
adc_input!(Adc0Input, 0, Stream0, Stream1, SPI2, Channel1, TIM2_CH1);
adc_input!(Adc1Input, 1, Stream2, Stream3, SPI3, Channel2, TIM2_CH2);

View File

@ -1,114 +1,157 @@
///! Stabilizer DAC output control
///! Stabilizer DAC management interface
///!
///! Stabilizer output DACs do not currently rely on DMA requests for generating output.
///! Instead, the DACs utilize an internal queue for storing output codes. A timer then periodically
///! generates an interrupt which triggers an update of the DACs via a write over SPI.
use super::hal;
use heapless::consts;
///! The Stabilizer DAC utilize a DMA channel to generate output updates. A timer channel is
///! configured to generate a DMA write into the SPI TXFIFO, which initiates a SPI transfer and
///! results in DAC update for both channels.
use super::{
hal, sampling_timer, DMAReq, DmaConfig, MemoryToPeripheral, TargetAddress,
Transfer, SAMPLE_BUFFER_SIZE,
};
/// Controller structure for managing the DAC outputs.
pub struct DacOutputs {
dac0_spi: hal::spi::Spi<hal::stm32::SPI4, hal::spi::Enabled, u16>,
dac1_spi: hal::spi::Spi<hal::stm32::SPI5, hal::spi::Enabled, u16>,
timer: hal::timer::Timer<hal::stm32::TIM3>,
// The following global buffers are used for the DAC code DMA transfers. Two buffers are used for
// each transfer in a ping-pong buffer configuration (one is being prepared while the other is being
// processed). Note that the contents of AXI SRAM is uninitialized, so the buffer contents on
// startup are undefined. The dimensions are `ADC_BUF[adc_index][ping_pong_index][sample_index]`.
#[link_section = ".axisram.buffers"]
static mut DAC_BUF: [[[u16; SAMPLE_BUFFER_SIZE]; 2]; 2] =
[[[0; SAMPLE_BUFFER_SIZE]; 2]; 2];
// The queue is provided a default length of 32 updates, but this queue can be updated by the
// end user to be larger if necessary.
outputs: heapless::spsc::Queue<(u16, u16), consts::U32>,
}
impl DacOutputs {
/// Construct a new set of DAC output controls
///
/// # Args
/// * `dac0_spi` - The SPI interface to the DAC0 output.
/// * `dac1_spi` - The SPI interface to the DAC1 output.
/// * `timer` - The timer used to generate periodic events for updating the DACs.
pub fn new(
mut dac0_spi: hal::spi::Spi<hal::stm32::SPI4, hal::spi::Enabled, u16>,
mut dac1_spi: hal::spi::Spi<hal::stm32::SPI5, hal::spi::Enabled, u16>,
mut timer: hal::timer::Timer<hal::stm32::TIM3>,
) -> Self {
// Start the DAC SPI interfaces in infinite transaction mode. CS is configured in
// auto-suspend mode.
dac0_spi.inner().cr1.modify(|_, w| w.cstart().started());
dac1_spi.inner().cr1.modify(|_, w| w.cstart().started());
dac0_spi.listen(hal::spi::Event::Error);
dac1_spi.listen(hal::spi::Event::Error);
// Stop the timer and begin listening for timeouts. Timeouts will be used as a means to
// generate new DAC outputs.
timer.pause();
timer.reset_counter();
timer.clear_irq();
timer.listen(hal::timer::Event::TimeOut);
Self {
dac0_spi,
dac1_spi,
outputs: heapless::spsc::Queue::new(),
timer,
macro_rules! dac_output {
($name:ident, $index:literal, $data_stream:ident,
$spi:ident, $trigger_channel:ident, $dma_req:ident) => {
/// $spi is used as a type for indicating a DMA transfer into the SPI TX FIFO
struct $spi {
spi: hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>,
_channel: sampling_timer::tim2::$trigger_channel,
}
}
/// Push a set of new DAC output codes to the internal queue.
///
/// # Note
/// The earlier DAC output codes will be generated within 1 update cycle of the codes. This is a
/// fixed latency currently.
///
/// This function will panic if too many codes are written.
///
/// # Args
/// * `dac0_value` - The value to enqueue for a DAC0 update.
/// * `dac1_value` - The value to enqueue for a DAC1 update.
pub fn push(&mut self, dac0_value: u16, dac1_value: u16) {
self.outputs.enqueue((dac0_value, dac1_value)).unwrap();
self.timer.resume();
}
/// Update the DAC codes with the next set of values in the internal queue.
///
/// # Note
/// This is intended to be called from the TIM3 update ISR.
///
/// If the last value in the queue is used, the timer is stopped.
pub fn update(&mut self) {
self.timer.clear_irq();
match self.outputs.dequeue() {
Some((dac0, dac1)) => self.write(dac0, dac1),
None => {
self.timer.pause();
self.timer.reset_counter();
self.timer.clear_irq();
impl $spi {
pub fn new(
_channel: sampling_timer::tim2::$trigger_channel,
spi: hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>,
) -> Self {
Self { _channel, spi }
}
};
}
/// Write immediate values to the DAC outputs.
///
/// # Note
/// The DACs will be updated as soon as the SPI transfer completes, which will be nominally
/// 320nS after this function call.
///
/// # Args
/// * `dac0_value` - The output code to write to DAC0.
/// * `dac1_value` - The output code to write to DAC1.
pub fn write(&mut self, dac0_value: u16, dac1_value: u16) {
// In order to optimize throughput and minimize latency, the DAC codes are written directly
// into the SPI TX FIFO. No error checking is conducted. Errors are handled via interrupts
// instead.
unsafe {
core::ptr::write_volatile(
&self.dac0_spi.inner().txdr as *const _ as *mut u16,
dac0_value,
);
core::ptr::write_volatile(
&self.dac1_spi.inner().txdr as *const _ as *mut u16,
dac1_value,
);
}
}
// Note(unsafe): This is safe because the DMA request line is logically owned by this module.
// Additionally, the SPI is owned by this structure and is known to be configured for u16 word
// sizes.
unsafe impl TargetAddress<MemoryToPeripheral> for $spi {
/// SPI is configured to operate using 16-bit transfer words.
type MemSize = u16;
/// SPI DMA requests are generated whenever TIM2 CHx ($dma_req) comparison occurs.
const REQUEST_LINE: Option<u8> = Some(DMAReq::$dma_req as u8);
/// Whenever the DMA request occurs, it should write into SPI's TX FIFO.
fn address(&self) -> u32 {
&self.spi.inner().txdr as *const _ as u32
}
}
/// Represents data associated with DAC.
pub struct $name {
next_buffer: Option<&'static mut [u16; SAMPLE_BUFFER_SIZE]>,
// Note: SPI TX functionality may not be used from this structure to ensure safety with DMA.
transfer: Transfer<
hal::dma::dma::$data_stream<hal::stm32::DMA1>,
$spi,
MemoryToPeripheral,
&'static mut [u16; SAMPLE_BUFFER_SIZE],
>,
first_transfer: bool,
}
impl $name {
/// Construct the DAC output channel.
///
/// # Args
/// * `spi` - The SPI interface used to communicate with the ADC.
/// * `stream` - The DMA stream used to write DAC codes over SPI.
/// * `trigger_channel` - The sampling timer output compare channel for update triggers.
pub fn new(
spi: hal::spi::Spi<hal::stm32::$spi, hal::spi::Enabled, u16>,
stream: hal::dma::dma::$data_stream<hal::stm32::DMA1>,
trigger_channel: sampling_timer::tim2::$trigger_channel,
) -> Self {
// Generate DMA events when an output compare of the timer hitting zero (timer roll over)
// occurs.
trigger_channel.listen_dma();
trigger_channel.to_output_compare(0);
// The stream constantly writes to the TX FIFO to write new update codes.
let trigger_config = DmaConfig::default()
.memory_increment(true)
.peripheral_increment(false);
// Listen for any potential SPI error signals, which may indicate that we are not generating
// update codes.
let mut spi = spi.disable();
spi.listen(hal::spi::Event::Error);
// Allow the SPI FIFOs to operate using only DMA data channels.
spi.enable_dma_tx();
// Enable SPI and start it in infinite transaction mode.
spi.inner().cr1.modify(|_, w| w.spe().set_bit());
spi.inner().cr1.modify(|_, w| w.cstart().started());
// Construct the trigger stream to write from memory to the peripheral.
let transfer: Transfer<_, _, MemoryToPeripheral, _> =
Transfer::init(
stream,
$spi::new(trigger_channel, spi),
// Note(unsafe): This buffer is only used once and provided for the DMA transfer.
unsafe { &mut DAC_BUF[$index][0] },
None,
trigger_config,
);
Self {
transfer,
// Note(unsafe): This buffer is only used once and provided for the next DMA transfer.
next_buffer: unsafe { Some(&mut DAC_BUF[$index][1]) },
first_transfer: true,
}
}
/// Acquire the next output buffer to populate it with DAC codes.
pub fn acquire_buffer(
&mut self,
) -> &'static mut [u16; SAMPLE_BUFFER_SIZE] {
self.next_buffer.take().unwrap()
}
/// Enqueue the next buffer for transmission to the DAC.
///
/// # Args
/// * `data` - The next data to write to the DAC.
pub fn release_buffer(
&mut self,
next_buffer: &'static mut [u16; SAMPLE_BUFFER_SIZE],
) {
// If the last transfer was not complete, we didn't write all our previous DAC codes.
// Wait for all the DAC codes to get written as well.
if self.first_transfer {
self.first_transfer = false
} else {
// Note: If a device hangs up, check that this conditional is passing correctly, as
// there is no time-out checks here in the interest of execution speed.
while !self.transfer.get_transfer_complete_flag() {}
}
// Start the next transfer.
self.transfer.clear_interrupts();
let (prev_buffer, _) =
self.transfer.next_transfer(next_buffer).unwrap();
// .unwrap_none() https://github.com/rust-lang/rust/issues/62633
self.next_buffer.replace(prev_buffer);
}
}
};
}
dac_output!(Dac0Output, 0, Stream4, SPI4, Channel3, TIM2_CH3);
dac_output!(Dac1Output, 1, Stream5, SPI5, Channel4, TIM2_CH4);

6
src/design_parameters.rs Normal file
View File

@ -0,0 +1,6 @@
/// The ADC setup time is the number of seconds after the CSn line goes low before the serial clock
/// may begin. This is used for performing the internal ADC conversion.
pub const ADC_SETUP_TIME: f32 = 220e-9;
/// The maximum DAC/ADC serial clock line frequency. This is a hardware limit.
pub const ADC_DAC_SCK_MHZ_MAX: u32 = 50;

View File

@ -1,108 +0,0 @@
use core::ops::{Add, Mul};
use serde::{Deserialize, Serialize};
use core::f32;
pub type IIRState = [f32; 5];
#[derive(Copy, Clone, Deserialize, Serialize)]
pub struct IIR {
pub ba: IIRState,
pub y_offset: f32,
pub y_min: f32,
pub y_max: f32,
}
fn abs(x: f32) -> f32 {
if x >= 0. {
x
} else {
-x
}
}
fn copysign(x: f32, y: f32) -> f32 {
if (x >= 0. && y >= 0.) || (x <= 0. && y <= 0.) {
x
} else {
-x
}
}
fn max(x: f32, y: f32) -> f32 {
if x > y {
x
} else {
y
}
}
fn min(x: f32, y: f32) -> f32 {
if x < y {
x
} else {
y
}
}
fn macc<T>(y0: T, x: &[T], a: &[T]) -> T
where
T: Add<Output = T> + Mul<Output = T> + Copy,
{
x.iter()
.zip(a.iter())
.map(|(&i, &j)| i * j)
.fold(y0, |y, xa| y + xa)
}
impl IIR {
pub fn set_pi(&mut self, kp: f32, ki: f32, g: f32) -> Result<(), &str> {
let ki = copysign(ki, kp);
let g = copysign(g, kp);
let (a1, b0, b1) = if abs(ki) < f32::EPSILON {
(0., kp, 0.)
} else {
let c = if abs(g) < f32::EPSILON {
1.
} else {
1. / (1. + ki / g)
};
let a1 = 2. * c - 1.;
let b0 = ki * c + kp;
let b1 = ki * c - a1 * kp;
if abs(b0 + b1) < f32::EPSILON {
return Err("low integrator gain and/or gain limit");
}
(a1, b0, b1)
};
self.ba[0] = b0;
self.ba[1] = b1;
self.ba[2] = 0.;
self.ba[3] = a1;
self.ba[4] = 0.;
Ok(())
}
pub fn get_x_offset(&self) -> Result<f32, &str> {
let b: f32 = self.ba[..3].iter().sum();
if abs(b) < f32::EPSILON {
Err("b is zero")
} else {
Ok(self.y_offset / b)
}
}
pub fn set_x_offset(&mut self, xo: f32) {
let b: f32 = self.ba[..3].iter().sum();
self.y_offset = xo * b;
}
pub fn update(&self, xy: &mut IIRState, x0: f32) -> f32 {
xy.rotate_right(1);
xy[0] = x0;
let y0 = macc(self.y_offset, xy, &self.ba);
let y0 = max(self.y_min, min(self.y_max, y0));
xy[xy.len() / 2] = y0;
y0
}
}

View File

@ -13,6 +13,9 @@
fn panic(_info: &core::panic::PanicInfo) -> ! {
let gpiod = unsafe { &*hal::stm32::GPIOD::ptr() };
gpiod.odr.modify(|_, w| w.odr6().high().odr12().high()); // FP_LED_1, FP_LED_3
#[cfg(feature = "nightly")]
core::intrinsics::abort();
#[cfg(not(feature = "nightly"))]
unsafe {
core::intrinsics::abort();
}
@ -51,8 +54,15 @@ use smoltcp::wire::Ipv4Address;
use heapless::{consts::*, String};
// The desired sampling frequency of the ADCs.
const SAMPLE_FREQUENCY_KHZ: u32 = 500;
// The desired ADC sample processing buffer size.
const SAMPLE_BUFFER_SIZE: usize = 1;
// The number of cascaded IIR biquads per channel. Select 1 or 2!
const IIR_CASCADE_LENGTH: usize = 1;
#[link_section = ".sram3.eth"]
static mut DES_RING: ethernet::DesRing = ethernet::DesRing::new();
@ -63,12 +73,15 @@ mod digital_input_stamper;
mod eeprom;
mod hrtimer;
mod iir;
mod design_parameters;
mod eeprom;
mod pounder;
mod sampling_timer;
mod server;
use adc::{Adc0Input, Adc1Input, AdcInputs};
use dac::DacOutputs;
use adc::{Adc0Input, Adc1Input};
use dac::{Dac0Output, Dac1Output};
use dsp::iir;
#[cfg(not(feature = "semihosting"))]
fn init_log() {}
@ -137,6 +150,7 @@ macro_rules! route_request {
match $request.attribute {
$(
$read_attribute => {
#[allow(clippy::redundant_closure_call)]
let value = match $getter() {
Ok(data) => data,
Err(_) => return server::Response::error($request.attribute,
@ -165,6 +179,7 @@ macro_rules! route_request {
"Failed to decode value"),
};
#[allow(clippy::redundant_closure_call)]
match $setter(new_value) {
Ok(_) => server::Response::success($request.attribute, &$request.value),
Err(_) => server::Response::error($request.attribute,
@ -182,17 +197,13 @@ macro_rules! route_request {
#[rtic::app(device = stm32h7xx_hal::stm32, peripherals = true, monotonic = rtic::cyccnt::CYCCNT)]
const APP: () = {
struct Resources {
afe0: AFE0,
afe1: AFE1,
adcs: AdcInputs,
dacs: DacOutputs,
afes: (AFE0, AFE1),
adcs: (Adc0Input, Adc1Input),
dacs: (Dac0Output, Dac1Output),
input_stamper: digital_input_stamper::InputStamper,
eeprom_i2c: hal::i2c::I2c<hal::stm32::I2C2>,
profiles: heapless::spsc::Queue<[u32; 4], heapless::consts::U32>,
// Note: It appears that rustfmt generates a format that GDB cannot recognize, which
// results in GDB breakpoints being set improperly.
#[rustfmt::skip]
@ -206,10 +217,11 @@ const APP: () = {
pounder: Option<pounder::PounderDevices>,
#[init([[0.; 5]; 2])]
iir_state: [iir::IIRState; 2],
#[init([iir::IIR { ba: [1., 0., 0., 0., 0.], y_offset: 0., y_min: -SCALE - 1., y_max: SCALE }; 2])]
iir_ch: [iir::IIR; 2],
// Format: iir_state[ch][cascade-no][coeff]
#[init([[[0.; 5]; IIR_CASCADE_LENGTH]; 2])]
iir_state: [[iir::IIRState; IIR_CASCADE_LENGTH]; 2],
#[init([[iir::IIR { ba: [1., 0., 0., 0., 0.], y_offset: 0., y_min: -SCALE - 1., y_max: SCALE }; IIR_CASCADE_LENGTH]; 2])]
iir_ch: [[iir::IIR; IIR_CASCADE_LENGTH]; 2],
}
#[init]
@ -300,12 +312,12 @@ const APP: () = {
})
.manage_cs()
.suspend_when_inactive()
.cs_delay(220e-9);
.cs_delay(design_parameters::ADC_SETUP_TIME);
let spi: hal::spi::Spi<_, _, u16> = dp.SPI2.spi(
(spi_sck, spi_miso, hal::spi::NoMosi),
config,
50.mhz(),
design_parameters::ADC_DAC_SCK_MHZ_MAX.mhz(),
ccdr.peripheral.SPI2,
&ccdr.clocks,
);
@ -338,12 +350,12 @@ const APP: () = {
})
.manage_cs()
.suspend_when_inactive()
.cs_delay(220e-9);
.cs_delay(design_parameters::ADC_SETUP_TIME);
let spi: hal::spi::Spi<_, _, u16> = dp.SPI3.spi(
(spi_sck, spi_miso, hal::spi::NoMosi),
config,
50.mhz(),
design_parameters::ADC_DAC_SCK_MHZ_MAX.mhz(),
ccdr.peripheral.SPI3,
&ccdr.clocks,
);
@ -356,7 +368,7 @@ const APP: () = {
)
};
AdcInputs::new(adc0, adc1)
(adc0, adc1)
};
let dacs = {
@ -393,7 +405,7 @@ const APP: () = {
dp.SPI4.spi(
(spi_sck, spi_miso, hal::spi::NoMosi),
config,
50.mhz(),
design_parameters::ADC_DAC_SCK_MHZ_MAX.mhz(),
ccdr.peripheral.SPI4,
&ccdr.clocks,
)
@ -425,19 +437,23 @@ const APP: () = {
dp.SPI5.spi(
(spi_sck, spi_miso, hal::spi::NoMosi),
config,
50.mhz(),
design_parameters::ADC_DAC_SCK_MHZ_MAX.mhz(),
ccdr.peripheral.SPI5,
&ccdr.clocks,
)
};
let timer = dp.TIM3.timer(
SAMPLE_FREQUENCY_KHZ.khz(),
ccdr.peripheral.TIM3,
&ccdr.clocks,
let dac0 = Dac0Output::new(
dac0_spi,
dma_streams.4,
sampling_timer_channels.ch3,
);
DacOutputs::new(dac0_spi, dac1_spi, timer)
let dac1 = Dac1Output::new(
dac1_spi,
dma_streams.5,
sampling_timer_channels.ch4,
);
(dac0, dac1)
};
let mut fp_led_0 = gpiod.pd5.into_push_pull_output();
@ -707,7 +723,7 @@ const APP: () = {
dp.ETHERNET_MTL,
dp.ETHERNET_DMA,
&mut DES_RING,
mac_addr.clone(),
mac_addr,
ccdr.peripheral.ETH1MAC,
&ccdr.clocks,
)
@ -768,12 +784,10 @@ const APP: () = {
sampling_timer.start();
init::LateResources {
afe0: afe0,
afe1: afe1,
afes: (afe0, afe1),
adcs,
dacs,
input_stamper,
pounder: pounder_devices,
@ -792,64 +806,38 @@ const APP: () = {
let _timestamps = c.resources.input_stamper.transfer_complete_handler();
}
#[task(binds = TIM3, resources=[dacs, profiles, pounder], priority = 3)]
fn dac_update(c: dac_update::Context) {
c.resources.dacs.update();
#[task(binds=DMA1_STR3, resources=[adcs, dacs, iir_state, iir_ch], priority=2)]
fn process(c: process::Context) {
let adc_samples = [
c.resources.adcs.0.acquire_buffer(),
c.resources.adcs.1.acquire_buffer(),
];
let dac_samples = [
c.resources.dacs.0.acquire_buffer(),
c.resources.dacs.1.acquire_buffer(),
];
if let Some(pounder) = c.resources.pounder {
if let Some(profile) = c.resources.profiles.dequeue() {
pounder.ad9959.interface.write_profile(profile).unwrap();
pounder.io_update_trigger.trigger();
for channel in 0..adc_samples.len() {
for sample in 0..adc_samples[0].len() {
let x = f32::from(adc_samples[channel][sample] as i16);
let mut y = x;
for i in 0..c.resources.iir_state[channel].len() {
y = c.resources.iir_ch[channel][i]
.update(&mut c.resources.iir_state[channel][i], y);
}
// Note(unsafe): The filter limits ensure that the value is in range.
// The truncation introduces 1/2 LSB distortion.
let y = unsafe { y.to_int_unchecked::<i16>() };
// Convert to DAC code
dac_samples[channel][sample] = y as u16 ^ 0x8000;
}
}
let [dac0, dac1] = dac_samples;
c.resources.dacs.0.release_buffer(dac0);
c.resources.dacs.1.release_buffer(dac1);
}
#[task(binds=DMA1_STR3, resources=[adcs, dacs, pounder, profiles, iir_state, iir_ch], priority=2)]
fn adc_update(mut c: adc_update::Context) {
let (adc0_samples, adc1_samples) =
c.resources.adcs.transfer_complete_handler();
for (adc0, adc1) in adc0_samples.iter().zip(adc1_samples.iter()) {
let result_adc0 = {
let x0 = f32::from(*adc0 as i16);
let y0 = c.resources.iir_ch[0]
.update(&mut c.resources.iir_state[0], x0);
y0 as i16 as u16 ^ 0x8000
};
let result_adc1 = {
let x1 = f32::from(*adc1 as i16);
let y1 = c.resources.iir_ch[1]
.update(&mut c.resources.iir_state[1], x1);
y1 as i16 as u16 ^ 0x8000
};
c.resources
.dacs
.lock(|dacs| dacs.push(result_adc0, result_adc1));
let profiles = &mut c.resources.profiles;
c.resources.pounder.lock(|pounder| {
if let Some(pounder) = pounder {
profiles.lock(|profiles| {
let profile = pounder
.ad9959
.serialize_profile(
pounder::Channel::Out0.into(),
100_000_000_f32,
0.0_f32,
*adc0 as f32 / 0xFFFF as f32,
)
.unwrap();
profiles.enqueue(profile).unwrap();
});
}
});
}
}
#[idle(resources=[net_interface, pounder, mac_addr, eth_mac, iir_state, iir_ch, afe0, afe1])]
#[idle(resources=[net_interface, pounder, mac_addr, eth_mac, iir_state, iir_ch, afes])]
fn idle(mut c: idle::Context) -> ! {
let mut socket_set_entries: [_; 8] = Default::default();
let mut sockets =
@ -901,16 +889,58 @@ const APP: () = {
let state = c.resources.iir_state.lock(|iir_state|
server::Status {
t: time,
x0: iir_state[0][0],
y0: iir_state[0][2],
x1: iir_state[1][0],
y1: iir_state[1][2],
x0: iir_state[0][0][0],
y0: iir_state[0][0][2],
x1: iir_state[1][0][0],
y1: iir_state[1][0][2],
});
Ok::<server::Status, ()>(state)
}),
"stabilizer/afe0/gain": (|| c.resources.afe0.get_gain()),
"stabilizer/afe1/gain": (|| c.resources.afe1.get_gain()),
// "_b" means cascades 2nd IIR
"stabilizer/iir_b/state": (|| {
let state = c.resources.iir_state.lock(|iir_state|
server::Status {
t: time,
x0: iir_state[0][IIR_CASCADE_LENGTH-1][0],
y0: iir_state[0][IIR_CASCADE_LENGTH-1][2],
x1: iir_state[1][IIR_CASCADE_LENGTH-1][0],
y1: iir_state[1][IIR_CASCADE_LENGTH-1][2],
});
Ok::<server::Status, ()>(state)
}),
"stabilizer/afe0/gain": (|| c.resources.afes.0.get_gain()),
"stabilizer/afe1/gain": (|| c.resources.afes.1.get_gain()),
"pounder/in0": (|| {
match c.resources.pounder {
Some(pounder) =>
pounder.get_input_channel_state(pounder::Channel::In0),
_ => Err(pounder::Error::Access),
}
}),
"pounder/in1": (|| {
match c.resources.pounder {
Some(pounder) =>
pounder.get_input_channel_state(pounder::Channel::In1),
_ => Err(pounder::Error::Access),
}
}),
"pounder/out0": (|| {
match c.resources.pounder {
Some(pounder) =>
pounder.get_output_channel_state(pounder::Channel::Out0),
_ => Err(pounder::Error::Access),
}
}),
"pounder/out1": (|| {
match c.resources.pounder {
Some(pounder) =>
pounder.get_output_channel_state(pounder::Channel::Out1),
_ => Err(pounder::Error::Access),
}
}),
>>>>>>> master
"pounder/dds/clock": (|| {
c.resources.pounder.lock(|pounder| {
match pounder {
@ -928,7 +958,7 @@ const APP: () = {
return Err(());
}
iir_ch[req.channel as usize] = req.iir;
iir_ch[req.channel as usize][0] = req.iir;
Ok::<server::IirRequest, ()>(req)
})
@ -939,7 +969,29 @@ const APP: () = {
return Err(());
}
iir_ch[req.channel as usize] = req.iir;
iir_ch[req.channel as usize][0] = req.iir;
Ok::<server::IirRequest, ()>(req)
})
}),
"stabilizer/iir_b0/state": server::IirRequest, (|req: server::IirRequest| {
c.resources.iir_ch.lock(|iir_ch| {
if req.channel > 1 {
return Err(());
}
iir_ch[req.channel as usize][IIR_CASCADE_LENGTH-1] = req.iir;
Ok::<server::IirRequest, ()>(req)
})
}),
"stabilizer/iir_b1/state": server::IirRequest,(|req: server::IirRequest| {
c.resources.iir_ch.lock(|iir_ch| {
if req.channel > 1 {
return Err(());
}
iir_ch[req.channel as usize][IIR_CASCADE_LENGTH-1] = req.iir;
Ok::<server::IirRequest, ()>(req)
})
@ -989,10 +1041,12 @@ const APP: () = {
})
}),
"stabilizer/afe0/gain": afe::Gain, (|gain| {
Ok::<(), ()>(c.resources.afe0.set_gain(gain))
c.resources.afes.0.set_gain(gain);
Ok::<(), ()>(())
}),
"stabilizer/afe1/gain": afe::Gain, (|gain| {
Ok::<(), ()>(c.resources.afe1.set_gain(gain))
c.resources.afes.1.set_gain(gain);
Ok::<(), ()>(())
})
]
)
@ -1004,7 +1058,7 @@ const APP: () = {
&mut sockets,
net::time::Instant::from_millis(time as i64),
) {
Ok(changed) => changed == false,
Ok(changed) => !changed,
Err(net::Error::Unrecognized) => true,
Err(e) => {
info!("iface poll error: {:?}", e);
@ -1023,22 +1077,22 @@ const APP: () = {
unsafe { ethernet::interrupt_handler() }
}
#[task(binds = SPI2, priority = 1)]
#[task(binds = SPI2, priority = 3)]
fn spi2(_: spi2::Context) {
panic!("ADC0 input overrun");
}
#[task(binds = SPI3, priority = 1)]
#[task(binds = SPI3, priority = 3)]
fn spi3(_: spi3::Context) {
panic!("ADC0 input overrun");
}
#[task(binds = SPI4, priority = 1)]
#[task(binds = SPI4, priority = 3)]
fn spi4(_: spi4::Context) {
panic!("DAC0 output error");
}
#[task(binds = SPI5, priority = 1)]
#[task(binds = SPI5, priority = 3)]
fn spi5(_: spi5::Context) {
panic!("DAC1 output error");
}

View File

@ -19,7 +19,7 @@ pub trait AttenuatorInterface {
channel: Channel,
attenuation: f32,
) -> Result<f32, Error> {
if attenuation > 31.5 || attenuation < 0.0 {
if !(0.0..=31.5).contains(&attenuation) {
return Err(Error::Bounds);
}

View File

@ -18,7 +18,7 @@ const ATT_RST_N_PIN: u8 = 8 + 5;
const ATT_LE3_PIN: u8 = 8 + 3;
const ATT_LE2_PIN: u8 = 8 + 2;
const ATT_LE1_PIN: u8 = 8 + 1;
const ATT_LE0_PIN: u8 = 8 + 0;
const ATT_LE0_PIN: u8 = 8;
#[derive(Debug, Copy, Clone)]
pub enum Error {

View File

@ -1,112 +1,120 @@
///! The sampling timer is used for managing ADC sampling and external reference timestamping.
use super::hal;
use hal::dma::{dma::DMAReq, traits::TargetAddress, PeripheralToMemory};
pub use hal::stm32::tim2::ccmr2_input::CC4S_A;
/// The timer used for managing ADC sampling.
pub struct SamplingTimer {
timer: hal::timer::Timer<hal::stm32::TIM2>,
channels: Option<TimerChannels>,
channels: Option<tim2::Channels>,
}
impl SamplingTimer {
/// Construct the sampling timer.
pub fn new(mut timer: hal::timer::Timer<hal::stm32::TIM2>) -> Self {
timer.pause();
Self {
timer,
channels: Some(TimerChannels::new()),
// Note(unsafe): Once these channels are taken, we guarantee that we do not modify any
// of the underlying timer channel registers, as ownership of the channels is now
// provided through the associated channel structures. We additionally guarantee this
// can only be called once because there is only one Timer2 and this resource takes
// ownership of it once instantiated.
channels: unsafe { Some(tim2::Channels::new()) },
}
}
pub fn channels(&mut self) -> TimerChannels {
/// Get the timer capture/compare channels.
pub fn channels(&mut self) -> tim2::Channels {
self.channels.take().unwrap()
}
/// Start the sampling timer.
pub fn start(&mut self) {
self.timer.reset_counter();
self.timer.resume();
}
}
pub struct TimerChannels {
pub ch1: Timer2Channel1,
pub ch2: Timer2Channel2,
pub ch3: Timer2Channel3,
pub ch4: Timer2Channel4,
macro_rules! timer_channel {
($name:ident, $TY:ty, ($ccxde:expr, $ccrx:expr, $ccmrx_output:expr, $ccxs:expr)) => {
pub struct $name {}
paste::paste! {
impl $name {
/// Construct a new timer channel.
///
/// Note(unsafe): This function must only be called once. Once constructed, the
/// constructee guarantees to never modify the timer channel.
unsafe fn new() -> Self {
Self {}
}
/// Allow CH4 to generate DMA requests.
pub fn listen_dma(&self) {
let regs = unsafe { &*<$TY>::ptr() };
regs.dier.modify(|_, w| w.[< $ccxde >]().set_bit());
}
/// Operate CH2 as an output-compare.
///
/// # Args
/// * `value` - The value to compare the sampling timer's counter against.
pub fn to_output_compare(&self, value: u32) {
let regs = unsafe { &*<$TY>::ptr() };
assert!(value <= regs.arr.read().bits());
regs.[< $ccrx >].write(|w| w.ccr().bits(value));
regs.[< $ccmrx_output >]()
.modify(|_, w| unsafe { w.[< $ccxs >]().bits(0) });
}
}
}
};
}
impl TimerChannels {
fn new() -> Self {
Self {
ch1: Timer2Channel1 {},
ch2: Timer2Channel2 {},
ch3: Timer2Channel3 {},
ch4: Timer2Channel4 {},
pub mod tim2 {
use stm32h7xx_hal as hal;
/// The channels representing the timer.
pub struct Channels {
pub ch1: Channel1,
pub ch2: Channel2,
pub ch3: Channel3,
pub ch4: Channel4,
}
impl Channels {
/// Construct a new set of channels.
///
/// Note(unsafe): This is only safe to call once.
pub unsafe fn new() -> Self {
Self {
ch1: Channel1::new(),
ch2: Channel2::new(),
ch3: Channel3::new(),
ch4: Channel4::new(),
}
}
}
}
pub struct Timer2Channel1 {}
impl Timer2Channel1 {
pub fn listen_dma(&self) {
let regs = unsafe { &*hal::stm32::TIM2::ptr() };
regs.dier.modify(|_, w| w.cc1de().set_bit());
}
pub fn to_output_compare(&self, value: u32) {
let regs = unsafe { &*hal::stm32::TIM2::ptr() };
assert!(value <= regs.arr.read().bits());
regs.ccr1.write(|w| w.ccr().bits(value));
regs.ccmr1_output()
.modify(|_, w| unsafe { w.cc1s().bits(0) });
}
}
pub struct Timer2Channel2 {}
impl Timer2Channel2 {
pub fn listen_dma(&self) {
let regs = unsafe { &*hal::stm32::TIM2::ptr() };
regs.dier.modify(|_, w| w.cc2de().set_bit());
}
pub fn to_output_compare(&self, value: u32) {
let regs = unsafe { &*hal::stm32::TIM2::ptr() };
assert!(value <= regs.arr.read().bits());
regs.ccr2.write(|w| w.ccr().bits(value));
regs.ccmr1_output()
.modify(|_, w| unsafe { w.cc2s().bits(0) });
}
}
pub struct Timer2Channel3 {}
pub struct Timer2Channel4 {}
unsafe impl TargetAddress<PeripheralToMemory> for Timer2Channel4 {
type MemSize = u16;
const REQUEST_LINE: Option<u8> = Some(DMAReq::TIM2_CH4 as u8);
fn address(&self) -> u32 {
let regs = unsafe { &*hal::stm32::TIM2::ptr() };
&regs.dmar as *const _ as u32
}
}
impl Timer2Channel4 {
pub fn listen_dma(&self) {
let regs = unsafe { &*hal::stm32::TIM2::ptr() };
regs.dier.modify(|_, w| w.cc4de().set_bit());
}
pub fn to_input_capture(&self, trig: CC4S_A) {
let regs = unsafe { &*hal::stm32::TIM2::ptr() };
regs.ccmr2_input().modify(|_, w| w.cc4s().variant(trig));
// Update the DMA control burst regs to point to CCR4.
regs.dcr
.modify(|_, w| unsafe { w.dbl().bits(1).dba().bits(16) });
}
timer_channel!(
Channel1,
hal::stm32::TIM2,
(cc1de, ccr1, ccmr1_output, cc1s)
);
timer_channel!(
Channel2,
hal::stm32::TIM2,
(cc2de, ccr2, ccmr1_output, cc1s)
);
timer_channel!(
Channel3,
hal::stm32::TIM2,
(cc3de, ccr3, ccmr2_output, cc3s)
);
timer_channel!(
Channel4,
hal::stm32::TIM2,
(cc4de, ccr4, ccmr2_output, cc4s)
);
}

View File

@ -89,7 +89,7 @@ impl Response {
/// Args:
/// * `attrbute` - The attribute of the success.
/// * `value` - The value of the attribute.
pub fn success<'a, 'b>(attribute: &'a str, value: &'b str) -> Self {
pub fn success(attribute: &str, value: &str) -> Self {
let mut res = Self {
code: 200,
attribute: String::from(attribute),
@ -106,7 +106,7 @@ impl Response {
/// Args:
/// * `attrbute` - The attribute of the success.
/// * `message` - The message denoting the error.
pub fn error<'a, 'b>(attribute: &'a str, message: &'b str) -> Self {
pub fn error(attribute: &str, message: &str) -> Self {
let mut res = Self {
code: 400,
attribute: String::from(attribute),
@ -123,7 +123,7 @@ impl Response {
/// Args:
/// * `attrbute` - The attribute of the success.
/// * `message` - The message denoting the status.
pub fn custom<'a>(code: i32, message: &'a str) -> Self {
pub fn custom(code: i32, message: &str) -> Self {
let mut res = Self {
code,
attribute: String::from(""),