This commit is contained in:
2025-09-30 20:27:07 -06:00
parent f73df7e0b8
commit d8a5ab465e
13 changed files with 285 additions and 7 deletions

19
Cargo.lock generated
View File

@@ -828,6 +828,14 @@ dependencies = [
"embedded-io-async",
]
[[package]]
name = "embedded-audio"
version = "0.1.0"
source = "git+https://github.com/LegitCamper/embedded-audio#087784644d810b94dd659a03dbed4795dfb0bd24"
dependencies = [
"heapless",
]
[[package]]
name = "embedded-graphics"
version = "0.8.1"
@@ -1343,6 +1351,7 @@ dependencies = [
"goblin",
"heapless",
"kolibri-embedded-gui",
"micromath",
"num_enum 0.7.4",
"once_cell",
"panic-probe",
@@ -2516,6 +2525,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wav_player"
version = "0.1.0"
dependencies = [
"abi",
"embedded-audio",
"embedded-graphics",
"rand",
]
[[package]]
name = "winapi"
version = "0.3.9"

View File

@@ -7,6 +7,7 @@ members = [
"user-apps/calculator",
"user-apps/snake",
"user-apps/gallery",
"user-apps/wav_player",
]
[profile.release]

View File

@@ -1,7 +1,10 @@
#![no_std]
pub use abi_sys::{
AUDIO_BUFFER_LEN, audio_buffer_ready, file_len, get_key, list_dir, lock_display, print,
read_file, send_audio_buffer, sleep,
};
use abi_sys::{RngRequest, draw_iter, gen_rand};
pub use abi_sys::{file_len, get_key, list_dir, lock_display, print, read_file, sleep};
use rand_core::RngCore;
pub use shared::keyboard::{KeyCode, KeyEvent, KeyState, Modifiers};
use talc::*;

View File

@@ -31,6 +31,8 @@ pub enum CallAbiTable {
ListDir = 6,
ReadFile = 7,
FileLen = 8,
AudioBufferReady = 9,
SendAudioBuffer = 10,
}
pub type PrintAbi = extern "C" fn(ptr: *const u8, len: usize);
@@ -145,3 +147,27 @@ pub fn file_len(file: &str) -> usize {
f(file.as_ptr(), file.len())
}
}
pub type AudioBufferReady = extern "C" fn() -> bool;
#[allow(unused)]
pub fn audio_buffer_ready() -> bool {
unsafe {
let ptr = CALL_ABI_TABLE[CallAbiTable::AudioBufferReady as usize];
let f: AudioBufferReady = core::mem::transmute(ptr);
f()
}
}
pub const AUDIO_BUFFER_LEN: usize = 1024;
pub type SendAudioBuffer = extern "C" fn(ptr: *const u8, len: usize);
#[allow(unused)]
pub fn send_audio_buffer(buf: &[u8; AUDIO_BUFFER_LEN]) {
unsafe {
let ptr = CALL_ABI_TABLE[CallAbiTable::SendAudioBuffer as usize];
let f: SendAudioBuffer = core::mem::transmute(ptr);
f(buf.as_ptr(), buf.len())
}
}

View File

@@ -13,3 +13,4 @@ userapps:
just userapp calculator
just userapp snake
just userapp gallery
just userapp wav_player

View File

@@ -94,3 +94,4 @@ bumpalo = "3.19.0"
shared = { path = "../shared" }
abi_sys = { path = "../abi_sys" }
micromath = "2.1.0"

View File

@@ -1,16 +1,17 @@
use abi_sys::{
DrawIterAbi, FileLen, GenRand, GetKeyAbi, ListDir, LockDisplay, Modifiers, PrintAbi, ReadFile,
RngRequest, SleepAbi,
AUDIO_BUFFER_LEN, AudioBufferReady, DrawIterAbi, FileLen, GenRand, GetKeyAbi, ListDir,
LockDisplay, Modifiers, PrintAbi, ReadFile, RngRequest, SendAudioBuffer, SleepAbi,
};
use alloc::{string::ToString, vec::Vec};
use core::sync::atomic::Ordering;
use embassy_rp::clocks::{RoscRng, clk_sys_freq};
use embedded_graphics::{Pixel, draw_target::DrawTarget, pixelcolor::Rgb565};
use embedded_sdmmc::{DirEntry, LfnBuffer, ShortFileName};
use embedded_sdmmc::{DirEntry, LfnBuffer};
use heapless::spsc::Queue;
use shared::keyboard::KeyEvent;
use crate::{
audio::{AUDIO_BUFFER, AUDIO_BUFFER_READY},
display::{FB_PAUSED, FRAMEBUFFER},
storage::{Dir, File, SDCARD},
};
@@ -181,7 +182,9 @@ pub extern "C" fn read_file(
if !file.is_empty() {
sd.access_root_dir(|root| {
if let Ok(result) = recurse_file(&root, &file[1..], |file| {
file.seek_from_start(start_from as u32).unwrap();
if file.offset() as usize != start_from {
file.seek_from_start(start_from as u32).unwrap();
}
file.read(&mut buf).unwrap()
}) {
read = result
@@ -210,3 +213,21 @@ pub extern "C" fn file_len(str: *const u8, len: usize) -> usize {
}
len as usize
}
const _: AudioBufferReady = audio_buffer_ready;
pub extern "C" fn audio_buffer_ready() -> bool {
AUDIO_BUFFER_READY.load(Ordering::Acquire)
}
const _: SendAudioBuffer = send_audio_buffer;
pub extern "C" fn send_audio_buffer(ptr: *const u8, len: usize) {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let buf = unsafe { core::slice::from_raw_parts(ptr, len) };
while !AUDIO_BUFFER_READY.load(Ordering::Acquire) {}
if buf.len() == AUDIO_BUFFER_LEN {
unsafe { AUDIO_BUFFER.copy_from_slice(buf) };
AUDIO_BUFFER_READY.store(false, Ordering::Release)
}
}

52
kernel/src/audio.rs Normal file
View File

@@ -0,0 +1,52 @@
use core::{
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use embassy_rp::{
Peri,
pio::Pio,
pio_programs::pwm::{PioPwm, PioPwmProgram},
pwm::{Config, Pwm, SetDutyCycle},
};
use embassy_time::Timer;
use crate::{Audio, Irqs};
const AUDIO_BUFFER_LEN: usize = 1024;
const _: () = assert!(AUDIO_BUFFER_LEN == abi_sys::AUDIO_BUFFER_LEN);
pub static mut AUDIO_BUFFER: [u8; AUDIO_BUFFER_LEN] = [0; AUDIO_BUFFER_LEN];
static mut AUDIO_BUFFER_1: [u8; AUDIO_BUFFER_LEN] = [0; AUDIO_BUFFER_LEN];
pub static AUDIO_BUFFER_READY: AtomicBool = AtomicBool::new(true);
pub const SAMPLE_RATE_HZ: u32 = 22_050;
#[embassy_executor::task]
pub async fn audio_handler(audio: Audio) {
let var_name = Pio::new(audio.pio_left, Irqs);
let Pio {
mut common, sm0, ..
} = var_name;
let prg = PioPwmProgram::new(&mut common);
let mut pwm_pio = PioPwm::new(&mut common, sm0, audio.left, &prg);
let period = Duration::from_nanos(1_000_000_000 / SAMPLE_RATE_HZ as u64);
pwm_pio.set_period(period);
pwm_pio.start();
let sample_interval = 1_000_000 / SAMPLE_RATE_HZ as u64; // in µs ≈ 45 µs
loop {
for &sample in unsafe { &AUDIO_BUFFER }.iter() {
let period_ns = period.as_nanos() as u32;
let duty_ns = period_ns * (sample as u32) / 255;
pwm_pio.write(Duration::from_nanos(duty_ns as u64));
Timer::after_micros(sample_interval).await; // sample interval = 1 / sample rate
}
unsafe { core::mem::swap(&mut AUDIO_BUFFER, &mut AUDIO_BUFFER_1) };
AUDIO_BUFFER_READY.store(true, Ordering::Release)
}
}

View File

@@ -204,6 +204,8 @@ fn patch_abi(
CallAbiTable::ListDir => abi::list_dir as usize,
CallAbiTable::ReadFile => abi::read_file as usize,
CallAbiTable::FileLen => abi::file_len as usize,
CallAbiTable::AudioBufferReady => abi::audio_buffer_ready as usize,
CallAbiTable::SendAudioBuffer => abi::send_audio_buffer as usize,
};
unsafe {
table_base.add(idx as usize).write(ptr);

View File

@@ -7,6 +7,7 @@
extern crate alloc;
mod abi;
mod audio;
mod display;
mod elf;
mod framebuffer;
@@ -19,6 +20,7 @@ mod utils;
use crate::{
abi::KEY_CACHE,
audio::audio_handler,
display::{FRAMEBUFFER, display_handler, init_display},
peripherals::{
conf_peripherals,
@@ -48,8 +50,10 @@ use embassy_rp::{
multicore::{Stack, spawn_core1},
peripherals::{
DMA_CH0, DMA_CH1, I2C1, PIN_6, PIN_7, PIN_10, PIN_11, PIN_12, PIN_13, PIN_14, PIN_15,
PIN_16, PIN_17, PIN_18, PIN_19, PIN_22, SPI0, SPI1, USB,
PIN_16, PIN_17, PIN_18, PIN_19, PIN_22, PIN_26, PIN_27, PIO0, PIO1, PWM_SLICE5, SPI0, SPI1,
USB,
},
pio,
spi::{self, Spi},
usb as embassy_rp_usb,
};
@@ -65,6 +69,7 @@ use talc::*;
embassy_rp::bind_interrupts!(struct Irqs {
I2C1_IRQ => i2c::InterruptHandler<I2C1>;
USBCTRL_IRQ => embassy_rp_usb::InterruptHandler<USB>;
PIO0_IRQ_0 => pio::InterruptHandler<PIO0>;
});
static mut CORE1_STACK: Stack<16384> = Stack::new();
@@ -105,6 +110,12 @@ async fn main(_spawner: Spawner) {
data: p.PIN_14,
reset: p.PIN_15,
};
let audio = Audio {
pio_left: p.PIO0,
pio_right: p.PIO1,
left: p.PIN_26,
right: p.PIN_27,
};
let sd = Sd {
spi: p.SPI0,
clk: p.PIN_18,
@@ -119,7 +130,9 @@ async fn main(_spawner: Spawner) {
data: p.PIN_6,
};
let executor0 = EXECUTOR0.init(Executor::new());
executor0.run(|spawner| unwrap!(spawner.spawn(kernel_task(spawner, display, sd, mcu, p.USB))));
executor0.run(|spawner| {
unwrap!(spawner.spawn(kernel_task(spawner, display, audio, sd, mcu, p.USB)))
});
}
// One-slot channel to pass EntryFn from core1
@@ -168,6 +181,12 @@ struct Display {
data: Peri<'static, PIN_14>,
reset: Peri<'static, PIN_15>,
}
struct Audio {
pio_left: Peri<'static, PIO0>,
pio_right: Peri<'static, PIO1>,
left: Peri<'static, PIN_26>,
right: Peri<'static, PIN_27>,
}
struct Sd {
spi: Peri<'static, SPI0>,
clk: Peri<'static, PIN_18>,
@@ -225,6 +244,7 @@ async fn setup_sd(sd: Sd) {
async fn kernel_task(
spawner: Spawner,
display: Display,
audio: Audio,
sd: Sd,
mcu: Mcu,
usb: Peri<'static, USB>,
@@ -234,6 +254,8 @@ async fn kernel_task(
setup_display(display, spawner).await;
setup_sd(sd).await;
spawner.spawn(audio_handler(audio)).unwrap();
let _usb = embassy_rp_usb::Driver::new(usb, Irqs);
// spawner.spawn(usb_handler(usb)).unwrap();

View File

@@ -0,0 +1,10 @@
[package]
name = "wav_player"
version = "0.1.0"
edition = "2024"
[dependencies]
abi = { path = "../../abi" }
embedded-graphics = "0.8.1"
rand = { version = "0.9.0", default-features = false }
embedded-audio = { git = "https://github.com/LegitCamper/embedded-audio" }

View File

@@ -0,0 +1,28 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
// Put `memory.x` in our output directory and ensure it's
// on the linker search path.
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("../memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
println!("cargo:rustc-link-arg-bins=-Tmemory.x");
}

View File

@@ -0,0 +1,92 @@
#![no_std]
#![no_main]
extern crate alloc;
use abi::{
AUDIO_BUFFER_LEN, KeyCode, KeyState, Rng, audio_buffer_ready,
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
file_len, get_key, lock_display, print, read_file, send_audio_buffer, sleep,
};
use alloc::{format, string::String};
use core::panic::PanicInfo;
use embedded_audio::{AudioFile, PlatformFile, PlatformFileError, wav::Wav};
use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
print(&format!(
"user panic: {} @ {:?}",
info.message(),
info.location(),
));
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
print("Starting Wav player app");
let mut display = Display;
let mut buf = [0_u8; AUDIO_BUFFER_LEN];
let file = File::new(String::from("/music/test.wav"));
let mut wav = Wav::new(file).unwrap();
loop {
if audio_buffer_ready() {
if wav.is_eof() {
wav.restart().unwrap()
}
wav.read(&mut buf).unwrap();
send_audio_buffer(&buf);
}
}
}
struct File {
current_pos: usize,
file: String,
}
impl File {
fn new(file: String) -> Self {
Self {
current_pos: 0,
file,
}
}
}
impl PlatformFile for File {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, PlatformFileError> {
let read = read_file(&self.file, self.current_pos, buf);
Ok(read)
}
fn seek_from_current(&mut self, offset: i64) -> Result<(), PlatformFileError> {
if offset.is_positive() {
self.current_pos += offset as usize;
} else {
self.current_pos -= offset as usize;
}
Ok(())
}
fn seek_from_start(&mut self, offset: usize) -> Result<(), PlatformFileError> {
self.current_pos = offset;
Ok(())
}
fn seek_from_end(&mut self, offset: usize) -> Result<(), PlatformFileError> {
self.current_pos = self.length() - offset;
Ok(())
}
fn length(&mut self) -> usize {
file_len(&self.file)
}
}