DIY ESP32 Oscilloscope

Published  June 29, 2022   6
DIY ESP32 based Oscilloscope

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 based Oscilloscope Circuit Connection

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.

ESP32 MCU Pinout

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.

1.69" TFT Display Module Pinout

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.

ESP32 Display Module

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.

Keypad Internal Circuit

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.

ESP32 Oscilloscope Ananlog Input Circuit

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.

ESP32 Oscilloscope PCB Design

And here is the PCB view for the same.

Oscilloscope PCB Layout

Bottom side PCB view.

PCB Layout for ESP32 Oscilloscope

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.

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() {}
Video

Comments

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

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 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)) ; }