lpc55s28-evk: use bbqueue for demo, it can now run at 96k

This commit is contained in:
2026-05-07 16:04:40 -07:00
parent 27c105b0df
commit 537e22a7ee
5 changed files with 586 additions and 109 deletions
+141 -1
View File
@@ -1,8 +1,22 @@
//! 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::{MCLK_FREQ, SAMPLE_RATE, pac};
use defmt::debug;
use hal::{
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
@@ -79,7 +93,7 @@ pub(crate) fn init_audio_pll() {
.xo32m_ctrl
.modify(|_, w| w.enable_system_clk_out().enable());
debug!("init pll: {}", AUDIO_PLL);
debug!("init pll0: {}", AUDIO_PLL);
pmc.pdruncfg0
.modify(|_, w| w.pden_pll0().poweredoff().pden_pll0_sscg().poweredoff());
syscon.pll0clksel.write(|w| w.sel().enum_0x1()); // clk_in
@@ -132,6 +146,61 @@ pub(crate) fn init_audio_pll() {
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
}
pub struct I2sTx {
pub i2s: pac::I2S7,
}
@@ -224,3 +293,74 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
I2sTx { i2s: regs }
}
pub struct SharedLed<T: OutputPin> {
inner: UnsafeCell<T>,
}
unsafe impl<T: OutputPin> Sync for SharedLed<T> {}
impl<T: OutputPin> SharedLed<T> {
pub fn new(inner: T) -> Self {
Self {
inner: UnsafeCell::new(inner),
}
}
pub fn on(&self) {
unsafe {
(*self.inner.get()).set_low().ok();
}
}
pub fn off(&self) {
unsafe {
(*self.inner.get()).set_high().ok();
}
}
}
impl<T: OutputPin + ToggleableOutputPin> SharedLed<T> {
pub fn toggle(&self) {
unsafe {
(*self.inner.get()).toggle().ok();
}
}
}
type RedLed = Pin<pins::Pio1_6, Gpio<Output>>;
type GreenLed = Pin<pins::Pio1_7, Gpio<Output>>;
type BlueLed = Pin<pins::Pio1_4, Gpio<Output>>;
pub static RED_LED: MaybeUninit<SharedLed<RedLed>> = MaybeUninit::uninit();
pub static GREEN_LED: MaybeUninit<SharedLed<GreenLed>> = MaybeUninit::uninit();
pub static BLUE_LED: MaybeUninit<SharedLed<BlueLed>> = MaybeUninit::uninit();
pub fn init_leds(iocon: &mut Iocon<Enabled>, gpio: &mut hal::Gpio<Enabled>) {
let red_led = SharedLed::new(
pins::Pio1_6::take()
.unwrap()
.into_gpio_pin(iocon, gpio)
.into_output_low(),
);
let green_led = SharedLed::new(
pins::Pio1_7::take()
.unwrap()
.into_gpio_pin(iocon, gpio)
.into_output_low(),
);
let blue_led = SharedLed::new(
pins::Pio1_4::take()
.unwrap()
.into_gpio_pin(iocon, gpio)
.into_output_low(),
);
unsafe {
core::ptr::write(RED_LED.as_ptr() as *mut SharedLed<RedLed>, red_led);
core::ptr::write(GREEN_LED.as_ptr() as *mut SharedLed<GreenLed>, green_led);
core::ptr::write(BLUE_LED.as_ptr() as *mut SharedLed<BlueLed>, blue_led);
}
}
pub fn red_led() -> &'static SharedLed<RedLed> {
unsafe { &*RED_LED.as_ptr() }
}
pub fn green_led() -> &'static SharedLed<GreenLed> {
unsafe { &*GREEN_LED.as_ptr() }
}
pub fn blue_led() -> &'static SharedLed<BlueLed> {
unsafe { &*BLUE_LED.as_ptr() }
}
+116 -95
View File
@@ -17,8 +17,12 @@ fn panic() -> ! {
panic_probe::hard_fault()
}
use core::ptr::null_mut;
use core::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use bbqueue::{
nicknames::Churrasco,
prod_cons::stream::{StreamConsumer, StreamProducer},
traits::bbqhdl::BbqHandle,
};
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering};
use cortex_m_rt::entry;
use defmt::debug;
use defmt_rtt as _;
@@ -29,10 +33,8 @@ use hal::{
prelude::*,
time::Hertz,
};
use heapless::spsc::{Consumer, Producer, Queue};
use lpc55_hal::{self as hal};
use lpc55_hal as hal;
use pac::interrupt;
use static_cell::StaticCell;
use usb_device::{
bus::{self},
device::{StringDescriptors, UsbDeviceBuilder, UsbVidPid},
@@ -45,7 +47,7 @@ use usbd_uac2::{
descriptors::{ChannelConfig, ClockType, FormatType1, LockDelay},
};
use crate::hw::I2sTx;
use crate::hw::{I2sTx, blue_led, green_led, red_led};
mod hw;
mod wm8904;
@@ -53,7 +55,7 @@ mod wm8904;
const CODEC_I2C_ADDR: u8 = 0b0011010;
const FIFO_LENGTH: usize = 256; // frames
const MCLK_FREQ: u32 = 12288000;
const SAMPLE_RATE: u32 = 48000; // example implementation runs okay at 48k but not 96k
const SAMPLE_RATE: u32 = 96000;
type SampleType = (i32, i32);
struct Clock {}
@@ -63,8 +65,8 @@ impl Clock {
impl UsbAudioClockImpl for Clock {
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
const SOF_SYNC: bool = false;
fn get_sample_rate(&self) -> core::result::Result<u32, usbd_uac2::UsbAudioClassError> {
Ok(Clock::RATES[0].min)
fn get_sample_rate(&self) -> u32 {
Clock::RATES[0].min
}
fn get_rates(
&self,
@@ -76,35 +78,63 @@ impl UsbAudioClockImpl for Clock {
}
}
static FIFO_CONSUMER_STORE: StaticCell<Consumer<SampleType>> = StaticCell::new();
static mut FIFO_CONSUMER: *mut Consumer<SampleType> = null_mut();
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);
#[inline]
fn try_write_one_frame<T: BbqHandle>(
cons: &mut StreamConsumer<T>,
i2s: &pac::i2s7::RegisterBlock,
) -> bool {
if let Ok(rgr) = cons.read() {
if rgr.len() >= BYTES_PER_FRAME {
let l = u32::from_le_bytes(rgr[0..4].try_into().unwrap());
let r = u32::from_le_bytes(rgr[4..8].try_into().unwrap());
i2s.fifowr.write(|w| unsafe { w.bits(l) });
i2s.fifowr.write(|w| unsafe { w.bits(r) });
// consume exactly one frame (8 bytes)
rgr.release(BYTES_PER_FRAME);
CONSUMED.fetch_add(BYTES_PER_FRAME as u32, Ordering::Relaxed);
return true;
} else {
// Not enough bytes for a full frame: leave it in the queue.
return false;
}
}
false
}
#[interrupt]
fn FLEXCOMM7() {
let i2s = unsafe { &*pac::I2S7::ptr() };
// refil the buffer to 4 frames / 8 samples
let fifo = unsafe { &mut *FIFO_CONSUMER };
// refill until fifo has >6 words
let mut cons = QUEUE.stream_consumer();
while i2s.fifostat.read().txlvl().bits() <= 6 {
if let Some((l, r)) = fifo.dequeue() {
i2s.fifowr.write(|w| unsafe { w.bits(l as u32) });
i2s.fifowr.write(|w| unsafe { w.bits(r as u32) });
} else {
// Queue underflow
defmt::error!("queue underflow");
if !try_write_one_frame(&mut cons, i2s) {
// No complete frame available: write silence to keep FIFO above threshold
defmt::error!("underflow");
red_led().toggle();
i2s.fifowr.write(|w| unsafe { w.bits(0) });
i2s.fifowr.write(|w| unsafe { w.bits(0) });
break;
}
}
}
struct Audio<'a> {
struct Audio<T: BbqHandle> {
running: AtomicBool,
i2s: I2sTx,
producer: Producer<'a, SampleType>,
producer: StreamProducer<T>,
integrator: AtomicI32,
}
impl<'a> Audio<'a> {
impl<T: BbqHandle> Audio<T> {
fn start(&self) {
self.running.store(true, Ordering::Relaxed);
defmt::info!("playback starting, enabling interrupts");
@@ -117,6 +147,7 @@ impl<'a> Audio<'a> {
// 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
@@ -125,9 +156,10 @@ impl<'a> Audio<'a> {
self.running.store(true, Ordering::Relaxed);
defmt::info!("playback stopped");
pac::NVIC::mask(pac::Interrupt::FLEXCOMM7);
green_led().off();
}
}
impl<'a, B: bus::UsbBus> UsbAudioClass<'a, B> for Audio<'_> {
impl<T: BbqHandle, B: bus::UsbBus> UsbAudioClass<'_, 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 {
@@ -138,10 +170,10 @@ impl<'a, B: bus::UsbBus> UsbAudioClass<'a, B> for Audio<'_> {
}
fn audio_data_rx(
&mut self,
ep: &usb_device::endpoint::Endpoint<'a, B, usb_device::endpoint::Out>,
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
) {
// Buffer must fit 1ms of audio data (based on how `usbd_uac2` sets up the descriptors), calculate that size here.
let mut buf = [0; SAMPLE_RATE as usize / 1000 * core::mem::size_of::<SampleType>()];
// Buffer must fit 125us of audio data (based on how `usbd_uac2` sets up the descriptors), calculate that size here.
let mut buf = [0; SAMPLE_RATE as usize / 8000 * core::mem::size_of::<SampleType>()];
let len = match ep.read(&mut buf) {
Ok(len) => len,
Err(_) => {
@@ -150,64 +182,55 @@ impl<'a, B: bus::UsbBus> UsbAudioClass<'a, B> for Audio<'_> {
}
};
let buf = &buf[..len];
// Translate the raw USB data into audio frames
for sample in buf
.chunks_exact(core::mem::size_of::<SampleType>())
.map(|b| {
// TODO: implement SampleType::from
(
i32::from_le_bytes(b[..4].try_into().unwrap()),
i32::from_le_bytes(b[4..].try_into().unwrap()),
)
})
{
if self.producer.enqueue(sample).is_err() {
defmt::error!("overflowed fifo, len: {}", self.producer.len());
}
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);
} else {
blue_led().on();
defmt::error!("overflowed bbq, asked {}", buf.len());
}
}
/// Provide rate feedback to the host, so that it doesn't over- or underflow
/// our queue.
fn feedback(&mut self) -> Option<UsbIsochronousFeedback> {
// Samples per USB interval (1ms)
const FRAME_SAMPLES: i32 = SAMPLE_RATE as i32 / 1000;
fn feedback(&mut self, nominal_rate: UsbIsochronousFeedback) -> Option<UsbIsochronousFeedback> {
let target = FIFO_LENGTH as i32 / 2 - nominal_rate.int as i32;
// Keep FIFO around half full, minus one USB packet worth
const TARGET: i32 = FIFO_LENGTH as i32 / 2 - FRAME_SAMPLES;
let fill = PRODUCED
.load(Ordering::Acquire)
.wrapping_sub(CONSUMED.load(Ordering::Acquire)) as i32
/ BYTES_PER_FRAME as i32;
// 16.16 fixed-point nominal feedback value
const NOMINAL: i32 = FRAME_SAMPLES << 16;
const MAX_ERROR: i32 = FRAME_SAMPLES / 2;
let error = fill - target;
let queuelen = self.producer.len() as i32;
// Clamp startup excursions.
let error = error.clamp(-(nominal_rate.int as i32 * 4), nominal_rate.int as i32 * 4);
let error = (queuelen - TARGET).clamp(-MAX_ERROR, MAX_ERROR);
let mut integrator = self.integrator.load(Ordering::Relaxed);
integrator += error / 32;
integrator = integrator.clamp(-1024, 1024);
self.integrator.store(integrator, Ordering::Relaxed);
// slow down accumulation of I
const I_ACCUM_DIV: i32 = 8;
let i_delta = error / I_ACCUM_DIV;
let integrator = self.integrator.fetch_add(i_delta, Ordering::Relaxed) + i_delta;
// gains
let p = error << 8;
let i = integrator;
// Integrator saturates at 1024xFRAME_SAMPLES
let i_limit = FRAME_SAMPLES << 10;
let integrator = integrator.clamp(-i_limit, i_limit);
// Gains
let p = error << 7;
let i = integrator << 3;
// Total correction in 16.16 space
let correction = -(p + i);
let nominal_v = nominal_rate.to_u32_12_13() as i32;
let v = NOMINAL + correction;
let mut v = nominal_v + correction;
// Tight clamp around nominal.
v = v.clamp(nominal_v - (1 << 14), nominal_v + (1 << 14));
defmt::debug!(
"q:{} err:{} i:{} fb:{}",
queuelen,
"fill:{} err:{} int:{} fb:{=u32:x}",
fill,
error,
integrator,
v >> 16
v
);
Some(UsbIsochronousFeedback::new(v as u32))
@@ -227,10 +250,7 @@ fn main() -> ! {
debug!("start");
let mut red_led = pins::Pio1_6::take()
.unwrap()
.into_gpio_pin(&mut iocon, &mut gpio)
.into_output_low(); // start turned off
hw::init_leds(&mut iocon, &mut gpio);
debug!("iocon");
let usb0_vbus_pin = pins::Pio0_22::take()
@@ -249,7 +269,7 @@ fn main() -> ! {
);
debug!("clocks");
// Run the system clock at 96MHz. The lpc55-hal will run it from the FRO.
// 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)
@@ -259,6 +279,8 @@ fn main() -> ! {
.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.
hw::init_audio_pll();
@@ -280,44 +302,44 @@ fn main() -> ! {
};
#[cfg(feature = "usbhs")]
let usb_peripheral = hal.usbhs.enabled_as_device(
&mut anactrl,
&mut pmc,
&mut syscon,
&mut _delay_timer,
clocks.support_usbhs_token().unwrap(),
let (usb_speed, usb_peripheral) = (
UsbSpeed::High,
hal.usbhs.enabled_as_device(
&mut anactrl,
&mut pmc,
&mut syscon,
&mut _delay_timer,
clocks.support_usbhs_token().unwrap(),
),
);
#[cfg(feature = "usbfs")]
let usb_peripheral = hal.usbfs.enabled_as_device(
&mut anactrl,
&mut pmc,
&mut syscon,
clocks.support_usbfs_token().unwrap(),
let (usb_speed, usb_peripheral) = (
UsbSpeed::Full,
hal.usbfs.enabled_as_device(
&mut anactrl,
&mut pmc,
&mut syscon,
clocks.support_usbfs_token().unwrap(),
),
);
let usb_bus = UsbBus::new(usb_peripheral, usb0_vbus_pin);
defmt::debug!("codec init");
wm8904::init_codec(&mut i2c_bus);
let queue = cortex_m::singleton!(
: Queue<SampleType, FIFO_LENGTH>
= Queue::new()
)
.unwrap();
let (producer, consumer) = queue.split();
let consumer_ref = FIFO_CONSUMER_STORE.init(consumer);
unsafe { FIFO_CONSUMER = consumer_ref as *mut _ };
// let consumer_ref = FIFO_CONSUMER_STORE.init(consumer);
// unsafe { FIFO_CONSUMER = consumer_ref as *mut _ };
let mut clock = Clock {};
let mut audio = Audio {
i2s: i2s_peripheral,
producer,
producer: QUEUE.stream_producer(),
running: AtomicBool::new(false),
integrator: AtomicI32::new(0),
};
let config = AudioClassConfig::new(UsbSpeed::High, FunctionCode::Other, &mut clock, &mut audio)
let config = AudioClassConfig::new(usb_speed, FunctionCode::Other, &mut clock, &mut audio)
.with_output_config(TerminalConfig::new(
4,
1,
@@ -353,6 +375,5 @@ fn main() -> ! {
loop {
usb_dev.poll(&mut [&mut uac2]);
red_led.set_high().ok(); // Turn off
}
}