lpc55s28-evk: add audio input
This commit is contained in:
@@ -10,6 +10,7 @@ use defmt::debug;
|
||||
use hal::{
|
||||
Enabled, Iocon, Pin,
|
||||
drivers::pins,
|
||||
peripherals::syscon::{ClockControl, ResetControl},
|
||||
traits::wg::digital::v2::{OutputPin, ToggleableOutputPin},
|
||||
typestates::pin::{gpio::direction::Output, state::Gpio},
|
||||
};
|
||||
@@ -143,18 +144,35 @@ pub(crate) fn init_audio_pll() {
|
||||
debug!("pll0 locked after {} tries", i);
|
||||
}
|
||||
|
||||
pub struct I2sTx {
|
||||
pub i2s: pac::I2S7,
|
||||
pub struct I2sHandles {
|
||||
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");
|
||||
// Enable BOTH
|
||||
syscon.reset(&mut fc7);
|
||||
syscon.enable_clock(&mut fc7);
|
||||
|
||||
unsafe {
|
||||
pac::IOCON::ptr().as_ref().unwrap().pio1_31.modify(|_, w| {
|
||||
fc7.clear_reset(syscon);
|
||||
fc7.enable_clock(syscon);
|
||||
fc6.clear_reset(syscon);
|
||||
fc6.enable_clock(syscon);
|
||||
{
|
||||
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()
|
||||
.alt1()
|
||||
.mode()
|
||||
@@ -168,57 +186,65 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
|
||||
.od()
|
||||
.normal()
|
||||
});
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fcclksel7()
|
||||
.modify(|_, w| w.sel().enum_0x5()); // MCLK
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mclkclksel
|
||||
.modify(|_, w| w.sel().enum_0x1()); // PLL0
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mclkdiv
|
||||
.modify(|_, w| w.div().bits(1).halt().run().reset().released()); // div by 2 = PLL0 fout / 2 = 12.288MHz, max for WM8904 @ 96k
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mclkio
|
||||
.modify(|_, w| w.mclkio().output());
|
||||
};
|
||||
// FC7 clock
|
||||
sc.fcclksel7().modify(|_, w| w.sel().enum_0x5()); // MCLK
|
||||
// FC6 clock
|
||||
sc.fcclksel6().modify(|_, w| w.sel().enum_0x5()); // MCLK
|
||||
// MCLK out
|
||||
sc.mclkio.modify(|_, w| w.mclkio().output());
|
||||
|
||||
// Enable clock for sysctl, it's not mapped into Peripherals
|
||||
sc.ahbclkctrlset[2].write(|w| unsafe { w.bits(1 << 15) });
|
||||
while sc.ahbclkctrl2.read().sysctl().is_disable() {}
|
||||
let sysctrl = unsafe { pac::SYSCTL::ptr().as_ref().unwrap() };
|
||||
sysctrl.sharedctrlset[0].write(|w| w.sharedscksel().flexcomm7().sharedwssel().flexcomm7()); // FC7 drives shared SCK, WS
|
||||
sysctrl.fcctrlsel[7].write(|w| {
|
||||
w.sckinsel()
|
||||
.shared_set0_i2s_signals()
|
||||
.wsinsel()
|
||||
.shared_set0_i2s_signals()
|
||||
}); // FC7 uses shared set
|
||||
sysctrl.fcctrlsel[6].write(|w| {
|
||||
w.sckinsel()
|
||||
.shared_set0_i2s_signals()
|
||||
.wsinsel()
|
||||
.shared_set0_i2s_signals()
|
||||
});
|
||||
|
||||
// for _ in 0..1000 {
|
||||
// cortex_m::asm::nop();
|
||||
// }
|
||||
}
|
||||
|
||||
// Select I2S TX function
|
||||
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
|
||||
regs.fifocfg.modify(|_, w| {
|
||||
out_regs.fifocfg.write(|w| {
|
||||
w.enabletx()
|
||||
.enabled()
|
||||
.enablerx()
|
||||
.disabled()
|
||||
.dmatx()
|
||||
.disabled()
|
||||
.txi2se0()
|
||||
.txi2se0() // transmit 0s when empty - only supported option for 32b data
|
||||
.zero()
|
||||
.emptytx() // reset the tx queue
|
||||
.set_bit()
|
||||
});
|
||||
|
||||
// Flush
|
||||
regs.fifocfg.modify(|_, w| w.emptytx().set_bit());
|
||||
|
||||
regs.cfg2
|
||||
.modify(|_, w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
|
||||
out_regs
|
||||
.cfg2
|
||||
.write(|w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
|
||||
|
||||
let bclk_div = (MCLK_FREQ / SAMPLE_RATE / 64) as u16;
|
||||
regs.div
|
||||
.modify(|_, w| unsafe { w.div().bits(bclk_div - 1) }); // Clock source is MCLK (12.288MHz) / 4 = 3MHz
|
||||
out_regs
|
||||
.div
|
||||
.write(|w| unsafe { w.div().bits(bclk_div - 1) }); // Clock source is MCLK (12.288MHz) / 4 = 3MHz
|
||||
|
||||
// Config
|
||||
regs.cfg1.modify(|_, w| unsafe {
|
||||
// TX Config
|
||||
out_regs.cfg1.write(|w| unsafe {
|
||||
w.mstslvcfg()
|
||||
.normal_master()
|
||||
.onechannel()
|
||||
@@ -233,7 +259,33 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
|
||||
.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> {
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
//! This is a minimal implementation intended to demonstrate only the minimum
|
||||
//! 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.
|
||||
//!
|
||||
//! Packets from USB are placed in a `bbqueue`. They are consumed by the I2S
|
||||
//! FIFO in the FLEXCOMM7 interrupt. This makes the implementation very sensitive
|
||||
//! to interrupt::free critical sections (which are widely used in the USB bus driver)
|
||||
//! Packets from USB are placed in a `bbqueue` (a lock free ring buffer). They
|
||||
//! are consumed by the I2S FIFO in the FLEXCOMM7 interrupt. This makes the
|
||||
//! 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_std]
|
||||
|
||||
@@ -26,9 +36,9 @@ use bbqueue::{
|
||||
prod_cons::stream::{StreamConsumer, StreamProducer},
|
||||
traits::bbqhdl::BbqHandle,
|
||||
};
|
||||
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
use cortex_m_rt::entry;
|
||||
use defmt::debug;
|
||||
use defmt::{debug, error, info, warn};
|
||||
use defmt_rtt as _;
|
||||
use hal::raw as pac;
|
||||
use hal::{
|
||||
@@ -41,18 +51,24 @@ use lpc55_hal as hal;
|
||||
use pac::interrupt;
|
||||
use usb_device::{
|
||||
bus::{self},
|
||||
device::{StringDescriptors, UsbDeviceBuilder, UsbVidPid},
|
||||
device::{StringDescriptors, UsbVidPid},
|
||||
};
|
||||
use usbd_uac2::{
|
||||
self, AudioHandler, ClockSource, RangeEntry, TerminalConfig, UsbAudioClassConfig,
|
||||
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 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 FIFO_LENGTH: usize = 256; // frames
|
||||
const MCLK_FREQ: u32 = 12288000;
|
||||
@@ -62,18 +78,25 @@ const SAMPLE_RATE: u32 = 48000;
|
||||
const USB_FRAME_RATE: usize = 8000;
|
||||
#[cfg(feature = "usbfs")]
|
||||
const USB_FRAME_RATE: usize = 1000;
|
||||
type SampleType = (i32, i32);
|
||||
|
||||
const BYTES_PER_FRAME: usize = 8;
|
||||
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]
|
||||
fn try_write_one_frame<T: BbqHandle>(
|
||||
fn try_consume_one_frame<T: BbqHandle>(
|
||||
cons: &mut StreamConsumer<T>,
|
||||
i2s: &pac::i2s7::RegisterBlock,
|
||||
) -> bool {
|
||||
@@ -87,7 +110,7 @@ fn try_write_one_frame<T: BbqHandle>(
|
||||
|
||||
// consume exactly one frame (8 bytes)
|
||||
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;
|
||||
} else {
|
||||
// Not enough bytes for a full frame: leave it in the queue.
|
||||
@@ -97,16 +120,43 @@ fn try_write_one_frame<T: BbqHandle>(
|
||||
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]
|
||||
fn FLEXCOMM7() {
|
||||
let i2s = unsafe { &*pac::I2S7::ptr() };
|
||||
let i2s = unsafe { pac::I2S7::ptr().as_ref().unwrap() };
|
||||
// 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 {
|
||||
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
|
||||
defmt::error!("underflow");
|
||||
red_led().toggle();
|
||||
// error!("tx queue underflow");
|
||||
blue_led().toggle();
|
||||
i2s.fifowr.write(|w| unsafe { w.bits(0) });
|
||||
i2s.fifowr.write(|w| unsafe { w.bits(0) });
|
||||
break;
|
||||
@@ -114,38 +164,83 @@ fn FLEXCOMM7() {
|
||||
}
|
||||
}
|
||||
|
||||
struct Audio<T: BbqHandle> {
|
||||
running: AtomicBool,
|
||||
i2s: I2sTx,
|
||||
producer: StreamProducer<T>,
|
||||
}
|
||||
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();
|
||||
/// I2S RX ISR. Produce frames until the FIFO is empty (or has only a partial frame).
|
||||
#[interrupt]
|
||||
fn FLEXCOMM6() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
|
||||
const SOF_SYNC: bool = false;
|
||||
@@ -161,15 +256,24 @@ impl<T: BbqHandle> ClockSource for Audio<T> {
|
||||
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> {
|
||||
fn alternate_setting_changed(&mut self, _terminal: usb_device::UsbDirection, alt_setting: u8) {
|
||||
// alt setting 0 means stopped
|
||||
match alt_setting {
|
||||
0 => self.stop(),
|
||||
1 => self.start(),
|
||||
_ => defmt::error!("unexpected alt setting {}", alt_setting),
|
||||
/// On an alt setting change, start or stop the related audio pipeline. This
|
||||
/// behaviour should be replaced by a state machine with at least an 'armed
|
||||
/// but no data yet' state for playback.
|
||||
fn alternate_setting_changed(&mut self, terminal: usb_device::UsbDirection, alt_setting: u8) {
|
||||
match (terminal, 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(
|
||||
&mut self,
|
||||
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
|
||||
// (based on how `usbd_uac2` sets up the descriptors), calculate that
|
||||
// size here.
|
||||
let mut buf =
|
||||
[0; (SAMPLE_RATE as usize / USB_FRAME_RATE + 1) * core::mem::size_of::<SampleType>()];
|
||||
let mut buf = [0; (SAMPLE_RATE as usize / USB_FRAME_RATE + 1) * BYTES_PER_FRAME];
|
||||
let len = match ep.read(&mut buf) {
|
||||
Ok(len) => len,
|
||||
Err(_) => {
|
||||
defmt::error!("usb error in rx callback");
|
||||
error!("usb error in rx callback");
|
||||
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()) {
|
||||
wg.copy_from_slice(buf);
|
||||
wg.commit(buf.len());
|
||||
PRODUCED.fetch_add(buf.len() as u32, Ordering::Relaxed);
|
||||
PRODUCED_OUT.fetch_add(buf.len() as u32, Ordering::Relaxed);
|
||||
} else {
|
||||
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
|
||||
/// 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> {
|
||||
let target = FIFO_LENGTH as i32 / 2;
|
||||
|
||||
let fill = PRODUCED
|
||||
let fill = PRODUCED_OUT
|
||||
.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;
|
||||
|
||||
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));
|
||||
|
||||
// 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))
|
||||
}
|
||||
@@ -253,11 +417,12 @@ 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 the variables 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),
|
||||
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
|
||||
);
|
||||
|
||||
@@ -287,10 +452,13 @@ fn main() -> ! {
|
||||
);
|
||||
|
||||
let i2s_peripheral = {
|
||||
let fc6 = hal.flexcomm.6.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")]
|
||||
let (usb_speed, usb_peripheral) = (
|
||||
UsbSpeed::High,
|
||||
@@ -312,41 +480,33 @@ fn main() -> ! {
|
||||
clocks.support_usbfs_token().unwrap(),
|
||||
),
|
||||
);
|
||||
|
||||
let usb_bus = UsbBus::new(usb_peripheral, usb0_vbus_pin);
|
||||
|
||||
defmt::debug!("codec init");
|
||||
debug!("codec init");
|
||||
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 {
|
||||
i2s: i2s_peripheral,
|
||||
producer: QUEUE.stream_producer(),
|
||||
running: AtomicBool::new(false),
|
||||
producer: QUEUE_AUDIO_OUT.stream_producer(),
|
||||
consumer: QUEUE_AUDIO_IN.stream_consumer(),
|
||||
};
|
||||
|
||||
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 usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x1209, 0xcc1d))
|
||||
.composite_with_iads()
|
||||
let mut usb_dev = usbd_uac2::builder(&usb_bus, UsbVidPid(USB_VID, USB_PID))
|
||||
.strings(&[StringDescriptors::default()
|
||||
.manufacturer("Generic")
|
||||
.product("usbd_uac2 device")
|
||||
.serial_number("123456789")])
|
||||
.manufacturer(USB_MANUFACTURER)
|
||||
.product(USB_PRODUCT)])
|
||||
.unwrap()
|
||||
.max_packet_size_0(64)
|
||||
.max_packet_size_0(64) // Required to be 64 on HS, allowed on FS
|
||||
.unwrap()
|
||||
.device_class(0xef)
|
||||
.device_sub_class(0x02)
|
||||
.device_protocol(0x01)
|
||||
.build();
|
||||
|
||||
defmt::info!("main loop");
|
||||
debug!("main loop");
|
||||
|
||||
loop {
|
||||
usb_dev.poll(&mut [&mut uac2]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use cortex_m::prelude::{_embedded_hal_blocking_i2c_Write, _embedded_hal_blocking_i2c_WriteRead};
|
||||
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
|
||||
pub(crate) fn init_codec<T>(i2c: &mut T)
|
||||
@@ -31,26 +31,29 @@ where
|
||||
}
|
||||
defmt::debug!("[codec] write seq done");
|
||||
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, &[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, &[0x0a, 0x00, 0x00]).ok(); // analog adc 0 = ADC_OSR128
|
||||
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, 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, &[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, &[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, &[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, &[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, &[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, &[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
|
||||
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
|
||||
|
||||
// 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 bclk_div = MCLK_FREQ / bits_per_second;
|
||||
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, &[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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user