mirror of
https://github.com/embassy-rs/embassy.git
synced 2026-03-06 14:19:06 +00:00
Merge pull request #5209 from leftger/feat/wba6-add-sai
stm32wba6: add SD-to-SAI PCM/WAV example
This commit is contained in:
commit
8b73fc9cee
@ -12,6 +12,8 @@ embassy-executor = { version = "0.9.0", path = "../../embassy-executor", feature
|
||||
embassy-time = { version = "0.5.0", path = "../../embassy-time", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] }
|
||||
embassy-usb = { version = "0.5.1", path = "../../embassy-usb", features = ["defmt"] }
|
||||
embassy-futures = { version = "0.1.2", path = "../../embassy-futures" }
|
||||
embedded-sdmmc = "0.9.0"
|
||||
embedded-hal-bus = "0.3.0"
|
||||
|
||||
defmt = "1.0.1"
|
||||
defmt-rtt = "1.0.0"
|
||||
|
||||
37
examples/stm32wba6/README_PCM.md
Normal file
37
examples/stm32wba6/README_PCM.md
Normal file
@ -0,0 +1,37 @@
|
||||
# SD to SAI PCM Streaming
|
||||
|
||||
This example streams audio from SD card to SAI using raw PCM files.
|
||||
|
||||
## File format
|
||||
|
||||
Raw PCM files (.pcm) must be:
|
||||
- Unsigned 16-bit little-endian
|
||||
- 48 kHz
|
||||
- Mono
|
||||
|
||||
## Convert WAV to PCM
|
||||
|
||||
Use the Python helper:
|
||||
|
||||
```bash
|
||||
python3 convert_wav.py input.wav output.pcm
|
||||
```
|
||||
|
||||
If your WAV is not 48 kHz, resample it first (for example with ffmpeg):
|
||||
|
||||
```bash
|
||||
ffmpeg -i input.wav -ar 48000 -ac 1 temp.wav
|
||||
```
|
||||
|
||||
Then convert `temp.wav` to PCM with the script above.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Copy a `.pcm` or `.wav` file to the root of the SD card.
|
||||
2. Flash and run:
|
||||
|
||||
```bash
|
||||
cargo run --bin sdmmc_sai --release
|
||||
```
|
||||
|
||||
The example plays the first `.pcm` or `.wav` file found in the SD card root.
|
||||
74
examples/stm32wba6/convert_wav.py
Normal file
74
examples/stm32wba6/convert_wav.py
Normal file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import wave
|
||||
|
||||
|
||||
def convert_wav_to_pcm(input_file: str, output_file: str) -> bool:
|
||||
"""Convert a WAV file to raw unsigned 16-bit PCM."""
|
||||
|
||||
print(f"Converting {input_file} -> {output_file}")
|
||||
|
||||
try:
|
||||
with wave.open(input_file, "rb") as wav_file:
|
||||
channels = wav_file.getnchannels()
|
||||
sample_width = wav_file.getsampwidth()
|
||||
sample_rate = wav_file.getframerate()
|
||||
frames = wav_file.getnframes()
|
||||
|
||||
print(
|
||||
f"WAV: {channels}ch, {sample_width * 8}bit, {sample_rate}Hz, {frames} frames"
|
||||
)
|
||||
|
||||
if sample_width != 2:
|
||||
print("Error: only 16-bit WAV files are supported")
|
||||
return False
|
||||
|
||||
if sample_rate != 48000:
|
||||
print("Warning: SAI example expects 48 kHz PCM")
|
||||
|
||||
data = wav_file.readframes(frames)
|
||||
|
||||
with open(output_file, "wb") as pcm_file:
|
||||
if channels == 1:
|
||||
for (sample,) in struct.iter_unpack("<h", data):
|
||||
unsigned = (sample + 0x8000) & 0xFFFF
|
||||
pcm_file.write(struct.pack("<H", unsigned))
|
||||
else:
|
||||
step = channels
|
||||
samples = struct.iter_unpack("<h", data)
|
||||
index = 0
|
||||
for (sample,) in samples:
|
||||
if index % step == 0:
|
||||
unsigned = (sample + 0x8000) & 0xFFFF
|
||||
pcm_file.write(struct.pack("<H", unsigned))
|
||||
index += 1
|
||||
|
||||
print("Done")
|
||||
print(f"Size: {os.path.getsize(output_file)} bytes")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f"Error: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: convert_wav.py input.wav output.pcm")
|
||||
sys.exit(1)
|
||||
|
||||
input_file = sys.argv[1]
|
||||
output_file = sys.argv[2]
|
||||
|
||||
if not os.path.exists(input_file):
|
||||
print(f"Error: input file '{input_file}' not found")
|
||||
sys.exit(1)
|
||||
|
||||
ok = convert_wav_to_pcm(input_file, output_file)
|
||||
sys.exit(0 if ok else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
examples/stm32wba6/convert_wav.sh
Executable file
7
examples/stm32wba6/convert_wav.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Thin wrapper around convert_wav.py so one workflow works for bash users too.
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
exec python3 "$SCRIPT_DIR/convert_wav.py" "$@"
|
||||
387
examples/stm32wba6/src/bin/sdmmc_sai.rs
Normal file
387
examples/stm32wba6/src/bin/sdmmc_sai.rs
Normal file
@ -0,0 +1,387 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use defmt::*;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_stm32::gpio::{Level, Output, Speed};
|
||||
use embassy_stm32::sai::{self, Sai};
|
||||
use embassy_stm32::spi::{self, Spi};
|
||||
use embassy_stm32::time::Hertz;
|
||||
use embassy_stm32::{Config, peripherals};
|
||||
use embedded_hal_bus::spi::{ExclusiveDevice, NoDelay};
|
||||
use embedded_sdmmc::filesystem::ShortFileName;
|
||||
use embedded_sdmmc::{BlockDevice, RawFile, SdCard, TimeSource, VolumeIdx, VolumeManager};
|
||||
use static_cell::StaticCell;
|
||||
use {defmt_rtt as _, panic_probe as _};
|
||||
|
||||
// Simple SD card audio streaming example for SAI.
|
||||
// - Supports raw unsigned 16-bit PCM (.pcm)
|
||||
// - Supports 16-bit mono WAV at 48 kHz (.wav)
|
||||
|
||||
const VOLUME_NUM: u32 = 3;
|
||||
const VOLUME_DEN: u32 = 4;
|
||||
|
||||
fn scale_u16(sample: u16) -> u16 {
|
||||
((sample as u32) * VOLUME_NUM / VOLUME_DEN) as u16
|
||||
}
|
||||
|
||||
type SdSpiDev = ExclusiveDevice<
|
||||
Spi<'static, embassy_stm32::mode::Async, embassy_stm32::spi::mode::Master>,
|
||||
Output<'static>,
|
||||
NoDelay,
|
||||
>;
|
||||
|
||||
type SdCardDev = SdCard<SdSpiDev, embassy_time::Delay>;
|
||||
|
||||
struct DummyTimesource;
|
||||
impl embedded_sdmmc::TimeSource for DummyTimesource {
|
||||
fn get_timestamp(&self) -> embedded_sdmmc::Timestamp {
|
||||
embedded_sdmmc::Timestamp {
|
||||
year_since_1970: 0,
|
||||
zero_indexed_month: 0,
|
||||
zero_indexed_day: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WavInfo {
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
bits_per_sample: u16,
|
||||
data_offset: usize,
|
||||
}
|
||||
|
||||
fn parse_wav_header(buf: &[u8]) -> Option<WavInfo> {
|
||||
if buf.len() < 44 {
|
||||
return None;
|
||||
}
|
||||
if &buf[0..4] != b"RIFF" || &buf[8..12] != b"WAVE" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut fmt: Option<(u16, u16, u32, u16)> = None;
|
||||
let mut data_offset: Option<usize> = None;
|
||||
|
||||
let mut off = 12usize;
|
||||
while off + 8 <= buf.len() {
|
||||
let chunk_id = &buf[off..off + 4];
|
||||
let chunk_size = u32::from_le_bytes([buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7]]) as usize;
|
||||
let chunk_data = off + 8;
|
||||
|
||||
if chunk_id == b"fmt " && chunk_data + 16 <= buf.len() {
|
||||
let audio_format = u16::from_le_bytes([buf[chunk_data], buf[chunk_data + 1]]);
|
||||
let channels = u16::from_le_bytes([buf[chunk_data + 2], buf[chunk_data + 3]]);
|
||||
let sample_rate = u32::from_le_bytes([
|
||||
buf[chunk_data + 4],
|
||||
buf[chunk_data + 5],
|
||||
buf[chunk_data + 6],
|
||||
buf[chunk_data + 7],
|
||||
]);
|
||||
let bits_per_sample = u16::from_le_bytes([buf[chunk_data + 14], buf[chunk_data + 15]]);
|
||||
fmt = Some((audio_format, channels, sample_rate, bits_per_sample));
|
||||
}
|
||||
|
||||
if chunk_id == b"data" {
|
||||
data_offset = Some(chunk_data);
|
||||
break;
|
||||
}
|
||||
|
||||
let mut next = chunk_data.saturating_add(chunk_size);
|
||||
if next % 2 != 0 {
|
||||
next += 1;
|
||||
}
|
||||
if next <= off {
|
||||
break;
|
||||
}
|
||||
off = next;
|
||||
}
|
||||
|
||||
let (audio_format, channels, sample_rate, bits_per_sample) = fmt?;
|
||||
let data_offset = data_offset?;
|
||||
|
||||
if audio_format != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(WavInfo {
|
||||
sample_rate,
|
||||
channels,
|
||||
bits_per_sample,
|
||||
data_offset,
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_samples(sai_tx: &mut Sai<'static, peripherals::SAI1, u16>, samples: &[u16]) {
|
||||
if samples.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = sai_tx.write(samples).await {
|
||||
warn!("SAI write error: {:?}", defmt::Debug2Format(&e));
|
||||
}
|
||||
}
|
||||
|
||||
async fn play_pcm<D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>(
|
||||
sai_tx: &mut Sai<'static, peripherals::SAI1, u16>,
|
||||
volume_mgr: &mut VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
|
||||
file: RawFile,
|
||||
) where
|
||||
D: BlockDevice,
|
||||
T: TimeSource,
|
||||
{
|
||||
info!("Playing PCM file");
|
||||
|
||||
let mut buf = [0u8; 512];
|
||||
let mut out = [0u16; 256];
|
||||
|
||||
loop {
|
||||
let n = match volume_mgr.read(file, &mut buf) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
warn!("SD read error: {:?}", defmt::Debug2Format(&e));
|
||||
break;
|
||||
}
|
||||
};
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut count = 0usize;
|
||||
let mut i = 0usize;
|
||||
while i + 1 < n && count < out.len() {
|
||||
let sample = u16::from_le_bytes([buf[i], buf[i + 1]]);
|
||||
out[count] = scale_u16(sample);
|
||||
count += 1;
|
||||
i += 2;
|
||||
}
|
||||
|
||||
write_samples(sai_tx, &out[..count]).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn play_wav<D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>(
|
||||
sai_tx: &mut Sai<'static, peripherals::SAI1, u16>,
|
||||
volume_mgr: &mut VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
|
||||
file: RawFile,
|
||||
) where
|
||||
D: BlockDevice,
|
||||
T: TimeSource,
|
||||
{
|
||||
let mut header = [0u8; 512];
|
||||
let header_len = match volume_mgr.read(file, &mut header) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
warn!("SD read error: {:?}", defmt::Debug2Format(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let info = match parse_wav_header(&header[..header_len]) {
|
||||
Some(info) => info,
|
||||
None => {
|
||||
warn!("Invalid WAV header");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if info.sample_rate != 48_000 || info.channels != 1 || info.bits_per_sample != 16 {
|
||||
warn!(
|
||||
"Unsupported WAV format: {} Hz, {} ch, {} bits",
|
||||
info.sample_rate, info.channels, info.bits_per_sample
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Playing WAV file: 48 kHz mono 16-bit");
|
||||
|
||||
let mut out = [0u16; 256];
|
||||
let mut buf = [0u8; 512];
|
||||
|
||||
if info.data_offset > header_len {
|
||||
let mut remaining = info.data_offset - header_len;
|
||||
while remaining > 0 {
|
||||
let to_read = remaining.min(buf.len());
|
||||
let n = match volume_mgr.read(file, &mut buf[..to_read]) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
warn!("SD read error: {:?}", defmt::Debug2Format(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if n == 0 {
|
||||
warn!("Unexpected end of file while seeking data");
|
||||
return;
|
||||
}
|
||||
remaining -= n;
|
||||
}
|
||||
} else if info.data_offset < header_len {
|
||||
let mut i = info.data_offset;
|
||||
let mut count = 0usize;
|
||||
while i + 1 < header_len && count < out.len() {
|
||||
let signed = i16::from_le_bytes([header[i], header[i + 1]]);
|
||||
let unsigned = (signed as i32 + 0x8000) as u16;
|
||||
out[count] = scale_u16(unsigned);
|
||||
count += 1;
|
||||
i += 2;
|
||||
}
|
||||
write_samples(sai_tx, &out[..count]).await;
|
||||
}
|
||||
|
||||
loop {
|
||||
let n = match volume_mgr.read(file, &mut buf) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
warn!("SD read error: {:?}", defmt::Debug2Format(&e));
|
||||
break;
|
||||
}
|
||||
};
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut count = 0usize;
|
||||
let mut i = 0usize;
|
||||
while i + 1 < n && count < out.len() {
|
||||
let signed = i16::from_le_bytes([buf[i], buf[i + 1]]);
|
||||
let unsigned = (signed as i32 + 0x8000) as u16;
|
||||
out[count] = scale_u16(unsigned);
|
||||
count += 1;
|
||||
i += 2;
|
||||
}
|
||||
|
||||
write_samples(sai_tx, &out[..count]).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
let mut config = Config::default();
|
||||
{
|
||||
use embassy_stm32::rcc::*;
|
||||
config.rcc.pll1 = Some(Pll {
|
||||
source: PllSource::HSI,
|
||||
prediv: PllPreDiv::DIV1,
|
||||
mul: PllMul::MUL12,
|
||||
divq: Some(PllDiv::DIV4),
|
||||
divr: Some(PllDiv::DIV5),
|
||||
divp: Some(PllDiv::DIV30),
|
||||
frac: Some(2363),
|
||||
});
|
||||
|
||||
config.rcc.ahb_pre = AHBPrescaler::DIV1;
|
||||
config.rcc.apb1_pre = APBPrescaler::DIV1;
|
||||
config.rcc.apb2_pre = APBPrescaler::DIV1;
|
||||
config.rcc.apb7_pre = APBPrescaler::DIV1;
|
||||
config.rcc.ahb5_pre = AHB5Prescaler::DIV2;
|
||||
config.rcc.voltage_scale = VoltageScale::RANGE1;
|
||||
|
||||
config.rcc.mux.sai1sel = mux::Sai1sel::PLL1_Q;
|
||||
}
|
||||
|
||||
let p = embassy_stm32::init(config);
|
||||
|
||||
info!("SDMMC SAI example");
|
||||
|
||||
let (sai_a, _sai_b) = sai::split_subblocks(p.SAI1);
|
||||
|
||||
static SAI_DMA_BUF: StaticCell<[u16; 4096]> = StaticCell::new();
|
||||
let sai_dma_buf = SAI_DMA_BUF.init([0u16; 4096]);
|
||||
|
||||
let mut sai_cfg = sai::Config::default();
|
||||
sai_cfg.mode = sai::Mode::Master;
|
||||
sai_cfg.tx_rx = sai::TxRx::Transmitter;
|
||||
sai_cfg.stereo_mono = sai::StereoMono::Mono;
|
||||
sai_cfg.data_size = sai::DataSize::Data16;
|
||||
sai_cfg.bit_order = sai::BitOrder::MsbFirst;
|
||||
sai_cfg.slot_size = sai::SlotSize::Channel32;
|
||||
sai_cfg.slot_count = sai::word::U4(2);
|
||||
sai_cfg.slot_enable = 0b11;
|
||||
sai_cfg.first_bit_offset = sai::word::U5(0);
|
||||
sai_cfg.frame_sync_polarity = sai::FrameSyncPolarity::ActiveLow;
|
||||
sai_cfg.frame_sync_offset = sai::FrameSyncOffset::BeforeFirstBit;
|
||||
sai_cfg.frame_length = 32;
|
||||
sai_cfg.frame_sync_active_level_length = sai::word::U7(16);
|
||||
sai_cfg.fifo_threshold = sai::FifoThreshold::Quarter;
|
||||
sai_cfg.master_clock_divider = sai::MasterClockDivider::DIV4;
|
||||
|
||||
let mut sai_tx = Sai::new_asynchronous(sai_a, p.PA7, p.PB14, p.PA8, p.GPDMA1_CH2, sai_dma_buf, sai_cfg);
|
||||
|
||||
let _max98357a_sd = Output::new(p.PA1, Level::High, Speed::Low);
|
||||
|
||||
let mut spi_cfg = spi::Config::default();
|
||||
spi_cfg.frequency = Hertz(400_000);
|
||||
let spi = Spi::new(p.SPI1, p.PB4, p.PA15, p.PB3, p.GPDMA1_CH0, p.GPDMA1_CH1, spi_cfg);
|
||||
let cs = Output::new(p.PA6, Level::High, Speed::VeryHigh);
|
||||
let spi_dev: SdSpiDev = ExclusiveDevice::new_no_delay(spi, cs).unwrap();
|
||||
let sd: SdCardDev = SdCard::new(spi_dev, embassy_time::Delay);
|
||||
|
||||
embassy_time::Timer::after_millis(50).await;
|
||||
sd.spi(|dev| {
|
||||
let dummy = [0xFFu8; 10];
|
||||
let _ = dev.bus_mut().write(&dummy);
|
||||
});
|
||||
|
||||
info!("SD size {} bytes", sd.num_bytes().unwrap_or(0));
|
||||
|
||||
let mut spi_cfg2 = spi::Config::default();
|
||||
spi_cfg2.frequency = Hertz(8_000_000);
|
||||
let _ = sd.spi(|dev| dev.bus_mut().set_config(&spi_cfg2));
|
||||
|
||||
static VOLUME_MANAGER: StaticCell<VolumeManager<SdCardDev, DummyTimesource>> = StaticCell::new();
|
||||
let vol_mgr: &'static mut VolumeManager<SdCardDev, DummyTimesource> =
|
||||
VOLUME_MANAGER.init(VolumeManager::new(sd, DummyTimesource));
|
||||
|
||||
let raw_vol = match vol_mgr.open_raw_volume(VolumeIdx(0)) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
warn!("SD open_raw_volume error: {:?}", defmt::Debug2Format(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let raw_root = match vol_mgr.open_root_dir(raw_vol) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!("SD open_root_dir error: {:?}", defmt::Debug2Format(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut names: heapless::Vec<ShortFileName, 16> = heapless::Vec::new();
|
||||
let _ = vol_mgr.iterate_dir(raw_root, |de| {
|
||||
if !de.attributes.is_directory() {
|
||||
let ext = de.name.extension();
|
||||
if ext == b"PCM" || ext == b"WAV" {
|
||||
let _ = names.push(de.name.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if names.is_empty() {
|
||||
warn!("No .pcm or .wav files in SD root");
|
||||
return;
|
||||
}
|
||||
|
||||
let name = &names[0];
|
||||
info!("Playing {}", defmt::Debug2Format(name));
|
||||
|
||||
let file = match vol_mgr.open_file_in_dir(raw_root, name, embedded_sdmmc::Mode::ReadOnly) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
warn!("SD open_file error: {:?}", defmt::Debug2Format(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let raw_file = file;
|
||||
if name.extension() == b"PCM" {
|
||||
play_pcm(&mut sai_tx, vol_mgr, raw_file).await;
|
||||
} else {
|
||||
play_wav(&mut sai_tx, vol_mgr, raw_file).await;
|
||||
}
|
||||
|
||||
let _ = vol_mgr.close_file(file);
|
||||
|
||||
let _ = spawner;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user