espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
13.33k stars 7.2k forks source link

ADC actual sampling rate is less than the configured (IDFGH-9195) #10586

Open marcusletric opened 1 year ago

marcusletric commented 1 year ago

Answers checklist.

IDF version.

v5.0

Operating System used.

Windows

How did you build your project?

VS Code IDE

If you are using Windows, please specify command line type.

CMD

Development Kit.

ESP32 -WROOM-32 - Custom board

Power Supply used.

USB

What is the expected behavior?

Using the continuous ADC read example from https://github.com/espressif/esp-idf/blob/master/examples/peripherals/adc/continuous_read/main/continuous_read_main.c#L65

I expected that the sample rate would be 20000Hz

What is the actual behavior?

The sample rate is about 0.8 times less than the configured number in adc_continuous_config_t dig_cfg

Steps to reproduce.

Preparations: Audio to sample: 60BPM metronome https://www.youtube.com/watch?v=ymJIXzvDvj4 OR Using a DAW make a song in 4/4 on 60BPM that has only tone pulses on quarter beats

This would give a sound that has a tone on every second

  1. Modify the example https://github.com/espressif/esp-idf/blob/master/examples/peripherals/adc/continuous_read/main/continuous_read_main.c#L65 so that the main function stores 60000 samples in heap and it logs the values only after sampling (I have provided the modified sample in the more info section)

  2. Sample the wav above (I used my mobile phone as a player and the balanced jack as output, plus raised the level with a voltage divider before the ADC pin) and write the output to a file idf_monitor.py -p COM4 -b 115200 >> dump.csv

  3. Remove every line except the ADC value logs

  4. Using a python file convert the logs into wav and open it in a wav editor (Audacity) (I have provided the py file in the more info section)

  5. Cut off the first silence period and check that the beginning of the notes are less than 1 second

Debug Logs.

No response

More Information.

Modified sample Please note the multiplication in adc_continuous_config_t dig_cfg to correct for the issue I describe here

#include <stdio.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "soc/soc_caps.h"
#include "esp_adc/adc_continuous.h"

#define EXAMPLE_ADC_UNIT                    ADC_UNIT_1
#define _EXAMPLE_ADC_UNIT_STR(unit)         #unit
#define EXAMPLE_ADC_UNIT_STR(unit)          _EXAMPLE_ADC_UNIT_STR(unit)
#define EXAMPLE_ADC_CONV_MODE               ADC_CONV_SINGLE_UNIT_1
#define EXAMPLE_ADC_ATTEN                   ADC_ATTEN_DB_11
#define EXAMPLE_ADC_BIT_WIDTH               SOC_ADC_DIGI_MAX_BITWIDTH

#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
#define EXAMPLE_ADC_OUTPUT_TYPE             ADC_DIGI_OUTPUT_FORMAT_TYPE1
#define EXAMPLE_ADC_GET_CHANNEL(p_data)     ((p_data)->type1.channel)
#define EXAMPLE_ADC_GET_DATA(p_data)        ((p_data)->type1.data)
#else
#define EXAMPLE_ADC_OUTPUT_TYPE             ADC_DIGI_OUTPUT_FORMAT_TYPE2
#define EXAMPLE_ADC_GET_CHANNEL(p_data)     ((p_data)->type2.channel)
#define EXAMPLE_ADC_GET_DATA(p_data)        ((p_data)->type2.data)
#endif

#define EXAMPLE_READ_LEN                    256
#define MULTISAMPLE_NUM                   8 

#if CONFIG_IDF_TARGET_ESP32
static adc_channel_t channel[1] = {ADC_CHANNEL_4};
#else
static adc_channel_t channel[2] = {ADC_CHANNEL_2, ADC_CHANNEL_3};
#endif

static TaskHandle_t s_task_handle;
static const char *TAG = "";

static bool IRAM_ATTR s_conv_done_cb(adc_continuous_handle_t handle, const adc_continuous_evt_data_t *edata, void *user_data)
{
    BaseType_t mustYield = pdFALSE;
    //Notify that ADC continuous driver has done enough number of conversions
    vTaskNotifyGiveFromISR(s_task_handle, &mustYield);

    return (mustYield == pdTRUE);
}

static void continuous_adc_init(adc_channel_t *channel, uint8_t channel_num, adc_continuous_handle_t *out_handle)
{
    adc_continuous_handle_t handle = NULL;

    adc_continuous_handle_cfg_t adc_config = {
        .max_store_buf_size = 1024,
        .conv_frame_size = EXAMPLE_READ_LEN,
    };
    ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_config, &handle));

    adc_continuous_config_t dig_cfg = {
        // Sampling freq is of by factor of 1.225102
        // 44,1 Khz * 1.225102
        .sample_freq_hz = (int)(44100 * 1.225102 * MULTISAMPLE_NUM),
        .conv_mode = EXAMPLE_ADC_CONV_MODE,
        .format = EXAMPLE_ADC_OUTPUT_TYPE,
    };

    adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX] = {0};
    dig_cfg.pattern_num = channel_num;
    for (int i = 0; i < channel_num; i++) {
        adc_pattern[i].atten = EXAMPLE_ADC_ATTEN;
        adc_pattern[i].channel = channel[i] & 0x7;
        adc_pattern[i].unit = EXAMPLE_ADC_UNIT;
        adc_pattern[i].bit_width = EXAMPLE_ADC_BIT_WIDTH;

        ESP_LOGI(TAG, "adc_pattern[%d].atten is :%"PRIx8, i, adc_pattern[i].atten);
        ESP_LOGI(TAG, "adc_pattern[%d].channel is :%"PRIx8, i, adc_pattern[i].channel);
        ESP_LOGI(TAG, "adc_pattern[%d].unit is :%"PRIx8, i, adc_pattern[i].unit);
    }
    dig_cfg.adc_pattern = adc_pattern;
    ESP_ERROR_CHECK(adc_continuous_config(handle, &dig_cfg));

    *out_handle = handle;
}

void app_main(void)
{
    uint32_t finalBufferSize = 60000;
    uint16_t numSamples = 0;
    uint32_t accumlator = 0;
    uint16_t accNumSample = 0;
    bool dump = false;

    uint16_t* finalBuffer = heap_caps_malloc(sizeof(uint16_t) * finalBufferSize, MALLOC_CAP_32BIT);

    esp_err_t ret;
    uint32_t ret_num = 0;
    uint8_t result[EXAMPLE_READ_LEN] = {0};
    memset(result, 0xcc, EXAMPLE_READ_LEN);

    s_task_handle = xTaskGetCurrentTaskHandle();

    adc_continuous_handle_t handle = NULL;
    continuous_adc_init(channel, sizeof(channel) / sizeof(adc_channel_t), &handle);

    adc_continuous_evt_cbs_t cbs = {
        .on_conv_done = s_conv_done_cb,
    };
    ESP_ERROR_CHECK(adc_continuous_register_event_callbacks(handle, &cbs, NULL));
    ESP_ERROR_CHECK(adc_continuous_start(handle));

    while(1 && numSamples < finalBufferSize) {

        /**
         * This is to show you the way to use the ADC continuous mode driver event callback.
         * This `ulTaskNotifyTake` will block when the data processing in the task is fast.
         * However in this example, the data processing (print) is slow, so you barely block here.
         *
         * Without using this event callback (to notify this task), you can still just call
         * `adc_continuous_read()` here in a loop, with/without a certain block timeout.
         */
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        char unit[] = EXAMPLE_ADC_UNIT_STR(EXAMPLE_ADC_UNIT);

        while (1) {
            ret = adc_continuous_read(handle, result, EXAMPLE_READ_LEN, &ret_num, 0);
            if (ret == ESP_OK) {
                //ESP_LOGI("TASK", "ret is %x, ret_num is %"PRIu32, ret, ret_num);
                for (int i = 0; i < ret_num; i += SOC_ADC_DIGI_RESULT_BYTES) {
                    adc_digi_output_data_t *p = (void*)&result[i];
                    uint32_t chan_num = EXAMPLE_ADC_GET_CHANNEL(p);
                    uint32_t data = EXAMPLE_ADC_GET_DATA(p);
                    /* Check the channel number validation, the data is invalid if the channel num exceed the maximum channel */
                    if (chan_num < SOC_ADC_CHANNEL_NUM(EXAMPLE_ADC_UNIT) && numSamples < finalBufferSize) {
                        if(accNumSample < MULTISAMPLE_NUM) {
                            accumlator += data;
                        } else {
                            finalBuffer[numSamples] = (uint16_t) (accumlator / MULTISAMPLE_NUM);
                            accumlator = data;
                            accNumSample = 0;
                            numSamples++;
                        }
                        accNumSample ++;
                        //ESP_LOGI(TAG, "Unit: %s, Channel: %"PRIu32", Value: %"PRIx32, unit, chan_num, data);
                        //vTaskDelay(1);
                        //ESP_LOGI(TAG, "%"PRIx32, numSamples);

                    } else {
                        //ESP_LOGW(TAG, "Invalid data [%s_%"PRIu32"_%"PRIx32"]", unit, chan_num, data);
                    }
                }
                /**
                 * Because printing is slow, so every time you call `ulTaskNotifyTake`, it will immediately return.
                 * To avoid a task watchdog timeout, add a delay here. When you replace the way you process the data,
                 * usually you don't need this delay (as this task will block for a while).
                 */
                //vTaskDelay(1);
            } else if (ret == ESP_ERR_TIMEOUT) {
                //We try to read `EXAMPLE_READ_LEN` until API returns timeout, which means there's no available data
                break;
            }
        }

    }

    if(numSamples>=finalBufferSize && !dump) {
        ESP_LOGI(TAG, "dump");
        uint16_t j = 0;
        //uint16_t k = 0;
        //uint32_t finalVal = 0;
        for(uint32_t i = 0; i < finalBufferSize; i++ ) {
            //if(k < 2) {
            //    finalVal += finalBuffer[i];
           // } else {
                ESP_LOGI(TAG, "%"PRIx16,  finalBuffer[i]);
              //  ESP_LOGI(TAG, "%"PRIx16, (uint16_t) finalVal / 2);
            //    finalVal = finalBuffer[i];
            //    k = 0;
           // }

            if( j == 1000 ) {
                vTaskDelay(1);
                j = 0;
            }
            j++;
           // k++;
        }
        dump = true;
    }

    ESP_ERROR_CHECK(adc_continuous_stop(handle));
    ESP_ERROR_CHECK(adc_continuous_deinit(handle));
}

Python to convert output logs to wav file

#!/usr/bin/python

import wave
import struct
import sys
import csv
import numpy 
from scipy.io import wavfile
from scipy.signal import resample

def write_wav(data, filename, framerate, amplitude):
    wavfile = wave.open(filename,'w')
    nchannels = 1
    sampwidth = 2
    framerate = framerate
    nframes = len(data)
    comptype = "NONE"
    compname = "not compressed"
    wavfile.setparams((nchannels,
                        sampwidth,
                        framerate,
                        nframes,
                        comptype,
                        compname))
    frames = []
    for s in data:
        mul = int(s * amplitude)
        frames.append(struct.pack('h', mul))

    frames = ''.join(frames)
    wavfile.writeframes(frames)
    wavfile.close()
    print("%s written" %(filename)) 

if __name__ == "__main__":
    if len(sys.argv) <= 1:
        print ("You must supply a filename to generate")
        exit(-1)
    for fname in sys.argv[1:]:

        data = []
        for time, value in csv.reader(open(fname, 'U'), delimiter=','):
            try:
                data.append(float(int("0x" + value, 16)-2048))#Here you can see that the time column is skipped
                print(int("0x" + value, 16)-2048)
            except ValueError:
                print(ValueError)
                pass # Just skip it

        arr = numpy.array(data)#Just organize all your samples into an array
        # Normalize data
        arr /= numpy.max(numpy.abs(data)) #Divide all your samples by the max sample value
        filename_head, extension = fname.rsplit(".", 1)        
        data_resampled = resample( arr, len(data) )
        wavfile.write('rec.wav', 44100, data_resampled)
        print ("File written succesfully !")
marcusletric commented 1 year ago

Could be this happening because of samples are lost while I copy them to the memory? Seems like sampling rate is much more precise when I2S is used with extra buffers. If that's the case, probably this issue can be closed...

uncle-betty commented 1 year ago

For what it's worth, I think that I might be experiencing the same issue, also with an ESP32. Powered via USB, project built and flashed on Linux from the command line. IDF 5.0.1.

I use a DMA buffer size (conv_frame_size) of 1024 * sizeof (adc_digi_output_data_t). My ring buffer (max_store_buf_size) is three times that size. I set the sampling rate to 116000 Hz. I use two channels, 6 and 7, on the first ADC. I then do adc_continuous_read() in a tight loop to read conv_frame_size-many bytes in each loop iteration.

I use dsp_get_cpu_cycle_count() to determine how long one loop iteration takes. I get ~94 loop iterations per second. However, I would have expected 116000 / 1024 = ~113 loop iterations per second. (It doesn't matter whether I clock the CPU with 160 MHz or with 240 MHz.)

And ~94 * ~1.20 = ~113. Which seems to match what @marcusletric is seeing.

Any additional data I could collect to help debug this?

uncle-betty commented 1 year ago

The math in adc_hal_digi_sample_freq_config() seems to be correct. I added some debug logging real quick:

I (320) XXX: freq 116000
I (330) XXX: bclk_div 16, bclk 232000, mclk 3712000, mclk_div 43

And 43 * 3712000 = 159616000, which is pretty close to 160 MHz.

Further down in the HAL in i2s_ll_tx_set_mclk() I also seem to get the correct fraction:

I (330) XXX: mclk_div 43, numerator 3, denominator 29

And (43 + 3 / 29) * 3712000 = 160000000.

marcusletric commented 1 year ago

@uncle-betty in my project I have abandoned this approach in favour of I2S read which seems to be sampling at a correct rate.

I also need to mention that this is true for

I used the following I2S config

i2s_config_t adcI2SConfig = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_LSB,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 20,
    .dma_buf_len = SAMPLE_STEP,
    .use_apll = true, // Probably this is the most consistent sampling clock
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
};

With this I needed to make sure that my buffer processing is faster than the buffer fills otherwise I missed some of the data.

uncle-betty commented 1 year ago

Oh, great! Thank you for sharing your workaround, @marcusletric. I appreciate it and I'll give it a try.

kamilsss655 commented 4 months ago

I can confirm this issue is happening on ESP32 with v5.2.1 IDF.