ES8388 codec
The codec converts between the microphones and 16-bit samples. It is configured over I2C at boot and not addressed again; audio then moves only over I2S.
I2C access
The codec answers at 7-bit address 0x10. The bus runs at 100 kHz with the ESP32’s internal pull-ups enabled. All traffic happens during es8388_init().
static void i2c_init(void)
{
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_SDA_PIN,
.scl_io_num = I2C_SCL_PIN,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000,
};
ESP_ERROR_CHECK(i2c_param_config(I2C_NUM_0, &conf));
ESP_ERROR_CHECK(i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0));
}Each register write is one transaction built as a command link: start, address with the write bit, register number, value, stop. Failures are logged with the register number and the ESP-IDF error name.
static esp_err_t es8388_write_reg(uint8_t reg, uint8_t val)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (ES8388_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_write_byte(cmd, val, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "ES8388 write reg 0x%02X failed: %s", reg, esp_err_to_name(ret));
}
return ret;
}The read variant issues a repeated start and reads one byte with NACK. It is called once, on register 0x00, to confirm the codec responds before the configuration sequence runs.
static esp_err_t es8388_read_reg(uint8_t reg, uint8_t *val)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (ES8388_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (ES8388_ADDR << 1) | I2C_MASTER_READ, true);
i2c_master_read_byte(cmd, val, I2C_MASTER_NACK);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
return ret;
}If that read fails, es8388_init() calls i2c_scan() and returns without configuring anything. The scan probes addresses 0x08 to 0x77 and logs every device that acknowledges, which distinguishes a wiring fault from a codec that is present but unresponsive.
static void i2c_scan(void)
{
ESP_LOGI(TAG, "Escaneando barramento I2C...");
int found = 0;
for (int addr = 0x08; addr <= 0x77; addr++) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(50));
i2c_cmd_link_delete(cmd);
if (ret == ESP_OK) {
ESP_LOGI(TAG, " Dispositivo encontrado em 0x%02X", addr);
found++;
}
}
ESP_LOGI(TAG, "Scan completo: %d dispositivo(s)", found);
}Register sequence
The codec is powered down before configuration and powered up after it: the sequence opens with 0x02 = 0xF3 and closes with 0x02 = 0x00. Order is significant; individual values are listed below.
es8388_write_reg(0x08, 0x00); /* Slave */
es8388_write_reg(0x02, 0xF3); /* Power down */
es8388_write_reg(0x2B, 0x80); /* Same LRCK */
es8388_write_reg(0x00, 0x36); /* VMIDSEL */
es8388_write_reg(0x01, 0x40); /* Ref up */
/* ADC */
es8388_write_reg(0x03, 0x00);
es8388_write_reg(0x09, 0x88); /* Mic gain +24dB */
es8388_write_reg(0x0A, 0x00); /* LIN1/RIN1 */
es8388_write_reg(0x0B, 0x02); /* Mono L */
es8388_write_reg(0x0C, 0x18); /* I2S fmt */
es8388_write_reg(0x0D, 0x02); /* Fs=256 */
es8388_write_reg(0x10, 0x00);
es8388_write_reg(0x11, 0x00);
/* DAC */
es8388_write_reg(0x04, 0x3C);
es8388_write_reg(0x17, 0x18);
es8388_write_reg(0x18, 0x02);
es8388_write_reg(0x1A, 0x00);
es8388_write_reg(0x1B, 0x00);
es8388_write_reg(0x19, 0x00); /* Unmute */
es8388_write_reg(0x26, 0x00);
es8388_write_reg(0x27, 0xB8);
es8388_write_reg(0x2A, 0xB8);
es8388_write_reg(0x2E, 0x1E);
es8388_write_reg(0x2F, 0x1E);
es8388_write_reg(0x30, 0x1E);
es8388_write_reg(0x02, 0x00); /* Power up */
ESP_LOGI(TAG, "ES8388 ok (ADC + DAC)");| Register | Value | Documented meaning |
|---|---|---|
| 0x02 | 0xF3 / 0x00 | power down before configuring, power up after |
| 0x08 | 0x00 | slave mode: the ESP32 supplies MCLK, BCLK and LRCK |
| 0x2B | 0x80 | same LRCK for ADC and DAC |
| 0x00 | 0x36 | VMIDSEL, analogue mid-rail reference |
| 0x01 | 0x40 | reference up |
| 0x09 | 0x88 | microphone gain, +24 dB |
| 0x0A | 0x00 | input select: LIN1 / RIN1 |
| 0x0B | 0x02 | ADC mono, left |
| 0x0C | 0x18 | ADC I2S format |
| 0x0D | 0x02 | Fs = 256 |
| 0x19 | 0x00 | DAC unmute |
The remaining writes in es8388_init() — 0x03, 0x04, 0x10, 0x11, 0x17, 0x18, 0x1A, 0x1B, 0x26, 0x27, 0x2A, 0x2E, 0x2F and 0x30 — carry no annotation in the source, and their bit-level meaning has not been derived from the datasheet. They cover DAC setup, mixer routing and output volume. Changing them requires reading the ES8388 datasheet directly.
Registers 0x0C and 0x17 set the I2S format for the ADC and the DAC. Both are 0x18. This value was determined by testing rather than from the datasheet: decoding it against the published bit fields does not clearly yield "I2S, 16-bit", and sources disagree on the mapping. It reproduces a 440 Hz tone correctly. ESP-ADF uses 0x0C for the same purpose, which is the alternative to try if audio is distorted or absent.
I2S configuration
One I2S port provides both directions, as two channel handles created together. The DMA settings are six descriptors of 240 frames, which at 16 kHz is about 90 ms of buffering per direction.
static void i2s_init(void)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
chan_cfg.dma_desc_num = 6;
chan_cfg.dma_frame_num = 240;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &i2s_tx, &i2s_rx));The slot configuration is 16-bit, mono, left slot. Mono-left is required by the amplifier wiring: the HT6872 is bridge-tied, so the right output must stay at zero for the difference to be non-zero. See the hardware page.
static const i2s_std_slot_config_t slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_16BIT,
.slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = I2S_STD_SLOT_LEFT,
.ws_width = 16,
.ws_pol = true,
.bit_shift = true,
};Both channels are initialised from one configuration and share the pins. dout is the ESP32 output and connects to the codec’s DSDIN; din is the ESP32 input and comes from ASDOUT.
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = slot_cfg,
.gpio_cfg = {
.mclk = I2S_MCLK_PIN,
.bclk = I2S_BCLK_PIN,
.ws = I2S_LRCLK_PIN,
.dout = I2S_DOUT_PIN,
.din = I2S_DIN_PIN,
.invert_flags = { false, false, false },
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(i2s_tx, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(i2s_rx, &std_cfg));
ESP_LOGI(TAG, "I2S ok (TX+RX)");The manufacturer’s example, play_spiffs_mp3_example.c, sets dout = 10 and din = 46 in its manual i2s_init(), which is the reverse of the schematic. That path is unused in the working example, which configures the pins through an ESP-ADF board definition instead, so the error is not visible until the manual path is copied.