Add MAX98357A I2S speaker driver with tone generator

I2S audio output on GPIO 9/10/11 (BCLK/WCLK/DOUT). Sine wave tone
generator with predefined alert sounds: beep, alert, success, error.
Boot beep confirms speaker is working. Speaker shut down before deep
sleep to release I2S driver.
This commit is contained in:
GlassOnTin 2026-03-27 19:00:48 +00:00
commit 8dd50ac901
2 changed files with 119 additions and 1 deletions

View file

@ -29,6 +29,9 @@
SensorBHI260AP *bhi260 = NULL;
bool bhi260_ready = false;
// MAX98357A I2S speaker
#include "Speaker.h"
// CST9217 capacitive touch panel
#include <touch/TouchDrvCST92xx.h>
TouchDrvCST92xx touch;
@ -304,6 +307,10 @@ void setup() {
attachInterrupt(TP_INT, touch_isr, FALLING);
}
// Init speaker (BLDO2 already enabled by PMU init)
speaker_init();
if (speaker_ready) speaker_beep(); // Boot audio feedback
// BHI260AP init deferred — firmware upload takes ~10s at 1MHz I2C
// and blocks serial communication during boot. Will be initialized
// lazily from the main loop after radio is up.
@ -2070,7 +2077,8 @@ void twatch_enter_deep_sleep(bool beacon_timer) {
delay(150); // Let the motor spin briefly before powering down
}
// 1. Put display controller into sleep mode (must happen before SPI.end)
// 1. Shut down audio and display before closing buses
speaker_end();
#if HAS_DISPLAY
co5300_sleep();
#endif

110
Speaker.h Normal file
View file

@ -0,0 +1,110 @@
// MAX98357A I2S Speaker Driver — Minimal tone generator for T-Watch Ultra
// Plays simple alert tones via I2S. No WAV files needed.
#ifndef SPEAKER_H
#define SPEAKER_H
#if BOARD_MODEL == BOARD_TWATCH_ULT
#include "driver/i2s.h"
#include <math.h>
#define SPK_BCLK I2S_BCLK
#define SPK_WCLK I2S_WCLK
#define SPK_DOUT I2S_DOUT
#define SPK_I2S_PORT I2S_NUM_0
#define SPK_SAMPLE_RATE 16000
#define SPK_TONE_BUF_SIZE 512
static bool speaker_ready = false;
bool speaker_init() {
i2s_config_t i2s_config = {};
i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
i2s_config.sample_rate = SPK_SAMPLE_RATE;
i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
i2s_config.channel_format = I2S_CHANNEL_FMT_ALL_RIGHT;
i2s_config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
i2s_config.dma_buf_count = 4;
i2s_config.dma_buf_len = 512;
i2s_config.use_apll = false;
i2s_config.tx_desc_auto_clear = true;
i2s_pin_config_t pin_config = {};
pin_config.bck_io_num = SPK_BCLK;
pin_config.ws_io_num = SPK_WCLK;
pin_config.data_out_num = SPK_DOUT;
pin_config.data_in_num = I2S_PIN_NO_CHANGE;
if (i2s_driver_install(SPK_I2S_PORT, &i2s_config, 0, NULL) != ESP_OK) {
return false;
}
if (i2s_set_pin(SPK_I2S_PORT, &pin_config) != ESP_OK) {
i2s_driver_uninstall(SPK_I2S_PORT);
return false;
}
speaker_ready = true;
return true;
}
void speaker_end() {
if (speaker_ready) {
i2s_driver_uninstall(SPK_I2S_PORT);
speaker_ready = false;
}
}
// Play a tone at given frequency (Hz) for given duration (ms) at volume (0-100)
void speaker_tone(uint16_t freq, uint16_t duration_ms, uint8_t volume) {
if (!speaker_ready || freq == 0) return;
float vol = (float)volume / 100.0f;
int16_t amplitude = (int16_t)(16000 * vol); // ~half of int16 max
uint32_t total_samples = (uint32_t)SPK_SAMPLE_RATE * duration_ms / 1000;
int16_t buf[SPK_TONE_BUF_SIZE];
uint32_t samples_written = 0;
while (samples_written < total_samples) {
uint32_t chunk = total_samples - samples_written;
if (chunk > SPK_TONE_BUF_SIZE) chunk = SPK_TONE_BUF_SIZE;
for (uint32_t i = 0; i < chunk; i++) {
float t = (float)(samples_written + i) / (float)SPK_SAMPLE_RATE;
buf[i] = (int16_t)(amplitude * sinf(2.0f * M_PI * freq * t));
}
size_t bytes_written = 0;
i2s_write(SPK_I2S_PORT, buf, chunk * sizeof(int16_t), &bytes_written, portMAX_DELAY);
samples_written += chunk;
}
// Brief silence to flush DMA buffer
memset(buf, 0, sizeof(buf));
size_t dummy;
i2s_write(SPK_I2S_PORT, buf, sizeof(buf), &dummy, portMAX_DELAY);
}
// Predefined alert tones
void speaker_beep() {
speaker_tone(1000, 100, 50); // Short 1kHz beep
}
void speaker_alert() {
speaker_tone(800, 200, 70);
speaker_tone(1200, 200, 70);
}
void speaker_success() {
speaker_tone(523, 100, 40); // C5
speaker_tone(659, 100, 40); // E5
speaker_tone(784, 150, 40); // G5
}
void speaker_error() {
speaker_tone(300, 300, 60);
speaker_tone(200, 400, 60);
}
#endif // BOARD_MODEL == BOARD_TWATCH_ULT
#endif // SPEAKER_H