Page 1 of 1

ESP32-S3 Fast readings with ADC and DMA low level functions

Posted: Sun Jan 19, 2025 6:47 pm
by kaefe_br
I am trying to make a code using ESP-IDF low level ADC and DMA functions to read 3 analog signals in sequence in a fast way (480kHz sample rate) on a ESP32-S3.

1 - Does anyone know any place I can get more information about adc_ll and gdma_ll functions? or better an example?
There is no information about them on esp-idf docs search. I got some help from ChatGPT, but it is version dependent and not so "intelligent" ;)

2 - The code I put together on VSCode is this one, and the issue is it does not generate DMA RX_SUC_EOF event or any other.
There are a lot of comments with the tests I have done.
If you know what I am missing, please tell me. :idea:

Code: ADC_ESP32S3_Direto.c Select all


/*
ADC_ESP32S3_Direto.c
Author: Klaus Fensterseifer
Jan2025

ESP-IDF v5.3.2
ESP32-S3 N16R8 (16 MB Quad SPI flash, 8 MB Octal PSRAM die)

From Compilation Summary:
Flash Code 8MB
DIRAM 340KB
Flash Data 3.3MB
IRAM 16kB
RTC FAST 8kB

Reads 3 ADC inputs in sequence
Uses the DMA to fill a buffer with the ADC samples
Interrupts the main task when the buffer is full
- a new buffer is set to be filled by the DMA
- the main task deal with the data from the filled buffer

Sample frequency = 480kHz (3 ADCs = 160kHz each ADC input)
Size of the buffer = should be large, 3000 or 30000, but he DMA setup restricts it to 4095 bytes (or 2047 12 bits samples)

Similar approach = https://github.com/kaefe64/Arduino_uSDX_Pico_FFT_Proj

*/




#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "soc/soc.h"
#include "soc/lldesc.h"
#include "soc/gdma_struct.h"
#include "soc/soc_caps.h"
//#ifndef __DECLARE_RCC_ATOMIC_ENV
//#define __DECLARE_RCC_ATOMIC_ENV()
//#endif
#include "hal/adc_ll.h"
#include "hal/adc_hal_common.h"
#include "hal/gdma_ll.h"
#include "hal/clk_gate_ll.h" //
#include "hal/gpio_ll.h" //
//#include "esp_task_wdt.h" //
#include "esp_timer.h" //

//#include "esp_private/periph_ctrl.h" //
//#include "esp_private/adc_share_hw_ctrl.h" //
//#define ADC_BUS_CLK_ATOMIC() PERIPH_RCC_ATOMIC()
#include "hal/spi_types.h"
#include "hal/spi_ll.h"
#include "esp_private/spi_common_internal.h"
#include "esp_private/adc_dma.h"



#define DMA_BUFFER_SIZE 100 //2000 // Tamanho do buffer DMA max = 4095 / 2 = 2047
#define DMA_BUFFER_NUM 20 // number of DMA buffers (if size is small, increase the number of buffers)
#define TAG "LowLevel_ADC_DMA"
#define DMA_CHANNEL 2 //wifi uses DMA channel 0

static uint16_t dma_buffer[DMA_BUFFER_NUM][DMA_BUFFER_SIZE] __attribute__((aligned(4)));
static volatile bool buffer_ready = false; // buffer filled
static uint16_t active_buffer_idx = 0; //buffer index
static uint16_t active_buffer_main = 0; //buffer index
static uint16_t active_buffer_cnt = 0; //buffer index
static uint16_t active_buffer_cnt_main = 0; //buffer index

// Descritor DMA
static lldesc_t dma_descriptor[DMA_BUFFER_NUM] = {0};


/*
#include "hal/timer_ll.h"

//=================================================================

//=================================================================
void configure_timer_for_adc(void) {
// Configurar o timer periférico para gerar eventos
uint32_t timer_group = 0; // Grupo do timer (0 ou 1)
uint32_t timer_index = 0; // Índice do timer (0 ou 1 dentro do grupo)

timer_ll_set_auto_reload(timer_group, timer_index, true); // Habilitar modo auto-reload
//timer_ll_enable_auto_reload
timer_ll_set_counter_value(timer_group, timer_index, 0); // Reiniciar contador
//timer_ll_set_clock_prescale 1
timer_ll_set_divider(timer_group, timer_index, 2); // Configurar divisor (ajustar para precisão)
//timer_ll_set_alarm_value 166

// Frequência desejada = 480 kHz
uint32_t clock_source_freq = 80000000; // Clock base (80 MHz)
uint32_t timer_divider = clock_source_freq / 480000; // Divisor para 480 kHz
timer_ll_set_alarm_value(timer_group, timer_index, timer_divider);

timer_ll_enable_alarm(timer_group, timer_index, true); // Habilitar alarme
timer_ll_enable_counter(timer_group, timer_index, true); // Iniciar contador
}

// Configurar o ADC para ser acionado por um evento externo (como o timer)
adc_ll_digi_set_trigger_source(ADC_TRIGGER_TIMER);

adc_ll_digi_enable(); // Habilitar o ADC

*/



//=================================================================
// DMA Interrupt handler
//=================================================================
void IRAM_ATTR dma_isr_handler(void *arg)
{
// Check interrupt event
uint32_t isr_evt = gdma_ll_rx_get_interrupt_status(&GDMA, DMA_CHANNEL, true); //true = raw false = status
if(isr_evt & GDMA_LL_EVENT_RX_SUC_EOF)
{
uint16_t j = active_buffer_idx; //start to search for buffer ready from the last one
for(uint16_t i=0; i<DMA_BUFFER_NUM; i++)
{
if(++j >= DMA_BUFFER_NUM) j = 0;
// the descriptor with owner = 0 is the one that was filled by DMA
if (dma_descriptor[j].owner == 0) // Buffer filled
{
// get the buffer index
active_buffer_idx = j;
active_buffer_cnt++; //guess the next buffer filled is the next one

if(buffer_ready == false)
{
//processing
active_buffer_main = active_buffer_idx;
active_buffer_cnt_main = active_buffer_cnt;
//process_data(dma_descriptor[0].buf, dma_descriptor[0].length);

buffer_ready = true; //inform main
// inform main task about buffer filled
//xTaskNotifyFromISR(task_handle, 0, eNoAction, NULL);
}

// return buffer control to GDMA after processing
dma_descriptor[j].owner = 1;
break; //only one buffer ready (expect to receive another interrupt for other one)
}
}

// clear GDMA int
gdma_ll_rx_clear_interrupt_status(&GDMA, DMA_CHANNEL, GDMA_LL_EVENT_RX_SUC_EOF); // Limpa a interrupção de "success end of frame" (EOF)
}
else //another event generated the DMA interrupt - should not happen
{
gdma_ll_rx_clear_interrupt_status(&GDMA, DMA_CHANNEL, isr_evt); // clear all events set, but not RX SUC EOF
}


//buffer_ready = true; //for debug, any DMA interrupt will show something on main task

}


//#define __DECLARE_RCC_ATOMIC_ENV() //be used in an atomic way
//=================================================================
// Configuração do ADC no ESP32-S3
//=================================================================
static void configure_adc(void) {

adc_ll_digi_reset();

//adc_ll_reset_register(); //ADC regas reset, return to default values
// SYSTEM.perip_clk_en0 is a shared register, so this function must be used in an atomic way
// ADC_BUS_CLK_ATOMIC() {
SYSTEM.perip_rst_en0.apb_saradc_rst = 1;
SYSTEM.perip_rst_en0.apb_saradc_rst = 0;
// }
// Ativar o clock do ADC
//adc_ll_enable_bus_clock(true); //enables ADC clock on APB bus
// SYSTEM.perip_rst_en0 is a shared register, so this function must be used in an atomic way
// ADC_BUS_CLK_ATOMIC() {
SYSTEM.perip_clk_en0.apb_saradc_clk_en = true;
// }
adc_ll_digi_convert_limit_enable(false);

adc_hal_set_controller(ADC_UNIT_1, ADC_HAL_CONTINUOUS_READ_MODE);


//adc_ll_digi_clear_pattern_table(ADC_UNIT_1);

adc_ll_digi_set_pattern_table_len(ADC_UNIT_1, 3); // 3 channels

adc_digi_pattern_config_t adc_pattern;

adc_pattern.atten = ADC_ATTEN_DB_0; //No input attenuation, approx. 0 - 800mV
adc_pattern.channel = ADC_CHANNEL_3; //ADC channel index. ADC1_CH3 - GPIO 4
adc_pattern.unit = ADC_UNIT_1; //ADC unit index.
adc_ll_digi_set_pattern_table(ADC_UNIT_1, 0, adc_pattern);

adc_pattern.atten = ADC_ATTEN_DB_0; //No input attenuation, ADC can measure up to approx. 0 - 800mV
adc_pattern.channel = ADC_CHANNEL_4; //ADC channel index. ADC1_CH4 - GPIO 5
adc_pattern.unit = ADC_UNIT_1; //ADC unit index.
adc_ll_digi_set_pattern_table(ADC_UNIT_1, 1, adc_pattern);

adc_pattern.atten = ADC_ATTEN_DB_0; //No input attenuation, ADC can measure up to approx. 0 - 800mV
adc_pattern.channel = ADC_CHANNEL_5; //ADC channel index. ADC1_CH5 - GPIO 6
adc_pattern.unit = ADC_UNIT_1; //ADC unit index.
adc_ll_digi_set_pattern_table(ADC_UNIT_1, 2, adc_pattern);





// ADC basic configuration
adc_ll_set_controller(ADC_UNIT_1, ADC_LL_CTRL_DIG); // digital controler
adc_ll_digi_set_convert_mode(ADC_LL_DIGI_CONV_ONLY_ADC1); //uses only ADC1
adc_ll_set_sample_cycle(4); // number of clock cycles to charge the ADC internal capacitor before the conversion start
// 2 for source impedance < 1k 10 for impedances > 10k
//adc_ll_set_data_format(ADC_LL_DATA_FMT_12_BIT); // 12 bits format - ESP32S3 only supports 12bit
//adc_ll_digi_set_arbiter_stable_cycle()

adc_ll_digi_clk_sel(ADC_DIGI_CLK_SRC_APB); //use APB

//adc_ll_digi_controller_clk_div(0,1,0); //base clock for the ADC digital controller.
adc_ll_digi_controller_clk_div(1,1,1); //base clock for the ADC digital controller.
//ctrl_clk = (APLL or APB) / (div_num + div_a / div_b + 1).

adc_ll_digi_set_clk_div(2); //sampling clock determines the time taken for the ADC to perform a conversion.

//80MHz / 500kHz = 160 500000 / 3 = 166666
//80MHz / 481927 = 166 481927 / 3 = 160642
//80MHz / 479041 = 167 479041 / 3 = 159680
//interval = ((80MHz /(0 + (0 / 1) + 1)) /2) /480kHz = 83.33
//interval = ((80MHz /(1 + (1 / 1) + 1)) /2) /480kHz = 27.77
adc_ll_digi_set_trigger_interval(167); //Configures ADC digital controller clock cycles between ADC conversions.
//adc_ll_digi_start(ADC_UNIT_1);



/*
uint32_t interval = APB_CLK_FREQ / (ADC_LL_CLKM_DIV_NUM_DEFAULT + ADC_LL_CLKM_DIV_A_DEFAULT / ADC_LL_CLKM_DIV_B_DEFAULT + 1) / 2 / freq;
//set sample interval
adc_ll_digi_set_trigger_interval(interval);
//Here we set the clock divider factor to make the digital clock to 5M Hz
adc_ll_digi_controller_clk_div(ADC_LL_CLKM_DIV_NUM_DEFAULT, ADC_LL_CLKM_DIV_B_DEFAULT, ADC_LL_CLKM_DIV_A_DEFAULT);
adc_ll_digi_clk_sel(0); //use APB
*/


adc_ll_digi_set_power_manage(ADC_LL_POWER_SW_ON);

//adc_ll_digi_dma_set_eof_num(10);
//adc_dma_ll_rx_start

//adc_dma_start(adc_dma_t adc_dma, dma_descriptor_t *addr)
// spi_ll_clear_intr(adc_dma.adc_spi_dev, ADC_DMA_INTR_MASK);
// spi_ll_enable_intr(adc_dma.adc_spi_dev, ADC_DMA_INTR_MASK);
// spi_dma_ll_rx_start(adc_dma.adc_spi_dev, adc_dma.spi_dma_ctx->rx_dma_chan.chan_id, (lldesc_t *)addr);


// enables DMA for ADC
adc_ll_digi_dma_enable(); //enables DMA to tranfer data from ADC direct to the memory

adc_ll_digi_trigger_enable(); //enables ADC internal trigger to start continuous conversion

}







//=================================================================
// DMA configuration for ESP32-S3
//=================================================================
static void configure_dma(void) {
esp_err_t ret;

//The DMA RX (Receive) functions are designed for scenarios where data is being transferred
//from a peripheral (in this case, the ADC) into memory via DMA.


// GDMA channel reset
gdma_ll_rx_reset_channel(&GDMA, DMA_CHANNEL);
//gdma_ll_rx_reset_channel0(&GDMA);

//gdma_trigger_peripheral_t GDMA_TRIG_PERIPH_ADC

/*
gdma_dev_t *dev
A pointer to the GDMA device structure.
Represents the specific GDMA instance being configured. For ESP32-S3, this would typically be &GDMA.

uint32_t channel
Specifies the GDMA channel number to configure.
RX channels are used for peripherals that produce data, such as the ADC.
Example: If you are using channel 0, you pass 0 here.

gdma_trigger_peripheral_t periph
Specifies the type of peripheral triggering the DMA.
For ADC, this should be GDMA_TRIG_PERIPH_ADC.
Other possible values could include UART, SPI, I2S, etc.

int periph_id
Identifies the specific peripheral instance or unit to connect to the DMA.
For ADC, this might differentiate between ADC units or specific configurations of the ADC digital controller.
Example: If using ADC digital controller 1 (ADC1), you might pass 1.
*/
// Connect the DMA RX channel to the ADC peripheral
gdma_ll_rx_connect_to_periph(&GDMA, DMA_CHANNEL, GDMA_TRIG_PERIPH_ADC, 1); //1=ADC1

// descriptor configuration
for(int i=0; i<DMA_BUFFER_NUM; i++)
{
//size: max size buffer. max: 4095 bytes
dma_descriptor[i].size = (uint32_t)(DMA_BUFFER_SIZE * sizeof(uint16_t)); // & 0xFFF max 4095 bytes
//length: number of bytes in the buffer, should be equal or less than size.
dma_descriptor[i].length = (uint32_t)(DMA_BUFFER_SIZE * sizeof(uint16_t));
dma_descriptor[i].buf = (uint8_t *)dma_buffer[i];
dma_descriptor[i].owner = 1; //release the buffer to DMA
/*
sosf = 0: not the beginnig of a frame
sosf = 1: it is a frame beginnig
If the use is a continuous stream of data, it could be set to 0
*/
dma_descriptor[i].sosf = 0;
/*
eof = 0: not the end of frame
eof = 1: it is a frame end
If not using frames, it could be left as 0.
*/
dma_descriptor[i].eof = 1; //=1 to set the interrupt when buffer full
dma_descriptor[i].qe.stqe_next = &dma_descriptor[i+1];
}
//dma_descriptor[DMA_BUFFER_NUM-1].eof = 1;
dma_descriptor[DMA_BUFFER_NUM-1].qe.stqe_next = &dma_descriptor[0];

// Configurar o GDMA para o ADC
gdma_ll_rx_set_desc_addr(&GDMA, DMA_CHANNEL, (uint32_t)&dma_descriptor[0]);
//gdma_ll_rx_set_desc_addr(dma_dev, DMA_CHANNEL, (uint32_t)dma_descriptor);

// transfer block size
//gdma_ll_rx_set_block_size(DMA_CHANNEL, DMA_BUFFER_SIZE);

// interrupt configuration
//ret = esp_intr_alloc(ETS_DMA_IN_CH0_INTR_SOURCE, 0, dma_isr_handler, NULL, NULL);
ret = esp_intr_alloc(ETS_DMA_IN_CH2_INTR_SOURCE, // DMA RX interrupt source DMA_CHANNEL = 2
ESP_INTR_FLAG_IRAM, // Allocate in IRAM for fast execution.
// ESP_INTR_FLAG_LEVEL6, // Allocate in IRAM for fast execution.
dma_isr_handler, // Interrupt handler function
NULL, // Argument to pass to handler
NULL); // Optional handle (not needed here)
if (ret != ESP_OK) {
printf("Failed to allocate DMA interrupt\n");
}


//gdma_ll_force_enable_reg_clock(&GDMA, true);
//gdma_ll_rx_restart(&GDMA, DMA_CHANNEL);
//gdma_ll_rx_enable_auto_return(&GDMA, DMA_CHANNEL,true);
gdma_ll_rx_set_burst_size(&GDMA, DMA_CHANNEL, 16);

// set interrupt of end of transfer
//GDMA.channel[0].in.int_ena.in_suc_eof = 1;
// enables the GDMA to set the EOF interrupt
gdma_ll_rx_enable_interrupt(&GDMA, DMA_CHANNEL, GDMA_LL_EVENT_RX_SUC_EOF, true);
//gdma_ll_rx_enable_interrupt(&GDMA, DMA_CHANNEL, 0x2ff, true); //for debug, enables all possible interrupt event

//gdma_ll_rx_enable_channel(&GDMA, DMA_CHANNEL, true);
//GDMA.channel[0].in.link.start = 1; // Habilitar o canal RX
gdma_ll_rx_start(&GDMA, DMA_CHANNEL);

//adc_digi_start();
//DR_REG_GDMA_BASE

}





uint64_t tim, tim_old;

//=================================================================
// Função principal
//=================================================================
void app_main()
{
uint32_t i = 0;

//ADC1_3 GPIO4
gpio_ll_input_enable(&GPIO, 3);
gpio_ll_pullup_dis(&GPIO, 4);
gpio_ll_pulldown_dis(&GPIO, 4);
//adc_ll_set_channel_io_map(ADC_UNIT_1, ADC_CHANNEL_3, 4);

//ADC1_4 GPIO5
gpio_ll_input_enable(&GPIO, 4);
gpio_ll_pullup_dis(&GPIO, 5);
gpio_ll_pulldown_dis(&GPIO, 5);
//adc_ll_set_channel_io_map(ADC_UNIT_1, ADC_CHANNEL_4, 5);

//ADC1_5 GPIO6
gpio_ll_input_enable(&GPIO, 5);
gpio_ll_pullup_dis(&GPIO, 6);
gpio_ll_pulldown_dis(&GPIO, 6);
//adc_ll_set_channel_io_map(ADC_UNIT_1, ADC_CHANNEL_5, 6);

ESP_LOGI(TAG, "************************");
ESP_LOGI(TAG, "************************");
ESP_LOGI(TAG, "Starting ADC with DMA...");
ESP_LOGI(TAG, "************************");
ESP_LOGI(TAG, "************************");

configure_dma();
ESP_LOGI(TAG, "1************************");
configure_adc();
ESP_LOGI(TAG, "2************************");

tim_old = esp_timer_get_time();
while (1)
{
if (buffer_ready)
{
// Main task - Data processing
//uint16_t *processed_buffer = (active_buffer == dma_buffer_a) ? dma_buffer_b : dma_buffer_a;
ESP_LOGI(TAG, "Buffer filled:");
//for (int i = 0; i < DMA_BUFFER_SIZE; i++) {
// printf("ADC[%d]: %d\n", i, processed_buffer[i]);
//}
printf(" cnt:%d buffer:%d [0]= %d %d %d\n",
active_buffer_cnt_main,
active_buffer_main,
dma_buffer[active_buffer_main][0],
dma_buffer[active_buffer_main][1],
dma_buffer[active_buffer_main][2] );
//printf("\n");
buffer_ready = false;
}

if(++i>11000000) //20M ~3s
{
//printf(" i:%d\n",(uint16_t)(i-10000000));

tim = esp_timer_get_time();
printf(" time: %d ms\n",(uint16_t)((tim-tim_old)/1000));
tim_old = tim;
vTaskDelay(1); //to avoid watchdog trigger
i = 0;
}
}
}

Re: ESP32-S3 Fast readings with ADC and DMA low level functions

Posted: Mon Jan 20, 2025 2:09 pm
by kaefe_br
Looking further into the datasheet: esp32-s3_technical_reference_manual_en.pdf:

"Note that due to speed limits of SAR ADCs, the operating clock of Digital Reader1 and SAR ADC1 is
DIGADC_SARCLK, the frequency of which affects the sampling precision. When the frequency of
DIGADC_SARCLK is higher than 5 MHz, the sampling precision will be lowered."

"The frequency of DIGADC_SARCLK must not exceed 5 MHz."

"The ADC needs 25 DIGADC_SARCLK clock cycles per sample, so the maximum sampling rate is limited by the
DIGADC_SARCLK frequency."


ADC takes 25 DIGADC_SARCLK clocks for a sample -> 5MHz / 25 = 200kHz is the max sample rate suggested by the datasheet.
:cry: :cry: :cry:

The information from Google suggest a better sample rate...
"The ESP32 Analog-to-Digital Converter (ADC) has a theoretical maximum sampling rate of 2 MHz when Wi-Fi is off and ADC DMA is used. However, it is recommended to use a lower sampling rate in practice."

Too bad. Abort!