telemetry improvements / cli CSV export

This commit is contained in:
2026-05-20 10:13:16 -07:00
parent 5e8d6e2a34
commit ab09c98084
8 changed files with 396 additions and 46 deletions
+2
View File
@@ -8,7 +8,9 @@ default = ["nodac", "hid"]
ak4490 = []
cs4398 = []
nodac = []
wm8904 = []
hid = [ "dep:usbd-hid", "dep:shared" ]
evk = [ "wm8904" ]
[dependencies]
shared = { path="../shared", optional = true }
-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,
}
+56 -18
View File
@@ -9,6 +9,7 @@ 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;
@@ -64,21 +65,19 @@ pub mod dac {
}
mod dma;
#[cfg(feature = "hid")]
mod hid;
mod hw;
mod traits;
const BYTES_PER_SAMPLE: usize = 4; // 32 bit samples
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 FRAMES_PER_SLOT: usize = SAMPLE_RATE as usize / 4000; // run the DMA at 4khz
const BYTES_PER_SLOT: usize = FRAMES_PER_SLOT * BYTES_PER_FRAME;
const N_SLOTS: usize = 8;
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
// 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 NODATA_TIMEOUT_FRAMES: usize = SAMPLE_RATE as usize / 100; // ~100ms
#[cfg(not(feature = "evk"))]
@@ -87,7 +86,7 @@ const MCLK_FREQ: u32 = 24576000;
const MCLK_FREQ: u32 = 24576000 / 2;
const SAMPLE_RATE: u32 = 192000;
const HID_INTERVAL_MS: u8 = 100;
const HID_INTERVAL_MS: u8 = 10;
struct CodecPins {
reset: Pin<pins::Pio0_3, Gpio<Output>>,
@@ -100,6 +99,7 @@ struct ClockSelPins {
#[derive(Default)]
struct PerfCounters {
state: Atomic<AudioState>,
received_frames: AtomicUsize,
played_frames: AtomicUsize,
min_fill: AtomicUsize,
@@ -107,6 +107,10 @@ struct PerfCounters {
queue_underflows: AtomicUsize,
queue_overflows: AtomicUsize,
audio_underflows: AtomicUsize,
integrator: AtomicI32,
p: AtomicI32,
i: AtomicI32,
fb: AtomicI32,
}
impl PerfCounters {
@@ -120,14 +124,22 @@ impl PerfCounters {
self.queue_underflows.store(0, Ordering::Relaxed);
self.queue_overflows.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 {
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,
dac_underflow_count: self.audio_underflows.load(Ordering::Relaxed) as i32,
usb_underflow_count: self.queue_underflows.load(Ordering::Relaxed) as i32,
dac_overflow_count: self.queue_overflows.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 u16,
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: self.fb.load(Ordering::Relaxed) as i32,
}
}
}
@@ -149,6 +161,7 @@ impl defmt::Format for PerfCounters {
}
static PERF: PerfCounters = PerfCounters {
state: Atomic::new(AudioState::Stopped),
received_frames: AtomicUsize::new(0), // received from USB
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
@@ -156,6 +169,10 @@ static PERF: PerfCounters = PerfCounters {
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),
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();
@@ -255,6 +272,11 @@ enum AudioState {
/// 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!(
@@ -328,6 +350,7 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
AudioState::NoData => self.nodata(),
}
self.state.store(state, Ordering::SeqCst);
PERF.state.store(state, Ordering::Relaxed);
}
fn init(&mut self) {
@@ -568,28 +591,43 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
PERF.queue_underflows.fetch_add(1, Ordering::Relaxed);
return Some(nominal_rate);
}
PERF.avg_fill
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| {
Some(((v << 6) - v + current_bytes as usize) >> 6)
})
.ok();
// normalize error wrt. frame size etc.
let error_permille = ((current_bytes - FILL_TARGET_BYTES) * 1000) / FILL_TARGET_BYTES;
let raw_error = current_bytes - 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 max_allowed_deviation = nominal_v / 500; // 0.2%
// 0.2% which is a huge clock error
let max_allowed_deviation = nominal_v / 500;
// 3. SEPARATE GAINS FOR P AND I
// For P: Keep your working math (converting raw error to a permille equivalent scale)
let error_permille = (raw_error * 1000) / FILL_TARGET_BYTES;
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
let i_term = 0; // placeholder
PERF.p.store(p_term, Ordering::Relaxed);
PERF.i.store(i_term, Ordering::Relaxed);
let mut v = nominal_v + p_term + i_term;
v = v.clamp(
nominal_v - max_allowed_deviation,
nominal_v + max_allowed_deviation,
);
PERF.fb.store(v, Ordering::Relaxed);
Some(UsbIsochronousFeedback::new(v as u32))
}
@@ -820,15 +858,15 @@ fn main() -> ! {
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
move || {
let active = usb_dev.poll(&mut [&mut uac2, &mut hid]);
if active && hid_update_timer.wait().is_ok() {
usb_dev.poll(&mut [&mut uac2, &mut hid]);
if hid_update_timer.wait().is_ok() {
let report = PERF.build_report();
match hid.push_input(&report) {
Ok(_) => {}
Err(UsbError::WouldBlock) => {}
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));
}
}