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::{
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> {
+245 -85
View File
@@ -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]);
+14 -8
View File
@@ -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
}