diff options
| author | elagil <[email protected]> | 2024-11-02 20:01:20 +0100 |
|---|---|---|
| committer | Dario Nieuwenhuis <[email protected]> | 2024-11-24 00:33:08 +0100 |
| commit | 36292ada6228bff99cb0baa916786da394e05a42 (patch) | |
| tree | 46478c7b921fe527a1dd03f8ff0c18417136220f /examples | |
| parent | 0d299301efaa0843d3da09909f02e795eeffc035 (diff) | |
feat(stm32h5): add usb audio example
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/stm32h5/src/bin/usb_uac_speaker.rs | 367 |
1 files changed, 367 insertions, 0 deletions
diff --git a/examples/stm32h5/src/bin/usb_uac_speaker.rs b/examples/stm32h5/src/bin/usb_uac_speaker.rs new file mode 100644 index 000000000..6b992690f --- /dev/null +++ b/examples/stm32h5/src/bin/usb_uac_speaker.rs | |||
| @@ -0,0 +1,367 @@ | |||
| 1 | #![no_std] | ||
| 2 | #![no_main] | ||
| 3 | |||
| 4 | use core::cell::RefCell; | ||
| 5 | |||
| 6 | use defmt::{panic, *}; | ||
| 7 | use embassy_executor::Spawner; | ||
| 8 | use embassy_stm32::time::Hertz; | ||
| 9 | use embassy_stm32::{bind_interrupts, interrupt, peripherals, timer, usb, Config}; | ||
| 10 | use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}; | ||
| 11 | use embassy_sync::blocking_mutex::Mutex; | ||
| 12 | use embassy_sync::signal::Signal; | ||
| 13 | use embassy_sync::zerocopy_channel; | ||
| 14 | use embassy_usb::class::uac1; | ||
| 15 | use embassy_usb::class::uac1::speaker::{self, Speaker}; | ||
| 16 | use embassy_usb::driver::EndpointError; | ||
| 17 | use heapless::Vec; | ||
| 18 | use micromath::F32Ext; | ||
| 19 | use static_cell::StaticCell; | ||
| 20 | use {defmt_rtt as _, panic_probe as _}; | ||
| 21 | |||
| 22 | bind_interrupts!(struct Irqs { | ||
| 23 | USB_DRD_FS => usb::InterruptHandler<peripherals::USB>; | ||
| 24 | }); | ||
| 25 | |||
| 26 | static TIMER: Mutex<CriticalSectionRawMutex, RefCell<Option<timer::low_level::Timer<peripherals::TIM5>>>> = | ||
| 27 | Mutex::new(RefCell::new(None)); | ||
| 28 | |||
| 29 | // A counter signal that is written by the feedback timer, once every `FEEDBACK_REFRESH_PERIOD`. | ||
| 30 | // At that point, a feedback value is sent to the host. | ||
| 31 | pub static FEEDBACK_SIGNAL: Signal<CriticalSectionRawMutex, u32> = Signal::new(); | ||
| 32 | |||
| 33 | // Stereo input | ||
| 34 | pub const INPUT_CHANNEL_COUNT: usize = 2; | ||
| 35 | |||
| 36 | // This example uses a fixed sample rate of 48 kHz. | ||
| 37 | pub const SAMPLE_RATE_HZ: u32 = 48_000; | ||
| 38 | pub const FEEDBACK_COUNTER_TICK_RATE: u32 = 31_250_000; | ||
| 39 | |||
| 40 | // Use 32 bit samples, which allow for a lot of (software) volume adjustment without degradation of quality. | ||
| 41 | pub const SAMPLE_WIDTH: uac1::SampleWidth = uac1::SampleWidth::Width4Byte; | ||
| 42 | pub const SAMPLE_WIDTH_BIT: usize = SAMPLE_WIDTH.in_bit(); | ||
| 43 | pub const SAMPLE_SIZE: usize = SAMPLE_WIDTH as usize; | ||
| 44 | pub const SAMPLE_SIZE_PER_S: usize = (SAMPLE_RATE_HZ as usize) * INPUT_CHANNEL_COUNT * SAMPLE_SIZE; | ||
| 45 | |||
| 46 | // Size of audio samples per 1 ms - for the full-speed USB frame period of 1 ms. | ||
| 47 | pub const USB_FRAME_SIZE: usize = SAMPLE_SIZE_PER_S.div_ceil(1000); | ||
| 48 | |||
| 49 | // Select front left and right audio channels. | ||
| 50 | pub const AUDIO_CHANNELS: [uac1::Channel; INPUT_CHANNEL_COUNT] = [uac1::Channel::LeftFront, uac1::Channel::RightFront]; | ||
| 51 | |||
| 52 | // Factor of two as a margin for feedback (this is an excessive amount) | ||
| 53 | pub const USB_MAX_PACKET_SIZE: usize = 2 * USB_FRAME_SIZE; | ||
| 54 | pub const USB_MAX_SAMPLE_COUNT: usize = USB_MAX_PACKET_SIZE / SAMPLE_SIZE; | ||
| 55 | |||
| 56 | // The data type that is exchanged via the zero-copy channel (a sample vector). | ||
| 57 | pub type SampleBlock = Vec<u32, USB_MAX_SAMPLE_COUNT>; | ||
| 58 | |||
| 59 | // Feedback is provided in 10.14 format for full-speed endpoints. | ||
| 60 | pub const FEEDBACK_REFRESH_PERIOD: uac1::FeedbackRefresh = uac1::FeedbackRefresh::Period8Frames; | ||
| 61 | const FEEDBACK_SHIFT: usize = 14; | ||
| 62 | |||
| 63 | const TICKS_PER_SAMPLE: f32 = (FEEDBACK_COUNTER_TICK_RATE as f32) / (SAMPLE_RATE_HZ as f32); | ||
| 64 | |||
| 65 | struct Disconnected {} | ||
| 66 | |||
| 67 | impl From<EndpointError> for Disconnected { | ||
| 68 | fn from(val: EndpointError) -> Self { | ||
| 69 | match val { | ||
| 70 | EndpointError::BufferOverflow => panic!("Buffer overflow"), | ||
| 71 | EndpointError::Disabled => Disconnected {}, | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | /// Sends feedback messages to the host. | ||
| 77 | async fn feedback_handler<'d, T: usb::Instance + 'd>( | ||
| 78 | feedback: &mut speaker::Feedback<'d, usb::Driver<'d, T>>, | ||
| 79 | feedback_factor: f32, | ||
| 80 | ) -> Result<(), Disconnected> { | ||
| 81 | let mut packet: Vec<u8, 4> = Vec::new(); | ||
| 82 | |||
| 83 | // Collects the fractional component of the feedback value that is lost by rounding. | ||
| 84 | let mut rest = 0.0_f32; | ||
| 85 | |||
| 86 | loop { | ||
| 87 | let counter = FEEDBACK_SIGNAL.wait().await; | ||
| 88 | |||
| 89 | packet.clear(); | ||
| 90 | |||
| 91 | let raw_value = counter as f32 * feedback_factor + rest; | ||
| 92 | let value = raw_value.round(); | ||
| 93 | rest = raw_value - value; | ||
| 94 | |||
| 95 | let value = value as u32; | ||
| 96 | |||
| 97 | packet.push(value as u8).unwrap(); | ||
| 98 | packet.push((value >> 8) as u8).unwrap(); | ||
| 99 | packet.push((value >> 16) as u8).unwrap(); | ||
| 100 | |||
| 101 | feedback.write_packet(&packet).await?; | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | /// Handles streaming of audio data from the host. | ||
| 106 | async fn stream_handler<'d, T: usb::Instance + 'd>( | ||
| 107 | stream: &mut speaker::Stream<'d, usb::Driver<'d, T>>, | ||
| 108 | sender: &mut zerocopy_channel::Sender<'static, NoopRawMutex, SampleBlock>, | ||
| 109 | ) -> Result<(), Disconnected> { | ||
| 110 | loop { | ||
| 111 | let mut usb_data = [0u8; USB_MAX_PACKET_SIZE]; | ||
| 112 | let data_size = stream.read_packet(&mut usb_data).await?; | ||
| 113 | |||
| 114 | let word_count = data_size / SAMPLE_SIZE; | ||
| 115 | |||
| 116 | if word_count * SAMPLE_SIZE == data_size { | ||
| 117 | // Obtain a buffer from the channel | ||
| 118 | let samples = sender.send().await; | ||
| 119 | samples.clear(); | ||
| 120 | |||
| 121 | for w in 0..word_count { | ||
| 122 | let byte_offset = w * SAMPLE_SIZE; | ||
| 123 | let sample = u32::from_le_bytes(usb_data[byte_offset..byte_offset + SAMPLE_SIZE].try_into().unwrap()); | ||
| 124 | |||
| 125 | // Fill the sample buffer with data. | ||
| 126 | samples.push(sample).unwrap(); | ||
| 127 | } | ||
| 128 | |||
| 129 | sender.send_done(); | ||
| 130 | } else { | ||
| 131 | debug!("Invalid USB buffer size of {}, skipped.", data_size); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | /// Receives audio samples from the USB streaming task and can play them back. | ||
| 137 | #[embassy_executor::task] | ||
| 138 | async fn audio_receiver_task(mut usb_audio_receiver: zerocopy_channel::Receiver<'static, NoopRawMutex, SampleBlock>) { | ||
| 139 | loop { | ||
| 140 | let _samples = usb_audio_receiver.receive().await; | ||
| 141 | // Use the samples, for example play back via the SAI peripheral. | ||
| 142 | |||
| 143 | // Notify the channel that the buffer is now ready to be reused | ||
| 144 | usb_audio_receiver.receive_done(); | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | /// Receives audio samples from the host. | ||
| 149 | #[embassy_executor::task] | ||
| 150 | async fn usb_streaming_task( | ||
| 151 | mut stream: speaker::Stream<'static, usb::Driver<'static, peripherals::USB>>, | ||
| 152 | mut sender: zerocopy_channel::Sender<'static, NoopRawMutex, SampleBlock>, | ||
| 153 | ) { | ||
| 154 | loop { | ||
| 155 | stream.wait_connection().await; | ||
| 156 | info!("USB connected."); | ||
| 157 | _ = stream_handler(&mut stream, &mut sender).await; | ||
| 158 | info!("USB disconnected."); | ||
| 159 | } | ||
| 160 | } | ||
| 161 | |||
| 162 | /// Sends sample rate feedback to the host. | ||
| 163 | /// | ||
| 164 | /// The `feedback_factor` scales the feedback timer's counter value so that the result is the number of samples that | ||
| 165 | /// this device played back or "consumed" during one SOF period (1 ms) - in 10.14 format. | ||
| 166 | /// | ||
| 167 | /// Ideally, the `feedback_factor` that is calculated below would be an integer for avoiding numerical errors. | ||
| 168 | /// This is achieved by having `TICKS_PER_SAMPLE` be a power of two. For audio applications at a sample rate of 48 kHz, | ||
| 169 | /// 24.576 MHz would be one such option. | ||
| 170 | #[embassy_executor::task] | ||
| 171 | async fn usb_feedback_task(mut feedback: speaker::Feedback<'static, usb::Driver<'static, peripherals::USB>>) { | ||
| 172 | let feedback_factor = | ||
| 173 | ((1 << FEEDBACK_SHIFT) as f32 / TICKS_PER_SAMPLE) / FEEDBACK_REFRESH_PERIOD.frame_count() as f32; | ||
| 174 | |||
| 175 | loop { | ||
| 176 | feedback.wait_connection().await; | ||
| 177 | _ = feedback_handler(&mut feedback, feedback_factor).await; | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | #[embassy_executor::task] | ||
| 182 | async fn usb_task(mut usb_device: embassy_usb::UsbDevice<'static, usb::Driver<'static, peripherals::USB>>) { | ||
| 183 | usb_device.run().await; | ||
| 184 | } | ||
| 185 | |||
| 186 | /// Checks for changes on the control monitor of the class. | ||
| 187 | /// | ||
| 188 | /// In this case, monitor changes of volume or mute state. | ||
| 189 | #[embassy_executor::task] | ||
| 190 | async fn usb_control_task(control_monitor: speaker::ControlMonitor<'static>) { | ||
| 191 | loop { | ||
| 192 | control_monitor.changed().await; | ||
| 193 | |||
| 194 | for channel in AUDIO_CHANNELS { | ||
| 195 | let volume = control_monitor.volume(channel).unwrap(); | ||
| 196 | info!("Volume changed to {} on channel {}.", volume, channel); | ||
| 197 | } | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | /// Feedback value measurement and calculation | ||
| 202 | /// | ||
| 203 | /// Used for measuring/calculating the number of samples that were received from the host during the | ||
| 204 | /// `FEEDBACK_REFRESH_PERIOD`. | ||
| 205 | /// | ||
| 206 | /// Configured in this example with | ||
| 207 | /// - a refresh period of 8 ms, and | ||
| 208 | /// - a tick rate of 42 MHz. | ||
| 209 | /// | ||
| 210 | /// This gives an (ideal) counter value of 336.000 for every update of the `FEEDBACK_SIGNAL`. | ||
| 211 | #[interrupt] | ||
| 212 | fn TIM5() { | ||
| 213 | static mut LAST_TICKS: u32 = 0; | ||
| 214 | static mut FRAME_COUNT: usize = 0; | ||
| 215 | |||
| 216 | critical_section::with(|cs| { | ||
| 217 | // Read timer counter. | ||
| 218 | let ticks = TIMER.borrow(cs).borrow().as_ref().unwrap().regs_gp32().cnt().read(); | ||
| 219 | |||
| 220 | // Clear trigger interrupt flag. | ||
| 221 | TIMER | ||
| 222 | .borrow(cs) | ||
| 223 | .borrow_mut() | ||
| 224 | .as_mut() | ||
| 225 | .unwrap() | ||
| 226 | .regs_gp32() | ||
| 227 | .sr() | ||
| 228 | .modify(|r| r.set_tif(false)); | ||
| 229 | |||
| 230 | // Count up frames and emit a signal, when the refresh period is reached (here, every 8 ms). | ||
| 231 | *FRAME_COUNT += 1; | ||
| 232 | if *FRAME_COUNT >= FEEDBACK_REFRESH_PERIOD.frame_count() { | ||
| 233 | *FRAME_COUNT = 0; | ||
| 234 | FEEDBACK_SIGNAL.signal(ticks.wrapping_sub(*LAST_TICKS)); | ||
| 235 | *LAST_TICKS = ticks; | ||
| 236 | } | ||
| 237 | }); | ||
| 238 | } | ||
| 239 | |||
| 240 | // If you are trying this and your USB device doesn't connect, the most | ||
| 241 | // common issues are the RCC config and vbus_detection | ||
| 242 | // | ||
| 243 | // See https://embassy.dev/book/#_the_usb_examples_are_not_working_on_my_board_is_there_anything_else_i_need_to_configure | ||
| 244 | // for more information. | ||
| 245 | #[embassy_executor::main] | ||
| 246 | async fn main(spawner: Spawner) { | ||
| 247 | let mut config = Config::default(); | ||
| 248 | { | ||
| 249 | use embassy_stm32::rcc::*; | ||
| 250 | config.rcc.hsi = None; | ||
| 251 | config.rcc.hsi48 = Some(Hsi48Config { sync_from_usb: true }); // needed for USB | ||
| 252 | config.rcc.hse = Some(Hse { | ||
| 253 | freq: Hertz(8_000_000), | ||
| 254 | mode: HseMode::BypassDigital, | ||
| 255 | }); | ||
| 256 | config.rcc.pll1 = Some(Pll { | ||
| 257 | source: PllSource::HSE, | ||
| 258 | prediv: PllPreDiv::DIV2, | ||
| 259 | mul: PllMul::MUL125, | ||
| 260 | divp: Some(PllDiv::DIV2), // 250 Mhz | ||
| 261 | divq: None, | ||
| 262 | divr: None, | ||
| 263 | }); | ||
| 264 | config.rcc.pll2 = Some(Pll { | ||
| 265 | source: PllSource::HSE, | ||
| 266 | prediv: PllPreDiv::DIV4, | ||
| 267 | mul: PllMul::MUL123, | ||
| 268 | divp: Some(PllDiv::DIV20), // 12.3 Mhz, close to 12.288 MHz for 48 kHz audio | ||
| 269 | divq: None, | ||
| 270 | divr: None, | ||
| 271 | }); | ||
| 272 | config.rcc.ahb_pre = AHBPrescaler::DIV2; | ||
| 273 | config.rcc.apb1_pre = APBPrescaler::DIV4; | ||
| 274 | config.rcc.apb2_pre = APBPrescaler::DIV2; | ||
| 275 | config.rcc.apb3_pre = APBPrescaler::DIV4; | ||
| 276 | config.rcc.sys = Sysclk::PLL1_P; | ||
| 277 | config.rcc.voltage_scale = VoltageScale::Scale0; | ||
| 278 | config.rcc.mux.usbsel = mux::Usbsel::HSI48; | ||
| 279 | config.rcc.mux.sai2sel = mux::Saisel::PLL2_P; | ||
| 280 | } | ||
| 281 | let p = embassy_stm32::init(config); | ||
| 282 | |||
| 283 | info!("Hello World!"); | ||
| 284 | |||
| 285 | // Configure all required buffers in a static way. | ||
| 286 | debug!("USB packet size is {} byte", USB_MAX_PACKET_SIZE); | ||
| 287 | static CONFIG_DESCRIPTOR: StaticCell<[u8; 256]> = StaticCell::new(); | ||
| 288 | let config_descriptor = CONFIG_DESCRIPTOR.init([0; 256]); | ||
| 289 | |||
| 290 | static BOS_DESCRIPTOR: StaticCell<[u8; 32]> = StaticCell::new(); | ||
| 291 | let bos_descriptor = BOS_DESCRIPTOR.init([0; 32]); | ||
| 292 | |||
| 293 | const CONTROL_BUF_SIZE: usize = 64; | ||
| 294 | static CONTROL_BUF: StaticCell<[u8; CONTROL_BUF_SIZE]> = StaticCell::new(); | ||
| 295 | let control_buf = CONTROL_BUF.init([0; CONTROL_BUF_SIZE]); | ||
| 296 | |||
| 297 | static STATE: StaticCell<speaker::State> = StaticCell::new(); | ||
| 298 | let state = STATE.init(speaker::State::new()); | ||
| 299 | |||
| 300 | let usb_driver = usb::Driver::new(p.USB, Irqs, p.PA12, p.PA11); | ||
| 301 | |||
| 302 | // Basic USB device configuration | ||
| 303 | let mut config = embassy_usb::Config::new(0xc0de, 0xcafe); | ||
| 304 | config.manufacturer = Some("Embassy"); | ||
| 305 | config.product = Some("USB-audio-speaker example"); | ||
| 306 | config.serial_number = Some("12345678"); | ||
| 307 | |||
| 308 | // Required for windows compatibility. | ||
| 309 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help | ||
| 310 | config.device_class = 0xEF; | ||
| 311 | config.device_sub_class = 0x02; | ||
| 312 | config.device_protocol = 0x01; | ||
| 313 | config.composite_with_iads = true; | ||
| 314 | |||
| 315 | let mut builder = embassy_usb::Builder::new( | ||
| 316 | usb_driver, | ||
| 317 | config, | ||
| 318 | config_descriptor, | ||
| 319 | bos_descriptor, | ||
| 320 | &mut [], // no msos descriptors | ||
| 321 | control_buf, | ||
| 322 | ); | ||
| 323 | |||
| 324 | // Create the UAC1 Speaker class components | ||
| 325 | let (stream, feedback, control_monitor) = Speaker::new( | ||
| 326 | &mut builder, | ||
| 327 | state, | ||
| 328 | USB_MAX_PACKET_SIZE as u16, | ||
| 329 | uac1::SampleWidth::Width4Byte, | ||
| 330 | &[SAMPLE_RATE_HZ], | ||
| 331 | &AUDIO_CHANNELS, | ||
| 332 | FEEDBACK_REFRESH_PERIOD, | ||
| 333 | ); | ||
| 334 | |||
| 335 | // Create the USB device | ||
| 336 | let usb_device = builder.build(); | ||
| 337 | |||
| 338 | // Establish a zero-copy channel for transferring received audio samples between tasks | ||
| 339 | static SAMPLE_BLOCKS: StaticCell<[SampleBlock; 2]> = StaticCell::new(); | ||
| 340 | let sample_blocks = SAMPLE_BLOCKS.init([Vec::new(), Vec::new()]); | ||
| 341 | |||
| 342 | static CHANNEL: StaticCell<zerocopy_channel::Channel<'_, NoopRawMutex, SampleBlock>> = StaticCell::new(); | ||
| 343 | let channel = CHANNEL.init(zerocopy_channel::Channel::new(sample_blocks)); | ||
| 344 | let (sender, receiver) = channel.split(); | ||
| 345 | |||
| 346 | // Run a timer for counting between SOF interrupts. | ||
| 347 | let mut tim5 = timer::low_level::Timer::new(p.TIM5); | ||
| 348 | tim5.set_tick_freq(Hertz(FEEDBACK_COUNTER_TICK_RATE)); | ||
| 349 | tim5.set_trigger_source(timer::low_level::TriggerSource::ITR12); // The USB SOF signal. | ||
| 350 | tim5.set_slave_mode(timer::low_level::SlaveMode::TRIGGER_MODE); | ||
| 351 | tim5.regs_gp16().dier().modify(|r| r.set_tie(true)); // Enable the trigger interrupt. | ||
| 352 | tim5.start(); | ||
| 353 | |||
| 354 | TIMER.lock(|p| p.borrow_mut().replace(tim5)); | ||
| 355 | |||
| 356 | // Unmask the TIM5 interrupt. | ||
| 357 | unsafe { | ||
| 358 | cortex_m::peripheral::NVIC::unmask(interrupt::TIM5); | ||
| 359 | } | ||
| 360 | |||
| 361 | // Launch USB audio tasks. | ||
| 362 | unwrap!(spawner.spawn(usb_control_task(control_monitor))); | ||
| 363 | unwrap!(spawner.spawn(usb_streaming_task(stream, sender))); | ||
| 364 | unwrap!(spawner.spawn(usb_feedback_task(feedback))); | ||
| 365 | unwrap!(spawner.spawn(usb_task(usb_device))); | ||
| 366 | unwrap!(spawner.spawn(audio_receiver_task(receiver))); | ||
| 367 | } | ||
