Master Efficient ADC Data Collection with ESP32 DMA and I2S | The Complete Guide


In this article, we will introduce how to efficiently collect ADC (Analog-to-Digital Converter) data on the ESP32 using ESP32 DMA (Direct Memory Access) and I2S (Inter-IC Sound) technologies. By leveraging the efficient data transfer capabilities of ESP32 DMA, we can significantly reduce CPU load and increase data throughput, making it suitable for high-speed applications. This method enhances the performance of the ESP32 in continuous data stream applications such as audio processing, real-time data monitoring, and high-frequency data sampling.

ESP32 DMA 

Introduction

In embedded systems, the efficiency of data collection is crucial for performance, especially when using microcontrollers like the ESP32. If you are working with analog signals, the ADC is essential for converting these signals into digital data. However, when a large amount of data needs to be collected, the ESP32’s DMA and I2S peripherals can significantly improve this process by reducing CPU load and optimizing data throughput.

ESP32 DMA and I2S

Direct Memory Access (DMA)

ESP32 DMA is a technology that allows peripherals (like ADC) to transfer data directly to memory, bypassing the CPU. This is ideal for scenarios that require continuous or high-volume data collection, as it avoids interrupting the CPU with every sample transfer. Using ESP32 DMA for ADC operations ensures that samples can be directly transferred to memory buffers without interrupting the CPU for each sample.

I2S Interface

Although I2S is traditionally used for audio data transfer, it can also be utilized to transfer high-speed data from the ADC. The ESP32 is equipped with a built-in I2S interface, which can be configured for various tasks, including high-speed ADC data collection. By using I2S, we can take advantage of its continuous data stream handling capabilities, making it an ideal choice for efficient data transfer when paired with DMA.

Setting up ESP32 DMA and I2S

Step 1: Configure ADC

Before setting up DMA and I2S, we need to configure the ADC on the ESP32. The ESP32 has multiple built-in ADC channels that can be used for sampling analog signals. Here’s an example of how to configure the ADC:

#include "driver/adc.h"
#include "esp_log.h"#define ADC_WIDTH ADC_WIDTH_BIT_12
#define ADC_SAMPLE_RATE 5000  // 5k samples per secondvoid configure_adc() {
    adc1_config_width(ADC_WIDTH);
    adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_0);
}

This code configures ADC1_CHANNEL_0 to sample with a resolution of 12 bits and a default attenuation of 0 dB.

Step 2: Set up I2S for Data Transfer

Once the ADC is configured, we will set up the I2S interface for high-speed data transfer. The I2S interface will be configured to collect data from the ADC and store it in memory.

#include "driver/i2s.h"#define I2S_NUM I2S_NUM_0
#define BUFFER_SIZE 1024void configure_i2s() {
    i2s_config_t i2s_config = {
        .mode = I2S_MODE_MASTER | I2S_MODE_RX, // Set I2S as a receiver
        .sample_rate = ADC_SAMPLE_RATE,        // Sample rate, should match ADC sample rate
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,  // We use only one channel for ADC data
        .communication_format = I2S_COMM_FORMAT_I2S,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
        .dma_buf_count = 8,
        .dma_buf_len = BUFFER_SIZE
    };
    i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);    // Connect the I2S interface to the ADC
    i2s_pin_config_t pin_config = {
        .ws_io_num = I2S_PIN_NO_CHANGE,
        .bck_io_num = I2S_PIN_NO_CHANGE,
        .data_in_num = ADC1_CHANNEL_0
    };
    i2s_set_pin(I2S_NUM, &pin_config);
}

Step 3: Configure DMA

Once I2S is set up, DMA needs to be configured to transfer data from the ADC to memory without involving the CPU. The I2S driver automatically manages the DMA buffers, so we don’t need to configure DMA manually, but we must ensure that the memory allocated for the DMA buffers is large enough to handle the data stream and avoid buffer overflow.

#define DMA_BUFFER_SIZE 1024
int16_t dma_buffer[DMA_BUFFER_SIZE];void start_adc_reading() {
    size_t bytes_read;
    while (1) {
        // Read data into DMA buffer
        i2s_read(I2S_NUM, dma_buffer, DMA_BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);        // Process data in dma_buffer...
        ESP_LOGI("ADC", "Data read: %d", dma_buffer[0]);
    }
}

In this example, the i2s_read() function reads data from the I2S interface and stores it in the ESP32 DMA buffer. These data can then be processed without requiring CPU intervention.

Data Collection and Processing

Once ESP32 DMA and I2S are set up, collecting data becomes straightforward. The data collected from the ADC is stored in the ESP32 DMA buffer, and you can process it in real-time or store it for later use. Here’s a simple loop to read and process ADC values from the buffer:

void process_adc_data() {
    while (1) {
        size_t bytes_read;
        // Read data from DMA buffer
        i2s_read(I2S_NUM, dma_buffer, DMA_BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);        // Process the data (e.g., store or analyze)
        ESP_LOGI("ADC", "ADC Value: %d", dma_buffer[0]);
    }
}

Full Code

Complete ESP32 Code Example that includes the setup of ADCI2SDMA, and data collection, and uses ESP_LOGI to print the ADC data to the serial monitor for real-time viewing of the read values.

#include <stdio.h>
#include "driver/adc.h"
#include "driver/i2s.h"
#include "esp_log.h"

#define ADC_WIDTH            ADC_WIDTH_BIT_12         // ADC resolution: 12 bits
#define ADC_SAMPLE_RATE      5000                     // ADC sample rate: 5000 samples per second
#define I2S_NUM              I2S_NUM_0                // I2S peripheral number
#define BUFFER_SIZE          1024                     // DMA buffer size
#define DMA_BUFFER_SIZE      1024                     // Length of DMA buffer

static const char *TAG = "ADC_I2S_DMA";              // Log tag

// DMA buffer for ADC data
int16_t dma_buffer[DMA_BUFFER_SIZE];

// Configure the ADC
void configure_adc() {
    // Set ADC width to 12 bits
    adc1_config_width(ADC_WIDTH);
    
    // Set channel attenuation
    adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_0);
    
    ESP_LOGI(TAG, "ADC configured");
}

// Configure I2S
void configure_i2s() {
    i2s_config_t i2s_config = {
        .mode = I2S_MODE_MASTER | I2S_MODE_RX,      // I2S as master, in receive mode
        .sample_rate = ADC_SAMPLE_RATE,             // Sample rate
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16-bit samples
        .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, // Single channel
        .communication_format = I2S_COMM_FORMAT_I2S, // I2S communication format
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,    // Interrupt allocation
        .dma_buf_count = 8,                          // Number of DMA buffers
        .dma_buf_len = BUFFER_SIZE                   // DMA buffer size
    };
    
    // Initialize I2S driver
    i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
    
    // Configure I2S pin mapping to the ADC
    i2s_pin_config_t pin_config = {
        .ws_io_num = I2S_PIN_NO_CHANGE,
        .bck_io_num = I2S_PIN_NO_CHANGE,
        .data_in_num = ADC1_CHANNEL_0                // Connect I2S data input to ADC channel
    };
    
    i2s_set_pin(I2S_NUM, &pin_config);
    
    ESP_LOGI(TAG, "I2S configured");
}

// Start ADC data reading
void start_adc_reading() {
    size_t bytes_read;
    
    // Continuously read and log ADC data
    while (1) {
        // Use I2S to read data into the DMA buffer
        i2s_read(I2S_NUM, dma_buffer, DMA_BUFFER_SIZE * sizeof(int16_t), &bytes_read, portMAX_DELAY);

        // Log the first sample in the buffer (or process it further if needed)
        ESP_LOGI(TAG, "ADC Data read: %d", dma_buffer[0]);
    }
}

void app_main() {
    // Configure ADC and I2S
    configure_adc();
    configure_i2s();

    // Start reading data
    start_adc_reading();
}

Code Explanation

  • ADC Setup: Configures the ADC to use 12-bit resolution and channels with 0 dB attenuation.
  • I2S Setup: Sets up the I2S interface to operate in master receive mode with a sample rate matching the ADC.
  • ESP32 DMA Buffering: The i2s_read() function reads data from the I2S interface into a buffer for processing.
  • Data Logging: Continuously logs ADC data to the serial monitor.

Results

Running the code will continuously display ADC values from the selected channel, which can be used for monitoring or further processing.

I (500) ADC_I2S_DMA: ADC configured
I (500) ADC_I2S_DMA: I2S configured
I (1000) ADC_I2S_DMA: ADC Data read: 1234
I (1500) ADC_I2S_DMA: ADC Data read: 1250
...

DMA Advantages

  • Efficient Data CollectionDMA offloads the data transfer, allowing the CPU to focus on other tasks.
  • Reduced CPU Load: The CPU is only involved in processing the data, not transferring it.
  • High Throughput: With DMA handling the data transfer, high-speed data collection is possible, especially for real-time applications like audio or sensor data monitoring.

Practical Application Scenarios

  • Audio Processing: Collect audio data in real-time from microphones.
  • Data Logging: Collect sensor data over a period of time with minimal CPU overhead.
  • Signal Processing: Use DMA to collect signals for real-time analysis or transformation.

Conclusion

We have demonstrated how to efficiently collect ADC data using ESP32 DMA and I2S. By leveraging these powerful peripherals, you can offload the data transfer work from the CPU, achieving high-speed, low-latency data collection. This approach is ideal for projects that require real-time data acquisition and processing, making the ESP32 a powerful platform for embedded applications. Whether you are building a sensor array, an audio system, or just need fast ADC sampling, using ESP32 DMA and I2S is an excellent way to enhance project performance and efficiency.