Arduin-O-Scope (a tiny oscilloscope project)

Arduin-O-Scope v01

I put together a little oscilloscope based on an arduino duemilanove as a first project to learn about how the system works. There's plenty of polishing to be done, but it mostly works and runs at about 5kHz with the default setting.

It uses a counter interrupt as a time base and just uses the top 8 bits of the ADC to give 8-bit resolution. The Arduino reads analog input 0 every time the interrupt fires and sends the value over USB to a program running in Processing. The Processing code scans the incoming data to see when it passes through a trigger level. Once the trigger level is detected it starts saving the incoming data and then plots it to the screen. If too much time goes by it just plots what data it has so you get a 'live' view of what is going on at the input pin. Clicking anywhere on the screen sets the trigger level to the mouse location.

The Arduino code contains one special line, "#define TEST_MODE". If you leave this line as-is the Arduino ignores the analog input and sends a mathematically generated ramp wave which is nice for debugging the Processing code. Comment this line out and you will get the analog input pin values instead.

The Processing code has a few debugging items too. A white circle in the upper left-hand corner blinks when the frame updates. If this isn't blinking something is hanging in the code. There is another green circle that appears if the frame was started by a trigger event, and a red circle appears if the frame was started without a trigger event. A horizontal purple line shows where the trigger level is.

Welp, it's good enough for now and I'm sick of reading datasheets. Thanks to these forums and the Arduino team for all the great help!

e.

Known Bugs

  1. The frames in Processing sometimes do not update smoothly. I think the root of this problem is that the loops that read the serial port are too short and run the CPU to 100% constantly polling for new input. Eventually the CPU wanders off to handle its other tasks and the screen hangs for a minute. There is likely a better way to do this polling.

  2. Values of 5V and 0V input can run the scope trace off the screen. This is kind of annoying, but just needs a better scaling method.

To Do

  1. It would be nice to have a pre-trigger, just like on a real oscilloscope, that would let you see a bit of the data that came in before the trigger event.

  2. I think the sampling rate can still be faster. Maybe running the ADC on self-triggering mode and fixing the choking issue with the Processing code would help. If anyone can point me to a good guide on setting the ADC to free-running mode I'd greatly appreciate it.

  3. It would be cool to add the ability for Processing to change the sampling rate from the UI by sending messages to the Arduino.

  4. Add a scale to the UI for both voltage and time steps.

Arduino Code

#include <avr/io.h>
#include <avr/interrupt.h>

#define BAUD_RATE 115200 
#define INPUT_PIN 0
#define LED_PIN 13

//#define TEST_MODE                            // comment out to read analog pin, uncomment for test ramp wave
                           
volatile int j;

void setup()
{
  Serial.begin(BAUD_RATE); 
  pinMode(LED_PIN, OUTPUT);
  cli();                                     // disable interrupts while messing with their settings
  TCCR1A = 0x00;                             // clear default timer settings, this kills the millis() funciton
  TCCR1B = 0x00;
  TCCR1B |= (1 << WGM12);                    // Configure timer 1 for CTC mode
  TCCR1B |= (0 << CS12);                     // Set timer prescaling by setting 3 bits 
  TCCR1B |= (1 << CS11);                     // 001=1, 010=8, 011=64, 101=1024 
  TCCR1B |= (1 << CS10); 
  TIMSK1 |= (1 << OCIE1A);                   // Enable CTC interrupt
  OCR1A  = 50;                               // Set CTC compare value
  sei();                                     // turn interrupts back on
}

void loop() {
  // nothing to do, its all in the interrupt handler!
}  


ISR(TIMER1_COMPA_vect)                            // when timer counts down it fires this interrupt
{   
  #ifdef TEST_MODE
    Serial.print((j%64)*4 , BYTE);                // test mode, generate a ramp wave
    j++;
  #else
    Serial.print( analogRead(INPUT_PIN), BYTE);   // real mode, sample analog pin
  #endif  
}

Processing Code

import processing.serial.*;

Serial inPort;                           // the port to read from
int BAUD_RATE = 115200;                  // set baud rate here, needs to be the same value as in the arduino code
int BUFFER_SIZE=200;                     // data buffer size
int GRIDS=10;                            // number of grids to draw
int inVal;                               // y-data read in from the arduino
int lastVal;                             // old value of y-data
int[] yVals = new int[BUFFER_SIZE];      // y-data buffer, scaled to the screen size
int[] xVals = new int[BUFFER_SIZE];      // x-data buffer, scaled to the screen size
int trigger;                             // trigger, when the incoming data passes this value a frame starts
int timeOut;                             // if no trigger is detected by timeOut samples, plot what is at input port
int i;                                   // counter
boolean blinker;                         // blinks a light on each frame update so you know when program is running     
boolean noTrigger;                       // true until trigger is detected
boolean noTimeOut;                       // true until timeout runs out if no trigger is found first


void setup() 
{                                      
  inPort = new Serial(this, Serial.list()[0], BAUD_RATE);    
  size(600, 400);                                            
  background(0);
  stroke(255);
  trigger=100;
  timeOut=2*BUFFER_SIZE;
}

void draw()
{
  // dump any old data sent while program wasn't running or was busy
  inPort.clear();
  
  // wait for trigger event or timeout 
  noTrigger=true;
  noTimeOut=true;
  lastVal=1023;
  i=0;
  while(noTrigger & noTimeOut){
     if (inPort.available()>1){                                    // wait for a byte to appear on serial port
         inVal=(inPort.read());                                    // read the byte
         if((inVal>trigger)&(lastVal<=trigger)) noTrigger=false;   // check for trigger event
         lastVal=inVal;
         i++;
         if (i>timeOut) noTimeOut=false;                           // check for timeout event
      }
      else{
        delay(1);
      }
  }
  
  // collect a frame of data
  i=0;
  while(i<BUFFER_SIZE){                                            // read a buffer full of date                  
    if (inPort.available()>1){
          inVal=( inPort.read());
          yVals[i]=height-((height)*inVal)/254;                    // scale data to screen height
          xVals[i]=(width*i)/BUFFER_SIZE;                          // scale x-value to screen width
          i++;
      }
      else{
        delay(1);
      }
  }
  
  // draw grid lines
  background(0);
  stroke(0,64,0);
  for(i=1;i<GRIDS;i++){
    line((width*i)/GRIDS,0,(width*i)/GRIDS,height);
    line(0,(height*i)/GRIDS,width,(height*i)/GRIDS);
  }
  
  // draw trigger level
  stroke(128,0,128);
  line(0,height-(height*trigger)/254,width,height-(height*trigger)/254);
  
  // draw scope trace
  stroke(255,255,0);
  for (i=1;i<BUFFER_SIZE;i++){
    line(xVals[i-1],yVals[i-1],xVals[i],yVals[i]);
  }
  
  // draw a dot that changes state each screen update
  // if this isnt blinking, something is wrong
  stroke(255);
  if (blinker) ellipse(10,10,5,5);
  blinker=!blinker;
  
  // draw a green dot if trigger event fired frame
  if (!noTrigger){
    stroke(0,255,0);
    ellipse(20,10,5,5);
  }
  // draw a red dot if timeout event fired frame
  if (!noTimeOut){
    stroke(255,0,0);
    ellipse(30,10,5,5);
  }
  
  // this delay seems to be needed to let the system handle random events
  delay(50);
}

// move the trigger level to wherever the user clicks the mouse
void mousePressed() {
  trigger=(height-mouseY)*255/height;
}

If you want a faster sampling rate, how about repeatedly calling analogRead in loop instead of using an interrupt?

nice work, really love this idea. wish i had the skills to help you with it.

i tried your setup with a standard IR receiver module but can't see any results. any ideas?

It runs on 38khz. Maybe that's too fast?

If you want a faster sampling rate, how about repeatedly calling analogRead in loop instead of using an interrupt?

I used the timer/interrupt setup so that eventually the user can set the sampling rate to some arbitrary number (1kHz, 5kHz, 10kHz, etc) and have it be somewhat accurate. Hopefully this will mimic the behavior of a 'real' oscilloscope better.

Plus it was an excuse to learn how to do interrupts. :wink:

nice work, really love this idea. wish i had the skills to help you with it.

i tried your setup with a standard IR receiver module but can't see any results. any ideas?

It runs on 38khz. Maybe that's too fast?

For the sake of simplicity I pinned the sampling rate at 5kHz until the serial interface with Processing works a bit better, so I imagine that might be one problem. Also, there is a scaling issue where values of 5V and 0V don't display correctly - if your module is flipping between those two points rapidly you might just see some vertical lines.

Hopefully I can fix some of these things when I get a bit of time to play with it again, check back in a week or so and it may be working better.

ah i see. maybe i will try to understand processing a bit better and then mix your idea with the infrared anayser code:

http://www.arduino.cc/playground/Code/InfraredReceivers

would be a nice live analyser.

I used the timer/interrupt setup so that eventually the user can set the sampling rate to some arbitrary number (1kHz, 5kHz, 10kHz, etc) and have it be somewhat accurate. Hopefully this will mimic the behavior of a 'real' oscilloscope better.

That works for the slower sampling rates but because the overhead in the interrupt handler may get in the way when the sampling rate is fast, why not do a simple test and see how fast you can go with just analogReads and the serial print. If you can get usefully higher sampling rates, then you could use the interrupts for the slower modes and the non-interrupt version for higher speed. You would probably need to pad each analogRead/Serial.print with some delay to round out the sampling rate to a decade boundary.

But it may be that that delays in serial.print are a bigger factor than the interrupts - but it could be intersting to find out :wink:

i tried to pair your processing script with the infrared analyzer. one issue i have is that the arduino sends out pairs of data with a time index and a value.
any suggestions how to send a pair of values to processing?

The baud rate used is 115200. From that we get maximum data rate of 11520 bytes/sec of 11.52KB.

Now analogRead() returns an integer, ranging from 0 to 1023.
Serial.Print() prints it in ASCII format, which means a maximum of 4 bytes per sample.

So maximum sampling frequency would be 11.52KB/4 = 2.888KHz.

You are limited by serial port transmission rate. Check if Processing allows you to set non-standard baud rate for the serial port.

The code in the first post sends a single byte per sample, so the sampling rate is around 11.5k samples per second.

My bad, I missed the second parameter BYTE.
I just assumed he is sending the full precision 10bits of the Analog value.

Looking again into code, as it is written it would result in truncated values being sent out. Check following code from serial.print() library where long is type casted as char, which implies only bottom 8 bits are sent out.

void Print::print(long n, int base)
{
if (base == 0)
print((char) n);
else if (base == 10)
print(n);
else
printNumber(n, base);
}

Right shifting the analog sample by 2 bits and sending as BYTE would solve the problem I guess.

If I am not mistaken doesn't one ADC read take 100us? So the real limitation then becomes the reading. At 100us, the sampling rate is 10khz. Plus the overhead for sending (I actually did an experiment and was able to use a 500,000 baud rate, yes 500k, baud rates depend on the Crystal Frequency (8khz in my case), certain frequency conflict with certain baud rates) .

I was doing a forever loop with a delay at the end (to sync my reads) I was able to sample every 342us (2.924khz). Anything lower would fail. (1 ADC reading, sending 2 Bytes, 2 Calls to Microseconds, 1 Delay Statement).

As far as sending on Serial, the limitation I believe is the overhead for initializing the connection internally (interrupts etc maybe?)

***HAS ANYONE FIGURED OUT A WAY TO OBTAIN A HIGHER RATE?
I was thinking of getting a 16mhz model, but I can only imagine this helping out slightly. (the 100us read is still present).

There is information on increasing the ADC conversion rate in this thread: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1208715493/11

I've tried several different versions of oscilloscopes from around here including the google code one, simple and tiny one but I'm getting the same trace "noise" on all channels.
I am using a Pro Mini with FTDI cable and seem to get noise. It seems to be related to the USB but is not transmission or receiving or Raw voltage or Vcc. I can only assume it to be a combination or interference from the pro mini circuit as the only thing i have added is the lead for probing.
Any ideas on this frustration?

I've tried several different versions of oscilloscopes from around here including the google code one, simple and tiny one but I'm getting the same trace "noise" on all channels.
I am using a Pro Mini with FTDI cable and seem to get noise. It seems to be related to the USB but is not transmission or receiving or Raw voltage or Vcc. I can only assume it to be a combination or interference from the pro mini circuit as the only thing i have added is the lead for probing.

maybe you could show what you mean by "noise".. I would have thought that anything in the USB circuit would be too high frequency to show up in the signal. what does your proble look line? have you tried grounding or shielding it?

You better give up on high sample rates.

See table 28-7 of ATmega328P (duemilanove) for example:

Input bandwidth: 38.5KHz.

Absolute Inaccuracy: 4.5LSB @ 1/16 presample (ADC 1MHz)

There's absolutely no noise induced on USB cable (digital). BUT you might have leaking current to ADC inputs. To overcome that, try the following tips:

  • Have a good ground connected on ADC cabling.
  • Disable every other ADC input. Also don't use ADC gain (if Mega).
  • Despite datasheet saying input impedance is 100M, it is not. I am not sure what's it impedance is. But the idea is to match it on your input (if you have a 1M ohm impedance, make sure your input is also 1M ohm). Also use a stable ADC ref value, this is very very important. If possible, use the internal 1.1V source. Why do I say input impedance is not that high? Simple - connect VCC to ADC input and watch your controller heat a lot. But might be an issue with the muxer only.

I never used a tiny, but should not differ that much. I've a clone actually, but did not use it (yet).

Wondering : if you tie your ADC input to ground, using a 100 ohm resistor, what do you get ? Noise ? And if directly connected to ground ? And if using a 100Kohm ?

Álvaro