initial audio playback working success - fixed pin 41 guard dac issue for pro variant by guarding with has es8311 audio

This commit is contained in:
pelgraine
2026-06-03 15:10:28 +10:00
parent d66f202081
commit 0a119eac4f
7 changed files with 376 additions and 20 deletions
+70 -14
View File
@@ -90,6 +90,9 @@
#ifdef MECK_AUDIO_VARIANT
#include "VoiceMessageScreen.h"
#include "BundledSounds.h"
#ifdef HAS_ES8311_AUDIO
#include "ES8311.h" // MAX: native ES8311 codec init (Arduino Wire)
#endif
#endif
#if defined(MECK_AUDIO_VARIANT) || defined(HAS_4G_MODEM)
#include "NotifSounds.h"
@@ -1850,6 +1853,22 @@ void setup() {
board.begin();
MESH_DEBUG_PRINTLN("setup() - board.begin() done");
// Initialize touch input (CST328) HERE, immediately after board.begin(),
// while the I2C bus is freshly initialised and quiet -- mirroring LilyGo's
// working example, which calls touch.begin() first thing. Running it later
// (after display/radio/filesystem init) left the bus in a state where the
// CST328 firmware-info read failed.
#if defined(LilyGo_TDeck_Pro) && defined(HAS_TOUCHSCREEN)
#if defined(LilyGo_TDeck_Pro_Max)
board.touchReset(); // fresh XL9555 reset immediately before driver init
#endif
if (touchInput.begin(CST328_PIN_INT)) {
MESH_DEBUG_PRINTLN("setup() - Touch input initialized");
} else {
MESH_DEBUG_PRINTLN("setup() - Touch input FAILED");
}
#endif
#ifdef DISPLAY_CLASS
DisplayDriver* disp = NULL;
MESH_DEBUG_PRINTLN("setup() - about to call display.begin()");
@@ -2283,14 +2302,8 @@ void setup() {
initKeyboard();
#endif
// Initialize touch input (CST328 on T-Deck Pro)
#if defined(LilyGo_TDeck_Pro) && defined(HAS_TOUCHSCREEN)
if (touchInput.begin(CST328_PIN_INT)) {
MESH_DEBUG_PRINTLN("setup() - Touch input initialized");
} else {
MESH_DEBUG_PRINTLN("setup() - Touch input FAILED");
}
#endif
// Touch input (CST328) is initialised earlier, right after board.begin(),
// on a freshly-initialised quiet I2C bus (see setup() near board.begin()).
// Initialize GT911 touch (T5S3 E-Paper Pro)
// Wire is already initialized by T5S3Board::begin(). The 4-arg begin() re-calls
@@ -2824,7 +2837,9 @@ void loop() {
audio->setVolume(0);
audio->stopSong();
delay(50);
// digitalWrite(41, LOW);
#ifndef HAS_ES8311_AUDIO
digitalWrite(41, LOW);
#endif
notifTonePlaying = false;
notifFadingOut = false;
notifMuted = false;
@@ -2868,8 +2883,36 @@ void loop() {
// Stop any stale playback before touching DAC
audio->stopSong();
// Configure I2S pins first (before DAC power)
bool ok = audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT, 0);
// MAX: route audio to the ES8311 codec and enable the speaker amp.
// NOTE: the codec REGISTER init is deferred until AFTER the I2S clocks
// are actually running (after connecttoFS + the silence-fill loop). The
// ES8311 appears to need live BCLK/LRCK to lock its clock tree; init'd
// on a clockless bus it configures correctly (readback confirms) but
// produces no output. Routing/amp are safe to set here.
#ifdef HAS_ES8311_AUDIO
board.selectAudioES8311(); // XL9555 AUDIO_SEL low -> ES8311
board.amplifierEnable(); // XL9555 AMPLIFIER high -> NS4150B on
// DIAG: read back the two routing pins to confirm they took effect
// (AUDIO_SEL should be LOW=0 for ES8311, AMPLIFIER should be HIGH=1).
Serial.printf("NotifTone: routing AUDIO_SEL=%d (want 0) AMP=%d (want 1)\n",
board.xl9555_digitalRead(XL_PIN_AUDIO_SEL) ? 1 : 0,
board.xl9555_digitalRead(XL_PIN_AMPLIFIER) ? 1 : 0);
#endif
// Configure I2S pins. Mirroring Ripple, the ES8311 is driven WITH MCLK
// (use_mclk=true), so MCLK must be generated on BOARD_I2S_MCLK. Use the
// 5-arg setPinout: the 4th arg is DIN (data-in, unused -> no change), the
// 5th is MCK. The earlier 4-arg call mistakenly passed MCLK as the 4th
// (DIN) arg, so MCLK was never generated -- this passes it as MCK.
#ifdef HAS_ES8311_AUDIO
bool ok = audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT,
I2S_PIN_NO_CHANGE, BOARD_I2S_MCLK);
Serial.printf("NotifTone: setPinout(BCLK=%d LRC=%d DOUT=%d MCK=%d) -> %s\n",
BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT, BOARD_I2S_MCLK,
ok ? "ok" : "FAILED");
#else
bool ok = audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT, 0);
#endif
if (!ok) ok = audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT);
// Start playback at volume 0 (silence into DAC during power-on)
@@ -2877,9 +2920,11 @@ void loop() {
audio->connecttoFS(SD, file);
// Enable DAC amplifier and let it stabilise with silence flowing
// pinMode(41, OUTPUT);
// digitalWrite(41, HIGH);
// delay(80);
#ifndef HAS_ES8311_AUDIO
pinMode(41, OUTPUT);
digitalWrite(41, HIGH);
delay(80);
#endif
// Fill audio buffer with silence at volume 0 (prevents power-on pop)
for (int i = 0; i < 30; i++) {
@@ -2887,6 +2932,17 @@ void loop() {
delay(2);
}
// I2S clocks are now running (connecttoFS started the channel and the
// loop above has pushed samples). NOW bring up the ES8311 over I2C, so
// it can lock to the live BCLK/LRCK. Done once per boot.
#ifdef HAS_ES8311_AUDIO
static bool es8311_ready = false;
if (!es8311_ready) {
es8311_ready = es8311_init_44100_16bit();
Serial.printf("NotifTone: ES8311 init (post-I2S) %s\n", es8311_ready ? "OK" : "FAILED");
}
#endif
// Ramp volume up to max
audio->setVolume(21);
notifTonePlaying = true;
@@ -264,13 +264,17 @@ private:
// ---- DAC power (same pattern as AudiobookPlayerScreen) ----
void enableDAC() {
#ifndef HAS_ES8311_AUDIO
pinMode(41, OUTPUT);
digitalWrite(41, HIGH);
delay(50);
#endif
}
void disableDAC() {
#ifndef HAS_ES8311_AUDIO
digitalWrite(41, LOW);
#endif
}
// ---- Audio control for alarm ringing ----
@@ -219,17 +219,21 @@ private:
// On the audio variant, this pin supplies power to the DAC circuit.
// TDeckBoard::begin() sets it LOW ("disable modem") which starves the DAC.
void enableDAC() {
#ifndef HAS_ES8311_AUDIO
pinMode(41, OUTPUT);
digitalWrite(41, HIGH);
if (!_dacPowered) {
delay(50);
}
_dacPowered = true;
#endif
}
void disableDAC() {
#ifndef HAS_ES8311_AUDIO
digitalWrite(41, LOW);
_dacPowered = false;
#endif
}
// Restore an M4B file that was temporarily renamed to .m4a for playback.
+272
View File
@@ -0,0 +1,272 @@
#pragma once
// =============================================================================
// ES8311.h -- Minimal self-contained ES8311 codec init for T-Deck Pro MAX
//
// The MAX uses an ES8311 I2C-configured audio codec (the Pro V1.1 used a
// PCM5102A "dumb" DAC that needed no config). The ES8311 will not produce
// sound until its registers are configured over I2C. This driver does exactly
// that, over Arduino Wire, with no esp_codec_dev / Cpp_Bus_Driver framework.
//
// The register sequence and clock-coefficient table are taken from, and
// cross-verified between, Espressif's esp_codec_dev es8311.c and LilyGo's P4
// Cpp_Bus_Driver Es8311 -- both agree on every clock register, so the sequence
// here is not reverse-engineered.
//
// Configuration (fixed for Stage 1):
// - ES8311 in SLAVE mode (ESP32 I2S is master: drives MCLK/BCLK/LRCK)
// - use_mclk = TRUE: a real MCLK is driven on BOARD_I2S_MCLK (GPIO38) by the
// I2S peripheral (APLL on, auto-derived 256 * fs). This mirrors the working
// Ripple firmware, which drives the ES8311 with MCLK on this exact platform.
// - 44100 Hz, 16-bit, DAC (playback) only; MCLK = 256 * sample_rate
// - Register sequence transcribed from the proven esp_codec_dev es8311.c
// (es8311_open + es8311_set_bits_per_sample + es8311_config_sample +
// es8311_start) for the use_mclk == true path
//
// Routing/amplifier (XL9555: AUDIO_SEL low, AMPLIFIER high) is handled by the
// board class (board.selectAudioES8311(); board.amplifierEnable()), NOT here.
//
// I2C: the ES8311 is at 0x18 on the shared Wire bus (SDA13/SCL14), already
// initialised by board.begin(). This driver only uses Wire, never re-inits it.
//
// Guard: HAS_ES8311_AUDIO
// =============================================================================
#ifdef HAS_ES8311_AUDIO
#ifndef MECK_ES8311_H
#define MECK_ES8311_H
#include <Arduino.h>
#include <Wire.h>
#include "variant.h"
// ES8311 register addresses (subset used here)
#define ES8311_REG00_RESET 0x00
#define ES8311_REG01_CLK_MANAGER 0x01
#define ES8311_REG02_CLK_MANAGER 0x02
#define ES8311_REG03_CLK_MANAGER 0x03
#define ES8311_REG04_CLK_MANAGER 0x04
#define ES8311_REG05_CLK_MANAGER 0x05
#define ES8311_REG06_CLK_MANAGER 0x06
#define ES8311_REG07_CLK_MANAGER 0x07
#define ES8311_REG08_CLK_MANAGER 0x08
#define ES8311_REG09_SDPIN 0x09
#define ES8311_REG0A_SDPOUT 0x0A
#define ES8311_REG0B_SYSTEM 0x0B
#define ES8311_REG0C_SYSTEM 0x0C
#define ES8311_REG0D_SYSTEM 0x0D
#define ES8311_REG0E_SYSTEM 0x0E
#define ES8311_REG10_SYSTEM 0x10
#define ES8311_REG11_SYSTEM 0x11
#define ES8311_REG12_SYSTEM 0x12
#define ES8311_REG13_SYSTEM 0x13
#define ES8311_REG14_SYSTEM 0x14
#define ES8311_REG15_ADC 0x15
#define ES8311_REG16_ADC 0x16
#define ES8311_REG17_ADC 0x17
#define ES8311_REG1B_ADC 0x1B
#define ES8311_REG1C_ADC 0x1C
#define ES8311_REG31_DAC 0x31
#define ES8311_REG32_DAC 0x32
#define ES8311_REG37_DAC 0x37
#define ES8311_REG44_GPIO 0x44
#define ES8311_REG45_GP 0x45
#define ES8311_REGFD_CHIPID1 0xFD
#ifndef I2C_ADDR_ES8311
#define I2C_ADDR_ES8311 0x18
#endif
// --- low-level I2C register access over Wire ---
static inline bool es8311_write(uint8_t reg, uint8_t val) {
Wire.beginTransmission((uint8_t)I2C_ADDR_ES8311);
Wire.write(reg);
Wire.write(val);
return Wire.endTransmission() == 0;
}
static inline uint8_t es8311_read(uint8_t reg) {
Wire.beginTransmission((uint8_t)I2C_ADDR_ES8311);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return 0;
if (Wire.requestFrom((uint8_t)I2C_ADDR_ES8311, (uint8_t)1) != 1) return 0;
return Wire.read();
}
// Initialise the ES8311 for 44100 Hz, 16-bit, slave mode, MCLK = 256 * fs.
// Returns true if the chip ID read back as 0x83 (ES8311 reports ID 0x8311
// across REGFD/REGFE; REGFD high byte is 0x83).
static inline bool es8311_init_44100_16bit() {
// Chip presence / ID check (REGFD should read 0x83 on ES8311)
uint8_t id = es8311_read(ES8311_REGFD_CHIPID1);
Serial.printf("[ES8311] chip ID REGFD = 0x%02X (expect 0x83)\n", id);
// ---- open(): base register setup (from esp_codec_dev es8311_open) ----
// GPIO REG44 written twice: ES8311 first I2C write can be unreliable.
es8311_write(ES8311_REG44_GPIO, 0x08);
es8311_write(ES8311_REG44_GPIO, 0x08);
es8311_write(ES8311_REG01_CLK_MANAGER, 0x30);
es8311_write(ES8311_REG02_CLK_MANAGER, 0x00);
es8311_write(ES8311_REG03_CLK_MANAGER, 0x10);
es8311_write(ES8311_REG16_ADC, 0x24);
es8311_write(ES8311_REG04_CLK_MANAGER, 0x10);
es8311_write(ES8311_REG05_CLK_MANAGER, 0x00);
es8311_write(ES8311_REG0B_SYSTEM, 0x00);
es8311_write(ES8311_REG0C_SYSTEM, 0x00);
es8311_write(ES8311_REG10_SYSTEM, 0x1F);
es8311_write(ES8311_REG11_SYSTEM, 0x7F);
es8311_write(ES8311_REG00_RESET, 0x80); // CSM power up
// Slave mode: REG00 bit6 = 0. (read-modify-write)
{
uint8_t regv = es8311_read(ES8311_REG00_RESET);
regv &= 0xBF; // slave
es8311_write(ES8311_REG00_RESET, regv);
}
// Clock source for internal MCLK (REG01), transcribed from the proven
// esp_codec_dev es8311.c es8311_open() for use_mclk == true:
// regv = 0x3F; use_mclk true -> regv &= 0x7F; invert_mclk false -> regv &= ~0x40
// => REG01 = 0x3F
// The codec uses the externally supplied MCLK (driven on GPIO38 via APLL),
// mirroring the working Ripple firmware on this platform.
{
uint8_t regv = 0x3F;
regv &= 0x7F; // use_mclk = true
regv &= ~0x40; // mclk not inverted
es8311_write(ES8311_REG01_CLK_MANAGER, regv);
}
// SCLK not inverted (REG06)
{
uint8_t regv = es8311_read(ES8311_REG06_CLK_MANAGER);
regv &= ~0x20;
es8311_write(ES8311_REG06_CLK_MANAGER, regv);
}
es8311_write(ES8311_REG13_SYSTEM, 0x10);
es8311_write(ES8311_REG1B_ADC, 0x0A);
es8311_write(ES8311_REG1C_ADC, 0x6A);
// no_dac_ref == false: set internal reference signal (ADCL + DACR) -> REG44 = 0x58.
// (es8311.c writes 0x58 here for DAC playback; the earlier 0x08 writes were the
// I2C-noise-immunity double-write.)
es8311_write(ES8311_REG44_GPIO, 0x58);
// ---- config_sample(): clock dividers, transcribed from es8311_config_sample ----
// For use_mclk == true the driver looks up the coeff row by
// (sample_rate * mclk_div, sample_rate) with mclk_div = 256 default, i.e. the
// {11289600, 44100} row: pre_div=1, pre_multi=1, adc_div=1, dac_div=1,
// fs_mode=0, lrck_h=0x00, lrck_l=0xff, bclk_div=0x04, adc_osr=0x10, dac_osr=0x10.
// pre_multi == 1 -> datmp = 0 (the use_mclk==false datmp=3 override does NOT
// apply here). So REG02 = (pre_div-1)<<5 | 0<<3 = 0x00.
{
const uint8_t pre_div = 1, pre_multi_enc = 0; // datmp=0 (pre_multi=1, use_mclk=true)
const uint8_t adc_div = 1, dac_div = 1, fs_mode = 0;
const uint8_t lrck_h = 0x00, lrck_l = 0xFF, bclk_div = 0x04;
const uint8_t adc_osr = 0x10, dac_osr = 0x10;
uint8_t regv = es8311_read(ES8311_REG02_CLK_MANAGER);
regv &= 0x07;
regv |= (pre_div - 1) << 5;
regv |= pre_multi_enc << 3;
es8311_write(ES8311_REG02_CLK_MANAGER, regv);
regv = (fs_mode << 6) | adc_osr;
es8311_write(ES8311_REG03_CLK_MANAGER, regv);
es8311_write(ES8311_REG04_CLK_MANAGER, dac_osr);
regv = ((adc_div - 1) << 4) | (dac_div - 1);
es8311_write(ES8311_REG05_CLK_MANAGER, regv);
regv = es8311_read(ES8311_REG06_CLK_MANAGER);
regv &= 0xE0;
if (bclk_div < 19) regv |= (bclk_div - 1);
else regv |= bclk_div;
es8311_write(ES8311_REG06_CLK_MANAGER, regv);
regv = es8311_read(ES8311_REG07_CLK_MANAGER);
regv &= 0xC0;
regv |= lrck_h;
es8311_write(ES8311_REG07_CLK_MANAGER, regv);
es8311_write(ES8311_REG08_CLK_MANAGER, lrck_l);
}
// ---- format + bits per sample: 16-bit, I2S (REG09 DAC SDP) ----
// 16-bit -> SDP word length field = 0x0C in bits [4:2]; I2S format = 0x00.
{
uint8_t dac_iface = es8311_read(ES8311_REG09_SDPIN);
dac_iface &= 0xE3; // clear word-length field bits [4:2] only
dac_iface |= (0x03 << 2); // 0x0C = 16-bit (bits [4:2] = 011)
es8311_write(ES8311_REG09_SDPIN, dac_iface);
}
// ---- start(): power up DAC path (from es8311_start, DAC mode) ----
{
uint8_t regv = 0x80; // CSM on
regv &= 0xBF; // slave
es8311_write(ES8311_REG00_RESET, regv);
// REG01 clock source (from es8311_start): regv=0x3F; use_mclk true -> &=0x7F;
// invert_mclk false -> &=~0x40 => 0x3F. Re-applied here exactly as the proven
// start sequence does, so it stays consistent with the open() write.
{
uint8_t r01 = 0x3F;
r01 &= 0x7F; // use_mclk = true
r01 &= ~0x40; // not inverted
es8311_write(ES8311_REG01_CLK_MANAGER, r01);
}
// DAC interface power on (REG09 bit6 = 0)
uint8_t dac_iface = es8311_read(ES8311_REG09_SDPIN);
dac_iface &= ~(1 << 6);
es8311_write(ES8311_REG09_SDPIN, dac_iface);
es8311_write(ES8311_REG17_ADC, 0xBF);
es8311_write(ES8311_REG0E_SYSTEM, 0x02);
es8311_write(ES8311_REG12_SYSTEM, 0x00); // enable DAC
es8311_write(ES8311_REG14_SYSTEM, 0x1A);
es8311_write(ES8311_REG0D_SYSTEM, 0x01);
es8311_write(ES8311_REG15_ADC, 0x40);
es8311_write(ES8311_REG37_DAC, 0x08);
es8311_write(ES8311_REG45_GP, 0x00);
}
// Unmute DAC and set a sane default volume.
es8311_write(ES8311_REG31_DAC, 0x00); // unmute
es8311_write(ES8311_REG32_DAC, 0xBF); // ~0 dB (191 = 0 dB)
// DIAG: read back key registers to confirm the power-up/format writes landed.
// REG00 (CSM/reset), REG01 (clock src), REG02 (clk div), REG09 (SDP/DAC iface),
// REG0D (power up/down), REG12 (DAC enable), REG31 (DAC mute), REG32 (DAC vol).
Serial.printf("[ES8311] readback R00=%02X R01=%02X R02=%02X R09=%02X R0D=%02X R12=%02X R31=%02X R32=%02X\n",
es8311_read(ES8311_REG00_RESET),
es8311_read(ES8311_REG01_CLK_MANAGER),
es8311_read(ES8311_REG02_CLK_MANAGER),
es8311_read(ES8311_REG09_SDPIN),
es8311_read(ES8311_REG0D_SYSTEM),
es8311_read(ES8311_REG12_SYSTEM),
es8311_read(ES8311_REG31_DAC),
es8311_read(ES8311_REG32_DAC));
return id == 0x83;
}
// DAC volume, 0..255 (0 = mute-ish, 191 = 0 dB, 255 = +32 dB).
static inline void es8311_set_dac_volume(uint8_t vol) {
es8311_write(ES8311_REG32_DAC, vol);
}
// Hard mute / unmute the DAC.
static inline void es8311_set_mute(bool mute) {
uint8_t regv = es8311_read(ES8311_REG31_DAC);
regv &= 0x9F;
es8311_write(ES8311_REG31_DAC, mute ? (regv | 0x60) : regv);
}
#endif // MECK_ES8311_H
#endif // HAS_ES8311_AUDIO
@@ -218,15 +218,19 @@ private:
// DAC power control (same as AudiobookPlayerScreen)
void enableDAC() {
#ifndef HAS_ES8311_AUDIO
pinMode(41, OUTPUT);
digitalWrite(41, HIGH);
if (!_dacPowered) delay(50);
_dacPowered = true;
#endif
}
void disableDAC() {
#ifndef HAS_ES8311_AUDIO
digitalWrite(41, LOW);
_dacPowered = false;
#endif
}
// ---------------------------------------------------------------------------