Introduction: Girino - Fast Arduino Oscilloscope

I am a Physicist and the nicest part of working in this field is that I get to build my own instruments. With this way of thinking, I decided to build a homebrew Arduino Oscilloscope. This instructable was written with the purpose of teaching a bit about microcontrollers and data acquisition. This is an extreme project because I wanted to squeeze out from Arduino as much velocity as I could, I have not seen any other Arduino Oscilloscope as fast as this one.

Some time ago I was working on an Arduino project and I needed to see if the output signal was into compliance with the specifics. Thus I spent some time on the internet looking for Arduino Oscilloscopes already implemented, but I did not like what I found. The projects that I found were mostly composed of a Graphical User Interface for the computer written in Processing and a very simple arduino sketch. The sketches were something like:
void setup() {

void loop() {
    int val = analogRead(ANALOG_IN);
This approach is not wrong and I do not want to insult anyone, but this is too slow for me. The serial port is slow and sending every result of an analogRead() through it is a bottleneck.

I have been studying Waveform Digitizers for some time and I know reasonably well how do they work, so I got inspiration from them. These were the starting points of the oscilloscope that I wanted to create:
  • the incoming signal should be decoupled from the arduino to preserve it;
  • with an offset of the signal it is possible to see negative signals;
  • the data should be buffered;
  • a hardware trigger is required to catch the signals;
  • a circular buffer can give the signal shape prior to the trigger (more to follow on this point);
  • using lower lever functions that the standard ones makes the program run faster.

The sketch for the Arduino is attached to this step, along with the schematic of the circuit that I made.

The name that I came up with, Girino, is a frivolous pun in Italian. Giro means rotation and adding the suffix -ino you get a small rotation, but Girino also means tadpole. This way I got a name and a mascot.

Step 1: Disclaimer


Electronics can be dangerous if you do not know what you are doing and the author cannot guarantee the validity of the information found here. This is not a professional advice and anything written in this instructable can be inaccurate, misleading, dangerous or wrong. Do not rely upon any information found here without independent verification.

It is up to you to verify any information and to double check that you are not exposing yourself, or anyone, to any harm or exposing anything to any damage; I take no responsibility. You have to follow by yourself the proper safety precautions, if you want to reproduce this project.

Use this guide at your own risk!

Step 2: What You Need

What we really need for this project is an Arduino board and the datasheet of the ATMega328P.
The datasheet is what tells us how the microcontroller works and it is very important to keep it if we want a lower lever of control.

The datasheet can be found here:

The hardware that I added to the Arduino is partly necessary, its purpose is just to form the signal for the ADC and to provide a voltage level for the trigger. If you want, you could send the signal directly to the Arduino and use some voltage reference defined by a voltage divider, or even the 3.3 V given by the Arduino itself.

Step 3: Debug Output

I usually put a lot of debug output in my programs because I want to keep track of anything that happens; the problem with Arduino is that we do not have a stdout to write to. I decided to use the Serial port as a stdout.

Be aware, though, that this approach does not work all the times! Because writing to the Serial port requires some time for the execution and it can dramatically change things during some time sensible routines.

I usually define debugging outputs inside a preprocessor macro, so when the debug is disabled they simply disappear from the program and do not slow down the execution:
  • dprint(x); - Writes to the serial port something like: # x: 123
  • dshow("Some string"); - Writes the string

This is the definition:

#if DEBUG == 1
#define dprint(expression) Serial.print("# "); Serial.print( #expression ); Serial.print( ": " ); Serial.println( expression )
#define dshow(expression) Serial.println( expression )
#define dprint(expression)
#define dshow(expression)

Step 4: Setting Register Bits

With the purpose of being fast, it is necessary to manipulate the microcontroller features with lower lever functions than the standard ones provided by the Arduino IDE. The internal functions are managed through some registers, that are collections of eight bits where each one governs something particular. Each register contains eight bits because the ATMega328P has an 8-bit architecture.

The registers have some names that are specified in the datasheet depending on their meanings, like ADCSRA for the ADC Setting Register A. Also each meaningful bit of the registers has a name, like ADEN for the ADC Enable Bit in the ADCSRA register.

To set their bits we could use the usual C syntax for binary algebra, but I found on the internet a couple of macros that are very nice and clean:

// Defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

Using them is very simple, if we want to set to 1 the Enable Bit of the ADC we can just write:


While if we want to set it to 0 (id est clear it) we can just write:


Step 5: What Are the Interrupts

As we will see in the next steps, the use of interrupts is required in this project. Interrupts are signals that tell the microcontroller to stop the execution of the main loop and pass it to some special functions. The images give an idea of the program flow.

The functions that are executed are called Interrupt Service Routines (ISR) and are more or less simple functions, but that do not take arguments.

Let us see an example, something like counting some pulses. The ATMega328P has an Analog Comparator that has an interrupt associated that is activated when a signal surpasses a reference voltage. First of all you must define the function that will be exectuted:


This is really simple, the instruction ISR() is a macro that tells the compiler that the following function is an Interrupt Service Routine. While ANALOG_COMP_vect is called Interrupt Vector and it tells the compiler which interrupt is associated to that routine. In this case it is the Analog Comparator Interrupt. So everytime that the comparator sees a signal bigger than a reference it tells the microcontroller to execute that code, id est in this case to increment that variable.

The next step is to enable the interrupt associated. To enable it we must set the ACIE (Analog Comparator Interrupt Enable) bit of the ACSR (Analog Comparator Setting Register) register:


In the following site we can see the list of all Interrupt Vectors:

Step 6: Continuously Acquire With a Circular Buffer

The concept of using a Circular Buffer is pretty straight forward:

Acquire continuously till a signal is found, then send the digitized signal to the computer.

This approach allows to have the incoming signal shape also before the trigger event.

I prepared some diagrams to make myself clear. The following points are referring to the images.
  • On the first image we can see what I mean with continuous acquisition. We define a buffer that will store the data, in my case an array with 1280 slots, then we start to continuously read the ADC output register (ADCH) ad filling the buffer with the data. When we get to the end of the buffer we restart from the beggining without clearing it. If we immagine the array arranged in a circular way it is easy to see what I mean.
  • When the signal surpasses the threshold, the Analog Comparator Interrupt is activated. Then we start a waiting phase in which we continue to acquire the signal but keep a count of the ADC cycles that passed from the Analog Comparator Interrupt.
  • When we waited for N cycles (with N < 1280), we freeze the situation and stop the ADC cycles. So we end up with a buffer filled with the digitization of the signal temporal shape. The great part of this, is that we have also the shape prior to the trigger event, because we were already acquiring before that.
  • Now we can send the whole buffer to the serial port in a block of binary data, instead of sending the single ADC reads. This reduced the overhead required to send the data and the bottleneck of the sketches that I found on the internet.

Step 7: Oscilloscope Triggering

An oscilloscope shows on its display a signal, on that we all agree, but how can it show it steadily and do not show it jumping around the screen? It has an internal trigger that is able to show the signal always on the same position of the screen (or at least most of the times), creating the illusion of a stable plot.

The trigger is associated with a threshold that activates a sweep when the signal passes it. A sweep is the phase in which the oscilloscope records and displays the signal. After a sweep another phase occurs: the holdoff, in which the oscilloscope rejects any incoming signal. The holdoff period can be composed of a part of dead time, in which the oscilloscope is unable to accept any signal, and a part that can be user selectable. The dead time can be caused by various reasons like having to draw on the screen or having to store the data somewhere.

Looking at the image we get a sense of what happens.
  1. Signal 1 surpasses the threshold and activates the sweep;
  2. signal 2 is inside the sweep time and gets caught with the first;
  3. after the holdoff, signal 3 activates the sweep again;
  4. instead signal 4 is rejected because it falls inside the holdoff region.
The raison d'être of the holdoff phase is to prevent some undesired signals to get in the sweep region. It is a little bit long to explain this point and it eludes the purpose of this instructable.

The moral of this story is that we need:
  1. a threshold level to wich we can compare the incoming signal;
  2. a signal that tells the microcontroller to start the waiting phase (see preceding step).
We have several possible solutions for point 1. :
  • using a trimmer we can manually set a voltage level;
  • using the PWM of the Arduino we can set the level by software;
  • using the 3.3 V provided by the Arduino itself;
  • using the internal bangap reference we can use a fixed level.
For point 2. we have the right solution: we can use the interrupt of the internal Analog Comparator of the microcontroller.

Step 8: How the ADC Works

The Arduino microcontroller features a single 10-bit successive approximation ADC. Before the ADC there is an analog multiplexer that lets us send, to the ADC, the signals from different pins and sources (but only one at a time).

Successive Approximation ADC means that the ADC takes 13 clock cycles to complete the conversion (and 25 clock cycles for the first conversion). There is a clock signal dedicated to the ADC that is "computed" from the main clock of the Arduino; this is because the ADC is a little slow and can not keep up with the pace of the other parts of the microcontroller. It requires an input clock frequency between 50 kHz and 200 kHz to get maximum resolution. If a lower resolution than 10 bits is needed, the input clock frequency to the ADC can be higher than 200 kHz to get a higher sample rate.

But how much higher rates can we use? There are a couple of good guides about the ADC at the Open Music Labs that I suggest to read: Since my purpose is to get a fast oscilloscope I decided to limit the precision to 8-bits. This has several bonuses:
  1. the data buffer can store more data;
  2. you do not waste 6-bits of RAM per datum;
  3. the ADC can acquire faster.
The prescaler lets us divide the frequency, by some factors, by setting the ADPS0-1-2 bits of the ADCSRA register. Seeing the plot of the precision from the Open Music Labs Article, we can see that for 8-bits precision the frequency could go up to 1.5 MHz, good! But since the ability of changing the prescaler factor lets us change the acquisition rate, we can use it also to change the timescale of the oscilloscope.

There is a good feature about the output registers: we can decide the adjusting of conversion bits, by setting the ADLAR bit in the ADMUX register. If it is 0 they are right adjusted and viceversa (see the image). Since I wanted 8-bits precision I set it to 1 so I could read just the ADCH register and ignore the ADCL.

I decided to have just one input channel to avoid having to switch channel back and forth at every conversion.

One last thing about the ADC, it has different running modes each one with a different trigger source:
  • Free Running mode
  • Analog Comparator
  • External Interrupt Request 0
  • Timer/Counter0 Compare Match A
  • Timer/Counter0 Overflow
  • Timer/Counter1 Compare Match B
  • Timer/Counter1 Overflow
  • Timer/Counter1 Capture Event
I was interested in the free running mode that is a mode in which the ADC continuously converts the input and throws an Interrupt at the end of each conversion (associated vector: ADC_vect).

Step 9: Digital Input Buffers

The analog input pins of the Arduino can also be used as digital I/O pins, therefore they have an input buffer for digital functions. If we want to use them as analog pins you should disable this feature.

Sending an analog signal to a digital pin induces it to toggle between HIGH and LOW states, especially if the signal is near the boundary between the two states; this toggling induces some noise to the near circuits like the ADC itself (and induces a higher energy consumption).

To disable the digital buffer we should set the ADCnD bits of the DIDR0 register:


Step 10: Setting Up the ADC

In the sketch, I wrote a initialization function that sets-up all the parameters of the ADC functioning. As I tend to write clean and commented code, I will just past the function here. We can refer to the preceding step and to the comments for the meaning of the registers.

void initADC(void)
        // ADMUX settings
        // These bits select the voltage reference for the ADC. If these bits
        // are changed during a conversion, the change will not go in effect
        // until this conversion is complete (ADIF in ADCSRA is set). The
        // internal voltage reference options may not be used if an external
        // reference voltage is being applied to the AREF pin.
        // REFS1 REFS0 Voltage reference
        // 0 0 AREF, Internal Vref turned off
        // 0 1 AVCC with external capacitor at AREF pin
        // 1 0 Reserved
        // 1 1 Internal 1.1V Voltage Reference with external
        //   capacitor at AREF pin
        // The ADLAR bit affects the presentation of the ADC conversion result
        // in the ADC Data Register. Write one to ADLAR to left adjust the
        // result. Otherwise, the result is right adjusted. Changing the ADLAR
        // bit will affect the ADC Data Register immediately, regardless of any
        // ongoing conversions.
        // The value of these bits selects which analog inputs are connected to
        // the ADC. If these bits are changed during a conversion, the change
        // will not go in effect until this conversion is complete (ADIF in
        // ADCSRA is set).
        ADMUX |= ( ADCPIN & 0x07 );

        // ADCSRA settings
        // Writing this bit to one enables the ADC. By writing it to zero, the
        // ADC is turned off. Turning the ADC off while a conversion is in
        // progress, will terminate this conversion.
        // In Single Conversion mode, write this bit to one to start each
        // conversion. In Free Running mode, write this bit to one to start the
        // first conversion. The first conversion after ADSC has been written
        // after the ADC has been enabled, or if ADSC is written at the same
        // time as the ADC is enabled, will take 25 ADC clock cycles instead of
        // the normal 13. This first conversion performs initialization of the
        // ADC. ADSC will read as one as long as a conversion is in progress.
        // When the conversion is complete, it returns to zero. Writing zero to
        // this bit has no effect.
        // When this bit is written to one, Auto Triggering of the ADC is
        // enabled. The ADC will start a conversion on a positive edge of the
        // selected trigger signal. The trigger source is selected by setting
        // the ADC Trigger Select bits, ADTS in ADCSRB.
        // When this bit is written to one and the I-bit in SREG is set, the
        // ADC Conversion Complete Interrupt is activated.
        // These bits determine the division factor between the system clock
        // frequency and the input clock to the ADC.
        // ADPS2 ADPS1 ADPS0 Division Factor
        // 0 0 0 2
        // 0 0 1 2
        // 0 1 0 4
        // 0 1 1 8
        // 1 0 0 16
        // 1 0 1 32
        // 1 1 0 64
        // 1 1 1 128

        // ADCSRB settings
        // When this bit is written logic one and the ADC is switched off
        // (ADEN in ADCSRA is zero), the ADC multiplexer selects the negative
        // input to the Analog Comparator. When this bit is written logic zero,
        // AIN1 is applied to the negative input of the Analog Comparator.
        // If ADATE in ADCSRA is written to one, the value of these bits
        // selects which source will trigger an ADC conversion. If ADATE is
        // cleared, the ADTS2:0 settings will have no effect. A conversion will
        // be triggered by the rising edge of the selected Interrupt Flag. Note
        // that switching from a trigger source that is cleared to a trigger
        // source that is set, will generate a positive edge on the trigger
        // signal. If ADEN in ADCSRA is set, this will start a conversion.
        // Switching to Free Running mode (ADTS[2:0]=0) will not cause a
        // trigger event, even if the ADC Interrupt Flag is set.
        // ADTS2 ADTS1 ADTS0 Trigger source
        // 0 0 0 Free Running mode
        // 0 0 1 Analog Comparator
        // 0 1 0 External Interrupt Request 0
        // 0 1 1 Timer/Counter0 Compare Match A
        // 1 0 0 Timer/Counter0 Overflow
        // 1 0 1 Timer/Counter1 Compare Match B
        // 1 1 0 Timer/Counter1 Overflow
        // 1 1 1 Timer/Counter1 Capture Event

        // DIDR0 settings
        // When this bit is written logic one, the digital input buffer on the
        // corresponding ADC pin is disabled. The corresponding PIN Register
        // bit will always read as zero when this bit is set. When an analog
        // signal is applied to the ADC5..0 pin and the digital input from this
        // pin is not needed, this bit should be written logic one to reduce
        // power consumption in the digital input buffer.
        // Note that ADC pins ADC7 and ADC6 do not have digital input buffers,
        // and therefore do not require Digital Input Disable bits.

Step 11: How the Analog Comparator Works

The Analog Comparator is an internal module of the microcontroller and it compares the input values on the positive pin (Digital Pin 6) and negative pin (Digital Pin 7). When the voltage on the positive pin is higher than the voltage on the negative pin AIN1, the Analog Comparator outputs a 1 in the ACO bit of the ACSR register.

Optionally, the comparator can trigger an interrupt, exclusive to the Analog Comparator. The associated vector is ANALOG_COMP_vect.

We can also set the the interrupt to be launched on a rising edge, falling edge or on a toggle of the state.

The Analog Comparator is just what we need for the triggering connecting out input signal to pin 6, now what is left is a threshold level on pin 7.

Step 12: Setting Up the Analog Comparator

In the sketch, I wrote another initialization function that sets-up all the parameters of the Analog Comparator functioning. The same issue about ADC digital buffers applies to the Analog Comparator, as we can see on the bottom of the routine.

void initAnalogComparator(void)
        // ACSR settings
        // When this bit is written logic one, the power to the Analog
        // Comparator is switched off. This bit can be set at any time to turn
        // off the Analog Comparator. This will reduce power consumption in
        // Active and Idle mode. When changing the ACD bit, the Analog
        // Comparator Interrupt must be disabled by clearing the ACIE bit in
        // ACSR. Otherwise an interrupt can occur when the bit is changed.
        // When this bit is set, a fixed bandgap reference voltage replaces the
        // positive input to the Analog Comparator. When this bit is cleared,
        // AIN0 is applied to the positive input of the Analog Comparator. When
        // the bandgap referance is used as input to the Analog Comparator, it
        // will take a certain time for the voltage to stabilize. If not
        // stabilized, the first conversion may give a wrong value.
        // When the ACIE bit is written logic one and the I-bit in the Status
        // Register is set, the Analog Comparator interrupt is activated.
        // When written logic zero, the interrupt is disabled.
        // When written logic one, this bit enables the input capture function
        // in Timer/Counter1 to be triggered by the Analog Comparator. The
        // comparator output is in this case directly connected to the input
        // capture front-end logic, making the comparator utilize the noise
        // canceler and edge select features of the Timer/Counter1 Input
        // Capture interrupt. When written logic zero, no connection between
        // the Analog Comparator and the input capture function exists. To
        // make the comparator trigger the Timer/Counter1 Input Capture
        // interrupt, the ICIE1 bit in the Timer Interrupt Mask Register
        // (TIMSK1) must be set.
        // These bits determine which comparator events that trigger the Analog
        // Comparator interrupt.
        // ACIS1 ACIS0 Mode
        // 0 0 Toggle
        // 0 1 Reserved
        // 1 0 Falling edge
        // 1 1 Rising edge
        // DIDR1 settings
        // When this bit is written logic one, the digital input buffer on the
        // AIN1/0 pin is disabled. The corresponding PIN Register bit will
        // always read as zero when this bit is set. When an analog signal is
        // applied to the AIN1/0 pin and the digital input from this pin is not
        // needed, this bit should be written logic one to reduce power
        // consumption in the digital input buffer.

Step 13: Threshold

Recalling what we said about the trigger, we can implement these two solutions for the threshold:
  • using a trimmer we can manually set a voltage level;
  • using the PWM of the Arduino we can set the level by software.
On the image we can see the hardware implementation of the threshold in both paths.

For the manual selection, a multi-turn potentiometer put between +5 V and GND is sufficient.

While for software selection we need a low-pass filter that filters a PWM signal coming from the Arduino. PWM signals (more on this to follow) are square signals with a constant frequency but a variable pulse-width. This variability brings a variable mean value of the signal that can be extracted with a low-pass filter. A good cutoff frequency for the filter is about one hundredth of the PWM frequency and I chose about 560 Hz.

After the two threshold sources I inserted a couple of pins that allows to select, with a jumper, which source I wanted. After the selection I also added an emitter follower to decouple the sources from the Arduino pin.

Step 14: How the Pulse Width Modulation Works

As stated before, a Pulse Width Modulation (PWM) signal is a square signal with fixed frequency but variable width. On the image we see an example. On each row there is one of such signals with a different duty cycle (id est the period portion in which the signal is High). Taking the mean signal over a period, we get the red line that correspond to the duty cycle with respect to the signal maximum.

Electronically "taking the mean of a signal" can be translated to "passing it to a low-pass filter", as seen on the preceding step.

How does the Arduino generate a PWM signal? There is a really good tutorial about PWM here:
We will see just the points that are needed for this project.

In the ATMega328P there are three timers that can be used to generate PWM signals, each one of those has different characteristics that you can use. For each timer correspond two registers called Output Compare Registers A/B (OCRnx) that are used to set the signal duty cycle.

As for the ADC there is a prescaler (see image), that slows down the main clock to have a precise control of the PWM frequency. The slowed down clock is fed to a counter that increments a Timer/Counter Register (TCNTn). This register is continuously compared to the OCRnx, when they are equal a signal is sent to a Waveform Generator that generate a pulse on the output pin. So the trick is setting the OCRnx register to some value to change the mean value of the signal.

If we want a 5 V signal (maximum) we must set a 100% duty cycle or a 255 in the OCRnx (maximum for a 8-bit number), while if we want a 0.5 V signal we must set a 10% duty cycle or a 25 in the OCRnx.

Since the clock has to fill the TCNTn register before starting from the beginning for a new pulse the output frequency of the PWM is:

f = (Main clock) / prescaler / (TCNTn maximum)

exempli gratia for the Timer 0 and 2 (8-bit) with no prescaler it will be: 16 MHz / 256 = 62.5 KHz while for Timer 1 (16-bit) it will be 16 MHz / 65536 = 244 Hz.

I decided to use the Timer number 2 because
  • Timer 0 is used internally by the Arduino IDE for functions such as millis();
  • Timer 1 has an output frequency too slow because it is a 16-bit timer.

In the ATMega328P there are different kinds of operation mode of the timers, but what I wanted was the Fast PWM one with no prescaling to get the maximum possible output frequency.

Step 15: Setting Up the PWM

In the sketch, I wrote another initialization function that sets-up all the parameters of the Timer functioning and initializes a couple of pins.

void initPins(void)
        // TCCR2A settings
        // These bits control the Output Compare pin (OC2A) behavior. If one or
        // both of the COM2A1:0 bits are set, the OC2A output overrides the
        // normal port functionality of the I/O pin it is connected to.
        // However, note that the Data Direction Register (DDR) bit
        // corresponding to the OC2A pin must be set in order to enable the
        // output driver.
        // When OC2A is connected to the pin, the function of the COM2A1:0 bits
        // depends on the WGM22:0 bit setting.
        // Fast PWM Mode
        // COM2A1 COM2A0
        // 0 0 Normal port operation, OC2A disconnected.
        // 0 1 WGM22 = 0: Normal Port Operation, OC0A Disconnected.
        //   WGM22 = 1: Toggle OC2A on Compare Match.
        // 1 0 Clear OC2A on Compare Match, set OC2A at BOTTOM
        // 1 1 Clear OC2A on Compare Match, clear OC2A at BOTTOM

        // Combined with the WGM22 bit found in the TCCR2B Register, these bits
        // control the counting sequence of the counter, the source for maximum
        // (TOP) counter value, and what type of waveform generation to be used
        // Modes of operation supported by the Timer/Counter unit are:
        // - Normal mode (counter),
        // - Clear Timer on Compare Match (CTC) mode,
        // - two types of Pulse Width Modulation (PWM) modes.
        // Mode WGM22 WGM21 WGM20 Operation TOP
        // 0 0 0 0 Normal  0xFF
        // 1 0 0 1 PWM  0xFF
        // 2 0 1 0 CTC  OCRA
        // 3 0 1 1 Fast PWM 0xFF
        // 4 1 0 0 Reserved -
        // 5 1 0 1 PWM  OCRA
        // 6 1 1 0 Reserved -
        // 7 1 1 1 Fast PWM OCRA

        // TCCR2B settings
        // The FOC2A bit is only active when the WGM bits specify a non-PWM
        // mode.
        // However, for ensuring compatibility with future devices, this bit
        // must be set to zero when TCCR2B is written when operating in PWM
        // mode. When writing a logical one to the FOC2A bit, an immediate
        // Compare Match is forced on the Waveform Generation unit. The OC2A
        // output is changed according to its COM2A1:0 bits setting. Note that
        // the FOC2A bit is implemented as a strobe. Therefore it is the value
        // present in the COM2A1:0 bits that determines the effect of the
        // forced compare.
        // A FOC2A strobe will not generate any interrupt, nor will it clear
        // the timer in CTC mode using OCR2A as TOP.
        // The FOC2A bit is always read as zero.

        // The three Clock Select bits select the clock source to be used by
        // the Timer/Counter.
        // CS22 CS21 CS20 Prescaler
        // 0 0 0 No clock source (Timer/Counter stopped).
        // 0 0 1 No prescaling
        // 0 1 0 8
        // 0 1 1 32
        // 1 0 0 64
        // 1 0 1 128
        // 1 1 0 256
        // 1 1 1 1024

        pinMode( errorPin, OUTPUT );
        pinMode( thresholdPin, OUTPUT );

        analogWrite( thresholdPin, 127 );

Step 16: Volatile Variables

I can not remember where, but I read that variables that are modified inside an ISR should be declared as volatile.

Volatile variables are variables that can change during time, even if the program that is running does not modify them. Just like Arduino registers that can change value for some external interventions.

Why does the compiler want to know about such variables? That is because the compiler always tries to optimize the code that we write, to make it faster, and it modifies it a little bit, trying not to change its meaning. If a variable changes by its own it could seem to the compiler that it is never modified during execution of, say, a loop and it could ignore it; while it could be crucial that the variable changes its value. So declaring volatile variables it prevents the compiler to modify the code concerning those.

For some more information I suggest to read the Wikipedia page:

Step 17: Writing the Kernel of the Sketch

Finally we have gotten to the kernel of the program!

As we saw before, I wanted a continuous acquisition and I wrote the ADC Interrupt Service Routine to store in the circular buffer the data continuously. It stops whenever it reaches the index that is equal to stopIndex. The buffer is implemented as circular employing the modulo operator.

// ADC Conversion Complete Interrupt
        // When ADCL is read, the ADC Data Register is not updated until ADCH
        // is read. Consequently, if the result is left adjusted and no more
        // than 8-bit precision is required, it is sufficient to read ADCH.
        // Otherwise, ADCL must be read first, then ADCH.
        ADCBuffer[ADCCounter] = ADCH;

        ADCCounter = ( ADCCounter + 1 ) % ADCBUFFERSIZE;

        if ( wait )
                if ( stopIndex == ADCCounter )
                        // Freeze situation
                        // Disable ADC and stop Free Running Conversion Mode
                        cbi( ADCSRA, ADEN );

                        freeze = true;

The Analog Comparator Interrupt Service Routine (that is called when a signal passes the threshold) disables itself and tells the ADC ISR to start the waiting phase and sets the stopIndex.

// Analog Comparator interrupt
        // Disable Analog Comparator interrupt
        cbi( ACSR,ACIE );

        // Turn on errorPin
        //digitalWrite( errorPin, HIGH );
        sbi( PORTB, PORTB5 );

        wait = true;
        stopIndex = ( ADCCounter + waitDuration ) % ADCBUFFERSIZE;

This was really easy after all that grounding!

Step 18: Forming Incoming Signal

Let us get to the hardware now. The circuit may look complicated but it is really simple.
  • There is a 1 MΩ resistor at the input, to give a mass reference to the signal and have a high impedance input. A high impedance "simulates" an open circuit if you connect it to a lower impedance one, so the presence of the Girino does not mess too much with the circuit you want to measure.
  • After the resistor there is an emitter follower to decouple the signal and protect the following electronics.
  • There is a simple offset that generates a 2.5 V level with a voltage divider. It is attached to a capacitor to stabilize it.
  • There is a non-inverting sum-amplifier that sums the incoming signal and the offset. I used this technique because I wanted to be able to see also negative signals, as the Arduino ADC could see signals only between 0 V to 5 V.
  • After the sum-amp there is another emitter follower.
  • A jumper lets us decide if we want to feed the signal with a offset or not.
The Operational Amplifier that I intended to use was a LM324 that is able to work between 0 V to 5 V but also between, say, -12 V to 12 V. This gives us more possibilities with the power supplies. I also tried a TL084 that is way faster than the LM324 but requires a dual power supply. They both have the same pinout so can be changed without any modification of the circuit.

Step 19: Bypass Capacitors

Bypass Capacitors are capacitors that are used to filter the power supplies of Integrated Circuits (IC) and they should be put as close as possible to the alimentation pins of the IC. They are used usually in couple, one ceramic and one electrolytic because they can filter out different frequencies.

Step 20: Power Sources

I used a dual power supply for the TL084 that can be converted to a single power supply for the LM324.

On the image we can see that I used a couple of voltage regulators a 7812, for +12 V, and a 7912, for -12 V. The capacitors are, as usual, used to stabilize the levels and their values are the ones suggested in the datasheets.

Obviously to have a ±12 V we have to have at least about 30 V on the input because the voltage regulators require a higher input to provide a stabilized output. Since I did not have such power supply I used the trick of using two 15 V power supplies in series. One of the two is connected to the Arduino power connector (so it feeds both the Arduino and my circuit) and the other directly to the circuit.

It is not an error to connect the +15 V of the second power supply to the GND of the first! This is how we get a -15 V with isolated power supplies.

If I do not want to carry around an Arduino and two power supplies I can still use the +5 V provided by the Arduino changing those jumpers (and using the LM324).

Step 21: Preparing a Shield Connector

I have always been annoyed by the connectors that I could find to make an Arduino shield, because they always have pins that are too short and the boards that I use can be soldered only on one side. So I made up a little trick to make the pins longer so they can be soldered and inserted in the Arduino.

Inserting the pin strip into the board, as on the picture, we can push the pins, to have them only on one side of the black plastic. Then we can solder them on the same side where they will be inserted in the Arduino.

Step 22: Soldering and Testing

I am unable to show you all the soldering procedure of the circuit because it underwent a lot of trial and error work. In the end it got a little messy but not too bad, though I will not show the underside because that is really messy.

At this stage there is not much to say because I already explained in detail all the parts of the circuit. I tested it with an oscilloscope, that a friend borrowed me, to see the signals at each point of the circuit. It seems that everything is working alright and I am pretty satisfied.

The connector for the incoming signal could seem a little strange for someone that does not come from the High Energy Physics, it is a LEMO connector. It is the standard connector for nuclear signals, at least in Europe as in the USA I have seen mostly BNC connectors.

Step 23: Test Signals

To test the circuit and the Data AcQuisition (DAQ) I used a second Arduino with a simple sketch that generates square pulses with different lenghts. I also wrote a python script that talks to Girino and tells it to acquire some data series and saves one of those to a file.
They are both attached to this step.

Step 24: Time Calibration

Using the test signals I calibrated the horizontal scale of the plots. By measuring the widths of the pulses (that are known because were generated) and plotting the measured pulses widths against the known values, we get a hopefully linear plot. Doing this for each prescaler setting we have the time calibration for all acquisition rates.

On the images we can see all the data that I took an analyzed. The "Fitted slopes" plot is the most interesting because it tells us the actual acquisition rate of my system at each prescaler setting. The slopes were measured as a [ch/ms] number but this is equivalent to a [kHz], so the slopes values are actually kHz or also kS/s (kilo Samples per second). That means that with the prescaler set to 8 we get an acquisition rate of:

(154±2) kS/s

Not bad, uh?

While from the "Fitted y-intercepts" plot we get an insight of the system linearity. All the y-intercepts should be zero because at a signal with zero length should correspond a pulse with a zero length. As we can see on the graph they all are compatible with zero, but not the 18-prescaler dataset. This dataset, though, is the worst one because is has just two data and its calibration can not be trusted.

Following there is a table with the acquisition rates for each prescaler setting.

Prescaler Acquisition rate [kS/s]
128 9.74 ± 0.04
64 19.39 ± 0.06
32 37.3 ± 0.6
16 75.5 ± 0.3
8 153 ± 2
The cited errors are coming from the Gnuplot fit engine and I am unsure about them.

I also tried an unweighted fit of the rates because you can see that they roughly double when the prescaling halves, this looks like an inverse proportionality law. So I fitted the rates vs the prescaler settings with a simple law of


I got a value for a of


with a χ²=3.14 and 4 degrees of freedom, this means that the law is accepted with a 95% confidence level!

Step 25: Done! (Almost)

At the end of this long experience, I feel very satisfied because
  • I learned a lot about microcontrollers in general;
  • I learned a lot more about the Arduino ATMega328P;
  • I had some hands-on experience of Data Acquisition, not by using something already done but by making something;
  • I realized an amateur oscilloscope that is not that bad.
I hope that this guide will be useful to anybody that reads it. I wanted to write it so detailed because I learned all that in the hard way (surfing the internet, reading the datasheet and by a lot of trial and error) and I would like to spare anybody from that experience.

Step 26: To Be Continued...

The project if far from completed, though. What it is missing is:
  1. A test with different analog signals (I am missing an analog signal generator);
  2. A Graphical User Interface for the computer side.
While for point 1. I am not sure when it will be completed, because I am not planning to buy/build one in the near future.

For point 2. the situation could be better. Is anyone willing to help me with that? I found a nice Python Oscilloscope here:
I would like to modify it to fit it for Girino, but I am accepting suggestions.


stanleyella made it! (author)2017-07-14

I want to use your method. I have uno scope running using GCBasic.

TheNoviceResearcher made it! (author)2014-06-20

Hello! My professor and I are doing research that requires us to make an inexpensive Multi-channel analyzer. A component of the MCA is the Arduino Uno, which we want to turn into an ADC. The Uno, however, is too slow so we turned to the instructable here for the Sketch that's supposed to increase the frequency at which the Uno reads analog signals. However, when we try to run the code (with minor changes) the serial monitor outputs data that makes no sense. We used Girino.7z. Here are the changes we made;

* When we download the Girino code and turn on debugging (by setting DEBUG=1 in Girino.h), we get the expected output:


# setup()

Girino ready

# ADCCounter: 0

# stopIndex: 65535

# wait: 0

# freeze: 0



# ADCCounter: 0

# stopIndex: 65535

# wait: 0

# freeze: 0



// repeats


* However if I add a single print statement in the setup() function, then that print statement is repeatedly executed (strange) and the output is garbled.


void setup ()



Serial.println("Girino ready");

Serial.println(" X1 We're outside the loop!! XD"); //This is the line we added.




Here is our output;


Girino ready

X# setup()

Girino# setup()

Girino : etup(# setup()

Girino read# setup()

Girino# setup()


Girino #######....//pounds forever.


So as you can see the setup is printed repeatedly which should only happen once.

Any help is appreciated.

here are my system & software specs:

MacBook Pro, 12-inch, Early 2011

Processor 2.3 GHz Intel Core i5

Memory 4 GB 1333 MHz DDR3

Graphics Intel HD Graphics 3000 384 MB

We are using Arduino Uno SMD edition, software version 1.0.5

Software OS X 10.9.3 (13D65)


SergioM227 made it! (author)SergioM2272017-05-01

I'm working on a similar project, did you manage to solve this problem?

kerahari made it! (author)2016-03-20

It is an awesome project.
Iam working on similar kind of projects,actually i am not that much good in Analog Electronics. I have some doubts,please help me
1) what is triggering?
2)why do we need to to give threshold voltage?arduino itself does the conversion right? Just like AnalogRead.

LindsayF10 made it! (author)LindsayF102017-03-06

An oscilloscope lets you view a voltage in a small snapshot of time, but we need something to mark the start of that snapshot. Usually we wait for the voltage to hit a certain "trigger" point and that marks the beginning of the recording. Oscilloscopes usually take a snapshots continually. If we don't trigger at the same voltage each time, periodic (repeating) signals will not be displayed with any stability - every frame will show the signal in a different position for a split second and it becomes impossible to study the signal.

womai made it! (author)2012-03-28

Two ideas to make the code more efficient (run faster):

The calculation

ADCCounter = ( ADCCounter + 1 ) % ADCBUFFERSIZE;

involves an integer division which tends to be time consuming (at least on a Microchip PIC - I do not know Atmel as well). Instead, try

if (++ADCCounter >= ADCBUFFERSIZE) ADCCounter = 0;

Second, you can completely avoid the time required to evaluate


and live without the wait variable if you simply set stopIndex to a value that the counter never reaches, as long as you aren't yet in the post-trigger phase. I.e. during initialization (when starting a new sweep) set

stopIndex = ADCBUFFERSIZE + 1;

and when the trigger event happens then just do as you did so far, but without the boolean wait variable:

// Disable Analog Comparator interrupt
cbi( ACSR,ACIE );

// Turn on errorPin
//digitalWrite( errorPin, HIGH );
sbi( PORTB, PORTB5 );

stopIndex = ( ADCCounter + waitDuration ) % ADCBUFFERSIZE;

and the ADC ISR routine becomes

if (++ADCCounter >= ADCBUFFERSIZE) ADCCounter = 0;

if ( stopIndex == ADCCounter )
cbi( ADCSRA, ADEN );
freeze = true;

Chipakero made it! (author)Chipakero2014-10-24

Hi there! Do I need to erase the line ADCBuffer[ADCCounter] = ADCH


if (++ADCCounter >= ADCBUFFERSIZE) ADCCounter = 0;

in the ADC ISR routine?

Also, where do I have to put the line "stopIndex = ADCBUFFERSIZE + 1;"?

Thank you very much! Sorry my english!

IdanR3 made it! (author)IdanR32017-02-26


you put stopIndex = ADCBUFFERSIZE + 1 at:

Girino.ino to replace the line:

"wait = false;"

Jimmus made it! (author)Jimmus2014-08-17

I wish I had read this before I did my project. I could only get it to work with a prescaler as low as 4. I wanted to do 2, but it wouldn't respond.

After I had already torn out my test setup and put my project into production, I thought that maybe the ISR was taking too long. I looked at the assembly it created, and I came to the same conclusion that you did, that the integer division was part of the culprit. My solution would have been to set ADCBUFFERSIZE to 1024, which the compiler converted to a simple AND instruction. But I like your solution better. Isn't it funny how we sometimes write code that looks more elegant in C, but using a simple if block (that takes more code in C) actually builds more efficient machine code?

Caffeinomane made it! (author)Caffeinomane2012-07-10

Thank you very much womai for your precious advices!
This period is very though for me but I am planning to modify the project according to your suggestions.

DarrenF20 made it! (author)2017-02-08

Finally, someone that realises the importance of detail. Many thanks for the time and effort you've put into this. This should be used as a benchmark for posting a project here.

Do you really need that last emitter follower though?

asdfarkward made it! (author)2017-01-02

All I want is a bill of materials :(

johnl375 made it! (author)2016-08-10

Great Instructable

AlexA31 made it! (author)2016-05-10

Can anyone help me, what pins of the arduino need to be connected where? I don't understand

PirateKittyK made it! (author)2016-03-26

Excellent article. However, you should also mention the higher ADC clock setting for lower resolution than 8 bits.

Also, the link no longer works.

kerahari made it! (author)2016-03-23

I am finding difficulty to understand pin connections to the Arduino from LM324..please help me

Yonatan24 made it! (author)2016-01-31

Hi, I've added your project to the "Make Your Own Oscilloscope!" Collection

This is the link If you are interested:

efrem.hug made it! (author)2015-11-20

I don't see where you mention what sample rates you were getting with this. I was in a desperate situation last week where I needed an oscilloscope to check on an automotive signal, I didnt know if it was 5V or 12V or the wave shape. I built one in an hour (a few 100K pots as voltage dividers from 16V to 5V), and a small sketch... Sampling 4 ADC channels I was getting 400 samples/second at 10 bit resolution, I transferred each sample in a 2 bytes over serial.

Starting with the high bit of the high byte

H7 = Text identifier (1=Data, 0 is debug, since you usually only print <128 ascii value characters in debug

H6= High byte ID (Always 1)

H5 = Address bit 1

H4 = Address bit 2

H3 -> H0 = Data bit 9 -> 6 respectively

Now the low byte

L7 = Text identifier (same as high byte)

L6 = Low byte ID (Always 0)

L5 ->L0 = Data bit 5 ->0 respectively

This enabled me to transfer my data, even if it got corrupted and have error recovery using only 2 bytes per sample, and doing only bitwise operations, leaving all the scaling, etc to be done by the computer.. I made a small VB program that parsed the serial input. It also made it possible to print raw ADC values directly to the serial port in human readable form while keeping the data flowing to the program.

The VB 'frontend' just outputted the values to a CSV file with a timestamp (not perfectly accurate) that I opened in excel, where I could graph it from there. not the prettiest way of doing it but it worked for what I needed

efrem.hug made it! (author)efrem.hug2015-11-20

Doh, I see the sample rate now.. so never mind that part

karlooie made it! (author)2015-03-21

Caffeinomane congrats, it looks like 3 years are passed and it's still one of the best project online for cheap oscilloscope!

It surprise me that I could't find any android gui (via bluetooth, I'd expect). Anyone is aware of it?

If not I'm gonna work on a simple interface. I'll make it opensource of course. Anyone would show some interested? let me hear some feedback!

Thank you, and kind regards!

RowanS1 made it! (author)RowanS12015-09-05

interested in hooking it up to a web server on a raspberry pie to display charts on my phones web browser

agis68 made it! (author)2015-08-12

grate idea keep doing!

Jupiterov made it! (author)2015-07-26

Hi All! There is a nice project of an Oscilloscope with Arduino UNO and miuPanel that permits to see and control the oscilloscope with a smart phone. The sample rate is 50 kSa/s, it implements the trigger and can provide more than 20 FPS on a smartphone LCD. See: You could use miuPanel too to provide the graphical interface to your Arduino projects.

Festrada007 made it! (author)2015-07-03


marvin42 made it! (author)2015-05-21

Nice project, I wrote a GUI in c++ and did some work on firmware and PCB. Available here.

Franciscodr made it! (author)2015-05-04

Great project!

Congratulations and keep going!

Antiath. made it! (author)2015-03-26

That's a really nice tutorial, very clear in every step.

Still, it misses something very basic : a summary of the used pins on the arduino and the functions that goes with them. It's annoying that we have to dive in the code to search for the input pin used by default.

bersham made it! (author)2015-03-24

i know this sounds really stupid but i somehow can't see how to actually see the signal ?? which pins in the uno should i connect the input signal to ?

DragosP made it! (author)2015-02-24

I have a question about the low-pass filter. Using a 1.8k resistor and a 1uF capacitor gets you a cut-off frequency of 88.5Hz [ 1/(2*pi*R*C) ], not 560Hz [ 1 / (R*C) ] which is rad/s.

Question is, should correct the filter to cut-off at 560Hz or is it okay as it is to cu-off at 88Hz=560 rad/s?

DragosP made it! (author)2015-02-24

Did anyone else have problems compiling?

I'm getting this error:

'HardwareSerial::HardwareSerial' is not a type

Thought it might be a missing library. Tried adding it, didn't help.

Any help would be hugely appreciated.

DragosP made it! (author)DragosP2015-02-24

For anyone else that might run into this problem. Replacing

'HardwareSerial::HardwareSerial' with 'HardwareSerial' seems to work.

Good luck.

zavorra made it! (author)2015-01-25

This is actually one of the most clear and complete instructables I have read so far!


Deltafee made it! (author)2014-11-13

Hi I am trying to get this oscilloscope working by problem is the python scripted Here is a picture of the problem script. The error it gives is "missing parenthesis" and I am not sure hoe to get this resolved. I hope to hear from you soon of how to fix the error.

hunt333r made it! (author)hunt333r2014-12-15

the parenthesis error is because u install Python 3xx and it was created for 2xx u need to do for all ... print fpwhat was befor naw become print (what was before), i hope is not too late.... i have some problems with libraries

hunt333r made it! (author)2014-12-07

hy there! first of all i whant to say nice job nice project!!! but i have some problems building the pcb, can someone help me with this? the pins that are used are confusing me... does someone have another scematic of this project, something i can understand or two pictures with the pcb one from the top and one from the bottom. thenks alot for the help

yoelk made it! (author)2014-11-08

Hi Caffeinomane,

My name is Joel Koenka, I'm a PhD student in the university of Basel.

I recently developed a Python GUI framework for Arduino-based instruments - "Instrumentino".

You can read about it here:

In the GitHub link you'll find the code and some documents.

I believe we can modify Instrumentino to fit your needs as a frontend for Girino.

I'm on my way these days for preparing instructables for installing and using Instrumentino, but it will take a while.

If you're interested, please contact me: yoel.koenka _at_

Chatanga made it! (author)2014-08-08

Hi all,

project is starting to getting old, but, for those still interested in,
I've developed a(nother) small Java GUI frontend to drive the Girino:

It is not in pure Java since there is a native part to deal with the
serial communication, but it is the same as the one used by Arduino.
This way and provided you have a working Arduino IDE on your machine,
you should have no problem to run it on you Linux / Mac / Windows. Take
some time to look at the README file however since there is a lingering
problem with the 'wait duration' settings.

Chatanga made it! (author)Chatanga2014-08-22

I’ve released a new version which mostly fixes launching problems on Mac and Windows:

I’ve also tried the optimization described by womai and it has finally allowed me to unlock the x8 and x16 prescalers. Nice! The x4 prescaler is still out of reach, but I don’t think the code could be optimized further.

Sine signal acquired on Mac.png
Chipakero made it! (author)Chipakero2014-10-23

Hi, I'm trying to use your program to interpret Girino data, which I have already configured properly. It works great!. I'm trying to do a bluetooth signal transmitter, the problem is that the program does not detect my bt receiver port (COM4). How can I solve this? Also, is there a way I can save the read data? Thank you very much for everything, especially for sharing your work, greetings!

PD. English is not my native language sorry if you have trouble understanding me.

Chatanga made it! (author)Chatanga2014-10-23

Did you tweak the Girino code as stated by Womai? It should allow you
to use the "yellow" sampling frequencies. Regarding your problem with
Bluetooth virtual port, I didn’t have a clue since I’ve never tried to
use one. In theory, it should at least has been been detected by the
RXTX library. As a bluetooth device seems to actually create 2 ports to
communicate (one for reading, another for writing), having it working
without change in the Girinoscope is doubtful...

Chipakero made it! (author)Chipakero2014-10-24

Thank you very much for your answer!, I will try the fix given by Womai (where do I have to put the code "stopIndex = ADCBUFFERSIZE + 1;"?). Returning to the bluetooth problem, when I try the python code written by Caffeinomane, it works very well and it detected the BT port and I can send data. But when I use your GUI, it only detected one of the two created bluetooth ports, the one that is not reading the Arduino. Also, is there a way I can save the read data using your program? Again, I thank you a lot for taking the time to answer.

Chatanga made it! (author)Chatanga2014-10-28

**Patch:** at the begining of the sweep (case 's' in the loop function).

**Bluetooth:** try to add the "" option in the startup script.

**Data export:** you can’t but it shouldn’t be hard to implement... You need to capture a single frame or do you have some specific needs?

Chipakero made it! (author)Chipakero2014-11-06

I 'm trying to make a signal transmitter using bluetooth. I need that the acquisition be as continuous as possible and I'm having this problem as you can see in the figure. In each sweep I have lost data. Do you know how can I solve this?. Regarding saving data I would need to be able to configure the saving time, for example 5 min o 30 sec, etc.

Chatanga made it! (author)Chatanga2014-11-06

If you want to achieve fast continuous acquisition *and* transmission, the Girino (in fact the Arduino) won’t be powerfull enough. Just think about Womai’s tweak to unlock the highest capture frequencies: the Arduino is clearly 100% busy and hardly manage to simply store digital data. No way it can also send them through a serial connection in the same time. Doing both tasks should become possible at lower frequencies, but it would however require a significant refactoring of the Girino code. In fact, a more common configuration for a circuit doing continuous acquisition would be to use two specialized microcontrollers communicating using some kind of buffer.

daveleach42037 made it! (author)2014-10-16

Hi Guys

I would really like to get this project going as it looks fun and useful! I am getting an error message that I just can't get passed: Change has been rejected for parameter Threshold: 150 =/= 15. Do you know what it means?

daveleach42037 made it! (author)daveleach420372014-10-16

Ok, I am passed the last error message but now I get Error 0x5...see picture.

I am running Windows 7 and using an Arduino Duemilanova. Any ideas?



Chatanga made it! (author)Chatanga2014-10-19

Did you patch your Girino code as stated in the README ( If it the case, choosing a higher value for COMMANDDELAY could solve the problem (the communication protocol is not very elaborated).

Regarding your second problem, maybe you have a lingering java instance using your COM port?

phockney made it! (author)2014-10-11

Brilliant guide! Very clear explanations on key concepts, very thorough, and sets a great example on how to go about structuring a project like this.

paul18fr made it! (author)2014-09-12

Dear All,

Where can I find a bill of materials to build a girino-scope (I've the pdf file, but I don't want to forget anything) ?

I'm a biginner in electronics, and girino project is an interesting one that allows to learn many things.



paul18fr made it! (author)paul18fr2014-09-13


I'm a newby in electronics but I'm learning (hobby) ; girino is my first main project and I'm trying to understand the design of the (great) girino scope in order to build my own one ; after reading the different documents and the schematics, I do not fgure out some key point (especially in the double power supply). ...

I think the best thing is to find somebody than has ever built his one girino scope and can explain to me different things (in PM I think)



About This Instructable




More by Caffeinomane:Paper EmbossingGirino - Fast Arduino OscilloscopeCeltic Knot Bone Pendant - Triquetra Pendant
Add instructable to: