Compare commits
3
Commits
31edf91933
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e87561b37
|
||
|
|
34df570dda
|
||
|
|
bdd946ad11
|
@@ -0,0 +1,66 @@
|
|||||||
|
name: Build and Release Firmware
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
TARGET: thumbv8m.main-none-eabihf
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Install cargo-binutils
|
||||||
|
run: cargo install cargo-binutils
|
||||||
|
- name: Build Firmware Binaries
|
||||||
|
run: |
|
||||||
|
cd firmware
|
||||||
|
|
||||||
|
# Array of "variant_name:features"
|
||||||
|
VARIANTS=(
|
||||||
|
"cs4398:cs4398,hid"
|
||||||
|
"ak4490:ak4490,hid"
|
||||||
|
"evk:evk,hid"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p ../dist
|
||||||
|
|
||||||
|
for entry in "${VARIANTS[@]}"; do
|
||||||
|
IFS=":" read -r name features <<< "$entry"
|
||||||
|
echo "Building variant: $name with features: $features"
|
||||||
|
|
||||||
|
# 1. Compile release binary
|
||||||
|
cargo build --release --target ${{ env.TARGET }} --no-default-features --features "$features"
|
||||||
|
|
||||||
|
# 2. Paths setup
|
||||||
|
ELF_PATH="../target/${{ env.TARGET }}/release/guac"
|
||||||
|
HEX_PATH="../target/${{ env.TARGET }}/release/guac-${name}.hex"
|
||||||
|
RENAMED_ELF="../target/${{ env.TARGET }}/release/guac-${name}.elf"
|
||||||
|
|
||||||
|
# 3. Create .hex file via rust-objcopy
|
||||||
|
cargo objcopy --release --target ${{ env.TARGET }} --no-default-features --features "$features" -- -O ihex "$HEX_PATH"
|
||||||
|
cp "$ELF_PATH" "$RENAMED_ELF"
|
||||||
|
|
||||||
|
# 4. Package ELF and HEX into target-specific ZIP
|
||||||
|
ZIP_NAME="guac-firmware-${name}.zip"
|
||||||
|
zip -j "../dist/${ZIP_NAME}" "$RENAMED_ELF" "$HEX_PATH"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
uses: akkuman/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
files: dist/*.zip
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
Generated
+38
-37
@@ -217,9 +217,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck_derive",
|
"bytemuck_derive",
|
||||||
]
|
]
|
||||||
@@ -515,14 +515,14 @@ version = "0.3.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
|
checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.1.0",
|
"defmt 1.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt"
|
name = "defmt"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
|
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"defmt-macros",
|
"defmt-macros",
|
||||||
@@ -530,12 +530,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt-macros"
|
name = "defmt-macros"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
|
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt-parser",
|
"defmt-parser",
|
||||||
"proc-macro-error2",
|
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
@@ -557,7 +556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c0f73a4a4a91609e977ae3b7bd831ffa292edfd42ad140a3244a61d805b0e05e"
|
checksum = "c0f73a4a4a91609e977ae3b7bd831ffa292edfd42ad140a3244a61d805b0e05e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"defmt 1.1.0",
|
"defmt 1.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -757,14 +756,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "guac"
|
name = "guac"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic",
|
"atomic",
|
||||||
"bbqueue",
|
"bbqueue",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cortex-m",
|
"cortex-m",
|
||||||
"cortex-m-rt",
|
"cortex-m-rt",
|
||||||
"defmt 1.1.0",
|
"defmt 1.1.1",
|
||||||
"defmt-rtt",
|
"defmt-rtt",
|
||||||
"embedded-hal",
|
"embedded-hal",
|
||||||
"embedded-io",
|
"embedded-io",
|
||||||
@@ -1178,6 +1177,27 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||||
|
dependencies = [
|
||||||
|
"num_enum_derive",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum_derive"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2"
|
name = "objc2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -1245,7 +1265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a"
|
checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cortex-m",
|
"cortex-m",
|
||||||
"defmt 1.1.0",
|
"defmt 1.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1338,28 +1358,6 @@ dependencies = [
|
|||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-error-attr2"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-error2"
|
|
||||||
version = "2.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-error-attr2",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -1538,7 +1536,10 @@ dependencies = [
|
|||||||
name = "shared"
|
name = "shared"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"defmt 1.1.1",
|
||||||
"deku",
|
"deku",
|
||||||
|
"num_enum",
|
||||||
"serde",
|
"serde",
|
||||||
"usbd-hid",
|
"usbd-hid",
|
||||||
]
|
]
|
||||||
@@ -1794,12 +1795,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usbd-uac2"
|
name = "usbd-uac2"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "117c289dcd316caa7aca9c0909117d8cf9d35f3ed2e7a5739067f0bcedc93e35"
|
checksum = "07c0e506a7eadf9dee3039a4fa00dee84b8ed0c4a5931424b81659e0800c4965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder-embedded-io",
|
"byteorder-embedded-io",
|
||||||
"defmt 1.1.0",
|
"defmt 1.1.1",
|
||||||
"embedded-io",
|
"embedded-io",
|
||||||
"modular-bitfield",
|
"modular-bitfield",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
|||||||
+5
-5
@@ -4,7 +4,7 @@ use async_hid::{AsyncHidRead, HidBackend, HidResult};
|
|||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use deku::DekuContainerRead;
|
use deku::DekuContainerRead;
|
||||||
use futures_lite::StreamExt;
|
use futures_lite::StreamExt;
|
||||||
use shared::hid::AudioTelemetryReport;
|
use shared::{AudioTelemetrySnapshot, hid::AudioTelemetryReport};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||||
enum Format {
|
enum Format {
|
||||||
@@ -23,7 +23,7 @@ trait StateEmitter<W: io::Write> {
|
|||||||
fn from_writer(writer: W) -> Self
|
fn from_writer(writer: W) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn emit(&mut self, r: &AudioTelemetryReport);
|
fn emit(&mut self, r: &AudioTelemetrySnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DebugEmitter<T: io::Write> {
|
struct DebugEmitter<T: io::Write> {
|
||||||
@@ -34,7 +34,7 @@ impl<W: io::Write> StateEmitter<W> for DebugEmitter<W> {
|
|||||||
fn from_writer(writer: W) -> Self {
|
fn from_writer(writer: W) -> Self {
|
||||||
Self { writer }
|
Self { writer }
|
||||||
}
|
}
|
||||||
fn emit(&mut self, r: &AudioTelemetryReport) {
|
fn emit(&mut self, r: &AudioTelemetrySnapshot) {
|
||||||
writeln!(self.writer, "{r:?}");
|
writeln!(self.writer, "{r:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ impl<W: io::Write> StateEmitter<W> for CsvEmitter<W> {
|
|||||||
csv: csv::Writer::from_writer(writer),
|
csv: csv::Writer::from_writer(writer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn emit(&mut self, r: &AudioTelemetryReport) {
|
fn emit(&mut self, r: &AudioTelemetrySnapshot) {
|
||||||
if let Err(e) = self.csv.serialize(r) {
|
if let Err(e) = self.csv.serialize(r) {
|
||||||
eprintln!("Serialization error: {e:?}");
|
eprintln!("Serialization error: {e:?}");
|
||||||
} else {
|
} else {
|
||||||
@@ -80,7 +80,7 @@ async fn main() -> HidResult<()> {
|
|||||||
log::debug!("read {}: {:?}", r, &buf[..r]);
|
log::debug!("read {}: {:?}", r, &buf[..r]);
|
||||||
let buf = &buf[..r];
|
let buf = &buf[..r];
|
||||||
match AudioTelemetryReport::from_bytes((buf, 0)) {
|
match AudioTelemetryReport::from_bytes((buf, 0)) {
|
||||||
Ok((_, r)) => writer.emit(&r),
|
Ok((_, r)) => writer.emit(&r.into()),
|
||||||
Err(e) => eprintln!("Unable to parse report: {:?}", e),
|
Err(e) => eprintln!("Unable to parse report: {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "guac"
|
name = "guac"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
@@ -9,11 +9,11 @@ ak4490 = []
|
|||||||
cs4398 = []
|
cs4398 = []
|
||||||
nodac = []
|
nodac = []
|
||||||
wm8904 = []
|
wm8904 = []
|
||||||
hid = [ "dep:usbd-hid", "dep:shared" ]
|
hid = [ "dep:usbd-hid" ]
|
||||||
evk = [ "wm8904" ]
|
evk = [ "wm8904" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
shared = { path="../shared", optional = true }
|
shared = { path="../shared" }
|
||||||
atomic = "0.6.1"
|
atomic = "0.6.1"
|
||||||
bbqueue = "0.7.0"
|
bbqueue = "0.7.0"
|
||||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||||
@@ -32,7 +32,7 @@ panic-probe = { version = "1.0.0", features = ["print-defmt"] }
|
|||||||
static_cell = "2.1.1"
|
static_cell = "2.1.1"
|
||||||
usb-device = { version = "0.3", features = ["control-buffer-256"] }
|
usb-device = { version = "0.3", features = ["control-buffer-256"] }
|
||||||
usbd-hid = { version = "0.10.0", optional = true }
|
usbd-hid = { version = "0.10.0", optional = true }
|
||||||
usbd-uac2 = { version = "0.1.0", features = ["defmt"]}
|
usbd-uac2 = { version = "0.1.2", features = ["defmt"]}
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ where
|
|||||||
fn cr1_for_rate(&self, rate: u32) -> u16 {
|
fn cr1_for_rate(&self, rate: u32) -> u16 {
|
||||||
let fs_ratio = self.mclk / rate;
|
let fs_ratio = self.mclk / rate;
|
||||||
if !self.mclk.is_multiple_of(rate) {
|
if !self.mclk.is_multiple_of(rate) {
|
||||||
defmt::warn!("sample rate should be a multiple of mclk");
|
defmt::warn!("[wm8904] sample rate should be a multiple of mclk");
|
||||||
}
|
}
|
||||||
let clk_sys_rate: u16 = match fs_ratio {
|
let clk_sys_rate: u16 = match fs_ratio {
|
||||||
64 => 0,
|
64 => 0,
|
||||||
@@ -144,7 +144,7 @@ where
|
|||||||
1408 => 8,
|
1408 => 8,
|
||||||
1536 => 9,
|
1536 => 9,
|
||||||
_ => {
|
_ => {
|
||||||
defmt::warn!("unsupport ratio {}", fs_ratio);
|
defmt::warn!("[wm8904] unsupport ratio {}", fs_ratio);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -181,26 +181,30 @@ where
|
|||||||
match self.i2c.write_read(WM8904_I2C_ADDRESS, &[0], &mut buf) {
|
match self.i2c.write_read(WM8904_I2C_ADDRESS, &[0], &mut buf) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let chip_id = ((buf[0] as u16) << 8) | buf[1] as u16;
|
let chip_id = ((buf[0] as u16) << 8) | buf[1] as u16;
|
||||||
defmt::info!("Read chip ID: {:x}", chip_id)
|
defmt::info!("[wm8904] Read chip ID: {:x}", chip_id)
|
||||||
}
|
}
|
||||||
Err(_) => defmt::error!("Error reading I2C"),
|
Err(_) => defmt::error!("[wm8904] Error reading I2C"),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_reg(RegisterAddress::ClockRates2, 0x000f); // OPCLK_ENA | CLK_SYS_ENA | CLK_DSP_ENA | TOCLK_ENA
|
self.write_reg(RegisterAddress::ClockRates2, 0x000f); // OPCLK_ENA | CLK_SYS_ENA | CLK_DSP_ENA | TOCLK_ENA
|
||||||
self.write_reg(RegisterAddress::WriteSeq0, 0x0100); // write sequencer 0 ENA
|
self.write_reg(RegisterAddress::WriteSeq0, 0x0100); // write sequencer 0 ENA
|
||||||
self.write_reg(RegisterAddress::WriteSeq3, 0x0100); // write sequencer 3 START, INDEX=0
|
self.write_reg(RegisterAddress::WriteSeq3, 0x0100); // write sequencer 3 START, INDEX=0
|
||||||
// wait on write sequencer
|
// wait on write sequencer
|
||||||
defmt::info!("[codec] waiting on write seq");
|
defmt::info!("[wm8904] waiting on write seq");
|
||||||
loop {
|
loop {
|
||||||
let mut buf = [0; 2];
|
let mut buf = [0; 2];
|
||||||
self.i2c
|
self.i2c
|
||||||
.write_read(WM8904_I2C_ADDRESS, &[0x70], &mut buf)
|
.write_read(
|
||||||
|
WM8904_I2C_ADDRESS,
|
||||||
|
&[RegisterAddress::WriteSeq4 as u8],
|
||||||
|
&mut buf,
|
||||||
|
)
|
||||||
.ok();
|
.ok();
|
||||||
if buf[1] & 1 == 0 {
|
if buf[1] & 1 == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defmt::debug!("[codec] write seq done");
|
defmt::debug!("[wm8904] write seq done");
|
||||||
self.write_reg(RegisterAddress::ClockRates0, 0);
|
self.write_reg(RegisterAddress::ClockRates0, 0);
|
||||||
self.write_reg(RegisterAddress::PowerMgmt0, 0); // IN PGAs disabled
|
self.write_reg(RegisterAddress::PowerMgmt0, 0); // IN PGAs disabled
|
||||||
self.write_reg(RegisterAddress::PowerMgmt2, 0x0003); // HPL_PGA_ENA | HPR_PGA_ENA
|
self.write_reg(RegisterAddress::PowerMgmt2, 0x0003); // HPL_PGA_ENA | HPR_PGA_ENA
|
||||||
@@ -236,7 +240,7 @@ where
|
|||||||
}
|
}
|
||||||
fn change_rate(&mut self, new_rate: u32) {
|
fn change_rate(&mut self, new_rate: u32) {
|
||||||
// TODO: mute, stop clocks etc.
|
// TODO: mute, stop clocks etc.
|
||||||
defmt::info!("dac rate -> {}", new_rate);
|
defmt::info!("[wm8904] dac rate -> {}", new_rate);
|
||||||
self.write_reg(RegisterAddress::ClockRates1, self.cr1_for_rate(new_rate));
|
self.write_reg(RegisterAddress::ClockRates1, self.cr1_for_rate(new_rate));
|
||||||
self.write_reg(
|
self.write_reg(
|
||||||
RegisterAddress::AudioInterface2,
|
RegisterAddress::AudioInterface2,
|
||||||
|
|||||||
+40
-53
@@ -70,29 +70,22 @@ impl core::fmt::Display for DmaError {
|
|||||||
/// Slot-based DMA ring
|
/// Slot-based DMA ring
|
||||||
pub struct DmaRing<const N: usize, const MAX_SLOT_BYTES: usize> {
|
pub struct DmaRing<const N: usize, const MAX_SLOT_BYTES: usize> {
|
||||||
dma: pac::DMA0,
|
dma: pac::DMA0,
|
||||||
|
|
||||||
/// Destination peripheral register (FIFO write register)
|
|
||||||
dst_reg: *mut u32,
|
dst_reg: *mut u32,
|
||||||
|
|
||||||
// SAFETY: only written by USB task (on start)
|
|
||||||
pub(crate) channel_desc: UnsafeCell<DescriptorTable>,
|
pub(crate) channel_desc: UnsafeCell<DescriptorTable>,
|
||||||
// SAFETY: only written by USB task (on start)
|
|
||||||
pub(crate) desc: UnsafeCell<RingDescriptors<N>>,
|
pub(crate) desc: UnsafeCell<RingDescriptors<N>>,
|
||||||
slots: UnsafeCell<[[u8; MAX_SLOT_BYTES]; N]>,
|
slots: UnsafeCell<[[u8; MAX_SLOT_BYTES]; N]>,
|
||||||
|
|
||||||
/// Effective bytes per slot. Maybe be smaller than MAX_SLOT_BYTES (e.g. at lower sample rates), as the setup is designed for constant rate not constant size.
|
/// Effective bytes per slot (atomic for interior mutability)
|
||||||
slot_bytes: usize,
|
slot_bytes: AtomicUsize,
|
||||||
/// How many bytes to transfer to the FIFO
|
|
||||||
word_bytes: usize,
|
word_bytes: usize,
|
||||||
|
|
||||||
// SAFETY: producer only
|
|
||||||
write_slot: UnsafeCell<usize>,
|
write_slot: UnsafeCell<usize>,
|
||||||
write_off: UnsafeCell<usize>,
|
write_off: UnsafeCell<usize>,
|
||||||
|
|
||||||
produced: AtomicUsize,
|
produced: AtomicUsize,
|
||||||
consumed: AtomicUsize,
|
consumed: AtomicUsize,
|
||||||
|
|
||||||
/// Leave at least one slot empty so producer never overwrites a slot DMA may still read.
|
|
||||||
safety_gap: usize,
|
safety_gap: usize,
|
||||||
pub produced_bytes: AtomicUsize,
|
pub produced_bytes: AtomicUsize,
|
||||||
pub consumed_bytes: AtomicUsize,
|
pub consumed_bytes: AtomicUsize,
|
||||||
@@ -132,7 +125,7 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
}; N],
|
}; N],
|
||||||
}),
|
}),
|
||||||
slots: UnsafeCell::new([[0u8; MAX_SLOT_BYTES]; N]),
|
slots: UnsafeCell::new([[0u8; MAX_SLOT_BYTES]; N]),
|
||||||
slot_bytes: MAX_SLOT_BYTES,
|
slot_bytes: AtomicUsize::new(MAX_SLOT_BYTES),
|
||||||
word_bytes,
|
word_bytes,
|
||||||
write_slot: UnsafeCell::new(0),
|
write_slot: UnsafeCell::new(0),
|
||||||
write_off: UnsafeCell::new(0),
|
write_off: UnsafeCell::new(0),
|
||||||
@@ -149,9 +142,10 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
self.safety_gap = gap_slots.min(N);
|
self.safety_gap = gap_slots.min(N);
|
||||||
}
|
}
|
||||||
pub fn slot_size(&self) -> usize {
|
pub fn slot_size(&self) -> usize {
|
||||||
self.slot_bytes
|
self.slot_bytes.load(Ordering::Acquire)
|
||||||
}
|
}
|
||||||
pub fn set_slot_size(&mut self, slot_bytes: usize) -> Result<(), ConfigError> {
|
|
||||||
|
pub fn set_slot_size(&self, slot_bytes: usize) -> Result<(), ConfigError> {
|
||||||
if slot_bytes == 0 {
|
if slot_bytes == 0 {
|
||||||
return Err(ConfigError::SlotTooSmall);
|
return Err(ConfigError::SlotTooSmall);
|
||||||
}
|
}
|
||||||
@@ -161,8 +155,13 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
if slot_bytes % self.word_bytes != 0 {
|
if slot_bytes % self.word_bytes != 0 {
|
||||||
return Err(ConfigError::SlotNotAligned);
|
return Err(ConfigError::SlotNotAligned);
|
||||||
}
|
}
|
||||||
self.slot_bytes = slot_bytes;
|
|
||||||
self.reset_producer();
|
// Update atomic size
|
||||||
|
self.slot_bytes.store(slot_bytes, Ordering::Release);
|
||||||
|
|
||||||
|
// Re-initialize descriptors and reset producer state safely through internal mutability
|
||||||
|
self.init_descriptors();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +184,8 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cap = self.slot_bytes - *write_off;
|
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||||
|
let cap = slot_bytes - *write_off;
|
||||||
let n = core::cmp::min(cap, data.len());
|
let n = core::cmp::min(cap, data.len());
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -197,7 +197,7 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
written += n;
|
written += n;
|
||||||
data = &data[n..];
|
data = &data[n..];
|
||||||
|
|
||||||
if *write_off == self.slot_bytes {
|
if *write_off == slot_bytes {
|
||||||
// publish completed slot
|
// publish completed slot
|
||||||
compiler_fence(Ordering::Release);
|
compiler_fence(Ordering::Release);
|
||||||
self.produced.fetch_add(1, Ordering::Release);
|
self.produced.fetch_add(1, Ordering::Release);
|
||||||
@@ -221,8 +221,9 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
let consumed = self.consumed.load(Ordering::Relaxed);
|
let consumed = self.consumed.load(Ordering::Relaxed);
|
||||||
if consumed < produced {
|
if consumed < produced {
|
||||||
self.consumed.fetch_add(slots, Ordering::Release);
|
self.consumed.fetch_add(slots, Ordering::Release);
|
||||||
|
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||||
self.consumed_bytes
|
self.consumed_bytes
|
||||||
.fetch_add(slots * self.slot_bytes, Ordering::Relaxed);
|
.fetch_add(slots * slot_bytes, Ordering::Relaxed);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
defmt::error!("DMA underrun!");
|
defmt::error!("DMA underrun!");
|
||||||
@@ -243,25 +244,23 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
loop {
|
loop {
|
||||||
let consumed_start = self.consumed.load(Ordering::Acquire);
|
let consumed_start = self.consumed.load(Ordering::Acquire);
|
||||||
|
|
||||||
let reg_1 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
let reg_1 = (self.dma.channel19.xfercfg.read().bits() >> 16) & 0x3FF;
|
||||||
let reg_2 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
let reg_2 = (self.dma.channel19.xfercfg.read().bits() >> 16) & 0x3FF;
|
||||||
|
|
||||||
let consumed_end = self.consumed.load(Ordering::Acquire);
|
let consumed_end = self.consumed.load(Ordering::Acquire);
|
||||||
|
|
||||||
if consumed_start == consumed_end && reg_1 == reg_2 {
|
if consumed_start == consumed_end && reg_1 == reg_2 {
|
||||||
// 1. Map the hardware remaining countdown into a clean byte count
|
let remaining_bytes = if reg_1 == 0x3FF {
|
||||||
let remaining_bytes = if reg_1 == 0x3ff {
|
0
|
||||||
0 // 0x3FF means all transfers completed, 0 bytes remaining
|
|
||||||
} else {
|
} else {
|
||||||
// Formula from NXP manual: (XFERCOUNT + 1) * Data Width
|
(reg_1 as usize + 1) * self.word_bytes
|
||||||
(reg_1 + 1) * self.word_bytes
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Total bytes consumed in this specific active slot
|
// Active slot consumed calculation accounts for dynamic slot size
|
||||||
let active_slot_consumed = self.slot_bytes - remaining_bytes;
|
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||||
|
let active_slot_consumed = slot_bytes.saturating_sub(remaining_bytes);
|
||||||
|
|
||||||
// 3. Combine with your software index history accumulator
|
return consumed_start * slot_bytes + active_slot_consumed;
|
||||||
return consumed_start * self.slot_bytes + active_slot_consumed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,8 +310,8 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
|
|
||||||
fn reset_producer(&self) {
|
fn reset_producer(&self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
*(&mut *self.write_slot.get()) = 0;
|
*self.write_slot.get() = 0;
|
||||||
*(&mut *self.write_off.get()) = 0;
|
*self.write_off.get() = 0;
|
||||||
}
|
}
|
||||||
self.produced.store(0, Ordering::Relaxed);
|
self.produced.store(0, Ordering::Relaxed);
|
||||||
self.produced_bytes.store(0, Ordering::Relaxed);
|
self.produced_bytes.store(0, Ordering::Relaxed);
|
||||||
@@ -324,45 +323,30 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
let fill = self.fill_slots();
|
let fill = self.fill_slots();
|
||||||
fill >= N.wrapping_sub(self.safety_gap)
|
fill >= N.wrapping_sub(self.safety_gap)
|
||||||
}
|
}
|
||||||
fn reset_producer_init_only(&self) {
|
|
||||||
unsafe {
|
|
||||||
*self.write_slot.get() = 0;
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
*self.write_off.get() = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.produced.store(0, Ordering::Relaxed);
|
|
||||||
self.consumed.store(0, Ordering::Relaxed);
|
|
||||||
|
|
||||||
self.produced_bytes.store(0, Ordering::Relaxed);
|
|
||||||
self.consumed_bytes.store(0, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_descriptors(&self) {
|
fn init_descriptors(&self) {
|
||||||
|
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||||
let slots = unsafe { &mut *self.slots.get() };
|
let slots = unsafe { &mut *self.slots.get() };
|
||||||
let desc = unsafe { &mut *self.desc.get() };
|
let desc = unsafe { &mut *self.desc.get() };
|
||||||
let chan_desc = unsafe { &mut *self.channel_desc.get() };
|
let chan_desc = unsafe { &mut *self.channel_desc.get() };
|
||||||
defmt::debug!("slots base: &{:x}", self.slots.get());
|
|
||||||
|
|
||||||
// Pre-fill with silence so underrun replays silence.
|
// Pre-fill active slot regions with silence
|
||||||
for i in 0..N {
|
for i in 0..N {
|
||||||
slots[i][..self.slot_bytes].fill(0);
|
slots[i][..slot_bytes].fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let transfers = (self.slot_bytes / self.word_bytes) as u32;
|
let transfers = (slot_bytes / self.word_bytes) as u32;
|
||||||
|
|
||||||
for i in 0..N {
|
for i in 0..N {
|
||||||
let src_start = slots[i].as_ptr() as usize;
|
let src_start = slots[i].as_ptr() as usize;
|
||||||
let src_end = (src_start + self.slot_bytes - self.word_bytes) as *const u8;
|
let src_end = (src_start + slot_bytes - self.word_bytes) as *const u8;
|
||||||
|
|
||||||
let next = &desc.d[(i + 1) % N] as *const DmaDescriptor;
|
let next = &desc.d[(i + 1) % N] as *const DmaDescriptor;
|
||||||
|
|
||||||
desc.d[i] = DmaDescriptor {
|
desc.d[i] = DmaDescriptor {
|
||||||
xfercfg: encode_xfercfg(
|
xfercfg: encode_xfercfg(
|
||||||
true, // valid
|
true, // valid
|
||||||
true, // reload
|
true, // reload
|
||||||
false, // swtrig (we use XFERCFG SWTRIG kick)
|
false, // swtrig
|
||||||
false, // clrtrig
|
false, // clrtrig
|
||||||
true, // intA
|
true, // intA
|
||||||
false, // intB
|
false, // intB
|
||||||
@@ -376,11 +360,14 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure memory writes complete before reloading DMA hardware pointers
|
||||||
|
compiler_fence(Ordering::Release);
|
||||||
|
|
||||||
chan_desc.d[19] = desc.d[0];
|
chan_desc.d[19] = desc.d[0];
|
||||||
chan_desc.d[19].xfercfg = 0;
|
chan_desc.d[19].xfercfg = 0;
|
||||||
|
|
||||||
// reset producer indices + counters (init-only action)
|
self.reset_producer();
|
||||||
self.reset_producer_init_only();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
|
use core::cell::UnsafeCell;
|
||||||
|
use core::mem::MaybeUninit;
|
||||||
|
|
||||||
|
use crate::hal;
|
||||||
use crate::pac;
|
use crate::pac;
|
||||||
|
|
||||||
use defmt::{debug, info};
|
use defmt::{debug, info};
|
||||||
|
use hal::{
|
||||||
|
Enabled, Iocon, Pin,
|
||||||
|
drivers::pins,
|
||||||
|
traits::wg::digital::v2::{OutputPin, ToggleableOutputPin},
|
||||||
|
typestates::pin::{gpio::direction::Output, state::Gpio},
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) struct PllConstants {
|
pub(crate) struct PllConstants {
|
||||||
pub m: u16, // 1-65535
|
pub m: u16, // 1-65535
|
||||||
@@ -184,3 +195,62 @@ pub(crate) fn init_audio_pll() {
|
|||||||
}
|
}
|
||||||
info!("pll0 locked after {} loops", i);
|
info!("pll0 locked after {} loops", i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SharedLed<T: OutputPin> {
|
||||||
|
inner: UnsafeCell<T>,
|
||||||
|
}
|
||||||
|
unsafe impl<T: OutputPin> Sync for SharedLed<T> {}
|
||||||
|
impl<T: OutputPin> SharedLed<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: UnsafeCell::new(inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn on(&self) {
|
||||||
|
unsafe {
|
||||||
|
(*self.inner.get()).set_low().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn off(&self) {
|
||||||
|
unsafe {
|
||||||
|
(*self.inner.get()).set_high().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: OutputPin + ToggleableOutputPin> SharedLed<T> {
|
||||||
|
pub fn toggle(&self) {
|
||||||
|
unsafe {
|
||||||
|
(*self.inner.get()).toggle().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Led1 = Pin<pins::Pio0_13, Gpio<Output>>;
|
||||||
|
type Led2 = Pin<pins::Pio0_14, Gpio<Output>>;
|
||||||
|
pub static LED1: MaybeUninit<SharedLed<Led1>> = MaybeUninit::uninit();
|
||||||
|
pub static LED2: MaybeUninit<SharedLed<Led2>> = MaybeUninit::uninit();
|
||||||
|
|
||||||
|
pub fn init_leds(iocon: &mut Iocon<Enabled>, gpio: &mut hal::Gpio<Enabled>) {
|
||||||
|
let led1 = SharedLed::new(
|
||||||
|
pins::Pio0_13::take()
|
||||||
|
.unwrap()
|
||||||
|
.into_gpio_pin(iocon, gpio)
|
||||||
|
.into_output_low(),
|
||||||
|
);
|
||||||
|
let led2 = SharedLed::new(
|
||||||
|
pins::Pio0_14::take()
|
||||||
|
.unwrap()
|
||||||
|
.into_gpio_pin(iocon, gpio)
|
||||||
|
.into_output_low(),
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
core::ptr::write(LED1.as_ptr() as *mut SharedLed<Led1>, led1);
|
||||||
|
core::ptr::write(LED2.as_ptr() as *mut SharedLed<Led2>, led2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn led1() -> &'static SharedLed<Led1> {
|
||||||
|
unsafe { &*LED1.as_ptr() }
|
||||||
|
}
|
||||||
|
pub fn led2() -> &'static SharedLed<Led2> {
|
||||||
|
unsafe { &*LED2.as_ptr() }
|
||||||
|
}
|
||||||
|
|||||||
+179
-141
@@ -8,8 +8,6 @@ fn panic() -> ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use atomic::Atomic;
|
use atomic::Atomic;
|
||||||
use bytemuck::NoUninit;
|
|
||||||
use core::error;
|
|
||||||
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
||||||
use cortex_m_rt::entry;
|
use cortex_m_rt::entry;
|
||||||
use defmt;
|
use defmt;
|
||||||
@@ -40,7 +38,10 @@ use usbd_uac2::{
|
|||||||
|
|
||||||
use crate::dac::DacImpl;
|
use crate::dac::DacImpl;
|
||||||
use crate::dma::DmaRing;
|
use crate::dma::DmaRing;
|
||||||
|
use crate::hw::led1;
|
||||||
|
use crate::hw::led2;
|
||||||
use crate::traits::Dac;
|
use crate::traits::Dac;
|
||||||
|
use shared::AudioState;
|
||||||
use shared::hid::AudioTelemetryReport;
|
use shared::hid::AudioTelemetryReport;
|
||||||
|
|
||||||
#[cfg(feature = "ak4490")]
|
#[cfg(feature = "ak4490")]
|
||||||
@@ -68,24 +69,41 @@ mod dma;
|
|||||||
mod hw;
|
mod hw;
|
||||||
mod traits;
|
mod traits;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "evk"))]
|
||||||
|
const MAX_SAMPLE_RATE: u32 = 192000;
|
||||||
|
#[cfg(feature = "evk")]
|
||||||
|
const MAX_SAMPLE_RATE: u32 = 96000;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "evk"))]
|
||||||
|
const SAMPLE_RATES: [RangeEntry<u32>; 6] = [
|
||||||
|
RangeEntry::new_fixed(44100),
|
||||||
|
RangeEntry::new_fixed(48000),
|
||||||
|
RangeEntry::new_fixed(44100 * 2),
|
||||||
|
RangeEntry::new_fixed(48000 * 2),
|
||||||
|
RangeEntry::new_fixed(44100 * 4),
|
||||||
|
RangeEntry::new_fixed(48000 * 4),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(feature = "evk")]
|
||||||
|
const SAMPLE_RATES: [RangeEntry<u32>; 2] = [
|
||||||
|
RangeEntry::new_fixed(48000),
|
||||||
|
RangeEntry::new_fixed(48000 * 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
const DMA_RATE: usize = 4000;
|
||||||
const BYTES_PER_SAMPLE: usize = 4; // 32 bit samples
|
const BYTES_PER_SAMPLE: usize = 4; // 32 bit samples
|
||||||
const BYTES_PER_FRAME: usize = BYTES_PER_SAMPLE * 2; // 2 channels
|
const BYTES_PER_FRAME: usize = BYTES_PER_SAMPLE * 2; // 2 channels
|
||||||
const FRAMES_PER_SLOT: usize = SAMPLE_RATE as usize / 4000; // run the DMA at 4khz
|
const MAX_FRAMES_PER_SLOT: usize = MAX_SAMPLE_RATE as usize / 4000; // run the DMA at 4khz
|
||||||
const BYTES_PER_SLOT: usize = FRAMES_PER_SLOT * BYTES_PER_FRAME;
|
const MAX_BYTES_PER_SLOT: usize = MAX_FRAMES_PER_SLOT * BYTES_PER_FRAME;
|
||||||
const N_SLOTS: usize = 8;
|
const N_SLOTS: usize = 8;
|
||||||
const FILL_TARGET_BYTES: i32 = (BYTES_PER_SLOT * N_SLOTS) as i32 / 2;
|
// const FILL_TARGET_BYTES: i32 = (BYTES_PER_SLOT * N_SLOTS) as i32 / 2;
|
||||||
const USB_FRAME_RATE: u32 = 8000; // microframe rate: 8000 for HS, 1000 for FS
|
const USB_FRAME_RATE: u32 = 8000; // microframe rate: 8000 for HS, 1000 for FS
|
||||||
|
|
||||||
// In frames
|
// In frames
|
||||||
const QUEUE_RUNNING_UP: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 5) / 10; // 50%
|
// const QUEUE_RUNNING_UP: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 5) / 10; // 50%
|
||||||
const QUEUE_RUNNING_DOWN: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 2) / 10; // 20%
|
// const QUEUE_RUNNING_DOWN: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 2) / 10; // 20%
|
||||||
const NODATA_TIMEOUT_FRAMES: usize = SAMPLE_RATE as usize / 100; // ~100ms
|
// const NODATA_TIMEOUT_FRAMES: usize = SAMPLE_RATE as usize / 100; // ~100ms
|
||||||
#[cfg(not(feature = "evk"))]
|
|
||||||
const MCLK_FREQ: u32 = 24576000;
|
|
||||||
#[cfg(feature = "evk")]
|
|
||||||
const MCLK_FREQ: u32 = 24576000 / 2;
|
|
||||||
|
|
||||||
const SAMPLE_RATE: u32 = 192000;
|
|
||||||
const HID_INTERVAL_MS: u8 = 10;
|
const HID_INTERVAL_MS: u8 = 10;
|
||||||
|
|
||||||
struct CodecPins {
|
struct CodecPins {
|
||||||
@@ -118,9 +136,8 @@ impl PerfCounters {
|
|||||||
self.received_frames.store(0, Ordering::Relaxed);
|
self.received_frames.store(0, Ordering::Relaxed);
|
||||||
self.played_frames.store(0, Ordering::Relaxed);
|
self.played_frames.store(0, Ordering::Relaxed);
|
||||||
self.min_fill
|
self.min_fill
|
||||||
.store(N_SLOTS * BYTES_PER_SLOT, Ordering::Relaxed);
|
.store(N_SLOTS * MAX_BYTES_PER_SLOT, Ordering::Relaxed);
|
||||||
self.avg_fill
|
self.avg_fill.store(0 as usize, Ordering::Relaxed);
|
||||||
.store(FILL_TARGET_BYTES as usize, Ordering::Relaxed);
|
|
||||||
self.queue_underflows.store(0, Ordering::Relaxed);
|
self.queue_underflows.store(0, Ordering::Relaxed);
|
||||||
self.queue_overflows.store(0, Ordering::Relaxed);
|
self.queue_overflows.store(0, Ordering::Relaxed);
|
||||||
self.audio_underflows.store(0, Ordering::Relaxed);
|
self.audio_underflows.store(0, Ordering::Relaxed);
|
||||||
@@ -139,7 +156,7 @@ impl PerfCounters {
|
|||||||
integrator: self.integrator.load(Ordering::Relaxed),
|
integrator: self.integrator.load(Ordering::Relaxed),
|
||||||
p: self.p.load(Ordering::Relaxed),
|
p: self.p.load(Ordering::Relaxed),
|
||||||
i: self.i.load(Ordering::Relaxed),
|
i: self.i.load(Ordering::Relaxed),
|
||||||
fb: self.fb.load(Ordering::Relaxed) as i32,
|
fb: u32::cast_signed(self.fb.load(Ordering::Relaxed) as u32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +182,7 @@ static PERF: PerfCounters = PerfCounters {
|
|||||||
received_frames: AtomicUsize::new(0), // received from USB
|
received_frames: AtomicUsize::new(0), // received from USB
|
||||||
played_frames: AtomicUsize::new(0), // played audio frames
|
played_frames: AtomicUsize::new(0), // played audio frames
|
||||||
min_fill: AtomicUsize::new(0), // not recording this for now, need to figure out how to make it meaningful, since the queue starts empty
|
min_fill: AtomicUsize::new(0), // not recording this for now, need to figure out how to make it meaningful, since the queue starts empty
|
||||||
avg_fill: AtomicUsize::new(FILL_TARGET_BYTES as usize),
|
avg_fill: AtomicUsize::new(0),
|
||||||
queue_underflows: AtomicUsize::new(0), // ditto here, since we underflow at startup, but we record this one as it can be trended
|
queue_underflows: AtomicUsize::new(0), // ditto here, since we underflow at startup, but we record this one as it can be trended
|
||||||
queue_overflows: AtomicUsize::new(0),
|
queue_overflows: AtomicUsize::new(0),
|
||||||
audio_underflows: AtomicUsize::new(0),
|
audio_underflows: AtomicUsize::new(0),
|
||||||
@@ -175,10 +192,12 @@ static PERF: PerfCounters = PerfCounters {
|
|||||||
fb: AtomicI32::new(0),
|
fb: AtomicI32::new(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
static DMA_RING: StaticCell<DmaRing<N_SLOTS, BYTES_PER_SLOT>> = StaticCell::new();
|
static NODATA_FLAG: AtomicBool = AtomicBool::new(false);
|
||||||
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, BYTES_PER_SLOT>> = None;
|
|
||||||
|
static DMA_RING: StaticCell<DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> = StaticCell::new();
|
||||||
|
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> = None;
|
||||||
#[inline]
|
#[inline]
|
||||||
fn dma_ring() -> &'static DmaRing<N_SLOTS, BYTES_PER_SLOT> {
|
fn dma_ring() -> &'static DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT> {
|
||||||
unsafe { DMA_RING_REF.unwrap() }
|
unsafe { DMA_RING_REF.unwrap() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +209,24 @@ fn cur_fill() -> usize {
|
|||||||
produced_bytes.wrapping_sub(consumed_bytes) as usize
|
produced_bytes.wrapping_sub(consumed_bytes) as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cur_fill_target() -> i32 {
|
||||||
|
(dma_ring().slot_size() * N_SLOTS) as i32 / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frames_per_slot() -> usize {
|
||||||
|
dma_ring().slot_size() / BYTES_PER_FRAME
|
||||||
|
}
|
||||||
|
|
||||||
|
// 50%
|
||||||
|
fn queue_running_up_threshold() -> usize {
|
||||||
|
(frames_per_slot() * N_SLOTS) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20%
|
||||||
|
fn queue_running_down_threshold() -> usize {
|
||||||
|
(frames_per_slot() * N_SLOTS) / 5
|
||||||
|
}
|
||||||
|
|
||||||
#[interrupt]
|
#[interrupt]
|
||||||
fn DMA0() {
|
fn DMA0() {
|
||||||
defmt::debug!("dma0");
|
defmt::debug!("dma0");
|
||||||
@@ -211,17 +248,21 @@ fn DMA0() {
|
|||||||
err,
|
err,
|
||||||
mem
|
mem
|
||||||
);
|
);
|
||||||
// red_led().on();
|
|
||||||
dma.errint0.write(|w| unsafe { w.bits(1 << 19) });
|
dma.errint0.write(|w| unsafe { w.bits(1 << 19) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inta & (1 << 19)) != 0 {
|
if (inta & (1 << 19)) != 0 {
|
||||||
dma.inta0.write(|w| unsafe { w.bits(1 << 19) });
|
dma.inta0.write(|w| unsafe { w.bits(1 << 19) });
|
||||||
if dma_ring().advance_consumed(1).is_err() {
|
if dma_ring().advance_consumed(1).is_err() {
|
||||||
// red_led().on();
|
PERF.audio_underflows.fetch_add(1, Ordering::Relaxed);
|
||||||
} else {
|
} else {
|
||||||
|
led1().toggle();
|
||||||
PERF.played_frames
|
PERF.played_frames
|
||||||
.fetch_add(FRAMES_PER_SLOT, Ordering::Relaxed);
|
.fetch_add(frames_per_slot(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if cur_fill() <= dma_ring().slot_size() {
|
||||||
|
led2().on();
|
||||||
|
NODATA_FLAG.store(true, Ordering::Release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,65 +276,6 @@ fn FLEXCOMM7() {
|
|||||||
.modify(|_, w| w.txerr().set_bit())
|
.modify(|_, w| w.txerr().set_bit())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Clone, Copy, NoUninit, Eq, PartialEq)]
|
|
||||||
enum AudioState {
|
|
||||||
/// Knowingly stopped, ie. AltSetting=0. DAC muted, I2S disabled.
|
|
||||||
///
|
|
||||||
/// AltSetting = 1 -> ARMED
|
|
||||||
Stopped,
|
|
||||||
/// Waiting for data. DAC muted, I2S running sending 0s (FIFO not serviced).
|
|
||||||
///
|
|
||||||
/// USB OUT data packet -> ARMED
|
|
||||||
/// AltSetting = 0 -> STOPPED
|
|
||||||
Armed,
|
|
||||||
/// Filling the buffer before playback starts. Feedback does not run,
|
|
||||||
/// playout does not start draining the queue. Gets us better feedback
|
|
||||||
/// behaviour and a full buffer without a feedback rate spike at startup.
|
|
||||||
///
|
|
||||||
/// queue reaches <QUEUE_RUNNING_UP> -> RUNNING
|
|
||||||
/// AltSetting = 0 -> STOPPED
|
|
||||||
///
|
|
||||||
Prefill,
|
|
||||||
/// Normal running state. Start servicing FIFO and begin playing out from the buffer.
|
|
||||||
///
|
|
||||||
/// queue reaches <QUEUE_RUNNING_DOWN> -> DRAINING
|
|
||||||
/// AltSetting = 0 -> DRAINING
|
|
||||||
Running,
|
|
||||||
/// The queue is low. We will continue playout.
|
|
||||||
///
|
|
||||||
/// queue is empty && altSetting 1 -> NODATA
|
|
||||||
/// queue is empty && altSetting 0 -> STOPPED
|
|
||||||
/// queue reaches <QUEUE_RUNNING_UP> && altSetting 1 -> RUNNING
|
|
||||||
LowData,
|
|
||||||
/// There is no data in the queue. We will count underflows for a while, send 0s, and hope the host comes back, but maybe playback is done, which we should notice and shut down.
|
|
||||||
///
|
|
||||||
/// countdown reaches DATA_TIMEOUT -> STOPPED
|
|
||||||
/// AltSetting = 0 -> STOPPED
|
|
||||||
NoData,
|
|
||||||
}
|
|
||||||
impl Default for AudioState {
|
|
||||||
fn default() -> Self {
|
|
||||||
AudioState::Stopped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl defmt::Format for AudioState {
|
|
||||||
fn format(&self, fmt: defmt::Formatter) {
|
|
||||||
defmt::write!(
|
|
||||||
fmt,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Self::Stopped => "Stopped",
|
|
||||||
Self::Armed => "Armed",
|
|
||||||
Self::Prefill => "Prefill",
|
|
||||||
Self::Running => "Running",
|
|
||||||
Self::LowData => "Draining",
|
|
||||||
Self::NoData => "NoData",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FeedbackState {
|
struct FeedbackState {
|
||||||
correction_enabled: AtomicBool,
|
correction_enabled: AtomicBool,
|
||||||
integrator: AtomicI32,
|
integrator: AtomicI32,
|
||||||
@@ -307,7 +289,7 @@ impl FeedbackState {
|
|||||||
self.correction_enabled.store(false, Ordering::Relaxed);
|
self.correction_enabled.store(false, Ordering::Relaxed);
|
||||||
self.integrator.store(0, Ordering::Relaxed);
|
self.integrator.store(0, Ordering::Relaxed);
|
||||||
self.filtered_fill
|
self.filtered_fill
|
||||||
.store(FILL_TARGET_BYTES, Ordering::Relaxed);
|
.store(cur_fill_target(), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Default for FeedbackState {
|
impl Default for FeedbackState {
|
||||||
@@ -315,7 +297,7 @@ impl Default for FeedbackState {
|
|||||||
Self {
|
Self {
|
||||||
correction_enabled: AtomicBool::new(false),
|
correction_enabled: AtomicBool::new(false),
|
||||||
integrator: AtomicI32::new(0),
|
integrator: AtomicI32::new(0),
|
||||||
filtered_fill: AtomicI32::new(FILL_TARGET_BYTES),
|
filtered_fill: AtomicI32::new(cur_fill_target()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,7 +307,7 @@ struct Audio<'a, D: Dac<I>, I> {
|
|||||||
alt_setting: u8,
|
alt_setting: u8,
|
||||||
i2s: I2sTx,
|
i2s: I2sTx,
|
||||||
dac: D,
|
dac: D,
|
||||||
dma: &'a DmaRing<N_SLOTS, BYTES_PER_SLOT>,
|
dma: &'a DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>,
|
||||||
fb: FeedbackState,
|
fb: FeedbackState,
|
||||||
nodata_timeout_frame: AtomicUsize,
|
nodata_timeout_frame: AtomicUsize,
|
||||||
cur_rate: u32,
|
cur_rate: u32,
|
||||||
@@ -333,7 +315,7 @@ struct Audio<'a, D: Dac<I>, I> {
|
|||||||
_marker: core::marker::PhantomData<I>,
|
_marker: core::marker::PhantomData<I>,
|
||||||
}
|
}
|
||||||
impl<D: Dac<I>, I> Audio<'_, D, I> {
|
impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||||
const RATES: [RangeEntry<u32>; 1] = [RangeEntry::new_fixed(SAMPLE_RATE)];
|
const RATES: &'static [RangeEntry<u32>] = &SAMPLE_RATES;
|
||||||
/// Perform a state transition to `state`
|
/// Perform a state transition to `state`
|
||||||
fn transition(&mut self, state: AudioState) {
|
fn transition(&mut self, state: AudioState) {
|
||||||
defmt::info!(
|
defmt::info!(
|
||||||
@@ -348,15 +330,15 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
AudioState::Running => self.run(),
|
AudioState::Running => self.run(),
|
||||||
AudioState::LowData => {}
|
AudioState::LowData => {}
|
||||||
AudioState::NoData => self.nodata(),
|
AudioState::NoData => self.nodata(),
|
||||||
|
AudioState::Stopping => self.stopping(),
|
||||||
}
|
}
|
||||||
self.state.store(state, Ordering::SeqCst);
|
self.state.store(state, Ordering::SeqCst);
|
||||||
PERF.state.store(state, Ordering::Relaxed);
|
PERF.state.store(state, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(&mut self) {
|
fn init(&mut self) {
|
||||||
let regs = &self.i2s.i2s;
|
|
||||||
// Enable TX FIFO only
|
// Enable TX FIFO only
|
||||||
regs.fifocfg.modify(|_, w| {
|
self.i2s.i2s.fifocfg.modify(|_, w| {
|
||||||
w.enabletx()
|
w.enabletx()
|
||||||
.enabled()
|
.enabled()
|
||||||
.enablerx()
|
.enablerx()
|
||||||
@@ -368,17 +350,17 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Flush
|
// Flush
|
||||||
regs.fifocfg.modify(|_, w| w.emptytx().set_bit());
|
self.i2s.i2s.fifocfg.modify(|_, w| w.emptytx().set_bit());
|
||||||
|
|
||||||
regs.cfg2
|
self.i2s
|
||||||
|
.i2s
|
||||||
|
.cfg2
|
||||||
.modify(|_, w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
|
.modify(|_, w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
|
||||||
|
|
||||||
let bclk_div = (MCLK_FREQ / SAMPLE_RATE / 64) as u16;
|
self.update_bclk();
|
||||||
regs.div
|
|
||||||
.modify(|_, w| unsafe { w.div().bits(bclk_div - 1) }); // Clock source is MCLK (12.288MHz) / 4 = 3MHz
|
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
regs.cfg1.modify(|_, w| unsafe {
|
self.i2s.i2s.cfg1.modify(|_, w| unsafe {
|
||||||
w.mstslvcfg()
|
w.mstslvcfg()
|
||||||
.normal_master()
|
.normal_master()
|
||||||
.onechannel()
|
.onechannel()
|
||||||
@@ -413,8 +395,8 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
// reset performance counters
|
// reset performance counters
|
||||||
PERF.reset();
|
PERF.reset();
|
||||||
// Stop the clocks
|
// Stop the clocks
|
||||||
self.clock_pins.sel_22m.set_low().ok();
|
// self.clock_pins.sel_22m.set_low().ok();
|
||||||
self.clock_pins.sel_24m.set_low().ok();
|
// self.clock_pins.sel_24m.set_low().ok();
|
||||||
}
|
}
|
||||||
///Transition -> Armed
|
///Transition -> Armed
|
||||||
/// Start I2S peripheral and MCLK. Since we assume we have interrupts disabled at
|
/// Start I2S peripheral and MCLK. Since we assume we have interrupts disabled at
|
||||||
@@ -455,14 +437,31 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
///Transition->NoData
|
///Transition->NoData
|
||||||
///store framecount at transition so we can time out recovery
|
///store framecount at transition so we can time out recovery
|
||||||
fn nodata(&mut self) {
|
fn nodata(&mut self) {
|
||||||
self.nodata_timeout_frame.store(
|
// TODO: Actually handle this
|
||||||
PERF.queue_underflows.load(Ordering::Relaxed) + NODATA_TIMEOUT_FRAMES, // we underflow every frame, use it as a timeout counter
|
// self.nodata_timeout_frame.store(
|
||||||
Ordering::Relaxed,
|
// PERF.queue_underflows.load(Ordering::Relaxed) + NODATA_TIMEOUT_FRAMES, // we underflow every frame, use it as a timeout counter
|
||||||
);
|
// Ordering::Relaxed,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
/// Transition -> Stopping
|
||||||
|
/// just a marker that upcoming nodata is expected, do nothing
|
||||||
|
fn stopping(&mut self) {}
|
||||||
|
|
||||||
|
fn update_bclk(&mut self) {
|
||||||
|
let mclk_freq = if 24_576_000u32.is_multiple_of(self.cur_rate) {
|
||||||
|
24576000
|
||||||
|
} else {
|
||||||
|
22579200
|
||||||
|
};
|
||||||
|
let bclk_div = (mclk_freq / self.cur_rate / 64) as u16;
|
||||||
|
self.i2s
|
||||||
|
.i2s
|
||||||
|
.div
|
||||||
|
.modify(|_, w| unsafe { w.div().bits(bclk_div - 1) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
||||||
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
|
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalProgrammable;
|
||||||
const SOF_SYNC: bool = false;
|
const SOF_SYNC: bool = false;
|
||||||
|
|
||||||
fn sample_rate(&self) -> u32 {
|
fn sample_rate(&self) -> u32 {
|
||||||
@@ -472,25 +471,34 @@ impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
) -> core::result::Result<(), usbd_uac2::UsbAudioClassError> {
|
) -> core::result::Result<(), usbd_uac2::UsbAudioClassError> {
|
||||||
|
defmt::info!("[clock] changing rate to {}", sample_rate);
|
||||||
|
if self.state.load(Ordering::SeqCst) != AudioState::Stopped {
|
||||||
|
defmt::warn!("[clock] changing rate when not stopped, stopping first");
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
let slot_bytes = (self.cur_rate as usize / DMA_RATE) * BYTES_PER_FRAME;
|
||||||
|
dma_ring().set_slot_size(slot_bytes);
|
||||||
|
self.cur_rate = sample_rate;
|
||||||
if 24_576_000u32.is_multiple_of(sample_rate) {
|
if 24_576_000u32.is_multiple_of(sample_rate) {
|
||||||
defmt::info!("[clock] 24M clock selected");
|
defmt::info!("[clock] 24M osc selected");
|
||||||
self.clock_pins.sel_22m.set_low().ok();
|
self.clock_pins.sel_22m.set_low().ok();
|
||||||
// hal::wait_at_least(1);
|
// hal::wait_at_least(1);
|
||||||
self.clock_pins.sel_24m.set_high().ok();
|
self.clock_pins.sel_24m.set_high().ok();
|
||||||
} else {
|
} else {
|
||||||
defmt::info!("[clock] 22M clock selected");
|
defmt::info!("[clock] 22M osc selected");
|
||||||
self.clock_pins.sel_24m.set_low().ok();
|
self.clock_pins.sel_24m.set_low().ok();
|
||||||
// hal::wait_at_least(1);
|
// hal::wait_at_least(1);
|
||||||
self.clock_pins.sel_22m.set_high().ok();
|
self.clock_pins.sel_22m.set_high().ok();
|
||||||
};
|
};
|
||||||
self.dac.change_rate(sample_rate);
|
self.dac.change_rate(sample_rate);
|
||||||
self.cur_rate = sample_rate;
|
self.update_bclk();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn sample_rates(
|
fn sample_rates(
|
||||||
&self,
|
&self,
|
||||||
) -> core::result::Result<&[usbd_uac2::RangeEntry<u32>], usbd_uac2::UsbAudioClassError> {
|
) -> core::result::Result<&[usbd_uac2::RangeEntry<u32>], usbd_uac2::UsbAudioClassError> {
|
||||||
Ok(&Self::RATES)
|
defmt::debug!("[clock] sample_rates will return {:?}", &Self::RATES.len());
|
||||||
|
Ok(Self::RATES)
|
||||||
}
|
}
|
||||||
fn clock_validity(&self) -> Result<bool, UsbAudioClassError> {
|
fn clock_validity(&self) -> Result<bool, UsbAudioClassError> {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@@ -503,7 +511,8 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
(0, AudioState::Armed | AudioState::Prefill | AudioState::NoData) => {
|
(0, AudioState::Armed | AudioState::Prefill | AudioState::NoData) => {
|
||||||
self.transition(AudioState::Stopped)
|
self.transition(AudioState::Stopped)
|
||||||
}
|
}
|
||||||
(0, AudioState::Running | AudioState::Stopped) => {} // noop, we naturally transition through LowData to Stopped
|
(0, AudioState::Running) => self.transition(AudioState::Stopping),
|
||||||
|
(0, AudioState::Stopped | AudioState::Stopping) => {} // already stopped/ing
|
||||||
(1, AudioState::Stopped) => self.transition(AudioState::Armed),
|
(1, AudioState::Stopped) => self.transition(AudioState::Armed),
|
||||||
(1, _) => {} // altSetting 1 in any other state is a no-op
|
(1, _) => {} // altSetting 1 in any other state is a no-op
|
||||||
(_, _) => {
|
(_, _) => {
|
||||||
@@ -517,7 +526,8 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
|
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
|
||||||
) {
|
) {
|
||||||
let state = self.state.load(Ordering::Relaxed);
|
let state = self.state.load(Ordering::Relaxed);
|
||||||
let mut buf = [0; (SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
let mut buf =
|
||||||
|
[0; (MAX_SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
||||||
let len = match ep.read(&mut buf) {
|
let len = match ep.read(&mut buf) {
|
||||||
Ok(len) => len,
|
Ok(len) => len,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -530,7 +540,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
|
|
||||||
if res.dropped != 0 {
|
if res.dropped != 0 {
|
||||||
// Overflow: some or all bytes couldn't be queued.
|
// Overflow: some or all bytes couldn't be queued.
|
||||||
defmt::error!(
|
defmt::warn!(
|
||||||
"overflowed dma ring, asked {}, wrote {}, dropped {}",
|
"overflowed dma ring, asked {}, wrote {}, dropped {}",
|
||||||
buf.len(),
|
buf.len(),
|
||||||
res.written,
|
res.written,
|
||||||
@@ -545,14 +555,16 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
|
|
||||||
// Valid states here are Armed, Prefill, Running, Draining and NoData
|
// Valid states here are Armed, Prefill, Running, Draining and NoData
|
||||||
match state {
|
match state {
|
||||||
AudioState::Stopped => {
|
AudioState::Stopped | AudioState::Stopping => {
|
||||||
defmt::error!("Received audio data when stopped")
|
defmt::error!("Received audio data when stopped/stopping")
|
||||||
}
|
}
|
||||||
// When armed, data rx goes to prefill
|
// When armed, data rx goes to prefill
|
||||||
AudioState::Armed => self.transition(AudioState::Prefill),
|
AudioState::Armed => self.transition(AudioState::Prefill),
|
||||||
// When prefilling, if we have received frames over the up threshold, move to running
|
// When prefilling, if we have received frames over the up threshold, move to running
|
||||||
AudioState::Prefill => {
|
AudioState::Prefill => {
|
||||||
if PERF.received_frames.load(Ordering::Relaxed) >= QUEUE_RUNNING_UP {
|
if PERF.received_frames.load(Ordering::Relaxed) >= queue_running_up_threshold()
|
||||||
|
// 50%
|
||||||
|
{
|
||||||
self.transition(AudioState::Running);
|
self.transition(AudioState::Running);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -562,7 +574,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
AudioState::LowData => {
|
AudioState::LowData => {
|
||||||
let fill = cur_fill() as usize;
|
let fill = cur_fill() as usize;
|
||||||
// Do we check alt setting here? We shouldn't be receiving data at all if we are not in altSetting 1
|
// Do we check alt setting here? We shouldn't be receiving data at all if we are not in altSetting 1
|
||||||
if fill >= QUEUE_RUNNING_UP {
|
if fill >= queue_running_up_threshold() {
|
||||||
self.transition(AudioState::Running);
|
self.transition(AudioState::Running);
|
||||||
} else if fill == 0 && self.alt_setting == 0 {
|
} else if fill == 0 && self.alt_setting == 0 {
|
||||||
self.transition(AudioState::Stopped);
|
self.transition(AudioState::Stopped);
|
||||||
@@ -598,7 +610,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let raw_error = current_bytes - FILL_TARGET_BYTES;
|
let raw_error = current_bytes - cur_fill_target();
|
||||||
let i_error = if raw_error.abs() <= 4 { 0 } else { raw_error }; // deadband
|
let i_error = if raw_error.abs() <= 4 { 0 } else { raw_error }; // deadband
|
||||||
let current_i = self.fb.integrator.load(Ordering::Relaxed);
|
let current_i = self.fb.integrator.load(Ordering::Relaxed);
|
||||||
let leak = current_i >> 7;
|
let leak = current_i >> 7;
|
||||||
@@ -614,7 +626,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
|
|
||||||
// 3. SEPARATE GAINS FOR P AND I
|
// 3. SEPARATE GAINS FOR P AND I
|
||||||
// For P: Keep your working math (converting raw error to a permille equivalent scale)
|
// For P: Keep your working math (converting raw error to a permille equivalent scale)
|
||||||
let error_permille = (raw_error * 1000) / FILL_TARGET_BYTES;
|
let error_permille = (raw_error * 1000) / cur_fill_target();
|
||||||
let p_term = (-((error_permille as i64) * (nominal_v as i64)) / (10 * 256000)) as i32;
|
let p_term = (-((error_permille as i64) * (nominal_v as i64)) / (10 * 256000)) as i32;
|
||||||
let i_term = (-((new_i as i64) * (nominal_v as i64)) / (256000 * 1000)) as i32;
|
let i_term = (-((new_i as i64) * (nominal_v as i64)) / (256000 * 1000)) as i32;
|
||||||
let i_term = 0;
|
let i_term = 0;
|
||||||
@@ -643,6 +655,14 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
|
|||||||
syscon.reset(&mut fc7);
|
syscon.reset(&mut fc7);
|
||||||
syscon.enable_clock(&mut fc7);
|
syscon.enable_clock(&mut fc7);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
pac::SYSCON::ptr()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.fcclksel7()
|
||||||
|
.modify(|_, w| w.sel().enum_0x5()); // MCLK
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "evk"))]
|
||||||
unsafe {
|
unsafe {
|
||||||
pac::IOCON::ptr().as_ref().unwrap().pio0_23.modify(|_, w| {
|
pac::IOCON::ptr().as_ref().unwrap().pio0_23.modify(|_, w| {
|
||||||
w.func()
|
w.func()
|
||||||
@@ -658,23 +678,29 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
|
|||||||
.od()
|
.od()
|
||||||
.normal()
|
.normal()
|
||||||
});
|
});
|
||||||
pac::SYSCON::ptr()
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.fcclksel7()
|
|
||||||
.modify(|_, w| w.sel().enum_0x5()); // MCLK
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "evk"))]
|
|
||||||
unsafe {
|
|
||||||
pac::SYSCON::ptr()
|
pac::SYSCON::ptr()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.mclkio
|
.mclkio
|
||||||
.modify(|_, w| w.mclkio().input());
|
.modify(|_, w| w.mclkio().input());
|
||||||
}
|
};
|
||||||
|
|
||||||
#[cfg(feature = "evk")]
|
#[cfg(feature = "evk")]
|
||||||
unsafe {
|
unsafe {
|
||||||
|
pac::IOCON::ptr().as_ref().unwrap().pio1_31.modify(|_, w| {
|
||||||
|
w.func()
|
||||||
|
.alt1()
|
||||||
|
.mode()
|
||||||
|
.inactive()
|
||||||
|
.slew()
|
||||||
|
.fast()
|
||||||
|
.invert()
|
||||||
|
.disabled()
|
||||||
|
.digimode()
|
||||||
|
.digital()
|
||||||
|
.od()
|
||||||
|
.normal()
|
||||||
|
});
|
||||||
pac::SYSCON::ptr()
|
pac::SYSCON::ptr()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -748,16 +774,8 @@ fn main() -> ! {
|
|||||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||||
.into_output_low(),
|
.into_output_low(),
|
||||||
};
|
};
|
||||||
let leds = (
|
|
||||||
pins::Pio0_13::take()
|
hw::init_leds(&mut iocon, &mut gpio);
|
||||||
.unwrap()
|
|
||||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
|
||||||
.into_output_low(),
|
|
||||||
pins::Pio0_14::take()
|
|
||||||
.unwrap()
|
|
||||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
|
||||||
.into_output_low(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// iocon.disabled(&mut syscon).release(); // save the environment :)
|
// iocon.disabled(&mut syscon).release(); // save the environment :)
|
||||||
|
|
||||||
@@ -805,9 +823,13 @@ fn main() -> ! {
|
|||||||
|
|
||||||
defmt::info!("dma init");
|
defmt::info!("dma init");
|
||||||
let i2s_dma_addr = &i2s_peripheral.i2s.fifowr as *const _ as *mut u32;
|
let i2s_dma_addr = &i2s_peripheral.i2s.fifowr as *const _ as *mut u32;
|
||||||
let dma =
|
let dma = DmaRing::<N_SLOTS, MAX_BYTES_PER_SLOT>::new(
|
||||||
DmaRing::<N_SLOTS, BYTES_PER_SLOT>::new(hal.dma.release(), &mut syscon, i2s_dma_addr, 4)
|
hal.dma.release(),
|
||||||
.unwrap();
|
&mut syscon,
|
||||||
|
i2s_dma_addr,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let dma_ref = DMA_RING.init(dma);
|
let dma_ref = DMA_RING.init(dma);
|
||||||
unsafe { DMA_RING_REF = Some(dma_ref) };
|
unsafe { DMA_RING_REF = Some(dma_ref) };
|
||||||
|
|
||||||
@@ -820,7 +842,7 @@ fn main() -> ! {
|
|||||||
fb: FeedbackState::default(),
|
fb: FeedbackState::default(),
|
||||||
alt_setting: 0,
|
alt_setting: 0,
|
||||||
nodata_timeout_frame: AtomicUsize::new(0),
|
nodata_timeout_frame: AtomicUsize::new(0),
|
||||||
cur_rate: SAMPLE_RATE,
|
cur_rate: SAMPLE_RATES[0].min,
|
||||||
clock_pins: clock_sel_pins,
|
clock_pins: clock_sel_pins,
|
||||||
_marker: core::marker::PhantomData,
|
_marker: core::marker::PhantomData,
|
||||||
};
|
};
|
||||||
@@ -859,6 +881,14 @@ fn main() -> ! {
|
|||||||
|
|
||||||
move || {
|
move || {
|
||||||
usb_dev.poll(&mut [&mut uac2, &mut hid]);
|
usb_dev.poll(&mut [&mut uac2, &mut hid]);
|
||||||
|
// DMA ring is empty; if altsetting = 0 then -> stopped else NoData
|
||||||
|
// TODO: handle NoData state
|
||||||
|
if NODATA_FLAG.swap(false, Ordering::Acquire) {
|
||||||
|
match uac2.handler().state.load(Ordering::Acquire) {
|
||||||
|
AudioState::Stopping => uac2.handler().transition(AudioState::Stopped),
|
||||||
|
_ => uac2.handler().transition(AudioState::Stopped),
|
||||||
|
}
|
||||||
|
}
|
||||||
if hid_update_timer.wait().is_ok() {
|
if hid_update_timer.wait().is_ok() {
|
||||||
let report = PERF.build_report();
|
let report = PERF.build_report();
|
||||||
match hid.push_input(&report) {
|
match hid.push_input(&report) {
|
||||||
@@ -872,8 +902,16 @@ fn main() -> ! {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
#[cfg(not(feature = "hid"))]
|
#[cfg(not(feature = "hid"))]
|
||||||
let poll_all = || {
|
let mut poll_all = {
|
||||||
usb_dev.poll(&mut [&mut uac2]);
|
move || {
|
||||||
|
usb_dev.poll(&mut [&mut uac2]);
|
||||||
|
if NODATA_FLAG.swap(false, Ordering::Acquire) {
|
||||||
|
match uac2.handler().state.load(Ordering::Acquire) {
|
||||||
|
AudioState::Stopping => uac2.handler().transition(AudioState::Stopped),
|
||||||
|
_ => uac2.handler().transition(AudioState::Stopped),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
defmt::info!("main loop");
|
defmt::info!("main loop");
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.95.0"
|
||||||
|
targets = ["thumbv8m.main-none-eabihf"]
|
||||||
|
components = ["llvm-tools-preview"]
|
||||||
@@ -3,9 +3,13 @@ name = "shared"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
std = []
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||||
|
defmt = "1.1.1"
|
||||||
deku = { version = "0.20.3", default-features = false }
|
deku = { version = "0.20.3", default-features = false }
|
||||||
|
num_enum = { version = "0.7.6", default-features = false }
|
||||||
serde = { version = "1.0.228", optional = true, features = ["derive"] }
|
serde = { version = "1.0.228", optional = true, features = ["derive"] }
|
||||||
usbd-hid = { version = "0.10.0" }
|
usbd-hid = { version = "0.10.0" }
|
||||||
|
|||||||
+124
-2
@@ -1,14 +1,120 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
|
use bytemuck::NoUninit;
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, NoUninit, Eq, PartialEq, TryFromPrimitive)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum AudioState {
|
||||||
|
/// Knowingly stopped, ie. AltSetting=0. DAC muted, I2S disabled.
|
||||||
|
///
|
||||||
|
/// AltSetting = 1 -> ARMED
|
||||||
|
Stopped = 0,
|
||||||
|
/// Waiting for data. DAC muted, I2S running sending 0s (FIFO not serviced).
|
||||||
|
///
|
||||||
|
/// USB OUT data packet -> ARMED
|
||||||
|
/// AltSetting = 0 -> STOPPED
|
||||||
|
Armed = 1,
|
||||||
|
/// Filling the buffer before playback starts. Feedback does not run,
|
||||||
|
/// playout does not start draining the queue. Gets us better feedback
|
||||||
|
/// behaviour and a full buffer without a feedback rate spike at startup.
|
||||||
|
///
|
||||||
|
/// queue reaches <QUEUE_RUNNING_UP> -> RUNNING
|
||||||
|
/// AltSetting = 0 -> STOPPED
|
||||||
|
///
|
||||||
|
Prefill = 2,
|
||||||
|
/// Normal running state. Start servicing FIFO and begin playing out from the buffer.
|
||||||
|
///
|
||||||
|
/// queue reaches <QUEUE_RUNNING_DOWN> -> DRAINING
|
||||||
|
/// AltSetting = 0 -> DRAINING
|
||||||
|
Running = 3,
|
||||||
|
/// The queue is low. We will continue playout.
|
||||||
|
///
|
||||||
|
/// queue is empty && altSetting 1 -> NODATA
|
||||||
|
/// queue is empty && altSetting 0 -> STOPPED
|
||||||
|
/// queue reaches <QUEUE_RUNNING_UP> && altSetting 1 -> RUNNING
|
||||||
|
LowData = 4,
|
||||||
|
/// There is no data in the queue. We will count underflows for a while, send 0s, and hope the host comes back.
|
||||||
|
///
|
||||||
|
/// countdown reaches DATA_TIMEOUT -> STOPPED
|
||||||
|
/// AltSetting = 0 -> STOPPED
|
||||||
|
NoData = 5,
|
||||||
|
/// The host has asked us to stop (altSetting 0), but we need to play out the remaining buffer
|
||||||
|
///
|
||||||
|
/// queue is empty -> STOPPED
|
||||||
|
Stopping = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AudioState {
|
||||||
|
fn default() -> Self {
|
||||||
|
AudioState::Stopped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl defmt::Format for AudioState {
|
||||||
|
fn format(&self, fmt: defmt::Formatter) {
|
||||||
|
defmt::write!(
|
||||||
|
fmt,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Stopped => "Stopped",
|
||||||
|
Self::Armed => "Armed",
|
||||||
|
Self::Prefill => "Prefill",
|
||||||
|
Self::Running => "Running",
|
||||||
|
Self::LowData => "Draining",
|
||||||
|
Self::NoData => "NoData",
|
||||||
|
Self::Stopping => "Stopping",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for AudioState {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Stopped => "Stopped",
|
||||||
|
Self::Armed => "Armed",
|
||||||
|
Self::Prefill => "Prefill",
|
||||||
|
Self::Running => "Running",
|
||||||
|
Self::LowData => "Draining",
|
||||||
|
Self::NoData => "NoData",
|
||||||
|
Self::Stopping => "Stopping",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub struct AudioTelemetrySnapshot {
|
||||||
|
pub state: AudioState,
|
||||||
|
pub average_buffer_fill: u16,
|
||||||
|
pub frame_count: u32,
|
||||||
|
pub dac_underflow_count: u16,
|
||||||
|
pub usb_underflow_count: u16,
|
||||||
|
pub dac_overflow_count: u16,
|
||||||
|
pub p: i32,
|
||||||
|
pub i: i32,
|
||||||
|
pub fb: i32,
|
||||||
|
}
|
||||||
|
|
||||||
pub mod hid {
|
pub mod hid {
|
||||||
use deku::DekuRead;
|
use deku::DekuRead;
|
||||||
|
use usbd_hid::descriptor::generator_prelude::*;
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use usbd_hid::descriptor::generator_prelude::*;
|
|
||||||
|
use crate::{AudioState, AudioTelemetrySnapshot};
|
||||||
|
|
||||||
#[derive(DekuRead)]
|
#[derive(DekuRead)]
|
||||||
#[deku(endian = "little")]
|
#[deku(endian = "little")]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[gen_hid_descriptor(
|
#[gen_hid_descriptor(
|
||||||
(collection = APPLICATION, usage_page = VENDOR_DEFINED_START, usage = 0x01, ) = {
|
(collection = APPLICATION, usage_page = VENDOR_DEFINED_START, usage = 0x01, ) = {
|
||||||
state=input;
|
state=input;
|
||||||
@@ -37,4 +143,20 @@ pub mod hid {
|
|||||||
pub fb: i32,
|
pub fb: i32,
|
||||||
pub integrator: i32,
|
pub integrator: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AudioTelemetryReport> for AudioTelemetrySnapshot {
|
||||||
|
fn from(value: AudioTelemetryReport) -> Self {
|
||||||
|
AudioTelemetrySnapshot {
|
||||||
|
state: AudioState::try_from(value.state).expect("Invalid AudioState"),
|
||||||
|
average_buffer_fill: value.average_buffer_fill,
|
||||||
|
frame_count: i32::cast_unsigned(value.frame_count), // on firmware side is usize == u32
|
||||||
|
dac_underflow_count: value.dac_underflow_count,
|
||||||
|
usb_underflow_count: value.usb_underflow_count,
|
||||||
|
dac_overflow_count: value.dac_overflow_count,
|
||||||
|
p: value.p,
|
||||||
|
i: value.i,
|
||||||
|
fb: value.fb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user