lpc55s28-evk: add audio input

This commit is contained in:
2026-05-16 16:07:45 -07:00
parent fb050c4a40
commit ffbdef1b6d
3 changed files with 357 additions and 139 deletions
+98 -46
View File
@@ -10,6 +10,7 @@ use defmt::debug;
use hal::{ use hal::{
Enabled, Iocon, Pin, Enabled, Iocon, Pin,
drivers::pins, drivers::pins,
peripherals::syscon::{ClockControl, ResetControl},
traits::wg::digital::v2::{OutputPin, ToggleableOutputPin}, traits::wg::digital::v2::{OutputPin, ToggleableOutputPin},
typestates::pin::{gpio::direction::Output, state::Gpio}, typestates::pin::{gpio::direction::Output, state::Gpio},
}; };
@@ -143,18 +144,35 @@ pub(crate) fn init_audio_pll() {
debug!("pll0 locked after {} tries", i); debug!("pll0 locked after {} tries", i);
} }
pub struct I2sTx { pub struct I2sHandles {
pub i2s: pac::I2S7, pub tx: pac::I2S7,
pub rx: pac::I2S6,
} }
pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -> I2sTx { pub fn init_i2s(
fc7: pac::FLEXCOMM7,
i2s7: pac::I2S7,
fc6: pac::FLEXCOMM6,
i2s6: pac::I2S6,
syscon: &mut Syscon,
) -> I2sHandles {
defmt::debug!("init i2s"); defmt::debug!("init i2s");
// Enable BOTH // Enable BOTH
syscon.reset(&mut fc7); fc7.clear_reset(syscon);
syscon.enable_clock(&mut fc7); fc7.enable_clock(syscon);
fc6.clear_reset(syscon);
unsafe { fc6.enable_clock(syscon);
pac::IOCON::ptr().as_ref().unwrap().pio1_31.modify(|_, w| { {
let sc = unsafe { pac::SYSCON::ptr().as_ref().unwrap() };
let ioc = unsafe { pac::IOCON::ptr().as_ref().unwrap() };
// MCLK source
//
sc.mclkclksel.write(|w| w.sel().enum_0x1()); // PLL0
// MCLK div
sc.mclkdiv
.write(|w| unsafe { w.div().bits(1).halt().run().reset().released() }); // div by 2 = PLL0 fout / 2 = 12.288MHz, max for WM8904 @ 96k
// MCLK out config
ioc.pio1_31.modify(|_, w| {
w.func() w.func()
.alt1() .alt1()
.mode() .mode()
@@ -168,57 +186,65 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
.od() .od()
.normal() .normal()
}); });
pac::SYSCON::ptr() // FC7 clock
.as_ref() sc.fcclksel7().modify(|_, w| w.sel().enum_0x5()); // MCLK
.unwrap() // FC6 clock
.fcclksel7() sc.fcclksel6().modify(|_, w| w.sel().enum_0x5()); // MCLK
.modify(|_, w| w.sel().enum_0x5()); // MCLK // MCLK out
pac::SYSCON::ptr() sc.mclkio.modify(|_, w| w.mclkio().output());
.as_ref()
.unwrap() // Enable clock for sysctl, it's not mapped into Peripherals
.mclkclksel sc.ahbclkctrlset[2].write(|w| unsafe { w.bits(1 << 15) });
.modify(|_, w| w.sel().enum_0x1()); // PLL0 while sc.ahbclkctrl2.read().sysctl().is_disable() {}
pac::SYSCON::ptr() let sysctrl = unsafe { pac::SYSCTL::ptr().as_ref().unwrap() };
.as_ref() sysctrl.sharedctrlset[0].write(|w| w.sharedscksel().flexcomm7().sharedwssel().flexcomm7()); // FC7 drives shared SCK, WS
.unwrap() sysctrl.fcctrlsel[7].write(|w| {
.mclkdiv w.sckinsel()
.modify(|_, w| w.div().bits(1).halt().run().reset().released()); // div by 2 = PLL0 fout / 2 = 12.288MHz, max for WM8904 @ 96k .shared_set0_i2s_signals()
pac::SYSCON::ptr() .wsinsel()
.as_ref() .shared_set0_i2s_signals()
.unwrap() }); // FC7 uses shared set
.mclkio sysctrl.fcctrlsel[6].write(|w| {
.modify(|_, w| w.mclkio().output()); w.sckinsel()
}; .shared_set0_i2s_signals()
.wsinsel()
.shared_set0_i2s_signals()
});
// for _ in 0..1000 {
// cortex_m::asm::nop();
// }
}
// Select I2S TX function // Select I2S TX function
fc7.pselid.write(|w| w.persel().i2s_transmit()); fc7.pselid.write(|w| w.persel().i2s_transmit());
// Select I2S RX function
fc6.pselid.write(|w| w.persel().i2s_receive());
let regs = i2s7; let out_regs = i2s7;
let in_regs = i2s6;
// Enable TX FIFO only // Enable TX FIFO only
regs.fifocfg.modify(|_, w| { out_regs.fifocfg.write(|w| {
w.enabletx() w.enabletx()
.enabled() .enabled()
.enablerx() .txi2se0() // transmit 0s when empty - only supported option for 32b data
.disabled()
.dmatx()
.disabled()
.txi2se0()
.zero() .zero()
.emptytx() // reset the tx queue
.set_bit()
}); });
// Flush out_regs
regs.fifocfg.modify(|_, w| w.emptytx().set_bit()); .cfg2
.write(|w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
regs.cfg2
.modify(|_, w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
let bclk_div = (MCLK_FREQ / SAMPLE_RATE / 64) as u16; let bclk_div = (MCLK_FREQ / SAMPLE_RATE / 64) as u16;
regs.div out_regs
.modify(|_, w| unsafe { w.div().bits(bclk_div - 1) }); // Clock source is MCLK (12.288MHz) / 4 = 3MHz .div
.write(|w| unsafe { w.div().bits(bclk_div - 1) }); // Clock source is MCLK (12.288MHz) / 4 = 3MHz
// Config // TX Config
regs.cfg1.modify(|_, w| unsafe { out_regs.cfg1.write(|w| unsafe {
w.mstslvcfg() w.mstslvcfg()
.normal_master() .normal_master()
.onechannel() .onechannel()
@@ -233,7 +259,33 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
.normal() .normal()
}); });
I2sTx { i2s: regs } // Enable RX FIFO only
in_regs
.fifocfg
.write(|w| w.enablerx().enabled().emptyrx().set_bit());
in_regs
.cfg2
.write(|w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
in_regs.div.write(|w| unsafe { w.div().bits(0) });
in_regs.cfg1.write(|w| unsafe {
w.mstslvcfg()
.normal_slave_mode()
.onechannel()
.dual_channel()
.datalen()
.bits(31)
.mainenable()
.enabled()
.mode()
.classic_mode()
.datapause()
.normal()
});
I2sHandles {
tx: out_regs,
rx: in_regs,
}
} }
pub struct SharedLed<T: OutputPin> { pub struct SharedLed<T: OutputPin> {
+244 -84
View File
@@ -3,12 +3,22 @@
//! This is a minimal implementation intended to demonstrate only the minimum //! This is a minimal implementation intended to demonstrate only the minimum
//! essential implementation, to clarify usage of the class driver. //! essential implementation, to clarify usage of the class driver.
//! //!
//! Uses the EVK's WM8904 DAC at 48KHz. Clock is generated by PLL0. Simple //! Uses the EVK's WM8904 CODEC at 48KHz. Clock is generated by PLL0. Simple
//! proportional feedback is implemented. //! proportional feedback is implemented.
//! //!
//! Packets from USB are placed in a `bbqueue`. They are consumed by the I2S //! Packets from USB are placed in a `bbqueue` (a lock free ring buffer). They
//! FIFO in the FLEXCOMM7 interrupt. This makes the implementation very sensitive //! are consumed by the I2S FIFO in the FLEXCOMM7 interrupt. This makes the
//! to interrupt::free critical sections (which are widely used in the USB bus driver) //! implementation very sensitive to interrupt::free critical sections (which
//! are widely used in the USB bus driver), but it works well enough at 48k.
//!
//! Audio frames from the CODEC are placed in a separate `bbqueue` from the
//! FLEXCOMM6 interrupt handler. They're drained out to the USB host in the USB
//! callback context.
//!
//! LEDs:
//! - Blue - audio under/overrun - note this toggles a bunch at playback stream startup
//! - Red - recording stream running
//! - Green - playback stream running
#![no_main] #![no_main]
#![no_std] #![no_std]
@@ -26,9 +36,9 @@ use bbqueue::{
prod_cons::stream::{StreamConsumer, StreamProducer}, prod_cons::stream::{StreamConsumer, StreamProducer},
traits::bbqhdl::BbqHandle, traits::bbqhdl::BbqHandle,
}; };
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use core::sync::atomic::{AtomicU32, Ordering};
use cortex_m_rt::entry; use cortex_m_rt::entry;
use defmt::debug; use defmt::{debug, error, info, warn};
use defmt_rtt as _; use defmt_rtt as _;
use hal::raw as pac; use hal::raw as pac;
use hal::{ use hal::{
@@ -41,18 +51,24 @@ use lpc55_hal as hal;
use pac::interrupt; use pac::interrupt;
use usb_device::{ use usb_device::{
bus::{self}, bus::{self},
device::{StringDescriptors, UsbDeviceBuilder, UsbVidPid}, device::{StringDescriptors, UsbVidPid},
}; };
use usbd_uac2::{ use usbd_uac2::{
self, AudioHandler, ClockSource, RangeEntry, TerminalConfig, UsbAudioClassConfig, self, AudioHandler, ClockSource, RangeEntry, TerminalConfig, UsbAudioClassConfig,
UsbIsochronousFeedback, UsbSpeed, constants::FunctionCode, descriptors::ClockType, UsbIsochronousFeedback, UsbSpeed, constants::FunctionCode, descriptors::ClockType,
}; };
use crate::hw::{I2sTx, blue_led, green_led, red_led}; use crate::hw::{I2sHandles, blue_led, green_led, red_led};
mod hw; mod hw;
mod wm8904; mod wm8904;
// pid.codes test IDs
const USB_VID: u16 = 0x1209;
const USB_PID: u16 = 0x0001;
const USB_MANUFACTURER: &str = "usbd_uac2";
const USB_PRODUCT: &str = "interrupt example device";
const CODEC_I2C_ADDR: u8 = 0b0011010; const CODEC_I2C_ADDR: u8 = 0b0011010;
const FIFO_LENGTH: usize = 256; // frames const FIFO_LENGTH: usize = 256; // frames
const MCLK_FREQ: u32 = 12288000; const MCLK_FREQ: u32 = 12288000;
@@ -62,18 +78,25 @@ const SAMPLE_RATE: u32 = 48000;
const USB_FRAME_RATE: usize = 8000; const USB_FRAME_RATE: usize = 8000;
#[cfg(feature = "usbfs")] #[cfg(feature = "usbfs")]
const USB_FRAME_RATE: usize = 1000; const USB_FRAME_RATE: usize = 1000;
type SampleType = (i32, i32);
const BYTES_PER_FRAME: usize = 8; const BYTES_PER_FRAME: usize = 8;
const QUEUE_BYTES: usize = FIFO_LENGTH * BYTES_PER_FRAME; const QUEUE_BYTES: usize = FIFO_LENGTH * BYTES_PER_FRAME;
// We use bbqueue here for performance in the USB driver that runs almost entirely in interrupt free critical section.
static QUEUE: Churrasco<QUEUE_BYTES> = Churrasco::new();
// Used for feedback calculation of current fifo state
static PRODUCED: AtomicU32 = AtomicU32::new(0);
static CONSUMED: AtomicU32 = AtomicU32::new(0);
static QUEUE_AUDIO_OUT: Churrasco<QUEUE_BYTES> = Churrasco::new();
// Used for feedback calculation of current fifo state
static PRODUCED_OUT: AtomicU32 = AtomicU32::new(0);
static CONSUMED_OUT: AtomicU32 = AtomicU32::new(0);
static QUEUE_AUDIO_IN: Churrasco<QUEUE_BYTES> = Churrasco::new();
// Used for feedback calculation of current fifo state
static PRODUCED_IN: AtomicU32 = AtomicU32::new(0);
static CONSUMED_IN: AtomicU32 = AtomicU32::new(0);
/// Consume one audio output frame and send it to the I2S FIFO. If there's
/// insufficient data in the queue, return false without consuming what was
/// there.
#[inline] #[inline]
fn try_write_one_frame<T: BbqHandle>( fn try_consume_one_frame<T: BbqHandle>(
cons: &mut StreamConsumer<T>, cons: &mut StreamConsumer<T>,
i2s: &pac::i2s7::RegisterBlock, i2s: &pac::i2s7::RegisterBlock,
) -> bool { ) -> bool {
@@ -87,7 +110,7 @@ fn try_write_one_frame<T: BbqHandle>(
// consume exactly one frame (8 bytes) // consume exactly one frame (8 bytes)
rgr.release(BYTES_PER_FRAME); rgr.release(BYTES_PER_FRAME);
CONSUMED.fetch_add(BYTES_PER_FRAME as u32, Ordering::Relaxed); CONSUMED_OUT.fetch_add(BYTES_PER_FRAME as u32, Ordering::Relaxed);
return true; return true;
} else { } else {
// Not enough bytes for a full frame: leave it in the queue. // Not enough bytes for a full frame: leave it in the queue.
@@ -97,16 +120,43 @@ fn try_write_one_frame<T: BbqHandle>(
false false
} }
/// Produce one audio input from from the I2S FIFO into the queue. If there's no
/// room in the queue, discard the sample and return false.
#[inline]
fn try_produce_one_frame<T: BbqHandle>(
prod: &mut StreamProducer<T>,
i2s: &pac::i2s6::RegisterBlock,
) -> bool {
let l = i2s.fiford.read().rxdata().bits();
let r = i2s.fiford.read().rxdata().bits();
if let Ok(mut rgr) = prod.grant_exact(BYTES_PER_FRAME) {
unsafe {
let wp = rgr.as_mut_ptr();
core::ptr::copy_nonoverlapping(&l as *const u32 as *const u8, wp, 4);
core::ptr::copy_nonoverlapping(&r as *const u32 as *const u8, wp.add(4), 4);
}
rgr.commit(BYTES_PER_FRAME * 2);
PRODUCED_IN.fetch_add(BYTES_PER_FRAME as u32, Ordering::Relaxed);
true
} else {
// No space in the queue, discard the sample
false
}
}
/// I2S TX ISR. Consume frames until the FIFO is full. Only write full frames
/// (L+R) so we can't desync left/right.
#[interrupt] #[interrupt]
fn FLEXCOMM7() { fn FLEXCOMM7() {
let i2s = unsafe { &*pac::I2S7::ptr() }; let i2s = unsafe { pac::I2S7::ptr().as_ref().unwrap() };
// refill until fifo has >6 words // refill until fifo has >6 words
let mut cons = QUEUE.stream_consumer(); let mut cons = QUEUE_AUDIO_OUT.stream_consumer();
while i2s.fifostat.read().txlvl().bits() <= 6 { while i2s.fifostat.read().txlvl().bits() <= 6 {
if !try_write_one_frame(&mut cons, i2s) { if !try_consume_one_frame(&mut cons, i2s) {
// No complete frame available: write silence to keep FIFO above threshold // No complete frame available: write silence to keep FIFO above threshold
defmt::error!("underflow"); // error!("tx queue underflow");
red_led().toggle(); blue_led().toggle();
i2s.fifowr.write(|w| unsafe { w.bits(0) }); i2s.fifowr.write(|w| unsafe { w.bits(0) });
i2s.fifowr.write(|w| unsafe { w.bits(0) }); i2s.fifowr.write(|w| unsafe { w.bits(0) });
break; break;
@@ -114,38 +164,83 @@ fn FLEXCOMM7() {
} }
} }
struct Audio<T: BbqHandle> { /// I2S RX ISR. Produce frames until the FIFO is empty (or has only a partial frame).
running: AtomicBool, #[interrupt]
i2s: I2sTx, fn FLEXCOMM6() {
producer: StreamProducer<T>, let i2s = unsafe { pac::I2S6::ptr().as_ref().unwrap() };
let mut prod = QUEUE_AUDIO_IN.stream_producer();
while i2s.fifostat.read().rxlvl().bits() >= 2 {
if !try_produce_one_frame(&mut prod, i2s) {
blue_led().toggle();
// error!("rx queue overflow");
break;
} }
impl<T: BbqHandle> Audio<T> {
const RATES: [RangeEntry<u32>; 1] = [RangeEntry::new_fixed(SAMPLE_RATE)];
fn start(&self) {
self.running.store(true, Ordering::Relaxed);
defmt::info!("playback starting, enabling interrupts");
self.i2s.i2s.fifointenclr.write(|w| w.txlvl().set_bit());
// FIFO trigger threshold = <= 6 entries
self.i2s
.i2s
.fifotrig
.modify(|_, w| unsafe { w.txlvl().bits(6).txlvlena().enabled() });
// FIFO level interrupt enable
self.i2s.i2s.fifointenset.modify(|_, w| w.txlvl().enabled());
unsafe { pac::NVIC::unmask(pac::Interrupt::FLEXCOMM7) };
green_led().on();
}
fn stop(&self) {
// 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.
self.running.store(true, Ordering::Relaxed);
defmt::info!("playback stopped");
pac::NVIC::mask(pac::Interrupt::FLEXCOMM7);
green_led().off();
} }
} }
/// Storage for Audio Class implementation state
struct Audio<T: BbqHandle> {
i2s: I2sHandles,
producer: StreamProducer<T>,
consumer: StreamConsumer<T>,
}
impl<T: BbqHandle> Audio<T> {
const RATES: [RangeEntry<u32>; 1] = [RangeEntry::new_fixed(SAMPLE_RATE)];
/// Start audio playback. Clear anything in the FIFO, enable interrupts.
fn start_audio_out(&self) {
info!("playback starting, enabling interrupts");
self.i2s.tx.fifocfg.modify(|_, w| w.emptytx().set_bit());
self.i2s.tx.fifointenclr.write(|w| w.txlvl().set_bit());
// TX FIFO trigger threshold = <= 6 entries
self.i2s
.tx
.fifotrig
.modify(|_, w| unsafe { w.txlvl().bits(6).txlvlena().enabled() });
// TX FIFO level interrupt enable
self.i2s.tx.fifointenset.write(|w| w.txlvl().enabled());
// enable interrupts
unsafe { pac::NVIC::unmask(pac::Interrupt::FLEXCOMM7) };
green_led().on();
}
/// Start audio recording. Clear the FIFO, enable interrupts, and enable the peripheral.
fn start_audio_in(&self) {
info!("recording starting, enabling interrupts");
self.i2s.rx.fifocfg.modify(|_, w| w.emptyrx().set_bit());
self.i2s.rx.fifointenclr.write(|w| w.rxlvl().set_bit());
// RX FIFO trigger threshold = >= 2 entries
self.i2s
.rx
.fifotrig
.modify(|_, w| unsafe { w.rxlvl().bits(1).rxlvlena().enabled() }); // 1 = generate when 2 pieces of data are in fifo
// RX FIFO level interrupt enable
self.i2s.rx.fifointenset.write(|w| w.rxlvl().enabled());
// Start the peripheral
self.i2s.rx.cfg1.modify(|_, w| w.mainenable().enabled());
unsafe { pac::NVIC::unmask(pac::Interrupt::FLEXCOMM6) };
red_led().on();
}
/// Stop audio playback. Disable interrupts, but leave the peripheral
/// running. It is responsible for clocks, so is needed to clock the CODEC
/// and input I2S. Leaving it running continuously is not harmful as the tx
/// FIFO will empty and then emit 0s to the DAC.
fn stop_audio_out(&self) {
pac::NVIC::mask(pac::Interrupt::FLEXCOMM7);
info!("playback stopped");
green_led().off();
}
/// Stop audio recording. Disable the peripheral and mask any interrupts.
fn stop_audio_in(&self) {
self.i2s.rx.cfg1.modify(|_, w| w.mainenable().disabled());
pac::NVIC::mask(pac::Interrupt::FLEXCOMM6);
info!("recording stopped");
red_led().off();
}
}
/// Implement a fixed internal clock offering a single fixed audio rate, which is always valid.
impl<T: BbqHandle> ClockSource for Audio<T> { impl<T: BbqHandle> ClockSource for Audio<T> {
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed; const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
const SOF_SYNC: bool = false; const SOF_SYNC: bool = false;
@@ -161,15 +256,24 @@ impl<T: BbqHandle> ClockSource for Audio<T> {
Ok(true) Ok(true)
} }
} }
/// Implement the main audio handlers. This is a very naive implementation that
/// doesn't handle edge cases properly. For example, while altSetting=1
/// indicates the host expects to be able to emit audio data to the endpoint, it
/// does not indicate that the stream has started.
impl<T: BbqHandle, B: bus::UsbBus> AudioHandler<'_, B> for Audio<T> { impl<T: BbqHandle, B: bus::UsbBus> AudioHandler<'_, B> for Audio<T> {
fn alternate_setting_changed(&mut self, _terminal: usb_device::UsbDirection, alt_setting: u8) { /// On an alt setting change, start or stop the related audio pipeline. This
// alt setting 0 means stopped /// behaviour should be replaced by a state machine with at least an 'armed
match alt_setting { /// but no data yet' state for playback.
0 => self.stop(), fn alternate_setting_changed(&mut self, terminal: usb_device::UsbDirection, alt_setting: u8) {
1 => self.start(), match (terminal, alt_setting) {
_ => defmt::error!("unexpected alt setting {}", alt_setting), (usb_device::UsbDirection::Out, 0) => self.stop_audio_out(),
(usb_device::UsbDirection::Out, 1) => self.start_audio_out(),
(usb_device::UsbDirection::In, 0) => self.stop_audio_in(),
(usb_device::UsbDirection::In, 1) => self.start_audio_in(),
_ => error!("unexpected alt setting {} on {}", alt_setting, terminal),
} }
} }
/// Receive audio data from USB. If we can, queue it into the output queue which will be drained by the ISR.
fn audio_data_rx( fn audio_data_rx(
&mut self, &mut self,
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>, ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
@@ -177,12 +281,11 @@ impl<T: BbqHandle, B: bus::UsbBus> AudioHandler<'_, B> for Audio<T> {
// Buffer must fit one microframe's (125us @ HS, 1ms @ FS) of audio data // Buffer must fit one microframe's (125us @ HS, 1ms @ FS) of audio data
// (based on how `usbd_uac2` sets up the descriptors), calculate that // (based on how `usbd_uac2` sets up the descriptors), calculate that
// size here. // size here.
let mut buf = let mut buf = [0; (SAMPLE_RATE as usize / USB_FRAME_RATE + 1) * BYTES_PER_FRAME];
[0; (SAMPLE_RATE as usize / USB_FRAME_RATE + 1) * core::mem::size_of::<SampleType>()];
let len = match ep.read(&mut buf) { let len = match ep.read(&mut buf) {
Ok(len) => len, Ok(len) => len,
Err(_) => { Err(_) => {
defmt::error!("usb error in rx callback"); error!("usb error in rx callback");
return; return;
} }
}; };
@@ -191,21 +294,82 @@ impl<T: BbqHandle, B: bus::UsbBus> AudioHandler<'_, B> for Audio<T> {
if let Ok(mut wg) = self.producer.grant_exact(buf.len()) { if let Ok(mut wg) = self.producer.grant_exact(buf.len()) {
wg.copy_from_slice(buf); wg.copy_from_slice(buf);
wg.commit(buf.len()); wg.commit(buf.len());
PRODUCED.fetch_add(buf.len() as u32, Ordering::Relaxed); PRODUCED_OUT.fetch_add(buf.len() as u32, Ordering::Relaxed);
} else { } else {
blue_led().on(); blue_led().on();
defmt::error!("overflowed bbq, asked {}", buf.len()); error!("overflowed bbq, asked {}", buf.len());
}
}
/// Transmit audio data to USB. If we have data in the queue, write it to
/// the endpoint in appropriately sized microframes
fn audio_data_tx(
&mut self,
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::In>,
) {
const NOMINAL_SAMPLES: usize = SAMPLE_RATE as usize / USB_FRAME_RATE;
const DRAINING: i32 = ((FIFO_LENGTH * 4) / 10) as i32;
const FILLING: i32 = ((FIFO_LENGTH * 6) / 10) as i32;
// Allocate space for one additional audio frame, in case the host is
// running slower than us and we need to occasionally stuff an extra
// frame.
let mut buf = [0; (NOMINAL_SAMPLES + 1) * BYTES_PER_FRAME];
let fill = PRODUCED_IN
.load(Ordering::Acquire)
.wrapping_sub(CONSUMED_IN.load(Ordering::Acquire)) as i32
/ BYTES_PER_FRAME as i32;
// Decide how many samples to write based on fill, this is how we deal with clock slippage.
// Draining (<40%) -> 1 fewer
// Filling (>50%) -> 1 more
// Nominal -> Nominal
let samples = match fill {
f if f < DRAINING => NOMINAL_SAMPLES - 1,
f if f > FILLING => NOMINAL_SAMPLES + 1,
_ => NOMINAL_SAMPLES,
};
let mut written = 0;
let mut woff = 0usize;
while written < samples {
let Ok(rgr) = self.consumer.read() else {
// warn!("underflowed IN queue, wrote {} of {}", written, samples);
break;
};
let available_frames = rgr.len() / BYTES_PER_FRAME;
if available_frames == 0 {
break;
}
let to_copy = (samples - written).min(available_frames);
let bytes = to_copy * BYTES_PER_FRAME;
buf[woff..woff + bytes].copy_from_slice(&rgr[..bytes]);
woff += bytes;
rgr.release(bytes);
written += to_copy;
CONSUMED_IN.fetch_add(to_copy as u32, Ordering::Relaxed);
}
if let Err(e) = ep.write(&buf[..written * BYTES_PER_FRAME]) {
warn!("Error writing to IN EP: {:?}", e);
} }
} }
/// Provide rate feedback to the host, so that it doesn't over- or underflow /// Provide rate feedback to the host, so that it doesn't over- or underflow
/// our queue. This implementation is not tuned. /// our queue. This implementation is not tuned and without an I term is
/// pretty unstable, but works fine.
fn feedback(&mut self, nominal_rate: UsbIsochronousFeedback) -> Option<UsbIsochronousFeedback> { fn feedback(&mut self, nominal_rate: UsbIsochronousFeedback) -> Option<UsbIsochronousFeedback> {
let target = FIFO_LENGTH as i32 / 2; let target = FIFO_LENGTH as i32 / 2;
let fill = PRODUCED let fill = PRODUCED_OUT
.load(Ordering::Acquire) .load(Ordering::Acquire)
.wrapping_sub(CONSUMED.load(Ordering::Acquire)) as i32 .wrapping_sub(CONSUMED_OUT.load(Ordering::Acquire)) as i32
/ BYTES_PER_FRAME as i32; / BYTES_PER_FRAME as i32;
let error = fill - target; let error = fill - target;
@@ -224,7 +388,7 @@ impl<T: BbqHandle, B: bus::UsbBus> AudioHandler<'_, B> for Audio<T> {
v = v.clamp(nominal_v - (1 << 14), nominal_v + (1 << 14)); v = v.clamp(nominal_v - (1 << 14), nominal_v + (1 << 14));
// Note: this log will cause continuous underflows // Note: this log will cause continuous underflows
defmt::debug!("fill:{} err:{} fb:{=u32:x}", fill, error, v as u32); debug!("fill:{} err:{} fb:{=u32:x}", fill, error, v as u32);
Some(UsbIsochronousFeedback::new(v as u32)) Some(UsbIsochronousFeedback::new(v as u32))
} }
@@ -253,11 +417,12 @@ fn main() -> ! {
pins::Pio1_20::take().unwrap().into_i2c4_scl_pin(&mut iocon), pins::Pio1_20::take().unwrap().into_i2c4_scl_pin(&mut iocon),
pins::Pio1_21::take().unwrap().into_i2c4_sda_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 the variables do not get used
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),
pins::Pio0_19::take().unwrap().into_i2s7_ws_pin(&mut iocon), pins::Pio0_19::take().unwrap().into_i2s7_ws_pin(&mut iocon),
pins::Pio1_13::take().unwrap().into_i2s6_sda_pin(&mut iocon),
pins::Pio1_31::take().unwrap(), // MCLK pins::Pio1_31::take().unwrap(), // MCLK
); );
@@ -287,10 +452,13 @@ fn main() -> ! {
); );
let i2s_peripheral = { let i2s_peripheral = {
let fc6 = hal.flexcomm.6.release();
let fc7 = hal.flexcomm.7.release(); let fc7 = hal.flexcomm.7.release();
hw::init_i2s(fc7.0, fc7.2, &mut syscon)
hw::init_i2s(fc7.0, fc7.2, fc6.0, fc6.2, &mut syscon)
}; };
debug!("usb");
#[cfg(feature = "usbhs")] #[cfg(feature = "usbhs")]
let (usb_speed, usb_peripheral) = ( let (usb_speed, usb_peripheral) = (
UsbSpeed::High, UsbSpeed::High,
@@ -312,41 +480,33 @@ fn main() -> ! {
clocks.support_usbfs_token().unwrap(), clocks.support_usbfs_token().unwrap(),
), ),
); );
let usb_bus = UsbBus::new(usb_peripheral, usb0_vbus_pin); let usb_bus = UsbBus::new(usb_peripheral, usb0_vbus_pin);
defmt::debug!("codec init"); debug!("codec init");
wm8904::init_codec(&mut i2c_bus); wm8904::init_codec(&mut i2c_bus);
// let consumer_ref = FIFO_CONSUMER_STORE.init(consumer);
// unsafe { FIFO_CONSUMER = consumer_ref as *mut _ };
let mut audio = Audio { let mut audio = Audio {
i2s: i2s_peripheral, i2s: i2s_peripheral,
producer: QUEUE.stream_producer(), producer: QUEUE_AUDIO_OUT.stream_producer(),
running: AtomicBool::new(false), consumer: QUEUE_AUDIO_IN.stream_consumer(),
}; };
let config = UsbAudioClassConfig::new(usb_speed, FunctionCode::IoBox, &mut audio) let config = UsbAudioClassConfig::new(usb_speed, FunctionCode::IoBox, &mut audio)
.with_output_config(TerminalConfig::builder().base_id(2).build()); .with_output_config(TerminalConfig::builder().base_id(2).build())
.with_input_config(TerminalConfig::builder().base_id(4).build());
let mut uac2 = config.build(&usb_bus).unwrap(); let mut uac2 = config.build(&usb_bus).unwrap();
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x1209, 0xcc1d)) let mut usb_dev = usbd_uac2::builder(&usb_bus, UsbVidPid(USB_VID, USB_PID))
.composite_with_iads()
.strings(&[StringDescriptors::default() .strings(&[StringDescriptors::default()
.manufacturer("Generic") .manufacturer(USB_MANUFACTURER)
.product("usbd_uac2 device") .product(USB_PRODUCT)])
.serial_number("123456789")])
.unwrap() .unwrap()
.max_packet_size_0(64) .max_packet_size_0(64) // Required to be 64 on HS, allowed on FS
.unwrap() .unwrap()
.device_class(0xef)
.device_sub_class(0x02)
.device_protocol(0x01)
.build(); .build();
defmt::info!("main loop"); debug!("main loop");
loop { loop {
usb_dev.poll(&mut [&mut uac2]); usb_dev.poll(&mut [&mut uac2]);
+14 -8
View File
@@ -1,7 +1,7 @@
use cortex_m::prelude::{_embedded_hal_blocking_i2c_Write, _embedded_hal_blocking_i2c_WriteRead}; use cortex_m::prelude::{_embedded_hal_blocking_i2c_Write, _embedded_hal_blocking_i2c_WriteRead};
use defmt::warn; use defmt::warn;
use crate::{CODEC_I2C_ADDR, MCLK_FREQ, SAMPLE_RATE, SampleType}; use crate::{BYTES_PER_FRAME, CODEC_I2C_ADDR, MCLK_FREQ, SAMPLE_RATE};
// copied from NXP SDK WM8904_Init // copied from NXP SDK WM8904_Init
pub(crate) fn init_codec<T>(i2c: &mut T) pub(crate) fn init_codec<T>(i2c: &mut T)
@@ -31,26 +31,29 @@ where
} }
defmt::debug!("[codec] write seq done"); defmt::debug!("[codec] write seq done");
i2c.write(CODEC_I2C_ADDR, &[0x14, 0x00, 0x00]).ok(); // clock rates 0 i2c.write(CODEC_I2C_ADDR, &[0x14, 0x00, 0x00]).ok(); // clock rates 0
i2c.write(CODEC_I2C_ADDR, &[0x0c, 0x00, 0x00]).ok(); // power management 0 = IN PGAs disabled i2c.write(CODEC_I2C_ADDR, &[0x05, 0x00, 0x03]).ok(); // vmid control 0 = normal | enabled
i2c.write(CODEC_I2C_ADDR, &[0x0c, 0x00, 0x03]).ok(); // power management 0 = INL_ENA | INR_ENA
i2c.write(CODEC_I2C_ADDR, &[0x0e, 0x00, 0x03]).ok(); // power management 2 = HPL_PGA_ENA | HPR_PGA_ENA i2c.write(CODEC_I2C_ADDR, &[0x0e, 0x00, 0x03]).ok(); // power management 2 = HPL_PGA_ENA | HPR_PGA_ENA
i2c.write(CODEC_I2C_ADDR, &[0x0f, 0x00, 0x00]).ok(); // power management 3 = line outs disabled i2c.write(CODEC_I2C_ADDR, &[0x0f, 0x00, 0x00]).ok(); // power management 3 = line outs disabled
i2c.write(CODEC_I2C_ADDR, &[0x12, 0x00, 0x0c]).ok(); // power management 6 = DACL_ENA | DACR_ENA i2c.write(CODEC_I2C_ADDR, &[0x12, 0x00, 0x0f]).ok(); // power management 6 = DACL_ENA | DACR_ENA | ADCL_ENA | ADCR_ENA
i2c.write(CODEC_I2C_ADDR, &[0x0a, 0x00, 0x00]).ok(); // analog adc 0 = ADC_OSR128 i2c.write(CODEC_I2C_ADDR, &[0x0a, 0x00, 0x01]).ok(); // analog adc 0 = ADC_OSR128
i2c.write(CODEC_I2C_ADDR, &[0x18, 0x00, 0x50]).ok(); // audio if 0 = AIFADCR_SRC | AIFDACR_SRC i2c.write(CODEC_I2C_ADDR, &[0x18, 0x00, 0x50]).ok(); // audio if 0 = AIFADCR_SRC | AIFDACR_SRC
i2c.write(CODEC_I2C_ADDR, &[0x21, 0x00, 0x40]).ok(); // dac digital 1 = DAC_OSR128 i2c.write(CODEC_I2C_ADDR, &[0x21, 0x00, 0x40]).ok(); // dac digital 1 = DAC_OSR128
i2c.write(CODEC_I2C_ADDR, &[0x2c, 0x00, 0x05]).ok(); // analog lin 0 = 0dB (unmute) i2c.write(CODEC_I2C_ADDR, &[0x2c, 0x00, 0x05]).ok(); // analog lin 0 = 0dB (unmute)
i2c.write(CODEC_I2C_ADDR, &[0x2d, 0x00, 0x05]).ok(); // analog rin 0 = 0dB (unmute) i2c.write(CODEC_I2C_ADDR, &[0x2d, 0x00, 0x05]).ok(); // analog rin 0 = 0dB (unmute)
i2c.write(CODEC_I2C_ADDR, &[0x2e, 0x00, 1 << 4]).ok(); // analog lin 1 = single ended, inv_input = in2l
i2c.write(CODEC_I2C_ADDR, &[0x2f, 0x00, 1 << 4]).ok(); // analog rin 1 = single ended, inv_input = in2r
i2c.write(CODEC_I2C_ADDR, &[0x39, 0x00, 0x39]).ok(); // analog out1 left = vol=0dB i2c.write(CODEC_I2C_ADDR, &[0x39, 0x00, 0x39]).ok(); // analog out1 left = vol=0dB
i2c.write(CODEC_I2C_ADDR, &[0x3a, 0x00, 0x39]).ok(); // analog out1 right = vol=0dB i2c.write(CODEC_I2C_ADDR, &[0x3a, 0x00, 0x39]).ok(); // analog out1 right = vol=0dB
i2c.write(CODEC_I2C_ADDR, &[0x3b, 0x00, 0x39]).ok(); // analog out2 left = vol=0dB i2c.write(CODEC_I2C_ADDR, &[0x3b, 0x00, 0x39]).ok(); // analog out2 left = vol=0dB
i2c.write(CODEC_I2C_ADDR, &[0x3c, 0x00, 0x39]).ok(); // analog out2 right = vol=0dB i2c.write(CODEC_I2C_ADDR, &[0x3c, 0x00, 0x39]).ok(); // analog out2 right = vol=0dB
i2c.write(CODEC_I2C_ADDR, &[0x43, 0x00, 0x03]).ok(); // dc server 0 = HPOUTL_ENA | HPOUTR_ENA i2c.write(CODEC_I2C_ADDR, &[0x43, 0x00, 0x03]).ok(); // dc servo 0 = HPOUTL_ENA | HPOUTR_ENA
i2c.write(CODEC_I2C_ADDR, &[0x5a, 0x00, 0xff]).ok(); // analog hp 0 = remove all shorts etc i2c.write(CODEC_I2C_ADDR, &[0x5a, 0x00, 0xff]).ok(); // analog hp 0 = remove all shorts etc
i2c.write(CODEC_I2C_ADDR, &[0x5e, 0x00, 0xff]).ok(); // analog lineout 0 = remove all shorts etc i2c.write(CODEC_I2C_ADDR, &[0x5e, 0x00, 0xff]).ok(); // analog lineout 0 = remove all shorts etc
i2c.write(CODEC_I2C_ADDR, &[0x68, 0x00, 0x01]).ok(); // enable class w charge pump i2c.write(CODEC_I2C_ADDR, &[0x68, 0x00, 0x01]).ok(); // enable class w charge pump
i2c.write(CODEC_I2C_ADDR, &[0x62, 0x00, 0x01]).ok(); // enable charge pump i2c.write(CODEC_I2C_ADDR, &[0x62, 0x00, 0x01]).ok(); // enable charge pump
let aif_wl = match core::mem::size_of::<SampleType>() { let aif_wl = match BYTES_PER_FRAME {
4 => 0, // 16 bits per sample 4 => 0, // 16 bits per sample
8 => 3, // 32 bits per sample 8 => 3, // 32 bits per sample
_ => { _ => {
@@ -98,7 +101,7 @@ where
i2c.write(CODEC_I2C_ADDR, &[0x16, 0x00, 0x0f]).ok(); // clock rates 2 = CLK_SYS_ENA i2c.write(CODEC_I2C_ADDR, &[0x16, 0x00, 0x0f]).ok(); // clock rates 2 = CLK_SYS_ENA
// Calculate bclk_div // Calculate bclk_div
let bits_per_frame = core::mem::size_of::<SampleType>() * 8; let bits_per_frame = BYTES_PER_FRAME * 8;
let bits_per_second = bits_per_frame as u32 * SAMPLE_RATE; let bits_per_second = bits_per_frame as u32 * SAMPLE_RATE;
let bclk_div = MCLK_FREQ / bits_per_second; let bclk_div = MCLK_FREQ / bits_per_second;
i2c.write(CODEC_I2C_ADDR, &[0x1a, 0x00, bclk_div as u8]) i2c.write(CODEC_I2C_ADDR, &[0x1a, 0x00, bclk_div as u8])
@@ -106,5 +109,8 @@ where
i2c.write(CODEC_I2C_ADDR, &[0x1b, 0x00, 0x00]).ok(); // audio interface 3 = input lrclock i2c.write(CODEC_I2C_ADDR, &[0x1b, 0x00, 0x00]).ok(); // audio interface 3 = input lrclock
i2c.write(CODEC_I2C_ADDR, &[0x3d, 0x00, 0x00]).ok(); // analog out12 zc = play source = dac i2c.write(CODEC_I2C_ADDR, &[0x3d, 0x00, 0x00]).ok(); // analog out12 zc = play source = dac
i2c.write(CODEC_I2C_ADDR, &[0x1e, 0x01, 0xff]).ok(); // dac vol left = update left/right = 0dB i2c.write(CODEC_I2C_ADDR, &[0x1e, 0x01, 0xc0]).ok(); // dac vol left = update left/right = 0dB
i2c.write(CODEC_I2C_ADDR, &[0x24, 0x01, 0xc0]).ok(); // adc vol left = update left/right = 0dB
i2c.write(CODEC_I2C_ADDR, &[0x2c, 0x00, 0x05]).ok(); // pga gain = 0dB
i2c.write(CODEC_I2C_ADDR, &[0x2d, 0x00, 0x05]).ok(); // pga gain = 0dB
} }