add stopping state, handle dma drain, improve cli readability (state) by moving state to shared

This commit is contained in:
2026-08-25 12:51:52 -07:00
parent 31edf91933
commit bdd946ad11
7 changed files with 271 additions and 121 deletions
Generated
+37 -36
View File
@@ -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",
+5 -5
View File
@@ -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<W: io::Write> {
fn from_writer(writer: W) -> Self
where
Self: Sized;
fn emit(&mut self, r: &AudioTelemetryReport);
fn emit(&mut self, r: &AudioTelemetrySnapshot);
}
struct DebugEmitter<T: io::Write> {
@@ -34,7 +34,7 @@ impl<W: io::Write> StateEmitter<W> for DebugEmitter<W> {
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<W: io::Write> StateEmitter<W> for CsvEmitter<W> {
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),
}
}
+1 -1
View File
@@ -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"
+70
View File
@@ -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<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() }
}
+30 -77
View File
@@ -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<DmaRing<N_SLOTS, BYTES_PER_SLOT>> = StaticCell::new();
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, BYTES_PER_SLOT>> = 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 <QUEUE_RUNNING_UP> -> RUNNING
/// AltSetting = 0 -> STOPPED
///
Prefill,
/// Normal running state. Start servicing FIFO and begin playing out from the buffer.
///
/// queue reaches <QUEUE_RUNNING_DOWN> -> DRAINING
/// AltSetting = 0 -> DRAINING
Running,
/// The queue is low. We will continue playout.
///
/// queue is empty && altSetting 1 -> NODATA
/// queue is empty && altSetting 0 -> STOPPED
/// queue reaches <QUEUE_RUNNING_UP> && altSetting 1 -> RUNNING
LowData,
/// There is no data in the queue. We will count underflows for a while, send 0s, and hope the host comes back, but maybe playback is done, which we should notice and shut down.
///
/// countdown reaches DATA_TIMEOUT -> STOPPED
/// AltSetting = 0 -> STOPPED
NoData,
}
impl Default for AudioState {
fn default() -> Self {
AudioState::Stopped
}
}
impl defmt::Format for AudioState {
fn format(&self, fmt: defmt::Formatter) {
defmt::write!(
fmt,
"{}",
match self {
Self::Stopped => "Stopped",
Self::Armed => "Armed",
Self::Prefill => "Prefill",
Self::Running => "Running",
Self::LowData => "Draining",
Self::NoData => "NoData",
}
)
}
}
struct FeedbackState {
correction_enabled: AtomicBool,
integrator: AtomicI32,
@@ -348,6 +296,7 @@ impl<D: Dac<I>, 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<D: Dac<I>, I> Audio<'_, D, I> {
Ordering::Relaxed,
);
}
/// Transition -> Stopping
/// just a marker that upcoming nodata is expected, do nothing
fn stopping(&mut self) {}
}
impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
@@ -503,7 +455,8 @@ impl<D: Dac<I>, 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<D: Dac<I>, 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) {
+4
View File
@@ -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" }
+124 -2
View File
@@ -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 <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 {
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<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,
}
}
}
}