Build a DIY Mini Oscilloscope with Arduino
A real oscilloscope costs anywhere from a couple hundred to several thousand dollars, but most basic analog debugging — checking a PWM waveform, seeing whether a sensor signal is noisy, measuring the frequency of a slow signal — can be done entirely with an Arduino and a plotting script on your computer. This isn’t a professional-grade scope (Arduino’s sample rate tops out at a few tens of kHz), but it’s a genuinely useful learning and debugging tool, and nearly free if you already own an Arduino.
How it works
- The Arduino continuously reads analog voltage through its 10-bit ADC (values 0-1023, mapping to 0-5V) at the highest sample rate it can manage.
- Each reading is immediately sent over the Serial (USB) connection to the computer, packed as binary to maximize transfer speed.
- A Python script on the computer continuously reads from the serial port, decodes the values, and plots voltage over time using
matplotlib, updating in real time like an actual oscilloscope display. - Because Serial throughput is limited (~115200 baud), the achievable sample rate is roughly 5,000-10,000 samples/second — enough to observe low-frequency signals (PWM in the hundreds of Hz to a few kHz, sensor signals, low-frequency audio) but not fast enough to capture MHz-range signals like a dedicated scope.
Parts list
- Arduino Uno or Nano (boards with faster ADC handling, like SAMD/ARM-based ones, will sample faster)
- USB cable connecting the Arduino to your computer
- A computer with Python 3,
pyserial, andmatplotlibinstalled - Test leads or a probe wire to connect the signal you want to observe to pin A0
- (Recommended) A voltage divider or zener protection circuit if the measured signal could exceed 5V, to avoid damaging the ADC pin
Sample code
Arduino firmware, sampling continuously and sending binary data over Serial (faster than sending text):
const int SIGNAL_PIN = A0;
void setup() {
Serial.begin(115200);
// Speed up the ADC by lowering the prescaler (default 128 -> 16, faster but slightly less accurate)
ADCSRA = (ADCSRA & 0xF8) | 0x04;
}
void loop() {
int value = analogRead(SIGNAL_PIN); // 0-1023
byte low = value & 0xFF;
byte high = (value >> 8) & 0xFF;
Serial.write(high);
Serial.write(low);
}
Python script that reads the data and plots it live:
import serial
import matplotlib.pyplot as plt
from collections import deque
PORT = "COM3" # change to your port, e.g. /dev/ttyUSB0 on Linux
BAUD = 115200
WINDOW = 500 # number of samples shown on the plot
ser = serial.Serial(PORT, BAUD, timeout=1)
data = deque([0] * WINDOW, maxlen=WINDOW)
plt.ion()
fig, ax = plt.subplots()
line, = ax.plot(data)
ax.set_ylim(0, 1023)
ax.set_ylabel("ADC value (0-1023)")
ax.set_xlabel("Sample")
while True:
raw = ser.read(2)
if len(raw) < 2:
continue
value = (raw[0] << 8) | raw[1]
data.append(value)
line.set_ydata(data)
fig.canvas.draw()
fig.canvas.flush_events()
Common pitfalls
- Measuring a signal above 0-5V (or 0-3.3V on a 3.3V board) can damage the ADC pin — always use a voltage divider or attenuator to bring the signal into the Arduino’s accepted range before measuring.
- Sending data as text (
Serial.println) instead of binary drastically lowers the achievable sample rate since each number takes several ASCII bytes — useSerial.write()with binary data whenever you need speed. - No shared GND between the probe point and the Arduino produces skewed or completely noisy readings — always connect GND before connecting the signal wire.
- Expecting to capture high-frequency signals (MHz range) — the Arduino’s ADC tops out at a few tens of kHz, which isn’t fast enough to view high-speed digital signals like I2C or SPI.
Extensions
Add a trigger feature (only start plotting once the signal crosses a threshold, like a real oscilloscope) for a more stable view of repeating signals, switch to an ESP32 with a faster ADC and stream data over WiFi instead of Serial for untethered use, or pair it with the self-hosted sensor dashboard with Grafana project to store and replay waveforms over time.
Comments