viniciusdias.tech

research

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().

play_tone/main/main.c — i2c_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.

play_tone/main/main.c — es8388_write_reg()
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.

play_tone/main/main.c — es8388_read_reg()
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.

play_tone/main/main.c — i2c_scan()
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.

play_tone/main/main.c — es8388_init()
    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.

play_tone/main/main.c — i2s_init()
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.

play_tone/main/main.c
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.

play_tone/main/main.c — i2s_init()
    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.