Introduction: Girino - Fast Arduino Oscilloscope
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() {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.
Serial.begin(9600);
}
void loop() {
int val = analogRead(ANALOG_IN);
Serial.println(val);
}
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
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: http://www.atmel.com/Images/doc8271.pdf
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
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 )
#else
#define dprint(expression)
#define dshow(expression)
#endif
Step 4: Setting Register Bits
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))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
Using them is very simple, if we want to set to 1 the Enable Bit of the ADC we can just write:
sbi(ADCSRA,ADEN);
While if we want to set it to 0 (id est clear it) we can just write:
cbi(ADCSRA,ADEN);
Step 5: What Are the Interrupts
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.ISR(ANALOG_COMP_vect)
{
counter++;
}
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:
sbi(ACSR,ACIE);
In the following site we can see the list of all Interrupt Vectors:
http://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html
Step 6: Continuously Acquire With a Circular Buffer
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
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.
- Signal 1 surpasses the threshold and activates the sweep;
- signal 2 is inside the sweep time and gets caught with the first;
- after the holdoff, signal 3 activates the sweep again;
- instead signal 4 is rejected because it falls inside the holdoff region.
The moral of this story is that we need:
- a threshold level to wich we can compare the incoming signal;
- a signal that tells the microcontroller to start the waiting phase (see preceding step).
- 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.
Step 8: How the ADC Works
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:
- http://www.openmusiclabs.com/learning/digital/atmega-adc/
- http://www.openmusiclabs.com/learning/digital/atmega-adc/in-depth/
- the data buffer can store more data;
- you do not waste 6-bits of RAM per datum;
- the ADC can acquire faster.
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
Step 9: Digital Input Buffers
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:
sbi(DIDR0,ADC5D);
sbi(DIDR0,ADC4D);
sbi(DIDR0,ADC3D);
sbi(DIDR0,ADC2D);
sbi(DIDR0,ADC1D);
sbi(DIDR0,ADC0D);
Step 10: Setting Up the ADC
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
cbi(ADMUX,REFS1);
sbi(ADMUX,REFS0);
// 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.
sbi(ADMUX,ADLAR);
// 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.
cbi(ADCSRA,ADEN);
// 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.
cbi(ADCSRA,ADSC);
// 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.
sbi(ADCSRA,ADATE);
// When this bit is written to one and the I-bit in SREG is set, the
// ADC Conversion Complete Interrupt is activated.
sbi(ADCSRA,ADIE);
// 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
sbi(ADCSRA,ADPS2);
sbi(ADCSRA,ADPS1);
sbi(ADCSRA,ADPS0);
//---------------------------------------------------------------------
// 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.
cbi(ADCSRB,ACME);
// 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
cbi(ADCSRB,ADTS2);
cbi(ADCSRB,ADTS1);
cbi(ADCSRB,ADTS0);
//---------------------------------------------------------------------
// 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.
sbi(DIDR0,ADC5D);
sbi(DIDR0,ADC4D);
sbi(DIDR0,ADC3D);
sbi(DIDR0,ADC2D);
sbi(DIDR0,ADC1D);
sbi(DIDR0,ADC0D);
}
Step 11: How the Analog Comparator Works
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
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.
cbi(ACSR,ACD);
// 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.
cbi(ACSR,ACBG);
// 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.
cbi(ACSR,ACIE);
// 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.
cbi(ACSR,ACIC);
// 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
sbi(ACSR,ACIS1);
sbi(ACSR,ACIS0);
//---------------------------------------------------------------------
// 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.
sbi(DIDR1,AIN1D);
sbi(DIDR1,AIN0D);
}
Step 13: Threshold
- using a trimmer we can manually set a voltage level;
- using the PWM of the Arduino we can set the level by software.
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
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:
http://arduino.cc/en/Tutorial/SecretsOfArduinoPWM
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:
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.f = (Main clock) / prescaler / (TCNTn maximum)
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
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
cbi(TCCR2A,COM2A1);
cbi(TCCR2A,COM2A0);
sbi(TCCR2A,COM2B1);
cbi(TCCR2A,COM2B0);
// 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
cbi(TCCR2B,WGM22);
sbi(TCCR2A,WGM21);
sbi(TCCR2A,WGM20);
//---------------------------------------------------------------------
// 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.
cbi(TCCR2B,FOC2A);
cbi(TCCR2B,FOC2B);
// 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
cbi(TCCR2B,CS22);
cbi(TCCR2B,CS21);
sbi(TCCR2B,CS20);
pinMode( errorPin, OUTPUT );
pinMode( thresholdPin, OUTPUT );
analogWrite( thresholdPin, 127 );
}
Step 16: Volatile Variables
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: http://en.wikipedia.org/wiki/Volatile_variable
Step 17: Writing the Kernel of the Sketch
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
//-----------------------------------------------------------------------------
ISR(ADC_vect)
{
// 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
//-----------------------------------------------------------------------------
ISR(ANALOG_COMP_vect)
{
// 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
- 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.
Step 19: Bypass Capacitors
Step 20: Power Sources
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
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
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
They are both attached to this step.
Step 24: Time Calibration
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 |
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
y=a/x
I got a value for a of
a=1223
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)
- 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.
Step 26: To Be Continued...
- A test with different analog signals (I am missing an analog signal generator);
- A Graphical User Interface for the computer side.
For point 2. the situation could be better. Is anyone willing to help me with that? I found a nice Python Oscilloscope here:
http://www.phy.uct.ac.za/courses/python/examples/moreexamples.html#oscilloscope-and-spectrum-analyser
I would like to modify it to fit it for Girino, but I am accepting suggestions.