Oscilloscope in a Matchbox - Arduino

28,772

301

40

Why would I want a tiny oscillscope? I've got a room full of electronic Stuff including four oscillscopes. But it's a fuss using them. It would be nice to have something that fits in my pocket, that sits next to the circuit I'm working on and that's as easy to use as a multimeter.

This oscilloscope costs the price of an Arduino Nano (£2 and a display (£3) plus a few pence for resistors, etc. It's specification is:

  • max 1M samples/second, min 1000sps
  • 8-bits per sample
  • DC 0-5V; AC +/- 550mV, AC +/- 117mV, AC +/- 25mV
  • USB "PC scope" or built-in display
  • could be battery-powered
  • optional logic display
  • optional frequency meter
  • optional voltmeter
  • optional signal generator

Mostly what I want from an oscilloscope is to know: is the signal present? Roughly how big is it? And roughly what's its frequency? It's not often I really need all the bells and whistles of a proper bench oscilloscope.

How well does it work? At lower sample rates it's quite reasonable. But at 1M samples/second it's pretty poor. You can see that there's a signal and see its frequency but the y-axis is quite crude.You should only really use it for audio (to 20kHz) in the analogue mode but 1Msps works well in Logic mode. After all, it's just an Arduino Nano so it "is like a dog's walking on his hind legs. It is not done well; but you are surprised to find it done at all".

A couple of years ago, I made a "development station" - a plastic box with a solderless-breadboard on the top and a USB-serial converter, PIC programmer, voltmeter and logic analyser inside. My "workbench" has shrunk from several square metres to 15cm square. It now also has its own oscilloscope. I'll publish an Instructable for the "development station" real soon now.

Step 1: Sampling at 1M Samples Per Second

Let's call this oscilloscope "ArdOsc" (because that's the name of the INO file).

Six years ago Cristiano Lino Fontana published an Instructable for his Girino design.

It sort-of works but has problems. In particular, its maximum reliable sample rate is around 37ksps (at 75ksps it occasionally freezes) and the trigger doesn't seem to work properly. It also doesn't have a display.

The Girino is slow because it uses interrupts. Interrupts are slow because of the code needed to save and restore registers. Interrupts are dangerous because they can result in flaky software with errors that occur only rarely. I've been writing embedded code for 40 years and I avoid interrupts whenever I can. Polling Good, Interrupts Bad. Heed My Words.

So ArdOsc disables all interrupts, goes into a tight loop and grabs the data from the ADC when it wants it. If the ADC hasn't finished: too bad - just give me what you've got. It grabs 1000 samples (one byte each), then re-enables interrupts and sends the bytes to the PC through the serial port at 115200 baud - or it grabs 128 samples and displays them on its screen.

The Girino Instructable describes the Arduino ADC in huge detail. If you're interested, read it and read the Atmega328p datasheet. I'm just going to tell you the outline.

The Arduino ADC uses "successive approximation". It measures the most significant bit - is it 0 or 1? Having got that, it then compares its "answer so far" with the input voltage and measures the next most significant bit. Then the next. And so on for 10 bits. The bits are stored in the ADCH register (first 8 bits) and ADCL (next two bits. I only want 8 bits so I ignore ADCL.

The ADC sets a flag when it's measured all 10 bits. But I only want 8 bits so I ignore the flag and read ADCH whether the ADC is finished or not. I originally thought that meant I would get the "answer so far" but I don't. The "answer so far" is stored somewhere else and all we get is the last answer uploaded to ADCH. That means that in the 1Msps mode, every successive set of 4 samples are identical. The Arduino sketch smooths them so they look good but don't be fooled: you're seeing 250ksps. (Thank you to AndrewJ177 for pointing that out - see discussion below.)

It takes time to measure each bit. That timing pulse comes from dividing the Atmega's clock (16MHz) by a "prescaler" value: 2, 4, 8, 16, 32, 64 or 128. If you set the prescaler to 2, that's 0.125uS which is too short for the ADC to do its comparison properly - it's very poor quality. Prescaler=4, means 0.25uS which kind-of works - the result is noisy. Prescaler=8, means 0.5uS which is pretty reasonable for 8-bits. In general, the longer you give the ADC per bit, the better it works.

But if you allow the ADC, say, 1uS per bit then it's going to take 8uS per byte which is 125ksps - rather slow. If you set the prescaler too low, you'll only get the top few bits converted properly and the resulting graph has big jagged steps. If you set the prescaler too high then you'll have to wait a long time for the conversion.

So it's a trade-off between time per bit and samples per second.

We must also consider how long it takes for the input signal to change the voltage in the sample-and-hold capacitor of the ADC. We're not changing channel before each conversion so the charge time dosn't have to be as long as the Atmel documentation suggests but there is still an effect. The oscilloscope is decent up to 20kHz but then the response rolls-off. You can see a 50kHz sine wave but is a quarter the size it should be.

The ArdOsc code just has a loop that is exactly the right length to sample at 1Msps - i.e. it takes 16 clock cycles round the loop. Another more complicated loop does longer sample times.

Step 2: The Simplest Oscilloscope

The simplest ArdOsc consists of an Arduino Nano (328p 16MHz) 4 resistors and 3 capacitors.

The oscilloscope is powered from a USB connection and transmits frames of data to the PC via USB.

The input signal is fed into the ADC A0 pin. A 10k resistor provides some protection to the Atmega in case of extreme voltages. Atmega pins have diodes that prevent their input going above Vcc (5V) or below 0V. The diodes can conduct up to 1mA so the input signal of the oscilloscope can safely vary between -10V and +15V. The input impedance of the the ADC pin is around 100M and 14pF so the additional 10k has little effect on the accuracy of the ADC.

The A0 pin voltage is read by the ADC using Vcc as a reference voltage - so the measurement is from 0V to 5V. (Actually it's 0 to Vcc*254/255.) Unfortunately, Vcc is rarely exactly 5V so the program reads the actual value of Vcc and draws the graticule of the "oscilloscope display" appropriately.

The input is also fed through a 100nF capacitor and into the ADC A1 pin. A1 is connected to 0.55V through a 1M resistor. The A1 pin therefore sees the AC component of the input signal centred on 0.55V.

The A1 pin voltage is read by the ADC using the internal 1.1V reference voltage - so the measurement is from -0.55V to +0.55V.

The 0.55V is generated with a potential divider from the 3V3 pin of the Nano. The 3V3 pin voltage is a lot more stable that the "5V" from a USB connection. The output from the 3V3 pin is not exactly 3.3V so you'll have to trim the potential divider to give 0.55V. Connect the oscilloscope input to ground and see what "voltage" the AC range displays. Adjust R1 until the line is in the centre of the screen - I needed R1=33k.

I've shown a stripboard layout for the circuit. The stripboard is the same size as the Arduino Nano and they form a sandwich. The Nano's underside is next to the copper side of the stripboard (so in the diagram, the Nano is shown from underneath). Solder some pins onto the stripboard then fit the Nano over the pins and solder them to the Nano. In my diagram, the copper of the stripboard is shown in cyan. Red lines are wire links on the stripboard or flexible wires going off the board for signals and power.

Step 3: Amplifying the Signal

The "Simplest" oscilloscope has two input ranges:

  • 0V to 5V
  • -0.55V to +0.55V

but many signals we're interested in are smaller than that. So we can add two stages of amplification.

An LM358 dual op-amp amplifies the AC signal at A1. The op-amps are AC-coupled and both inputs are centred around 0.55V. Both op-amp stages have a gain of just under 5x. Their outputs go to A2 and A3 so the Atmega can choose which signal to sample.

The oscilloscope now has four input ranges:

  • 0V to 5V
  • -0.55V to +0.55V
  • -117mV to +117mV
  • -25mV to +25mV

It uses the same INO file and exe as the "Simplest".

The advantage of centering the AC signal around 0.55V is that the op-amp signal stays low. The LM258 output cannot go within 1.5V of Vcc; so it's range is 0V to 3.5V - dreadful.

I've shown a stripboard layout for the circuit. There are two stripboards - one for the Nano and one for the LM358. They should form a sandwich. The boards are shown from the component side. Fine flexible wires join the two boards. Attach the boards together with sticky pads, soldered stand-offs, or whatever. In my diagram, the copper of the stripboard is shown in cyan. Red lines are wire links on the stripboard or flexible wires joining the boards together. I haven't shown the "test leads".

Once again, you might have to trim the potential divider to give 0.55V. Connect the oscilloscope input to ground and adjust R9 until the line is in the centre of the screen - I needed R9=33k.

There can be a problem with the LM358. If the signal is bigger than the LM358 can handle the output of the LM358 is distorted. You should be using the higher-gain settings to look at small signals. If you use them on big signals they'll get distorted. You could try a better chip if you've got one - the LM358 is a rather poor chip.

Step 4: Logic Display

Often you're dealing with logic levels - could the oscilloscope show a few channels of "logic". Yes - and it's a lot easier than messing about with the ADC.

Is it worth it? Probably not but it's easy to do so why not?

The oscilloscope now has five input ranges:

  • 0V to 5V
  • -0.55V to +0.55V
  • -117mV to +117mV
  • -25mV to +25mV
  • Logic

In the "Logic" mode, four channels of logic can be connected to Arduino pins D8, D9, D10 and D11. They are shown as four lines on the display.

D8 to D11 correspond to the Atmega328p chip's Port-B pins 0 to 3. The chip reads the whole of Port B into its sample buffer rather than the ADC output in the ADCH register.

In the 1Msps mode, the theoretical maximum frequency you'll be able to see is 500kHz - but all you'll get is a solid bar of "state changes". In practice a 250kHz signal is easier to see.

If you don't want the "logic" input then don't include the connectors to D8 to D11. In the INO file, set the bool constant bHasLogic to false. (I tried rewiting the code to use a #define rather than a bool const but it was a mess.)

Step 5: The Trigger

Let's say you're looking at a repetitive waveform, for instance a sine wave. It's nice if the oscilloscope shows it in the same place on the screen for every sweep. So the oscilloscope sweep should be triggered to start just as, say, the wave goes from negative to positive.

At first I tried to use the comparator to trigger the sweep (i.e. starting to collect data) as the Girino does. It seems ideal but turns out to have disadvantages. I decided on a fixed trigger voltage of 0.55V - the middle of the AC signal. The Atmel allows you to connect the comparator to the current ADC channel. Sounds good. But you have to turn off the ADC and, when the trigger occurs, turn it back on again. It takes a while for the ADC to start up. Not so good.

So I take the simple way out - run the ADC and watch the values it produces. When they go from under half-way to over half-way, start the sweep.

In "Logic" mode, D8 is used as the trigger.

If there's no signal then the oscilloscope ought to free-run. To start a sweep after after it's waited a while. I chose a maximum wait of 250mS. The program initialises Timer1 (a 16-bit timer) then waits until it has counted a sufficient number of ticks. I simply watch the Timer1's counter - there ought to be a better way of doing it with flags but it's hugely complicated and I couldn't get it to work 100% reliably.

Step 6: Test Signal Output

You occasionally need a signal to test whatever circuit you're building. Many people will already have a signal generator.

The ArdOsc circuit can provide a square wave at the following frequencies:

  • 31250/1 = 31250Hz
  • 31250/8 = 3906Hz
  • 31250/32 = 977Hz
  • 31250/64 = 488Hz
  • 31250/128 = 244Hz
  • 31250/256 = 122Hz
  • 31250/1024 = 31Hz

The test signal is generated on pin D3.

If you don't want the "test signal" output then don't include the connectors to D3. In the INO file, set the bool constant bHasTestSignal to false.

Step 7: Serial Protocol

The oscilloscope transmits frames of data to the PC via the USB cable as though it were a serial data stream at 115200 baud, 8-bits, no parity.

The PC can send the two kinds of commands to the oscilloscope. Lower case commands are single bytes:

  • 'a' set x-axis to "1mS" = 1Msps
  • 'b' set x-axis to "2mS"
  • 'c' set x-axis to "5mS"
  • 'd' set x-axis to "10mS"
  • 'e' set x-axis to "20mS"
  • 'f' set x-axis to "50mS"
  • 'g' set x-axis to "100mS"
  • 'j' set y-axis to 5V
  • 'k' set y-axis to 0.5V
  • 'l' set y-axis to 0.1V 'm' set y-axis to 200mV
  • 'n' set mode to "Logic"
  • 'p' set trigger to Falling
  • 'q' set trigger to Rising
  • 'r' set test signal Off
  • 's' set test signal 31250Hz
  • 't' set test signal 3906Hz
  • 'u' set test signal 976Hz
  • 'v' set test signal 488Hz
  • 'w' set test signal 244Hz
  • 'x' set test signal 122Hz
  • 'y' set test signal 30Hz
  • 'z' sweep and send data

The data from a sweep is sent as:

  • 0xAA
  • 0xBB
  • 0xCC
  • 1000 bytes of data

The program responds to every command by transmitting an Ack byte - "@".

The Arduino Serial library uses interrupts to read the serial input. Inputs are turned off during a sweep so the incoming byte remains in the Atmega's serial input register. When the sweep ends, the Serial library collects the byte and the program can read it. But if a second byte arrives during a sweep, it will be discarded.

Upper case commands can contain several bytes so can get corrupted is sent during a sweep. Stop sending 'z' and wait for the result before sending an upper case commands. Upper case commands are only used for debugging and testing. Several can containg an integer decimal value 'n':

  • 'A'n set ADC channel to n
  • 'B' report "battery" voltage = Vcc
  • 'D' report status
  • 'F'n set frequency of pwm
  • 'R'n set Vref for ADC
  • 'T'n trigger rising or falling
  • 'U'n set prescaler and send sweep data
  • 'V'n set sample period for ADC

Step 8: Adding a Display

The oscilloscope can have its own buil-in display - a 1.3" OLED. Although 1.3" sounds small, these displays are very legible.

The display has a 1.3" OLE running at 3.3V which is controlled by an SH1106 chip via an I2C bus. (SPI versions are available but I'm using the Arduino SPI pins for "logic".)

I needed a very fast Arduino library and it should preferably be small. The U8glib library is slow and huge so I wrote my own. It has very few commands so it is called "SimpleSH1106".

The SH1106 has a built-in buffer with one bit per pixel. It is arranged as 128 columns by 7 swathes (other sizes are available). Each swathe is 8 pixels high with the lsb at the top. In the SH1106 documentation, swathes are called "pages" but "swathe" is the standard term in computer graphics. The smallest unit you can write is one byte - a column of 8 pixels starting on an 8-pixel boundary.

My library has no screen buffer on the Arduino so all the commands are based on writing whole bytes to pages. It's less convenient but you gain 1k of RAM.

The Atmel328p has a built in I2C driverconnected to pins A4 (SDA) and A5 (SCL). SDA and SCL need pull-up resistors; the built-in I2C driver uses the Atmel328p weak pull-ups of around 50kohm. The 50k pull-ups work at low speed but the rising edges are not fast enough for high-speed so I've added 1k pull-ups to the 3V3 pin of the Nano.

The Arduino IDE has an I2C driver library called Wire.h. It's a nice small fast library but, as you would expect with Arduino, is poorly documented. The library initialises the I2C hardware to run at 100kHz but I wanted faster. So after calling Wire.begin(), I set the Atmel328p TWBR register to a smaller value.

The resulting library is fast - the sweep display of the oscilloscope is drawn in 40mS. The following commands are available:

  • void clearSH1106() fills the screen with 0 bytes (black).
  • void DrawByteSH1106 draws a single byte (a column of 8 pixels).
  • int DrawImageSH1106 draws an image.
  • int DrawCharSH1106 draws a character.
  • int DrawStringSH1106 draws a string.
  • int DrawIntSH1106 draws an integer.

Images are declared in program memory (PROGMEM). A Windows program is provided to convert a BMP file into a run-length-encoded image for SimpleSH1106.

A full description is given with the library.

I've shown a stripboard layout for the circuit. There are three stripboards - one for the Nano, one for the display and one for the LM358. They should form a sandwich. The boards are shown from the component side. Fine flexible wires join the two boards. Attach the boards together with soldered stand-offs. In my diagram, the copper of the stripboard is shown in cyan. Red lines are wire links on the stripboard or flexible wires joining the boards together. I haven't shown the "test leads".

Some displays seem to have the pins in a different order. Check them.

The x-coordinate of the pixels of the 0.9" display I bought run from x=0 to 127. With the 1.3" display they're from x=2 to 129. The library contains a constant "colOffset" which allows you to adjust the offset for your display.

I have attached Gerber files and EasyPC source files for an SM PCB. These have not been tested so use them at your own risk.

Step 9: Different Screens

The analogue display shows the waveform with a graticule. The horizontal axis shows the time im mS. The vertical axis shows Volts with dotted lines drawn for 4V, 0.5V, 0.1V and 20mV; in the DC mode, 0V is at the bottom; in the AC mode, 0V is in the middle shown as a dashed line.

The logic display shows four channels of bits. D8 is the top channel and D11 is the bottom. The horizontal axis shows the time in mS.

There are two pushbuttons: a "Horizontal" button to adjust Timebase axis and a "Vertical" button to adjust the Gain axis. If you hold either button down for 1 second then a menu screen appears.

When the menu is showing, the "Vertical" button scrolls through the different settings and the "Horizontal" button sets the value for each setting. If you don't press either button for 2 seconds, the program goes back to showing the waveform.

Step 10: Frequency Counter

ArdOsc can also act as a frequency counter by using uses Timer1 and Timer2. There are two ways a frequency counter can work: count the number of rising edges in exactly one second or measure the time from one rising edge to the next rising edge.

Once again, is it worth it? Maybe. I can't remember a time I've needed a frequency counter. It's easy to do so why not?

To count the edges of the "logic" signal at D8, the program selects D5 as the clock input of Timer1 (a 16-bit counter/timer). D5 is externally connected to D8 - one of the "logic" inputs. Timer0 (an 8-bit timer) is set to overflow once every milliSecond. Each time Timer0 overflows, it causes an interrupt. After 1000 interrupts, the count in Timer1 is displayed as the "frequency". If the Timer1 count exceeds 65536, it causes an interrupt and the number of such interrupts is noted.

To measure the time from one edge to the next of the AC signal, Timer 1 is set up as a free-running 16MHz timer. The ICR1 register is set to capture the current value in Timer1 whenever the comparator goes high. The negative input of the comparator is connected to A3 and the positive input is connected to D6. Each time the comparator goes high, it causes an interrupt. The time between one interrupt and the next is the period (1/frequency). The program displays the average frequency measured over one second. The comparator is meant to trigger on a rising edge but it has no hyteresis so if there is noise, a falling edge will be seen as a rising edge. This is particularly important at low frequencies so, for instance, a 10Hz signal will ofetn be reported as 20Hz.

Meanwhile, Timer2 might be producing the "test signal". So there are no timers free and we cannot use the normal Arduino functions delay(), millis(), etc.

The code for the frequency counter is based on the excellent web page by Nick Gammon.

If you don't want the "frequency counter" input then you don't need the connection from D5 to D8. In the INO file, set the bool constant bHasFreq to false.

Step 11: Voltmeter

The oscilloscope can also act as a voltmeter which can measure between approximately -20V and +20V. It uses the built-in bandgap voltage-reference of the Atmega328p so is fairly accurate.

Is it worth it? The number of features is getting ridiculous. OK, why not?

The voltage is measured at A6 and the ADC uses Vcc (i.e. approximately 5V) as its reference. Because "5V" is approximate, we also measure the actual value of Vcc by comparing it with the 1.1V bandgap.According to the datasheet, the bandgap is only 10% accurate but the few I tried are close to 1.1V.

The incoming voltage that you want to measure goes through the resistor network. I've chosen the values shown above

  • Ra=120k
  • Rb=150k
  • Rc=470k

You'll find those constants near the beginning of the MeasureVoltage() function.

Rc tells you the input impedance of the voltmeter. 470k is low compared with a cheap digital multimeter but is high enough to be useful.

The lowest voltage that the voltmeter can measure is

-5*Rc/Ra = -19.6V

The highest it can measure is

5*Rc/Rb+5 = 20.7V

You can choose different resistors if you want.

What if you exceed those voltages? It will be fine. If the voltage at an input pin of the Arduino goes above Vcc or below 0V, the protection diodes can survive a 1mA current. With a 470k that means you could, in theory, have a test voltage of 470V. But I wouldn't trust the insulation of the stripboard at 470V and you shouldn't be playing with voltages like that and a circuit this crude.

You'll need to calibrate the voltmeter if want accurate measurements. Connect the voltmeter input "probe" to 0V and see what the the Voltmeter reports. Adjust the calibrateZero constant until the Voltmeter reads "0.00V". Now connect the voltmeter input to a known voltage source - if you have a decent multimeter then measure the voltage of a 9V battery. Adjust the calibrateVolts constant until the Voltmeter gives the right answer.

If you don't want the "voltmeter" input then you don't need the resistors connected to D6. In the INO file, set the bool constant bHasVoltmeter to false.

Step 12: Test Leads

Oscilloscopes usually have fancy test leads. I am generally using a solderless breadboard so I just attached the sort of plug-in wires one uses with a breadboard. As the oscilloscope is powered from 5V, I connect it to whatever 5V and 0V supply I'm using on the breadboard with more plug-in wires.

Step 13: Add a Signal Generator

A signal generator is a very useful piece of test gear. This one uses an AD9833 module. I've decribed a stand-alone version here; this step describes how to add one to to ArdOsc. (This Step is an edit to this original Instructable.)

The AD9833 can gererate sine, triangle and square waves from 0.1 MHz to 12.5 MHz - the software in this project is limited to 1Hz to 100kHz. It can be used as a sweep generator. Sweep generators help test the frequency response of filters, amplifiers and so on.

The AD9833 module I chose is similar to this one. I'm not saying that's the best or cheapest supplier but you should buy one that looks like that photo.

The connections between the modules are:

  • grounds connected together
  • 5V = Vcc of AD9833
  • D2 = FSync
  • D13 = Clk
  • D12 = Data

The schematic above is in addition to the schematic in Step 8. You can use another piece of stripboard to add another layer to the sandwich described in Step 8.

I've updated the INO file in Step 8 to include code to control the AD9833. If you add an AD9833, you should set the bHasSigGen variable to true (I have left it as false as most people won't have an AD9833).

A new menu controls the AD9833. It allows you to select the frequency and the waveform and whether the frequency is being swept.

The sweep generator repeatedly outputs a gradually increasing frequency over over 1, 5 or 20 seconds. It starts at "min" frequency and 1, 5 or 20 seconds later is at "max" frequency. The frequency change is logarithmic and it is changed every milliSecond. While the frequency is being swept, the oscilloscope cannot display it.

In a different mode, the sweep generator outputs a frequency, displays the oscilloscope input, changes the frequency, displays the oscilloscope input and so on. The frequency changes from "min" to "max" over 20, 100 or 500 of these steps (or "frames" as I've called them). The frequency changes are cruder than in the "sweep" mode but you can watch what's going on.

Step 14: Future Developments

Could it be battery powered? Yes, just add a 9V PP3 connected to the RAW pin of the Nano. It typically uses 25mA.

Could it be powered by a single lithium cell? That's not so straightforward as 3.7V may not be enough. The code that displays voltages in DC mode already reads the Vcc voltage so it will adjust the graticule position. The Nano can run on 3.7V (into the "5V" pin). However, the 3V3 output will probably not be at 3.3V; the drop-out of the regulator is too big. You could run the display straight off the 3.7V of the lithium cell but then where do you get the 0.55V reference voltage from? It needs to be stable. Perhaps you could use an LM317 (which gives a stable 1.25V if you connect its Adjust pin to 0V - the drop-out should be low enough at that current). Or you could use an LED as a zener. Or the voltage at the Vref pin can be used so long as you draw a tiny current - connect it to an emitter-follower. You may need to replace the LM358 with an op-amp that works at a lower voltage.

Could the trigger be better? Digital oscilloscopes collect data before the trigger into a circular buffer. Could the trigger level be variable? Could you have single sweep? Yes, you could do all of those but you'd probably be better just buying a "proper" oscilloscope.

Could you use a Pro Mini? Yes but it's not worth it. You will need to make your own 3V3 signal for the display and for the 0.55V reference voltage. If you're sending the data to a PC then you'll need a serial-to-USB converter. Just use a Nano.

Could it be wireless? Yes. Add you own bluetooth with an HC-05 (Instructables are available) and connect to a PC or Android phone. An ESP 8266 would be more trouble that it's worth for this project.

Could you use a bigger display? Yes but why bother, the quality isn't that good. Just buy an oscilloscope.

Can you do better than an LM358? Yes. If you have a variety of op-amps in you component drawer, try them out. Let me know which one works best.

Share

    Recommendations

    • Optics Contest

      Optics Contest
    • Plastics Contest

      Plastics Contest
    • Make it Glow Contest 2018

      Make it Glow Contest 2018

    40 Discussions

    0
    None
    GaborS47

    19 days ago

    Big kudos to AndrewJ177 for the analysis, though I think that 3.25 μs can still be scraped down a bit, if we are ready to sacrifice some precision.

    With the prescaler at 1:4, the required 13 adc-clks map to 4*13 = 52 cpu-clks (that is the 3.25 μs) indeed. And surely we can't access the half-baked ADC value during the conversion, nor can we terminate the conversion early.

    But we can reset the prescaler to 1:2 anytime, so the conversion finishes earlier :) !

    It's too fast for the comparator, so the subsequent bits will be 0 and thus we lose some precision, but consider this timing:

    Clk=0: Start the conversion and set the prescaler to 1:4 by writing (ADSC + 2) to ADCSRA
    Wait 3 adc-clks to start the conversion (sample&hold occurs at 1.5), then wait 4 more adc-clks to let it produce the 4 MSBs, that is 7 adc-clks = 4*7 = 28 cpu-clks
    Clk = 28: Set the prescaler to 1:2 by writing 1 to ADCSRA: 'sts ADCSRA, r17' for 2 clks
    Clk = 30: The prescaler is at 1:2, we need to wait for the remaining 13 - 7 = 6 adc-clks, that is only 12 cpu-clks now
    Clk = 42: Conversion is ready, we may obtain the result: 'lds r16, ADCH' for 2 clks
    Clk = 44: Start the next conversion, which will implicitely set the prescaler back to 1:4

    So one conversion takes only 44 cpu-clks or 2.75 μs at 16MHz, though it will have only 4 bits of resolution. Not really much a gain, but more than nothing :).

    Regards,
    Gabor

    2 replies
    0
    None
    Peter BalchGaborS47

    Reply 19 days ago

    Nice idea. Have you tried it and examined the quality of the resulting data?
    Peter

    0
    None
    GaborS47Peter Balch

    Reply 18 days ago

    Yes, though it's not really the quality but rather the lack of it :).

    For a quick demo I've set up an oscillator, driven at 10kHz, the driving signal is a nice 50% square (00_10kHz_feed.png), and if we sample the output of the oscillator at every 2μs (01_10kHz_output_at_2us.png), below the conversion time, there are those ominout replicated pixels every now and then.
    At 5μs (02_10kHz_output_at_5us.png), it's quite nice sharp, as expected.

    As of the full-speed sampling, I've tried 5 different setups of different number of significant bits.
    The signal frequency is 10kHz, so one cycle is 100 μs, so 10 cycles are 1000 μs.
    The graphs are drawn at 1 pixel per sample, so if we measure the length of 10 cycles with an image editor, that will be the 1000 μs, so the sampling time is 1000 μs / the width of these 10 cycles in pixels.

    So, the full-speed setups:
    1., Nothing fancy, just a normal conversion at 1:4 prescaler: 03_10kHz_output_at_full_speed_10_bits.png
    Ten cycles are approx. 286 pixels, that give a 1000/286 = 3.497 μs/sample, that is quite realistic.

    2., Starting the prescaler at 1:4, but setting it to 1:2 after 3+8 = 11 bits: 04_10kHz_output_at_full_speed_8_bits.png
    Ten cycles are 294 pixels, that is 1000/294 = 3.4 μs/sample. According to the code it should be 54 clks (at 16MHz), which is 3.375 μs, again acceptable. The graph shows no deterioration, which is again as expected.

    3., Speeding up the prescaler after 3+6 = 9 bits: 05_10kHz_output_at_full_speed_6_bits.png
    Ten cycles are 336 pixels, 1000/336 = 2.976 μs/sample. Counting the code, it's 48 clks or 3 μs, seems correct.
    Still no serious graph degradation, at least the noise is bigger :). This resolution would be ideal for the 128x64 mini display.

    4., Speeding up after 3+5 = 8 bits: 06_10kHz_output_at_full_speed_5_bits.png
    Ten cycles are 346 pixels, 1000/346 = 2.89 μs/sample. The code periodicity is 46 clks = 2.875 μs, correlates.
    The graph is a bit flaky, but still more-or-less usable.

    5., Speeding up after 3+4 = 7 bits: 07_10kHz_output_at_full_speed_4_bits.png
    Ten cycles are 357 pixels, 1000/357 = 2.8 μs/sample. The code periodicity is 44 clks = 2.75 μs, believable.
    Now, the graph starts looking really poor. Just left to the display there is a small density column, it clearly shows how quantised the input got. IMHO this small increase of speed isn't worth the degradation.

    The main sampling loop looks like this:
    <pre>
    FUNCTION(sampler_fullspeed_scope)
    ; Read the next sweep of samples from ADC
    ldi(r16, NUM_SAMPLES / 4)
    mov(r2, r16)

    ldi(cmd_start_adc, _BV(ADEN, ADSC) | 2)
    ldi(r17, _BV(ADEN) | 1) ; fullspeed prescaler setting

    LOC(CHECK_TRIG):
    ; ... wait for a rising/falling trigger at some level ...

    LOC(START_SAMPLING):
    cli()
    sts(ADCSRA, cmd_start_adc)

    LOC(NEXT_SAMPLE):

    rcall(LOC(WAIT))
    ; here: N clks after start, conversion ready
    lds(r16, ADCH) ; 2 clk
    sts(ADCSRA, cmd_start_adc) ; 2 clk
    ; here: N+4 clks after start, this is the total convesion time
    st(X+, r16) ; 2 clk
    in(r16, PIND) ; 1 clk
    st(X+, r16) ; 2 clk

    rcall(LOC(WAIT))
    lds(r16, ADCH) ; 2 clk
    sts(ADCSRA, cmd_start_adc) ; 2 clk
    st(X+, r16) ; 2 clk
    in(r16, PIND) ; 1 clk
    st(X+, r16) ; 2 clk

    rcall(LOC(WAIT))
    lds(r16, ADCH) ; 2 clk
    sts(ADCSRA, cmd_start_adc) ; 2 clk
    st(X+, r16) ; 2 clk
    in(r16, PIND) ; 1 clk
    st(X+, r16) ; 2 clk

    rcall(LOC(WAIT))
    lds(r16, ADCH) ; 2 clk
    sts(ADCSRA, cmd_start_adc) ; 2 clk
    st(X+, r16) ; 2 clk
    in(r16, PIND) ; 1 clk
    st(X+, r16) ; 2 clk

    dec(r2) ; 1 clk
    brne(LOC(NEXT_SAMPLE)) ; 2 clks

    ret()

    LOC(WAIT):
    ; ... here comes the appropriate wait loop implementation ...
    ret() ; 4 clks = +2 ADCclks -> 13 after return

    ENDFUNC ; sampler_fullspeed_scope
    </pre>
    (Sorry for the syntax, a homebrew mixture of m4 macros and avra assembly.)

    The wait blocks are like this (this is for the 6-bit sampling):
    <pre>
    LOC(WAIT): ; rcall = 3 clks
    ; here: 8 CPUclks after start = 2 ADCclks
    nop2() ; 2 clks
    nop2() ; 2 clks
    ; here: 12 CPUclks after start = 3 ADCclks
    nop2() ; 2 clks
    nop2() ; 2 clks
    ; here: 16 CPUclks after start = 4 ADCclks
    nop2() ; 2 clks
    nop2() ; 2 clks
    ; here: 20 CPUclks after start = 5 ADCclks
    nop2() ; 2 clks
    nop2() ; 2 clks
    ; here: 24 CPUclks after start = 6 ADCclks
    nop2() ; 2 clks
    nop2() ; 2 clks
    ; here: 28 CPUclks after start = 7 ADCclks
    nop2() ; 2 clks
    nop2() ; 2 clks
    ; here: 32 CPUclks after start = 8 ADCclks
    nop2() ; 2 clks
    nop2() ; 2 clks
    ; here: 36 CPUclks after start = 9 ADCclks
    sts(ADCSRA, r17) ; 2 clk, set to 1:2
    ; here: 38 CPUclks after start = 10 ADCclks
    nop2() ; 2 clks
    ; here: 40 CPUclks after start = 11 ADCclks
    ret() ; +4 clks = +2 ADCclks -> 13 after return
    </pre>

    As a conclusion, I think the most reasonable compromise is to leave the prescaler at 1:4 for the first 3+6 = 9 adc clks, that gives us 6 valuable bits, then set it to 1:2, so the conversion completes in 3 μs.

    00_10kHz_feed.png01_10kHz_output_at_2us.png02_10kHz_output_at_5us.png03_10kHz_output_at_full_speed_10_bits.png04_10kHz_output_at_full_speed_8_bits.png05_10kHz_output_at_full_speed_6_bits.png06_10kHz_output_at_full_speed_5_bits.png07_10kHz_output_at_full_speed_4_bits.png
    0
    None
    argha halder

    24 days ago

    Seems like a fun project to make. Thank you for writing such a well documented instructable for it!

    0
    None
    Peter Balch

    4 weeks ago

    I have added a new step - Step 13 - which describes how to add a signal generator. I have also updated the INO to improve the frequency counter.

    0
    None
    AndrewJ177

    5 weeks ago

    Hi Peter

    Inspired by your project, I decided to do some experimentation with the Arduino ADC myself. What I've found is that it doesn't actually appear to be possible to get unique samples every microsecond. I believe if you check the contents of your buffer when you're running at 1 sample per microsecond you'll have 4 consecutive identical samples every time.

    What I believe happens with the ADC circuit on the Atmega328 is that the ADCH and ADCL registers aren't updated until the conversion fully completes. Before then any partially converted values are held somewhere internally in the ADC circuitry that you can't access. Therefore you can't read ADCH early and get a partially converted value; if you try then what you are actually getting is the result from the previous completed conversion. Nor can you force a new conversion to start before the current one has completed (if you try to do so by setting ADSC again nothing will happen).

    So, with your 1 microsecond loop

    * On pass 1 you'll get the ADC value from the conversion you did before you entered your loop, and as no conversion is currently in progress you successfully start a new conversion.

    * On passes 2 to 4 your conversion won't yet have finished so you'll still get the same value as pass 1, and attempting to start a new conversion will be ignored by the ADC.

    * On pass 5 the conversion will have finished so you'll now get a new value and be able to start the next conversion.

    * Passes 6 to 8 will give the same value as pass 5 as the new conversion won't yet be finished.

    etc, etc.

    So, the fastest you can actually get new samples from the ADC is by setting pre-scaler to 4, using free running mode and reading samples every 3.25 microseconds. However in free-running mode at this speed it only gives 250 nanoseconds for the sample and hold capacitor to settle which isn't really enough. So you're probably better off not using free-running mode but instead using a loop similar to yours but with 4 microseconds per loop rather than one. This will give the conversion time to complete and allow a little gap between conversions for the sample and hold capacitor to settle.

    At pre-scaler values of 8 and above, it should be possible to use free-running mode as the sample and hold time will probably now be enough. So the next possible fastest conversion rate after the 4 microsecond loop would be 1 sample every 6.5 microseconds.

    Regards

    Andrew

    3 replies
    0
    None
    Peter BalchAndrewJ177

    Reply 5 weeks ago

    Hi Andrew

    Thank you for your interesting comments.

    It's certainly the case that it's poor quality at high sample rates. I keep telling people that it's just a vague indication of the signal and they should buy a "proper" oscilloscope.

    > when you're running at 1 sample per microsecond you'll have 4 consecutive identical samples every time.

    I checked and yes, you're right. Darn it.

    I'd been running the 1MHz code with a prescaler of 1 and it needed a lot of smoothing. At prescaler = 1, the buffer values come in identical pairs.

    When I changed the prescaler to 2 I forgot to take the smoothing out and it looked really good. So I didn't notice the identical readings.

    > you can't read ADCH early and get a partially converted value;

    Yes, you're right. I'd thought I understood how the ADC worked but clearly I didn't.

    The next slower sample rate that the program uses is 4 times slower so it can (and does) manage one conversion per sample.

    > it only gives 250 nanoseconds for the sample and hold capacitor to settle which isn't really enough

    Settle from what? I don't think the capacitor is altered by the ADC conversion is it? And we're not changing channels. What you're saying is that the capacitor may not be able to follow the input voltage as closely as one would like. It will act as a low-pass filter - which may be no bad thing. The cut-off frequency will depend on the output impedance of the signal you're measuring.

    I think the capacitance is 14pF. Assume a signal impedance of 10k. And maybe assume that because the cap is only connected to the signal for 1/4 of the time, the cut-off freq is 1/8th (?). I reckon that's a low pass cut-off of 142kHz. I can see a 50kHz sine wave but it's a quarter the size of a 10kHz wave so clearly the cut-off freq is lower. Hmmm.

    > At pre-scaler values of 8 and above, it should be possible to use free-running mode

    I can't remember why I didn't like free-running mode. Maybe it was because I had to use interrupts and the overhead was far too long.

    I think I'll leave the 1MHz mode in but change the instructable to warn people about the sampling. 1MHz is good in Logic mode.

    I'll emphasize that the analogue input is OK for audio up to 20kHz but the Logic mode works well up to 200kHz (I've just tried it).

    Thank you for your input.

    Peter

    0
    None
    AndrewJ177Peter Balch

    Reply 4 weeks ago

    Hi Peter

    "> it only gives 250 nanoseconds for the sample and hold capacitor to settle which isn't really enough

    Settle
    from what? I don't think the capacitor is altered by the ADC conversion
    is it? And we're not changing channels. What you're saying is that the
    capacitor may not be able to follow the input voltage as closely as one
    would like. It will act as a low-pass filter - which may be no bad
    thing. The cut-off frequency will depend on the output impedance of the
    signal you're measuring.

    I think the capacitance is 14pF. Assume a
    signal impedance of 10k. And maybe assume that because the cap is only
    connected to the signal for 1/4 of the time, the cut-off freq is 1/8th
    (?). I reckon that's a low pass cut-off of 142kHz. I can see a 50kHz
    sine wave but it's a quarter the size of a 10kHz wave so clearly the
    cut-off freq is lower. Hmmm."

    Actually I got the figure wrong, it's 500 nanoseconds. The capacitor is disconnected from the signal whilst the conversion is taking place so the value stored on the capacitor remains constant during the conversion. In free running mode it is connected to the input signal for the first 2 ADC clock cycles = 500nS if pre-scaler = 4 (so probably is long enough). In single conversion mode it is connected from when one conversion finishes until 1.5 ADC clock cycles after you start the next.

    "> At pre-scaler values of 8 and above, it should be possible to use free-running mode

    I
    can't remember why I didn't like free-running mode. Maybe it was
    because I had to use interrupts and the overhead was far too long."

    In theory (I haven't tried yet), you could use free-running mode with interrupts disabled by starting it running and then just using a loop to read the ADC at the relevant interval, e.g. a 3.25 microsecond loop for pre-scaler of 4 (which may now be OK now I've got the correct figure for the sample and hold time), 6.5 microseconds for pre-scaler of 8, etc.

    Regards

    Andrew

    0
    None
    Peter BalchAndrewJ177

    Reply 4 weeks ago

    I'm new to Atmels (25 years with PICs) so it's all a bit of a mystery to me.

    > free-running mode ... just using a loop to read the ADC at the relevant interval,

    That's a really nice idea. I wish I'd done that. Not sure it's advantageous to change now though.

    > 500nS if pre-scaler = 4 (so probably is long enough).

    OK. But that means that "unable to fill the capacitor" is even less of an explaination of the roll-off above 20kHz.

    Any thoughts?

    Peter

    0
    None
    NicoParis

    Question 5 weeks ago on Step 2

    Hi Peter,

    Thanks for your incredible project :-)

    I'm at the step 2 (The simplest oscilloscope) of reproducing it at home but I have some difficulty with the exe file.

    I'm able to view a signal for exemple 3.3v or cycling 0...5v at 200ms period. It's great.

    But I get an multiple error messages "Range check error" that stack-up in UI anytime I want to change x range or if I change the signal. Do you have any idea of how to prevent this ?

    Thanks you again

    nico from Paris

    10 more answers
    0
    None
    Peter BalchNicoParis

    Answer 5 weeks ago

    I can't get it give the error you report. The exe was having trouble opening the COM port: the correct port wasn't being listed as an option.

    I've uploaded a new version of the exe to fix the port problem but it probably won't fix the "Range check error". Download the new version from Step 2 - it should be dated 9 Oct 2018.

    Can you give me any more clues on how to reproduce the error?

    Thanks,

    Peter

    0
    None
    NicoParisPeter Balch

    Reply 5 weeks ago

    Hi,

    I just tried the new exe for a quick test without any arduino connected.

    I get a com port error popup (ok) them a second one (still not bad) when I select a range (for instance 1ms) I get a first "Range check error" popup and after 2 seconds another one and again with no limit. So it becomes hard to change anything or to close the software (and I had the same experience with old exe monday).

    Does it help ?

    Thanks

    Nico

    0
    None
    Peter BalchNicoParis

    Reply 5 weeks ago

    I have tried it without any Arduino connected and just get "Error opening COM port" errors.

    The way that version of the exe works is that it attempts to send a 'z' command to the Arduino. If that fails (e.g. because the COM port does not exist) then it closes the COM port and stops trying to send any more commands until you select another COM port. If it succeeds in sending the 'z' command then it does so again every 100msec.

    I've uploaded (to step 2) a new version that waits 3 sec if it doesn't receive a buffer. That should allow you to clear any messages.

    If you have no Arduino connected then the exe should give you an error - "Specified com port doesn't exist" from the driver software or "Error opening COM port" from my exe. I don't understand why it's giving a "Range check error" - that suggests the COM port is open and has sent bad data to my exe.

    Are you using the right COM port (at 115200 baud)? Close my exe if it's running then click on the 'Serial Monitor' button in the Arduino IDE (at the top right). That will display the COM number in the caption bar and reset the Arduino. The Arduino should reply with "@ArdOsc ready". Type z into the editbox at the top and click the Send button. The Arduino should send a buffer of ADC data. Does that work?

    The commands a, b, c, d, e, f, g and j, k, l, m, n are the same as clicking one of the "x-time" or "y-gain" buttons. Type one of them into the editbox - the Arduino should reply with "@".

    If all seems well, close the Arduino IDE Serial Monitor window and open my exe. When you've cleared any error messages, click Options|Debug then click Options|Comms. You can see what is being sent and received. Is the COM port correct (bottom left)? Click the mouse in the left hand pane of sent data. Type z. A buffer of data should be recieved (right pane). Type one of a to g or j to m. The Arduino should reply with "@".

    If all seems well, uncheck Options|Debug (but leave the Comms window open). The program should now run continuously. Watch the data in Comms window. What's going wrong?

    Thanks,

    Peter

    0
    None
    NicoParisPeter Balch

    Reply 5 weeks ago

    I can't see new exe (from today) in step 2 downloads. Is it normal ?

    0
    None
    Peter BalchNicoParis

    Reply 5 weeks ago

    Sorry I hadn't uploaded it to the right place. It should be visible now. The date of the exe should be 10 Oct.

    0
    None
    NicoParisPeter Balch

    Reply 5 weeks ago

    Just tried it new exe, same problem.

    I will try more in depth investigation later in the week.

    I'll tell you as soon as I have more feedback

    Thanks

    0
    None
    NicoParisNicoParis

    Reply 5 weeks ago

    Hello Peter,

    Good news, using another computer, I don't have the "Range check error" anymore.

    Bad news, I may really have a problem with com ports on my main computer. I will investigate further this weekend.

    Thanks

    0
    None
    Peter BalchNicoParis

    Reply 5 weeks ago

    Is it a difference in the Windows version? If you're able to program the Arduino on the "bad" computer then the COM port must be working. My program must be doing something different.

    0
    None
    NicoParisPeter Balch

    Reply 5 weeks ago

    "bad" computer is windows 7

    working computer is windows 8

    0
    None
    Peter BalchNicoParis

    Answer 5 weeks ago

    Hi Nico

    I've only just received your message and I'm busy most of the day. I'll try to check it out this evening. It's very odd that changing the signal produces errors.

    Peter