Hello all,
I’m in the middle of trying to build a DIY Low-cost oscilloscope with ESP32 .... but I’m running into a few issues and could use some hlp. HEre's my setup:
1) ESP32 Dev Kit (WROOM-32)
2) Attempting to read analog signals (0–3.3V) via the onboard ADC
3) Planning to display results on either a small TFT screen or stream to a PC(may be both options as TFT is expensive option)
My problems:
1) ADC readings are noisy and unstable .... even with simple voltage dividers ... sometimes get peak values for an instant
2) Sampling rate seems too low for anything above ~1 kHz signals.
3) Confused about the best approach for real-time plotting .... should I use SPI display? can I do Wi-Fi streaming in real time?
I’ve looked at a few open-source ESP32 oscilloscope projects, but many either lack documentation or rely on custom hardware. I ma building something like this:
https://circuitdigest.com/microcontroll ... cilloscope
https://www.aivon.com/blog/measurement- ... pe-basics/
https://hackaday.com/2025/12/21/a-compa ... illoscope/
But as you can see these have basic info but not detailed data. Has anyone successfully built something similar? What are the best practices for stable ADC readings, signal conditioning and real-time plotting on ESP32? Any libraries, schematics, or project links would be hugely appreciated
Thansk in advance
ESP32 Low Cost Oscilloscope – Need Help with ADC Accuracy and Display
-
lichurbagan
- Posts: 59
- Joined: Thu Nov 13, 2025 3:20 pm
Re: ESP32 Low Cost Oscilloscope – Need Help with ADC Accuracy and Display
Dear lichurbagan,
I would advise you to use another MC and an external ADC. There is no "low cost Osci" around!
I successfully build one with lots of hard personal work any many pitfalls for my Synth. You will need a preamp, lots of experiences with freetos, ADC, ESP-IDF, LCD, ...
I started with "https://circuitdigest.com/microcontroll ... cilloscope"
But it was full of bugs and very misleading, because you will not be able to have readings below 0 with that setup. It structure was messy. I had to completely revise it, but it was a startup.
My best advise:by a simple chinese handheld (above 130€) if you really want to work more seriously and have readings higher than 20 kHz and maybe a larger voltage range, FFT, data saving,... and reliablity.
Sorry do not ask for my codes, they are halfway documented, messy, and maybe also faulty. I use PlatformIO and VSCode.
I you really want to learn all details: Think about >=1 year of experinences (blood and tears).
Another tipp for ADC: you have to switch MSWord and lower Word in ADC readings.
Maybe this will help. I use a sample rate of 200kHz and 48000 buffersize. The docs tell you, that you can go much higher (fake!).
/*********************************************************************************************
* All ADC and I2S routines used:
* 1) configure_i2s
* 2) set_sample_rate (not used)
* 3) Start und stop I2S
* 4) ADC_Sampling
* 5) Start and stop sampling
**********************************************************************************************/
//#include <Arduino.h> // only for debugging by serial output
//#include <stddef.h> // within Arduino.h
#include <esp_log.h>
#include <driver/i2s.h>
#include <driver/adc.h>
//#include "../../src/DFTdisplay.h" // only for DMA_BUF_LEN but fixed to 1024
#define DMA_BUF_LEN (1024)
const char TAG[] = "ADC";
/**********************************************
* From https://docs.espressif.com/projects/esp ... s/i2s.html
Please follow these steps to prevent data lost:
1. Determine the interrupt interval. Generally, when data lost happened, the interval should be the bigger the better, it can help to reduce the interrupt times, i.e., dma_buf_len should be as big as possible while the DMA buffer size won’t exceed its maximum value 4092. The relationships are:
interrupt_interval(unit: sec) = dma_buf_len / sample_rate
dma_buffer_size = dma_buf_len * slot_num * data_bit_width / 8 <= 4092
2. Determine the dma_buf_count. The dma_buf_count is decided by the max time of i2s_read polling cycle, all the received data are supposed to be stored between two i2s_read. This cycle can be measured by a timer or an outputting gpio signal. The relationship is:
dma_buf_count > polling_cycle / interrupt_interval
3. Determine the receiving buffer size. The receiving buffer that offered by user in i2s_read should be able to take all the data in all dma buffers, that means it should be bigger than the total size of all the dma buffers:
recv_buffer_size > dma_buf_count * dma_buffer_size
To check whether there are data lost, you can offer an event queue handler to the driver during installation:
QueueHandle_t evt_que;
i2s_driver_install(i2s_num, &i2s_config, 10, &evt_que);
You will receive I2S_EVENT_RX_Q_OVF event when there are data lost.
/// @brief Configure the I2S bus 0
/// @param[IN] rate : sample rate in kHz
/// @param[IN] buf_count : goes to i2s_config.dma_buf_count. 8 for common, up to 35 for longer reads
i2s_config.dma_buf_len fixed to 1024!
******************************************/
void configure_i2s(int ADCchannel, int rate, int buf_count) {
/* interrupt_interval(unit: sec) = dma_buf_len / sample_rate
dma_buf_count > max_time(I2S_read_polling_cycle) / interrupt_interval
all the received data are supposed to be stored between two i2s_read
dma_buffer_size = dma_buf_len * dma_buf_count * bits_per_sample/8 <= 4092
In our case : sample_rate=1000000, I2S_BITS_PER_SAMPLE_16BIT=16
but polling cycle is unknown! Sprite_draw ca. 37 ms, data analysis ca 37 ms, mayby polling_cycle=80ms?
==> dma_buf_len = 4092(==dma_buffer_size)/2 = 2046 (max!)
==> interrupt_interval = 2046/1000000 = 2.046ms
==> dma_buf_count > 80/2.046 ca. 40
real overall buffer size recommended: recv_buffer_size > dma_buf_count * dma_buffer_size
If all i2s_read are performed in sequence, polling_cycle<1ms, thus a few dma_buffers suffice.
Measured: 620ms for all of 48000 samples, thus 13ms for each i2s_read. Maybe 15=dma_buf_count?
*/
i2s_config_t i2s_config =
{
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), // I2S receive mode with ADC
.sample_rate = (uint32_t)rate, // sample rate
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16 bit I2S
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // only the left channel originally:I2S_CHANNEL_FMT_ALL_LEFT
//.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), // I2S format
// here is a difference in doc: I2S_COMM_FORMAT_I2S_MSB=1 but I2S_COMM_FORMAT_STAND_MSB=2
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_MSB), //I2S_COMM_FORMAT_STAND_I2S) | I2S_COMM_FORMAT_STAND_MSB), // updated @JB
.intr_alloc_flags = 1, // 1 is not defined! ESP_INTR_FLAG_LEVEL1==1<<1=2, // 0: default. ESP_INTR_FLAG_LEVEL1, in examples changed from 1 @JB // none
.dma_buf_count = buf_count, // number of DMA buffers. @JB changed to 8
.dma_buf_len = DMA_BUF_LEN, // number of samples (samples, see bits_per_sample)
.use_apll = 0, //true, //@JB was 0=no Audio PLL, @JB but which clock?
//.fixed_mclk = 0
};
if(ESP_OK != adc1_config_channel_atten((adc1_channel_t)ADCchannel, ADC_ATTEN_DB_12)){
ESP_LOGE(TAG, "Error setting up ADC attenuation. Halt!");
//while(1);
}
if(ESP_OK != adc1_config_width(ADC_WIDTH_BIT_12)){ // Configure ADC1 capture width, meanwhile enable output invert for ADC1
ESP_LOGE(TAG, "Error setting ADC bit width. Halt!");
//while(1);
}
if(ESP_OK != i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL)){
ESP_LOGE(TAG, "Error installing I2S. Halt!");
//while(1);
}
else ESP_LOGI(TAG, "I2S installed");
// Serial.println("I2S driver installed!");
if(ESP_OK != i2s_set_adc_mode(ADC_UNIT_1, (adc1_channel_t)ADCchannel)){ // ??? In this mode, the ADC maximum sampling rate is 150KHz !!!
ESP_LOGE(TAG, "Error setting up ADC mode. Halt!");
//while(1);
}
else ESP_LOGI(TAG, "I2S adc setup");
/* from original, but seems to be irrelevant:
//SET_PERI_REG_MASK(SYSCON_SARADC_CTRL2_REG, SYSCON_SARADC_SAR1_INV); Instead using viewtopic.php?t=15849 :
SYSCON.saradc_ctrl2.sar1_inv = 1; //SAR ADC samples are inverted by default
//SYSCON.saradc_ctrl.sar1_patt_len = 0; //Use only the first entry of the pattern table
//SYSCON.saradc_sar1_patt_tab[0] = 0x5C0F0F0F; //Only MSByte for channel 0 : ch_sel:3, bit_width:3?, atten:2? in one byte
// also in details: ...=((ADC1_CHANNEL_0 << 4) | (ADC_WIDTH_BIT_12 << 2) | ADC_ATTEN_DB_11) << 24
// ***IMPORTANT*** enable continuous adc sampling
SYSCON.saradc_ctrl2.meas_num_limit = 0; // ref above claims, that this is ineffective
*/
//Serial.printf("I2S ADC setup OK.\n");
vTaskDelay(1000/portTICK_PERIOD_MS); //required for stability of ADC , see ref above
// start ADC sampling
/*
if(ESP_OK != i2s_adc_enable(I2S_NUM_0)){
ESP_LOGE(TAG, "Error enabling ADC. Halt!");
//while(1);
}
else ESP_LOGI(TAG, "I2S adc enabled");
*/
ESP_LOGI(TAG, "configure_i2s success");
} /* configure_i2s */
/*
void set_sample_rate(uint32_t rate) { // rate [Hz]
i2s_driver_uninstall(I2S_NUM);
configure_i2s(rate, 8);
}
*/
esp_err_t uninstall_i2s(void) {
esp_err_t ret;
//i2s_stop(I2S_NUM_0); // there is no need to call i2s_stop() before calling i2s_driver_uninstall().
//ret = i2s_adc_disable(I2S_NUM_0);
//if(ret != ESP_OK) ESP_LOGE(TAG, "Could not disable i2s_adc");
ret = i2s_driver_uninstall(I2S_NUM_0);
if(ret != ESP_OK) ESP_LOGE(TAG, "Could not uninstall i2s");
return ret;
}
/***************************************
* Sampling len ADC values into buff
* Returns: number of 16bit samples read
* of -esp_err_t or -1 in case of error.
****************************************/
size_t ADC_Sampling(uint16_t *buff, uint32_t len)
{
size_t bytes_read; //, num2=NUM_SAMPLES*sizeof(uint16_t);
esp_err_t ret;
// see: https://docs.espressif.com/projects/esp ... s/i2s.html
if(ESP_OK != i2s_adc_enable(I2S_NUM_0)){
ESP_LOGE(TAG, "Error enabling ADC. Halt!");
//while(1);
}
// read all at once:
ret = i2s_read(I2S_NUM_0, buff, len* sizeof(uint16_t), &bytes_read, 150);
if(ESP_OK != i2s_adc_disable(I2S_NUM_0)){
ESP_LOGE(TAG, "Error disabling ADC. Halt!");
//while(1);
}
if(ret != ESP_OK) {
//Serial.printf("i2s_read returned error %d\n", ret);
ESP_LOGE(TAG, "i2s_read returned error %d", ret);
if(ret > 0) return (-ret);
else return ret;
}
else {
ESP_LOGD(TAG, "i2s_read %d Bytes", bytes_read); // check next time, if return should be size_t
return (bytes_read>>1); // number of samples returned, not no bytes!
}
} /* ADC_Sampling */
/*********************
* Just appropriation, embedding of I2S routines
**********************/
void ADC_Start(void)
{
i2s_start(I2S_NUM_0);
}
void ADC_Stop(void)
{
i2s_stop(I2S_NUM_0);
}
void ADC_ZeroDMA(void)
{
i2s_zero_dma_buffer(I2S_NUM_0);
}
I would advise you to use another MC and an external ADC. There is no "low cost Osci" around!
I successfully build one with lots of hard personal work any many pitfalls for my Synth. You will need a preamp, lots of experiences with freetos, ADC, ESP-IDF, LCD, ...
I started with "https://circuitdigest.com/microcontroll ... cilloscope"
But it was full of bugs and very misleading, because you will not be able to have readings below 0 with that setup. It structure was messy. I had to completely revise it, but it was a startup.
My best advise:by a simple chinese handheld (above 130€) if you really want to work more seriously and have readings higher than 20 kHz and maybe a larger voltage range, FFT, data saving,... and reliablity.
Sorry do not ask for my codes, they are halfway documented, messy, and maybe also faulty. I use PlatformIO and VSCode.
I you really want to learn all details: Think about >=1 year of experinences (blood and tears).
Another tipp for ADC: you have to switch MSWord and lower Word in ADC readings.
Maybe this will help. I use a sample rate of 200kHz and 48000 buffersize. The docs tell you, that you can go much higher (fake!).
/*********************************************************************************************
* All ADC and I2S routines used:
* 1) configure_i2s
* 2) set_sample_rate (not used)
* 3) Start und stop I2S
* 4) ADC_Sampling
* 5) Start and stop sampling
**********************************************************************************************/
//#include <Arduino.h> // only for debugging by serial output
//#include <stddef.h> // within Arduino.h
#include <esp_log.h>
#include <driver/i2s.h>
#include <driver/adc.h>
//#include "../../src/DFTdisplay.h" // only for DMA_BUF_LEN but fixed to 1024
#define DMA_BUF_LEN (1024)
const char TAG[] = "ADC";
/**********************************************
* From https://docs.espressif.com/projects/esp ... s/i2s.html
Please follow these steps to prevent data lost:
1. Determine the interrupt interval. Generally, when data lost happened, the interval should be the bigger the better, it can help to reduce the interrupt times, i.e., dma_buf_len should be as big as possible while the DMA buffer size won’t exceed its maximum value 4092. The relationships are:
interrupt_interval(unit: sec) = dma_buf_len / sample_rate
dma_buffer_size = dma_buf_len * slot_num * data_bit_width / 8 <= 4092
2. Determine the dma_buf_count. The dma_buf_count is decided by the max time of i2s_read polling cycle, all the received data are supposed to be stored between two i2s_read. This cycle can be measured by a timer or an outputting gpio signal. The relationship is:
dma_buf_count > polling_cycle / interrupt_interval
3. Determine the receiving buffer size. The receiving buffer that offered by user in i2s_read should be able to take all the data in all dma buffers, that means it should be bigger than the total size of all the dma buffers:
recv_buffer_size > dma_buf_count * dma_buffer_size
To check whether there are data lost, you can offer an event queue handler to the driver during installation:
QueueHandle_t evt_que;
i2s_driver_install(i2s_num, &i2s_config, 10, &evt_que);
You will receive I2S_EVENT_RX_Q_OVF event when there are data lost.
/// @brief Configure the I2S bus 0
/// @param[IN] rate : sample rate in kHz
/// @param[IN] buf_count : goes to i2s_config.dma_buf_count. 8 for common, up to 35 for longer reads
i2s_config.dma_buf_len fixed to 1024!
******************************************/
void configure_i2s(int ADCchannel, int rate, int buf_count) {
/* interrupt_interval(unit: sec) = dma_buf_len / sample_rate
dma_buf_count > max_time(I2S_read_polling_cycle) / interrupt_interval
all the received data are supposed to be stored between two i2s_read
dma_buffer_size = dma_buf_len * dma_buf_count * bits_per_sample/8 <= 4092
In our case : sample_rate=1000000, I2S_BITS_PER_SAMPLE_16BIT=16
but polling cycle is unknown! Sprite_draw ca. 37 ms, data analysis ca 37 ms, mayby polling_cycle=80ms?
==> dma_buf_len = 4092(==dma_buffer_size)/2 = 2046 (max!)
==> interrupt_interval = 2046/1000000 = 2.046ms
==> dma_buf_count > 80/2.046 ca. 40
real overall buffer size recommended: recv_buffer_size > dma_buf_count * dma_buffer_size
If all i2s_read are performed in sequence, polling_cycle<1ms, thus a few dma_buffers suffice.
Measured: 620ms for all of 48000 samples, thus 13ms for each i2s_read. Maybe 15=dma_buf_count?
*/
i2s_config_t i2s_config =
{
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), // I2S receive mode with ADC
.sample_rate = (uint32_t)rate, // sample rate
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16 bit I2S
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // only the left channel originally:I2S_CHANNEL_FMT_ALL_LEFT
//.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), // I2S format
// here is a difference in doc: I2S_COMM_FORMAT_I2S_MSB=1 but I2S_COMM_FORMAT_STAND_MSB=2
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_MSB), //I2S_COMM_FORMAT_STAND_I2S) | I2S_COMM_FORMAT_STAND_MSB), // updated @JB
.intr_alloc_flags = 1, // 1 is not defined! ESP_INTR_FLAG_LEVEL1==1<<1=2, // 0: default. ESP_INTR_FLAG_LEVEL1, in examples changed from 1 @JB // none
.dma_buf_count = buf_count, // number of DMA buffers. @JB changed to 8
.dma_buf_len = DMA_BUF_LEN, // number of samples (samples, see bits_per_sample)
.use_apll = 0, //true, //@JB was 0=no Audio PLL, @JB but which clock?
//.fixed_mclk = 0
};
if(ESP_OK != adc1_config_channel_atten((adc1_channel_t)ADCchannel, ADC_ATTEN_DB_12)){
ESP_LOGE(TAG, "Error setting up ADC attenuation. Halt!");
//while(1);
}
if(ESP_OK != adc1_config_width(ADC_WIDTH_BIT_12)){ // Configure ADC1 capture width, meanwhile enable output invert for ADC1
ESP_LOGE(TAG, "Error setting ADC bit width. Halt!");
//while(1);
}
if(ESP_OK != i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL)){
ESP_LOGE(TAG, "Error installing I2S. Halt!");
//while(1);
}
else ESP_LOGI(TAG, "I2S installed");
// Serial.println("I2S driver installed!");
if(ESP_OK != i2s_set_adc_mode(ADC_UNIT_1, (adc1_channel_t)ADCchannel)){ // ??? In this mode, the ADC maximum sampling rate is 150KHz !!!
ESP_LOGE(TAG, "Error setting up ADC mode. Halt!");
//while(1);
}
else ESP_LOGI(TAG, "I2S adc setup");
/* from original, but seems to be irrelevant:
//SET_PERI_REG_MASK(SYSCON_SARADC_CTRL2_REG, SYSCON_SARADC_SAR1_INV); Instead using viewtopic.php?t=15849 :
SYSCON.saradc_ctrl2.sar1_inv = 1; //SAR ADC samples are inverted by default
//SYSCON.saradc_ctrl.sar1_patt_len = 0; //Use only the first entry of the pattern table
//SYSCON.saradc_sar1_patt_tab[0] = 0x5C0F0F0F; //Only MSByte for channel 0 : ch_sel:3, bit_width:3?, atten:2? in one byte
// also in details: ...=((ADC1_CHANNEL_0 << 4) | (ADC_WIDTH_BIT_12 << 2) | ADC_ATTEN_DB_11) << 24
// ***IMPORTANT*** enable continuous adc sampling
SYSCON.saradc_ctrl2.meas_num_limit = 0; // ref above claims, that this is ineffective
*/
//Serial.printf("I2S ADC setup OK.\n");
vTaskDelay(1000/portTICK_PERIOD_MS); //required for stability of ADC , see ref above
// start ADC sampling
/*
if(ESP_OK != i2s_adc_enable(I2S_NUM_0)){
ESP_LOGE(TAG, "Error enabling ADC. Halt!");
//while(1);
}
else ESP_LOGI(TAG, "I2S adc enabled");
*/
ESP_LOGI(TAG, "configure_i2s success");
} /* configure_i2s */
/*
void set_sample_rate(uint32_t rate) { // rate [Hz]
i2s_driver_uninstall(I2S_NUM);
configure_i2s(rate, 8);
}
*/
esp_err_t uninstall_i2s(void) {
esp_err_t ret;
//i2s_stop(I2S_NUM_0); // there is no need to call i2s_stop() before calling i2s_driver_uninstall().
//ret = i2s_adc_disable(I2S_NUM_0);
//if(ret != ESP_OK) ESP_LOGE(TAG, "Could not disable i2s_adc");
ret = i2s_driver_uninstall(I2S_NUM_0);
if(ret != ESP_OK) ESP_LOGE(TAG, "Could not uninstall i2s");
return ret;
}
/***************************************
* Sampling len ADC values into buff
* Returns: number of 16bit samples read
* of -esp_err_t or -1 in case of error.
****************************************/
size_t ADC_Sampling(uint16_t *buff, uint32_t len)
{
size_t bytes_read; //, num2=NUM_SAMPLES*sizeof(uint16_t);
esp_err_t ret;
// see: https://docs.espressif.com/projects/esp ... s/i2s.html
if(ESP_OK != i2s_adc_enable(I2S_NUM_0)){
ESP_LOGE(TAG, "Error enabling ADC. Halt!");
//while(1);
}
// read all at once:
ret = i2s_read(I2S_NUM_0, buff, len* sizeof(uint16_t), &bytes_read, 150);
if(ESP_OK != i2s_adc_disable(I2S_NUM_0)){
ESP_LOGE(TAG, "Error disabling ADC. Halt!");
//while(1);
}
if(ret != ESP_OK) {
//Serial.printf("i2s_read returned error %d\n", ret);
ESP_LOGE(TAG, "i2s_read returned error %d", ret);
if(ret > 0) return (-ret);
else return ret;
}
else {
ESP_LOGD(TAG, "i2s_read %d Bytes", bytes_read); // check next time, if return should be size_t
return (bytes_read>>1); // number of samples returned, not no bytes!
}
} /* ADC_Sampling */
/*********************
* Just appropriation, embedding of I2S routines
**********************/
void ADC_Start(void)
{
i2s_start(I2S_NUM_0);
}
void ADC_Stop(void)
{
i2s_stop(I2S_NUM_0);
}
void ADC_ZeroDMA(void)
{
i2s_zero_dma_buffer(I2S_NUM_0);
}
Who is online
Users browsing this forum: PetalBot, Qwantbot, YisouSpider and 8 guests