I have seen many projects over the years for building an Arduino oscilloscope. Although they always looked interesting, I never built one because they’re basically too slow when implemented on a 16 MHz processor – perhaps useful to 10 KHz.
But I recently bought the Arduino Giga and its Giga Display shield. With a 480 MHz processor and some very fast ADCs (analog to digital converters), I thought maybe it’s time I built an oscilloscope. I’ve used oscilloscopes a lot over the years, but don’t know much about them. After doing this project, I learned a lot, but also realized that there is still a lot I don’t know!
I am dividing this article into two parts. In the first part, we will look at the Arduino Giga and its performance as an oscilloscope. Do we actually achieve a 2 MHz oscilloscope? What is the sample rate? Etc.
Then, in the second part, I will get into my rather primitive attempt to actually create a working oscilloscope. By that I mean our oscilloscope needs to have a front end that conditions the incoming signals to shift the signal to the correct voltage and scale it to the proper range. It also needs to handle a wide range of frequencies. And it needs triggering to sync the signal with the scope's horizontal sweep.
But First, a Little Bit of TheoryOscilloscopes used to be very fast but fairly simple analog devices – amplify and scale a signal and project it onto a cathode ray tube with an appropriate horizontal scan rate. Everything happened in real time. If you wanted to store the waveform, you took a picture of the CRT screen.
Then storage oscilloscopes came along which digitally store the waveform in memory where you can perform all sorts of processing and analysis. I think of digital storage oscilloscopes as sophisticated, advanced versions of the analog oscilloscope, but they are actually the only way we are able to implement the oscilloscope using microcontrollers and/or modern digital electronics in general. We basically have a very short amount a time, like a microsecond in the case of a 1 MHz signal, to capture the waveform and store it in memory. Then we have practically forever to put it up on our display. I am refreshing my display about every 45 milliseconds, so that is 45, 000 times as long as it took to capture the waveform.
The display or human interface can be very slow because we (humans) are very slow. But our oscilloscope is only as good as its ADCs can very rapidly capture and store the waveform we are trying to observe. There are two main parameters to determine how good (or fast) an oscilloscope is. The first is sample rate – how many times in 1 microsecond can we measure the value of our incoming signal. The second is bandwidth, which basically states how fast the analog circuitry can respond to the incoming signal, before that signal starts to degrade, distort, and fade away.
The Arduino GigaSo how well does the Giga do at performing this fast capture from ADCs? Fortunately, there are people out there who understand how to get the fastest performance out of ADCs. I found a library called STMSpeeduino, written by Benjamin Gombala. It basically provides exactly what we need to implement our oscilloscope – two ADC channels each capturing data at close to 10 MSPS (million samples per second). This library is specific to the STM32H747 processor, so specific to the Arduino Portenta and Giga platforms. I found this library very easy to use, even though it is a beta version with limited documentation.
My original objective was to build a 1 MHz oscilloscope with two channels and at least 5 MSPS sampling rate. The Giga actually exceeds that. I don’t have the tools to accurately determine the final result – a 10 MHz signal generator and a 50 MHz professional oscilloscope come to mind, but I don’t have either one. I know I can clearly see an approximately 2 MHz sine wave. I am getting about 9 MSPS with 2 channels, and with a single channel in “interleaved” mode, I can get about 18 MSPS.
Below is an image from our Giga display with an approximately 1 MHz sine wave. It is projected onto the display at the horizontal scan rate of about 1 microsecond / div.
In this image we can see the individual ADC reads. We are in single channel “interleaved” mode and getting about 18 samples per microsecond!
From what I’ve learned about oscilloscope specifications, that 18 MSPS qualifies as a 6 MHz oscilloscope. (Or a 3 MHz oscilloscope at 9 MSPS in 2 channel mode.) But what about bandwidth? My signal generator stops working at 2 MHz! I know I can clearly see a 2 MHz signal, so we at least have a 2 MHz bandwidth – probably much better than that. So I am calling the Giga with the STMSpeeduino library a 2 MHz dual channel oscilloscope and a 4 MHz single channel oscilloscope!
Creating a Working OscilloscopeThe Giga provides one other feature besides its fast ADCs. With the Giga Display shield and the GigaDisplay_GFX library, we are able to fairly easily implement an output screen for our oscilloscope. Since the display shield plugs directly into the back of the Giga, and gets its power from the Giga, hardware implementation is very simple.
However, after working on input signal conditioning, selecting different horizontal scan rates, and different triggering schemes, I have come to realize that there are a lot of requirements to creating a working oscilloscope. What I finally came up with for the project presented here is some physical hardware to do the minimum in signal conditioning. Then for the other stuff, specifically setting single or dual channel, setting the horizontal scan rate, and selecting a trigger mode, I created a GUI (graphical user interface) that only comes up when you power up or reset the Giga. There you can make your selections on the touch screeen, then hit Start to switch into oscilloscope mode. It is not as convenient as being able to change these things on the fly, but it is a way for the user to select these things without modifying the code. By making these choices at startup, I was able to avoid the possible issues of trying to reconfigure the STMSpeeduino Library while it is running.
Input Signal ConditioningI suspect the right way to condition the input signal would have been to get some op amps and build a flexible front end with switchable scalers and shifters. I actually got out some op amps and started experimenting. I naively thought regular op amps like the quad LM324 or dual LM358 would work – turns out they aren’t even close to being fast enough. They don’t work at all at 1 MHz!
There are high frequency op amps that can handle signals well into the MHz range, but I took the easy way out. What I did instead was minimal and super simple:
This simple circuit is the input for each channel. It only does two things: cuts the incoming signal down to something less than 3 volts peak to peak, and then shifts its mid-level to 1.5 volts, giving us a signal between 0 and 3 volts which should always be in the range of our 3.2 volt ADCs.
Just like getting our signal to the proper voltage and amplitude, we also need to set our horizontal sweep frequency to the proper scale. Again, I took a rather primitive and simplistic approach. My display uses a fixed scale with a vertical line every 100 pixels. I display the output of the ADCs sequential readings every 4 pixels (or every 12 pixels in the high speed stretched mode).
The “SampleTime” setting in the STMSpeeduino library basically sets the time differential between each ADC reading and hence the horizontal time scale. All I have done is empirically measure the approximate microseconds / div. for each of the different SampleTime settings. You can’t change these time scales on the fly, but you can select one in the GUI at startup ( or by hitting the reset button). We will talk more about the horizontal sweep in Software.
Trigger ModesTriggers are used in oscilloscopes to synchronize the incoming signal with the horizontal sweep so that the waveform stands still on the display. Since we capture the waveform as data, it is very easy to go through the data and find a particular event to sync to. As I have done with everything else, I tried to keep the options simple. We can sync to channel 1 or channel 2. We can sync to a rising 0 crossover or a falling 0 crossover. I also have a dual sync where both channels sync to a rising crossover. As a last option, we can let the scan free run, i.e. no attempt to sync. That gives us 6 options in our GUI.
The Graphical User InterfaceThe photo below shows the GUI. It has three sections.
The Aqua buttons in the middle allow you to select 1 channel or 2 channel operation. One channel is Channel 1 only, the ADC on pin A5. Dual channels allow you to see channel 2 as well, the ADC on pin A6.
The Green buttons on the right allow you to select the trigger mode, using Channel 1 or 2, using rising or falling transition through 0, free running with no trigger, or finally, dual sync mode, where both channels trigger on the rising transition through 0.
The Blue buttons on the left allow you to select the horizontal scan rate from 0.5 microsecond / division on the screen to 250 microseconds/ div.
The default font that comes pre-installed with GFX graphics is pretty small. For the larger text like the title and Start button, I added a larger font, which is one of the files in the code download.
I won’t go into a lot of additional detail here about the design of the GUI. But I recently published a tutorial specifically about building a GUI on the Giga and Giga Display Shield. If you want more information, you can view it at:
Oscilloscope SoftwareI have to start by again mentioning the STMSpeeduino library. It did most of the hard stuff.
The main loop is where the action is to actually create our oscilloscope image. First there is a collection loop, where the STMSpeeduino library rapidly reads and stores the ADC values. Then the findTriggerValue() routine finds the first point in the results where the trigger requirement is met. Then we clear the active display area and proceed to fill it, first with our fixed horizontal and vertical lines and then with the new data from our ADC readings. Finally we wait 15 msec. and do it all over again.
We only draw the voltage labels for the horizontal lines in setup. They are only drawn once, but everything else in the active area of the display is redrawn about 20 times per second in the main loop. This active area of the display is 698 pixels across and 473 high. Our ADCs are set at 8 bits, so 0 to 3 volts corresponds to 0-233 in ADC output. If you use 2 pixels to represent each number in our ADC output, we get a 466 pixel representation of the ADC output. In the horizontal direction, we are going to display 175 different ADC readings. So each value gets 4 pixels horizontally. By the way, when we capture the ADC readings, I actually take 600 readings, even though we only display 175. That is because we don’t know how far into the data we will need to go to find a place that meets our trigger requirement. We want to find the trigger point and still have 175 readings left in our data.
For the highest frequencies and fastest vertical sweeps, I have changed the horizontal spacing between ADC readings from 4 pixels to 12 pixels. This is used on the 0.5 and 1 microsecond / division horizontal sweeps to spread the readings out more horizontally. At 1 and 3 microseconds / division, we are at the fastest single ADC sampling rate. The only difference between 1 and 3 is spreading the readings out horizontally. The 0.5 microsecond / division scan rate switches to interleaving two ADCs to double the sampling rate as well as spreading out the readings horizontally.
The next reasonable question would be: how do we actually plot the oscilloscope output? With infinite computer power available, we might use curve fitting algorithms to draw a nice curve that matches our readings. But the GFX graphics, in spite of the Giga's speed, somewhat limit how fast the Giga can draw. It can draw horizontal and vertical lines and even rectangles much faster than general purpose lines going from point A to point B. To further complicate the issue, lines must be wider than 1 pixel to be clearly visible, and they need to be wider in both the x and y direction. So we are using vertical rectangles to connect our ADC readings.
I started out thinking I would refresh the display every 15 or 20 milliseconds. That wasn't possible because the time required to clear the active area and redraw the dual oscilloscope traces was almost 30 milliseconds. I ran some tests to see why it was taking so long and was surprised by the result. It turned out that clearing the active area (about 3/4 of the display) took 20 milliseconds all by itself. Drawing an actual oscilloscope trace was pretty fast - only a couple of milliseconds. Even so, to clear the active area, redraw the fixed lines, and then draw two two oscilloscope traces ends up taking about 30 milliseconds. I add another 15 milliseconds of wait time (so we can view the final image) and we end up refreshing our oscilloscope images every 45 milliseconds. That's about 22 frames/second.
Another subject deserving a little more explanation is the triggering. It is pretty easy and fast to go through the newly acquired data and find something like a 0 crossover. We then simply use that point as the starting point for displaying data.
The dual sync mode was a late addition to the project, and is a more interesting condition to set up. First we find the sync point on channel 1 just as we normally do. Then starting from that location in the data, we look for the first sync point on channel 2, and measure the difference or "adder" between the two sync points. During display, we use that adder to get channel 2 to sync along with channel 1.
ConclusionsThe Giga processor and its display shield successfully made a 2 MHz dual channel oscilloscope. We were able to get the sampling rate up to 18 MSPS, which is impressive performance. As we mentioned earlier, we need to collect and store our samples in only a few microseconds and then have a much, much longer time (up to 30 milliseconds) to get it drawn on the display. It is somewhat surprising to find that getting everything drawn every 30 milliseconds is as big a challenge as collecting the data.
There is a lot of technology incorporated in modern storage oscilloscopes. I learned a lot during this project, but one thing I realized is how much I still have to learn.
Comments