Add I2S3/DMA audio output driver for MAX98357A/PCM5102A class-D amps: - audio_init(): PLLI2S N=192/R=2 → 96 MHz → FS≈22058 Hz (<0.04% error), GPIO PC10/PA15/PB5 (AF6), PC5 mute, DMA1_Stream7_Ch0 circular, HAL_I2S_Transmit_DMA ping-pong, 441-sample half-buffers (20 ms each) - Square-wave tone generator (ISR-safe, integer volume scaling 0-100) - Tone sequencer: STARTUP/ARM/DISARM/FAULT/BEEP sequences via audio_tick() - PCM FIFO (4096 samples, SPSC ring): receives Jetson audio via JLink - JLink protocol: JLINK_CMD_AUDIO = 0x08, JLINK_MAX_PAYLOAD 64→252 bytes (supports 126 int16 samples/frame = 5.7 ms @22050 Hz) - main.c: audio_init(), STARTUP tone on boot, ARM/FAULT tones, audio_tick() - config.h: AUDIO_BCLK/LRCK/DOUT/MUTE pin defines + PLLI2S constants - test_audio.py: 45 tests, all passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
107 lines
3.4 KiB
C
107 lines
3.4 KiB
C
#ifndef AUDIO_H
|
||
#define AUDIO_H
|
||
|
||
#include <stdint.h>
|
||
#include <stdbool.h>
|
||
|
||
/*
|
||
* audio.h — I2S audio output driver (Issue #143)
|
||
*
|
||
* Hardware: SPI3 repurposed as I2S3 master TX (blackbox flash not used
|
||
* on balance bot). Supports MAX98357A (I2S class-D amp) and PCM5102A
|
||
* (I2S DAC + external amp) — both use standard Philips I2S.
|
||
*
|
||
* Pin assignment (SPI3 / I2S3, defined in config.h):
|
||
* PC10 I2S3_CK (BCLK) AF6
|
||
* PA15 I2S3_WS (LRCLK) AF6
|
||
* PB5 I2S3_SD (DIN) AF6
|
||
* PC5 AUDIO_MUTE (GPIO) active-high = enabled; low = muted/shutdown
|
||
*
|
||
* PLLI2S: N=192, R=2 → 96 MHz I2S clock → 22058 Hz (< 0.04% from 22050)
|
||
* DMA1 Stream7 Channel0 (SPI3_TX), circular, double-buffer ping-pong.
|
||
*
|
||
* Mixer priority (highest to lowest):
|
||
* 1. PCM audio chunks from Jetson (via JLINK_CMD_AUDIO, written to FIFO)
|
||
* 2. Notification tones (queued by audio_play_tone)
|
||
* 3. Silence
|
||
*
|
||
* Volume applies to all sources via integer sample scaling (0–100).
|
||
*/
|
||
|
||
/* Maximum int16_t samples per JLINK_CMD_AUDIO frame (252-byte payload / 2) */
|
||
#define AUDIO_CHUNK_MAX_SAMPLES 126u
|
||
|
||
/* Pre-defined notification tones */
|
||
typedef enum {
|
||
AUDIO_TONE_BEEP_SHORT = 0, /* 880 Hz, 100 ms — acknowledge / UI feedback */
|
||
AUDIO_TONE_BEEP_LONG = 1, /* 880 Hz, 500 ms — generic warning */
|
||
AUDIO_TONE_STARTUP = 2, /* C5→E5→G5 arpeggio (3 × 120 ms) */
|
||
AUDIO_TONE_ARM = 3, /* 880 Hz→1047 Hz two-beep ascending */
|
||
AUDIO_TONE_DISARM = 4, /* 880 Hz→659 Hz two-beep descending */
|
||
AUDIO_TONE_FAULT = 5, /* 200 Hz buzz, 500 ms — tilt/safety fault */
|
||
AUDIO_TONE_COUNT
|
||
} AudioTone;
|
||
|
||
/*
|
||
* audio_init()
|
||
*
|
||
* Configure PLLI2S, GPIO, DMA1 Stream7, and SPI3/I2S3.
|
||
* Pre-fills DMA buffer with silence, starts circular DMA TX, then
|
||
* unmutes the amp. Call once before safety_init().
|
||
*/
|
||
void audio_init(void);
|
||
|
||
/*
|
||
* audio_mute(mute)
|
||
*
|
||
* Drive AUDIO_MUTE_PIN: false = hardware-muted (SD/XSMT low),
|
||
* true = active (amp enabled). Does NOT stop DMA; allows instant
|
||
* un-mute without DMA restart clicks.
|
||
*/
|
||
void audio_mute(bool active);
|
||
|
||
/*
|
||
* audio_set_volume(vol)
|
||
*
|
||
* Software volume 0–100. Applied in ISR fill path via integer scaling.
|
||
* 0 = silence, 100 = full scale (±16384 for square wave, passthrough for PCM).
|
||
*/
|
||
void audio_set_volume(uint8_t vol);
|
||
|
||
/*
|
||
* audio_play_tone(tone)
|
||
*
|
||
* Queue a pre-defined notification tone. The tone plays after any tones
|
||
* already in the queue. Returns false if the tone queue is full (depth 4).
|
||
* Tones are pre-empted by incoming PCM audio from the Jetson.
|
||
*/
|
||
bool audio_play_tone(AudioTone tone);
|
||
|
||
/*
|
||
* audio_write_pcm(samples, n)
|
||
*
|
||
* Write mono 16-bit 22050 Hz PCM samples into the Jetson PCM FIFO.
|
||
* Called from jlink_process() dispatch on JLINK_CMD_AUDIO (main-loop context).
|
||
* Returns the number of samples actually accepted (0 if FIFO is full).
|
||
*/
|
||
uint16_t audio_write_pcm(const int16_t *samples, uint16_t n);
|
||
|
||
/*
|
||
* audio_tick(now_ms)
|
||
*
|
||
* Advance the tone sequencer state machine. Must be called every 1 ms
|
||
* from the main loop. Manages step transitions and gap timing; updates
|
||
* the volatile active-tone parameters read by the ISR fill path.
|
||
*/
|
||
void audio_tick(uint32_t now_ms);
|
||
|
||
/*
|
||
* audio_is_playing()
|
||
*
|
||
* Returns true if the DMA is running (always true after audio_init()
|
||
* unless the amp is hardware-muted or the I2S peripheral has an error).
|
||
*/
|
||
bool audio_is_playing(void);
|
||
|
||
#endif /* AUDIO_H */
|