Multichannel Arduino Oscilloscope

16,980

78

30

Introduction: Multichannel Arduino Oscilloscope

An oscilloscope shows how electric signals vary in time. It is an essential instrument for electronic design and experiments that involve sensors or actuators.

High-end oscilloscopes that measure billions samples per second (Gs/s) or more can cost thousands of euros, but one can also find oscilloscopes with lower speeds for much less.

The popular Arduino (Uno R3) microcontroller has a built-in analog-to-digital converter (ADC) to sample analog electric signals and can thus be used as a rudimentary oscilloscope, when connected to a PC for displaying the traces. Indeed you can find several examples of those here on Instructables. However, I did not manage to find one that requires no additional hardware, samples at the maximum speed of the Arduino ADC (77ks/s) and runs on both Windows, Mac and Linux.

The Arduino oscilloscope described in these instructions has the following highlights:

  • No extra hardware beyond an Arduino connected to a PC
  • Sample rate of up to 77ks/s
  • Up to six input channels
  • 10-bit precision
  • Two vertical ranges (0-5V or 0-1.1V)
  • Trigger on rising or falling edge
  • Free running or one-shot operation
  • Integrated pulse generator
  • Display on computer screen using open-source ‘processing’ sketch

Although the sampling speed is modest, its applications are many, to name a few:

  • Debugging electronic circuits with audio-frequency signals
  • Studying signal filters
  • Testing transformers, inductors and speakers
  • Testing and characterising sensors
  • Testing and characterising motors and other actuators
  • Education

Step 1: Setup

  1. Download and install the Arduino integrated development environment (IDE). I used version 1.6.12.
  2. Download and install the Processing IDE. I used version 3.3.
  3. Connect the Arduino to the PC with USB cable
  4. Download the Arduino sketch ArduinoOscilloscope_v1_0.ino and upload to the Arduino (the warning about low memory can be ignored)
  5. Download the Processing sketch ArduinoOscilloscope_v1_0.pde and run it
  6. Connect pin D9 to A0 on the Arduino

The last step sends the signal from the pulse generator to the first input channel. You should now see on your computer screen a window displaying a 1kHz signal, similar as in the picture above. The signal will not be stable since the readout rate does not in general correspond to an integer number of pulses. Take a single shot or activate the trigger (next step) to stabilise the trace.

Step 2: Instructions for Use

The main screen with the traces is divided in divisions (div), which are then divided in subdivision with thinner lines. The time scale (ms/div) and voltage scale (V/div) refer to the ‘full’ divisions. The subdivisions are there to improve the precision of time and voltage reading from the main screen.

By clicking on various parts of the screen the settings can be changed:

Free running or single shots

By default, the display continually updates the signal. If instead you want to freeze the signal, click on ‘shot’ and it will make a single sweep which then can be studied quietly. To take another sweep, click ‘shot’ again. To go back to free running, click ‘run’

Channel selection

The active input channels are highlighted in grey. Click on the numbers 0-5 to select or deselect additional channels. The channels 0-5 take their input from the Arduino pins A0-A5, respectively. Beware that the Arduino internally has only one ADC, so if you select more than one channel, it rotates the measurements between the different channels, thus reducing the actual sampling rate on each channel. Beware that the at the fasted speed, 1ms/s, the Arduino ADC cannot switch fast enough between channels, and the multi-channel readout gives distorted signals in case the signals differ strongly. Revert back to either single-channel operation or slower readout speeds.

Trigger settings

Next to the channel number are two symbols, for rising edge and falling edge, respectively. Click on any of these to activate edge-triggering. This will stabilise the signal because it fixes the time reference to the point where the signal goes either above (rising edge) or below (falling edge) a certain threshold. The threshold is indicated with the little arrow on the left of the screen. You can set it higher or lower by clicking (not dragging!) at any other height above or below the arrow. The little arrow on the top of the screen is the trigger offset. Anything to the left of the arrow happened before the trigger, anything to the right of the arrow happened after the trigger. The trigger offset can also be changed by clicking (not dragging!) to the left or to the right of the arrow. To go back to triggerless running, click on the (rising or falling) trigger symbol that is highlighted in grey. It will go turn black again and the trigger will turn off.

Time scale settings

The fastest possible readout of 77ks/s is selected by choosing 1ms/div. To see slower signals over longer time spans, a larger value of ms/div can be chosen. This will slow down the sampling rate to 35.5ks/s for 2ms/div, 15.5 ks/s for 5ms/div etc. For the slowest settings, the update frequency of the screen will go down, simply because it takes more than 1,5s to make a full sweep at 100ms/div. (again: Beware that the at the fasted speed, 1ms/s, the Arduino ADC cannot switch fast enough between channels, and the multi-channel readout gives distorted signals in case the signals differ strongly. Revert back to either single-channel operation or slower readout speeds.)

Number of samples

The default number of samples is 1200. That is close to the maximum that the Arduino can store in its 2kB RAM memory. This number can be reduced to 600 or 300 to zoom in on very fast signals or to increase the refresh rate of the display.

Vertical scale settings

The Arduino has two voltage references that can be used without additional hardware: the 5V USB power or an internal 1.1V reference voltage. By default, the full scale refers to 5V (divided into 5 divisions of 1V each), but to zoom in on smaller signals, click on 0.2V, which will reduce the full scale to 1.1V.

Pulser on pin 9

The Arduino has integrated hardware timers, which can be used to generate signals. Here, output A of timer 1 is used, which gives pulsed signals on pin D9. This signal can be directly fed to any of the analog inputs to test the oscilloscope, or it can be used to activate other components (for example an RC filter), whose output can then be studied on any of the other analog inputs. The pulser bar has four rows. The upper two rows are to select the period/frequency and the lower two rows to select the pulse length/duty cycle.

Step 3: Technical Notes About the Implementation

Here I mention some of the considerations for the technical implementation. Feel free to skip to the next sections with example measurements.

ADC clock speed

The ADC built into Arduino's ATMEGA328 microcontroller is of the successive approximation type and needs 13 ADC-clock cycles to complete a measurement. The 16MHz Arduino clock is too fast for the ADC to operate properly, therefore it needs to be prescaled. Prescale factors of 1 (no prescale) to 128 can be selected. By default, the Arduino IDE sets the prescale to 128, resulting in an ADC-clock period of 128/16MHz=8 microseconds, and thus a sampling period of 13*8=104 microseconds. Many tests have shown reducing the prescale to 16 results in only a slight loss of precision, while even faster clocks result in garbage. Therefore the fastest possible measurement, at the 1ms/div setting, corresponds to an ADC prescale of 16, an ADC-clock of 1MHz, a sampling time of 13 microseconds, and a sampling speed of 1/13 microseconds=76.92ks/s. At the 2, 5, and 10ms/div settings the prescale is increased to 32,64, and 128, respectively. Thus the full 10-bit precision is supposed to be achieved for time scales of 10ms/div and above. But even at 1ms/div the traces look good with little visible noise.

ADC sampling

The Arduino ADC can be operated in single acquisition or free running mode. The standard analogRead() function does a single acquisition and returns the value when the ADC is ready. Even with a faster ADC clock, this can never result in the highest possible sampling rate, since some processing time is needed in between measurements. It is also possible to start the ADC, do some processing, wait for the ADC to be ready and then start a new measurement. However, this approach will result in a minimum of 14 ADC-clock cycles per measurement. Here, the free-running mode is used, which is the fastest. The challenge is to synchronise the ADC measurement with the storage and processing (e.g triggering, multiplexing). It is possible to use interrupts when the ADC measurement is ready, but interrupts have a small CPU overhead and can be difficult to program. Instead, it is also possible to use a loop and poll for the ADIF flag which indicates that the ADC reading is complete, even if no interrupts are called.

Signal storage

A circular buffer allows to visualise signals before the trigger, an extremely useful feature. The samples are stored continuously and when a trigger is fired continue for less than a full cycle. The limited RAM of the Arduino is another issue. 1200 samples of 8-bit readings take up 1200 bytes. To get 10-bit precision, we cannot store the 2 extra bits in a byte each, as that would require 2400 bytes. Instead, the 2 least significant bits (LSBs) of 4 measurements can be packed into a byte, resulting in a total of 1500 bytes, which leaves 548 bytes for local and global variables. Some attention is paid to use low-level fast bit-shifting operations to pack the LSBs. When sending to the PC, the bits are rearranged into 6+6 to avoid the number 255, which is used to indicate a new sample.

Processing between measurements

The CPU time between ADC measurements is very limited: 13x16=208 clock cycles are available to store the signal in a circular buffer, switch between signals and check for the trigger. However, it seems to be doable, paying attention to the following:

  • Disable interrupts. By default, timer0 generates an interrupt every ms to update the values used in millis() and micros(). These take too long and result in missed samples.
  • Use the smallest possible data types (‘byte’ for any number in the range 0-255)
  • Use local variables where possible: they can be stored in fast registers
  • Avoid divisions and the modulo operator. For a circular counter is is much faster to reset the counter when the top is reached.
  • Order if-statements in such a way that the most unlikely condition is tested first.

Channel multiplexing

The oscilloscope allows arbitrary channel selection, and the ADC multiplexer must switch after each measurement. To speed this up, a lookup table of 6 entries which contain the next channel is used. This table can be generated outside the measurement loop. The ATMEGA328P datasheet mentions “The user is advised not to write new channel or reference selection values during Free Running mode.” However it seems to give few problems in practice. The only issue I’ve seen is at the highest sampling speed, when multiplexing between signals that differ strongly in voltage level, the signals seem pulled towards each other, as if there is a follower that cannot keep up with the fast changes of the ADC input.

Triggering

There is not much time for processing the signal to check for triggering, however, a simple edge trigger is feasible: the new measurement is compared to the previous one, and if one is above the trigger and the other below, a trigger can be fired. The sampling will continue by the total number of samples minus the trigger offset, so that samples from before the trigger are not overwritten and can be displayed.

Pulse generation

The three built-in hardware timers are great to generate pulses so that the same Arduino can also be used as a pulse generator. There is no CPU cost since the timers run in parallel to the CPU and activating them can be done simply by setting the timer registers outside of the measurement loop. Timer1 is the most powerful timer: it has 16-bit precision and has an operating mode known as “Phase and Frequency Correct PWM mode”, which allows to set independently the frequency and the duty cycle.

Communication between the Arduino and the PC

The communication goes over the serial link, using the highest possible baud rate (115200 b/s). It is set up that the Arduino will only generate a sweep when instructed to do so by the PC. There is no formal handshaking and the way the communication is set up presently may not be 100% robust: for example, the number 255 is used to indicate the beginning of a set of commands, but the number 255 can also appear as part of the command. The strict checking of the 30-byte command seems to remove most of the corrupt commands.

Step 4: Example 1: 50Hz Pickup

The electric cables that supply power to our household appliances create electric and magnetic fields that are easily picked up with an antenna. For the high-impedance analog input of the Arduino, a short single wire will show up this signal easily.

Attach a wire of ~10cm to the A0 input and let the end float in the air. On the oscilloscope, lower the trigger threshold to ~1V, select the upgoing trigger and a timescale of 10ms/div. You may see something similar as in the picture, but the precise image will depend strongly on the presence of conducting bodies nearby and on how they are grounded.

Note how the signal is asymmetric. Since the coupling is capacitive, it is purely alternating current (AC), therefore it is as often negative as positive. Here we see a limitation of this Arduino oscilloscope: it can only measure signals positive with respect to ground. Negative signals are simply clipped off at zero. Note also that the distance between the peaks is precisely 20ms, indicating that the net frequency is indeed 50Hz and that the time scale of the Arduino oscilloscope is precise.

Step 5: Example 2 : Capacitive Coupling

The capacitive coupling from the net also applies to signals that we generate ourselves. Two wires that run close to each other will influence each other even if there is no direct electrical contact, just as if there is a small-value capacitance between the two. The effect is strongest at high frequencies, but can also be demonstrated at the default pulser frequency of 1kHz.

Attach a ~10 cm wire to the pulse output of pin D9 and another ~10cm wire to the input of A0. Get the wires really close together, for example by twisting them together. Set the oscilloscope to a timescale of 2ms/div. You should get an image similar to the one on the picture. In addition to the 50Hz net pickup, there is now clearly visible a 1kHz square wave from pin D9.

Step 6: Example 3 : Low-pass RC Filter

Resistor-capacitor (RC) filters are about the simplest signal filters that one can implement. Depending on the configuration, they can pass either the low frequencies (low-pass) or the high frequencies (high-pass). More complex configurations can filter away both high and low frequencies (band-pass filter). The main parameter of an RC filter is the characteristics time, which can be calculated as the product of the resistor value in Ohm with the capacitor value in Farad. In this example a resistor of 100kOhm and a capacitor of 10nF are used, corresponding to a characteristic time of tau=RC=10^5Ohm x 10^-8F =10^-3s, or 1 ms. using a 10kOhm resistor with a 0.1muF capacitor will give the same results.

For setups like this, which require additional components, it is really handy to use a prototype shield for the Arduino: you attach it on the top and on the mini-breadboard you can place the components, connect them between each other and with the Arduino pins. But also connecting it to a regular breadboard will be fine.

Connect the resistor and capacitor in series between the pulser on D9 and the ground. Connect input A0 directly to the pulser and A1 to the connection between the resistor and the capacitor, as in the picture and in the schematics shown.

First see the response of this filter for signals with a frequency well below the characteristic time tau=RC=1ms. Therefore, select the pulser to fire with period 20ms and display both traces 0 and 1, triggering on the rising edge of A0. Set the timescale to 5ms/div. The red curve shows the output of the filter and shows how it follows the input with some delay: after every step, it takes a couple of ms to reach the level of the input.

A low-pass RC filter can also be used to transform a modulated signal into a constant voltage. The other screenshot of the oscilloscope traces shows the response to a pulser frequency of 2kHz and a 20% duty cycle. The output of the filter is centred around 1.0V (20% of 5V) with a saw-tooth ripple of circa 0.2V.

Build a Tool Contest 2017

Participated in the
Build a Tool Contest 2017

Microcontroller Contest 2017

Participated in the
Microcontroller Contest 2017

1 Person Made This Project!

Recommendations

  • Laser Challenge

    Laser Challenge
  • Edible Art Challenge

    Edible Art Challenge
  • Paint Challenge

    Paint Challenge

30 Comments

0
tterbo
tterbo

3 months ago

@rgco : Congratulations of a nice piece of work, and thank you for sharing. Very good o/scope for somebody like me who is just getting into electronics and unwilling to spend a ton of money on expensive equipment. I understand it's got its limitations, but it will do very nicely for now!
I delved into the Processing code & Arduino code (not too hard for me as I was a software developer before retiring) and I've learnt a lot just from doing that. So, thanks again.

PS @Pierre-LouisL : thanks to you too for your serial port selector suggestion - I'l be looking to add that later.

PPS: A few days later ... I had this working fine on a genuine Arduino UNO both when connected to Processing running on a Linux Mint laptop (when it connected as /dev/ttyACM0) and on Windows10 (connected as COM3). Out of interest, I then tried it on a cheap Nano clone, which worked fine when connected to the Linux machine (this time connected as /dev/ttyUSB0); but Windows10 refused to recognise the connection as a valid COM port - presumably because the USB chip on the Nano clone is the inferior CH340.
A variant of Pierre-LouisL's routine was very useful in figuring out what was going on with the connections.
[EDIT - now resolved on W10 - missed the fact that I had to install driver of CH340]

0
dmp59
dmp59

Question 6 months ago

I downloaded the .ino and .pde files. The pde file gave me an error that says 'import' does not name a type; did you mean 'qsport'?
The line in the code is at the start that says import processing.serial.*;
What do I do to make it work?

0
rgco
rgco

Answer 6 months ago

Sorry I haven't used it for a while, nor have I ever used 'processing' since. Probably 'processing' updated and became incompatible. You may check in the comments below if someone ran into a similar issue.

0
Pierre-LouisL
Pierre-LouisL

Reply 3 months ago

I'm on Mac and I'm using Processing 3 (as Processing 4 don't run on my "old OS version"). The pde code run correctly on Processing 3. Maybe dmp59 use version 4?

0
Pierre-LouisL
Pierre-LouisL

Reply 3 months ago

This came from the fact the Processing code set the baud rate of the serial port at 115200 but don't check the serial port: the serial port index is hardcoded. So if your Arduino is not on the hardcoded serial port, it doesnt' work. The index of the serial port which is used, is at the end of the setup() function.

0
Pierre-LouisL
Pierre-LouisL

Tip 3 months ago

Very good. Just compile the ino and run the pde. Just a point: the pde
code set the baud rate of the serial port at 115200 but don't check the
serial port (serial port number is hardcoded). So I've made a change in
the pde code. Hope it can be usefull. It shows the list of the available
serial and let you choose the right one. Feel free to add to your project:
import javax.swing.JOptionPane; // Add this at beginning

After the drawpb(); of setup():
// Get list of serial port
final String[] lst_ports = Serial.list();
int num_port = lst_ports.length; // number of serial port

// The list has (on Mac OSX), both "cu" and "tty" port. We just want cu ones.
// In order to declare the Object[] you need to know how many cu port we have
int num_port_cu = 0; // init number
for (int x = 0; x < num_port; x++) // loop on all port
{
String nom_port = lst_ports[x];
if (!nom_port.startsWith("/dev/tty"))
{
num_port_cu++; // If it's a cu one, count it
}
}

// Now delcare the object array with the right size
Object[] selection = new Object[num_port_cu];

//Loop again againts all port
for (int x = 0; x < num_port; x++)
{
String nom_port = lst_ports[x];
// If it's oa cu port, add it to the array and add it index (+1) before the name
if (!nom_port.startsWith("/dev/tty"))
{
nom_port = (x+1)+"-"+nom_port;
selection[x] = nom_port;
}
}

// Display the form wih the menu filled with the serial port names
String retour = (String) JOptionPane.showInputDialog(
null, "Choose the serial port of your Arduino",
"Dialogue",
JOptionPane.QUESTION_MESSAGE,
null, selection, selection[0]);

// If the user cancel or close the dialogue, we quit
if ((retour == null)||(retour == ""))
{
exit();
}
else
{
// Split the name of the serial port to get the index
String[] parts = retour.split("-");
int idx_serie = (Integer.parseInt(parts[0]))-1; // -1 because in the list we've done +1
port = new Serial(this, Serial.list()[idx_serie], 115200);
}

0
rbright
rbright

2 years ago

I also had nothing in Processing until from another post here, I changed the Processing line to the following, including quotes,
port = new Serial(this, “COM6”, 115200); obviously you need to specify the com port your arduino is connected to..... Cheers all

0
Jawad Julkernine
Jawad Julkernine

2 years ago

What is the maximum voltage that can be observed by this oscilloscope?

0
rgco
rgco

Reply 2 years ago

5V. but with a resistor-based voltage divider there is no limit.

0
souravg009
souravg009

3 years ago

Hi, I tried to make the project. but in Processing 3.4, it gives a black screen for oscilloscope with a (not responding) also in processing (see screenshot) I get a 'Problem". how to solve? My Arduino Uno is connected as COM4.

Ok. I solved it by changing,
//println(Serial.list()[0]);
//println(Serial.list()[1]);
and,
port = new Serial(this, Serial.list()[0], 115200);

If I may suggest, STM32F103C8T6 MCU AKA Bluepill Can also be used in place of Arduino Uno. It'd be much faster.

Screen Shot 11-05-18 at 12.12 AM.PNG
0
BuddyMartn
BuddyMartn

3 years ago

Hi, I came over from your frequency generator page when I saw this. Very nice work Sir! I see some of your issues but this is wonderful stuff. Thank you for sharing!

Buddy

0
ostogu
ostogu

3 years ago

I had a ArrayIndexOutOfBoundsExpectations: 0 problem in the line of 121. How can I fix it?

0
ostogu
ostogu

Reply 3 years ago

I solved it. No problem.

0
LarryGB01
LarryGB01

3 years ago

Nice use of a single chip, any tips on trouble shooting the Arduino IDE shield, before applying the Process code? I have no errors reported, but cannot seem to get any results on the display. No RX/TX indications, yet shield seems to load properly. The A0 to D9 connection doesn't seem to activate the appropriate output to display.

Thanks

0
rgco
rgco

Reply 3 years ago

Sorry I'm of not much help here. I found the up-and-down communication part between the computer and the Arduino the hardest part to grasp. It kind-of worked without implementing a robust communication protocol..

0
LarryGB01
LarryGB01

Reply 3 years ago

From another post here, changed the Processing line to the following, including quotes,

port = new Serial(this, “COM17”, 115200);

Now one with the show.....

Thanks all,

0
KenNet
KenNet

4 years ago

Good job!

I do get it to work on a borrowed UNO compatible, but not with my own MEGA2560.

What could be the difference ?

KenNet

0
rgco
rgco

Reply 4 years ago

The Arduino sketch is full of direct read/writes to registers (most are needed for speed), it's not expected to work on any other chip than the ATMEGA328. That said, from a quick look at the 2560 data sheet I don't see immediate incompatibilities. What happens exactly, it doesn't compile, doesn't run, gives garbage, gives a flat line? ( I don't have a MEGA2560 so can't try it out...)

0
KenNet
KenNet

Reply 4 years ago

I will have to check the exact details at work tomorrow, but I do remember that the Processing sketch ran flatlined, the Rx diode on the MEGA2560 board was blinking regularly, and when I checked with an oscilloscope, D9 did not produce the expected pulse train.

I guess that means that the two sketches ( INO and PDE ) had established communication, but Your signal generator had not generated its proper output due to some internal non-328 colloquialism.