workspace with shared hid shape for cli
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[target.thumbv8m.main-none-eabihf]
|
||||
rustflags = [
|
||||
"-C", "link-arg=-Tlink.x",
|
||||
"-C", "link-arg=-Tdefmt.x",
|
||||
# "-C", "debug-assertions",
|
||||
]
|
||||
|
||||
[build]
|
||||
target = "thumbv8m.main-none-eabihf"
|
||||
|
||||
[env]
|
||||
DEFMT_LOG = "off"
|
||||
Generated
+1168
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "guac"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["nodac", "hid"]
|
||||
ak4490 = []
|
||||
cs4398 = []
|
||||
nodac = []
|
||||
hid = [ "dep:usbd-hid", "dep:shared" ]
|
||||
|
||||
[dependencies]
|
||||
shared = { path="../shared", optional = true }
|
||||
atomic = "0.6.1"
|
||||
bbqueue = "0.7.0"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7.5"
|
||||
defmt = "1.0.1"
|
||||
defmt-rtt = "1.1.0"
|
||||
embedded-hal = "0.2.7"
|
||||
embedded-io = "0.7.1"
|
||||
log-to-defmt = "0.1.0"
|
||||
# Includes update to usb-device 0.3, fix for isochronous and smaller critical sections
|
||||
lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" }
|
||||
nb = "1.1.0"
|
||||
panic-halt = "1.0.0"
|
||||
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
|
||||
static_cell = "2.1.1"
|
||||
usb-device = { version = "0.3", features = ["control-buffer-256"] }
|
||||
usbd-hid = { version = "0.10.0", optional = true }
|
||||
usbd-uac2 = { version = "0.1.0", features = ["defmt"]}
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
debug = true
|
||||
codegen-units = 1
|
||||
@@ -0,0 +1,5 @@
|
||||
// Find the actual path of memory.x and add it to link search, required for building in workspace
|
||||
fn main() {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
println!("cargo:rustc-link-search={}", manifest_dir);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MEMORY
|
||||
{
|
||||
FLASH : ORIGIN = 0x00000000, LENGTH = 512K
|
||||
|
||||
/* for use with standard link.x */
|
||||
RAM : ORIGIN = 0x20000000, LENGTH = 192K
|
||||
|
||||
/* would be used with proper link.x */
|
||||
/* needs changes to r0 (initialization code) */
|
||||
/* SRAM0 : ORIGIN = 0x20000000, LENGTH = 64K */
|
||||
/* SRAM1 : ORIGIN = 0x20010000, LENGTH = 64K */
|
||||
/* SRAM2 : ORIGIN = 0x20020000, LENGTH = 64K */
|
||||
/* SRAM3 : ORIGIN = 0x20030000, LENGTH = 64K */
|
||||
|
||||
/* CASPER SRAM regions */
|
||||
/* SRAMX0: ORIGIN = 0x1400_0000, LENGTH = 4K /1* to 0x1400_0FFF *1/ */
|
||||
/* SRAMX1: ORIGIN = 0x1400_4000, LENGTH = 4K /1* to 0x1400_4FFF *1/ */
|
||||
|
||||
/* USB1 SRAM regin */
|
||||
/* USB1_SRAM : ORIGIN = 0x40100000, LENGTH = 16K */
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use cortex_m::prelude::{_embedded_hal_blocking_i2c_Write, _embedded_hal_blocking_i2c_WriteRead};
|
||||
|
||||
use crate::CodecPins;
|
||||
use crate::hal::prelude::*;
|
||||
use crate::traits::Dac;
|
||||
|
||||
const AK4490_I2C_ADDRESS: u8 = 0x10;
|
||||
|
||||
#[repr(u8)]
|
||||
#[allow(dead_code)]
|
||||
enum RegisterAddress {
|
||||
Control1 = 0x00,
|
||||
Control2 = 0x01,
|
||||
Control3 = 0x02,
|
||||
LeftAtt = 0x03,
|
||||
RightAtt = 0x04,
|
||||
Control4 = 0x05,
|
||||
Control5 = 0x06,
|
||||
Control6 = 0x07,
|
||||
Control7 = 0x08,
|
||||
Control8 = 0x09,
|
||||
}
|
||||
|
||||
pub struct Ak4490Dac<T> {
|
||||
i2c: T,
|
||||
pins: CodecPins, // this dependency is unfortunate, but non trivial to generalize
|
||||
}
|
||||
|
||||
impl<T> Ak4490Dac<T>
|
||||
where
|
||||
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
|
||||
{
|
||||
#[inline]
|
||||
fn write_reg(&mut self, reg: RegisterAddress, val: u8) {
|
||||
self.i2c.write(AK4490_I2C_ADDRESS, &[reg as u8, val]).ok();
|
||||
}
|
||||
fn dfs_for_rate(&self, rate: u32) -> u8 {
|
||||
match rate {
|
||||
r if r < 54000 => 0,
|
||||
r if r < 108000 => 1,
|
||||
r if r < 216000 => 2,
|
||||
r if r <= 384000 => 4,
|
||||
_ => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dac<T> for Ak4490Dac<T>
|
||||
where
|
||||
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
|
||||
{
|
||||
fn new(i2c: T, pins: CodecPins) -> Self {
|
||||
Self { i2c, pins }
|
||||
}
|
||||
fn init(&mut self) {
|
||||
// bring out of reset
|
||||
self.pins.reset.set_high();
|
||||
self.write_reg(RegisterAddress::Control1, (1 << 7) | 0x0e | (1 << 0)); // ACKS | I2S-32 | RSTN
|
||||
let dfs = 0; // start in 48k mode, change_rate will be called after init
|
||||
self.write_reg(RegisterAddress::Control2, 0x22 | ((dfs & 0x3) << 3));
|
||||
self.write_reg(RegisterAddress::Control4, (dfs & 0x4) >> 1);
|
||||
}
|
||||
fn change_rate(&mut self, new_rate: u32) {
|
||||
let dfs = self.dfs_for_rate(new_rate);
|
||||
self.write_reg(RegisterAddress::Control2, 0x22 | ((dfs & 0x3) << 3));
|
||||
self.write_reg(RegisterAddress::Control4, (dfs & 0x4) >> 1);
|
||||
}
|
||||
fn set_volume(&mut self, left: u8, right: u8) {
|
||||
self.write_reg(RegisterAddress::LeftAtt, left);
|
||||
self.write_reg(RegisterAddress::RightAtt, right);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use cortex_m::prelude::{_embedded_hal_blocking_i2c_Write, _embedded_hal_blocking_i2c_WriteRead};
|
||||
|
||||
use crate::CodecPins;
|
||||
use crate::hal::prelude::*;
|
||||
use crate::traits::Dac;
|
||||
|
||||
const CS4398_I2C_ADDRESS: u8 = 0x4f;
|
||||
|
||||
#[repr(u8)]
|
||||
#[allow(dead_code)]
|
||||
enum RegisterAddress {
|
||||
ChipId = 0x01,
|
||||
ModeControl = 0x02,
|
||||
VolMixInvControl = 0x03,
|
||||
MuteControl = 0x04,
|
||||
ChAVol = 0x05,
|
||||
ChBVol = 0x06,
|
||||
RampFiltControl = 0x07,
|
||||
MiscControl = 0x08,
|
||||
MiscControl2 = 0x09,
|
||||
}
|
||||
|
||||
pub struct Cs4398Dac<T> {
|
||||
i2c: T,
|
||||
pins: CodecPins,
|
||||
}
|
||||
|
||||
impl<T> Cs4398Dac<T>
|
||||
where
|
||||
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
|
||||
{
|
||||
#[inline]
|
||||
fn write_reg(&mut self, reg: RegisterAddress, val: u8) {
|
||||
self.i2c.write(CS4398_I2C_ADDRESS, &[reg as u8, val]).ok();
|
||||
}
|
||||
fn fm_for_rate(&self, rate: u32) -> u8 {
|
||||
match rate {
|
||||
r if r <= 50000 => 0b00,
|
||||
r if r <= 100000 => 0b01,
|
||||
_ => 0b10,
|
||||
// DSD mode 0b11 is not used
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dac<T> for Cs4398Dac<T>
|
||||
where
|
||||
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
|
||||
{
|
||||
fn new(i2c: T, pins: CodecPins) -> Self {
|
||||
Cs4398Dac { i2c, pins }
|
||||
}
|
||||
fn init(&mut self) {
|
||||
// reset
|
||||
self.pins.reset.set_low().ok();
|
||||
self.pins.reset.set_high().ok();
|
||||
// power up, enable control port
|
||||
self.write_reg(RegisterAddress::MiscControl, 1 << 6);
|
||||
self.mute();
|
||||
// set audio protocol to I2S, Single rate mode
|
||||
self.write_reg(RegisterAddress::ModeControl, 1 << 4);
|
||||
}
|
||||
fn change_rate(&mut self, new_rate: u32) {
|
||||
let mode_control = (1 << 4) | self.fm_for_rate(new_rate);
|
||||
self.write_reg(RegisterAddress::ModeControl, mode_control);
|
||||
}
|
||||
fn mute(&mut self) {
|
||||
self.write_reg(RegisterAddress::MuteControl, 0xc0 | (0b11 << 3));
|
||||
}
|
||||
fn unmute(&mut self) {
|
||||
self.write_reg(RegisterAddress::MuteControl, 0xc0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use crate::traits::Dac;
|
||||
|
||||
/// Noop DAC for debugging on the EVK without a DAC connected
|
||||
pub struct NoopDac<T> {
|
||||
__: PhantomData<T>,
|
||||
}
|
||||
impl<T> Dac<T> for NoopDac<T> {
|
||||
fn init(&mut self) {}
|
||||
fn change_rate(&mut self, _new_rate: u32) {}
|
||||
fn mute(&mut self) {}
|
||||
fn new(_i2c: T, _pins: crate::CodecPins) -> Self {
|
||||
Self { __: PhantomData }
|
||||
}
|
||||
fn set_volume(&mut self, _left: u8, _right: u8) {}
|
||||
fn unmute(&mut self) {}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
use hal::Syscon;
|
||||
use hal::peripherals::syscon::ClockControl;
|
||||
|
||||
use crate::{hal, pac};
|
||||
use core::cell::UnsafeCell;
|
||||
use core::convert::Infallible;
|
||||
use core::ptr::copy_nonoverlapping;
|
||||
use core::sync::atomic::{AtomicUsize, Ordering, compiler_fence};
|
||||
|
||||
pub const DMA0_FLEXCOMM7_TX: u8 = 19;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct DmaDescriptor {
|
||||
pub xfercfg: u32,
|
||||
pub src_end: *const u8,
|
||||
pub dst_end: *mut u32,
|
||||
pub next: *const DmaDescriptor,
|
||||
}
|
||||
|
||||
impl defmt::Format for DmaDescriptor {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"xfercfg={:x} src_end={:x} dst_end={:x} next={:x}",
|
||||
self.xfercfg,
|
||||
self.src_end,
|
||||
self.dst_end,
|
||||
self.next
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Channel descriptor table; linked from SRAMBASE
|
||||
#[repr(C, align(512))]
|
||||
pub struct DescriptorTable {
|
||||
pub d: [DmaDescriptor; 32],
|
||||
}
|
||||
// Our ring that we will transition to once the transfer begins
|
||||
#[repr(C)]
|
||||
pub struct RingDescriptors<const N: usize> {
|
||||
pub d: [DmaDescriptor; N],
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct PushResult {
|
||||
pub written: usize,
|
||||
pub dropped: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum ConfigError {
|
||||
SlotTooLarge,
|
||||
SlotTooSmall,
|
||||
SlotNotAligned,
|
||||
UnsupportedWidth,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DmaError {
|
||||
Underrun,
|
||||
}
|
||||
impl core::error::Error for DmaError {}
|
||||
impl core::fmt::Display for DmaError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.write_str("DmaUnderrun")
|
||||
}
|
||||
}
|
||||
|
||||
/// Slot-based DMA ring
|
||||
pub struct DmaRing<const N: usize, const MAX_SLOT_BYTES: usize> {
|
||||
dma: pac::DMA0,
|
||||
|
||||
/// Destination peripheral register (FIFO write register)
|
||||
dst_reg: *mut u32,
|
||||
|
||||
// SAFETY: only written by USB task (on start)
|
||||
pub(crate) channel_desc: UnsafeCell<DescriptorTable>,
|
||||
// SAFETY: only written by USB task (on start)
|
||||
pub(crate) desc: UnsafeCell<RingDescriptors<N>>,
|
||||
slots: UnsafeCell<[[u8; MAX_SLOT_BYTES]; N]>,
|
||||
|
||||
/// Effective bytes per slot. Maybe be smaller than MAX_SLOT_BYTES (e.g. at lower sample rates), as the setup is designed for constant rate not constant size.
|
||||
slot_bytes: usize,
|
||||
/// How many bytes to transfer to the FIFO
|
||||
word_bytes: usize,
|
||||
|
||||
// SAFETY: producer only
|
||||
write_slot: UnsafeCell<usize>,
|
||||
write_off: UnsafeCell<usize>,
|
||||
|
||||
produced: AtomicUsize,
|
||||
consumed: AtomicUsize,
|
||||
|
||||
/// Leave at least one slot empty so producer never overwrites a slot DMA may still read.
|
||||
safety_gap: usize,
|
||||
pub produced_bytes: AtomicUsize,
|
||||
pub consumed_bytes: AtomicUsize,
|
||||
}
|
||||
|
||||
impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
/// Construct using PAC DMA0 + &mut SYSCON + a destination FIFO register.
|
||||
pub fn new(
|
||||
dma: pac::DMA0,
|
||||
syscon: &mut Syscon,
|
||||
dst_reg: *mut u32,
|
||||
word_bytes: usize,
|
||||
) -> Result<Self, ConfigError> {
|
||||
if word_bytes != 1 && word_bytes != 2 && word_bytes != 4 {
|
||||
return Err(ConfigError::UnsupportedWidth);
|
||||
}
|
||||
// Start the DMA0 clock
|
||||
dma.enable_clock(syscon);
|
||||
|
||||
Ok(Self {
|
||||
dma,
|
||||
dst_reg: dst_reg,
|
||||
channel_desc: UnsafeCell::new(DescriptorTable {
|
||||
d: [DmaDescriptor {
|
||||
xfercfg: 0,
|
||||
src_end: core::ptr::null(),
|
||||
dst_end: core::ptr::null_mut(),
|
||||
next: core::ptr::null(),
|
||||
}; 32],
|
||||
}),
|
||||
desc: UnsafeCell::new(RingDescriptors {
|
||||
d: [DmaDescriptor {
|
||||
xfercfg: 0,
|
||||
src_end: core::ptr::null(),
|
||||
dst_end: core::ptr::null_mut(),
|
||||
next: core::ptr::null(),
|
||||
}; N],
|
||||
}),
|
||||
slots: UnsafeCell::new([[0u8; MAX_SLOT_BYTES]; N]),
|
||||
slot_bytes: MAX_SLOT_BYTES,
|
||||
word_bytes,
|
||||
write_slot: UnsafeCell::new(0),
|
||||
write_off: UnsafeCell::new(0),
|
||||
produced: AtomicUsize::new(0),
|
||||
consumed: AtomicUsize::new(0),
|
||||
safety_gap: 1,
|
||||
produced_bytes: AtomicUsize::new(0),
|
||||
consumed_bytes: AtomicUsize::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Optional: adjust safety gap (defaults to 1 empty slot).
|
||||
pub fn set_safety_gap(&mut self, gap_slots: usize) {
|
||||
self.safety_gap = gap_slots.min(N);
|
||||
}
|
||||
pub fn slot_size(&self) -> usize {
|
||||
self.slot_bytes
|
||||
}
|
||||
pub fn set_slot_size(&mut self, slot_bytes: usize) -> Result<(), ConfigError> {
|
||||
if slot_bytes == 0 {
|
||||
return Err(ConfigError::SlotTooSmall);
|
||||
}
|
||||
if slot_bytes > MAX_SLOT_BYTES {
|
||||
return Err(ConfigError::SlotTooLarge);
|
||||
}
|
||||
if slot_bytes % self.word_bytes != 0 {
|
||||
return Err(ConfigError::SlotNotAligned);
|
||||
}
|
||||
self.slot_bytes = slot_bytes;
|
||||
self.reset_producer();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Producer: copy into ring; commits whole slots; reports overflow by returning dropped bytes.
|
||||
pub fn push(&self, mut data: &[u8]) -> PushResult {
|
||||
let mut written = 0usize;
|
||||
|
||||
let write_slot = unsafe { &mut *self.write_slot.get() };
|
||||
let write_off = unsafe { &mut *self.write_off.get() };
|
||||
|
||||
let slots = unsafe { &mut *self.slots.get() };
|
||||
defmt::debug!(
|
||||
"produced={} consumed={} fill={}",
|
||||
self.produced(),
|
||||
self.consumed(),
|
||||
self.fill_slots()
|
||||
);
|
||||
while !data.is_empty() {
|
||||
if self.is_full_for_producer() {
|
||||
break;
|
||||
}
|
||||
|
||||
let cap = self.slot_bytes - *write_off;
|
||||
let n = core::cmp::min(cap, data.len());
|
||||
|
||||
unsafe {
|
||||
let dst = slots[*write_slot].as_mut_ptr().add(*write_off);
|
||||
copy_nonoverlapping(data.as_ptr(), dst, n);
|
||||
}
|
||||
|
||||
*write_off += n;
|
||||
written += n;
|
||||
data = &data[n..];
|
||||
|
||||
if *write_off == self.slot_bytes {
|
||||
// publish completed slot
|
||||
compiler_fence(Ordering::Release);
|
||||
self.produced.fetch_add(1, Ordering::Release);
|
||||
|
||||
*write_slot = (*write_slot + 1) % N;
|
||||
*write_off = 0;
|
||||
}
|
||||
}
|
||||
|
||||
self.produced_bytes.fetch_add(written, Ordering::Release);
|
||||
|
||||
PushResult {
|
||||
written,
|
||||
dropped: data.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Call from DMA IRQ bookkeeping when a slot has been consumed.
|
||||
pub fn advance_consumed(&self, slots: usize) -> Result<(), DmaError> {
|
||||
let produced = self.produced.load(Ordering::Acquire);
|
||||
let consumed = self.consumed.load(Ordering::Relaxed);
|
||||
if consumed < produced {
|
||||
self.consumed.fetch_add(slots, Ordering::Release);
|
||||
self.consumed_bytes
|
||||
.fetch_add(slots * self.slot_bytes, Ordering::Relaxed);
|
||||
Ok(())
|
||||
} else {
|
||||
defmt::error!("DMA underrun!");
|
||||
Err(DmaError::Underrun)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn produced(&self) -> usize {
|
||||
self.produced.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn produced_bytes(&self) -> usize {
|
||||
self.produced_bytes.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn consumed(&self) -> usize {
|
||||
self.consumed.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn consumed_bytes(&self) -> usize {
|
||||
loop {
|
||||
let consumed_start = self.consumed.load(Ordering::Acquire);
|
||||
|
||||
let reg_1 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
||||
let reg_2 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
||||
|
||||
let consumed_end = self.consumed.load(Ordering::Acquire);
|
||||
|
||||
if consumed_start == consumed_end && reg_1 == reg_2 {
|
||||
// 1. Map the hardware remaining countdown into a clean byte count
|
||||
let remaining_bytes = if reg_1 == 0x3ff {
|
||||
0 // 0x3FF means all transfers completed, 0 bytes remaining
|
||||
} else {
|
||||
// Formula from NXP manual: (XFERCOUNT + 1) * Data Width
|
||||
(reg_1 + 1) * self.word_bytes
|
||||
};
|
||||
|
||||
// 2. Total bytes consumed in this specific active slot
|
||||
let active_slot_consumed = self.slot_bytes - remaining_bytes;
|
||||
|
||||
// 3. Combine with your software index history accumulator
|
||||
return consumed_start * self.slot_bytes + active_slot_consumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_slots(&self) -> usize {
|
||||
self.produced().wrapping_sub(self.consumed())
|
||||
}
|
||||
|
||||
pub fn init(&self) {
|
||||
self.init_descriptors();
|
||||
|
||||
// Descriptor table base
|
||||
let desc = unsafe { &*self.desc.get() };
|
||||
let base = self.channel_desc.get() as u32;
|
||||
self.dma.srambase.write(|w| unsafe { w.bits(base) });
|
||||
self.dma
|
||||
.channel19
|
||||
.cfg
|
||||
.write(|w| w.periphreqen().enabled().hwtrigen().disabled());
|
||||
self.dma
|
||||
.channel19
|
||||
.xfercfg
|
||||
.write(|w| unsafe { w.bits(desc.d[0].xfercfg) });
|
||||
|
||||
self.dma.enableclr0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
self.dma.ctrl.write(|w| w.enable().enabled());
|
||||
self.dma.setvalid0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
self.dma.intenset0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
|
||||
self.dma.settrig0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
}
|
||||
|
||||
pub fn run(&self) {
|
||||
self.dma.enableset0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
self.dma.enableclr0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
nb::block!(if (self.dma.busy0.read().bits() & 1 << 19) == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(nb::Error::<Infallible>::WouldBlock)
|
||||
});
|
||||
self.dma.abort0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
self.reset_producer();
|
||||
}
|
||||
|
||||
fn reset_producer(&self) {
|
||||
unsafe {
|
||||
*(&mut *self.write_slot.get()) = 0;
|
||||
*(&mut *self.write_off.get()) = 0;
|
||||
}
|
||||
self.produced.store(0, Ordering::Relaxed);
|
||||
self.produced_bytes.store(0, Ordering::Relaxed);
|
||||
self.consumed.store(0, Ordering::Relaxed);
|
||||
self.consumed_bytes.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn is_full_for_producer(&self) -> bool {
|
||||
let fill = self.fill_slots();
|
||||
fill >= N.wrapping_sub(self.safety_gap)
|
||||
}
|
||||
fn reset_producer_init_only(&self) {
|
||||
unsafe {
|
||||
*self.write_slot.get() = 0;
|
||||
}
|
||||
unsafe {
|
||||
*self.write_off.get() = 0;
|
||||
}
|
||||
|
||||
self.produced.store(0, Ordering::Relaxed);
|
||||
self.consumed.store(0, Ordering::Relaxed);
|
||||
|
||||
self.produced_bytes.store(0, Ordering::Relaxed);
|
||||
self.consumed_bytes.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn init_descriptors(&self) {
|
||||
let slots = unsafe { &mut *self.slots.get() };
|
||||
let desc = unsafe { &mut *self.desc.get() };
|
||||
let chan_desc = unsafe { &mut *self.channel_desc.get() };
|
||||
defmt::debug!("slots base: &{:x}", self.slots.get());
|
||||
|
||||
// Pre-fill with silence so underrun replays silence.
|
||||
for i in 0..N {
|
||||
slots[i][..self.slot_bytes].fill(0);
|
||||
}
|
||||
|
||||
let transfers = (self.slot_bytes / self.word_bytes) as u32;
|
||||
|
||||
for i in 0..N {
|
||||
let src_start = slots[i].as_ptr() as usize;
|
||||
let src_end = (src_start + self.slot_bytes - self.word_bytes) as *const u8;
|
||||
|
||||
let next = &desc.d[(i + 1) % N] as *const DmaDescriptor;
|
||||
|
||||
desc.d[i] = DmaDescriptor {
|
||||
xfercfg: encode_xfercfg(
|
||||
true, // valid
|
||||
true, // reload
|
||||
false, // swtrig (we use XFERCFG SWTRIG kick)
|
||||
false, // clrtrig
|
||||
true, // intA
|
||||
false, // intB
|
||||
self.word_bytes as u32,
|
||||
1, // src_inc
|
||||
0, // dst_inc
|
||||
transfers,
|
||||
),
|
||||
src_end,
|
||||
dst_end: self.dst_reg,
|
||||
next,
|
||||
};
|
||||
}
|
||||
chan_desc.d[19] = desc.d[0];
|
||||
chan_desc.d[19].xfercfg = 0;
|
||||
|
||||
// reset producer indices + counters (init-only action)
|
||||
self.reset_producer_init_only();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<const N: usize, const MAX_SLOT_BYTES: usize> Sync for DmaRing<N, MAX_SLOT_BYTES> {}
|
||||
|
||||
/// XFERCFG encoding follows the common LPC DMA layout:
|
||||
/// - SETINTA at bit4, SETINTB at bit5
|
||||
/// - WIDTH at bits 9:8
|
||||
/// - SRCINC at bits 13:12
|
||||
/// - DSTINC at bits 15:14
|
||||
/// - XFERCOUNT at bits 25:16
|
||||
/// This layout is shown in LPC DMA examples. [5](https://www.kernel.org/doc/html/latest/core-api/dma-api-howto.html)
|
||||
fn encode_xfercfg(
|
||||
cfgvalid: bool,
|
||||
reload: bool,
|
||||
swtrig: bool,
|
||||
clrtrig: bool,
|
||||
inta: bool,
|
||||
intb: bool,
|
||||
width_bytes: u32,
|
||||
src_inc: u32,
|
||||
dst_inc: u32,
|
||||
transfers: u32,
|
||||
) -> u32 {
|
||||
let width_code = match width_bytes {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
4 => 2,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let count_field = transfers.saturating_sub(1) & 0x3FF;
|
||||
|
||||
((cfgvalid as u32) << 0)
|
||||
| ((reload as u32) << 1)
|
||||
| ((swtrig as u32) << 2)
|
||||
| ((clrtrig as u32) << 3)
|
||||
| ((inta as u32) << 4)
|
||||
| ((intb as u32) << 5)
|
||||
| ((width_code & 0x3) << 8)
|
||||
| ((src_inc & 0x3) << 12)
|
||||
| ((dst_inc & 0x3) << 14)
|
||||
| (count_field << 16)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use usbd_hid::descriptor::generator_prelude::*;
|
||||
|
||||
#[gen_hid_descriptor(
|
||||
(collection = APPLICATION, usage_page = VENDOR_DEFINED_START, usage = 0x01, ) = {
|
||||
average_buffer_fill=input;
|
||||
frame_count=input;
|
||||
dac_underflow_count=input;
|
||||
usb_underflow_count=input;
|
||||
dac_overflow_count=input;
|
||||
}
|
||||
)]
|
||||
#[repr(C)]
|
||||
// Note these are all actually u32
|
||||
pub struct AudioTelemetryReport {
|
||||
pub average_buffer_fill: i32,
|
||||
pub frame_count: i32,
|
||||
pub dac_underflow_count: i32,
|
||||
pub usb_underflow_count: i32,
|
||||
pub dac_overflow_count: i32,
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use crate::pac;
|
||||
use defmt::debug;
|
||||
|
||||
pub(crate) struct PllConstants {
|
||||
pub m: u16, // 1-65535
|
||||
pub n: u8, // 1-255
|
||||
pub p: u8, // 1-31
|
||||
pub selp: u8, // 5 bits
|
||||
pub seli: u8, // 6 bits
|
||||
}
|
||||
|
||||
impl PllConstants {
|
||||
pub(crate) const fn new(n: u8, m: u16, p: u8) -> Self {
|
||||
assert!(n != 0, "1 <= N <= 255");
|
||||
assert!(m != 0, "1 <= M <= 65535");
|
||||
assert!(p != 0 && p <= 31, "1 <= P <= 31");
|
||||
|
||||
// Following ripped from lpc55-hal and made const
|
||||
// UM 4.6.6.3.2
|
||||
let selp = {
|
||||
let v = (m >> 2) + 1;
|
||||
if v < 31 { v } else { 31 }
|
||||
} as u8;
|
||||
|
||||
let seli = {
|
||||
let v = match m {
|
||||
m if m >= 8000 => 1,
|
||||
m if m >= 122 => 8000 / m,
|
||||
_ => 2 * (m >> 2) + 3,
|
||||
};
|
||||
|
||||
if v < 63 { v } else { 63 }
|
||||
} as u8;
|
||||
// let seli = min(2*(m >> 2) + 3, 63);
|
||||
Self {
|
||||
n,
|
||||
m,
|
||||
p,
|
||||
selp,
|
||||
seli,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl defmt::Format for PllConstants {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
let factor = f32::from(self.m) / (f32::from(self.n) * 2.0 * f32::from(self.p));
|
||||
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"m: {} n: {} p: {} selp: {} seli: {} fout: fin * {}",
|
||||
self.m,
|
||||
self.n,
|
||||
self.p,
|
||||
self.selp,
|
||||
self.seli,
|
||||
factor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
extern crate panic_probe;
|
||||
#[defmt::panic_handler]
|
||||
fn panic() -> ! {
|
||||
panic_probe::hard_fault()
|
||||
}
|
||||
|
||||
use atomic::Atomic;
|
||||
use bytemuck::NoUninit;
|
||||
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
||||
use cortex_m_rt::entry;
|
||||
use defmt;
|
||||
use defmt::debug;
|
||||
use defmt_rtt as _;
|
||||
use hal::Pin;
|
||||
use hal::Syscon;
|
||||
use hal::drivers::{Timer, UsbBus, pins, pins::direction::Output};
|
||||
use hal::prelude::*;
|
||||
use hal::raw as pac;
|
||||
use hal::time::{Hertz, Microseconds};
|
||||
use hal::typestates::pin::state::Gpio;
|
||||
use lpc55_hal as hal;
|
||||
use pac::interrupt;
|
||||
use static_cell::StaticCell;
|
||||
use usb_device::{
|
||||
bus::{self},
|
||||
device::{StringDescriptors, UsbVidPid},
|
||||
};
|
||||
#[cfg(feature = "hid")]
|
||||
use usbd_hid::{descriptor::SerializedDescriptor, hid_class::HIDClass};
|
||||
use usbd_uac2::{
|
||||
self, AudioHandler, ClockSource, RangeEntry, TerminalConfig, UsbAudioClassConfig,
|
||||
UsbAudioClassError, UsbIsochronousFeedback, UsbSpeed,
|
||||
constants::{FunctionCode, TerminalType},
|
||||
descriptors::ClockType,
|
||||
};
|
||||
|
||||
use crate::dac::DacImpl;
|
||||
use crate::dma::DmaRing;
|
||||
use crate::traits::Dac;
|
||||
use shared::hid::AudioTelemetryReport;
|
||||
|
||||
#[cfg(feature = "ak4490")]
|
||||
pub mod dac {
|
||||
mod ak4490;
|
||||
pub use self::ak4490::Ak4490Dac as DacImpl;
|
||||
}
|
||||
#[cfg(feature = "cs4398")]
|
||||
pub mod dac {
|
||||
mod cs4398;
|
||||
pub use self::cs4398::Cs4398Dac as DacImpl;
|
||||
}
|
||||
#[cfg(feature = "nodac")]
|
||||
pub mod dac {
|
||||
mod noop;
|
||||
pub use self::noop::NoopDac as DacImpl;
|
||||
}
|
||||
|
||||
mod dma;
|
||||
#[cfg(feature = "hid")]
|
||||
mod hid;
|
||||
mod hw;
|
||||
mod traits;
|
||||
|
||||
const BYTES_PER_SAMPLE: usize = 4; // 32 bit samples
|
||||
const BYTES_PER_FRAME: usize = BYTES_PER_SAMPLE * 2; // 2 channels
|
||||
const FRAMES_PER_SLOT: usize = SAMPLE_RATE as usize / 2000; // run the DMA at 2khz
|
||||
const BYTES_PER_SLOT: usize = FRAMES_PER_SLOT * BYTES_PER_FRAME;
|
||||
const N_SLOTS: usize = 8;
|
||||
const FILL_TARGET_BYTES: i32 = (BYTES_PER_SLOT * N_SLOTS) as i32 / 2;
|
||||
const USB_FRAME_RATE: u32 = 8000; // microframe rate: 8000 for HS, 1000 for FS
|
||||
|
||||
// In frames
|
||||
const QUEUE_RUNNING_UP: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 4) / 10; // 40%
|
||||
const QUEUE_RUNNING_DOWN: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 2) / 10; // 20%
|
||||
const NODATA_TIMEOUT_FRAMES: usize = SAMPLE_RATE as usize / 100; // ~100ms
|
||||
const MCLK_FREQ: u32 = 24576000;
|
||||
const SAMPLE_RATE: u32 = 192000;
|
||||
const HID_INTERVAL_MS: u8 = 100;
|
||||
|
||||
struct CodecPins {
|
||||
reset: Pin<pins::Pio0_3, Gpio<Output>>,
|
||||
}
|
||||
|
||||
struct ClockSelPins {
|
||||
sel_24m: Pin<pins::Pio0_27, Gpio<Output>>,
|
||||
sel_22m: Pin<pins::Pio0_31, Gpio<Output>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PerfCounters {
|
||||
received_frames: AtomicUsize,
|
||||
played_frames: AtomicUsize,
|
||||
min_fill: AtomicUsize,
|
||||
avg_fill: AtomicUsize,
|
||||
queue_underflows: AtomicUsize,
|
||||
queue_overflows: AtomicUsize,
|
||||
audio_underflows: AtomicUsize,
|
||||
}
|
||||
|
||||
impl PerfCounters {
|
||||
fn reset(&self) {
|
||||
self.received_frames.store(0, Ordering::Relaxed);
|
||||
self.played_frames.store(0, Ordering::Relaxed);
|
||||
self.min_fill
|
||||
.store(N_SLOTS * BYTES_PER_SLOT, Ordering::Relaxed);
|
||||
self.avg_fill
|
||||
.store(FILL_TARGET_BYTES as usize, Ordering::Relaxed);
|
||||
self.queue_underflows.store(0, Ordering::Relaxed);
|
||||
self.queue_overflows.store(0, Ordering::Relaxed);
|
||||
self.audio_underflows.store(0, Ordering::Relaxed);
|
||||
}
|
||||
fn build_report(&self) -> AudioTelemetryReport {
|
||||
AudioTelemetryReport {
|
||||
average_buffer_fill: self.avg_fill.load(Ordering::Relaxed) as i32,
|
||||
frame_count: self.played_frames.load(Ordering::Relaxed) as i32,
|
||||
dac_underflow_count: self.audio_underflows.load(Ordering::Relaxed) as i32,
|
||||
usb_underflow_count: self.queue_underflows.load(Ordering::Relaxed) as i32,
|
||||
dac_overflow_count: self.queue_overflows.load(Ordering::Relaxed) as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl defmt::Format for PerfCounters {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"frames: {}/{} min_fill: {} avg fill: {} a_underflows: {} q_underflows: {} q_overflows: {}",
|
||||
self.played_frames.load(Ordering::Relaxed),
|
||||
self.received_frames.load(Ordering::Relaxed),
|
||||
self.min_fill.load(Ordering::Relaxed),
|
||||
self.avg_fill.load(Ordering::Relaxed),
|
||||
self.audio_underflows.load(Ordering::Relaxed),
|
||||
self.queue_underflows.load(Ordering::Relaxed),
|
||||
self.queue_overflows.load(Ordering::Relaxed)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static PERF: PerfCounters = PerfCounters {
|
||||
received_frames: AtomicUsize::new(0), // received from USB
|
||||
played_frames: AtomicUsize::new(0), // played audio frames
|
||||
min_fill: AtomicUsize::new(0), // not recording this for now, need to figure out how to make it meaningful, since the queue starts empty
|
||||
avg_fill: AtomicUsize::new(FILL_TARGET_BYTES as usize),
|
||||
queue_underflows: AtomicUsize::new(0), // ditto here, since we underflow at startup, but we record this one as it can be trended
|
||||
queue_overflows: AtomicUsize::new(0),
|
||||
audio_underflows: AtomicUsize::new(0),
|
||||
};
|
||||
|
||||
static DMA_RING: StaticCell<DmaRing<N_SLOTS, BYTES_PER_SLOT>> = StaticCell::new();
|
||||
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, BYTES_PER_SLOT>> = None;
|
||||
#[inline]
|
||||
fn dma_ring() -> &'static DmaRing<N_SLOTS, BYTES_PER_SLOT> {
|
||||
unsafe { DMA_RING_REF.unwrap() }
|
||||
}
|
||||
|
||||
fn cur_fill() -> usize {
|
||||
let produced_bytes = dma_ring().produced_bytes() as u32;
|
||||
let consumed_bytes = dma_ring().consumed_bytes() as u32;
|
||||
|
||||
// Handle rollover properly
|
||||
produced_bytes.wrapping_sub(consumed_bytes) as usize
|
||||
}
|
||||
|
||||
#[interrupt]
|
||||
fn DMA0() {
|
||||
defmt::debug!("dma0");
|
||||
let dma = unsafe { &*pac::DMA0::ptr() };
|
||||
|
||||
let inta = dma.inta0.read().bits();
|
||||
let err = dma.errint0.read().bits();
|
||||
|
||||
// TODO: figure out how to track underflows properly
|
||||
if (err & (1 << 19)) != 0 {
|
||||
let live = dma.channel19.xfercfg.read().bits();
|
||||
|
||||
let desc = unsafe { &*dma_ring().channel_desc.get() };
|
||||
let mem = desc.d[19];
|
||||
defmt::error!(
|
||||
"DMA error ch19: live={=u32:08x} INTA={=u32:x} ERR={=u32:x}\n desc: {}",
|
||||
live,
|
||||
inta,
|
||||
err,
|
||||
mem
|
||||
);
|
||||
// red_led().on();
|
||||
dma.errint0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
}
|
||||
|
||||
if (inta & (1 << 19)) != 0 {
|
||||
dma.inta0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
if dma_ring().advance_consumed(1).is_err() {
|
||||
// red_led().on();
|
||||
} else {
|
||||
PERF.played_frames
|
||||
.fetch_add(FRAMES_PER_SLOT, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, NoUninit, Eq, PartialEq)]
|
||||
enum AudioState {
|
||||
/// Knowingly stopped, ie. AltSetting=0. DAC muted, I2S disabled.
|
||||
///
|
||||
/// AltSetting = 1 -> ARMED
|
||||
Stopped,
|
||||
/// Waiting for data. DAC muted, I2S running sending 0s (FIFO not serviced).
|
||||
///
|
||||
/// USB OUT data packet -> ARMED
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
Armed,
|
||||
/// Filling the buffer before playback starts. Feedback does not run,
|
||||
/// playout does not start draining the queue. Gets us better feedback
|
||||
/// behaviour and a full buffer without a feedback rate spike at startup.
|
||||
///
|
||||
/// queue reaches <QUEUE_RUNNING_UP> -> RUNNING
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
///
|
||||
Prefill,
|
||||
/// Normal running state. Start servicing FIFO and begin playing out from the buffer.
|
||||
///
|
||||
/// queue reaches <QUEUE_RUNNING_DOWN> -> DRAINING
|
||||
/// AltSetting = 0 -> DRAINING
|
||||
Running,
|
||||
/// The queue is low. We will continue playout.
|
||||
///
|
||||
/// queue is empty && altSetting 1 -> NODATA
|
||||
/// queue is empty && altSetting 0 -> STOPPED
|
||||
/// queue reaches <QUEUE_RUNNING_UP> && altSetting 1 -> RUNNING
|
||||
LowData,
|
||||
/// There is no data in the queue. We will count underflows for a while, send 0s, and hope the host comes back, but maybe playback is done, which we should notice and shut down.
|
||||
///
|
||||
/// countdown reaches DATA_TIMEOUT -> STOPPED
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
NoData,
|
||||
}
|
||||
impl defmt::Format for AudioState {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Stopped => "Stopped",
|
||||
Self::Armed => "Armed",
|
||||
Self::Prefill => "Prefill",
|
||||
Self::Running => "Running",
|
||||
Self::LowData => "Draining",
|
||||
Self::NoData => "NoData",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedbackState {
|
||||
correction_enabled: AtomicBool,
|
||||
integrator: AtomicI32,
|
||||
filtered_fill: AtomicI32,
|
||||
}
|
||||
impl FeedbackState {
|
||||
fn start(&mut self) {
|
||||
self.correction_enabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.correction_enabled.store(false, Ordering::Relaxed);
|
||||
self.integrator.store(0, Ordering::Relaxed);
|
||||
self.filtered_fill
|
||||
.store(FILL_TARGET_BYTES, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
impl Default for FeedbackState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
correction_enabled: AtomicBool::new(false),
|
||||
integrator: AtomicI32::new(0),
|
||||
filtered_fill: AtomicI32::new(FILL_TARGET_BYTES),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Audio<'a, D: Dac<I>, I> {
|
||||
state: Atomic<AudioState>,
|
||||
alt_setting: u8,
|
||||
i2s: I2sTx,
|
||||
dac: D,
|
||||
dma: &'a DmaRing<N_SLOTS, BYTES_PER_SLOT>,
|
||||
fb: FeedbackState,
|
||||
nodata_timeout_frame: AtomicUsize,
|
||||
cur_rate: u32,
|
||||
clock_pins: ClockSelPins,
|
||||
_marker: core::marker::PhantomData<I>,
|
||||
}
|
||||
impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
const RATES: [RangeEntry<u32>; 1] = [RangeEntry::new_fixed(SAMPLE_RATE)];
|
||||
/// Perform a state transition to `state`
|
||||
fn transition(&mut self, state: AudioState) {
|
||||
defmt::info!(
|
||||
"AudioState {} -> {}",
|
||||
self.state.load(Ordering::Relaxed),
|
||||
state
|
||||
);
|
||||
match state {
|
||||
AudioState::Stopped => self.stop(),
|
||||
AudioState::Armed => self.arm(),
|
||||
AudioState::Prefill => self.prefill(),
|
||||
AudioState::Running => self.run(),
|
||||
AudioState::LowData => {}
|
||||
AudioState::NoData => self.nodata(),
|
||||
}
|
||||
self.state.store(state, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
let regs = &self.i2s.i2s;
|
||||
// Enable TX FIFO only
|
||||
regs.fifocfg.modify(|_, w| {
|
||||
w.enabletx()
|
||||
.enabled()
|
||||
.enablerx()
|
||||
.disabled()
|
||||
.dmatx()
|
||||
.disabled()
|
||||
.txi2se0()
|
||||
.zero()
|
||||
});
|
||||
|
||||
// Flush
|
||||
regs.fifocfg.modify(|_, w| w.emptytx().set_bit());
|
||||
|
||||
regs.cfg2
|
||||
.modify(|_, 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
|
||||
|
||||
// Config
|
||||
regs.cfg1.modify(|_, w| unsafe {
|
||||
w.mstslvcfg()
|
||||
.normal_master()
|
||||
.onechannel()
|
||||
.dual_channel()
|
||||
.datalen()
|
||||
.bits(31)
|
||||
.mainenable()
|
||||
.disabled()
|
||||
.mode()
|
||||
.classic_mode()
|
||||
.datapause()
|
||||
.normal()
|
||||
});
|
||||
self.dac.init();
|
||||
}
|
||||
///Transition -> Stopped:
|
||||
///clear queue, mute DAC, mask I2S ISR, stop I2S peripheral, disable & reset feedback and performance queues
|
||||
fn stop(&mut self) {
|
||||
dma_ring().stop();
|
||||
pac::NVIC::mask(pac::Interrupt::DMA0);
|
||||
self.dac.mute();
|
||||
// Clear any samples in the FIFO
|
||||
self.i2s.i2s.fifocfg.modify(|_, w| w.emptytx().set_bit());
|
||||
// Disable I2S
|
||||
self.i2s.i2s.cfg1.modify(|_, w| w.mainenable().disabled());
|
||||
// Reset feedback state
|
||||
self.fb.reset();
|
||||
// reset performance counters
|
||||
PERF.reset();
|
||||
// Stop the clocks
|
||||
self.clock_pins.sel_22m.set_low().ok();
|
||||
self.clock_pins.sel_24m.set_low().ok();
|
||||
}
|
||||
///Transition -> Armed
|
||||
/// Start I2S peripheral and MCLK. Since we assume we have interrupts disabled at
|
||||
/// this point (as we came from Stopped), and the FIFO is empty, this will
|
||||
/// play out 0s.
|
||||
fn arm(&mut self) {
|
||||
dma_ring().init();
|
||||
self.set_sample_rate(self.cur_rate).ok();
|
||||
self.i2s.i2s.cfg1.modify(|_, w| w.mainenable().enabled());
|
||||
}
|
||||
///Transition -> Prefill
|
||||
/// Unmute DAC
|
||||
fn prefill(&mut self) {
|
||||
self.dac.unmute();
|
||||
}
|
||||
///Transition -> Running
|
||||
///Unmask I2S ISR, start feedback
|
||||
fn run(&mut self) {
|
||||
self.fb.start();
|
||||
// FIFO threshold trigger enable
|
||||
self.i2s
|
||||
.i2s
|
||||
.fifotrig
|
||||
.modify(|_, w| unsafe { w.txlvl().bits(6).txlvlena().enabled() });
|
||||
self.i2s
|
||||
.i2s
|
||||
.fifocfg
|
||||
.modify(|_, w| w.enabletx().enabled().dmatx().enabled());
|
||||
dma_ring().run();
|
||||
unsafe {
|
||||
pac::NVIC::unmask(pac::Interrupt::DMA0);
|
||||
}
|
||||
}
|
||||
///Transition->NoData
|
||||
///store framecount at transition so we can time out recovery
|
||||
fn nodata(&mut self) {
|
||||
self.nodata_timeout_frame.store(
|
||||
PERF.queue_underflows.load(Ordering::Relaxed) + NODATA_TIMEOUT_FRAMES, // we underflow every frame, use it as a timeout counter
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
}
|
||||
impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
||||
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
|
||||
const SOF_SYNC: bool = false;
|
||||
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.cur_rate
|
||||
}
|
||||
fn set_sample_rate(
|
||||
&mut self,
|
||||
sample_rate: u32,
|
||||
) -> core::result::Result<(), usbd_uac2::UsbAudioClassError> {
|
||||
if 24_576_000u32.is_multiple_of(sample_rate) {
|
||||
defmt::info!("[clock] 24M clock selected");
|
||||
self.clock_pins.sel_22m.set_low().ok();
|
||||
// hal::wait_at_least(1);
|
||||
self.clock_pins.sel_24m.set_high().ok();
|
||||
} else {
|
||||
defmt::info!("[clock] 22M clock selected");
|
||||
self.clock_pins.sel_24m.set_low().ok();
|
||||
// hal::wait_at_least(1);
|
||||
self.clock_pins.sel_22m.set_high().ok();
|
||||
};
|
||||
self.dac.change_rate(sample_rate);
|
||||
self.cur_rate = sample_rate;
|
||||
Ok(())
|
||||
}
|
||||
fn sample_rates(
|
||||
&self,
|
||||
) -> core::result::Result<&[usbd_uac2::RangeEntry<u32>], usbd_uac2::UsbAudioClassError> {
|
||||
Ok(&Self::RATES)
|
||||
}
|
||||
fn clock_validity(&self) -> Result<bool, UsbAudioClassError> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
||||
fn alternate_setting_changed(&mut self, _terminal: usb_device::UsbDirection, alt_setting: u8) {
|
||||
let state = self.state.load(Ordering::Relaxed);
|
||||
match (alt_setting, state) {
|
||||
(0, AudioState::Armed | AudioState::Prefill | AudioState::NoData) => {
|
||||
self.transition(AudioState::Stopped)
|
||||
}
|
||||
(0, AudioState::Running | AudioState::Stopped) => {} // noop, we naturally transition through LowData to Stopped
|
||||
(1, AudioState::Stopped) => self.transition(AudioState::Armed),
|
||||
(1, _) => {} // altSetting 1 in any other state is a no-op
|
||||
(_, _) => {
|
||||
defmt::error!("Invalid alt setting {}", alt_setting)
|
||||
}
|
||||
}
|
||||
self.alt_setting = alt_setting;
|
||||
}
|
||||
fn audio_data_rx(
|
||||
&mut self,
|
||||
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
|
||||
) {
|
||||
let state = self.state.load(Ordering::Relaxed);
|
||||
let mut buf = [0; (SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
||||
let len = match ep.read(&mut buf) {
|
||||
Ok(len) => len,
|
||||
Err(_) => {
|
||||
defmt::error!("usb error in rx callback");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let buf = &buf[..len];
|
||||
let res = self.dma.push(buf);
|
||||
|
||||
if res.dropped != 0 {
|
||||
// Overflow: some or all bytes couldn't be queued.
|
||||
defmt::error!(
|
||||
"overflowed dma ring, asked {}, wrote {}, dropped {}",
|
||||
buf.len(),
|
||||
res.written,
|
||||
res.dropped
|
||||
);
|
||||
PERF.queue_overflows
|
||||
.fetch_add(res.dropped / BYTES_PER_FRAME, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
PERF.received_frames
|
||||
.fetch_add(res.written / BYTES_PER_FRAME, Ordering::Relaxed);
|
||||
|
||||
// Valid states here are Armed, Prefill, Running, Draining and NoData
|
||||
match state {
|
||||
AudioState::Stopped => {
|
||||
defmt::error!("Received audio data when stopped")
|
||||
}
|
||||
// When armed, data rx goes to prefill
|
||||
AudioState::Armed => self.transition(AudioState::Prefill),
|
||||
// When prefilling, if we have received frames over the up threshold, move to running
|
||||
AudioState::Prefill => {
|
||||
if PERF.received_frames.load(Ordering::Relaxed) >= QUEUE_RUNNING_UP {
|
||||
self.transition(AudioState::Running);
|
||||
}
|
||||
}
|
||||
// When running, USB RX is a no-op
|
||||
AudioState::Running => {}
|
||||
// If draining, check cur_fill, if it rises above QUEUE_RUNNING_UP, move back to running. If it drops to 0, move to NoData or Stopped
|
||||
AudioState::LowData => {
|
||||
let fill = cur_fill() as usize;
|
||||
// Do we check alt setting here? We shouldn't be receiving data at all if we are not in altSetting 1
|
||||
if fill >= QUEUE_RUNNING_UP {
|
||||
self.transition(AudioState::Running);
|
||||
} else if fill == 0 && self.alt_setting == 0 {
|
||||
self.transition(AudioState::Stopped);
|
||||
} else if fill == 0 {
|
||||
self.transition(AudioState::NoData);
|
||||
}
|
||||
}
|
||||
// Any data in NoData moves us into LowData. But maybe it should be more like prefill?
|
||||
AudioState::NoData => self.transition(AudioState::LowData),
|
||||
}
|
||||
}
|
||||
fn audio_data_tx(
|
||||
&mut self,
|
||||
_ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::In>,
|
||||
) {
|
||||
}
|
||||
fn feedback(&mut self, nominal_rate: UsbIsochronousFeedback) -> Option<UsbIsochronousFeedback> {
|
||||
if !self.fb.correction_enabled.load(Ordering::Relaxed) {
|
||||
return Some(nominal_rate);
|
||||
}
|
||||
|
||||
let current_bytes = cur_fill() as i32;
|
||||
|
||||
if current_bytes == 0 {
|
||||
defmt::error!("[fb] dma underrun detected!");
|
||||
PERF.queue_underflows.fetch_add(1, Ordering::Relaxed);
|
||||
return Some(nominal_rate);
|
||||
}
|
||||
PERF.avg_fill
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| {
|
||||
Some(((v << 6) - v + current_bytes as usize) >> 6)
|
||||
})
|
||||
.ok();
|
||||
|
||||
// normalize error wrt. frame size etc.
|
||||
let error_permille = ((current_bytes - FILL_TARGET_BYTES) * 1000) / FILL_TARGET_BYTES;
|
||||
|
||||
let nominal_v = nominal_rate.to_u32_12_13() as i32;
|
||||
|
||||
// 0.2% which is a huge clock error
|
||||
let max_allowed_deviation = nominal_v / 500;
|
||||
|
||||
let p_term = -(error_permille * nominal_v) / 256000; // this works reasonably well to keep the buffer
|
||||
let i_term = 0; // placeholder
|
||||
|
||||
let mut v = nominal_v + p_term + i_term;
|
||||
v = v.clamp(
|
||||
nominal_v - max_allowed_deviation,
|
||||
nominal_v + max_allowed_deviation,
|
||||
);
|
||||
|
||||
Some(UsbIsochronousFeedback::new(v as u32))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct I2sTx {
|
||||
pub i2s: pac::I2S7,
|
||||
}
|
||||
|
||||
pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -> I2sTx {
|
||||
defmt::debug!("init i2s");
|
||||
// Enable BOTH
|
||||
syscon.reset(&mut fc7);
|
||||
syscon.enable_clock(&mut fc7);
|
||||
|
||||
unsafe {
|
||||
pac::IOCON::ptr().as_ref().unwrap().pio0_23.modify(|_, w| {
|
||||
w.func()
|
||||
.alt1() // MCLK
|
||||
.mode()
|
||||
.inactive()
|
||||
.slew()
|
||||
.fast()
|
||||
.invert()
|
||||
.disabled()
|
||||
.digimode()
|
||||
.digital()
|
||||
.od()
|
||||
.normal()
|
||||
});
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mclkio
|
||||
.modify(|_, w| w.mclkio().input());
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fcclksel7()
|
||||
.modify(|_, w| w.sel().enum_0x5()); // MCLK
|
||||
};
|
||||
|
||||
// Select I2S TX function
|
||||
fc7.pselid.write(|w| w.persel().i2s_transmit());
|
||||
|
||||
let regs = i2s7;
|
||||
I2sTx { i2s: regs }
|
||||
}
|
||||
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
let hal = hal::new();
|
||||
|
||||
let mut anactrl = hal.anactrl;
|
||||
let mut pmc = hal.pmc;
|
||||
let mut syscon = hal.syscon;
|
||||
let mut gpio = hal.gpio.enabled(&mut syscon);
|
||||
let mut iocon = hal.iocon.enabled(&mut syscon);
|
||||
|
||||
debug!("start");
|
||||
|
||||
debug!("iocon");
|
||||
let usb0_vbus_pin = pins::Pio0_22::take()
|
||||
.unwrap()
|
||||
.into_usb0_vbus_pin(&mut iocon);
|
||||
let codec_i2c_pins = (
|
||||
pins::Pio0_16::take().unwrap().into_i2c4_scl_pin(&mut iocon),
|
||||
pins::Pio0_5::take().unwrap().into_i2c4_sda_pin(&mut iocon),
|
||||
);
|
||||
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::Pio0_23::take().unwrap(),
|
||||
);
|
||||
let codec_gpio_pins = CodecPins {
|
||||
reset: pins::Pio0_3::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
};
|
||||
let clock_sel_pins = ClockSelPins {
|
||||
sel_24m: pins::Pio0_27::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
sel_22m: pins::Pio0_31::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
};
|
||||
let leds = (
|
||||
pins::Pio0_13::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
pins::Pio0_14::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
);
|
||||
|
||||
// iocon.disabled(&mut syscon).release(); // save the environment :)
|
||||
|
||||
debug!("clocks");
|
||||
let clocks = hal::ClockRequirements::default()
|
||||
.system_frequency(96.MHz())
|
||||
.configure(&mut anactrl, &mut pmc, &mut syscon)
|
||||
.unwrap();
|
||||
hw::init_sys_pll1();
|
||||
let mut delay_timer = Timer::new(
|
||||
hal.ctimer
|
||||
.0
|
||||
.enabled(&mut syscon, clocks.support_1mhz_fro_token().unwrap()),
|
||||
);
|
||||
|
||||
debug!("peripherals");
|
||||
|
||||
let i2c_peripheral = hal
|
||||
.flexcomm
|
||||
.4
|
||||
.enabled_as_i2c(&mut syscon, &clocks.support_flexcomm_token().unwrap());
|
||||
|
||||
let i2c_bus = I2cMaster::new(
|
||||
i2c_peripheral,
|
||||
codec_i2c_pins,
|
||||
Hertz::try_from(400.kHz()).unwrap(),
|
||||
);
|
||||
let dac_impl = DacImpl::new(i2c_bus, codec_gpio_pins);
|
||||
|
||||
let i2s_peripheral = {
|
||||
let fc7 = hal.flexcomm.7.release();
|
||||
init_i2s(fc7.0, fc7.2, &mut syscon)
|
||||
};
|
||||
|
||||
let usb_peripheral = hal.usbhs.enabled_as_device(
|
||||
&mut anactrl,
|
||||
&mut pmc,
|
||||
&mut syscon,
|
||||
&mut delay_timer,
|
||||
clocks.support_usbhs_token().unwrap(),
|
||||
);
|
||||
|
||||
defmt::info!("dma init");
|
||||
let i2s_dma_addr = &i2s_peripheral.i2s.fifowr as *const _ as *mut u32;
|
||||
let dma =
|
||||
DmaRing::<N_SLOTS, BYTES_PER_SLOT>::new(hal.dma.release(), &mut syscon, i2s_dma_addr, 4)
|
||||
.unwrap();
|
||||
let dma_ref = DMA_RING.init(dma);
|
||||
unsafe { DMA_RING_REF = Some(dma_ref) };
|
||||
|
||||
defmt::info!("audio init");
|
||||
let mut audio = Audio {
|
||||
state: Atomic::new(AudioState::Stopped),
|
||||
i2s: i2s_peripheral,
|
||||
dac: dac_impl,
|
||||
dma: dma_ring(),
|
||||
fb: FeedbackState::default(),
|
||||
alt_setting: 0,
|
||||
nodata_timeout_frame: AtomicUsize::new(0),
|
||||
cur_rate: SAMPLE_RATE,
|
||||
clock_pins: clock_sel_pins,
|
||||
_marker: core::marker::PhantomData,
|
||||
};
|
||||
audio.init();
|
||||
|
||||
let usb_bus = UsbBus::new(usb_peripheral, usb0_vbus_pin);
|
||||
|
||||
let config = UsbAudioClassConfig::new(UsbSpeed::High, FunctionCode::Other, &mut audio)
|
||||
.with_output_config(
|
||||
TerminalConfig::builder()
|
||||
.base_id(2)
|
||||
.terminal_type(TerminalType::UsbUndefined)
|
||||
.build(),
|
||||
);
|
||||
let mut uac2 = config.build(&usb_bus).unwrap();
|
||||
#[cfg(feature = "hid")]
|
||||
let mut hid = HIDClass::new_ep_in(&usb_bus, AudioTelemetryReport::desc(), HID_INTERVAL_MS);
|
||||
|
||||
let mut usb_dev = usbd_uac2::builder(&usb_bus, UsbVidPid(0x1209, 0xcc1d))
|
||||
.strings(&[StringDescriptors::default()
|
||||
.manufacturer("VE7XEN")
|
||||
.product("Guac Tortilla")])
|
||||
.unwrap()
|
||||
.max_packet_size_0(64)
|
||||
.unwrap()
|
||||
.build();
|
||||
|
||||
#[cfg(feature = "hid")]
|
||||
let mut poll_all = {
|
||||
let mut hid_update_timer = Timer::new(
|
||||
hal.ctimer
|
||||
.1
|
||||
.enabled(&mut syscon, clocks.support_1mhz_fro_token().unwrap()),
|
||||
);
|
||||
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
||||
|
||||
move || {
|
||||
let active = usb_dev.poll(&mut [&mut uac2, &mut hid]);
|
||||
if active && hid_update_timer.wait().is_ok() {
|
||||
let report = PERF.build_report();
|
||||
match hid.push_input(&report) {
|
||||
Ok(_) => {}
|
||||
Err(UsbError::WouldBlock) => {}
|
||||
Err(e) => defmt::error!("Failed to send HID report: {:?}", e),
|
||||
}
|
||||
// lpc55 timer is not Periodic, so restart it
|
||||
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(not(feature = "hid"))]
|
||||
let poll_all = || {
|
||||
usb_dev.poll(&mut [&mut uac2]);
|
||||
};
|
||||
|
||||
defmt::info!("main loop");
|
||||
|
||||
loop {
|
||||
poll_all();
|
||||
// usb_dev.poll(&mut [&mut uac2]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::CodecPins;
|
||||
pub trait Dac<T> {
|
||||
fn new(i2c: T, pins: CodecPins) -> Self;
|
||||
/// The DAC should start muted
|
||||
fn init(&mut self);
|
||||
fn change_rate(&mut self, new_rate: u32);
|
||||
#[allow(unused_variables)]
|
||||
fn set_volume(&mut self, left: u8, right: u8) {}
|
||||
fn mute(&mut self);
|
||||
fn unmute(&mut self);
|
||||
}
|
||||
Reference in New Issue
Block a user