Merge branch 'master' into feature/digital-input-stamp
This commit is contained in:
commit
93ab3b7dfd
@ -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"
|
||||
|
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@ -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
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
/dsp/target
|
||||
.gdb_history
|
||||
|
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(&litude) {
|
||||
return Err(Error::Bounds);
|
||||
}
|
||||
|
||||
|
70
dsp/Cargo.lock
generated
Normal file
70
dsp/Cargo.lock
generated
Normal 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
12
dsp/Cargo.toml
Normal 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
205
dsp/src/iir.rs
Normal 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
11
dsp/src/lib.rs
Normal 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
518
dsp/src/lockin.rs
Normal 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
97
dsp/src/pll.rs
Normal 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
27
dsp/src/testing.rs
Normal 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
83
dsp/src/unwrap.rs
Normal 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
1137
dsp/tests/lockin_low_pass.rs
Normal file
File diff suppressed because it is too large
Load Diff
2
memory.x
2
memory.x
@ -17,7 +17,7 @@ SECTIONS {
|
||||
*(.itcm .itcm.*);
|
||||
. = ALIGN(8);
|
||||
} > ITCM
|
||||
.axisram : ALIGN(8) {
|
||||
.axisram (NOLOAD) : ALIGN(8) {
|
||||
*(.axisram .axisram.*);
|
||||
. = ALIGN(8);
|
||||
} > AXISRAM
|
||||
|
498
src/adc.rs
498
src/adc.rs
@ -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() };
|
||||
®s.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() };
|
||||
®s.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() };
|
||||
®s.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);
|
||||
|
255
src/dac.rs
255
src/dac.rs
@ -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
6
src/design_parameters.rs
Normal 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;
|
108
src/iir.rs
108
src/iir.rs
@ -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
|
||||
}
|
||||
}
|
250
src/main.rs
250
src/main.rs
@ -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");
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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() };
|
||||
®s.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)
|
||||
);
|
||||
}
|
||||
|
@ -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(""),
|
||||
|
Loading…
Reference in New Issue
Block a user