diff --git a/Cargo.lock b/Cargo.lock index 3d715ea..a76ac1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] @@ -515,14 +515,14 @@ version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" dependencies = [ - "defmt 1.1.0", + "defmt 1.1.1", ] [[package]] name = "defmt" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" dependencies = [ "bitflags 1.3.2", "defmt-macros", @@ -530,12 +530,11 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" dependencies = [ "defmt-parser", - "proc-macro-error2", "proc-macro2", "quote", "syn", @@ -557,7 +556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f73a4a4a91609e977ae3b7bd831ffa292edfd42ad140a3244a61d805b0e05e" dependencies = [ "critical-section", - "defmt 1.1.0", + "defmt 1.1.1", ] [[package]] @@ -764,7 +763,7 @@ dependencies = [ "bytemuck", "cortex-m", "cortex-m-rt", - "defmt 1.1.0", + "defmt 1.1.1", "defmt-rtt", "embedded-hal", "embedded-io", @@ -1178,6 +1177,27 @@ dependencies = [ "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]] name = "objc2" version = "0.6.4" @@ -1245,7 +1265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a" dependencies = [ "cortex-m", - "defmt 1.1.0", + "defmt 1.1.1", ] [[package]] @@ -1338,28 +1358,6 @@ dependencies = [ "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]] name = "proc-macro2" version = "1.0.106" @@ -1538,7 +1536,10 @@ dependencies = [ name = "shared" version = "0.0.0" dependencies = [ + "bytemuck", + "defmt 1.1.1", "deku", + "num_enum", "serde", "usbd-hid", ] @@ -1794,12 +1795,12 @@ dependencies = [ [[package]] name = "usbd-uac2" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117c289dcd316caa7aca9c0909117d8cf9d35f3ed2e7a5739067f0bcedc93e35" +checksum = "88b77e75c12ae8e2be3b2d8a111117a0025ea17ad6de7e8f941ba500adfa8a7a" dependencies = [ "byteorder-embedded-io", - "defmt 1.1.0", + "defmt 1.1.1", "embedded-io", "modular-bitfield", "num-traits", diff --git a/cli/src/main.rs b/cli/src/main.rs index ad08958..dd5b313 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,7 +4,7 @@ use async_hid::{AsyncHidRead, HidBackend, HidResult}; use clap::{Parser, ValueEnum}; use deku::DekuContainerRead; use futures_lite::StreamExt; -use shared::hid::AudioTelemetryReport; +use shared::{AudioTelemetrySnapshot, hid::AudioTelemetryReport}; #[derive(Clone, Copy, Debug, ValueEnum)] enum Format { @@ -23,7 +23,7 @@ trait StateEmitter { fn from_writer(writer: W) -> Self where Self: Sized; - fn emit(&mut self, r: &AudioTelemetryReport); + fn emit(&mut self, r: &AudioTelemetrySnapshot); } struct DebugEmitter { @@ -34,7 +34,7 @@ impl StateEmitter for DebugEmitter { fn from_writer(writer: W) -> Self { Self { writer } } - fn emit(&mut self, r: &AudioTelemetryReport) { + fn emit(&mut self, r: &AudioTelemetrySnapshot) { writeln!(self.writer, "{r:?}"); } } @@ -49,7 +49,7 @@ impl StateEmitter for CsvEmitter { 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) { eprintln!("Serialization error: {e:?}"); } else { @@ -80,7 +80,7 @@ async fn main() -> HidResult<()> { log::debug!("read {}: {:?}", r, &buf[..r]); let buf = &buf[..r]; 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), } } diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index a06f25a..6fb2601 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -32,7 +32,7 @@ panic-probe = { version = "1.0.0", features = ["print-defmt"] } static_cell = "2.1.1" usb-device = { version = "0.3", features = ["control-buffer-256"] } usbd-hid = { version = "0.10.0", optional = true } -usbd-uac2 = { version = "0.1.0", features = ["defmt"]} +usbd-uac2 = { version = "0.1.1", features = ["defmt"]} [profile.release] opt-level = "z" diff --git a/firmware/src/hw.rs b/firmware/src/hw.rs index 68fcfe8..875d348 100644 --- a/firmware/src/hw.rs +++ b/firmware/src/hw.rs @@ -1,5 +1,16 @@ +use core::cell::UnsafeCell; +use core::mem::MaybeUninit; + +use crate::hal; use crate::pac; + 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 m: u16, // 1-65535 @@ -184,3 +195,62 @@ pub(crate) fn init_audio_pll() { } info!("pll0 locked after {} loops", i); } + +pub struct SharedLed { + inner: UnsafeCell, +} +unsafe impl Sync for SharedLed {} +impl SharedLed { + 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 SharedLed { + pub fn toggle(&self) { + unsafe { + (*self.inner.get()).toggle().ok(); + } + } +} + +type Led1 = Pin>; +type Led2 = Pin>; +pub static LED1: MaybeUninit> = MaybeUninit::uninit(); +pub static LED2: MaybeUninit> = MaybeUninit::uninit(); + +pub fn init_leds(iocon: &mut Iocon, gpio: &mut hal::Gpio) { + 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); + core::ptr::write(LED2.as_ptr() as *mut SharedLed, led2); + } +} +pub fn led1() -> &'static SharedLed { + unsafe { &*LED1.as_ptr() } +} +pub fn led2() -> &'static SharedLed { + unsafe { &*LED2.as_ptr() } +} diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 7f463e9..8e3d22b 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -8,8 +8,6 @@ fn panic() -> ! { } use atomic::Atomic; -use bytemuck::NoUninit; -use core::error; use core::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering}; use cortex_m_rt::entry; use defmt; @@ -40,7 +38,10 @@ use usbd_uac2::{ use crate::dac::DacImpl; use crate::dma::DmaRing; +use crate::hw::led1; +use crate::hw::led2; use crate::traits::Dac; +use shared::AudioState; use shared::hid::AudioTelemetryReport; #[cfg(feature = "ak4490")] @@ -139,7 +140,7 @@ impl PerfCounters { integrator: self.integrator.load(Ordering::Relaxed), p: self.p.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), } } } @@ -175,6 +176,8 @@ static PERF: PerfCounters = PerfCounters { fb: AtomicI32::new(0), }; +static NODATA_FLAG: AtomicBool = AtomicBool::new(false); + static DMA_RING: StaticCell> = StaticCell::new(); static mut DMA_RING_REF: Option<&'static DmaRing> = None; #[inline] @@ -211,18 +214,22 @@ fn DMA0() { err, mem ); - // red_led().on(); dma.errint0.write(|w| unsafe { w.bits(1 << 19) }); } if (inta & (1 << 19)) != 0 { dma.inta0.write(|w| unsafe { w.bits(1 << 19) }); if dma_ring().advance_consumed(1).is_err() { - // red_led().on(); + PERF.audio_underflows.fetch_add(1, Ordering::Relaxed); } else { + led1().toggle(); PERF.played_frames .fetch_add(FRAMES_PER_SLOT, Ordering::Relaxed); } + if cur_fill() <= BYTES_PER_SLOT { + led2().on(); + NODATA_FLAG.store(true, Ordering::Release); + } } } @@ -235,65 +242,6 @@ fn FLEXCOMM7() { .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 -> RUNNING - /// AltSetting = 0 -> STOPPED - /// - Prefill, - /// Normal running state. Start servicing FIFO and begin playing out from the buffer. - /// - /// queue reaches -> 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 && 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 { correction_enabled: AtomicBool, integrator: AtomicI32, @@ -348,6 +296,7 @@ impl, I> Audio<'_, D, I> { AudioState::Running => self.run(), AudioState::LowData => {} AudioState::NoData => self.nodata(), + AudioState::Stopping => self.stopping(), } self.state.store(state, Ordering::SeqCst); PERF.state.store(state, Ordering::Relaxed); @@ -460,6 +409,9 @@ impl, I> Audio<'_, D, I> { Ordering::Relaxed, ); } + /// Transition -> Stopping + /// just a marker that upcoming nodata is expected, do nothing + fn stopping(&mut self) {} } impl, I> ClockSource for Audio<'_, D, I> { const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed; @@ -503,7 +455,8 @@ impl, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> { (0, AudioState::Armed | AudioState::Prefill | AudioState::NoData) => { 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, _) => {} // altSetting 1 in any other state is a no-op (_, _) => { @@ -545,8 +498,8 @@ impl, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> { // Valid states here are Armed, Prefill, Running, Draining and NoData match state { - AudioState::Stopped => { - defmt::error!("Received audio data when stopped") + AudioState::Stopped | AudioState::Stopping => { + defmt::error!("Received audio data when stopped/stopping") } // When armed, data rx goes to prefill AudioState::Armed => self.transition(AudioState::Prefill), @@ -748,16 +701,8 @@ fn main() -> ! { .into_gpio_pin(&mut iocon, &mut gpio) .into_output_low(), }; - let leds = ( - pins::Pio0_13::take() - .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(), - ); + + hw::init_leds(&mut iocon, &mut gpio); // iocon.disabled(&mut syscon).release(); // save the environment :) @@ -859,6 +804,14 @@ fn main() -> ! { move || { 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() { let report = PERF.build_report(); match hid.push_input(&report) { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 44688a6..b1a02c7 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -3,9 +3,13 @@ name = "shared" edition = "2024" [features] +std = [] serde = ["dep:serde"] [dependencies] +bytemuck = { version = "1.25.0", features = ["derive"] } +defmt = "1.1.1" 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" } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index fe09021..e56e654 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,14 +1,120 @@ #![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 -> RUNNING + /// AltSetting = 0 -> STOPPED + /// + Prefill = 2, + /// Normal running state. Start servicing FIFO and begin playing out from the buffer. + /// + /// queue reaches -> 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 && 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 { use deku::DekuRead; + use usbd_hid::descriptor::generator_prelude::*; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; - use usbd_hid::descriptor::generator_prelude::*; + + use crate::{AudioState, AudioTelemetrySnapshot}; #[derive(DekuRead)] #[deku(endian = "little")] - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[gen_hid_descriptor( (collection = APPLICATION, usage_page = VENDOR_DEFINED_START, usage = 0x01, ) = { state=input; @@ -37,4 +143,20 @@ pub mod hid { pub fb: i32, pub integrator: i32, } + + impl From 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, + } + } + } }