Mark's page icon
Knowledge Junkies

Arduino Oscilloscope

I teach Electrical Engineering at the Univiersity of Nebraska Lincoln. When the pandemic hit in the spring of 2020, access to the electronics labs ended up being very restricted. Entry level students just needed somthing to be able to see waveforms and generate a few signals. We sell a version of the Arduino for only $6 so I just wondered how far I could push an Arduino and if it would be enough for some basic limited instruction when you don't have much of anything else. Classes were over in May and nobody was really going anywhere. This ended up being one of those pandemic projects.

The processor used in the Arduino we sell, the Arduino Nano is an ATMEGA328P. This processor is clocked at 16Mhz and the first question is how fast can I get the A/D converter to run. I wanted to get to 10Khz sample rate with 2 channels. Under normal operation the A/D converter needs 13 ADC clock cycles and the maximum recommended clock rate is 200Khz. We have a limited selection of dividors off of the main 16Mhz clock and that would get us 125Khz. 13 cycles at 125Khz is already 104uS and that is for just one channel. The work around for this is that we don't actually need the full 10 bits. Memory issues will limit us to 8 anyway and we can over clock at the loss of some accuracy we don't need. By clocking the ADC to 500Khz and only expecting 8 bits allows for a 26 uS conversion time. Reading and processing both channels can be done in about 60uS so we can hit the 10KHz on both channels at one time.

With only 2048 bytes of ram, I decided to use two ring buffers, each 512 bytes in length to hold the results from the A/D converter channels. I use ring buffers so that we can continually read values and stuff them into memory. I later figured out that I still had enough memory to add a few more features. I added 4 digital inputs and they each have their own ring buffers. I do bit stuff the bytes so I only needed 64 bytes for each for a total of 256 bytes. These 6 buffers takes up 1,280 bytes of my 2048 available. There also a bunch of global variables to keep track of everything going on. As of version 1.4.2 we are using 1795 bytes out of the 2048. Those other 2 53 bytes are needed for local variables and and stack. You will get a compiler warning about being low on RAM, but haven’t seen any problems with running out.

The ATMEGA328 also has 3 timers built in, TIMER0 and TIMER2 are 8 bit and TIMER1 is 16 bit. I wanted to keep TIMER1 for clock output. Almost everything is interrupt driven starting with the system tick timer. We use TIMER2 to generate the 10KHz system tick that triggers each data sample cycle. This 100uS tick interrupt routine starts the A0 A/D conversion cycle and samples all 4 of the digital channels in one shot as they are on the same port. When the A/D conversion is done it generates its own interrupt. In this routine, if just completing A0, it starts the conversion on A1 and returns to the main loop. Once the A1 A/D conversion finishes, it generates another interrupt to the same vector. This time if adSampling is true we load up all of the ring buffers and advance the pointers. We also take a look at all of the trigger and measure settings. The trigger and measure state machines are also located in this area. It makes the routine look really long, but it should still be very fast. Once the system has been triggered and the count of samples satisfied, the adSampling is turned off and we wait to unload the data. For a rising trigger, the measured value on the channel must measure at least one sample below the selected level before it starts looking for a point higher or equal to the trigger level. Falling trigger works the same way with comparisons reversed.

If we stop sampling exactly when the trigger event occurs, we would only see what happens before the trigger. When a trigger event occurs, there is another value that determines how many points are to be collected after the event. This If you set the value to 512 then it will rewrite the full ring buffer and all of your data displayed will be from after the trigger. By setting this value to 400, you get to see what happened 100 points before the trigger. You can set this value to 65535 and it will collect 6.5535 seconds worth of data, but you will only get to see the last 500 points. The default value is set to 400 but you can adjust that with the P command. While it is waiting for a trigger event, you can still enter commands and adjust the level.

With only 512 locations to fill up and 100uS sample rate, we fill the buffer in 51.2mS. This is the maximum speed of the system. We can go slower by setting the skip parameter and forcing the program to ignore reads. The SC n command can set the skip parameter to what ever you wish. If you set it to 50 or more, you can also force the output into a smooth scroll mode with the SS command. The SN will return it to normal mode. Setting SC 10 will sample 1,000 times per second where as setting to 10,000 will get you to once per second.

Commands are 1 or 2 character sometimes followed by an argument. A space between the command and the argument is optional. If the command is 2 characters, you cannot put a space between them.

Characters from the keyboard also generate interrupts and are processed as they come in. Once it hits the 0x0D Return character, the line is passed to the processCommand() routine. With most things happening in interrupt routines, commands are processed at just about any time.

After a trigger has completed the display is updated. You need to hit a carriage return with no arguments to get it to trigger again. There is a command TH n to force a re-trigger after n seconds. So you can have the display auto update after 5 seconds or so. The system isn’t that fast so 5 seconds works ok.

There is a measure feature that works just like the trigger function. It will tell you how long between two events. The resolution isn’t all that great due to only sampling at 100uS. Again the condition must be false before it can measure trigger can be true. If the condition is never met, a value is still printed but is bogus. There is a status line in the plot that needs to be looked at.

I have a PDF that explains things a bit better and also has example plots for you to look at. I keep it up to date a bit better than this text.

AOScope Documentation PDF

AOScope Program

YouTube Demo Videos