This commit is contained in:
Nathan Rashleigh committed 2026-07-23 17:20:31 +10:00
1 parent 295391b119
commit c523f504cc
12 files changed
+1022 -553

No files matched your search

+176 -2
View File
@@ -1,3 +1,177 @@
fn main() {
println!("Hello, world!");
//! 400 Hz sawtooth out the F411E-DISCO's 3.5 mm jack.
//!
//! The jack is driven by the on-board CS43L22 codec (see `f411::codec` for
//! the control interface and wiring). I2S3 runs master transmit at 48 kHz
//! with MCLK (256×fs) enabled; the main loop streams a 400 Hz sawtooth
//! (48 000 / 400 = 120 samples per cycle) into both channels.
//!
//! The USER button (B1, PA0, high = pressed) toggles the tone via EXTI0: the
//! interrupt only sets a flag, the audio loop applies it (with a debounce
//! hold-off), so no GPIO polling or I2C traffic happens in the sample path.
//! While off, silence is streamed so the codec keeps its clocks.
#![no_std]
#![no_main]
// defmt global logger (RTT transport) + panic handler
use defmt_rtt as _;
use panic_probe as _;
use core::cell::RefCell;
use core::sync::atomic::{AtomicBool, Ordering};
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
// the HAL is re-exported by the f411 board-support crate as `f411::hal`
use f411::{
codec::Cs43l22,
led::LedCompass,
hal::{
gpio::{gpioa, gpioa::PA0, gpioc, Edge, Input},
i2c::{I2c, Mode},
i2s::stm32_i2s_v12x::transfer::*,
i2s::I2s,
nb::block,
pac,
pac::interrupt,
prelude::*,
rcc::{Config, Rcc},
},
};
// ---- tone ----
const SAW_HZ: u32 = 400;
const SAMPLE_RATE_HZ: u32 = 48_000;
const SAMPLES_PER_CYCLE: u32 = SAMPLE_RATE_HZ / SAW_HZ; // 120
// Full-scale is painfully loud on headphones; run the saw at ~1/8 amplitude.
const AMPLITUDE: i32 = i16::MAX as i32 / 8;
// ---- button ----
// After a toggle, ignore further presses for ~20 ms of samples (bounce).
const DEBOUNCE_SAMPLES: u32 = SAMPLE_RATE_HZ / 50;
/// Button pin, parked here so the EXTI0 handler can clear its pending bit.
static BUTTON: Mutex<RefCell<Option<PA0<Input>>>> = Mutex::new(RefCell::new(None));
/// Set by EXTI0 on a press edge, consumed by the audio loop.
static TOGGLE_REQ: AtomicBool = AtomicBool::new(false);
type AudioOut = I2sTransfer<I2s<pac::SPI3>, Master, Transmit, Philips, Data16Channel16>;
/// I2S3 master transmit, 16-bit stereo, MCLK out for the codec.
fn audio_out(
spi: pac::SPI3,
pins: (gpioa::PA4, gpioc::PC10, Option<gpioc::PC7>, gpioc::PC12),
rcc: &mut Rcc,
) -> AudioOut {
let i2s = I2s::new(spi, pins, rcc);
let config = I2sTransferConfig::new_master()
.transmit()
.standard(Philips)
.data_format(Data16Channel16)
.master_clock(true)
.request_frequency(SAMPLE_RATE_HZ);
I2sTransfer::new(i2s, config)
}
/// Sample `i` (of SAMPLES_PER_CYCLE) of the sawtooth: a linear ramp from
/// -AMPLITUDE to +AMPLITUDE, wrapping each cycle.
fn saw_sample(i: u32) -> i16 {
(i as i32 * 2 * AMPLITUDE / SAMPLES_PER_CYCLE as i32 - AMPLITUDE) as i16
}
#[entry]
fn main() -> ! {
let cp = cortex_m::Peripherals::take().unwrap();
let mut p = pac::Peripherals::take().unwrap();
// PLLI2S tuned to 61.44 MHz so 48 kHz divides exactly (61.44 MHz / 256fs / 5).
let mut rcc = p
.RCC
.freeze(Config::hsi().sysclk(96.MHz()).i2s_clk(61440.kHz()));
let mut delay = cp.SYST.delay(&rcc.clocks);
let gpioa = p.GPIOA.split(&mut rcc);
let gpiob = p.GPIOB.split(&mut rcc);
let gpioc = p.GPIOC.split(&mut rcc);
let gpiod = p.GPIOD.split(&mut rcc);
// USER button (B1): rising-edge interrupt on EXTI0.
let mut syscfg = p.SYSCFG.constrain(&mut rcc);
let mut button = gpioa.pa0.into_pull_down_input();
button.make_interrupt_source(&mut syscfg);
button.trigger_on_edge(&mut p.EXTI, Edge::Rising);
button.enable_interrupt(&mut p.EXTI);
cortex_m::interrupt::free(|cs| BUTTON.borrow(cs).replace(Some(button)));
unsafe { pac::NVIC::unmask(pac::Interrupt::EXTI0) };
// CS43L22 codec: control port + reset via the board-support driver.
let i2c = I2c::new(
p.I2C1,
(gpiob.pb6, gpiob.pb9),
Mode::standard(100.kHz()),
&mut rcc,
);
let mut codec = Cs43l22::new(i2c, gpiod.pd4.into_push_pull_output());
match codec.init(&mut delay) {
Ok(id) => defmt::info!("codec up, ID reg {=u8:#04x}", id),
Err(e) => defmt::error!("codec init failed: {}", defmt::Debug2Format(&e)),
}
let mut transfer = audio_out(
p.SPI3,
(gpioa.pa4, gpioc.pc10, Some(gpioc.pc7), gpioc.pc12),
&mut rcc,
);
defmt::info!(
"saw: {} Hz, sample rate {} Hz ({} samples/cycle)",
SAW_HZ,
transfer.sample_rate(),
SAMPLES_PER_CYCLE
);
// On-board LEDs: south (LD6, blue) = 1 Hz heartbeat so it's visible the
// firmware is alive without a debugger; west (LD4, green) = saw playing.
let compass = LedCompass::new(gpiod.pd12, gpiod.pd13, gpiod.pd14, gpiod.pd15);
let mut heartbeat = compass.s;
let mut saw_led = compass.w;
let mut i = 0;
let mut frames: u32 = 0;
let mut on = false; // silent at boot; USER button toggles the saw on
let mut holdoff: u32 = 0;
loop {
// Consume any press flag every frame (so bounce during the hold-off
// window is discarded), apply it only outside the window.
holdoff = holdoff.saturating_sub(1);
if TOGGLE_REQ.swap(false, Ordering::AcqRel) && holdoff == 0 {
on = !on;
holdoff = DEBOUNCE_SAMPLES;
saw_led.set(on);
defmt::info!("saw {}", if on { "ON" } else { "OFF" });
}
let sample = match on {
true => saw_sample(i),
false => 0,
};
block!(transfer.write((sample, sample))).unwrap();
i = (i + 1) % SAMPLES_PER_CYCLE;
frames += 1;
if frames == SAMPLE_RATE_HZ {
frames = 0;
heartbeat.toggle();
}
}
}
#[interrupt]
fn EXTI0() {
cortex_m::interrupt::free(|cs| {
if let Some(pin) = BUTTON.borrow(cs).borrow_mut().as_mut() {
pin.clear_interrupt_pending_bit();
}
});
TOGGLE_REQ.store(true, Ordering::Release);
}