Introduction: Arduino - Multi-Channel Oscilloscope (Poor Man's Oscilloscope)

About: I got into wood working a few years back and have also been dabbling with electronics since about forever. The combination of both I find very fascinating and so I am always trying to come up with projects tha…

I am presenting software upgrades for the Arduino Oscilloscope (Poor Man's Oscilloscope) that allow to visualize two or three data channels simultaneously. The main features are identical to the original oscilloscope with exception of a few lines that I added to improve the users ability to estimate what the actual measured voltages are. RuiSantos had found the original software in the net and shared this information in his instructable and on his website. Thank you very much RuiSantos for sharing this.
Note that this "device" is not an actual oscilloscope. Most of the features you'd expect from an oscilloscope are not available. However, if you like to visualize data that is not too fast, then this is simply awesome. For example I will be using it to check a data sequence that I am going to create with a couple of 555 timers. But that is another story.

Features of the software/oscilloscope:
1-channel (original and fastest), 2-channel (slower), or 3-channel (even slower) data acquisition and display
0-5V signal range determined by hardware, i.e. Arduino
Time scale zoom in/out by pressing +/-
Red line = channel1 data
Green line = channel2 data
Blue line = channel3 data
Grey lines = 0.5V steps
Dark yellow lines = 1V steps
Screenshot functionality: On mouse button click the program stores a screen shot of your data window. This essentially allows you to "stop" the data and look at it. Convenient for data analysis. To make this work you have to go into the program "draw ()" section and adjust the file path to one that works with your computer.

All you need is an Arduino and a USB cable.

Step 1: The Code

Other than the software, there is nothing new in setting this up. For your convenience I am repeating the steps listed in RuiSantos' instructable.

1)
Download Processing. Processing is a programming environment that looks and feels identical to the Arduino programming environment. It also comes at the same price, which means it's free. Click here to download. Set it up like you set up your Arduino software. It runs like the Arduino IDE.
Processing can be used to display images, lines, or objects and interact with them. This is my first experience with the program.

2)
Depending on how many channels you like to have, upload one of the "Arduino sketches" shown below into your Arduino. The code for sending and receiving the data was based on the code in this great book chapter which explains how to send multiple text fields from an Arduino to the computer and processing it with Processing.

3)
Then you take the respective "Processing sketch" from below and copy it into Processing

4)
Change the "Path" in the last line of the code into a path of your choosing.

5)
Run it in Processing.

6)
Make sure your signals do not exceed 5V before you connect them to:
Channel1 => analog pin0
Channel2 => analog pin1
Channel3 => analog pin2

Notes:
  • The Processing sketches use serial port 0 as it's standard. You may have to adjust this value if you use a different serial port.
  • I documented each step in the program with plenty of comments. The code should thus be "self-explaining".
  • I experimentally found out that for some configurations the data may not be shown in real time due to several interconnected reasons:
    • The delay that is set in the Arduino program is not large enough. => Too many data is sent. => The processing time accumulates. => Data is shown with delay. To test for delays I manually switched the power supply that was connected to my Arduino's analog inputs off and on while observing the voltage response displayed by Processing. I did this for an extended period of time. If the response remained immediate I remained satisfied.
    • If you zoom in with "+" you display less data. This has a direct effect on the potential for delays, because less processing time is needed.
    • The window is too wide. => Too many x-values need to be shifted. => Too much time needed for the calculations. => I chose a much smaller window size than my screen size to address this effect.
Enjoy the multiple channels on your Poor-Man's-Oscilloscope and let me know if you happen to come up with any improvements.


Arduino 2-channel code:
/* CommaDelimitedOutput sketch */
#define ANALOG_IN_Ch1 0
#define ANALOG_IN_Ch2 1

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  int val1 = analogRead(ANALOG_IN_Ch1); /* Reads values from analog Pin0 = Channel1 */
  int val2 = analogRead (ANALOG_IN_Ch2); /* Reads values from analog Pin1 = Channel2 */

  Serial.print('H'); /* Unique header to identify start of message */
  Serial.print(",");
  Serial.print(val1,DEC);
  Serial.print(",");
  Serial.print(val2,DEC);
  Serial.print(",");  /* Note that a comma is sent after the last field */
  Serial.println();  /* send a cr/lf */
  delay(50); //This may be able to be faster than 50ms
}


Arduino 3-channel code:
/* CommaDelimitedOutput sketch */
#define ANALOG_IN_Ch1 0
#define ANALOG_IN_Ch2 1
#define ANALOG_IN_Ch3 2

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  int val1 = analogRead(ANALOG_IN_Ch1); /* Reads values from analog Pin0 = Channel1 */
  int val2 = analogRead (ANALOG_IN_Ch2); /* Reads values from analog Pin1 = Channel2 */
  int val3 = analogRead (ANALOG_IN_Ch3); /* Reads values from analog Pin2 = Channel3 */

  Serial.print('H'); /* Unique header to identify start of message */
  Serial.print(",");
  Serial.print(val1,DEC);
  Serial.print(",");
  Serial.print(val2,DEC);
  Serial.print(",");
  Serial.print(val3,DEC);
  Serial.print(",");  /* Note that a comma is sent after the last field */
  Serial.println();  /* send a cr/lf */
  delay(50);
}


Processing 2-Channel Code:
/*
* Oscilloscope
* Gives a visual rendering of three analog pins in realtime.
*
* This Software expands the channel amount of a previous version.
* The previous version was a project that is part of Accrochages
* See http://accrochages.drone.ws
*
* The author of this adapted software has no relation to Accrochages.
* He thanks them for the great template and the inspiration to write this software.
*
* The following declaration was part of the original software.
* This is for your information.
*
* (c) 2008 Sofian Audry (info@sofianaudry.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import processing.serial.*;

Serial myPort; /* Create object from Serial class */
char HEADER = 'H'; /* character to identify the start of a message */
short LF = 10; /* ASCII linefeed */
short portIndex = 0; /* determines the USB port used */
int[] val = {-1, -1}; /* Variable used for getY function, 2 positions for 2 channels */
int[] valuesCh1; /* The next three variables will hold the data of the window in memory so that they can be pushed and displayed */
int[] valuesCh2;
float zoom; /* Define "zoom" as floating-point variable */

void setup()
  {
    //size(1014, 690); /* Opens a window of specific size, max size of my laptop screen, delay problems may occur */
    size(600, 400); /* smaller screen => less to calculate => no delay problems */
    myPort = new Serial(this, Serial.list()[portIndex], 9600); /* Open the port that the board is connected to and use the same speed (9600 bps) */
    valuesCh1 = new int[width]; /* Define array with as many elements as x-pixel in window, used for plotting data of Channel1 */
    valuesCh2 = new int[width]; /* ... of Channel2 */
    zoom = 1.0f; /* Start with 1x zoom factor */
    smooth(); /* Drawing images with smooth edges */
  }

/* This function converts a channel data value into pixels shown from
* top of graph (which is the 0 position)
* this function returns an integer value */
int getY(int val)
    {
      return (int)(height - val / 1023.0f * (height - 1));
    }

/* Function that reads the serial string data that were sent by arduino
* Puts out an array with the three values of the arduino
* Function is called in draw function */
int[] getData()
    {
      int[]ChValues = {-1, -1}; /* Define array for this function */
      String message = myPort.readStringUntil(LF); /* Read in the serial data string sent by arduino */
      if(message != null) /* Do this only when a complete valid message was received */
        {
          String [] data  = message.split(","); /* Split the comma-separated message into it's segments */
          if(data[0].charAt(0) == HEADER)       /* Check for header character in the first field, always true for complete message */
            {
              for (int i = 1; i < data.length-1; i++) /* Skip the header and terminate cr and lf => look only at the three data points */
                 {
                   ChValues[i-1] = Integer.parseInt(data[i]); /* Write channel data into array, i shifted from data to array by 1 due to header */
                 }
            }
        }
         return ChValues; /* Returns array ChValues which contains channel data */
    }

/* This function pushes all the data points of the window one position further to the left, then it adds the data point that was just read */
void pushValue(int[] value)
  {
    for (int i=0; i<width-1; i++)
      {
        valuesCh1[i] = valuesCh1[i+1]; /* Move the data point over by on position */
        valuesCh2[i] = valuesCh2[i+1];  
      }

    valuesCh1[width-1] = value[0]; /* Add data point */
    valuesCh2[width-1] = value[1];
  }

/* This function draws the data into the window */
void drawLines()
  {
    int displayWidth = (int) (width / zoom); /* Calculates width of window, considering the x-change if a zoom is set */
    int k = valuesCh1.length - displayWidth; /* This calculates position up to which the data points are shown */
    int x0 = 0; /* x value at very left of window (=0) is assigned to x0 and used for all channels */
    int ya0 = getY(valuesCh1[k]); /* y value of the last shown point is assigned to ya0 for channel1 */
    int yb0 = getY(valuesCh2[k]); /* y value of the last shown point is assigned to yb0 for channel2 */
    for (int i=1; i<displayWidth-1; i++) /* Loop that runs from point k to the very right side of window */
      {
        k++; /* Increment k for next data point */
        int x1 = (int) (i * (width-1) / (displayWidth-1)); /* Calculate next x value */
        int ya1 = getY(valuesCh1[k]); /* Get next y-value for channel1 */
        int yb1 = getY(valuesCh2[k]); /* Get next y-value for channel2 */
        strokeWeight(2);  /* Draw thicker lines */
        stroke(255, 0, 0); /* Draw a red line for channel1 */
        line(x0, ya0, x1, ya1); /* Plot a line segment for channel1 */
        stroke(0, 255, 0); /* Draw a green line for channel2 */
        line(x0, yb0, x1, yb1); /* Plot a line segment for channel2 */
        x0 = x1; /* Shift x value to calculate next line segments */
        ya0 = ya1; /* Shift y-value for channel1 to calculate next line segments */
        yb0 = yb1; /* Shift y-value for channel2 to calculate next line segments */
      }
  }

/* This function draws grid lines into the window
* I spaced the lines so they represent 10% and 20% steps in 2 different colors
* For Signals of 5V max, that is 0.5 & 1.0V steps */
void drawGrid()
  {
    stroke(150, 150, 0);
    line(0, height/5, width, height/5);
    line(0, height*2/5, width, height*2/5);
    line(0, height*3/5, width, height*3/5);
    line(0, height*4/5, width, height*4/5);
    stroke(150, 150, 150);
    line(0, height/10, width, height/10);
    line(0, height*3/10, width, height*3/10);
    line(0, height*5/10, width, height*5/10);
    line(0, height*7/10, width, height*7/10);
    line(0, height*9/10, width, height*9/10);
}

/* This function allows to zoom in the x-axis of the data
* It runs in the background and notices when the right key is pressed
* Zoom in with pressing "+"
* Zoom out with pressing "-" */
void keyReleased()
  {
    switch (key)
      {
        case '+':
        zoom *= 2.0f;
        println(zoom);
        if ( (int) (width / zoom) <= 1 )
        zoom /= 2.0f;
        break;
        case '-':
        zoom /= 2.0f;
        if (zoom < 1.0f)
        zoom *= 2.0f;
        break;
      }
  }

/* This is the main function that calls the other functions
* This function runs continuously */
void draw()
  {
    background(1); /* Sets the background of the window */
    drawGrid(); /* Draws the grid into the window */
    val = getData(); /* Reads the data from the three Channels as sent by the arduino into an array */
    if (val[0] != -1) /* If data is in first channel, then carry out function */
      {
        pushValue(val); /* Pushes data down one position and adds one new data point */
      }
    drawLines(); /* Add the next data set to the window */
    if (mousePressed) /*perform action when mouse button is pressed */
      {
       save("/YourPathHere/OsciData1.png");  /* save screen shot of data window, but beware image will be overwritten with second mouse click */
      }
  }


Processing 3-Channel Code:

/*
* Oscilloscope
* Gives a visual rendering of three analog pins in realtime.
*
* This Software expands the channel amount of a previous version.
* The previous version was a project that is part of Accrochages
* See http://accrochages.drone.ws
*
* The author of this adapted software has no relation to Accrochages.
* He thanks them for the great template and the inspiration to write this software.
*
* The following declaration was part of the original software.
* This is for your information.
*
* (c) 2008 Sofian Audry (info@sofianaudry.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import processing.serial.*;

Serial myPort; /* Create object from Serial class */
char HEADER = 'H'; /* character to identify the start of a message */
short LF = 10; /* ASCII linefeed */
short portIndex = 0; /* determines the USB port used */
int[] val = {-1, -1, -1}; /* Variable used for getY function, 3 positions for 3channels */
int[] valuesCh1; /* The next three variables will hold the data of the window in memory so that they can be pushed and displayed */
int[] valuesCh2;
int[] valuesCh3;
float zoom; /* Define "zoom" as floating-point variable */

void setup()
  {
    //size(1014, 690); /* Opens a window of specific size, max size of my laptop screen, delay problems half through window */
    size(600, 400); /* smaller screen => less to calculate => no delay problems */
    myPort = new Serial(this, Serial.list()[portIndex], 9600); /* Open the port that the board is connected to and use the same speed (9600 bps) */
    valuesCh1 = new int[width]; /* Define array with as many elements as x-pixel in window, used for plotting data of Channel1 */
    valuesCh2 = new int[width]; /* ... of Channel2 */
    valuesCh3 = new int[width]; /* ... of Channel3 */
    zoom = 1.0f; /* Start with 1x zoom factor */
    smooth(); /* Drawing images with smooth edges */
  }

/* This function converts a channel data value into pixels shown from
* top of graph (which is the 0 position)
* this function returns an integer value */
int getY(int val)
    {
      return (int)(height - val / 1023.0f * (height - 1));
    }

/* Function that reads the serial string data that were sent by arduino
* Puts out an array with the three values of the arduino
* Function is called in draw function */
int[] getData()
    {
      int[]ChValues = {-1, -1, -1}; /* Define array for this function */
      String message = myPort.readStringUntil(LF); /* Read in the serial data string sent by arduino */
      if(message != null) /* Do this only when a complete valid message was received */
        {
          String [] data  = message.split(","); /* Split the comma-separated message into it's segments */
          if(data[0].charAt(0) == HEADER)       /* Check for header character in the first field, always true for complete message */
            {
              for (int i = 1; i < data.length-1; i++) /* Skip the header and terminate cr and lf => look only at the three data points */
                 {
                   ChValues[i-1] = Integer.parseInt(data[i]); /* Write channel data into array, i shifted from data to array by 1 due to header */
                 }
            }
        }
         return ChValues; /* Returns array ChValues which contains channel data */
    }

/* This function pushes all the data points of the window one position further to the left, then it adds the data point that was just read */
void pushValue(int[] value)
  {
    for (int i=0; i<width-1; i++)
      {
        valuesCh1[i] = valuesCh1[i+1]; /* Move the data point over by on position */
        valuesCh2[i] = valuesCh2[i+1];
        valuesCh3[i] = valuesCh3[i+1];    
      }

    valuesCh1[width-1] = value[0]; /* Add data point */
    valuesCh2[width-1] = value[1];
    valuesCh3[width-1] = value[2];   
  }

/* This function draws the data into the window */
void drawLines()
  {
    int displayWidth = (int) (width / zoom); /* Calculates width of window, considering the x-change if a zoom is set */
    int k = valuesCh1.length - displayWidth; /* This calculates position up to which the data points are shown */
    int x0 = 0; /* x value at very left of window (=0) is assigned to x0 and used for all channels */
    int ya0 = getY(valuesCh1[k]); /* y value of the last shown point is assigned to ya0 for channel1 */
    int yb0 = getY(valuesCh2[k]); /* y value of the last shown point is assigned to yb0 for channel2 */
    int yc0 = getY(valuesCh3[k]); /* y value of the last shown point is assigned to yc0 for channel3 */
    for (int i=1; i<displayWidth-1; i++) /* Loop that runs from point k to the very right side of window */
      {
        k++; /* Increment k for next data point */
        int x1 = (int) (i * (width-1) / (displayWidth-1)); /* Calculate next x value */
        int ya1 = getY(valuesCh1[k]); /* Get next y-value for channel1 */
        int yb1 = getY(valuesCh2[k]); /* Get next y-value for channel2 */
        int yc1 = getY(valuesCh3[k]); /* Get next y-value for channel3 */
        strokeWeight(2);  /* Draw thicker lines */
        stroke(255, 0, 0); /* Draw a red line for channel1 */
        line(x0, ya0, x1, ya1); /* Plot a line segment for channel1 */
        stroke(0, 255, 0); /* Draw a green line for channel2 */
        line(x0, yb0, x1, yb1); /* Plot a line segment for channel2 */
        stroke(0, 0, 255); /* Draw a blue line for channel3 */
        line(x0, yc0, x1, yc1); /* Plot a line segment for channel3 */
        x0 = x1; /* Shift x value to calculate next line segments */
        ya0 = ya1; /* Shift y-value for channel1 to calculate next line segments */
        yb0 = yb1; /* Shift y-value for channel2 to calculate next line segments */
        yc0 = yc1; /* Shift y-value for channel3 to calculate next line segments */     
      }
  }

/* This function draws grid lines into the window
* I spaced the lines so they represent 10% and 20% steps in 2 different colors
* For Signals of 5V max, that is 0.5 & 1.0V steps */
void drawGrid()
  {
    stroke(150, 150, 0);
    line(0, height/5, width, height/5);
    line(0, height*2/5, width, height*2/5);
    line(0, height*3/5, width, height*3/5);
    line(0, height*4/5, width, height*4/5);
    stroke(150, 150, 150);
    line(0, height/10, width, height/10);
    line(0, height*3/10, width, height*3/10);
    line(0, height*5/10, width, height*5/10);
    line(0, height*7/10, width, height*7/10);
    line(0, height*9/10, width, height*9/10);
}

/* This function allows to zoom in the x-axis of the data
* It runs in the background and notices when the right key is pressed
* Zoom in with pressing "+"
* Zoom out with pressing "-" */
void keyReleased()
  {
    switch (key)
      {
        case '+':
        zoom *= 2.0f;
        println(zoom);
        if ( (int) (width / zoom) <= 1 )
        zoom /= 2.0f;
        break;
        case '-':
        zoom /= 2.0f;
        if (zoom < 1.0f)
        zoom *= 2.0f;
        break;
      }
  }

/* This is the main function that calls the other functions
* This function runs continuously */
void draw()
  {
    background(1); /* Sets the background of the window */
    drawGrid(); /* Draws the grid into the window */
    val = getData(); /* Reads the data from the three Channels as sent by the arduino into an array */
    if (val[0] != -1) /* If data is in first channel, then carry out function */
      {
        pushValue(val); /* Pushes data down one position and adds one new data point */
      }
    drawLines(); /* Add the next data set to the window */
    if (mousePressed) /*perform action when mouse button is pressed */
      {
         save("/YourPathHere/OsciData1.png");  /* save screen shot of data window, but beware image will be overwritten with second mouse click */
      }
  }