Loop Back Test ESP32-S3 dan ES8311

Membuat Loopback Test Mic-ke-Speaker dengan ES8311+NS4150B dan ESP32-S3

Kalau kamu sedang eksplorasi modul audio codec ES8311+NS4150B untuk proyek berbasis ESP32-S3, misalnya untuk voice assistant, perekam suara, atau proyek AI yang butuh input mic dan output speaker sekaligus. Tulisan ini sekedar berbagi pengalaman saya saat menyalakan modul ini dari nol sampai bisa loopback (suara dari mic langsung diputar ke speaker) menggunakan PlatformIO.

Saya tulis juga beberapa jebakan yang saya temui di tengah jalan, siapa tahu bisa menghemat waktu kamu.

Kenalan dulu dengan modulnya

Modul ini punya dua komponen:

  • ES8311, codec audio mono (ADC + DAC dalam satu chip), berkomunikasi lewat I2S untuk data audio dan I2C untuk konfigurasi.
  • NS4150B, amplifier Class-D mono untuk menggerakkan speaker.

Pin yang tersedia di modul: Mic+Speaker, VCC, GND, SD, SDA, SCL, MC, BCK, DO, WS, DI.

Wiring ke ESP32-S3

Berikut pemetaan pin yang saya pakai (ESP32-S3-DevKitC-1):

Pin ModulFungsiGPIO ESP32-S3
VCCPower3V3
GNDGroundGND
SDAI2C data (konfigurasi codec)GPIO1
SCLI2C clock (konfigurasi codec)GPIO2
MC (MCLK)Master clock ke codecGPIO4
BCKBit clock I2SGPIO5
WSWord select I2SGPIO6
DIData masuk ke codec (dari ESP32, jalur speaker)GPIO7
DOData keluar dari codec (ke ESP32, jalur mic)GPIO8
SDEnable amplifier NS4150BGPIO9

Beberapa catatan penting soal wiring:

  • ESP32-S3 berperan sebagai I2S master, dialah yang menghasilkan sinyal MCLK, BCLK, dan WS. ES8311 berperan sebagai slave.
  • ES8311 dan NS4150B berbagi jalur BCK dan WS yang sama, karena keduanya bagian dari satu chip codec, beda dengan setup yang pakai chip ADC dan DAC terpisah.
  • Alamat I2C default ES8311 biasanya 0x18, tapi tergantung posisi pin CE di board bisa juga 0x19. Kalau codec tidak terdeteksi, ini yang pertama dicek.

Percobaan pertama: pakai library, ternyata ribet

Awalnya saya coba pakai library siap pakai (arduino-audio-driver + arduino-audio-tools dari Phil Schatzmann) supaya tidak perlu urus register codec secara manual. Tapi ternyata muncul error linker yang cukup membingungkan:

undefined reference to `audio_driver::TLV320AIC3110::HPL_ANA_VOL_CTRL_ADDR'

Anehnya, saya sama sekali tidak pakai codec TLV320AIC3110, karena saya pakai ES8311. Setelah ditelusuri, “nampaknya” penyebabnya adalah cara library ini mendeklarasikan alamat register sebagai static constexpr di dalam class. Ini baru otomatis “inline” (tidak perlu didefinisikan ulang) kalau di-compile dengan C++17 ke atas, sementara compiler default PlatformIO untuk ESP32-S3 masih pakai C++11.

Solusinya sebenarnya cukup sederhana, tambahkan flag compiler di platformio.ini:

build_unflags = -std=gnu++11
build_flags = -std=gnu++17

Tapi setelah masalah ini beres, saya memutuskan pindah ke pendekatan yang lebih ringan dan lebih mudah dikontrol: menulis driver I2C untuk ES8311 sendiri, langsung pakai driver/i2s.h bawaan ESP-IDF.

Pendekatan kedua: driver manual (yang akhirnya jalan)

Ini pendekatan yang saya pakai sampai akhir, dan hasilnya jauh lebih stabil.

Yang perlu dipahami: ES8311 itu codec asli, bukan sekadar mic atau DAC pasif. Artinya dia perlu diinisialisasi lewat I2C dulu power-up, konfigurasi clock, unmute sebelum data audio di jalur I2S benar-benar terdengar. Kalau langkah ini dilewatkan, wiring bisa 100% benar tapi speaker tetap bisu.

Berikut struktur kode yang saya pakai:

  1. Fungsi es8311_write() dan es8311_read() , komunikasi I2C dasar ke codec.
  2. Fungsi es8311_init_16k() , urutan inisialisasi register codec untuk sample rate 16kHz, format I2S normal, 16-bit, dengan codec dikonfigurasi sebagai slave (karena ESP32-S3 jadi master).
  3. Setup I2S di driver/i2s.h , satu port I2S saja (I2S_NUM_0) dalam mode full-duplex (TX + RX bersamaan), karena ES8311 memang cuma satu chip yang menangani mic dan speaker sekaligus.
  4. Loop utama: baca dari mic (i2s_read), langsung tulis ke speaker (i2s_write), inilah loopback-nya.

#include <Arduino.h>

#include <Wire.h>

#include <driver/i2s.h>

// ================== PINOUT ==================

// I2C (kontrol codec ES8311)

#define I2C_SDA   1

#define I2C_SCL   2

// I2S (audio, satu bus dipakai bareng utk speaker+mic karena satu chip codec)

#define I2S_MCLK  4

#define I2S_BCK   5

#define I2S_WS    6

#define I2S_DO_TO_CODEC   7   // ESP32 TX -> pin “DI” di modul (masuk ke speaker/DAC)

#define I2S_DI_FROM_CODEC 8   // pin “DO” di modul -> ESP32 RX (keluar dari mic/ADC)

#define PA_ENABLE_PIN 9       // pin “SD” di modul, enable ampli NS4150B

// ================== ES8311 I2C ==================

#define ES8311_ADDR 0x18   // 7-bit. Kalau gak ke-detect, coba 0x19

// Register map (dari driver resmi Espressif esp-adf)

#define REG00_RESET        0x00

#define REG01_CLK1         0x01

#define REG02_CLK2         0x02

#define REG03_CLK3         0x03

#define REG04_CLK4         0x04

#define REG05_CLK5         0x05

#define REG06_CLK6         0x06

#define REG07_CLK7         0x07

#define REG08_CLK8         0x08

#define REG09_SDPIN        0x09  // DAC (speaker) I2S format

#define REG0A_SDPOUT       0x0A  // ADC (mic) I2S format

#define REG0B_SYS          0x0B

#define REG0C_SYS          0x0C

#define REG0D_SYS_PWR      0x0D

#define REG0E_SYS_PWR      0x0E

#define REG10_SYS          0x10

#define REG11_SYS          0x11

#define REG12_SYS_DACEN    0x12

#define REG13_SYS          0x13

#define REG14_SYS_MIC      0x14

#define REG15_ADC          0x15

#define REG16_ADC_GAIN     0x16

#define REG17_ADC_VOL      0x17

#define REG1B_ADC          0x1B

#define REG1C_ADC          0x1C

#define REG32_DAC_VOL      0x32

#define REG37_DAC          0x37

#define REG44_GPIO         0x44

#define REG45_GP           0x45

static bool es8311_write(uint8_t reg, uint8_t val) {

  Wire.beginTransmission(ES8311_ADDR);

  Wire.write(reg);

  Wire.write(val);

  return Wire.endTransmission() == 0;

}

// Volume speaker, skala 0-100 (0=paling pelan, 100=paling kencang)

static void setSpeakerVolume(uint8_t percent) {

  if (percent > 100) percent = 100;

  uint8_t regval = map(percent, 0, 100, 0x00, 0xFF);

  es8311_write(REG32_DAC_VOL, regval);

}

// Gain mic, skala 0-100 (0=paling kecil, 100=paling besar/sensitif)

static void setMicGain(uint8_t percent) {

  if (percent > 100) percent = 100;

  uint8_t level = map(percent, 0, 100, 0, 10); // ES8311 punya ~10 step gain (tiap step ±3dB)

  es8311_write(REG16_ADC_GAIN, level);

}

static uint8_t es8311_read(uint8_t reg) {

  Wire.beginTransmission(ES8311_ADDR);

  Wire.write(reg);

  Wire.endTransmission(false);

  Wire.requestFrom((int)ES8311_ADDR, 1);

  if (Wire.available()) return Wire.read();

  return 0;

}

// Init utk MCLK = 256 * 16000Hz = 4.096MHz, format I2S normal, 16-bit, codec = SLAVE

// (ESP32-S3 sbg I2S master yg generate BCK/WS/MCLK)

static bool es8311_init_16k() {

  bool ok = true;

  ok &= es8311_write(REG01_CLK1, 0x30);

  ok &= es8311_write(REG02_CLK2, 0x00);

  ok &= es8311_write(REG03_CLK3, 0x10);

  ok &= es8311_write(REG16_ADC_GAIN, 0x24);

  ok &= es8311_write(REG04_CLK4, 0x10);

  ok &= es8311_write(REG05_CLK5, 0x00);

  ok &= es8311_write(REG0B_SYS, 0x00);

  ok &= es8311_write(REG0C_SYS, 0x00);

  ok &= es8311_write(REG10_SYS, 0x1F);

  ok &= es8311_write(REG11_SYS, 0x7F);

  ok &= es8311_write(REG00_RESET, 0x80);

  // Set codec sbg SLAVE (bit6 = 0), karena ESP32-S3 yg jadi I2S master

  uint8_t regv = es8311_read(REG00_RESET);

  regv &= 0xBF;

  ok &= es8311_write(REG00_RESET, regv);

  ok &= es8311_write(REG01_CLK1, 0x3F);

  // MCLK diambil dari pin MCLK eksternal (bukan dari BCK)

  regv = es8311_read(REG01_CLK1);

  regv &= 0x7F;

  ok &= es8311_write(REG01_CLK1, regv);

  // Koefisien clock utk MCLK 4.096MHz / Fs 16kHz

  // pre_div=1 pre_multi=1 adc_div=1 dac_div=1 fs_mode=0 lrck_h=0x00 lrck_l=0xFF bclk_div=0x04 osr=0x10

  ok &= es8311_write(REG02_CLK2, 0x00);

  ok &= es8311_write(REG05_CLK5, 0x00);

  ok &= es8311_write(REG03_CLK3, 0x10); // fs_mode=0, adc_osr=0x10

  ok &= es8311_write(REG04_CLK4, 0x10); // dac_osr=0x10

  ok &= es8311_write(REG07_CLK7, 0x00); // lrck_h

  ok &= es8311_write(REG08_CLK8, 0xFF); // lrck_l

  ok &= es8311_write(REG06_CLK6, 0x03); // bclk_div=4 -> reg=3

  // MCLK & SCLK tidak diinvert

  regv = es8311_read(REG01_CLK1);

  regv &= ~0x40;

  ok &= es8311_write(REG01_CLK1, regv);

  regv = es8311_read(REG06_CLK6);

  regv &= ~0x20;

  ok &= es8311_write(REG06_CLK6, regv);

  ok &= es8311_write(REG13_SYS, 0x10);

  ok &= es8311_write(REG1B_ADC, 0x0A);

  ok &= es8311_write(REG1C_ADC, 0x6A);

  ok &= es8311_write(REG44_GPIO, 0x08);

  // Format I2S normal, 16-bit, utk jalur DAC (speaker) & ADC (mic)

  ok &= es8311_write(REG09_SDPIN, 0x0C);

  ok &= es8311_write(REG0A_SDPOUT, 0x0C);

  // Start ADC + DAC

  ok &= es8311_write(REG32_DAC_VOL, 0x80); // volume speaker ~-32dB, turunkan lagi kalau masih feedback

  ok &= es8311_write(REG17_ADC_VOL, 0xBF); // volume mic max

  ok &= es8311_write(REG0E_SYS_PWR, 0x02);

  ok &= es8311_write(REG12_SYS_DACEN, 0x00); // un-mute DAC

  ok &= es8311_write(REG14_SYS_MIC, 0x1A);   // analog mic (bukan PDM)

  ok &= es8311_write(REG0D_SYS_PWR, 0x01);   // power up utama

  ok &= es8311_write(REG15_ADC, 0x40);

  ok &= es8311_write(REG37_DAC, 0x48);

  ok &= es8311_write(REG45_GP, 0x00);

  return ok;

}

static void setupI2S() {

  i2s_config_t cfg = {

    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX),

    .sample_rate = 16000,

    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,

    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,

    .communication_format = I2S_COMM_FORMAT_STAND_I2S,

    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,

    .dma_buf_count = 8,

    .dma_buf_len = 64,

    .use_apll = true,

    .tx_desc_auto_clear = true,

    .mclk_multiple = I2S_MCLK_MULTIPLE_256

  };

  i2s_pin_config_t pins = {

    .mck_io_num = I2S_MCLK,

    .bck_io_num = I2S_BCK,

    .ws_io_num = I2S_WS,

    .data_out_num = I2S_DO_TO_CODEC,

    .data_in_num = I2S_DI_FROM_CODEC

  };

  i2s_driver_install(I2S_NUM_0, &cfg, 0, NULL);

  i2s_set_pin(I2S_NUM_0, &pins);

}

void setup() {

  Serial.begin(115200);

  delay(300);

  Serial.println(“=== ES8311 Loopback (raw driver) ===”);

  pinMode(PA_ENABLE_PIN, OUTPUT);

  digitalWrite(PA_ENABLE_PIN, HIGH); // enable ampli NS4150B

  Wire.begin(I2C_SDA, I2C_SCL);

  Wire.setClock(100000);

  // cek codec kedetek

  Wire.beginTransmission(ES8311_ADDR);

  if (Wire.endTransmission() != 0) {

    Serial.println(“ES8311 TIDAK terdeteksi di 0x18! Coba 0x19, atau cek wiring SDA/SCL.”);

  } else {

    Serial.println(“ES8311 terdeteksi di I2C.”);

  }

  if (!es8311_init_16k()) {

    Serial.println(“Ada write I2C yang gagal saat init ES8311 (cek wiring).”);

  } else {

    Serial.println(“ES8311 init OK.”);

  }

  setSpeakerVolume(70); // 0-100, atur sesuai kebutuhan

  setMicGain(50);       // 0-100, atur sesuai kebutuhan

  setupI2S();

  Serial.println(“I2S siap. Bicara dekat mic…”);

}

void loop() {

  static int16_t buffer[64];

  size_t bytes_read = 0, bytes_written = 0;

  i2s_read(I2S_NUM_0, buffer, sizeof(buffer), &bytes_read, portMAX_DELAY);

  i2s_write(I2S_NUM_0, buffer, bytes_read, &bytes_written, portMAX_DELAY);

}

Dan platformio.ini-nya jauh lebih sederhana, tidak butuh library eksternal sama sekali:

[env:esp32-s3-devkitc-1]

platform = espressif32

board = esp32-s3-devkitc-1

framework = arduino

monitor_speed = 115200

build_flags = -DCORE_DEBUG_LEVEL=3

Masalah feedback (dan cara mengatasinya)

Begitu loopback pertama kali menyala, muncul masalah klasik: feedback/squeal, sama seperti mic sound system yang terlalu dekat dengan speaker. Ini wajar terjadi karena mic dan speaker saling “mendengar” satu sama lain secara fisik. Bukan bug software.

Dua hal yang membantu:

  1. Turunkan volume speaker. Register REG32 di ES8311 mengontrol volume DAC, dengan resolusi 0.5dB per step (0x00 = paling pelan, 0xFF = paling kencang, 0xBF ≈ 0dB netral).
  2. Jauhkan posisi mic dari speaker secara fisik, atau beri peredam di antaranya.

Supaya lebih gampang diatur, saya buat dua fungsi bantu yang menerima skala 0–100 (bukan hex) untuk volume speaker dan gain mic:

Contoh pemakaian:

setSpeakerVolume(40); // 0-100
setMicGain(50);       // 0-100

Setelah volume speaker diturunkan dan mic dijauhkan sedikit, feedback hilang dan loopback berjalan bersih.

Penutup

Beberapa pelajaran yang saya bawa dari eksperimen ini:

  • Untuk codec audio “beneran” seperti ES8311, jangan lupa langkah inisialisasi I2C — ini yang paling sering terlewat.
  • Kalau pakai library pihak ketiga dan muncul error linker aneh yang menyebut komponen yang tidak kamu pakai, curigai masalah standar C++ compiler, bukan cuma soal wiring.
  • Feedback pada loopback mic-speaker itu gejala akustik, bukan bug . Solusinya kombinasi software (volume) dan fisik (jarak).

Semoga tulisan ini membantu kalau kamu sedang mengutak-atik modul yang sama. Kalau ada pertanyaan, silakan tinggalkan komentar di bawah.


Ditulis berdasarkan pengalaman langsung menyambungkan ES8311+NS4150B ke ESP32-S3-DevKitC-1 menggunakan PlatformIO/Arduino framework.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *