factor into workspace, improve features & deps
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# usbd-uac2 Examples
|
||||
|
||||
This repository contains example implementations of a USB Audio Class 2 (UAC2) device.
|
||||
|
||||
Two example backends are provided, both based on the LPCXpresso55S28 demo board, using the onboard WM8904 DAC:
|
||||
|
||||
- **Interrupt-driven example** (`lpc55s28-evk`)
|
||||
- **DMA-based example** (`lpc55s28-evk-dma`)
|
||||
|
||||
|
||||
## Examples Overview
|
||||
|
||||
### Interrupt-driven (`lpc55s28-evk`)
|
||||
|
||||
Running at 32bit/48khz. It can't keep up at 96khz. Works on USBFS and USBHS.
|
||||
|
||||
This is a minimal implementation intended to demonstrate the fundamental
|
||||
structure of the class driver. It fills a `bbqueue` as data comes in from USB,
|
||||
and drains it into the I2S FIFO in the I2S interrupt. This requires a lot of
|
||||
time-critical CPU work managing buffers. Particularly, the USB peripheral driver
|
||||
uses a lot of interrupt-free critical sections which can cause late interrupts
|
||||
and underruns.
|
||||
|
||||
This is intended primarily as a learning/reference implementation
|
||||
|
||||
### DMA-based (`lpc55s28-evk-dma`)
|
||||
|
||||
Running at 32bit/96khz. Works on USBFS and USBHS.
|
||||
|
||||
A more realistic and robust implementation using DMA. It fills a static ring
|
||||
buffer as data comes in from USB, while the DMA chases it around the ring,
|
||||
draining into the TX FIFO. This is efficient and decouples interrupt latency
|
||||
from data delivery. It uses the DMA interrupt to track consumed slots, but as
|
||||
long as the USB doesn't catch up to the read slot before this happens, there is
|
||||
a lot of slack for other things to be happening.
|
||||
|
||||
This is a more useful demonstration, but is still lacking correct handling of
|
||||
edge cases, error conditions and so on you would want in a fully fleshed out
|
||||
implementation. Particularly, it behaves poorly in underrun, since the DMA will
|
||||
keep emitting from the ring regardless of whether the data is valid, which
|
||||
sounds terrible.
|
||||
|
||||
## Running the Examples
|
||||
|
||||
You can flash and run either example using `cargo embed`:
|
||||
|
||||
```sh
|
||||
cargo embed --release --example lpc55s28-evk --features usbfs
|
||||
Generated
+1
-7
@@ -313,6 +313,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lpc55-hal"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/ktims/lpc55-hal?branch=main#8dfefd62aff4abd2de535f23107812dda68437be"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"cipher",
|
||||
@@ -353,7 +354,6 @@ dependencies = [
|
||||
"log-to-defmt",
|
||||
"lpc55-hal",
|
||||
"nb 1.1.0",
|
||||
"panic-halt",
|
||||
"panic-probe",
|
||||
"static_cell",
|
||||
"usb-device",
|
||||
@@ -458,12 +458,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "panic-halt"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11"
|
||||
|
||||
[[package]]
|
||||
name = "panic-probe"
|
||||
version = "1.0.0"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "lpc55s28-evk-dma"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["usbhs"]
|
||||
@@ -16,13 +17,13 @@ defmt-rtt = "1.1.0"
|
||||
embedded-hal = "1.0.0"
|
||||
embedded-io = "0.7.1"
|
||||
log-to-defmt = "0.1.0"
|
||||
lpc55-hal = { version = "0.5.0", path = "../lpc55-hal" }
|
||||
nb = "1.1.0"
|
||||
panic-halt = "1.0.0"
|
||||
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
|
||||
static_cell = "2.1.1"
|
||||
usb-device = "0.3"
|
||||
usbd-uac2 = { version = "0.1.0", path = "../..", features = ["defmt"]}
|
||||
# Includes update to usb-device 0.3, fix for isochronous and smaller critical sections
|
||||
lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" }
|
||||
usb-device.workspace = true
|
||||
usbd-uac2 = { workspace = true, features = ["defmt"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[default.general]
|
||||
chip = "LPC55S28JBD100"
|
||||
[default.rtt]
|
||||
enabled = true
|
||||
[default.gdb]
|
||||
enabled = true
|
||||
|
||||
[debug.rtt]
|
||||
enabled = false
|
||||
@@ -0,0 +1,5 @@
|
||||
// Find the actual path of memory.x and add it to link search, required for building in workspace
|
||||
fn main() {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
println!("cargo:rustc-link-search={}", manifest_dir);
|
||||
}
|
||||
@@ -233,13 +233,37 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
pub fn produced(&self) -> usize {
|
||||
self.produced.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn produced_bytes(&self) -> usize {
|
||||
self.produced_bytes.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn consumed(&self) -> usize {
|
||||
self.consumed.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn consumed_bytes(&self) -> usize {
|
||||
self.consumed.load(Ordering::Acquire) * self.slot_bytes
|
||||
+ (self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff)
|
||||
loop {
|
||||
let consumed_start = self.consumed.load(Ordering::Acquire);
|
||||
|
||||
let reg_1 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
||||
let reg_2 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
||||
|
||||
let consumed_end = self.consumed.load(Ordering::Acquire);
|
||||
|
||||
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 {
|
||||
0 // 0x3FF means all transfers completed, 0 bytes remaining
|
||||
} else {
|
||||
// Formula from NXP manual: (XFERCOUNT + 1) * Data Width
|
||||
(reg_1 + 1) * self.word_bytes
|
||||
};
|
||||
|
||||
// 2. Total bytes consumed in this specific active slot
|
||||
let active_slot_consumed = self.slot_bytes - remaining_bytes;
|
||||
|
||||
// 3. Combine with your software index history accumulator
|
||||
return consumed_start * self.slot_bytes + active_slot_consumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_slots(&self) -> usize {
|
||||
@@ -319,7 +343,7 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
let slots = unsafe { &mut *self.slots.get() };
|
||||
let desc = unsafe { &mut *self.desc.get() };
|
||||
let chan_desc = unsafe { &mut *self.channel_desc.get() };
|
||||
defmt::info!("slots base: &{:x}", self.slots.get());
|
||||
defmt::debug!("slots base: &{:x}", self.slots.get());
|
||||
|
||||
// Pre-fill with silence so underrun replays silence.
|
||||
for i in 0..N {
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
//! Contains hardware setup unrelated to Usb Audio Class implementation
|
||||
|
||||
use crate::hal;
|
||||
use core::cell::{OnceCell, UnsafeCell};
|
||||
use core::mem::MaybeUninit;
|
||||
use core::ptr::null_mut;
|
||||
|
||||
use crate::Syscon;
|
||||
use crate::hal;
|
||||
use crate::{MCLK_FREQ, SAMPLE_RATE, pac};
|
||||
use defmt::debug;
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
use core::mem::MaybeUninit;
|
||||
use defmt::{debug, info};
|
||||
use hal::{
|
||||
Iocon, Pin,
|
||||
Enabled, Iocon, Pin,
|
||||
drivers::pins,
|
||||
prelude::*,
|
||||
traits::wg::digital::v2::{OutputPin, ToggleableOutputPin},
|
||||
typestates::pin::{gpio::direction::Output, state::Gpio},
|
||||
};
|
||||
use lpc55_hal::Enabled;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
pub(crate) struct PllConstants {
|
||||
pub m: u16, // 1-65535
|
||||
pub n: u8, // 1-255
|
||||
@@ -138,67 +135,12 @@ pub(crate) fn init_audio_pll() {
|
||||
|
||||
pmc.pdruncfg0
|
||||
.modify(|_, w| w.pden_pll0().poweredon().pden_pll0_sscg().poweredon());
|
||||
debug!("pll0 wait for lock");
|
||||
info!("pll0 wait for lock");
|
||||
let mut i = 0usize;
|
||||
while syscon.pll0stat.read().lock().bit_is_clear() {
|
||||
i += 1;
|
||||
}
|
||||
debug!("pll0 locked after {} tries", i);
|
||||
}
|
||||
|
||||
const SYS_PLL: PllConstants = PllConstants::new(4, 75, 1); // 150MHz
|
||||
|
||||
pub(crate) fn init_sys_pll1() {
|
||||
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 pll1: {}", SYS_PLL);
|
||||
pmc.pdruncfg0.modify(|_, w| w.pden_pll1().poweredoff());
|
||||
syscon.pll1clksel.write(|w| w.sel().enum_0x1()); // clk_in
|
||||
syscon.pll1ctrl.write(|w| unsafe {
|
||||
w.clken()
|
||||
.enable()
|
||||
.seli()
|
||||
.bits(SYS_PLL.seli)
|
||||
.selp()
|
||||
.bits(SYS_PLL.selp)
|
||||
});
|
||||
|
||||
syscon
|
||||
.pll1ndec
|
||||
.write(|w| unsafe { w.ndiv().bits(SYS_PLL.n) });
|
||||
syscon.pll1ndec.write(|w| unsafe {
|
||||
w.ndiv().bits(SYS_PLL.n).nreq().set_bit() // latch
|
||||
});
|
||||
syscon
|
||||
.pll1mdec
|
||||
.write(|w| unsafe { w.mdiv().bits(SYS_PLL.m) });
|
||||
syscon
|
||||
.pll1pdec
|
||||
.write(|w| unsafe { w.pdiv().bits(SYS_PLL.p) });
|
||||
syscon.pll1pdec.write(|w| unsafe {
|
||||
w.pdiv().bits(SYS_PLL.p).preq().set_bit() // latch
|
||||
});
|
||||
|
||||
pmc.pdruncfg0.modify(|_, w| w.pden_pll1().poweredon());
|
||||
debug!("pll1 wait for lock");
|
||||
let mut i = 0usize;
|
||||
while syscon.pll1stat.read().lock().bit_is_clear() {
|
||||
i += 1;
|
||||
}
|
||||
debug!("pll1 locked after {} tries", i);
|
||||
// switch system clock to pll1
|
||||
syscon.fmccr.modify(|_, w| w.flashtim().flashtim11());
|
||||
syscon.mainclkselb.modify(|_, w| w.sel().enum_0x2()); // pll1
|
||||
info!("pll0 locked after {} loops", i);
|
||||
}
|
||||
|
||||
pub struct I2sTx {
|
||||
@@ -207,7 +149,6 @@ pub struct I2sTx {
|
||||
|
||||
pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -> I2sTx {
|
||||
defmt::debug!("init i2s");
|
||||
// Enable BOTH
|
||||
syscon.reset(&mut fc7);
|
||||
syscon.enable_clock(&mut fc7);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
//! Interrupt driven example for the LPCXpresso55S28 demo board
|
||||
//! DMA based audio output example for the LPCXpresso55S28 demo board
|
||||
//!
|
||||
//! Uses the onboard WM8904 DAC at 48KHz. Clock is generated by PLL0. Simple PI feedback
|
||||
//! is implemented.
|
||||
//! Uses the onboard WM8904 DAC at 96KHz. Clock is generated by PLL0. Simple proportional feedback is implemented.
|
||||
//!
|
||||
//! Packets from USB are placed a `heapless::spsc::Queue`. They are consumed
|
||||
//! by the I2S FIFO in the FLEXCOMM7 interrupt.
|
||||
//! USB walks around a static ring of slots, filling them as data comes in from
|
||||
//! the host. DMA chases it, filling the I2S FIFO as it drains to the DAC.
|
||||
//! Feedback ensures that the host doesn't overrun or underrun the ring.
|
||||
//!
|
||||
//! This implementation is more suitable for real use than the interrupt-based
|
||||
//! example, but it is still missing many niceties and behaves worse in
|
||||
//! anomalous situations since the DMA just keeps trucking over the ring
|
||||
//! regardless of the data validity.
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
@@ -34,13 +39,11 @@ use static_cell::StaticCell;
|
||||
use usb_device::{
|
||||
bus::{self},
|
||||
device::{StringDescriptors, UsbDeviceBuilder, UsbVidPid},
|
||||
endpoint::IsochronousSynchronizationType,
|
||||
};
|
||||
use usbd_uac2::UsbIsochronousFeedback;
|
||||
use usbd_uac2::TerminalConfig;
|
||||
use usbd_uac2::{
|
||||
self, AudioClassConfig, RangeEntry, TerminalConfig, UsbAudioClass, UsbAudioClockImpl, UsbSpeed,
|
||||
constants::{FunctionCode, TerminalType},
|
||||
descriptors::{ChannelConfig, ClockType, FormatType1, LockDelay},
|
||||
self, AudioHandler, ClockSource, RangeEntry, UsbAudioClassConfig, UsbIsochronousFeedback,
|
||||
UsbSpeed, constants::FunctionCode, descriptors::ClockType,
|
||||
};
|
||||
|
||||
use crate::dma::DmaRing;
|
||||
@@ -52,41 +55,27 @@ mod wm8904;
|
||||
|
||||
const CODEC_I2C_ADDR: u8 = 0b0011010;
|
||||
const MCLK_FREQ: u32 = 12288000;
|
||||
|
||||
const SAMPLE_RATE: u32 = 96000;
|
||||
const USB_FRAME_RATE: u32 = if cfg!(feature = "usbhs") { 8000 } else { 1000 };
|
||||
|
||||
//latency ≈ (current_fill × FRAMES_PER_SLOT)
|
||||
// + FRAMES_PER_SLOT/2 - average DMA transfer position
|
||||
// + 8 - FIFO depth @ 32-bit samples
|
||||
// with example values, ~2.3ms
|
||||
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 SLOT_SIZE_BYTES: usize = FRAMES_PER_SLOT * BYTES_PER_FRAME; // run the DMA at 2khz
|
||||
const N_SLOTS: usize = 32;
|
||||
const FILL_TARGET: i32 = (FRAMES_PER_SLOT * N_SLOTS) as i32 / 2;
|
||||
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;
|
||||
|
||||
struct Clock {}
|
||||
impl Clock {
|
||||
const RATES: [RangeEntry<u32>; 1] = [RangeEntry::new_fixed(SAMPLE_RATE)];
|
||||
}
|
||||
impl UsbAudioClockImpl for Clock {
|
||||
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
|
||||
const SOF_SYNC: bool = false;
|
||||
fn get_sample_rate(&self) -> u32 {
|
||||
Clock::RATES[0].min
|
||||
}
|
||||
fn get_rates(
|
||||
&self,
|
||||
) -> core::result::Result<&[usbd_uac2::RangeEntry<u32>], usbd_uac2::UsbAudioClassError> {
|
||||
Ok(&Clock::RATES)
|
||||
}
|
||||
fn get_clock_validity(&self) -> core::result::Result<bool, usbd_uac2::UsbAudioClassError> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
const LOG_PERIOD: u32 = 1000;
|
||||
|
||||
static DMA_RING: StaticCell<DmaRing<N_SLOTS, SLOT_SIZE_BYTES>> = StaticCell::new();
|
||||
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, SLOT_SIZE_BYTES>> = None;
|
||||
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]
|
||||
fn dma_ring() -> &'static DmaRing<N_SLOTS, SLOT_SIZE_BYTES> {
|
||||
fn dma_ring() -> &'static DmaRing<N_SLOTS, BYTES_PER_SLOT> {
|
||||
unsafe { DMA_RING_REF.unwrap() }
|
||||
}
|
||||
|
||||
@@ -125,13 +114,15 @@ struct Audio<'a, const N: usize, const MAX_SLOT_BYTES: usize> {
|
||||
running: AtomicBool,
|
||||
i2s: I2sTx,
|
||||
dma: &'a DmaRing<N, MAX_SLOT_BYTES>,
|
||||
log_counter: u32,
|
||||
}
|
||||
impl<const N: usize, const MAX_SLOT_BYTES: usize> Audio<'_, N, MAX_SLOT_BYTES> {
|
||||
fn start(&self) {
|
||||
const RATES: [RangeEntry<u32>; 1] = [RangeEntry::new_fixed(SAMPLE_RATE)];
|
||||
fn start(&mut self) {
|
||||
red_led().off(); // clear any dma error
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
|
||||
defmt::info!("playback starting (DMA)");
|
||||
defmt::info!("playback armed (DMA)");
|
||||
|
||||
let i2s = &self.i2s.i2s;
|
||||
i2s.fifotrig
|
||||
@@ -150,15 +141,15 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> Audio<'_, N, MAX_SLOT_BYTES> {
|
||||
// If we don't disable interrupts while stopped, we will underflow constantly and continuously refill the fifo with 0s
|
||||
// We could actually stop the I2S here, but sometimes that makes the DAC misbehave. The peripheral is configured to send
|
||||
// 0s when the FIFO is empty, so this is fine.
|
||||
pac::NVIC::mask(pac::Interrupt::DMA0);
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
dma_ring().stop();
|
||||
defmt::info!("playback stopped");
|
||||
pac::NVIC::mask(pac::Interrupt::DMA0);
|
||||
green_led().off();
|
||||
blue_led().off();
|
||||
}
|
||||
}
|
||||
impl<const N: usize, const MAX_SLOT_BYTES: usize, B: bus::UsbBus> UsbAudioClass<'_, B>
|
||||
impl<const N: usize, const MAX_SLOT_BYTES: usize, B: bus::UsbBus> AudioHandler<'_, B>
|
||||
for Audio<'_, N, MAX_SLOT_BYTES>
|
||||
{
|
||||
fn alternate_setting_changed(&mut self, _terminal: usb_device::UsbDirection, alt_setting: u8) {
|
||||
@@ -174,7 +165,8 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize, B: bus::UsbBus> UsbAudioClass<
|
||||
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
|
||||
) {
|
||||
// Buffer must fit 125us of audio data (based on how `usbd_uac2` sets up the descriptors).
|
||||
let mut buf = [0; SAMPLE_RATE.div_ceil(8000) as usize * BYTES_PER_FRAME];
|
||||
// Buffer must have room for one additional frame in case the host clock runs slower than the device.
|
||||
let mut buf = [0; (SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
||||
|
||||
let len = match ep.read(&mut buf) {
|
||||
Ok(len) => len,
|
||||
@@ -199,8 +191,8 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize, B: bus::UsbBus> UsbAudioClass<
|
||||
}
|
||||
// If we're not running yet, wait until we reach 50% full then enable DMA requests
|
||||
if !self.running.load(Ordering::Relaxed) && self.dma.fill_slots() >= (N_SLOTS / 2) {
|
||||
defmt::debug!(
|
||||
"buffer has {} slots, start dma transfers",
|
||||
defmt::info!(
|
||||
"buffer warmed ({} slots) starting playback",
|
||||
self.dma.fill_slots()
|
||||
);
|
||||
self.dma.run();
|
||||
@@ -208,52 +200,76 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize, B: bus::UsbBus> UsbAudioClass<
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide rate feedback to the host, so that it doesn't over- or underflow
|
||||
/// the buffer. Proportional-only control is stable with normal hosts,
|
||||
/// adding an I term with proper tuning (quite weak) would stabilize the
|
||||
/// rate reported to the host but is not necessary for basic playback.
|
||||
/// Provide rate feedback to the host. P-only is stable and works fine, with
|
||||
/// most hosts. The host can either filter it internally or treat it
|
||||
/// instantaneously and send more data specifically when the error gets
|
||||
/// large; we will absorb reasonable clock drifts with our ring buffer.
|
||||
fn feedback(&mut self, nominal_rate: UsbIsochronousFeedback) -> Option<UsbIsochronousFeedback> {
|
||||
const MAX_CORRECTION: i32 = 1 << 10; // ~1.6%
|
||||
// Don't want to signal an absurd rate when not consuming; let the
|
||||
// buffer fill before starting feedback.
|
||||
if !self.running.load(Ordering::Acquire) {
|
||||
return Some(nominal_rate);
|
||||
}
|
||||
|
||||
let produced_bytes = self.dma.produced_bytes.load(Ordering::Acquire);
|
||||
let produced_bytes = self.dma.produced_bytes();
|
||||
let consumed_bytes = self.dma.consumed_bytes();
|
||||
|
||||
let valid = produced_bytes >= consumed_bytes; // else we are in underrun condition
|
||||
if produced_bytes < consumed_bytes || produced_bytes == 0 {
|
||||
defmt::error!("[fb] dma underrun detected!");
|
||||
red_led().on();
|
||||
return Some(nominal_rate);
|
||||
}
|
||||
|
||||
let fill_frames = if valid {
|
||||
(produced_bytes - consumed_bytes) as i32 / BYTES_PER_FRAME as i32
|
||||
} else {
|
||||
// we will emit a canonical error in the DMA ISR
|
||||
defmt::debug!("[fb] dma underrun detected");
|
||||
0
|
||||
};
|
||||
|
||||
let mut error = fill_frames - FILL_TARGET;
|
||||
error = error.clamp(-32, 32); // avoid huge spikes
|
||||
|
||||
let p = error * 256;
|
||||
let i = 0; // placeholder
|
||||
|
||||
let correction = -(p + i);
|
||||
let current_bytes = (produced_bytes - consumed_bytes) as i32;
|
||||
// normalize error wrt. frame size etc.
|
||||
let error_permille = ((current_bytes - FILL_TARGET_BYTES) * 1000) / FILL_TARGET_BYTES;
|
||||
|
||||
let nominal_v = nominal_rate.to_u32_12_13() as i32;
|
||||
|
||||
let mut v = nominal_v + correction;
|
||||
// 0.2% which is a huge clock error
|
||||
let max_allowed_deviation = nominal_v / 500;
|
||||
|
||||
v = v.clamp(nominal_v - MAX_CORRECTION, nominal_v + MAX_CORRECTION);
|
||||
let p_term = -(error_permille * nominal_v) / 256000; // this works reasonably well to keep the buffer
|
||||
let i_term = 0; // placeholder
|
||||
|
||||
defmt::debug!(
|
||||
"valid:{} fill:{} err:{} fb:{=u32:x}",
|
||||
valid,
|
||||
fill_frames,
|
||||
error,
|
||||
v as u32
|
||||
let mut v = nominal_v + p_term + i_term;
|
||||
v = v.clamp(
|
||||
nominal_v - max_allowed_deviation,
|
||||
nominal_v + max_allowed_deviation,
|
||||
);
|
||||
self.log_counter += 1;
|
||||
if self.log_counter.is_multiple_of(LOG_PERIOD) {
|
||||
defmt::info!(
|
||||
"fill:{}% err_pm:{} p:{} i:{} fb_delta:{} fb:{=u32:x}",
|
||||
(current_bytes * 100) / (N_SLOTS * BYTES_PER_SLOT) as i32,
|
||||
error_permille,
|
||||
p_term,
|
||||
i_term,
|
||||
v - nominal_v,
|
||||
v as u32
|
||||
);
|
||||
}
|
||||
|
||||
Some(UsbIsochronousFeedback::new(v as u32))
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, const MAX_SLOT_BYTES: usize> ClockSource for Audio<'_, N, MAX_SLOT_BYTES> {
|
||||
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
|
||||
const SOF_SYNC: bool = false;
|
||||
fn sample_rate(&self) -> u32 {
|
||||
Self::RATES[0].min
|
||||
}
|
||||
fn sample_rates(
|
||||
&self,
|
||||
) -> core::result::Result<&[usbd_uac2::RangeEntry<u32>], usbd_uac2::UsbAudioClassError> {
|
||||
Ok(&Self::RATES)
|
||||
}
|
||||
fn clock_validity(&self) -> core::result::Result<bool, usbd_uac2::UsbAudioClassError> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
let hal = hal::new();
|
||||
@@ -277,7 +293,7 @@ fn main() -> ! {
|
||||
pins::Pio1_20::take().unwrap().into_i2c4_scl_pin(&mut iocon),
|
||||
pins::Pio1_21::take().unwrap().into_i2c4_sda_pin(&mut iocon),
|
||||
);
|
||||
// We can initialize and iocon these, but there is no peripheral, so they do not get used
|
||||
// We can initialize and iocon these, but there is no peripheral driver, so they do not get used
|
||||
let _codec_i2s_pins = (
|
||||
pins::Pio0_21::take().unwrap().into_spi7_sck_pin(&mut iocon),
|
||||
pins::Pio0_20::take().unwrap().into_i2s7_sda_pin(&mut iocon),
|
||||
@@ -286,18 +302,16 @@ fn main() -> ! {
|
||||
);
|
||||
|
||||
debug!("clocks");
|
||||
// Run the system clock at 96MHz. The lpc55-hal will run it from the FRO. But we won't actually use these clocks, we just need the guards...
|
||||
let clocks = hal::ClockRequirements::default()
|
||||
.system_frequency(96.MHz())
|
||||
.configure(&mut anactrl, &mut pmc, &mut syscon)
|
||||
.unwrap();
|
||||
let mut _delay_timer = Timer::new(
|
||||
let mut usb_delay_timer = Timer::new(
|
||||
hal.ctimer
|
||||
.0
|
||||
.enabled(&mut syscon, clocks.support_1mhz_fro_token().unwrap()),
|
||||
);
|
||||
// Start PLL1 at 150MHz as main system clock
|
||||
hw::init_sys_pll1();
|
||||
|
||||
// Start PLL0 at 24.576MHz as the audio clock. The FRO cannot evenly divide
|
||||
// any common audio frequencies and is not particularly stable anyway.
|
||||
hw::init_audio_pll();
|
||||
@@ -326,7 +340,7 @@ fn main() -> ! {
|
||||
&mut anactrl,
|
||||
&mut pmc,
|
||||
&mut syscon,
|
||||
&mut _delay_timer,
|
||||
&mut usb_delay_timer,
|
||||
clocks.support_usbhs_token().unwrap(),
|
||||
),
|
||||
);
|
||||
@@ -348,34 +362,21 @@ fn main() -> ! {
|
||||
|
||||
defmt::debug!("dma init");
|
||||
let i2s_dma_addr = &i2s_peripheral.i2s.fifowr as *const _ as *mut u32;
|
||||
let dma = DmaRing::<32, SLOT_SIZE_BYTES>::new(hal.dma.release(), &mut syscon, i2s_dma_addr, 4)
|
||||
.unwrap();
|
||||
let dma =
|
||||
DmaRing::<N_SLOTS, BYTES_PER_SLOT>::new(hal.dma.release(), &mut syscon, i2s_dma_addr, 4)
|
||||
.unwrap();
|
||||
let dma_ref = DMA_RING.init(dma);
|
||||
unsafe { DMA_RING_REF = Some(dma_ref) };
|
||||
|
||||
let mut clock = Clock {};
|
||||
let mut audio = Audio {
|
||||
i2s: i2s_peripheral,
|
||||
dma: dma_ring(),
|
||||
running: AtomicBool::new(false),
|
||||
log_counter: 0,
|
||||
};
|
||||
defmt::debug!("usb init");
|
||||
let config = AudioClassConfig::new(usb_speed, FunctionCode::Other, &mut clock, &mut audio)
|
||||
.with_output_config(TerminalConfig::new(
|
||||
4,
|
||||
1,
|
||||
2,
|
||||
FormatType1 {
|
||||
bit_resolution: 32,
|
||||
bytes_per_sample: 4,
|
||||
},
|
||||
TerminalType::ExtLineConnector,
|
||||
ChannelConfig::default_chans(2),
|
||||
IsochronousSynchronizationType::Asynchronous,
|
||||
LockDelay::Milliseconds(10),
|
||||
None,
|
||||
));
|
||||
|
||||
let config = UsbAudioClassConfig::new(usb_speed, FunctionCode::IoBox, &mut audio)
|
||||
.with_output_config(TerminalConfig::builder().base_id(2).build());
|
||||
let mut uac2 = config.build(&usb_bus).unwrap();
|
||||
|
||||
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x1209, 0xcc1d))
|
||||
|
||||
Generated
+1
-18
@@ -411,6 +411,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lpc55-hal"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/ktims/lpc55-hal?branch=main#8dfefd62aff4abd2de535f23107812dda68437be"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"cipher",
|
||||
@@ -451,10 +452,7 @@ dependencies = [
|
||||
"embedded-io",
|
||||
"log-to-defmt",
|
||||
"lpc55-hal",
|
||||
"nb 1.1.0",
|
||||
"panic-halt",
|
||||
"panic-probe",
|
||||
"static_cell",
|
||||
"usb-device",
|
||||
"usbd-uac2",
|
||||
]
|
||||
@@ -615,12 +613,6 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "panic-halt"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11"
|
||||
|
||||
[[package]]
|
||||
name = "panic-probe"
|
||||
version = "1.0.0"
|
||||
@@ -825,15 +817,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "static_cell"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "lpc55s28-evk"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["usbhs"]
|
||||
@@ -17,13 +18,11 @@ defmt-rtt = "1.1.0"
|
||||
embedded-hal = "1.0.0"
|
||||
embedded-io = "0.7.1"
|
||||
log-to-defmt = "0.1.0"
|
||||
lpc55-hal = { version = "0.5.0", path = "../lpc55-hal" }
|
||||
nb = "1.1.0"
|
||||
panic-halt = "1.0.0"
|
||||
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
|
||||
static_cell = "2.1.1"
|
||||
usb-device = "0.3"
|
||||
usbd-uac2 = { version = "0.1.0", path = "../..", features = ["defmt"]}
|
||||
# Includes update to usb-device 0.3, fix for isochronous and smaller critical sections
|
||||
lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" }
|
||||
usb-device.workspace = true
|
||||
usbd-uac2 = { workspace = true, features = ["defmt"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Find the actual path of memory.x and add it to link search, required for building in workspace
|
||||
fn main() {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
println!("cargo:rustc-link-search={}", manifest_dir);
|
||||
}
|
||||
@@ -224,7 +224,7 @@ impl<T: BbqHandle, B: bus::UsbBus> AudioHandler<'_, B> for Audio<T> {
|
||||
v = v.clamp(nominal_v - (1 << 14), nominal_v + (1 << 14));
|
||||
|
||||
// Note: this log will cause continuous underflows
|
||||
defmt::debug!("fill:{} err:{} fb:{=u32:x}", fill, error, v);
|
||||
defmt::debug!("fill:{} err:{} fb:{=u32:x}", fill, error, v as u32);
|
||||
|
||||
Some(UsbIsochronousFeedback::new(v as u32))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user