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.