The Oscilloscope is a must-have test instrument for any electronics engineer. It is used to visualize and observe various signals, usually as a two-dimensional plot with one or more signals plotted against time. They are used in the design and debugging of electronic devices to view and compare waveforms, and determine voltage levels, frequency, noise, and other parameters of signals applied at its input as it changes with time. This makes Oscilloscopes a very important tool on the desk of an electronics engineer or maker. Oscilloscopes, however, are quite pricey; entry-level models can cost anywhere from $500 to $2,000. And the advanced oscilloscopes cost few thousands of dollars, which puts them beyond the reach of basic users. But what if we could create one which is cheaper, compact, and easy to make? That is the question that led to today’s tutorial.
ESP32 Oscilloscope Features
- Single-channel
- 1Msps
- 50000 @ 16bits buffer (50ms of data at 1Msps)
- Scale from 10us/div to 5ms/div at 1Msps
- Maximum VPP 3.3V in 1X and 33V in 10X mode
- Fast and responsive control using tactile switches.
- Frequency calculations (20hz min due to buffer size)
- Simple mean filter ON/OFF
- Max, min, average and Peak-Peak voltage
- Time and voltage offset
- Analog, Digital/Data Mode
- Single TRIGGER
- AUTOSCALE
Components Required to build ESP32-based Oscilloscope
- ESP32 Devkit
- 1.69” 240x280 Rounded Corner TFT display(ST7789s)
- Tactile switches
- SPDT switches
- 100K resistor
- 10K resistor
- 100nF capacitor
- Copper clad or perfboard
- Soldering tools
ESP32 Oscilloscope Circuit Diagram
The complete circuit diagram for the ESP32-based oscilloscope is given below.
ESP32 is used as the controller for the data acquisition. We will be utilizing the inbuilt I2S buffer to store and manipulate the signals. Here the 38 Pin variant is used but you can also use other development modules too.
For display, we are using a 1.69” TFT display module. It has a resolution of 240x280 pixels. The display controller is ST7789S and to drive this, we will be using the SPI communication.
The module also contains an SD card slot which we haven’t used. We can use this for waveform capture or similar applications in the future update.
The Keypad is very simple. Tactile switches with pullup resistors are used for this purpose. We are using the hardware interrupt to detect each key press. This will give us a very responsive keypad. You can learn about ESP32 Interrupts that we covered previously.
The analog input section is fairly simple. It consists of two SPDT switches for range selection and AC/DC coupling selection. For range selection, we have added a voltage divider that can be used to feed the signals which have a peak voltage higher than 3.3V. The voltage divider will convert the signal to a 10:1 ratio.
Building and Testing the Circuit
You can either build this project in a perfboard or you can make a PCB with the files from the link at the bottom of the page. Both PDF files for the toner transfer method and the Gerber file for the manufacturing are included. Here is the PCB layout for the Oscilloscope.
And here is the PCB view for the same.
Bottom side PCB view.
Arduino Code for Oscilloscope
Download the entire code from the Circuit Digest GitHub repo link given at the bottom of this article. In the GitHub repo, you can also find an archive named TFT_eSPI. This modified library is necessary to drive the display. Extract it to the Arduino library folder. If you have already installed TFT_eSPI library, make sure to remove it before extracting the modified one. Once it’s done, select esp32 in the board manager. Then compile the code and upload it. That’s it our DIY Oscilloscope is ready to use. You can power the Oscilloscope using the Micro USB port at the bottom. This port is only for power.
#include <Arduino.h> #include <driver/i2s.h> #include <driver/adc.h> #include <soc/syscon_reg.h> #include <TFT_eSPI.h> #include <SPI.h> #include "esp_adc_cal.h" #include "filters.h" //#define DEBUG_SERIAL //#define DEBUG_BUFF #define DELAY 1000 // Width and height of sprite #define WIDTH 240 #define HEIGHT 280 #define ADC_CHANNEL ADC1_CHANNEL_5 // GPIO33 #define NUM_SAMPLES 1000 // number of samples #define I2S_NUM (0) #define BUFF_SIZE 50000 #define B_MULT BUFF_SIZE/NUM_SAMPLES #define BUTTON_Ok 32 #define BUTTON_Plus 15 #define BUTTON_Minus 35 #define BUTTON_Back 34 TFT_eSPI tft = TFT_eSPI(); // Declare object "tft" TFT_eSprite spr = TFT_eSprite(&tft); // Declare Sprite object "spr" with pointer to "tft" object esp_adc_cal_characteristics_t adc_chars; TaskHandle_t task_menu; TaskHandle_t task_adc; float v_div = 825; float s_div = 10; float offset = 0; float toffset = 0; uint8_t current_filter = 1; //options handler enum Option { None, Autoscale, Vdiv, Sdiv, Offset, TOffset, Filter, Stop, Mode, Single, Clear, Reset, Probe, UpdateF, Cursor1, Cursor2 }; int8_t volts_index = 0; int8_t tscale_index = 0; uint8_t opt = None; bool menu = false; bool info = true; bool set_value = false; float RATE = 1000; //in ksps --> 1000 = 1Msps bool auto_scale = false; bool full_pix = true; bool stop = false; bool stop_change = false; uint16_t i2s_buff[BUFF_SIZE]; bool single_trigger = false; bool data_trigger = false; bool updating_screen = false; bool new_data = false; bool menu_action = false; uint8_t digital_wave_option = 0; //0-auto | 1-analog | 2-digital data (SERIAL/SPI/I2C/etc) int btnok,btnpl,btnmn,btnbk; void IRAM_ATTR btok() { btnok = 1; } void IRAM_ATTR btplus() { btnpl = 1; } void IRAM_ATTR btminus() { btnmn = 1; } void IRAM_ATTR btback() { btnbk = 1; } void setup() { Serial.begin(115200); configure_i2s(1000000); setup_screen(); pinMode(BUTTON_Ok , INPUT); pinMode(BUTTON_Plus , INPUT); pinMode(BUTTON_Minus , INPUT); pinMode(BUTTON_Back , INPUT); attachInterrupt(BUTTON_Ok, btok, RISING); attachInterrupt(BUTTON_Plus, btplus, RISING); attachInterrupt(BUTTON_Minus, btminus, RISING); attachInterrupt(BUTTON_Back, btback, RISING); characterize_adc(); #ifdef DEBUG_BUF debug_buffer(); #endif xTaskCreatePinnedToCore( core0_task, "menu_handle", 10000, /* Stack size in words */ NULL, /* Task input parameter */ 0, /* Priority of the task */ &task_menu, /* Task handle. */ 0); /* Core where the task should run */ xTaskCreatePinnedToCore( core1_task, "adc_handle", 10000, /* Stack size in words */ NULL, /* Task input parameter */ 3, /* Priority of the task */ &task_adc, /* Task handle. */ 1); /* Core where the task should run */ } void core0_task( void * pvParameters ) { (void) pvParameters; for (;;) { menu_handler(); if (new_data || menu_action) { new_data = false; menu_action = false; updating_screen = true; update_screen(i2s_buff, RATE); updating_screen = false; vTaskDelay(pdMS_TO_TICKS(10)); Serial.println("CORE0"); } vTaskDelay(pdMS_TO_TICKS(10)); } } void core1_task( void * pvParameters ) { (void) pvParameters; for (;;) { if (!single_trigger) { while (updating_screen) { vTaskDelay(pdMS_TO_TICKS(1)); } if (!stop) { if (stop_change) { i2s_adc_enable(I2S_NUM_0); stop_change = false; } ADC_Sampling(i2s_buff); new_data = true; } else { if (!stop_change) { i2s_adc_disable(I2S_NUM_0); i2s_zero_dma_buffer(I2S_NUM_0); stop_change = true; } } Serial.println("CORE1"); vTaskDelay(pdMS_TO_TICKS(300)); } else { float old_mean = 0; while (single_trigger) { stop = true; ADC_Sampling(i2s_buff); float mean = 0; float max_v, min_v; peak_mean(i2s_buff, BUFF_SIZE, &max_v, &min_v, &mean); //signal captured (pp > 0.4V || changing mean > 0.2V) -> DATA ANALYSIS if ((old_mean != 0 && fabs(mean - old_mean) > 0.2) || to_voltage(max_v) - to_voltage(min_v) > 0.05) { float freq = 0; float period = 0; uint32_t trigger0 = 0; uint32_t trigger1 = 0; //if analog mode OR auto mode and wave recognized as analog bool digital_data = !false; if (digital_wave_option == 1) { trigger_freq_analog(i2s_buff, RATE, mean, max_v, min_v, &freq, &period, &trigger0, &trigger1); } else if (digital_wave_option == 0) { digital_data = digital_analog(i2s_buff, max_v, min_v); if (!digital_data) { trigger_freq_analog(i2s_buff, RATE, mean, max_v, min_v, &freq, &period, &trigger0, &trigger1); } else { trigger_freq_digital(i2s_buff, RATE, mean, max_v, min_v, &freq, &period, &trigger0); } } else { trigger_freq_digital(i2s_buff, RATE, mean, max_v, min_v, &freq, &period, &trigger0); } single_trigger = false; new_data = true; Serial.println("Single GOT"); //return to normal execution in stop mode } vTaskDelay(pdMS_TO_TICKS(1)); //time for the other task to start (low priorit) } vTaskDelay(pdMS_TO_TICKS(300)); } } } void loop() {}
Comments
This is a very nice project…
This is a very nice project. I am going to build and use.
I have a breadboard up and running the code with one problem.
I am using the ESP32 D1 mini board (with limited GPIO)
The analog port of the project code is not availible (ADC1_CHANNEL05 GPIO33)
I have changed to: #define ADC_CHANNEL ADC1_CHANNEL_0 // GPIO36
The measurement of frequency and voltage (pp) on the display is ok
B U T I have no oscilloscop signal.
What must I do to fix this problem?
I have the same problem,…
I had the same problem, when menu was activated I got Vmax: -6.42V and Vmin: -6.6V
1. Function ADC_Sampling have to be changed to:
void ADC_Sampling(uint16_t *i2s_buff){
size_t bytes_read; for (int i = 0; i < B_MULT; i++) {
i2s_read(I2S_NUM_0, (void*)&i2s_buff[i * NUM_SAMPLES], NUM_SAMPLES * sizeof(uint16_t), &bytes_read, portMAX_DELAY);
for(size_t ix = 0; ix < bytes_read/2; ix++) i2s_buff[(i * NUM_SAMPLES) + ix] &= 0x0FFF; // 16bit to 12bit conversion
}
}
2. Functions in screen.ino have to be corrected as well:
float to_scale(float reading) {
float temp = WIDTH - (((reading / 4095.0) + (offset / 3.3)) * 3300 / (v_div * 6)) * (WIDTH - 1) - 1; return temp;
}
float to_voltage(float reading) { return reading / 4095.0 * 3.3; }
uint32_t from_voltage(float voltage) { return ((uint32_t)(voltage / 3.3 * 4095)) ; }
Dear Darius, Thank you so…
Dear Darius,
Thank you so much for solving the problem. I have changed the functions with your lines and now i have a oscilloscope signal.
Great, thanks again. You are a real professional.
Best regards
Paul
The same problem here. Code…
The same problem here. Code is running on display but no oscilloscope signal.
I have put a question on the forum. No answers yet.
Hi guy, It seems to be a…
Hi guy,
It seems to be a really nice project. I wonder if I can put a bigger LCD to be more comfortable.
Cheers
Will you please Guide how to…
Will you please Guide how to upload the code??
Hi all. Can this project be…
Hi all.
Can this project be used on TTGOS3? or with a external DAC if need?
Thanks.
I presume my noob-bench is…
I presume my noob-bench is surrounded by devices containing analog waveform sources using a compatible voltage, but I'm reluctant to open then up to my new un-tested scope. Can you recommend a simple demo/test setup (signal source) for breadboard?
Hi, Has anyone else ad…
Hi,
Has anyone else ad this error when compiling?
Compilation error: 'ADC_ATTEN_11db' was not declared in this scope; did you mean 'ADC_ATTEN_DB_6'?
These are the two lines that are causing problems in i2s.ino tab
In the code it says '//TODO i2s_read_bytes is deprecated, replace with new function'
Did you ever get to writing a new function with that because I'm not entirely sure how to go about doing that so that I can get input
Many Thanks,
Nic