Compare commits

...
8 Commits
16 changed files with 1292 additions and 314 deletions
+66
View File
@@ -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
+285 -37
View File
@@ -23,6 +23,56 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]] [[package]]
name = "async-hid" name = "async-hid"
version = "0.5.1" version = "0.5.1"
@@ -167,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",
] ]
@@ -233,17 +283,87 @@ dependencies = [
"inout", "inout",
] ]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]] [[package]]
name = "cli" name = "cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-hid", "async-hid",
"clap",
"colog",
"csv",
"deku", "deku",
"futures-lite", "futures-lite",
"log",
"pollster", "pollster",
"shared", "shared",
] ]
[[package]]
name = "colog"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df62599ba6adc9c6c04a54278c8209125343dc4775f57b9d76c9a4287e58f2bd"
dependencies = [
"colored",
"env_logger",
"log",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "colored"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -333,6 +453,27 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "csv"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde_core",
]
[[package]]
name = "csv-core"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.21.3" version = "0.21.3"
@@ -374,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",
@@ -389,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",
@@ -416,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]]
@@ -491,6 +631,29 @@ dependencies = [
"num", "num",
] ]
[[package]]
name = "env_filter"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -593,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",
@@ -672,6 +835,12 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
@@ -703,6 +872,42 @@ dependencies = [
"generic-array 0.14.7", "generic-array 0.14.7",
] ]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -972,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"
@@ -1020,6 +1246,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "panic-halt" name = "panic-halt"
version = "1.0.0" version = "1.0.0"
@@ -1033,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]]
@@ -1108,6 +1340,15 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "3.5.0" version = "3.5.0"
@@ -1117,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"
@@ -1169,6 +1388,18 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.14" version = "0.4.14"
@@ -1223,6 +1454,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.1" version = "1.0.1"
@@ -1263,6 +1500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]
@@ -1298,7 +1536,11 @@ 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",
"usbd-hid", "usbd-hid",
] ]
@@ -1553,18 +1795,24 @@ 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",
"usb-device", "usb-device",
] ]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
+5 -1
View File
@@ -5,7 +5,11 @@ edition = "2024"
[dependencies] [dependencies]
async-hid = "0.5.1" async-hid = "0.5.1"
clap = { version = "4.6.1", features = ["derive"] }
colog = "1.4.0"
csv = "1.4.0"
deku = "0.20.3" deku = "0.20.3"
futures-lite = "2.6.1" futures-lite = "2.6.1"
log = { version = "0.4.29", features = ["std"] }
pollster = { version = "0.4.0", features = ["macro"] } pollster = { version = "0.4.0", features = ["macro"] }
shared = { path = "../shared" } shared = { path = "../shared", features = ["serde"] }
+64 -2
View File
@@ -1,10 +1,67 @@
use std::io;
use async_hid::{AsyncHidRead, HidBackend, HidResult}; use async_hid::{AsyncHidRead, HidBackend, HidResult};
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)]
enum Format {
Debug,
Csv,
}
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[arg(short, long, default_value = "debug")]
format: Format,
}
trait StateEmitter<W: io::Write> {
fn from_writer(writer: W) -> Self
where
Self: Sized;
fn emit(&mut self, r: &AudioTelemetrySnapshot);
}
struct DebugEmitter<T: io::Write> {
writer: T,
}
impl<W: io::Write> StateEmitter<W> for DebugEmitter<W> {
fn from_writer(writer: W) -> Self {
Self { writer }
}
fn emit(&mut self, r: &AudioTelemetrySnapshot) {
writeln!(self.writer, "{r:?}");
}
}
struct CsvEmitter<W: io::Write> {
csv: csv::Writer<W>,
}
impl<W: io::Write> StateEmitter<W> for CsvEmitter<W> {
fn from_writer(writer: W) -> Self {
Self {
csv: csv::Writer::from_writer(writer),
}
}
fn emit(&mut self, r: &AudioTelemetrySnapshot) {
if let Err(e) = self.csv.serialize(r) {
eprintln!("Serialization error: {e:?}");
} else {
self.csv.flush().ok();
}
}
}
#[pollster::main] #[pollster::main]
async fn main() -> HidResult<()> { async fn main() -> HidResult<()> {
colog::init();
let args = Args::parse();
let usbhid = HidBackend::default(); let usbhid = HidBackend::default();
let dev = usbhid let dev = usbhid
.enumerate() .enumerate()
@@ -13,12 +70,17 @@ async fn main() -> HidResult<()> {
.await .await
.expect("GUAC device not found or not accessible (try as root?)"); .expect("GUAC device not found or not accessible (try as root?)");
let mut reader = dev.open_readable().await?; let mut reader = dev.open_readable().await?;
let mut writer: Box<dyn StateEmitter<_>> = match args.format {
Format::Debug => Box::new(DebugEmitter::from_writer(io::stdout())),
Format::Csv => Box::new(CsvEmitter::from_writer(io::stdout())),
};
let mut buf = [0u8; core::mem::size_of::<AudioTelemetryReport>()]; let mut buf = [0u8; core::mem::size_of::<AudioTelemetryReport>()];
while let Ok(r) = reader.read_input_report(&mut buf).await { while let Ok(r) = reader.read_input_report(&mut buf).await {
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)) => println!("{:?}", r), Ok((_, r)) => writer.emit(&r.into()),
Err(e) => eprintln!("Unable to parse report: {:?}", e), Err(e) => eprintln!("Unable to parse report: {:?}", e),
} }
} }
+6 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "guac" name = "guac"
version = "0.1.0" version = "0.2.0"
edition = "2024" edition = "2024"
[features] [features]
@@ -8,10 +8,12 @@ default = ["nodac", "hid"]
ak4490 = [] ak4490 = []
cs4398 = [] cs4398 = []
nodac = [] nodac = []
hid = [ "dep:usbd-hid", "dep:shared" ] wm8904 = []
hid = [ "dep:usbd-hid" ]
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"] }
@@ -30,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"
+18 -3
View File
@@ -24,6 +24,7 @@ enum RegisterAddress {
pub struct Ak4490Dac<T> { pub struct Ak4490Dac<T> {
i2c: T, i2c: T,
pins: CodecPins, // this dependency is unfortunate, but non trivial to generalize pins: CodecPins, // this dependency is unfortunate, but non trivial to generalize
volume: (u8, u8),
} }
impl<T> Ak4490Dac<T> impl<T> Ak4490Dac<T>
@@ -43,6 +44,10 @@ where
_ => 5, _ => 5,
} }
} }
fn set_volume_impl(&mut self, left: u8, right: u8) {
self.write_reg(RegisterAddress::LeftAtt, left);
self.write_reg(RegisterAddress::RightAtt, right);
}
} }
impl<T> Dac<T> for Ak4490Dac<T> impl<T> Dac<T> for Ak4490Dac<T>
@@ -50,7 +55,11 @@ where
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write, T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
{ {
fn new(i2c: T, pins: CodecPins) -> Self { fn new(i2c: T, pins: CodecPins) -> Self {
Self { i2c, pins } Self {
i2c,
pins,
volume: (0xff, 0xff),
}
} }
fn init(&mut self) { fn init(&mut self) {
// bring out of reset // bring out of reset
@@ -66,7 +75,13 @@ where
self.write_reg(RegisterAddress::Control4, (dfs & 0x4) >> 1); self.write_reg(RegisterAddress::Control4, (dfs & 0x4) >> 1);
} }
fn set_volume(&mut self, left: u8, right: u8) { fn set_volume(&mut self, left: u8, right: u8) {
self.write_reg(RegisterAddress::LeftAtt, left); self.set_volume_impl(left, right);
self.write_reg(RegisterAddress::RightAtt, right); self.volume = (left, right);
}
fn mute(&mut self) {
self.set_volume_impl(0, 0);
}
fn unmute(&mut self) {
self.set_volume_impl(self.volume.0, self.volume.1);
} }
} }
+257
View File
@@ -0,0 +1,257 @@
use crate::{CodecPins, traits::Dac};
use embedded_hal::prelude::{
_embedded_hal_blocking_i2c_Write, _embedded_hal_blocking_i2c_WriteRead,
};
const WM8904_I2C_ADDRESS: u8 = 0b0011010;
const MCLK: u32 = 24576000 / 2; // assume EVK TODO: better
#[repr(u8)]
#[derive(Clone, Copy)]
#[allow(dead_code)]
enum RegisterAddress {
SwResetId = 0x00,
BiasControl0 = 0x04,
VmidControl = 0x05,
MicBiasControl0 = 0x06,
MicBiasControl1 = 0x07,
AnaAdc0 = 0x0a,
PowerMgmt0 = 0x0c,
PowerMgmt2 = 0x0e,
PowerMgmt3 = 0x0f,
PowerMgmt6 = 0x12,
ClockRates0 = 0x14,
ClockRates1 = 0x15,
ClockRates2 = 0x16,
AudioInterface0 = 0x18,
AudioInterface1 = 0x19,
AudioInterface2 = 0x1a,
AudioInterface3 = 0x1b,
DacDigiVolLeft = 0x1e,
DacDigiVolRight = 0x1f,
DacDigital0 = 0x20,
DacDigi1 = 0x21,
AdcDigiVolLeft = 0x24,
AdcDigiVolRight = 0x25,
AdcDigital0 = 0x26,
DigiMic0 = 0x27,
Drc0 = 0x28,
Drc1 = 0x29,
Drc2 = 0x2a,
Drc3 = 0x2b,
AnaLeftIn0 = 0x2c,
AnaRightIn0 = 0x2d,
AnaLeftIn1 = 0x2e,
AnaRightIn1 = 0x2f,
AnaOut1Left = 0x39,
AnaOut1Right = 0x3a,
AnaOut2Left = 0x3b,
AnaOut2Right = 0x3c,
AnaOut12Zc = 0x3d,
DcServo0 = 0x43,
DcServo1 = 0x44,
DcServo2 = 0x45,
DcServo4 = 0x47,
DcServo5 = 0x48,
DcServo6 = 0x49,
DcServo7 = 0x4a,
DcServo8 = 0x4b,
DcServo9 = 0x4c,
DcServoRb0 = 0x4d,
AnaHp0 = 0x5a,
AnaLineOut0 = 0x5e,
ChargePump0 = 0x62,
ClassW = 0x68,
WriteSeq0 = 0x6c,
WriteSeq1 = 0x6d,
WriteSeq2 = 0x6e,
WriteSeq3 = 0x6f,
WriteSeq4 = 0x70,
FllControl1 = 0x74,
FllControl2 = 0x75,
FllControl3 = 0x76,
FllControl4 = 0x77,
FllControl5 = 0x78,
GpioControl1 = 0x79,
GpioControl2 = 0x7a,
GpioControl3 = 0x7b,
GpioControl4 = 0x7c,
DigiPulls = 0x7e,
IntStatus = 0x7f,
IntStatusMask = 0x80,
IntPriority = 0x81,
IntDebounce = 0x82,
Eq1 = 0x86,
Eq2 = 0x87,
Eq3 = 0x88,
Eq4 = 0x89,
Eq5 = 0x8a,
Eq6 = 0x8b,
Eq7 = 0x8c,
Eq8 = 0x8d,
Eq9 = 0x8e,
Eq10 = 0x8f,
Eq11 = 0x90,
Eq12 = 0x91,
Eq13 = 0x92,
Eq14 = 0x93,
Eq15 = 0x94,
Eq16 = 0x95,
Eq17 = 0x96,
Eq18 = 0x97,
Eq19 = 0x98,
Eq20 = 0x99,
Eq21 = 0x9a,
Eq22 = 0x9b,
Eq23 = 0x9c,
Eq24 = 0x9d,
AdcTest0 = 0xc6,
FllNcoTest0 = 0xf7,
FllNcoTest1 = 0xf8,
}
pub struct Wm8904Dac<T> {
i2c: T,
pins: CodecPins,
mclk: u32,
}
impl<T> Wm8904Dac<T>
where
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
{
#[inline]
fn write_reg(&mut self, reg: RegisterAddress, val: u16) {
let b = val.to_be_bytes();
defmt::info!("i2c w [{:?}]", &[reg as u8, b[0], b[1]]);
self.i2c
.write(WM8904_I2C_ADDRESS, &[reg as u8, b[0], b[1]])
.ok();
}
fn cr1_for_rate(&self, rate: u32) -> u16 {
let fs_ratio = self.mclk / rate;
if !self.mclk.is_multiple_of(rate) {
defmt::warn!("[wm8904] sample rate should be a multiple of mclk");
}
let clk_sys_rate: u16 = match fs_ratio {
64 => 0,
128 => 1,
192 => 2,
256 => 3,
384 => 4,
512 => 5,
768 => 6,
1024 => 7,
1408 => 8,
1536 => 9,
_ => {
defmt::warn!("[wm8904] unsupport ratio {}", fs_ratio);
0
}
};
let sample_rate: u16 = match rate {
r if r < 11025 => 0, // 0-11024
r if r < 16000 => 1, // 11025 - 15999
r if r < 22050 => 2, // 16000 - 22049
r if r < 32000 => 3, // 22050 - 31999
r if r < 44100 => 4, // 32000 - 44099
_ => 5, // 44100+
};
(clk_sys_rate << 10) | sample_rate
}
fn blck_div_for_rate(&self, rate: u32) -> u16 {
let bits_per_frame = 64;
let bits_per_second = bits_per_frame * rate;
(self.mclk / bits_per_second) as u16
}
}
impl<T> Dac<T> for Wm8904Dac<T>
where
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
{
fn new(i2c: T, pins: CodecPins) -> Self {
Self {
i2c,
pins,
mclk: MCLK,
}
}
fn init(&mut self) {
let mut buf = [0u8; 2];
match self.i2c.write_read(WM8904_I2C_ADDRESS, &[0], &mut buf) {
Ok(_) => {
let chip_id = ((buf[0] as u16) << 8) | buf[1] as u16;
defmt::info!("[wm8904] Read chip ID: {:x}", chip_id)
}
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::WriteSeq0, 0x0100); // write sequencer 0 ENA
self.write_reg(RegisterAddress::WriteSeq3, 0x0100); // write sequencer 3 START, INDEX=0
// wait on write sequencer
defmt::info!("[wm8904] waiting on write seq");
loop {
let mut buf = [0; 2];
self.i2c
.write_read(
WM8904_I2C_ADDRESS,
&[RegisterAddress::WriteSeq4 as u8],
&mut buf,
)
.ok();
if buf[1] & 1 == 0 {
break;
}
}
defmt::debug!("[wm8904] write seq done");
self.write_reg(RegisterAddress::ClockRates0, 0);
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::PowerMgmt3, 0); //line outs disabled
self.write_reg(RegisterAddress::PowerMgmt6, 0x000c); // power management 6 = DACL_ENA | DACR_ENA
self.write_reg(RegisterAddress::AudioInterface0, 0x0050); // audio if 0 = AIFADCR_SRC | AIFDACR_SRC
self.write_reg(RegisterAddress::DacDigi1, 0x0040); // dac digital 1 = DAC_OSR128
self.write_reg(RegisterAddress::AnaLeftIn0, 0x0005);
self.write_reg(RegisterAddress::AnaRightIn0, 0x0005);
self.write_reg(RegisterAddress::AnaOut1Left, 0x0039); // analog out1 left = vol=0dB
self.write_reg(RegisterAddress::AnaOut1Right, 0x0039); // analog out1 right = vol=0dB
self.write_reg(RegisterAddress::AnaOut2Left, 0x0039); // analog out2 left = vol=0dB
self.write_reg(RegisterAddress::AnaOut2Right, 0x0039); // analog out2 right = vol=0dB
self.write_reg(RegisterAddress::DcServo0, 0x0003); // dc servo 0 = HPOUTL_ENA | HPOUTR_ENA
self.write_reg(RegisterAddress::AnaHp0, 0x00ff); // analog hp 0 = remove all shorts etc
self.write_reg(RegisterAddress::AnaLineOut0, 0x00ff); // analog lineout 0 = remove all shorts etc
self.write_reg(RegisterAddress::ClassW, 0x0001); // enable class w charge pump
self.write_reg(RegisterAddress::ChargePump0, 0x0001); // enable charge pump
self.write_reg(RegisterAddress::AudioInterface1, (3 << 2) | 2); // audio if 1 = i2s, 32 bit per sample
self.write_reg(RegisterAddress::ClockRates1, self.cr1_for_rate(96000)); // Set up for 48k, impl will change if needed
self.write_reg(RegisterAddress::ClockRates2, 0x000f); // clock rates 2 = CLK_SYS_ENA
self.write_reg(
RegisterAddress::AudioInterface2,
self.blck_div_for_rate(96000),
);
self.write_reg(RegisterAddress::AudioInterface3, 0); // audio interface 3 = input lrclock
self.write_reg(RegisterAddress::AnaOut12Zc, 0); // analog out12 zc = play source = dac
self.write_reg(RegisterAddress::DacDigiVolLeft, 0x01ff); // dac vol left = update left/right = 0dB
}
fn change_rate(&mut self, new_rate: u32) {
// TODO: mute, stop clocks etc.
defmt::info!("[wm8904] dac rate -> {}", new_rate);
self.write_reg(RegisterAddress::ClockRates1, self.cr1_for_rate(new_rate));
self.write_reg(
RegisterAddress::AudioInterface2,
self.blck_div_for_rate(new_rate),
);
}
fn mute(&mut self) {
// self.write_reg(RegisterAddress::DacDigiVolLeft, 0x0100);
}
fn unmute(&mut self) {
// TODO: restore previous volume
// self.write_reg(RegisterAddress::DacDigiVolLeft, 0x01ff);
}
}
+40 -53
View File
@@ -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();
} }
} }
-20
View File
@@ -1,20 +0,0 @@
use usbd_hid::descriptor::generator_prelude::*;
#[gen_hid_descriptor(
(collection = APPLICATION, usage_page = VENDOR_DEFINED_START, usage = 0x01, ) = {
average_buffer_fill=input;
frame_count=input;
dac_underflow_count=input;
usb_underflow_count=input;
dac_overflow_count=input;
}
)]
#[repr(C)]
// Note these are all actually u32
pub struct AudioTelemetryReport {
pub average_buffer_fill: i32,
pub frame_count: i32,
pub dac_underflow_count: i32,
pub usb_underflow_count: i32,
pub dac_overflow_count: i32,
}
+143 -1
View File
@@ -1,5 +1,16 @@
use core::cell::UnsafeCell;
use core::mem::MaybeUninit;
use crate::hal;
use crate::pac; use crate::pac;
use defmt::debug;
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
@@ -112,3 +123,134 @@ pub(crate) fn init_sys_pll1() {
syscon.fmccr.modify(|_, w| w.flashtim().flashtim11()); syscon.fmccr.modify(|_, w| w.flashtim().flashtim11());
syscon.mainclkselb.modify(|_, w| w.sel().enum_0x2()); // pll1 syscon.mainclkselb.modify(|_, w| w.sel().enum_0x2()); // pll1
} }
// Fo = M/(N*2*P) * Fin
// Fo = 3072/(125*2*8) * 16MHz = 24.576MHz
const AUDIO_PLL: PllConstants = PllConstants::new(125, 3072, 8);
// Set PLL0 to 24.576MHz, start, and wait for lock
// This is not exposed by lpc55-hal, unfortunately. Copy their implementation here.
pub(crate) fn init_audio_pll() {
let syscon = unsafe { &*pac::SYSCON::ptr() };
let pmc = unsafe { &*pac::PMC::ptr() };
let anactrl = unsafe { &*pac::ANACTRL::ptr() };
debug!("start clk_in");
pmc.pdruncfg0
.modify(|_, w| w.pden_xtal32m().poweredon().pden_ldoxo32m().poweredon());
syscon.clock_ctrl.modify(|_, w| w.clkin_ena().enable());
anactrl
.xo32m_ctrl
.modify(|_, w| w.enable_system_clk_out().enable());
debug!("init pll0: {}", AUDIO_PLL);
pmc.pdruncfg0
.modify(|_, w| w.pden_pll0().poweredoff().pden_pll0_sscg().poweredoff());
syscon.pll0clksel.write(|w| w.sel().enum_0x1()); // clk_in
syscon.pll0ctrl.write(|w| unsafe {
w.clken()
.enable()
.seli()
.bits(AUDIO_PLL.seli)
.selp()
.bits(AUDIO_PLL.selp)
});
syscon
.pll0ndec
.write(|w| unsafe { w.ndiv().bits(AUDIO_PLL.n) });
syscon.pll0ndec.write(|w| unsafe {
w.ndiv().bits(AUDIO_PLL.n).nreq().set_bit() // latch
});
syscon
.pll0pdec
.write(|w| unsafe { w.pdiv().bits(AUDIO_PLL.p) });
syscon.pll0pdec.write(|w| unsafe {
w.pdiv().bits(AUDIO_PLL.p).preq().set_bit() // latch
});
syscon.pll0sscg0.write(|w| unsafe { w.md_lbs().bits(0) });
syscon
.pll0sscg1
.write(|w| unsafe { w.mdiv_ext().bits(AUDIO_PLL.m).sel_ext().set_bit() });
syscon.pll0sscg1.write(|w| unsafe {
w.mdiv_ext()
.bits(AUDIO_PLL.m)
.sel_ext()
.set_bit()
.mreq()
.set_bit() // latch
.md_req()
.set_bit() // latch
});
pmc.pdruncfg0
.modify(|_, w| w.pden_pll0().poweredon().pden_pll0_sscg().poweredon());
info!("pll0 wait for lock");
let mut i = 0usize;
while syscon.pll0stat.read().lock().bit_is_clear() {
i += 1;
}
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() }
}
+253 -136
View File
@@ -8,7 +8,6 @@ fn panic() -> ! {
} }
use atomic::Atomic; use atomic::Atomic;
use bytemuck::NoUninit;
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;
@@ -39,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")]
@@ -57,28 +59,52 @@ pub mod dac {
mod noop; mod noop;
pub use self::noop::NoopDac as DacImpl; pub use self::noop::NoopDac as DacImpl;
} }
#[cfg(feature = "wm8904")]
pub mod dac {
mod wm8904;
pub use self::wm8904::Wm8904Dac as DacImpl;
}
mod dma; mod dma;
#[cfg(feature = "hid")]
mod hid;
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 / 2000; // run the DMA at 2khz 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) * 4) / 10; // 40% // 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
const MCLK_FREQ: u32 = 24576000;
const SAMPLE_RATE: u32 = 192000; const HID_INTERVAL_MS: u8 = 10;
const HID_INTERVAL_MS: u8 = 100;
struct CodecPins { struct CodecPins {
reset: Pin<pins::Pio0_3, Gpio<Output>>, reset: Pin<pins::Pio0_3, Gpio<Output>>,
@@ -91,6 +117,7 @@ struct ClockSelPins {
#[derive(Default)] #[derive(Default)]
struct PerfCounters { struct PerfCounters {
state: Atomic<AudioState>,
received_frames: AtomicUsize, received_frames: AtomicUsize,
played_frames: AtomicUsize, played_frames: AtomicUsize,
min_fill: AtomicUsize, min_fill: AtomicUsize,
@@ -98,6 +125,10 @@ struct PerfCounters {
queue_underflows: AtomicUsize, queue_underflows: AtomicUsize,
queue_overflows: AtomicUsize, queue_overflows: AtomicUsize,
audio_underflows: AtomicUsize, audio_underflows: AtomicUsize,
integrator: AtomicI32,
p: AtomicI32,
i: AtomicI32,
fb: AtomicI32,
} }
impl PerfCounters { impl PerfCounters {
@@ -105,20 +136,27 @@ 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);
self.p.store(0, Ordering::Relaxed);
self.i.store(0, Ordering::Relaxed);
// FB loop will have to take care of the fb value
} }
fn build_report(&self) -> AudioTelemetryReport { fn build_report(&self) -> AudioTelemetryReport {
AudioTelemetryReport { AudioTelemetryReport {
average_buffer_fill: self.avg_fill.load(Ordering::Relaxed) as i32, state: self.state.load(Ordering::Relaxed) as u8,
average_buffer_fill: self.avg_fill.load(Ordering::Relaxed) as u16,
frame_count: self.played_frames.load(Ordering::Relaxed) as i32, frame_count: self.played_frames.load(Ordering::Relaxed) as i32,
dac_underflow_count: self.audio_underflows.load(Ordering::Relaxed) as i32, dac_underflow_count: self.audio_underflows.load(Ordering::Relaxed) as u16,
usb_underflow_count: self.queue_underflows.load(Ordering::Relaxed) as i32, usb_underflow_count: self.queue_underflows.load(Ordering::Relaxed) as u16,
dac_overflow_count: self.queue_overflows.load(Ordering::Relaxed) as i32, dac_overflow_count: self.queue_overflows.load(Ordering::Relaxed) as u16,
integrator: self.integrator.load(Ordering::Relaxed),
p: self.p.load(Ordering::Relaxed),
i: self.i.load(Ordering::Relaxed),
fb: u32::cast_signed(self.fb.load(Ordering::Relaxed) as u32),
} }
} }
} }
@@ -140,19 +178,26 @@ impl defmt::Format for PerfCounters {
} }
static PERF: PerfCounters = PerfCounters { static PERF: PerfCounters = PerfCounters {
state: Atomic::new(AudioState::Stopped),
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),
integrator: AtomicI32::new(0),
p: AtomicI32::new(0),
i: 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() }
} }
@@ -164,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");
@@ -185,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);
} }
} }
} }
@@ -209,60 +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 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,
@@ -276,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 {
@@ -284,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()),
} }
} }
} }
@@ -294,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,
@@ -302,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!(
@@ -317,14 +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);
} }
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()
@@ -336,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()
@@ -381,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
@@ -423,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 {
@@ -440,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)
@@ -471,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
(_, _) => { (_, _) => {
@@ -485,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(_) => {
@@ -498,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,
@@ -513,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);
} }
} }
@@ -530,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);
@@ -559,28 +603,43 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
PERF.queue_underflows.fetch_add(1, Ordering::Relaxed); PERF.queue_underflows.fetch_add(1, Ordering::Relaxed);
return Some(nominal_rate); return Some(nominal_rate);
} }
PERF.avg_fill PERF.avg_fill
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| { .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| {
Some(((v << 6) - v + current_bytes as usize) >> 6) Some(((v << 6) - v + current_bytes as usize) >> 6)
}) })
.ok(); .ok();
// normalize error wrt. frame size etc. let raw_error = current_bytes - cur_fill_target();
let error_permille = ((current_bytes - FILL_TARGET_BYTES) * 1000) / FILL_TARGET_BYTES; let i_error = if raw_error.abs() <= 4 { 0 } else { raw_error }; // deadband
let current_i = self.fb.integrator.load(Ordering::Relaxed);
let leak = current_i >> 7;
let new_i = current_i
.saturating_sub(leak)
.saturating_add(i_error)
.clamp(-5000, 5000);
self.fb.integrator.store(new_i, Ordering::Relaxed);
PERF.integrator.store(new_i, Ordering::Relaxed);
let nominal_v = nominal_rate.to_u32_12_13() as i32; let nominal_v = nominal_rate.to_u32_12_13() as i32;
let max_allowed_deviation = nominal_v / 500; // 0.2%
// 0.2% which is a huge clock error // 3. SEPARATE GAINS FOR P AND I
let max_allowed_deviation = nominal_v / 500; // For P: Keep your working math (converting raw error to a permille equivalent scale)
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 i_term = (-((new_i as i64) * (nominal_v as i64)) / (256000 * 1000)) as i32;
let i_term = 0;
let p_term = -(error_permille * nominal_v) / 256000; // this works reasonably well to keep the buffer PERF.p.store(p_term, Ordering::Relaxed);
let i_term = 0; // placeholder PERF.i.store(i_term, Ordering::Relaxed);
let mut v = nominal_v + p_term + i_term; let mut v = nominal_v + p_term + i_term;
v = v.clamp( v = v.clamp(
nominal_v - max_allowed_deviation, nominal_v - max_allowed_deviation,
nominal_v + max_allowed_deviation, nominal_v + max_allowed_deviation,
); );
PERF.fb.store(v, Ordering::Relaxed);
Some(UsbIsochronousFeedback::new(v as u32)) Some(UsbIsochronousFeedback::new(v as u32))
} }
@@ -596,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()
@@ -616,12 +683,40 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
.unwrap() .unwrap()
.mclkio .mclkio
.modify(|_, w| w.mclkio().input()); .modify(|_, w| w.mclkio().input());
};
#[cfg(feature = "evk")]
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()
.fcclksel7() .mclkclksel
.modify(|_, w| w.sel().enum_0x5()); // MCLK .modify(|_, w| w.sel().enum_0x1()); // PLL0
}; pac::SYSCON::ptr()
.as_ref()
.unwrap()
.mclkdiv
.modify(|_, w| w.div().bits(1).halt().run().reset().released()); // div by 2 = PLL0 fout / 2 = 12.288MHz, max for WM8904 @ 96k
pac::SYSCON::ptr()
.as_ref()
.unwrap()
.mclkio
.modify(|_, w| w.mclkio().output());
}
// Select I2S TX function // Select I2S TX function
fc7.pselid.write(|w| w.persel().i2s_transmit()); fc7.pselid.write(|w| w.persel().i2s_transmit());
@@ -646,10 +741,17 @@ fn main() -> ! {
let usb0_vbus_pin = pins::Pio0_22::take() let usb0_vbus_pin = pins::Pio0_22::take()
.unwrap() .unwrap()
.into_usb0_vbus_pin(&mut iocon); .into_usb0_vbus_pin(&mut iocon);
#[cfg(not(feature = "evk"))]
let codec_i2c_pins = ( let codec_i2c_pins = (
pins::Pio0_16::take().unwrap().into_i2c4_scl_pin(&mut iocon), pins::Pio0_16::take().unwrap().into_i2c4_scl_pin(&mut iocon),
pins::Pio0_5::take().unwrap().into_i2c4_sda_pin(&mut iocon), pins::Pio0_5::take().unwrap().into_i2c4_sda_pin(&mut iocon),
); );
#[cfg(feature = "evk")]
let codec_i2c_pins = (
pins::Pio1_20::take().unwrap().into_i2c4_scl_pin(&mut iocon),
pins::Pio1_21::take().unwrap().into_i2c4_sda_pin(&mut iocon),
);
let codec_i2s_pins = ( let codec_i2s_pins = (
pins::Pio0_21::take().unwrap().into_spi7_sck_pin(&mut iocon), pins::Pio0_21::take().unwrap().into_spi7_sck_pin(&mut iocon),
pins::Pio0_20::take().unwrap().into_i2s7_sda_pin(&mut iocon), pins::Pio0_20::take().unwrap().into_i2s7_sda_pin(&mut iocon),
@@ -672,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 :)
@@ -691,6 +785,9 @@ fn main() -> ! {
.configure(&mut anactrl, &mut pmc, &mut syscon) .configure(&mut anactrl, &mut pmc, &mut syscon)
.unwrap(); .unwrap();
hw::init_sys_pll1(); hw::init_sys_pll1();
#[cfg(feature = "evk")]
hw::init_audio_pll();
let mut delay_timer = Timer::new( let mut delay_timer = Timer::new(
hal.ctimer hal.ctimer
.0 .0
@@ -726,8 +823,12 @@ 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(),
&mut syscon,
i2s_dma_addr,
4,
)
.unwrap(); .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) };
@@ -741,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,
}; };
@@ -779,22 +880,38 @@ fn main() -> ! {
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000)); hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
move || { move || {
let active = usb_dev.poll(&mut [&mut uac2, &mut hid]); usb_dev.poll(&mut [&mut uac2, &mut hid]);
if active && hid_update_timer.wait().is_ok() { // 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() {
let report = PERF.build_report(); let report = PERF.build_report();
match hid.push_input(&report) { match hid.push_input(&report) {
Ok(_) => {} Ok(_) => {}
Err(UsbError::WouldBlock) => {} Err(UsbError::WouldBlock) => {}
Err(e) => defmt::error!("Failed to send HID report: {:?}", e), Err(e) => defmt::error!("Failed to send HID report: {:?}", e),
} }
// lpc55 timer is not Periodic, so restart it // lpc55 ctimer is not Periodic, so restart it
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000)); hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
} }
} }
}; };
#[cfg(not(feature = "hid"))] #[cfg(not(feature = "hid"))]
let poll_all = || { let mut poll_all = {
move || {
usb_dev.poll(&mut [&mut uac2]); 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");
+4
View File
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.95.0"
targets = ["thumbv8m.main-none-eabihf"]
components = ["llvm-tools-preview"]
-39
View File
@@ -1,39 +0,0 @@
from dataclasses import dataclass
import struct
from time import sleep
from typing import Self
import hid
VID = 0x1209
PID = 0xCC1D
INTERVAL = 0.1
@dataclass
class AudioTelemetry:
STRUCT = "<LLLLL"
LEN = struct.calcsize(STRUCT)
average_buffer_fill: int
frame_count: int
dac_underflow_count: int
usb_underflow_count: int
dac_overflow_count: int
def from_bytes(b: bytes) -> Self:
if len(b) != AudioTelemetry.LEN:
raise ValueError(f"wrong size report ({len(b)} != {AudioTelemetry.LEN})")
fields = struct.unpack(AudioTelemetry.STRUCT, b)
return AudioTelemetry(*fields)
def main():
with hid.Device(VID, PID) as h:
while True:
report = AudioTelemetry.from_bytes(h.read(AudioTelemetry.LEN))
print(f"{report}")
sleep(INTERVAL)
if __name__ == "__main__":
main()
-10
View File
@@ -1,10 +0,0 @@
[project]
name = "guac-scripts"
version = "0.1.0"
description = "Scripts to work with GUAC devices"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"hid>=1.0.9",
"rich-click>=1.9.7",
]
+8
View File
@@ -2,6 +2,14 @@
name = "shared" name = "shared"
edition = "2024" edition = "2024"
[features]
std = []
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"] }
usbd-hid = { version = "0.10.0" } usbd-hid = { version = "0.10.0" }
+141 -6
View File
@@ -1,27 +1,162 @@
#![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::*; use usbd_hid::descriptor::generator_prelude::*;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{AudioState, AudioTelemetrySnapshot};
#[derive(DekuRead)] #[derive(DekuRead)]
#[deku(endian = "little")] #[deku(endian = "little")]
#[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;
average_buffer_fill=input; average_buffer_fill=input;
frame_count=input; frame_count=input;
dac_underflow_count=input; dac_underflow_count=input;
usb_underflow_count=input; usb_underflow_count=input;
dac_overflow_count=input; dac_overflow_count=input;
p=input;
i=input;
fb=input;
integrator=input;
} }
)] )]
#[repr(C)] #[repr(C, packed)]
// Note these are all actually u32 // TODO: Fix that most of these values are actually u32, but usbd_hid macro doesn't work properly on u32
pub struct AudioTelemetryReport { pub struct AudioTelemetryReport {
pub average_buffer_fill: i32, pub state: u8,
pub average_buffer_fill: u16,
pub frame_count: i32, pub frame_count: i32,
pub dac_underflow_count: i32, pub dac_underflow_count: u16,
pub usb_underflow_count: i32, pub usb_underflow_count: u16,
pub dac_overflow_count: i32, pub dac_overflow_count: u16,
pub p: i32,
pub i: i32,
pub fb: 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,
}
}
} }
} }