Develop the full Daisy Pod spike so it can be flashed the moment the board arrives. Architecture: one shared engine, two front-ends. - pm-synth: make it `#![no_std]` (mirroring track-format), routing float math through `libm` so the SAME f32 code runs on the host and on the Daisy's Cortex-M7F (hardware FPU — no fixed-point port needed). Add `Player`, a self-running sequencer that owns the Synth + scheduled clicks and renders sample-by-sample, looping at the pattern boundary. Integer-only hot path (clicks pre-resolved to sample indices); exposes a `fired()` beat counter. Add SPIKE_PROGRAM/SPIKE_BARS as the shared source of truth. - synthrender: render the SAME Player to pm-daisy-preview.wav — the host-side "simulator". Bit-identical preview of the hardware output (before its codec); far more useful than chip emulation (Renode can't model the audio codec). - pm-daisy (new, workspace-excluded firmware): thin BSP binary for the Daisy Seed/Pod. embedded-alloc heap + board bring-up + SAI-DMA audio interrupt feeding Player::next_sample() into stereo frames, USER LED flashing per click. Audio loop follows the `daisy` crate's examples/audio.rs. Board revision (codec) is a Cargo feature; README documents matching it + both flash paths (probe-rs/RTT and USB DFU) + the QSPI-bootloader fallback. Verified without hardware: host build + preview render (48 kHz, onsets on the 8th-note grid at 124 BPM); firmware cross-compiles + links for thumbv7em-none- eabihf at ~87 KB (fits the 128 KB internal flash) across all three codec revisions; track-format conformance + `node tests/run.mjs` (47 pass) still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.9 KiB
Rust
121 lines
4.9 KiB
Rust
//! 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<RefCell<Option<Player>>> = Mutex::new(RefCell::new(None));
|
|
static AUDIO_INTERFACE: Mutex<RefCell<Option<audio::Interface>>> = 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<u8>; 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);
|
|
}
|
|
});
|
|
}
|