sl-firmware 1e76cb9fe3 feat(audio): I2S3 audio amplifier driver — Issue #143
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>
2026-03-02 10:31:27 -05:00

353 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "audio.h"
#include "config.h"
#include "stm32f7xx_hal.h"
#include <string.h>
/* ================================================================
* Buffer layout
* ================================================================
* AUDIO_BUF_HALF samples per DMA half (config.h: 441 at 22050 Hz → 20 ms).
* DMA runs in circular mode over 2 halves; TxHalfCplt refills [0] and
* TxCplt refills [AUDIO_BUF_HALF]. Both callbacks simply call fill_half().
*/
#define AUDIO_BUF_SIZE (AUDIO_BUF_HALF * 2u)
/* DMA buffer: must be in non-cached SRAM or flushed before DMA access.
* Placed in SRAM1 (default .bss section on STM32F722 — below 512 KB).
* The I-cache / D-cache is on by default; DMA1 is an AHB master that
* bypasses cache, so we keep this buffer in DTCM-uncacheable SRAM. */
static int16_t s_dma_buf[AUDIO_BUF_SIZE];
/* ================================================================
* PCM FIFO — Jetson audio (main-loop writer, ISR reader)
* ================================================================
* Single-producer / single-consumer lock-free ring buffer.
* Power-of-2 size so wrap uses bitwise AND (safe across ISR boundary).
* 4096 samples ≈ 185 ms at 22050 Hz — enough to absorb JLink jitter.
*/
#define PCM_FIFO_SIZE 4096u
#define PCM_FIFO_MASK (PCM_FIFO_SIZE - 1u)
static int16_t s_pcm_fifo[PCM_FIFO_SIZE];
static volatile uint16_t s_pcm_rd = 0; /* consumer (ISR advances) */
static volatile uint16_t s_pcm_wr = 0; /* producer (main loop advances) */
/* ================================================================
* Tone sequencer
* ================================================================
* Each AudioTone maps to a const ToneStep array. Steps are played
* sequentially; a gap_ms of 0 means no silence between steps.
* audio_tick() in the main loop advances the state machine and
* writes the volatile active-tone params read by the ISR fill path.
*/
typedef struct {
uint16_t freq_hz;
uint16_t dur_ms;
uint16_t gap_ms; /* silence after this step */
} ToneStep;
/* ---- Tone definitions ---- */
static const ToneStep s_def_beep_short[] = {{880, 100, 0}};
static const ToneStep s_def_beep_long[] = {{880, 500, 0}};
static const ToneStep s_def_startup[] = {{523, 120, 60},
{659, 120, 60},
{784, 200, 0}};
static const ToneStep s_def_arm[] = {{880, 80, 60},
{1047, 100, 0}};
static const ToneStep s_def_disarm[] = {{880, 80, 60},
{659, 100, 0}};
static const ToneStep s_def_fault[] = {{200, 500, 0}};
typedef struct {
const ToneStep *steps;
uint8_t n_steps;
} ToneDef;
static const ToneDef s_tone_defs[AUDIO_TONE_COUNT] = {
[AUDIO_TONE_BEEP_SHORT] = {s_def_beep_short, 1},
[AUDIO_TONE_BEEP_LONG] = {s_def_beep_long, 1},
[AUDIO_TONE_STARTUP] = {s_def_startup, 3},
[AUDIO_TONE_ARM] = {s_def_arm, 2},
[AUDIO_TONE_DISARM] = {s_def_disarm, 2},
[AUDIO_TONE_FAULT] = {s_def_fault, 1},
};
/* Active tone queue */
#define TONE_QUEUE_DEPTH 4u
typedef struct {
const ToneDef *def;
uint8_t step; /* current step index within def */
bool in_gap; /* true while playing inter-step silence */
uint32_t step_end_ms; /* abs HAL_GetTick() when step/gap expires */
} ToneSeq;
static ToneSeq s_tone_q[TONE_QUEUE_DEPTH];
static uint8_t s_tq_head = 0; /* consumer (audio_tick reads) */
static uint8_t s_tq_tail = 0; /* producer (audio_play_tone writes) */
/* Volatile parameters written by audio_tick(), read by ISR fill_half() */
static volatile uint16_t s_active_freq = 0; /* 0 = silence / gap */
static volatile uint32_t s_active_phase = 0; /* sample phase counter */
/* ================================================================
* Volume
* ================================================================ */
static uint8_t s_volume = AUDIO_VOLUME_DEFAULT; /* 0100 */
/* ================================================================
* HAL handles
* ================================================================ */
static I2S_HandleTypeDef s_i2s;
static DMA_HandleTypeDef s_dma_tx;
/* ================================================================
* fill_half() — called from ISR, O(AUDIO_BUF_HALF)
* ================================================================
* Priority: PCM FIFO (Jetson TTS) > square-wave tone > silence.
* Volume applied via integer scaling — no float in ISR.
*/
static void fill_half(int16_t *buf, uint16_t n)
{
uint16_t rd = s_pcm_rd;
uint16_t wr = s_pcm_wr;
uint16_t avail = (uint16_t)((wr - rd) & PCM_FIFO_MASK);
if (avail >= n) {
/* ---- Drain Jetson PCM FIFO ---- */
for (uint16_t i = 0; i < n; i++) {
int32_t s = (int32_t)s_pcm_fifo[rd] * (int32_t)s_volume / 100;
buf[i] = (s > 32767) ? (int16_t)32767 :
(s < -32768) ? (int16_t)-32768 : (int16_t)s;
rd = (rd + 1u) & PCM_FIFO_MASK;
}
s_pcm_rd = rd;
} else if (s_active_freq) {
/* ---- Square wave tone generator ---- */
uint32_t half_p = (uint32_t)AUDIO_SAMPLE_RATE / (2u * s_active_freq);
int16_t amp = (int16_t)(16384u * (uint32_t)s_volume / 100u);
uint32_t ph = s_active_phase;
uint32_t period = 2u * half_p;
for (uint16_t i = 0; i < n; i++) {
buf[i] = ((ph % period) < half_p) ? amp : (int16_t)(-amp);
ph++;
}
s_active_phase = ph;
} else {
/* ---- Silence ---- */
memset(buf, 0, (size_t)(n * 2u));
}
}
/* ================================================================
* DMA callbacks (ISR context)
* ================================================================ */
void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{
if (hi2s->Instance == SPI3)
fill_half(s_dma_buf, AUDIO_BUF_HALF);
}
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
if (hi2s->Instance == SPI3)
fill_half(s_dma_buf + AUDIO_BUF_HALF, AUDIO_BUF_HALF);
}
/* DMA1 Stream7 IRQ (SPI3/I2S3 TX) */
void DMA1_Stream7_IRQHandler(void)
{
HAL_DMA_IRQHandler(&s_dma_tx);
}
/* ================================================================
* audio_init()
* ================================================================ */
void audio_init(void)
{
/* ---- PLLI2S: N=192, R=2 → 96 MHz I2S clock ----
* With SPI3/I2S3 and I2S_DATAFORMAT_16B (16-bit, 32-bit frame slot):
* FS = 96 MHz / (32 × 2 × I2SDIV) where HAL picks I2SDIV = 68
* → 96 000 000 / (32 × 2 × 68) = 22 058 Hz (< 0.04 % error)
*/
RCC_PeriphCLKInitTypeDef pclk = {0};
pclk.PeriphClockSelection = RCC_PERIPHCLK_I2S;
pclk.I2sClockSelection = RCC_I2SCLKSOURCE_PLLI2S;
pclk.PLLI2S.PLLI2SN = 192; /* VCO = (HSE/PLLM) × 192 = 192 MHz */
pclk.PLLI2S.PLLI2SR = 2; /* I2S clock = 96 MHz */
HAL_RCCEx_PeriphCLKConfig(&pclk);
/* ---- GPIO ---- */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
/* PA15: I2S3_WS (LRCLK), AF6 */
gpio.Pin = AUDIO_LRCK_PIN;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio.Alternate = GPIO_AF6_SPI3;
HAL_GPIO_Init(AUDIO_LRCK_PORT, &gpio);
/* PC10: I2S3_CK (BCLK), AF6 */
gpio.Pin = AUDIO_BCLK_PIN;
HAL_GPIO_Init(AUDIO_BCLK_PORT, &gpio);
/* PB5: I2S3_SD (DIN), AF6 */
gpio.Pin = AUDIO_DOUT_PIN;
HAL_GPIO_Init(AUDIO_DOUT_PORT, &gpio);
/* PC5: AUDIO_MUTE GPIO output — drive low (muted) initially */
gpio.Pin = AUDIO_MUTE_PIN;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Alternate = 0;
HAL_GPIO_Init(AUDIO_MUTE_PORT, &gpio);
HAL_GPIO_WritePin(AUDIO_MUTE_PORT, AUDIO_MUTE_PIN, GPIO_PIN_RESET);
/* ---- DMA1 Stream7 Channel0 (SPI3/I2S3 TX) ---- */
__HAL_RCC_DMA1_CLK_ENABLE();
s_dma_tx.Instance = DMA1_Stream7;
s_dma_tx.Init.Channel = DMA_CHANNEL_0;
s_dma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
s_dma_tx.Init.PeriphInc = DMA_PINC_DISABLE;
s_dma_tx.Init.MemInc = DMA_MINC_ENABLE;
s_dma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
s_dma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
s_dma_tx.Init.Mode = DMA_CIRCULAR;
s_dma_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
s_dma_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&s_dma_tx);
__HAL_LINKDMA(&s_i2s, hdmatx, s_dma_tx);
HAL_NVIC_SetPriority(DMA1_Stream7_IRQn, 7, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream7_IRQn);
/* ---- SPI3 in I2S3 master TX mode ---- */
__HAL_RCC_SPI3_CLK_ENABLE();
s_i2s.Instance = SPI3;
s_i2s.Init.Mode = I2S_MODE_MASTER_TX;
s_i2s.Init.Standard = I2S_STANDARD_PHILIPS;
s_i2s.Init.DataFormat = I2S_DATAFORMAT_16B;
s_i2s.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;
s_i2s.Init.AudioFreq = I2S_AUDIOFREQ_22K;
s_i2s.Init.CPOL = I2S_CPOL_LOW;
s_i2s.Init.ClockSource = I2S_CLOCK_PLL;
HAL_I2S_Init(&s_i2s);
/* Pre-fill with silence and start circular DMA TX */
memset(s_dma_buf, 0, sizeof(s_dma_buf));
HAL_I2S_Transmit_DMA(&s_i2s, (uint16_t *)s_dma_buf, AUDIO_BUF_SIZE);
/* Unmute amp after DMA is running — avoids start-up click */
HAL_GPIO_WritePin(AUDIO_MUTE_PORT, AUDIO_MUTE_PIN, GPIO_PIN_SET);
}
/* ================================================================
* Public API
* ================================================================ */
void audio_mute(bool active)
{
HAL_GPIO_WritePin(AUDIO_MUTE_PORT, AUDIO_MUTE_PIN,
active ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
void audio_set_volume(uint8_t vol)
{
s_volume = (vol > 100u) ? 100u : vol;
}
bool audio_play_tone(AudioTone tone)
{
if (tone >= AUDIO_TONE_COUNT) return false;
uint8_t next = (s_tq_tail + 1u) % TONE_QUEUE_DEPTH;
if (next == s_tq_head) return false; /* queue full */
s_tone_q[s_tq_tail].def = &s_tone_defs[tone];
s_tone_q[s_tq_tail].step = 0;
s_tone_q[s_tq_tail].in_gap = false;
s_tone_q[s_tq_tail].step_end_ms = 0; /* audio_tick() sets this on first run */
s_tq_tail = next;
return true;
}
uint16_t audio_write_pcm(const int16_t *samples, uint16_t n)
{
uint16_t wr = s_pcm_wr;
uint16_t rd = s_pcm_rd;
uint16_t space = (uint16_t)((rd - wr - 1u) & PCM_FIFO_MASK);
uint16_t accept = (n < space) ? n : space;
for (uint16_t i = 0; i < accept; i++) {
s_pcm_fifo[wr] = samples[i];
wr = (wr + 1u) & PCM_FIFO_MASK;
}
s_pcm_wr = wr;
return accept;
}
void audio_tick(uint32_t now_ms)
{
/* Nothing to do if queue is empty */
if (s_tq_head == s_tq_tail) {
s_active_freq = 0;
return;
}
ToneSeq *seq = &s_tone_q[s_tq_head];
/* First call for this sequence entry: arm the first step */
if (seq->step_end_ms == 0u) {
const ToneStep *st = &seq->def->steps[0];
seq->in_gap = false;
seq->step_end_ms = now_ms + st->dur_ms;
s_active_freq = st->freq_hz;
s_active_phase = 0;
return;
}
/* Step / gap still running */
if (now_ms < seq->step_end_ms) return;
/* Current step or gap has expired */
const ToneStep *st = &seq->def->steps[seq->step];
if (!seq->in_gap && st->gap_ms) {
/* Transition: tone → inter-step gap (silence) */
seq->in_gap = true;
seq->step_end_ms = now_ms + st->gap_ms;
s_active_freq = 0;
return;
}
/* Advance to next step */
seq->step++;
seq->in_gap = false;
if (seq->step >= seq->def->n_steps) {
/* Sequence complete — pop from queue */
s_tq_head = (s_tq_head + 1u) % TONE_QUEUE_DEPTH;
s_active_freq = 0;
return;
}
/* Start next step */
st = &seq->def->steps[seq->step];
seq->step_end_ms = now_ms + st->dur_ms;
s_active_freq = st->freq_hz;
s_active_phase = 0;
}
bool audio_is_playing(void)
{
return (s_i2s.State == HAL_I2S_STATE_BUSY_TX);
}