Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a20cf8e1
|
||
|
|
9d66ee6ab8
|
||
|
|
ef39602b63
|
@@ -0,0 +1,66 @@
|
|||||||
|
name: Build and Release Firmware
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
TARGET: thumbv8m.main-none-eabihf
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust Toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Install cargo-binutils
|
||||||
|
run: cargo install cargo-binutils
|
||||||
|
- name: Build Firmware Binaries
|
||||||
|
run: |
|
||||||
|
cd firmware
|
||||||
|
|
||||||
|
# Array of "variant_name:features"
|
||||||
|
VARIANTS=(
|
||||||
|
"cs4398:cs4398,hid"
|
||||||
|
"ak4490:ak4490,hid"
|
||||||
|
"evk:evk,hid"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p ../dist
|
||||||
|
|
||||||
|
for entry in "${VARIANTS[@]}"; do
|
||||||
|
IFS=":" read -r name features <<< "$entry"
|
||||||
|
echo "Building variant: $name with features: $features"
|
||||||
|
|
||||||
|
# 1. Compile release binary
|
||||||
|
cargo build --release --target ${{ env.TARGET }} --no-default-features --features "$features"
|
||||||
|
|
||||||
|
# 2. Paths setup
|
||||||
|
ELF_PATH="../target/${{ env.TARGET }}/release/guac"
|
||||||
|
HEX_PATH="../target/${{ env.TARGET }}/release/guac-${name}.hex"
|
||||||
|
RENAMED_ELF="../target/${{ env.TARGET }}/release/guac-${name}.elf"
|
||||||
|
|
||||||
|
# 3. Create .hex file via rust-objcopy
|
||||||
|
cargo objcopy --release --target ${{ env.TARGET }} --no-default-features --features "$features" -- -O ihex "$HEX_PATH"
|
||||||
|
cp "$ELF_PATH" "$RENAMED_ELF"
|
||||||
|
|
||||||
|
# 4. Package ELF and HEX into target-specific ZIP
|
||||||
|
ZIP_NAME="guac-firmware-${name}.zip"
|
||||||
|
zip -j "../dist/${ZIP_NAME}" "$RENAMED_ELF" "$HEX_PATH"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
uses: akkuman/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
files: dist/*.zip
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
@@ -294,6 +294,8 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self) {
|
pub fn run(&self) {
|
||||||
|
self.dma.inta0.write(|w| unsafe { w.bits(1 << 19) });
|
||||||
|
self.dma.errint0.write(|w| unsafe { w.bits(1 << 19) });
|
||||||
self.dma.enableset0.write(|w| unsafe { w.bits(1 << 19) });
|
self.dma.enableset0.write(|w| unsafe { w.bits(1 << 19) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+202
-157
@@ -8,6 +8,8 @@ fn panic() -> ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use atomic::Atomic;
|
use atomic::Atomic;
|
||||||
|
use core::ptr;
|
||||||
|
use core::sync::atomic::AtomicPtr;
|
||||||
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
||||||
use cortex_m_rt::entry;
|
use cortex_m_rt::entry;
|
||||||
use defmt;
|
use defmt;
|
||||||
@@ -117,47 +119,29 @@ struct ClockSelPins {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct PerfCounters {
|
struct PerfCounters {
|
||||||
state: Atomic<AudioState>,
|
// state: Atomic<AudioState>,
|
||||||
received_frames: AtomicUsize,
|
received_frames: AtomicUsize,
|
||||||
played_frames: AtomicUsize,
|
played_frames: AtomicUsize,
|
||||||
min_fill: AtomicUsize,
|
// min_fill: AtomicUsize,
|
||||||
avg_fill: AtomicUsize,
|
// avg_fill: AtomicUsize,
|
||||||
queue_underflows: AtomicUsize,
|
queue_underflows: AtomicUsize,
|
||||||
queue_overflows: AtomicUsize,
|
queue_overflows: AtomicUsize,
|
||||||
audio_underflows: AtomicUsize,
|
audio_underflows: AtomicUsize,
|
||||||
integrator: AtomicI32,
|
// p: AtomicI32,
|
||||||
p: AtomicI32,
|
// fb: AtomicI32,
|
||||||
i: AtomicI32,
|
|
||||||
fb: AtomicI32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerfCounters {
|
impl PerfCounters {
|
||||||
fn reset(&self) {
|
fn reset(&self) {
|
||||||
self.received_frames.store(0, Ordering::Relaxed);
|
self.received_frames.store(0, Ordering::Relaxed);
|
||||||
self.played_frames.store(0, Ordering::Relaxed);
|
self.played_frames.store(0, Ordering::Relaxed);
|
||||||
self.min_fill
|
// self.min_fill
|
||||||
.store(N_SLOTS * MAX_BYTES_PER_SLOT, Ordering::Relaxed);
|
// .store(N_SLOTS * MAX_BYTES_PER_SLOT, Ordering::Relaxed);
|
||||||
self.avg_fill.store(0 as usize, Ordering::Relaxed);
|
// self.avg_fill.store(0 as usize, Ordering::Relaxed);
|
||||||
self.queue_underflows.store(0, Ordering::Relaxed);
|
self.queue_underflows.store(0, Ordering::Relaxed);
|
||||||
self.queue_overflows.store(0, Ordering::Relaxed);
|
self.queue_overflows.store(0, Ordering::Relaxed);
|
||||||
self.audio_underflows.store(0, Ordering::Relaxed);
|
self.audio_underflows.store(0, Ordering::Relaxed);
|
||||||
self.p.store(0, Ordering::Relaxed);
|
// self.p.store(0, Ordering::Relaxed);
|
||||||
self.i.store(0, Ordering::Relaxed);
|
|
||||||
// FB loop will have to take care of the fb value
|
|
||||||
}
|
|
||||||
fn build_report(&self) -> AudioTelemetryReport {
|
|
||||||
AudioTelemetryReport {
|
|
||||||
state: self.state.load(Ordering::Relaxed) as u8,
|
|
||||||
average_buffer_fill: self.avg_fill.load(Ordering::Relaxed) as u16,
|
|
||||||
frame_count: self.played_frames.load(Ordering::Relaxed) as i32,
|
|
||||||
dac_underflow_count: self.audio_underflows.load(Ordering::Relaxed) as u16,
|
|
||||||
usb_underflow_count: self.queue_underflows.load(Ordering::Relaxed) as u16,
|
|
||||||
dac_overflow_count: self.queue_overflows.load(Ordering::Relaxed) as u16,
|
|
||||||
integrator: self.integrator.load(Ordering::Relaxed),
|
|
||||||
p: self.p.load(Ordering::Relaxed),
|
|
||||||
i: self.i.load(Ordering::Relaxed),
|
|
||||||
fb: u32::cast_signed(self.fb.load(Ordering::Relaxed) as u32),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +149,11 @@ impl defmt::Format for PerfCounters {
|
|||||||
fn format(&self, fmt: defmt::Formatter) {
|
fn format(&self, fmt: defmt::Formatter) {
|
||||||
defmt::write!(
|
defmt::write!(
|
||||||
fmt,
|
fmt,
|
||||||
"frames: {}/{} min_fill: {} avg fill: {} a_underflows: {} q_underflows: {} q_overflows: {}",
|
"frames: {}/{} a_underflows: {} q_underflows: {} q_overflows: {}",
|
||||||
self.played_frames.load(Ordering::Relaxed),
|
self.played_frames.load(Ordering::Relaxed),
|
||||||
self.received_frames.load(Ordering::Relaxed),
|
self.received_frames.load(Ordering::Relaxed),
|
||||||
self.min_fill.load(Ordering::Relaxed),
|
// self.min_fill.load(Ordering::Relaxed),
|
||||||
self.avg_fill.load(Ordering::Relaxed),
|
// self.avg_fill.load(Ordering::Relaxed),
|
||||||
self.audio_underflows.load(Ordering::Relaxed),
|
self.audio_underflows.load(Ordering::Relaxed),
|
||||||
self.queue_underflows.load(Ordering::Relaxed),
|
self.queue_underflows.load(Ordering::Relaxed),
|
||||||
self.queue_overflows.load(Ordering::Relaxed)
|
self.queue_overflows.load(Ordering::Relaxed)
|
||||||
@@ -178,27 +162,141 @@ impl defmt::Format for PerfCounters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PERF: PerfCounters = PerfCounters {
|
static PERF: PerfCounters = PerfCounters {
|
||||||
state: Atomic::new(AudioState::Stopped),
|
// state: Atomic::new(AudioState::Stopped),
|
||||||
received_frames: AtomicUsize::new(0), // received from USB
|
received_frames: AtomicUsize::new(0), // received from USB
|
||||||
played_frames: AtomicUsize::new(0), // played audio frames
|
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
|
// 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(0),
|
// avg_fill: AtomicUsize::new(0),
|
||||||
queue_underflows: AtomicUsize::new(0), // ditto here, since we underflow at startup, but we record this one as it can be trended
|
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),
|
queue_overflows: AtomicUsize::new(0),
|
||||||
audio_underflows: AtomicUsize::new(0),
|
audio_underflows: AtomicUsize::new(0),
|
||||||
integrator: AtomicI32::new(0),
|
// p: AtomicI32::new(0),
|
||||||
p: AtomicI32::new(0),
|
// fb: AtomicI32::new(0),
|
||||||
i: AtomicI32::new(0),
|
|
||||||
fb: AtomicI32::new(0),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn build_telemetry_report<D: Dac<I>, I>(
|
||||||
|
perf: &PerfCounters,
|
||||||
|
audio: &Audio<D, I>,
|
||||||
|
) -> AudioTelemetryReport {
|
||||||
|
AudioTelemetryReport {
|
||||||
|
state: audio.state.load(Ordering::Relaxed) as u8,
|
||||||
|
average_buffer_fill: audio.fb.avg_fill as u16,
|
||||||
|
frame_count: perf.played_frames.load(Ordering::Relaxed).cast_signed() as i32,
|
||||||
|
dac_underflow_count: perf.audio_underflows.load(Ordering::Relaxed) as u16,
|
||||||
|
usb_underflow_count: perf.queue_underflows.load(Ordering::Relaxed) as u16,
|
||||||
|
dac_overflow_count: perf.queue_overflows.load(Ordering::Relaxed) as u16,
|
||||||
|
cur_rate: audio.cur_rate.cast_signed(),
|
||||||
|
fb_rate_estimate: audio.fb.current_freq_estimate() as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct FeedbackConfig {
|
||||||
|
/// Nominal USB rate in Q12.13 fixed-point format (or target sample rate context).
|
||||||
|
pub nominal_rate: u32,
|
||||||
|
/// Target ring buffer fill level in bytes.
|
||||||
|
pub target_fill_bytes: i32,
|
||||||
|
/// Deadband threshold in bytes (e.g., 4).
|
||||||
|
pub deadband_bytes: i32,
|
||||||
|
/// Maximum allowed feedback deviation permille denominator (e.g., 500 => 0.2%).
|
||||||
|
pub max_deviation_divider: i32,
|
||||||
|
/// Master toggle for feedback correction.
|
||||||
|
pub correction_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeedbackConfig {
|
||||||
|
pub fn new(nominal_rate: u32, target_fill_bytes: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
nominal_rate: nominal_rate,
|
||||||
|
target_fill_bytes: target_fill_bytes as i32,
|
||||||
|
deadband_bytes: 4,
|
||||||
|
max_deviation_divider: 500, // 0.2%
|
||||||
|
correction_enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn for_rate(nominal_rate: u32) -> Self {
|
||||||
|
let target_fill_bytes = (bytes_per_slot(nominal_rate) * N_SLOTS) / 2;
|
||||||
|
Self::new(nominal_rate, target_fill_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FeedbackLoop {
|
||||||
|
pub config: FeedbackConfig,
|
||||||
|
pub avg_fill: usize,
|
||||||
|
last_freq: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeedbackLoop {
|
||||||
|
pub fn new(config: FeedbackConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
avg_fill: config.target_fill_bytes as usize,
|
||||||
|
last_freq: config.nominal_rate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.avg_fill = self.config.target_fill_bytes as usize;
|
||||||
|
self.last_freq = self.config.nominal_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, config: FeedbackConfig) {
|
||||||
|
self.config = config;
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute(
|
||||||
|
&mut self,
|
||||||
|
nominal_rate: UsbIsochronousFeedback,
|
||||||
|
current_fill_bytes: usize,
|
||||||
|
) -> UsbIsochronousFeedback {
|
||||||
|
if !self.config.correction_enabled {
|
||||||
|
return nominal_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_bytes = current_fill_bytes as i32;
|
||||||
|
|
||||||
|
if current_bytes == 0 {
|
||||||
|
defmt::error!("[fb] dma underrun detected!");
|
||||||
|
PERF.queue_underflows.fetch_add(1, Ordering::Relaxed);
|
||||||
|
return nominal_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.avg_fill = ((self.avg_fill << 6) - self.avg_fill + current_bytes as usize) >> 6;
|
||||||
|
|
||||||
|
let target_fill = self.config.target_fill_bytes;
|
||||||
|
let raw_error = current_bytes - target_fill;
|
||||||
|
|
||||||
|
let nominal_v = nominal_rate.to_u32_12_13() as i32;
|
||||||
|
let max_allowed_deviation = nominal_v / self.config.max_deviation_divider;
|
||||||
|
|
||||||
|
let error_permille = (raw_error * 1000) / target_fill;
|
||||||
|
let p_term = (-((error_permille as i64) * (nominal_v as i64)) / (10 * 256000)) as i32;
|
||||||
|
|
||||||
|
let mut v = nominal_v + p_term;
|
||||||
|
v = v.clamp(
|
||||||
|
nominal_v - max_allowed_deviation,
|
||||||
|
nominal_v + max_allowed_deviation,
|
||||||
|
);
|
||||||
|
self.last_freq = v as u32;
|
||||||
|
UsbIsochronousFeedback::new(v as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_freq_estimate(&self) -> f32 {
|
||||||
|
// Divides by 8 (frame-to-microframe) and 65536.0 (Q16.16 shift)
|
||||||
|
(self.last_freq as f32 / (65536.0)) * USB_FRAME_RATE as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static NODATA_FLAG: AtomicBool = AtomicBool::new(false);
|
static NODATA_FLAG: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
static DMA_RING: StaticCell<DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> = StaticCell::new();
|
static DMA_RING: StaticCell<DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> = StaticCell::new();
|
||||||
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> = None;
|
static DMA_RING_PTR: AtomicPtr<DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> =
|
||||||
|
AtomicPtr::new(ptr::null_mut());
|
||||||
#[inline]
|
#[inline]
|
||||||
fn dma_ring() -> &'static DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT> {
|
fn dma_ring() -> &'static DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT> {
|
||||||
unsafe { DMA_RING_REF.unwrap() }
|
let ptr = DMA_RING_PTR.load(Ordering::Acquire);
|
||||||
|
unsafe { &*ptr }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cur_fill() -> usize {
|
fn cur_fill() -> usize {
|
||||||
@@ -209,14 +307,21 @@ fn cur_fill() -> usize {
|
|||||||
produced_bytes.wrapping_sub(consumed_bytes) as usize
|
produced_bytes.wrapping_sub(consumed_bytes) as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// current fill target (based on current slot size)
|
||||||
fn cur_fill_target() -> i32 {
|
fn cur_fill_target() -> i32 {
|
||||||
(dma_ring().slot_size() * N_SLOTS) as i32 / 2
|
(dma_ring().slot_size() * N_SLOTS) as i32 / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// frames per slot (based on current slot size)
|
||||||
fn frames_per_slot() -> usize {
|
fn frames_per_slot() -> usize {
|
||||||
dma_ring().slot_size() / BYTES_PER_FRAME
|
dma_ring().slot_size() / BYTES_PER_FRAME
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// bytes per slot (based on provided rate)
|
||||||
|
fn bytes_per_slot(rate: u32) -> usize {
|
||||||
|
(rate as usize / DMA_RATE) * BYTES_PER_FRAME
|
||||||
|
}
|
||||||
|
|
||||||
// 50%
|
// 50%
|
||||||
fn queue_running_up_threshold() -> usize {
|
fn queue_running_up_threshold() -> usize {
|
||||||
(frames_per_slot() * N_SLOTS) / 2
|
(frames_per_slot() * N_SLOTS) / 2
|
||||||
@@ -276,39 +381,13 @@ fn FLEXCOMM7() {
|
|||||||
.modify(|_, w| w.txerr().set_bit())
|
.modify(|_, w| w.txerr().set_bit())
|
||||||
}
|
}
|
||||||
|
|
||||||
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(cur_fill_target(), Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for FeedbackState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
correction_enabled: AtomicBool::new(false),
|
|
||||||
integrator: AtomicI32::new(0),
|
|
||||||
filtered_fill: AtomicI32::new(cur_fill_target()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Audio<'a, D: Dac<I>, I> {
|
struct Audio<'a, D: Dac<I>, I> {
|
||||||
state: Atomic<AudioState>,
|
state: Atomic<AudioState>,
|
||||||
alt_setting: u8,
|
alt_setting: u8,
|
||||||
i2s: I2sTx,
|
i2s: I2sTx,
|
||||||
dac: D,
|
dac: D,
|
||||||
dma: &'a DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>,
|
dma: &'a DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>,
|
||||||
fb: FeedbackState,
|
fb: FeedbackLoop,
|
||||||
nodata_timeout_frame: AtomicUsize,
|
nodata_timeout_frame: AtomicUsize,
|
||||||
cur_rate: u32,
|
cur_rate: u32,
|
||||||
clock_pins: ClockSelPins,
|
clock_pins: ClockSelPins,
|
||||||
@@ -332,8 +411,8 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
AudioState::NoData => self.nodata(),
|
AudioState::NoData => self.nodata(),
|
||||||
AudioState::Stopping => self.stopping(),
|
AudioState::Stopping => self.stopping(),
|
||||||
}
|
}
|
||||||
self.state.store(state, Ordering::SeqCst);
|
self.state.store(state, Ordering::Release);
|
||||||
PERF.state.store(state, Ordering::Relaxed);
|
// PERF.state.store(state, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(&mut self) {
|
fn init(&mut self) {
|
||||||
@@ -376,6 +455,7 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
unsafe { pac::NVIC::unmask(pac::Interrupt::FLEXCOMM7) };
|
unsafe { pac::NVIC::unmask(pac::Interrupt::FLEXCOMM7) };
|
||||||
|
|
||||||
self.dac.init();
|
self.dac.init();
|
||||||
}
|
}
|
||||||
///Transition -> Stopped:
|
///Transition -> Stopped:
|
||||||
@@ -415,7 +495,7 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
///Transition -> Running
|
///Transition -> Running
|
||||||
///Unmask I2S ISR, start feedback
|
///Unmask I2S ISR, start feedback
|
||||||
fn run(&mut self) {
|
fn run(&mut self) {
|
||||||
self.fb.start();
|
self.i2s.i2s.fifostat.write(|w| w.txerr().set_bit());
|
||||||
// FIFO threshold trigger enable
|
// FIFO threshold trigger enable
|
||||||
self.i2s
|
self.i2s
|
||||||
.i2s
|
.i2s
|
||||||
@@ -425,14 +505,14 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
|||||||
.i2s
|
.i2s
|
||||||
.fifocfg
|
.fifocfg
|
||||||
.modify(|_, w| w.enabletx().enabled().dmatx().enabled());
|
.modify(|_, w| w.enabletx().enabled().dmatx().enabled());
|
||||||
dma_ring().run();
|
|
||||||
// clear tx error status
|
|
||||||
self.i2s.i2s.fifostat.write(|w| w.txerr().set_bit());
|
|
||||||
// enable tx error interrupt
|
|
||||||
self.i2s.i2s.fifointenset.write(|w| w.txerr().enabled());
|
|
||||||
unsafe {
|
unsafe {
|
||||||
pac::NVIC::unmask(pac::Interrupt::DMA0);
|
pac::NVIC::unmask(pac::Interrupt::DMA0);
|
||||||
}
|
}
|
||||||
|
dma_ring().run();
|
||||||
|
// clear tx error status
|
||||||
|
// self.i2s.i2s.fifostat.write(|w| w.txerr().set_bit());
|
||||||
|
// enable tx error interrupt
|
||||||
|
self.i2s.i2s.fifointenset.write(|w| w.txerr().enabled());
|
||||||
}
|
}
|
||||||
///Transition->NoData
|
///Transition->NoData
|
||||||
///store framecount at transition so we can time out recovery
|
///store framecount at transition so we can time out recovery
|
||||||
@@ -472,13 +552,15 @@ impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
|||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
) -> core::result::Result<(), usbd_uac2::UsbAudioClassError> {
|
) -> core::result::Result<(), usbd_uac2::UsbAudioClassError> {
|
||||||
defmt::info!("[clock] changing rate to {}", sample_rate);
|
defmt::info!("[clock] changing rate to {}", sample_rate);
|
||||||
if self.state.load(Ordering::SeqCst) != AudioState::Stopped {
|
if self.state.load(Ordering::Acquire) != AudioState::Stopped {
|
||||||
defmt::warn!("[clock] changing rate when not stopped, stopping first");
|
defmt::warn!("[clock] changing rate when not stopped, stopping first");
|
||||||
self.stop();
|
self.stop();
|
||||||
}
|
}
|
||||||
let slot_bytes = (self.cur_rate as usize / DMA_RATE) * BYTES_PER_FRAME;
|
|
||||||
dma_ring().set_slot_size(slot_bytes);
|
|
||||||
self.cur_rate = sample_rate;
|
self.cur_rate = sample_rate;
|
||||||
|
let slot_bytes = bytes_per_slot(self.cur_rate);
|
||||||
|
dma_ring().set_slot_size(slot_bytes);
|
||||||
|
self.fb
|
||||||
|
.update_config(FeedbackConfig::for_rate(self.cur_rate));
|
||||||
if 24_576_000u32.is_multiple_of(sample_rate) {
|
if 24_576_000u32.is_multiple_of(sample_rate) {
|
||||||
defmt::info!("[clock] 24M osc selected");
|
defmt::info!("[clock] 24M osc selected");
|
||||||
self.clock_pins.sel_22m.set_low().ok();
|
self.clock_pins.sel_22m.set_low().ok();
|
||||||
@@ -506,7 +588,7 @@ impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
|||||||
}
|
}
|
||||||
impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
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) {
|
fn alternate_setting_changed(&mut self, _terminal: usb_device::UsbDirection, alt_setting: u8) {
|
||||||
let state = self.state.load(Ordering::Relaxed);
|
let state = self.state.load(Ordering::Acquire);
|
||||||
match (alt_setting, state) {
|
match (alt_setting, state) {
|
||||||
(0, AudioState::Armed | AudioState::Prefill | AudioState::NoData) => {
|
(0, AudioState::Armed | AudioState::Prefill | AudioState::NoData) => {
|
||||||
self.transition(AudioState::Stopped)
|
self.transition(AudioState::Stopped)
|
||||||
@@ -525,7 +607,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
|
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
|
||||||
) {
|
) {
|
||||||
let state = self.state.load(Ordering::Relaxed);
|
let state = self.state.load(Ordering::Acquire);
|
||||||
let mut buf =
|
let mut buf =
|
||||||
[0; (MAX_SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
[0; (MAX_SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
||||||
let len = match ep.read(&mut buf) {
|
let len = match ep.read(&mut buf) {
|
||||||
@@ -556,7 +638,8 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
// Valid states here are Armed, Prefill, Running, Draining and NoData
|
// Valid states here are Armed, Prefill, Running, Draining and NoData
|
||||||
match state {
|
match state {
|
||||||
AudioState::Stopped | AudioState::Stopping => {
|
AudioState::Stopped | AudioState::Stopping => {
|
||||||
defmt::error!("Received audio data when stopped/stopping")
|
defmt::warn!("Received audio data when stopped/stopping");
|
||||||
|
self.transition(AudioState::Prefill);
|
||||||
}
|
}
|
||||||
// When armed, data rx goes to prefill
|
// When armed, data rx goes to prefill
|
||||||
AudioState::Armed => self.transition(AudioState::Prefill),
|
AudioState::Armed => self.transition(AudioState::Prefill),
|
||||||
@@ -583,7 +666,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Any data in NoData moves us into LowData. But maybe it should be more like prefill?
|
// Any data in NoData moves us into LowData. But maybe it should be more like prefill?
|
||||||
AudioState::NoData => self.transition(AudioState::LowData),
|
AudioState::NoData => self.transition(AudioState::Prefill),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn audio_data_tx(
|
fn audio_data_tx(
|
||||||
@@ -592,11 +675,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
fn feedback(&mut self, nominal_rate: UsbIsochronousFeedback) -> Option<UsbIsochronousFeedback> {
|
fn feedback(&mut self, nominal_rate: UsbIsochronousFeedback) -> Option<UsbIsochronousFeedback> {
|
||||||
if !self.fb.correction_enabled.load(Ordering::Relaxed) {
|
let current_bytes = cur_fill();
|
||||||
return Some(nominal_rate);
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_bytes = cur_fill() as i32;
|
|
||||||
|
|
||||||
if current_bytes == 0 {
|
if current_bytes == 0 {
|
||||||
defmt::error!("[fb] dma underrun detected!");
|
defmt::error!("[fb] dma underrun detected!");
|
||||||
@@ -604,44 +683,11 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
|||||||
return Some(nominal_rate);
|
return Some(nominal_rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
PERF.avg_fill
|
let v = self.fb.compute(nominal_rate, current_bytes);
|
||||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| {
|
// PERF.avg_fill.store(self.fb.avg_fill, Ordering::Relaxed);
|
||||||
Some(((v << 6) - v + current_bytes as usize) >> 6)
|
// PERF.fb.store(v as i32, Ordering::Relaxed);
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let raw_error = current_bytes - cur_fill_target();
|
Some(v)
|
||||||
let i_error = if raw_error.abs() <= 4 { 0 } else { raw_error }; // deadband
|
|
||||||
let current_i = self.fb.integrator.load(Ordering::Relaxed);
|
|
||||||
let leak = current_i >> 7;
|
|
||||||
let new_i = current_i
|
|
||||||
.saturating_sub(leak)
|
|
||||||
.saturating_add(i_error)
|
|
||||||
.clamp(-5000, 5000);
|
|
||||||
self.fb.integrator.store(new_i, Ordering::Relaxed);
|
|
||||||
PERF.integrator.store(new_i, Ordering::Relaxed);
|
|
||||||
|
|
||||||
let nominal_v = nominal_rate.to_u32_12_13() as i32;
|
|
||||||
let max_allowed_deviation = nominal_v / 500; // 0.2%
|
|
||||||
|
|
||||||
// 3. SEPARATE GAINS FOR P AND I
|
|
||||||
// For P: Keep your working math (converting raw error to a permille equivalent scale)
|
|
||||||
let error_permille = (raw_error * 1000) / cur_fill_target();
|
|
||||||
let p_term = (-((error_permille as i64) * (nominal_v as i64)) / (10 * 256000)) as i32;
|
|
||||||
let i_term = (-((new_i as i64) * (nominal_v as i64)) / (256000 * 1000)) as i32;
|
|
||||||
let i_term = 0;
|
|
||||||
|
|
||||||
PERF.p.store(p_term, Ordering::Relaxed);
|
|
||||||
PERF.i.store(i_term, Ordering::Relaxed);
|
|
||||||
|
|
||||||
let mut v = nominal_v + p_term + i_term;
|
|
||||||
v = v.clamp(
|
|
||||||
nominal_v - max_allowed_deviation,
|
|
||||||
nominal_v + max_allowed_deviation,
|
|
||||||
);
|
|
||||||
PERF.fb.store(v, Ordering::Relaxed);
|
|
||||||
|
|
||||||
Some(UsbIsochronousFeedback::new(v as u32))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,15 +877,17 @@ fn main() -> ! {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let dma_ref = DMA_RING.init(dma);
|
let dma_ref = DMA_RING.init(dma);
|
||||||
unsafe { DMA_RING_REF = Some(dma_ref) };
|
DMA_RING_PTR.store(dma_ref as *const _ as *mut _, Ordering::Release);
|
||||||
|
// unsafe { DMA_RING_PTR = Some(dma_ref) };
|
||||||
|
|
||||||
defmt::info!("audio init");
|
defmt::info!("audio init");
|
||||||
|
|
||||||
let mut audio = Audio {
|
let mut audio = Audio {
|
||||||
state: Atomic::new(AudioState::Stopped),
|
state: Atomic::new(AudioState::Stopped),
|
||||||
i2s: i2s_peripheral,
|
i2s: i2s_peripheral,
|
||||||
dac: dac_impl,
|
dac: dac_impl,
|
||||||
dma: dma_ring(),
|
dma: dma_ring(),
|
||||||
fb: FeedbackState::default(),
|
fb: FeedbackLoop::new(FeedbackConfig::for_rate(SAMPLE_RATES[0].min)),
|
||||||
alt_setting: 0,
|
alt_setting: 0,
|
||||||
nodata_timeout_frame: AtomicUsize::new(0),
|
nodata_timeout_frame: AtomicUsize::new(0),
|
||||||
cur_rate: SAMPLE_RATES[0].min,
|
cur_rate: SAMPLE_RATES[0].min,
|
||||||
@@ -870,47 +918,45 @@ fn main() -> ! {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// 1. Initialize the HID timer conditionally outside the closure
|
||||||
#[cfg(feature = "hid")]
|
#[cfg(feature = "hid")]
|
||||||
let mut poll_all = {
|
let mut hid_update_timer = {
|
||||||
let mut hid_update_timer = Timer::new(
|
let mut timer = Timer::new(
|
||||||
hal.ctimer
|
hal.ctimer
|
||||||
.1
|
.1
|
||||||
.enabled(&mut syscon, clocks.support_1mhz_fro_token().unwrap()),
|
.enabled(&mut syscon, clocks.support_1mhz_fro_token().unwrap()),
|
||||||
);
|
);
|
||||||
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
||||||
|
timer
|
||||||
|
};
|
||||||
|
|
||||||
move || {
|
// 2. Single consolidated poll_all closure
|
||||||
usb_dev.poll(&mut [&mut uac2, &mut hid]);
|
let mut poll_all = move || {
|
||||||
// DMA ring is empty; if altsetting = 0 then -> stopped else NoData
|
// Poll active USB classes
|
||||||
// TODO: handle NoData state
|
#[cfg(feature = "hid")]
|
||||||
if NODATA_FLAG.swap(false, Ordering::Acquire) {
|
usb_dev.poll(&mut [&mut uac2, &mut hid]);
|
||||||
match uac2.handler().state.load(Ordering::Acquire) {
|
|
||||||
AudioState::Stopping => uac2.handler().transition(AudioState::Stopped),
|
#[cfg(not(feature = "hid"))]
|
||||||
_ => uac2.handler().transition(AudioState::Stopped),
|
usb_dev.poll(&mut [&mut uac2]);
|
||||||
}
|
|
||||||
}
|
// TODO: we should handle unexpected NODATA differently from 'Stopping'
|
||||||
if hid_update_timer.wait().is_ok() {
|
if NODATA_FLAG.swap(false, Ordering::AcqRel) {
|
||||||
let report = PERF.build_report();
|
match uac2.handler().state.load(Ordering::Acquire) {
|
||||||
match hid.push_input(&report) {
|
AudioState::Stopping => uac2.handler().transition(AudioState::Stopped),
|
||||||
Ok(_) => {}
|
_ => uac2.handler().transition(AudioState::Stopped),
|
||||||
Err(UsbError::WouldBlock) => {}
|
|
||||||
Err(e) => defmt::error!("Failed to send HID report: {:?}", e),
|
|
||||||
}
|
|
||||||
// lpc55 ctimer is not Periodic, so restart it
|
|
||||||
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
#[cfg(not(feature = "hid"))]
|
#[cfg(feature = "hid")]
|
||||||
let mut poll_all = {
|
if hid_update_timer.wait().is_ok() {
|
||||||
move || {
|
let report = build_telemetry_report(&PERF, uac2.handler());
|
||||||
usb_dev.poll(&mut [&mut uac2]);
|
if let Err(e) = hid.push_input(&report) {
|
||||||
if NODATA_FLAG.swap(false, Ordering::Acquire) {
|
if e != UsbError::WouldBlock {
|
||||||
match uac2.handler().state.load(Ordering::Acquire) {
|
defmt::error!("Failed to send HID report: {:?}", e);
|
||||||
AudioState::Stopping => uac2.handler().transition(AudioState::Stopped),
|
|
||||||
_ => uac2.handler().transition(AudioState::Stopped),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// LPC55 CTIMER is not periodic; restart manually
|
||||||
|
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -918,6 +964,5 @@ fn main() -> ! {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
poll_all();
|
poll_all();
|
||||||
// usb_dev.poll(&mut [&mut uac2]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-15
@@ -99,9 +99,8 @@ pub struct AudioTelemetrySnapshot {
|
|||||||
pub dac_underflow_count: u16,
|
pub dac_underflow_count: u16,
|
||||||
pub usb_underflow_count: u16,
|
pub usb_underflow_count: u16,
|
||||||
pub dac_overflow_count: u16,
|
pub dac_overflow_count: u16,
|
||||||
pub p: i32,
|
pub cur_rate: u32,
|
||||||
pub i: i32,
|
pub fb_rate_estimate: f32,
|
||||||
pub fb: i32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod hid {
|
pub mod hid {
|
||||||
@@ -123,10 +122,8 @@ pub mod hid {
|
|||||||
dac_underflow_count=input;
|
dac_underflow_count=input;
|
||||||
usb_underflow_count=input;
|
usb_underflow_count=input;
|
||||||
dac_overflow_count=input;
|
dac_overflow_count=input;
|
||||||
p=input;
|
cur_rate=input;
|
||||||
i=input;
|
fb_rate_estimate=input;
|
||||||
fb=input;
|
|
||||||
integrator=input;
|
|
||||||
}
|
}
|
||||||
)]
|
)]
|
||||||
#[repr(C, packed)]
|
#[repr(C, packed)]
|
||||||
@@ -138,10 +135,8 @@ pub mod hid {
|
|||||||
pub dac_underflow_count: u16,
|
pub dac_underflow_count: u16,
|
||||||
pub usb_underflow_count: u16,
|
pub usb_underflow_count: u16,
|
||||||
pub dac_overflow_count: u16,
|
pub dac_overflow_count: u16,
|
||||||
pub p: i32,
|
pub cur_rate: i32,
|
||||||
pub i: i32,
|
pub fb_rate_estimate: i32,
|
||||||
pub fb: i32,
|
|
||||||
pub integrator: i32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AudioTelemetryReport> for AudioTelemetrySnapshot {
|
impl From<AudioTelemetryReport> for AudioTelemetrySnapshot {
|
||||||
@@ -149,13 +144,12 @@ pub mod hid {
|
|||||||
AudioTelemetrySnapshot {
|
AudioTelemetrySnapshot {
|
||||||
state: AudioState::try_from(value.state).expect("Invalid AudioState"),
|
state: AudioState::try_from(value.state).expect("Invalid AudioState"),
|
||||||
average_buffer_fill: value.average_buffer_fill,
|
average_buffer_fill: value.average_buffer_fill,
|
||||||
frame_count: i32::cast_unsigned(value.frame_count), // on firmware side is usize == u32
|
frame_count: value.frame_count.cast_unsigned(), // on firmware side is usize == u32
|
||||||
dac_underflow_count: value.dac_underflow_count,
|
dac_underflow_count: value.dac_underflow_count,
|
||||||
usb_underflow_count: value.usb_underflow_count,
|
usb_underflow_count: value.usb_underflow_count,
|
||||||
dac_overflow_count: value.dac_overflow_count,
|
dac_overflow_count: value.dac_overflow_count,
|
||||||
p: value.p,
|
cur_rate: value.cur_rate.cast_unsigned(),
|
||||||
i: value.i,
|
fb_rate_estimate: (value.fb_rate_estimate as f32),
|
||||||
fb: value.fb,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user