Skip to content

Commit

Permalink
feat(stm32h5): add usb audio example
Browse files Browse the repository at this point in the history
  • Loading branch information
elagil committed Sep 5, 2024
1 parent 84c2b8e commit 14de351
Showing 1 changed file with 367 additions and 0 deletions.
367 changes: 367 additions & 0 deletions examples/stm32h5/src/bin/usb_uac_speaker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,367 @@
#![no_std]
#![no_main]

use core::cell::RefCell;

use defmt::{panic, *};
use embassy_executor::Spawner;
use embassy_stm32::time::Hertz;
use embassy_stm32::{bind_interrupts, interrupt, peripherals, timer, usb, Config};
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
use embassy_sync::blocking_mutex::Mutex;
use embassy_sync::signal::Signal;
use embassy_sync::zerocopy_channel;
use embassy_usb::class::uac1;
use embassy_usb::class::uac1::speaker::{self, Speaker};
use embassy_usb::driver::EndpointError;
use heapless::Vec;
use micromath::F32Ext;
use static_cell::StaticCell;
use {defmt_rtt as _, panic_probe as _};

bind_interrupts!(struct Irqs {
USB_DRD_FS => usb::InterruptHandler<peripherals::USB>;
});

static TIMER: Mutex<CriticalSectionRawMutex, RefCell<Option<timer::low_level::Timer<peripherals::TIM5>>>> =
Mutex::new(RefCell::new(None));

// A counter signal that is written by the feedback timer, once every `FEEDBACK_REFRESH_PERIOD`.
// At that point, a feedback value is sent to the host.
pub static FEEDBACK_SIGNAL: Signal<CriticalSectionRawMutex, u32> = Signal::new();

// Stereo input
pub const INPUT_CHANNEL_COUNT: usize = 2;

// This example uses a fixed sample rate of 48 kHz.
pub const SAMPLE_RATE_HZ: u32 = 48_000;
pub const FEEDBACK_COUNTER_TICK_RATE: u32 = 31_250_000;

// Use 32 bit samples, which allow for a lot of (software) volume adjustment without degradation of quality.
pub const SAMPLE_WIDTH: uac1::SampleWidth = uac1::SampleWidth::Width4Byte;
pub const SAMPLE_WIDTH_BIT: usize = SAMPLE_WIDTH.in_bit();
pub const SAMPLE_SIZE: usize = SAMPLE_WIDTH as usize;
pub const SAMPLE_SIZE_PER_S: usize = (SAMPLE_RATE_HZ as usize) * INPUT_CHANNEL_COUNT * SAMPLE_SIZE;

// Size of audio samples per 1 ms - for the full-speed USB frame period of 1 ms.
pub const USB_FRAME_SIZE: usize = SAMPLE_SIZE_PER_S.div_ceil(1000);

// Select front left and right audio channels.
pub const AUDIO_CHANNELS: [uac1::Channel; INPUT_CHANNEL_COUNT] = [uac1::Channel::LeftFront, uac1::Channel::RightFront];

// Factor of two as a margin for feedback (this is an excessive amount)
pub const USB_MAX_PACKET_SIZE: usize = 2 * USB_FRAME_SIZE;
pub const USB_MAX_SAMPLE_COUNT: usize = USB_MAX_PACKET_SIZE / SAMPLE_SIZE;

// The data type that is exchanged via the zero-copy channel (a sample vector).
pub type SampleBlock = Vec<u32, USB_MAX_SAMPLE_COUNT>;

// Feedback is provided in 10.14 format for full-speed endpoints.
pub const FEEDBACK_REFRESH_PERIOD: uac1::FeedbackRefresh = uac1::FeedbackRefresh::Period8Frames;
const FEEDBACK_SHIFT: usize = 14;

const TICKS_PER_SAMPLE: f32 = (FEEDBACK_COUNTER_TICK_RATE as f32) / (SAMPLE_RATE_HZ as f32);

struct Disconnected {}

impl From<EndpointError> for Disconnected {
fn from(val: EndpointError) -> Self {
match val {
EndpointError::BufferOverflow => panic!("Buffer overflow"),
EndpointError::Disabled => Disconnected {},
}
}
}

/// Sends feedback messages to the host.
async fn feedback_handler<'d, T: usb::Instance + 'd>(
feedback: &mut speaker::Feedback<'d, usb::Driver<'d, T>>,
feedback_factor: f32,
) -> Result<(), Disconnected> {
let mut packet: Vec<u8, 4> = Vec::new();

// Collects the fractional component of the feedback value that is lost by rounding.
let mut rest = 0.0_f32;

loop {
let counter = FEEDBACK_SIGNAL.wait().await;

packet.clear();

let raw_value = counter as f32 * feedback_factor + rest;
let value = raw_value.round();
rest = raw_value - value;

let value = value as u32;

packet.push(value as u8).unwrap();
packet.push((value >> 8) as u8).unwrap();
packet.push((value >> 16) as u8).unwrap();

feedback.write_packet(&packet).await?;
}
}

/// Handles streaming of audio data from the host.
async fn stream_handler<'d, T: usb::Instance + 'd>(
stream: &mut speaker::Stream<'d, usb::Driver<'d, T>>,
sender: &mut zerocopy_channel::Sender<'static, NoopRawMutex, SampleBlock>,
) -> Result<(), Disconnected> {
loop {
let mut usb_data = [0u8; USB_MAX_PACKET_SIZE];
let data_size = stream.read_packet(&mut usb_data).await?;

let word_count = data_size / SAMPLE_SIZE;

if word_count * SAMPLE_SIZE == data_size {
// Obtain a buffer from the channel
let samples = sender.send().await;
samples.clear();

for w in 0..word_count {
let byte_offset = w * SAMPLE_SIZE;
let sample = u32::from_le_bytes(usb_data[byte_offset..byte_offset + SAMPLE_SIZE].try_into().unwrap());

// Fill the sample buffer with data.
samples.push(sample).unwrap();
}

sender.send_done();
} else {
debug!("Invalid USB buffer size of {}, skipped.", data_size);
}
}
}

/// Receives audio samples from the USB streaming task and can play them back.
#[embassy_executor::task]
async fn audio_receiver_task(mut usb_audio_receiver: zerocopy_channel::Receiver<'static, NoopRawMutex, SampleBlock>) {
loop {
let _samples = usb_audio_receiver.receive().await;
// Use the samples, for example play back via the SAI peripheral.

// Notify the channel that the buffer is now ready to be reused
usb_audio_receiver.receive_done();
}
}

/// Receives audio samples from the host.
#[embassy_executor::task]
async fn usb_streaming_task(
mut stream: speaker::Stream<'static, usb::Driver<'static, peripherals::USB>>,
mut sender: zerocopy_channel::Sender<'static, NoopRawMutex, SampleBlock>,
) {
loop {
stream.wait_connection().await;
info!("USB connected.");
_ = stream_handler(&mut stream, &mut sender).await;
info!("USB disconnected.");
}
}

/// Sends sample rate feedback to the host.
///
/// The `feedback_factor` scales the feedback timer's counter value so that the result is the number of samples that
/// this device played back or "consumed" during one SOF period (1 ms) - in 10.14 format.
///
/// Ideally, the `feedback_factor` that is calculated below would be an integer for avoiding numerical errors.
/// This is achieved by having `TICKS_PER_SAMPLE` be a power of two. For audio applications at a sample rate of 48 kHz,
/// 24.576 MHz would be one such option.
#[embassy_executor::task]
async fn usb_feedback_task(mut feedback: speaker::Feedback<'static, usb::Driver<'static, peripherals::USB>>) {
let feedback_factor =
((1 << FEEDBACK_SHIFT) as f32 / TICKS_PER_SAMPLE) / FEEDBACK_REFRESH_PERIOD.frame_count() as f32;

loop {
feedback.wait_connection().await;
_ = feedback_handler(&mut feedback, feedback_factor).await;
}
}

#[embassy_executor::task]
async fn usb_task(mut usb_device: embassy_usb::UsbDevice<'static, usb::Driver<'static, peripherals::USB>>) {
usb_device.run().await;
}

/// Checks for changes on the control monitor of the class.
///
/// In this case, monitor changes of volume or mute state.
#[embassy_executor::task]
async fn usb_control_task(control_monitor: speaker::ControlMonitor<'static>) {
loop {
control_monitor.changed().await;

for channel in AUDIO_CHANNELS {
let volume = control_monitor.volume(channel).unwrap();
info!("Volume changed to {} on channel {}.", volume, channel);
}
}
}

/// Feedback value measurement and calculation
///
/// Used for measuring/calculating the number of samples that were received from the host during the
/// `FEEDBACK_REFRESH_PERIOD`.
///
/// Configured in this example with
/// - a refresh period of 8 ms, and
/// - a tick rate of 42 MHz.
///
/// This gives an (ideal) counter value of 336.000 for every update of the `FEEDBACK_SIGNAL`.
#[interrupt]
fn TIM5() {
static mut LAST_TICKS: u32 = 0;
static mut FRAME_COUNT: usize = 0;

critical_section::with(|cs| {
// Read timer counter.
let ticks = TIMER.borrow(cs).borrow().as_ref().unwrap().regs_gp32().cnt().read();

// Clear trigger interrupt flag.
TIMER
.borrow(cs)
.borrow_mut()
.as_mut()
.unwrap()
.regs_gp32()
.sr()
.modify(|r| r.set_tif(false));

// Count up frames and emit a signal, when the refresh period is reached (here, every 8 ms).
*FRAME_COUNT += 1;
if *FRAME_COUNT >= FEEDBACK_REFRESH_PERIOD.frame_count() {
*FRAME_COUNT = 0;
FEEDBACK_SIGNAL.signal(ticks.wrapping_sub(*LAST_TICKS));
*LAST_TICKS = ticks;
}
});
}

// If you are trying this and your USB device doesn't connect, the most
// common issues are the RCC config and vbus_detection
//
// See https://embassy.dev/book/#_the_usb_examples_are_not_working_on_my_board_is_there_anything_else_i_need_to_configure
// for more information.
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let mut config = Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = None;
config.rcc.hsi48 = Some(Hsi48Config { sync_from_usb: true }); // needed for USB
config.rcc.hse = Some(Hse {
freq: Hertz(8_000_000),
mode: HseMode::BypassDigital,
});
config.rcc.pll1 = Some(Pll {
source: PllSource::HSE,
prediv: PllPreDiv::DIV2,
mul: PllMul::MUL125,
divp: Some(PllDiv::DIV2), // 250 Mhz
divq: None,
divr: None,
});
config.rcc.pll2 = Some(Pll {
source: PllSource::HSE,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL123,
divp: Some(PllDiv::DIV20), // 12.3 Mhz, close to 12.288 MHz for 48 kHz audio
divq: None,
divr: None,
});
config.rcc.ahb_pre = AHBPrescaler::DIV2;
config.rcc.apb1_pre = APBPrescaler::DIV4;
config.rcc.apb2_pre = APBPrescaler::DIV2;
config.rcc.apb3_pre = APBPrescaler::DIV4;
config.rcc.sys = Sysclk::PLL1_P;
config.rcc.voltage_scale = VoltageScale::Scale0;
config.rcc.mux.usbsel = mux::Usbsel::HSI48;
config.rcc.mux.sai2sel = mux::Saisel::PLL2_P;
}
let p = embassy_stm32::init(config);

info!("Hello World!");

// Configure all required buffers in a static way.
debug!("USB packet size is {} byte", USB_MAX_PACKET_SIZE);
static CONFIG_DESCRIPTOR: StaticCell<[u8; 256]> = StaticCell::new();
let config_descriptor = CONFIG_DESCRIPTOR.init([0; 256]);

static BOS_DESCRIPTOR: StaticCell<[u8; 32]> = StaticCell::new();
let bos_descriptor = BOS_DESCRIPTOR.init([0; 32]);

const CONTROL_BUF_SIZE: usize = 64;
static CONTROL_BUF: StaticCell<[u8; CONTROL_BUF_SIZE]> = StaticCell::new();
let control_buf = CONTROL_BUF.init([0; CONTROL_BUF_SIZE]);

static STATE: StaticCell<speaker::State> = StaticCell::new();
let state = STATE.init(speaker::State::new());

let usb_driver = usb::Driver::new(p.USB, Irqs, p.PA12, p.PA11);

// Basic USB device configuration
let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
config.manufacturer = Some("Embassy");
config.product = Some("USB-audio-speaker example");
config.serial_number = Some("12345678");

// Required for windows compatibility.
// https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
config.device_class = 0xEF;
config.device_sub_class = 0x02;
config.device_protocol = 0x01;
config.composite_with_iads = true;

let mut builder = embassy_usb::Builder::new(
usb_driver,
config,
config_descriptor,
bos_descriptor,
&mut [], // no msos descriptors
control_buf,
);

// Create the UAC1 Speaker class components
let (stream, feedback, control_monitor) = Speaker::new(
&mut builder,
state,
USB_MAX_PACKET_SIZE as u16,
uac1::SampleWidth::Width4Byte,
&[SAMPLE_RATE_HZ],
&AUDIO_CHANNELS,
FEEDBACK_REFRESH_PERIOD,
);

// Create the USB device
let usb_device = builder.build();

// Establish a zero-copy channel for transferring received audio samples between tasks
static SAMPLE_BLOCKS: StaticCell<[SampleBlock; 2]> = StaticCell::new();
let sample_blocks = SAMPLE_BLOCKS.init([Vec::new(), Vec::new()]);

static CHANNEL: StaticCell<zerocopy_channel::Channel<'_, NoopRawMutex, SampleBlock>> = StaticCell::new();
let channel = CHANNEL.init(zerocopy_channel::Channel::new(sample_blocks));
let (sender, receiver) = channel.split();

// Run a timer for counting between SOF interrupts.
let mut tim5 = timer::low_level::Timer::new(p.TIM5);
tim5.set_tick_freq(Hertz(FEEDBACK_COUNTER_TICK_RATE));
tim5.set_trigger_source(timer::low_level::TriggerSource::ITR12); // The USB SOF signal.
tim5.set_slave_mode(timer::low_level::SlaveMode::TRIGGER_MODE);
tim5.regs_gp16().dier().modify(|r| r.set_tie(true)); // Enable the trigger interrupt.
tim5.start();

TIMER.lock(|p| p.borrow_mut().replace(tim5));

// Unmask the TIM5 interrupt.
unsafe {
cortex_m::peripheral::NVIC::unmask(interrupt::TIM5);
}

// Launch USB audio tasks.
unwrap!(spawner.spawn(usb_control_task(control_monitor)));
unwrap!(spawner.spawn(usb_streaming_task(stream, sender)));
unwrap!(spawner.spawn(usb_feedback_task(feedback)));
unwrap!(spawner.spawn(usb_task(usb_device)));
unwrap!(spawner.spawn(audio_receiver_task(receiver)));
}

0 comments on commit 14de351

Please sign in to comment.