//! PM Daisy-Pod spike — play the PolyMeter click engine on real hardware. //! //! Drives the shared [`pm_synth::Player`] (the SAME sequencer the host `synthrender` renders to //! `pm-daisy-preview.wav`) from the Daisy Seed's SAI audio DMA interrupt, out the Daisy Pod's audio //! jack. The engine is mono; both stereo channels get the same sample. The Seed's USER LED flashes //! on every click as a beat indicator. //! //! This is a digital/DSP-home experiment (see `docs/daisy-spike.md`), not the heirloom analog path. //! The audio loop structure follows the `daisy` crate's `examples/audio.rs`. //! //! Build/flash: see `build.sh` and `README.md`. The board REVISION feature (Cargo.toml) must match //! your Seed's codec or you get silence. #![no_main] #![no_std] extern crate alloc; use core::cell::RefCell; use core::mem::MaybeUninit; use core::sync::atomic::{AtomicU32, Ordering}; use cortex_m::asm; use cortex_m::interrupt::Mutex; use cortex_m_rt::entry; use defmt_rtt as _; use panic_probe as _; use embedded_alloc::LlffHeap as Heap; use hal::pac::{self, interrupt}; use stm32h7xx_hal as hal; use daisy::audio; use pm_synth::{Player, SPIKE_BARS, SPIKE_PROGRAM}; #[global_allocator] static HEAP: Heap = Heap::empty(); /// The running groove and the audio peripheral live in globals so the DMA audio interrupt can reach /// them. Both are installed once in `main` (inside a critical section) before audio starts. static PLAYER: Mutex>> = Mutex::new(RefCell::new(None)); static AUDIO_INTERFACE: Mutex>> = Mutex::new(RefCell::new(None)); /// Total clicks fired, published from the audio IRQ. The main loop watches it to flash the beat LED. static FIRED: AtomicU32 = AtomicU32::new(0); #[entry] fn main() -> ! { // Heap first: `Player::new` parses the program into Vec/String, and each `trigger()` allocates a // voice. 64 KB lives comfortably in the 128 KB DTCM. (Allocating inside the audio IRQ is a known // real-time smell — fine for the spike; see docs/daisy-spike.md "Decision criteria".) { const HEAP_SIZE: usize = 64 * 1024; static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) } } let mut cp = cortex_m::Peripherals::take().unwrap(); let dp = pac::Peripherals::take().unwrap(); // Caches give a major DSP performance boost; the `daisy` crate handles cache management around // the audio DMA buffers for us. cp.SCB.enable_icache(); cp.SCB.enable_dcache(&mut cp.CPUID); // Board bring-up (clocks, GPIO, LED, audio) via the daisy crate's macros. let board = daisy::Board::take().unwrap(); let ccdr = daisy::board_freeze_clocks!(board, dp); let pins = daisy::board_split_gpios!(board, ccdr, dp); let mut led_user = daisy::board_split_leds!(pins).USER; let audio_interface = daisy::board_split_audio!(ccdr, pins); // Build the groove from the shared spike pattern. let track = track_format::parse(SPIKE_PROGRAM); let player = Player::new(&track, SPIKE_BARS); defmt::info!("pm-daisy: playing `{}` ({=i64} bars), heap {=usize} B free", SPIKE_PROGRAM, SPIKE_BARS, HEAP.free()); // Start audio and hand the peripheral + player to the interrupt. let audio_interface = audio_interface.spawn().unwrap(); cortex_m::interrupt::free(|cs| { PLAYER.borrow(cs).replace(Some(player)); AUDIO_INTERFACE.borrow(cs).replace(Some(audio_interface)); }); // Beat LED: a ~25 ms flash whenever the click count advances. Audio is fully interrupt-driven, // so blocking the main loop with `asm::delay` is harmless. let ms = ccdr.clocks.sys_ck().to_Hz() / 1000; let mut last = 0u32; loop { let fired = FIRED.load(Ordering::Relaxed); if fired != last { last = fired; led_user.set_high(); asm::delay(ms * 25); led_user.set_low(); } asm::delay(ms); // ~1 ms poll cadence } } /// Audio is transferred to/from the codec periodically over DMA. When a transfer completes, the /// DMA1 Stream 1 interrupt fires asking for the next block — we fill it from the engine. #[interrupt] fn DMA1_STR1() { cortex_m::interrupt::free(|cs| { let mut ai = AUDIO_INTERFACE.borrow(cs).borrow_mut(); let mut pl = PLAYER.borrow(cs).borrow_mut(); if let (Some(audio_interface), Some(player)) = (ai.as_mut(), pl.as_mut()) { audio_interface .handle_interrupt_dma1_str1(|audio_buffer| { for frame in audio_buffer { let s = player.next_sample(); // mono engine sample *frame = (s, s); // → left + right } }) .unwrap(); // Publish the click count for the main-loop beat LED. FIRED.store(player.fired(), Ordering::Relaxed); } }); }