Welcome to ESP32 Core Board repository
1.I2S Microphone Recording and U-Disk Playback
I2S Microphone Recording and U-Disk Playback (Requires Two Type-C Cables)
I2S Microphone Recording and U-Disk Playback (Requires Two Type-C Cables)1. Overview1.1 Experiment Objective2. Core Concepts2.1 USB MSC Protocol2.2 PSRAM FAT16 Virtual Disk2.3 ES8311 Codec ADC Path2.4 WAV File Format2.5 Button Debounce State Machine3. Hardware Features and Dependencies3.1 ESP32-S3 USB OTG Features3.2 ES8311 Audio Codec Specifications3.3 Pin Assignment Table3.4 Arduino IDE Configuration3.5 Required Arduino Libraries4. Program Flow4.1 Overall Flowchart4.2 Audio System Initialization Flow (audioSystemInit)4.3 Recording State Machine4.4 PC Interaction After USB Connection5. Source Code Analysis5.1 Directory Structure5.2 Main Program: Arduino_Recorder_USB_MSC.ino5.2.1 File Header and Includes5.2.2 Constants and Global Variables5.2.3 MSC Callback Functions5.2.4 setup() -- System Initialization5.2.5 loop() -- Main Loop5.3 Audio System: AudioSystem.h / AudioSystem.cpp5.3.1 Audio Parameters and Initialization5.3.2 I2C Bus Initialization and Scan5.3.3 ES8311 Hardware Reset Sequence (es8311HardReset)5.3.4 ADC Path Start (es8311StartADC)5.3.5 Mic Gain Setting5.4 FAT16 Virtual Disk: Fat16Disk.h / Fat16Disk.cpp5.4.1 Class Interface5.4.2 begin -- Create FAT16 Virtual Disk5.4.3 resetForRecord -- Prepare for Recording5.4.4 publishFile -- Register Data as File5.5 Recorder: Recorder.h / Recorder.cpp5.5.1 Class Interface5.5.2 WAV Header Template5.5.3 start -- Begin Recording5.5.4 tick -- Main Loop Recording5.5.5 stop -- Stop Recording5.6 Button: Button.h / Button.cpp5.6.1 Complete tick() Implementation5.7 LED: Led.h / Led.cpp5.7.1 Class Interface5.7.2 Rainbow Effect6. Operation Flow6.1 IDE Configuration6.2 Compile and Flash6.3 Operation Steps6.4 Long Press Force Stop7. FAQ7.1 Version Compatibility Quick ReferenceAppendix A: Complete Code IndexAppendix B: ES8311 Register Quick Reference
1. Overview
1.1 Experiment Objective
This experiment implements a ESP32-S3-based I2S microphone recorder that writes recording data directly to a FAT16 virtual disk constructed in PSRAM, then simulates the ESP32-S3 as a USB flash drive (U disk) through the USB MSC (Mass Storage Class) protocol. When the user connects the computer via USB cable, the PC recognizes a FAT16-formatted U disk containing a playable .WAV audio file.
Complete Signal Chain
[MIC] --> [ES8311 ADC] --> [I2S DIN] --> [ESP32-S3 I2S RX]|[PSRAM FAT16 Disk]|[USB MSC] --> [PC recognizes as USB drive]
2. Core Concepts
2.1 USB MSC Protocol
USB MSC is a USB device class protocol that allows embedded devices to simulate standard storage devices (U disk, external hard drive, etc.). The PC communicates with the device using the standard SCSI command set.
SCSI Command Mapping for This Project:
| SCSI Command | Callback | This Project Implementation |
|---|---|---|
| READ(10) | onRead(lba, offset, buf, size) | Copy data from PSRAM disk image via memcpy |
| WRITE(10) | onWrite(lba, offset, buf, size) | Copy data to PSRAM disk image via memcpy |
| START STOP UNIT | onStartStop(power, start, eject) | Returns true |
MSC devices don't need real Flash chips - just build a FAT image in memory and expose it to the PC via
onRead/onWritecallbacks.
2.2 PSRAM FAT16 Virtual Disk
This experiment builds a FAT16 filesystem image (8MB / 16384 sectors) in ESP32-S3 PSRAM. Layout: boot(1) + FAT(64) + root(1) + data(16318). During recording, I2S data is written directly to data sectors, exposed to PC via USB MSC callbacks - no external TF card or SPI Flash required.
FAT16 is chosen over FAT32 because the FAT table is smaller, saving PSRAM; the root directory is fixed at 1 sector (16 directory entries), and this project only needs 1 WAV file, which is sufficient.
2.3 ES8311 Codec ADC Path
The ES8311 is a low-power audio codec from Sinx. This experiment uses only its ADC recording path:
xxxxxxxxxx[MIC analog signal] --> [ES8311 PGA programmable gain amplifier]|[Sigma-Delta ADC modulator]|[Digital decimation filter]|[I2S serial output SDOUT] --> [ESP32-S3 I2S DIN]
Clock Tree: MCLK 4.096MHz → pre_div=1 → ADC_MOD_CLK=4.096MHz → adc_osr=256 → Fs=16kHz. BCLK = 4.096MHz/4 = 1.024MHz, WS period 64 BCLK, per channel 32 BCLK (16bit data + 16bit padding), I2S rate 64KB/s.
Why choose 16kHz sample rate?
Voice frequency coverage: 16kHz sample rate can capture 8kHz frequency (main voice energy is 300-3400Hz)
Storage efficiency: 16kHz/16bit/mono = 32KB/s, 120 seconds ~ 3.84MB, 8MB PSRAM is more than enough
WAV format compatibility: All players support 16kHz sample rate
2.4 WAV File Format
WAV is based on RIFF specification, 44-byte header + PCM data. This project's WAV header definition:
xxxxxxxxxx// 1-byte alignment, no paddingstruct WavHdr { char riff[4]; // "RIFF" uint32_t fileLen; // Total file length - 8 char wave[4]; // "WAVE" char fmt[4]; // "fmt " uint32_t fmtLen; // 16 uint16_t audioFormat; // 1 = PCM uint16_t channels; // 1 = mono uint32_t sampleRate; // 16000 uint32_t byteRate; // 16000 x 1 x 2 = 32000 uint16_t blockAlign; // 1 x 2 = 2 uint16_t bitsPerSample; // 16 char data[4]; // "data" uint32_t dataLen; // Total PCM data bytes};// Restore default alignmentKey Technique: Before recording starts, write a "skeleton" WAV header (fileLen=0, dataLen=0). After recording ends, backfill the real file length and data length. This ensures the PC always sees a correct WAV file header during MSC callbacks.
2.5 Button Debounce State Machine
Button FSM handles mechanical bounce (5-30ms) and distinguishes short/long presses. IDLE → DEBOUNCE(30ms) → PRESS: release <800ms = short press, ≥800ms or held to threshold = long press (triggers early, doesn't wait for release).
3. Hardware Features and Dependencies
3.1 ESP32-S3 USB OTG Features
| Feature | Parameter |
|---|---|
| USB Controller | USB 2.0 OTG Full-Speed (12Mbps) |
| Built-in PHY | Yes (no external USB transceiver needed) |
| USB Pins | GPIO19 (D-), GPIO20 (D+) |
| TinyUSB Support | Built-in (integrated by default in ESP32 Arduino Core v2.0+) |
| Power Mode | Bus-powered, max 500mA |
ESP32-S3's USB OTG is an independent peripheral, separate from the UART download port. When USB MSC mode is enabled, USB CDC (Serial) cannot be used simultaneously - debugging must use UART0 (
Serial0).
3.2 ES8311 Audio Codec Specifications
| Feature | Parameter |
|---|---|
| ADC SNR | 93dBA |
| ADC Sample Rate | 8kHz ~ 96kHz |
| Supported Formats | I2S, Left-Justified, Right-Justified, DSP/PCM |
| Microphone Gain | 0/6/12/18/24/30/36/42 dB (8 programmable levels) |
| Operating Voltage | 1.8V ~ 3.3V (analog), 1.8V (digital) |
| I2C Control Interface | 7-bit address 0x18 (AD0=0, AD1=0) |
| Package | QFN-32 (4x4mm) |
3.3 Pin Assignment Table
| Function | ESP32-S3 GPIO | Direction | Description |
|---|---|---|---|
| I2C SCL | GPIO 42 | Output | ES8311 register configuration clock (100kHz) |
| I2C SDA | GPIO 41 | Bidirectional | ES8311 register configuration data |
| I2S MCLK | GPIO 15 | Output | Master clock 4.096MHz -> ES8311 |
| I2S BCLK | GPIO 16 | Output | I2S bit clock 1.024MHz |
| I2S WS (LRCLK) | GPIO 18 | Output | I2S word select/LR channel 16kHz |
| I2S DOUT | GPIO 8 | Output | I2S data output (DAC playback, not used in this experiment) |
| I2S DIN | GPIO 17 | Input | I2S data input (ADC recording) |
| PA_EN | GPIO 7 | Output | ES8311 enable (HIGH = power on/active) |
| BOOT Button | GPIO 0 | Input (pull-up) | User operation button (active low) |
| WS2812 LED | GPIO 48 | Output | Status indicator RGB LED |
| USB D- | GPIO 19 | Bidirectional | USB OTG differential data negative |
| USB D+ | GPIO 20 | Bidirectional | USB OTG differential data positive |
3.4 Arduino IDE Configuration
This is the most critical configuration step!
| Configuration | Value | Description |
|---|---|---|
| Board | ESP32S3 Dev Module | |
| USB Mode | TinyUSB | MSC depends on TinyUSB protocol stack |
| USB CDC On Boot | Disabled | USB OTG occupied by MSC, CDC cannot be used simultaneously |
| PSRAM | OPI PSRAM | Enable 8MB/16MB Octal PSRAM |
| Flash Size | 16MB | Based on onboard Flash |
| Partition Scheme | Default 4MB with spiffs | |
| Upload Mode | UART0 / Hardware CDC | Use onboard USB-UART for download |
Why must USB CDC On Boot be disabled?
ESP32-S3 has only one USB OTG peripheral (GPIO19/20) - USB Mode = TinyUSB hands OTG to TinyUSB protocol stack (required for MSC). At this point Serial is unavailable, debugging must go through onboard USB-UART bridge chip's Serial0.
3.5 Required Arduino Libraries
| Library | Version | Purpose | Reference Tutorial |
|---|---|---|---|
| ESP32 Arduino Core | v3.3.8+ | Basic framework, built-in TinyUSB + SPIFFS + Wire + ESP_I2S | Arduino → 1. Before Development → 1. Before Development → Environment Setup (Arduino-ESP32) |
| Adafruit NeoPixel | ^1.12.0 | WS2812 RGB LED driver | 2. Basic Peripherals → 9.RGB LED Experiment → RGB LED Experiment |
| USB/USBMSC (built-in) | — | TinyUSB USB MSC simulated U disk | 3. Serial Communication and Storage System → 5.USB Mass Storage Simulation → USB Mass Storage |
4. Program Flow
4.1 Overall Flowchart
xsetup():1. Serial0.begin(115200) → Debug serial2. led.begin() → WS2812 LED3. audioSystemInit() → ES8311 + I2S audio chain4. disk.begin(16384) → PSRAM FAT16 virtual disk5. msc.onRead/onWrite/onStartStop registration → USB MSC callbacks6. USB.begin() → USB OTG MSC device stack7. btn.begin(0) → BOOT button (long press 800ms/debounce 30ms)loop():rec.tick() → I2S read → PSRAM writebtn.tick() → SHORT? Start/stop recording : LONG? Force stop
4.2 Audio System Initialization Flow (audioSystemInit)
codecPowerOn()→ GPIO7 HIGH, ES8311 power oni2cBusInit()+i2cBusScan()→ I2C bus + scan confirm 0x18I2S.setPins()+I2S.begin()+I2S.configureRX()→ Start MCLK (4.096MHz) + RX stereo→monoes8311HardReset()+es8311InitADC()→ Clock tree + ADC path + Mic gain (24dB)
Step 3 must come before step 4: ES8311 initialization requires MCLK signal as clock source.
4.3 Recording State Machine
xxxxxxxxxxIDLE (LED OFF) --short press--> RECORDING (LED GREEN, I2S→PSRAM) --short press/full--> SAVING (LED RED, patch WAV header) --> READY (LED RAINBOW, file readable)
4.4 PC Interaction After USB Connection
USB enumeration → INQUIRY/READ_CAPACITY → PC reads boot sector+FAT+root directory → Recognize RECORD.WAV → User double-click to play → onRead() copies data from PSRAM.
5. Source Code Analysis
Full source code: Source code → Arduino → 4.Audio → Arduino_Recorder_USB_MSC → Arduino_Recorder_USB_MSC.ino
5.1 Directory Structure
Arduino_Recorder_USB_MSC.ino (main program) / AudioSystem (I2C+I2S+ES8311) / Recorder (WAV+I2S read) / Fat16Disk (FAT16 virtual disk) / Button (debounce state machine) / Led (WS2812)
5.2 Main Program: Arduino_Recorder_USB_MSC.ino
5.2.1 File Header and Includes
The main program includes 6 header files, covering all subsystems: audio system, FAT16 virtual disk, USB MSC, button, and LED.
xxxxxxxxxx// TinyUSB device framework// MSC class driver// Audio system (I2C + I2S + ES8311)// PSRAM FAT16 virtual disk// Debounced button// WS2812 RGB LED// Recorder state machine5.2.2 Constants and Global Variables
Global constants define disk size (8MB / 16384 sectors) and max recording duration (120s). Global variables hold instances of each subsystem. The Recorder constructor lambda simulates USB drive re-plug after recording completes to notify PC to refresh file list.
xxxxxxxxxx/* ===== Constants ===== */// 8MB (16384 x 512B)// Max record seconds// WS2812 data pin
/* ===== Globals ===== */static Fat16Disk disk; // FAT16 virtual disk instancestatic USBMSC msc; // USB MSC device instancestatic Button btn; // Button instancestatic Led led(PIN_LED); // LED instance (GPIO48)static Recorder rec(disk, led, REC_SEC, []() { msc.mediaPresent(false); delay(80); msc.mediaPresent(true); // Simulate USB drive re-plug to notify PC to refresh file list});5.2.3 MSC Callback Functions
The three callbacks are the core of MSC U disk simulation. PC calls onRead() when reading sectors (memcpy from PSRAM); onWrite() when writing (memcpy back); onStartStop accepts all mount/eject operations.
xxxxxxxxxx/* ===== MSC Callbacks ===== */static int32_t onRead(uint32_t lba, uint32_t offset, void *buf, uint32_t size) { // lba=sector number, offset=byte offset within sector // buf=PC read destination, size=bytes to read
// Bounds check if (!disk.ready() || lba >= disk.sectorCount()) return 0; // Copy data from PSRAM to PC buffer memcpy(buf, disk.data() + (size_t)lba * 512 + offset, size); return size; // Return bytes transferred}
static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t *buf, uint32_t size) { // Parameters symmetric to onRead, buf is data from PC
// Bounds check if (!disk.ready() || lba >= disk.sectorCount()) return 0; // Copy PC data to PSRAM memcpy(disk.data() + (size_t)lba * 512 + offset, buf, size); return size;}
static bool onStartStop(uint8_t power, bool start, bool eject) { // start=load media, eject=eject media return true; // Always accept}5.2.4 setup() -- System Initialization
setup() initializes 7 subsystems in strict order: serial→LED→audio→virtual disk→MSC→USB→button. Each step has clear dependencies - audio must be ready before disk creation (PSRAM available), MSC callbacks must be registered before USB starts.
xxxxxxxxxxvoid setup() { // 1. Debug serial (UART0, NOT USB CDC) Serial0.begin(115200); delay(300); Serial0.println("\n=== Rec+USB MSC ===");
// 2. LED init led.begin();
// 3. Audio system init audioSystemInit();
// 4. Create FAT16 virtual disk (8MB, label="AUDIODISK") if (!disk.begin(DISK_SECTORS, "AUDIODISK")) { Serial0.println("[FATAL] No PSRAM"); while (1); }
// 5. Configure MSC device info & callbacks msc.vendorID("ESP32"); msc.productID("Recorder"); msc.productRevision("1.0"); msc.onStartStop(onStartStop); msc.onRead(onRead); msc.onWrite(onWrite); msc.mediaPresent(true); // Declare media present msc.begin(DISK_SECTORS, 512); // Configure sector count & size
// 6. Start USB device stack USB.begin();
// 7. Print hint + button init Serial0.printf("[USB] FAT16 %luKB MSC *Plug USB cable*\n", (unsigned long)DISK_SECTORS / 2); btn.begin(PIN_BUTTON); // GPIO0, long press 800ms / debounce 30ms}5.2.5 loop() -- Main Loop
loop() is extremely concise - each frame drives recorder tick() (returns immediately if not recording) and button state machine, dispatching start/stop based on events. All complex logic is encapsulated in modules, main program only routes.
xxxxxxxxxxvoid loop() { rec.tick(); // 1. Recorder tick (returns immediately if not recording)
Button::Event e = btn.tick(); // 2. Button FSM tick if (e == Button::SHORT) { // 3. Short press: start/stop recording if (rec.busy()) rec.stop(); else rec.start(); } else if (e == Button::LONG) { // 4. Long press: force stop recording if (rec.busy()) rec.stop(); }}5.3 Audio System: AudioSystem.h / AudioSystem.cpp
5.3.1 Audio Parameters and Initialization
audioSystemInit() drives the entire recording chain in sequence: power on → I2C → I2S → ES8311 reset → ADC init. I2S must start before ES8311 (ES8311 is slave, depends on ESP32 for MCLK). I2S.configureRX() enables STEREO_TO_MONO hardware conversion, automatically averaging left/right channels - no software processing needed.
xxxxxxxxxx// Sample rate// MCLK = Fs x 256 = 4.096MHz// Microphone gain
void audioSystemInit() { Serial0.println("===== Audio System (ADC Only) =====");
// 1. Power on codec codecPowerOn();
// 2 & 3. I2C bus init + device scan i2cBusInit(); i2cBusScan();
// 4, 5 & 6. I2S start, provides MCLK to ES8311 I2S.setPins(PIN_I2S_BCLK, PIN_I2S_WS, PIN_I2S_DOUT, PIN_I2S_DIN, PIN_I2S_MCLK); if (!I2S.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) { Serial0.println("[I2S] ERROR"); return; } delay(100); I2S.configureRX(SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO, I2S_RX_TRANSFORM_16_STEREO_TO_MONO); // Stereo to mono conversion
// 7 & 8. ES8311 hardware reset + ADC path init es8311HardReset(); delay(50); es8311InitADC();
Serial0.println("===== Recorder Ready =====");}5.3.2 I2C Bus Initialization and Scan
I2C uses Arduino standard Wire library. After Wire.setPins() specifies pins and Wire.begin() starts, scan addresses 1~126 to confirm ES8311 responds at 0x18.
xxxxxxxxxxvoid i2cBusInit() { Wire.setPins(PIN_I2C_SDA, PIN_I2C_SCL); // Set I2C pins Wire.begin(); // Start I2C master mode delay(50); i2c_ready = true; Serial0.printf("[I2C] SDA:%d SCL:%d\n", PIN_I2C_SDA, PIN_I2C_SCL);}
void i2cBusScan() { if (!i2c_ready) return; Serial0.println("===== I2C Scan ====="); int found = 0; for (uint8_t a = 1; a < 127; a++) { Wire.beginTransmission(a); // Send start condition + device address if (Wire.endTransmission() == 0) { // Returns 0 = device ACK Serial0.printf(" [OK] 0x%02X\n", a); found++; } delay(3); } Serial0.printf("===== %d device(s) =====\n", found);}ES8311 register read/write foundation - write uses Wire.beginTransmission + Wire.write + Wire.endTransmission; read writes register address first (no STOP), then issues read request for 1 byte.
xxxxxxxxxxuint8_t es8311ReadReg(uint8_t reg) { Wire.beginTransmission(ES8311_ADDR); // START + device address(W) Wire.write(reg); // Send register address Wire.endTransmission(false); // No STOP → RSTART Wire.requestFrom((uint8_t)ES8311_ADDR, (uint8_t)1); // RSTART + device address(R) + read 1 byte return Wire.available() ? Wire.read() : 0;}5.3.3 ES8311 Hardware Reset Sequence (es8311HardReset)
Complete hardware reset sequence must be executed after ES8311 power on. Register writes are divided into 4 groups, strictly following chip manual order: analog power→clock tree→slave mode→ADC filter.
xxxxxxxxxxvoid es8311HardReset() { // -- 1. Analog power + GPIO reference -- es8311WriteReg(ES8311_REG0D_SYSTEM3, 0xFA); // Enable analog LDO & reference es8311WriteReg(ES8311_REG44_GPIO1, 0x08); // Set GPIO reference = 1.8V es8311WriteReg(ES8311_REG44_GPIO1, 0x08); // Double-write anti-I2C noise
// -- 2. Clock tree -- es8311WriteReg(ES8311_REG01_CLK_MGR1, 0x30); // Master clock config es8311WriteReg(ES8311_REG02_CLK_MGR2, 0x00); // pre_div=1, multi=x1 es8311WriteReg(ES8311_REG03_CLK_MGR3, 0x10); // fs_mode=0, adc_osr=16 es8311WriteReg(ES8311_REG04_CLK_MGR4, 0x10); // dac_osr initial es8311WriteReg(ES8311_REG05_CLK_MGR5, 0x00); // adc_div=1, dac_div=1 es8311WriteReg(ES8311_REG16_ADC2, 0x24); // ADC initial gain es8311WriteReg(ES8311_REG0B_SYSTEM1, 0x00); // System control 1 es8311WriteReg(ES8311_REG0C_SYSTEM2, 0x00); // System control 2 es8311WriteReg(ES8311_REG10_SYSTEM6, 0x1F); // System control 6 es8311WriteReg(ES8311_REG11_SYSTEM7, 0x7F); // System control 7 es8311WriteReg(ES8311_REG00_RESET, 0x80); // Release reset (bit7=1)
// -- 3. Slave mode + external MCLK -- es8311WriteReg(ES8311_REG00_RESET, es8311ReadReg(ES8311_REG00_RESET) & 0xBF); // Clear bit6 es8311WriteReg(ES8311_REG01_CLK_MGR1, 0x3F & 0x7F); // MCLK from external es8311WriteReg(ES8311_REG06_CLK_MGR6, es8311ReadReg(ES8311_REG06_CLK_MGR6) & ~0x20); // No BCLK invert
// -- 4. ADC analog + filters -- es8311WriteReg(ES8311_REG13_SYSTEM9, 0x10); // System control 9 es8311WriteReg(ES8311_REG1B_ADC7, 0x0A); // High-pass filter (HPF) es8311WriteReg(ES8311_REG1C_ADC8, 0x6A); // Equalizer (EQ) es8311WriteReg(ES8311_REG44_GPIO1, 0x58); // Enable internal DAC reference
delay(10); codec_ready = true; Serial0.println("[ES8311] HW init done");}5.3.4 ADC Path Start (es8311StartADC)
Called after hardware reset completes to start ADC path: release clock gating, enable ADC clock, power on digital/analog power, start ADC/DAC ramp, configure PGA gain and input channel.
xxxxxxxxxxstatic void es8311StartADC() { // Release clock gating es8311WriteReg(ES8311_REG00_RESET, es8311ReadReg(ES8311_REG00_RESET) & 0xBF);
uint8_t dac = es8311ReadReg(ES8311_REG09_SDP_IN); // Read DAC I2S format uint8_t adc = es8311ReadReg(ES8311_REG0A_SDP_OUT); // Read ADC I2S format dac &= ~(0x40); // Enable DAC clock adc &= ~(0x40); // Enable ADC clock es8311WriteReg(ES8311_REG09_SDP_IN, dac); es8311WriteReg(ES8311_REG0A_SDP_OUT, adc);
es8311WriteReg(ES8311_REG17_ADC3, 0xBF); // ADC digital volume max es8311WriteReg(ES8311_REG0E_SYSTEM4, 0x02); // Digital power enable es8311WriteReg(ES8311_REG12_SYSTEM8, 0x00); // DAC enable es8311WriteReg(ES8311_REG14_SYSTEM10, 0x1A); // PGA gain + ADC input enable es8311WriteReg(ES8311_REG0D_SYSTEM3, 0x01); // Analog power final config es8311WriteReg(ES8311_REG15_ADC1, 0x40); // ADC ramp start es8311WriteReg(ES8311_REG37_DAC7, 0x08); // DAC ramp start es8311WriteReg(ES8311_REG45_GPIO2, 0x00); // GPIO2 output control}5.3.5 Mic Gain Setting
Mic gain supports 8 levels from 0~42dB. es8311SetMicGain() converts dB value to register value 0~7 via lookup table, writes to REG16_ADC2. Default is 24dB, suitable for close-range voice acquisition.
xxxxxxxxxx/* * Mic gain mapping table: * +----------+-----+-----------+ * | Input dB | Reg | Actual | * +----------+-----+-----------+ * | < 6 | 0 | 0dB | * | 6~11 | 1 | 6dB | * | 12~17 | 2 | 12dB | * | 18~23 | 3 | 18dB | * | 24~29 | 4 | 24dB | * | 30~35 | 5 | 30dB | * | 36~41 | 6 | 36dB | * | >= 42 | 7 | 42dB | * +----------+-----+-----------+ */void es8311SetMicGain(uint8_t db) { uint8_t g; if (db < 6) g = 0; else if (db < 12) g = 1; else if (db < 18) g = 2; else if (db < 24) g = 3; else if (db < 30) g = 4; else if (db < 36) g = 5; else if (db < 42) g = 6; else g = 7; es8311WriteReg(ES8311_REG16_ADC2, g);}5.4 FAT16 Virtual Disk: Fat16Disk.h / Fat16Disk.cpp
5.4.1 Class Interface
Fat16Disk encapsulates all logic for building FAT16 filesystem image in PSRAM, exposing begin()/resetForRecord()/publishFile() core methods and data()/ready() status query interfaces.
xxxxxxxxxxclass Fat16Disk {public: bool begin(uint32_t sectors, const char* label); // Alloc PSRAM + format FAT16 void resetForRecord(const char* label); // Reset to recording-ready state void publishFile(uint8_t entry, const char* name, // Register data area as FAT16 file const char* ext, uint32_t size);
uint8_t* data() const { return _disk; } // PSRAM disk base pointer uint32_t sectorCount() const { return _sectors; } // Total sectors uint16_t dataOffset() const { return _dataOfs; } // Data area start sector bool ready() const { return _disk != nullptr; } // PSRAM allocation success?
private: uint8_t* _disk = nullptr; // PSRAM disk image base address uint32_t _sectors = 0; // Total sectors uint16_t _fatSecs = 0; // FAT table sectors uint16_t _dataOfs = 0; // Cluster-2 physical sector};5.4.2 begin -- Create FAT16 Virtual Disk
calloc 8MB disk image from PSRAM, calculate FAT sectors and data area offset (boot(1) + FAT + root(1)), then write boot sector BPB, FAT table (all clusters marked FREE), and root directory label in sequence.
xxxxxxxxxxbool Fat16Disk::begin(uint32_t sectors, const char* label) { // 1. Alloc disk image from PSRAM (calloc ensures zeroed) _disk = (uint8_t*)calloc(1, (size_t)sectors * 512); if (!_disk) return false; // PSRAM allocation failed _sectors = sectors;
// 2. Calculate FAT16 structure parameters _fatSecs = fat_sectors_per_alloc_table(sectors, true); // FAT16, 16-bit entries _dataOfs = 1 + _fatSecs + 1; // boot(1) + FAT + root(1)
// 3. Write boot sector BPB fat_add_boot_sector(_disk, sectors, _fatSecs, "FAT16", label, 0x1234);
// 4. Write FAT table (all clusters FREE) fat_add_table(_disk, (fat_boot_sector_t*)_disk, true);
// 5. Write root directory label fat_add_label(_disk, label);
Serial0.printf("[DISK] FAT16 %luKB | boot(1)+FAT(%d)+root(1)+data(%d sectors)\n", (unsigned long)sectors / 2, _fatSecs, sectors - _dataOfs); return true;}5.4.3 resetForRecord -- Prepare for Recording
Called before each new recording, rebuilds FAT table (all clusters FREE), rewrites root directory label, clears remaining directory entries. Ensures disk is clean with no residue, PC only sees new file.
xxxxxxxxxxvoid Fat16Disk::resetForRecord(const char* label) { if (!_disk) return;
// 1. Rebuild FAT table (all clusters FREE) fat_add_table(_disk, (fat_boot_sector_t*)_disk, true);
// 2. Rebuild label fat_add_label(_disk, label);
// 3. Clear remaining directory entries memset(_disk + (size_t)(1 + _fatSecs) * 512 + 32, 0, 15 * 32);}5.4.4 publishFile -- Register Data as File
Called after recording ends. Uses TinyUSB FAT helper to create 8.3 format file entry in root directory (filename + extension + size), internally writes 32-byte directory entry and marks cluster allocation chain in FAT table. PC can now see the file.
xxxxxxxxxxvoid Fat16Disk::publishFile(uint8_t entry, const char* name, const char* ext, uint32_t size) { if (!_disk) return;
// TinyUSB FAT helper: create root dir file entry // entry=dir index, name=8.3 filename // ext=extension, size=file size // 2=starting cluster, true=FAT16 fat_add_root_file(_disk, entry, name, ext, size, 2, true); // Internally: write 32-byte dir entry + FAT chain}5.5 Recorder: Recorder.h / Recorder.cpp
5.5.1 Class Interface
Recorder class encapsulates recording state machine, providing start()/stop()/tick() core methods and busy() status query. Constructor receives disk, LED, max duration, and completion callback four dependencies.
xxxxxxxxxxusing RecorderCallback = void (*)(); // Recording complete callback type
class Recorder {public: Recorder(Fat16Disk& d, Led& l, uint16_t maxSec, RecorderCallback onPublished = nullptr); void start(); // Start recording void stop(); // Stop recording, commit file, call onPublished void tick(); // Called by main loop, read I2S -> write PSRAM bool busy() const { return _recording; } // Is recording?
private: Fat16Disk& _disk; Led& _led; uint16_t _maxSec; RecorderCallback _onPublished; bool _recording = false; uint32_t _wavTotal = 0; // Recorded PCM total bytes uint32_t _wavMax = 0; // Max recording bytes};5.5.2 WAV Header Template
WAV header template - written to data area beginning before recording starts, fileLen and dataLen fields initially 0. Real values backfilled after recording ends, ensuring PC always sees correct WAV header when reading file.
xxxxxxxxxxstatic const WavHdr kHdr = { {'R','I','F','F'}, 0, // RIFF ID + file length (patch later) {'W','A','V','E'}, // WAVE format {'f','m','t',' '}, 16, // fmt chunk, len=16 1, 1, SAMPLE_RATE, // PCM, 1 channel, 16kHz SAMPLE_RATE * 1 * 2, // Byte rate: 16000*1*2 = 32000 B/s (uint16_t)(1 * 2), // Block align: 1ch * 2B = 2 16, // Bits per sample: 16 {'d','a','t','a'}, 0 // data chunk + data length (patch later)};5.5.3 start -- Begin Recording
First reset disk to clean state, then write WAV header skeleton to data area start, finally initialize PCM counter and light green LED. _wavMax = maxSec × byteRate calculates max recording bytes.
xxxxxxxxxxvoid Recorder::start() { // Already recording or disk not ready if (_recording || !_disk.ready()) return;
// 1. Reset FAT16 disk to clean state _disk.resetForRecord("AUDIODISK");
// 2. Write WAV header template to data area start memcpy(_disk.data() + (size_t)_disk.dataOffset() * 512, &kHdr, sizeof(kHdr));
// 3. Initialize counters _wavTotal = 0; _wavMax = (uint32_t)_maxSec * kHdr.byteRate; // 120s × 32000B/s = 3,840,000 bytes _recording = true;
// 4. LED green indicates recording _led.on(0, 150, 0); // R=0, G=150, B=0 Serial0.printf("[REC] START (max %ds, %luB)\n", _maxSec, _wavMax);}5.5.4 tick -- Main Loop Recording
Called by loop() each frame, returns immediately if not recording. Reads 640 bytes PCM from I2S (~20ms audio), appends to PSRAM disk image after WAV header. Automatically calls stop() when max duration reached.
xxxxxxxxxxvoid Recorder::tick() { if (!_recording) return; // Not recording, return
// 1. Read PCM from I2S (640B = 320 samples = 20ms) uint8_t buf[640]; size_t rd = I2S.readBytes((char*)buf, sizeof(buf)); if (rd == 0) return; // No data, read next time
// 2. Append PCM after WAV header memcpy(_disk.data() + (size_t)_disk.dataOffset() * 512 + sizeof(kHdr) + _wavTotal, buf, rd); _wavTotal += rd;
// 3. Check max recording duration if (_wavTotal >= _wavMax) stop();}5.5.5 stop -- Stop Recording
After stopping recording, completes three things in order: (1) Backfill WAV header fileLen and dataLen fields (memcpy to offset 4 and 40); (2) Register data area as FAT16 file via publishFile(); (3) Call _onPublished callback to simulate USB drive re-plug. LED red→rainbow sequence prompts user that save is complete.
xxxxxxxxxxvoid Recorder::stop() { if (!_recording) return; _recording = false;
// 1. LED red = saving _led.on(150, 0, 0); // R=150, G=0, B=0
// 2. Backfill WAV header file length and data length uint32_t fileSize = _wavTotal + sizeof(kHdr);// Total file size uint32_t riffLen = fileSize - 8; // RIFF length uint8_t* base = _disk.data() + (size_t)_disk.dataOffset() * 512; memcpy(base + 4, &riffLen, 4); // Offset 0x04: fileLen field memcpy(base + 40, &_wavTotal, 4); // Offset 0x28: dataLen field
// 3. Register data area as FAT16 file _disk.publishFile(1, "RECORD", "WAV", fileSize);
// 4. Call completion callback → notify PC media change if (_onPublished) _onPublished();
Serial0.printf("[REC] DONE: %luB WAV, %.1fs\n", fileSize, (float)_wavTotal / kHdr.byteRate);
// 5. LED rainbow = done delay(300); _led.rainbow();}5.6 Button: Button.h / Button.cpp
5.6.1 Complete tick() Implementation
Button state machine handles mechanical bounce and short/long press distinction, with three paths: (1) Press edge→debounce 30ms then confirm; (2) Release edge→distinguish short/long by <800ms/≥800ms; (3) Hold to threshold→trigger long press early without waiting for release.
xxxxxxxxxxButton::Event Button::tick() { bool pressed = !digitalRead(_pin); // LOW=pressed
// -- Press edge detect -- if (pressed && !_down) { delay(_debounceMs); // Debounce wait 30ms if (digitalRead(_pin)) return NONE; // Read back HIGH=bounce, ignore _down = true; // Confirm press _handled = false; // Event pending _pressStart = millis(); // Record press time return NONE; }
// -- Release edge detect -- if (!pressed && _down) { _down = false; if (_handled) return NONE;// Long already sent, skip _handled = true; return (millis() - _pressStart < _longPressMs) ? SHORT : LONG; // <800ms=short/>=800ms=long }
// -- Long press hold detect (fire early, don't wait for release) -- if (_down && !_handled && millis() - _pressStart >= _longPressMs) { _handled = true; // Mark handled return LONG; }
return NONE;}5.7 LED: Led.h / Led.cpp
5.7.1 Class Interface
Led class wraps Adafruit_NeoPixel library, constructor specifies 1 LED, GRB byte order, 800kHz timing. Only exposes on()/off()/rainbow() three methods, simplifying LED state switching.
xxxxxxxxxx// WS2812 driver
class Led {public: Led(uint8_t pin) : _neo(1, pin, NEO_GRB + NEO_KHZ800) {} // 1 LED, GRB, 800kHz
void begin(); // Init void on(uint8_t r, uint8_t g, uint8_t b); // Set color void off(); // Turn off void rainbow(); // Rainbow cycle
private: Adafruit_NeoPixel _neo;};5.7.2 Rainbow Effect
Simple 7-color cycle effect, each color lasts 200ms (~1.4s total). Called after recording completes as visual hint for "file ready". Auto-off after cycle ends.
xxxxxxxxxxvoid Led::rainbow() { static const uint8_t colors[][3] = { { 0, 150, 0}, // Green {150, 60, 0}, // Orange {150, 150, 0}, // Yellow { 0, 150, 0}, // Green { 0, 150, 150}, // Cyan { 0, 0, 150}, // Blue {120, 0, 150}, // Purple }; for (int i = 0; i < 7; i++) { _neo.setPixelColor(0, _neo.Color(colors[i][0], colors[i][1], colors[i][2])); _neo.show(); delay(200); // 200ms per color } off();}6. Operation Flow
6.1 IDE Configuration
After opening the project, configure the following in the Tools menu:
| Configuration | Value | Description |
|---|---|---|
| Board | ESP32S3 Dev Module | Select ESP32-S3 development board |
| USB Mode | TinyUSB | MSC requires TinyUSB protocol stack |
| USB CDC On Boot | Disabled (required!) | USB OTG reserved for MSC |
| PSRAM | OPI PSRAM | Enable 8MB Octal PSRAM |
| Flash Size | 16MB (128Mb) | Match onboard 16MB SPI Flash |
| Partition Scheme | 16M Flash (3MB APP/9.9MB FATFS) | Or custom partition |
| Upload Mode | UART0 / Hardware CDC | Use onboard USB-UART for download |
ESP32-S3 has only one USB OTG peripheral (GPIO19/20) -
USB Mode = TinyUSBhands OTG to TinyUSB protocol stack (required for MSC). At this pointSerialis unavailable,USB CDC On Boot = Disabledprevents USB CDC conflict, debugging through onboard USB-UART bridge chip'sSerial0.
Enable PSRAM:

Enable TinyUSB:

Enable Large FATFS Partition:

6.2 Compile and Flash
For library installation, see section 3.5. Only need to additionally install Adafruit NeoPixel, reference 2.Basic Peripherals → 9.RGB LED Experiment → RGB LED Experiment.
Arduino Flash Process: See
Arduino → 1. Before Development → 2. Build and Flash → Build and Flash.
6.3 Operation Steps
Step 1: Confirm Hardware Connection
This development board has ES8311 audio codec and MEMS microphone onboard, no external modules needed, ready to use after power on.
Step 2: Serial Output
After flashing completes, open serial monitor (115200 baud), should see:

xxxxxxxxxx=== Rec+USB MSC ======== Audio System (ADC Only) =====[PWR] GPIO7 HIGH[I2C] SDA:41 SCL:42===== I2C Scan =====[OK] 0x18[OK] 0x3C===== 2 device(s) =====[I2S] Ready MCLK:15 BCLK:16 WS:18 DIN:17[ES8311] HW init done[ES8311] ADC ready (FS:16000Hz Mic:24dB)===== Recorder Ready =====[DISK] FAT16 4096KB | boot(1)+FAT(32)+root(1)+data(8158 sectors)[USB] FAT16 4096KB MSC *Plug USB cable*
Step 3: Start Recording
Short press BOOT button (GPIO0), LED turns green, recording starts. Speak towards onboard MEMS microphone to capture audio.

Serial output:
xxxxxxxxxx[REC] START (max 120s, 3840000B)
Step 4: Stop Recording, Save and Export
Short press BOOT button again to stop recording. LED first turns red (saving), then lights rainbow (7-color cycle), indicating save complete.

Serial output:
xxxxxxxxxx[REC] DONE: 390444B WAV, 12.2s

After seeing rainbow light, continue to keep USB-UART port powered. (During first run, do not insert USB OTG port before rainbow light appears, otherwise you'll get an empty U disk. If not first run, simply re-plug the USB OTG data cable)
After rainbow light appears, connect USB OTG port to PC with another Type-C data cable. PC will pop up a U disk (volume label "AUDIODISK"), open to see
RECORD.WAV, double-click to play and verify recording effect.



Recording data is stored in PSRAM, lost on power failure. Unplugging USB-UART will cause board power loss and data cleared. Must keep powered and read file via USB OTG port.
To record again, unplug Type-C cable from USB OTG port, keep USB-UART powered, short press BOOT button (new recording will automatically overwrite old file).
6.4 Long Press Force Stop
If emergency stop is needed during recording (e.g., excessive noise), long press BOOT button >= 0.8 seconds to immediately stop recording and save file.
| Operation | Button Action | Result |
|---|---|---|
| Normal stop | Short press < 0.8s | Stop recording + save |
| Emergency force stop | Long press >= 0.8s | Immediately stop recording + save |
| 120s limit reached | Auto | Auto stop + save |
7. FAQ
| Symptom | Cause | Solution |
|---|---|---|
| PC cannot recognize U disk | USB CDC On Boot not disabled, USB OTG occupied by CDC | IDE → Tools → "USB CDC On Boot" → Disabled; debug using Serial0 |
| WAV file cannot play | WAV header not backfilled / player not supported / Mic gain insufficient | Check file size >44B; use VLC player; increase MIC_GAIN_DB |
| Recording time inaccurate | PSRAM disk space insufficient / I2S DMA data loss | Confirm DISK_SECTORS setting correct; avoid delay() in loop() |
| Button insensitive | Debounce time mismatch / GPIO0 conflict | Adjust Button::begin(pin, 800, 30) debounce parameters |
| Insufficient PSRAM | PSRAM not enabled or capacity insufficient | Tools → PSRAM → "OPI PSRAM"; need >=8MB PSRAM |
| Filename garbled | FAT16 only supports 8.3 short filename format | Use English uppercase filenames (RECORD, AUDIO, etc.) |
| Serial conflicts with MSC | ESP32-S3 has only one USB OTG, cannot support CDC+MSC simultaneously | Use Serial0 (UART0), disable USB CDC On Boot |
7.1 Version Compatibility Quick Reference
| Component | Minimum Version | Recommended Version | Notes |
|---|---|---|---|
| Arduino IDE | 2.0.0 | 2.3.x+ | ESP32 board management support |
| ESP32 Arduino Core | 2.0.6 | 3.3.8+ | TinyUSB MSC support starts from 2.0.6 |
| Adafruit NeoPixel | 1.12.0 | 1.12.3+ | WS2812 driver |
| Windows | 7 | 10/11 | FAT16 native support |
Appendix A: Complete Code Index
| File | Lines | Function |
|---|---|---|
Arduino_Recorder_USB_MSC.ino | 79 | Main program: setup/loop/MSC callbacks |
AudioSystem.h | 192 | Audio system header: pin macros/register addresses/function declarations |
AudioSystem.cpp | 307 | Audio system implementation: I2C driver/ES8311 init/I2S config |
Recorder.h | 33 | Recorder header: state machine interface declarations |
Recorder.cpp | 96 | Recorder implementation: WAV header build/I2S read/disk write/backfill |
Fat16Disk.h | 31 | FAT16 disk header: virtual disk interface |
Fat16Disk.cpp | 35 | FAT16 disk implementation: format/reset/file publish |
Button.h | 25 | Button header: debounce state machine interface |
Button.cpp | 42 | Button implementation: short/long press detection logic |
Led.h | 22 | LED header: WS2812 control interface |
Led.cpp | 39 | LED implementation: brightness/color/rainbow effect |
Appendix B: ES8311 Register Quick Reference
| Register | Address | This Project Value | Function |
|---|---|---|---|
| RESET | 0x00 | 0x80 -> 0x3F | Global reset release -> clear I2S reset |
| CLK_MGR1 | 0x01 | 0x30 -> 0x3F | Master clock config -> MCLK external input |
| CLK_MGR2 | 0x02 | 0x00 | pre_div=1, multi=x1 |
| CLK_MGR3 | 0x03 | 0x10 | fs_mode=0, adc_osr=16 |
| CLK_MGR4 | 0x04 | 0x20 | dac_osr=32 |
| CLK_MGR5 | 0x05 | 0x00 | adc_div=1, dac_div=1 |
| CLK_MGR6 | 0x06 | bclk_div=4 | BCLK = MCLK / 4 = 1.024MHz |
| CLK_MGR7 | 0x07 | 0x00 | lrck_h=0 |
| CLK_MGR8 | 0x08 | 0xFF | lrck_l=0xFF -> WS period = 256 BCLK |
| SDP_IN | 0x09 | 0x0C | DAC I2S standard mode, 16bit |
| SDP_OUT | 0x0A | 0x0C | ADC I2S standard mode, 16bit |
| SYSTEM3 | 0x0D | 0xFA -> 0x01 | Analog power config |
| SYSTEM4 | 0x0E | 0x02 | Digital power enable |
| SYSTEM10 | 0x14 | 0x1A | PGA gain + ADC input enable |
| ADC1 | 0x15 | 0x40 | ADC ramp start |
| ADC2 | 0x16 | 0x04 (24dB) | ADC analog gain select (0~7) |
| ADC3 | 0x17 | 0xBF | ADC digital volume (max) |
| ADC7 | 0x1B | 0x0A | ADC high-pass filter (HPF) |
| ADC8 | 0x1C | 0x6A | ADC equalizer (EQ) |
| GPIO1 | 0x44 | 0x08 -> 0x58 | GPIO reference -> enable internal DAC reference |