DIY ESP32 Oscilloscope Project : How to make Oscilloscope using ESP32
An oscilloscope is an electronic test instrument used to observe, measure, and analyze electrical signals. It displays voltage signals as waveforms on a screen, showing how the signal changes over time. Oscilloscopes are widely used in electronics, telecommunications, engineering, education, and scientific research.
ESP32 Oscilloscope Features & Specifications:
Key Features:
» It has a simple mean filter ON/OFF functionality
» Max, min, average, and Peak-Peak voltage measurements
» Time and voltage offset adjustments
» Analog and Digital/Data Mode operation
» Single TRIGGER capability
» AUTOSCALE function for automatic setup
Components Required for Your DIY ESP32 Oscilloscope Project:

The complete DIY oscilloscope schematic is given below:
The complete schematic diagram of the DIY oscilloscope is provided below. This circuit illustrates all the essential connections between the ESP32 microcontroller, signal conditioning components, power supply section, and input interface required for signal acquisition and processing. By following this schematic, you can accurately assemble the hardware, understand the signal flow throughout the system, and troubleshoot the circuit more effectively during testing and calibration.

Circuit Design Breakdown:
The ESP32 serves as the primary controller for data acquisition. Its built-in I2S peripheral and buffering capabilities are utilized to capture, store, and process incoming signals efficiently. In this project, the 38-pin ESP32 development board is used; however, other compatible ESP32 development modules can also be employed.

Display Interface & Communication Protocol:
The DIY oscilloscope utilizes a 1.69-inch color TFT display as its primary visualization interface. Featuring a resolution of 240 × 280 pixels, the display provides sharp and detailed graphics for plotting signal waveforms, displaying voltage and time scale information, and presenting various measurement parameters. The module is based on the ST7789S display controller, which interfaces with the ESP32 microcontroller via the SPI communication protocol. Thanks to the high data throughput offered by SPI, the ESP32 can continuously transfer waveform data to the display with minimal delay, enabling near real-time signal rendering. This fast refresh capability is essential for observing dynamic signals accurately and ensuring a smooth oscilloscope-like viewing experience.

Display Features:
» High-resolution waveform rendering
» Real-time signal updates
» Integrated SD card slot for future data logging
» The design keeps power consumption to a minimum

User Interface Design for the ESP32 Oscilloscope:
User input is provided through a keypad consisting of multiple tactile push-button switches. Each switch is connected using a pull-up resistor configuration to ensure stable logic levels and prevent floating inputs. The ESP32 monitors these buttons using hardware interrupts, allowing key presses to be detected in real time with minimal latency. This method is significantly more efficient than traditional polling techniques, as the processor only responds when an actual button event occurs. As a result, the keypad offers fast and reliable operation, making it ideal for controlling oscilloscope functions such as menu navigation, parameter adjustment, and waveform settings.

Input Signal Conditioning Circuit:
The analog front-end of the DIY oscilloscope consists of a straightforward signal conditioning circuit designed to enhance measurement flexibility and protect the ESP32's ADC inputs. Two SPDT switches are employed to implement input range selection and AC/DC coupling functionality. The coupling switch enables the user to select either direct signal measurement, where both AC and DC components are displayed, or AC-coupled measurement, where a capacitor blocks the DC component and passes only the varying portion of the signal.
For voltage range selection, a precision resistor-based voltage divider is incorporated into the input stage. This divider provides a 10:1 attenuation ratio, allowing signals with peak voltages significantly greater than 3.3V to be safely scaled down before reaching the ESP32 ADC. By reducing the signal amplitude while preserving its waveform characteristics, the oscilloscope can measure higher-voltage signals without exceeding the ADC input limits, thereby improving both safety and measurement versatility.

Input Circuit Features:
» Dual-range voltage measurement (3.3V and 33V)
» AC/DC coupling selection
» Input protection circuitry
» Precision voltage division
Programming Your DIY ESP32 Oscilloscope:
Programming Steps:
» Library Installation: Extract the modified TFT_eSPI library to Arduino libraries folder
» Board Selection: Choose ESP32 in Arduino Board Manager
» Code Compilation: Verify code compilation without errors
» Upload Process: Flash firmware to ESP32 via USB
» Power Configuration: Use Micro USB port for 5V power supply
Code Features:
» Optimized I2S buffer management
» Real-time waveform rendering
» Interrupt-driven user interface
» Calibration routines
» Signal processing algorithms
Complete Project Code:
#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() {}
Conclusion:
This DIY ESP32-based oscilloscope demonstrates how modern microcontrollers can be used to create powerful and affordable test equipment for electronics enthusiasts, students, and hobbyists. By combining the ESP32's high-speed processing capabilities with a TFT display, signal conditioning circuitry, and an intuitive user interface, the project provides a practical tool for visualizing and analyzing electrical waveforms in real time.
The inclusion of features such as selectable voltage ranges, AC/DC coupling, interrupt-driven keypad controls, and high-speed waveform rendering makes the oscilloscope versatile enough for a wide range of educational and troubleshooting applications. While it may not match the performance of professional laboratory oscilloscopes, it offers an excellent balance between functionality, cost, and ease of construction.
Beyond serving as a useful measurement instrument, this project also provides valuable insight into analog signal processing, ADC operation, display interfacing, interrupt handling, and embedded system design. The modular nature of the design allows for future enhancements such as higher sampling rates, advanced triggering options, waveform storage, frequency analysis, and wireless connectivity. Overall, this DIY oscilloscope is an excellent platform for learning electronics and embedded systems while creating a practical tool that can be used in everyday projects and experiments.
