Digital Oscilloscope
2025-07-28 | By Mustahsin Zarif
License: Attribution Oscilloscope Serial / UART Arduino
Oscilloscopes are fun engineering equipment that allow us to see how the voltage of a signal varies with time, and measure useful values such as frequency, peak-to-peak voltage, DC offset, and more. However, many oscilloscopes are bulky and may be expensive. In this project, we will explore building a portable, low-cost oscilloscope using:
Some standard functions in an oscilloscope that I will implement are:
Peak-to-peak
Vmax
Vmin
DC offset (Vavg)
Frequency
Calibrate
Lastly, since I do not want to use a signal generator, I will use a potentiometer to generate a wave manually.
Here’s an image of my put-together circuit, and a table mapping buttons to functions:
Side note: Pull-up resistors
Note how there is no resistor between the push buttons and the digital pins or ground. This is contradictory to Arduino’s tutorial on “How to Wire and Program a Button,” where they use a pull-down resistor. This is because I am instead using a pull-up resistor that is built into the Arduino, and I do this by declaring the pin as an INPUT_PULLUP in the code, as you will see.
Let’s see how a pull-up resistor works.
Code
Here is my full code if you want to give it a read before we dive into the functions individually:
#include <Arduino.h>
#include <LiquidCrystal.h>
void calibrate();
void findMaxVoltage();
void findMinVoltage();
void findAvgVoltage();
void peakToPeak();
float mapVoltage();
void findFrequency();
//declare 6 button pins
const int pk2pkButton = 6;
const int vmaxButton = 7;
const int vminButton = 8;
const int vavgButton = 9;
const int frqButton = 10;
const int calibrateButton = 13;
float minVoltage = 10.0; // Initialize min voltage to a high value
float maxVoltage = -10.0; // Initialize max voltage to a low value
float avgVoltage = 0.0; // Initialize average voltage to 0.0
float peakToPeakVoltage = 0.0; // Initialize peak-to-peak voltage to 0.0
//declare ADC pin to act as oscilloscope probe
const int probe1 = A0; // ADC pin for probe 1
float frequency = 0.0; // Frequency variable
// initialize the library by associating any needed LCD interface pin
// with the arduino pin number it is connected to
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
void setup() {
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
// Print a message to the LCD.
lcd.print("hello, world!");
delay(2000); // Wait for 2 seconds
lcd.clear(); // Clear the LCD
// Set button pins as input with pull-up resistors
pinMode(pk2pkButton, INPUT_PULLUP);
pinMode(vmaxButton, INPUT_PULLUP);
pinMode(vminButton, INPUT_PULLUP);
pinMode(vavgButton, INPUT_PULLUP);
pinMode(frqButton, INPUT_PULLUP);
pinMode(calibrateButton, INPUT_PULLUP);
pinMode(probe1, INPUT); // Set probe pin as input
lcd.print("calibrating on start up...");
delay(2000); // Wait for 2 seconds
Serial.begin(9600);
calibrate(); // Call the calibration function
lcd.clear();
lcd.print("ready!");
}
void loop() {
float voltage = analogRead(probe1); // Read the voltage from probe 1
voltage = mapVoltage(voltage);
Serial.println(voltage);
if (digitalRead(pk2pkButton) == LOW) {
lcd.clear();
lcd.print("pk2pkButton pressed");
lcd.setCursor(0, 1); // Set cursor to second line
peakToPeak(); // Call the function to find peak-to-peak voltage
lcd.print("pk2pk: ");
lcd.print(peakToPeakVoltage);
lcd.print(" V");
}
if (digitalRead(vmaxButton) == LOW) {
lcd.clear();
lcd.print("vmaxButton pressed");
lcd.setCursor(0, 1); // Set cursor to second line
findMaxVoltage(); // Call the function to find max voltage
lcd.print("Max: ");
lcd.print(maxVoltage); // Print max voltage
lcd.print(" V");
}
if (digitalRead(vminButton) == LOW) {
lcd.clear();
lcd.print("vminButton pressed");
lcd.setCursor(0, 1); // Set cursor to second line
findMinVoltage(); // Call the function to find min voltage
lcd.print("Min: ");
lcd.print(minVoltage); // Print min voltage
lcd.print(" V");
}
if (digitalRead(vavgButton) == LOW) {
lcd.clear();
lcd.print("vavgButton pressed");
lcd.setCursor(0, 1); // Set cursor to second line
findAvgVoltage(); // Call the function to find average voltage
lcd.print("Avg: ");
lcd.print(avgVoltage); // Print average voltage
lcd.print(" V");
}
if (digitalRead(frqButton) == LOW) {
lcd.clear();
lcd.print("frqButton pressed");
lcd.setCursor(0, 1); // Set cursor to second line
findFrequency(); // Call the function to find frequency
lcd.print("Freq: ");
lcd.print(frequency); // Print frequency
lcd.print(" Hz");
}
if (digitalRead(calibrateButton) == LOW) {
lcd.clear();
lcd.print("calibrateButton pressed");
delay(1000); // Wait for 1 second
calibrate(); // Call the calibration function
delay(50);
}
}
void calibrate() {
lcd.clear();
lcd.print("Calibrating...");
//measure voltages from probe for 30 seconds to find min and max
unsigned long startTime = millis();
float voltage;
maxVoltage = -10.0;
minVoltage = 10.0;
while (millis() - startTime < 10000) { // 30 seconds
voltage = analogRead(probe1); // Read the voltage from probe 1
//voltage = map(voltage, 0, 1023, 0, 5000) / 1000; // Convert to volts DOES NOT HANDLE FRACTIONS
voltage = mapVoltage(voltage);
if (voltage < minVoltage) {
minVoltage = voltage; // Update min voltage
}
if (voltage > maxVoltage) {
maxVoltage = voltage; // Update max voltage
}
Serial.println(voltage);
}
avgVoltage = (maxVoltage + minVoltage) / 2; // Calculate average voltage
startTime = millis(); // Reset start time for frequency measurement
unsigned long crossingTime = 0; // Initialize crossing time
int crossings = 0; // Initialize crossing count
frequency = 0.0; // Reset frequency
while (millis() - startTime < 10000) { // 30 seconds
voltage = digitalRead(probe1);
voltage = mapVoltage(voltage);
if (voltage == avgVoltage) {
crossings++;
crossingTime += millis() - startTime - crossingTime; // Time since the last crossing
}
Serial.println(voltage);
}
if (crossings > 0) {
crossingTime /= crossings; // Average time between crossings
// Calculate frequency from time period
long period = crossingTime 4; // Time period in milliseconds
frequency = 1/(period1000); // Frequency in Hz
lcd.setCursor(0, 1); // Set cursor to second line
lcd.print("Freq: ");
lcd.print(frequency);
lcd.print(" Hz");
lcd.setCursor(0, 0); // Set cursor to first line
delay(100); // Small delay to avoid rapid reading
}
delay(3000);
lcd.clear();
lcd.print("Calibration done!");
delay(1000);
}
//find max voltage in 10 seconds
void findMaxVoltage() {
unsigned long startTime = millis();
maxVoltage = -10.0; // Reset max voltage
while (millis() - startTime < 10000) { // 10 seconds
float voltage = analogRead(probe1); // Read the voltage from probe 1
voltage = mapVoltage(voltage);
if (voltage > maxVoltage) {
maxVoltage = voltage; // Update max voltage
}
Serial.println(voltage);
//delay(100);
}
}
//find min voltage in 10 seconds
void findMinVoltage() {
unsigned long startTime = millis();
minVoltage = 10.0; // Reset min voltage
while (millis() - startTime < 10000) { // 10 seconds
float voltage = analogRead(probe1); // Read the voltage from probe 1
voltage = mapVoltage(voltage);
if (voltage < minVoltage) {
minVoltage = voltage; // Update min voltage
}
Serial.println(voltage);
//delay(100);
}
}
//find average voltage in 10 seconds
void findAvgVoltage() {
unsigned long startTime = millis();
float totalVoltage = 0.0; // Initialize total voltage
int count = 0; // Initialize count of readings
while (millis() - startTime < 10000) { // 10 seconds
float voltage = analogRead(probe1);
voltage = mapVoltage(voltage);
totalVoltage += voltage; // Add to total voltage
count++;
//delay(100);
Serial.println(voltage);
}
avgVoltage = totalVoltage / count;
}
//find peak to peak voltage in 10 seconds
void peakToPeak() {
unsigned long startTime = millis();
float minVoltage = 10.0;
float maxVoltage = -10.0;
while (millis() - startTime < 10000) { // 10 seconds
float voltage = analogRead(probe1);
voltage = mapVoltage(voltage);
if (voltage < minVoltage) {
minVoltage = voltage;
}
if (voltage > maxVoltage) {
maxVoltage = voltage;
}
Serial.println(voltage);
//delay(100);
}
peakToPeakVoltage = maxVoltage - minVoltage; // Calculate peak-to-peak voltage
}
void findFrequency() {
unsigned long startTime = millis();
unsigned long halfPeriod = 0;
int crossings = 0;
for (crossings =0; crossings < 10; crossings++) {
float voltage = analogRead(probe1); // Read the voltage from probe 1
voltage = mapVoltage(voltage);
if (voltage == avgVoltage) {
halfPeriod += millis() - startTime - halfPeriod; // Time since the last crossing
}
Serial.println(voltage);
}
if (crossings > 0) {
halfPeriod /= crossings; // Average time between crossings
// Calculate frequency from time period
long period = halfPeriod 2; // Time period in milliseconds
frequency = 1/(period1000); // Frequency in Hz
}
}
float mapVoltage(float voltage) {
if (voltage < 511.5)
return -((511.5 - voltage) 5.0 / 511.5);
else
return ((voltage - 511.5) 5.0 / 511.5);
}
Vmax, Vmin, pk2pk
I read the analog pin measurement connected to the potentiometer for 10 seconds and find the maximum and minimum voltages the waveform takes within those 10 seconds. The difference between the two is the peak-to-peak value.
//find peak to peak voltage in 10 seconds
void peakToPeak() {
unsigned long startTime = millis();
float minVoltage = 10.0;
float maxVoltage = -10.0;
while (millis() - startTime < 10000) { // 10 seconds
float voltage = analogRead(probe1);
voltage = mapVoltage(voltage);
if (voltage < minVoltage) {
minVoltage = voltage;
}
if (voltage > maxVoltage) {
maxVoltage = voltage;
}
Serial.println(voltage);
//delay(100);
}
peakToPeakVoltage = maxVoltage - minVoltage; // Calculate peak-to-peak voltage
}The custom mapVoltage() vs Arduino map() function
Arduino has a built-in function called map() that can be used to, well, map the 1024 different values the analog pin can read to a value between -5 and 5.
y = map(x, 1, 50, 50, -100);
However, the problem here is that the map() function returns integer values only, so we need to write our own mapping function.
Frequency
I have a very rudimentary method of finding the frequency. My approach is to average the times at which the wave passes the average voltage value to find half the frequency. F = 1/T, so I multiply the average half a period by 2, then take the inverse.
Calibrate
In the first 10 seconds after starting up, I find the frequency and I find the DC offset of the wave by averaging the voltage readings. This section can be modified however we want. Be creative! For example, we cannot input any signal, and any reading picked up can be set as noise and subtracted from our actual readings.
I hope this gave you all a good overview of how to start building a basic digital oscilloscope. I did this in 4 hours for a competition, so I focused on basic functionality more than advanced, high-quality features. Feel free to clone my GitHub repository and add on to the project. Potential additions include building a better interface using Python or using an ESP32 to display the oscilloscope waveform on a webpage!

