speed of analogRead

Hello,
I'm interested how long it takes to Arduino Due board to read analog input. In Reference library I found only value 100us that is for Arduino Uno board when using standard analogRead function. And in some book I read that it can be reduced to few tens of us if I will use redefined read analogRead function. But how it is with Arduino Due? How quick is standard analogRead and what time can take read analog input when I will use redefined alanogRead function.

I want to build USB osciloscope and I need time resolution minimally few us. So I'm interested if I'm able to build it based on Arduino Due.

Thanks

Best Regards
Ales

1 Like

I got an analogue read in about 40uS.

I'm working on a similar project at the moment. If analogRead is too slow for you it is possible to put the ADC in 'free-running' mode and read the registers directly - doing this I am getting conversions in 1uS (although I have not tested for accuracy yet). This is the code I was using to test speed:

void setup() {
  Serial.begin(9600);
  int t=analogRead(0);

  ADC->ADC_MR |= 0x80; // these lines set free running mode on adc 7 (pin A0)
  ADC->ADC_CR=2;
  ADC->ADC_CHER=0x80;
}

void loop() {
  int t=micros();
  int q=0;
  int a0;
  for(int i=0;i<1000000;i++){
    while((ADC->ADC_ISR & 0x80)==0); // wait for conversion
    a0=ADC->ADC_CDR[7];              // read data
    q+=a0;
  }
  t=micros()-t;
  Serial.print("1 million conversions in ");Serial.print(t);Serial.println(" micros");
}

You might want to read this:-
http://www.atmel.com/Images/doc11106.pdf
The fast mode is what is known as a tracking mode. This is useful if the input signal is changing by less than the least significant bit amount between samples. Things like bandwidth limited audio waveforms are good for this.

When measuring a signal, time jitter is as important as amplitude accuracy.

See this Analog-to-Digital Converter Clock Optimization: A Test Engineering Perspective | Analog Devices

Here is an example for 100,000 samples per second with 10-bits of accuracy and a clock jitter of 5 ns.

SRN of an ideal n-bit ADC:

SNR(dB) = 6.02*n + 1.76

SNR due clock jitter:

SNR(dB) = -20log(6.28f*t)

f is the measurement frequency

t is the time jitter in seconds

For an ideal 10-bit ADC the max SNR is about 62 dB.

The SNR limit for clock jitter is

SNR(jitter) = -20log(6.28100000*5e-9) = 50.06

So jitter is the limiting factor. What is the point of fast measurements if you don't know the time to sufficient accuracy?

stimmer:
I'm working on a similar project at the moment. If analogRead is too slow for you it is possible to put the ADC in 'free-running' mode and read the registers directly - doing this I am getting conversions in 1uS (although I have not tested for accuracy yet). This is the code I was using to test speed:

void setup() {

Serial.begin(9600);
  int t=analogRead(0);

ADC->ADC_MR |= 0x80; // these lines set free running mode on adc 7 (pin A0)
  ADC->ADC_CR=2;
  ADC->ADC_CHER=0x80;
}

void loop() {
  int t=micros();
  int q=0;
  int a0;
  for(int i=0;i<1000000;i++){
    while((ADC->ADC_ISR & 0x80)==0); // wait for conversion
    a0=ADC->ADC_CDR[7];              // read data
    q+=a0;
  }
  t=micros()-t;
  Serial.print("1 million conversions in ");Serial.print(t);Serial.println(" micros");
}

This piece of code is great!! I'm sampling at 1MHz (one channel) in my Due.
Would the registers setup change for two channels?

Thank you

It's similar. Basically in free running mode the SAM3X cycles through all enabled channels. So we just enable 2 channels and wait for 2 conversions each time:

void setup() {
  Serial.begin(9600);
  int t=analogRead(0);

  ADC->ADC_MR |= 0x80; // these lines set free running mode on adc 7 and adc 6 (pin A0 and A1 - see Due Pinout Diagram thread)
  ADC->ADC_CR=2;
  ADC->ADC_CHER=0xC0; // this is (1<<7) | (1<<6) for adc 7 and adc 6                                     
}

void loop() {
  int t=micros();
  int q0=0,q1=0;
  int a0,a1;
  for(int i=0;i<500000;i++){
    while((ADC->ADC_ISR & 0xC0)!=0xC0);// wait for two conversions (pin A0[7]  and A1[6])
    a0=ADC->ADC_CDR[7];              // read data on A0 pin
    a1=ADC->ADC_CDR[6];              // read data on A1 pin
    q0+=a0;
    q1+=a1;
  }
  t=micros()-t;
  Serial.print("500000 pairs of conversions in ");Serial.print(t);Serial.println(" micros");
  Serial.print("A0 total:");Serial.println(q0);
  Serial.print("A1 total:");Serial.println(q1);
}

Obviously there's only one ADC which is shared by the 2 channels so the effective sample rate halves. This means that you will lose some accuracy, because internally the chip is having to switch between two different input signals 1 million times per second, and that won't be as good as it tracking the same input all the time like in the 1-channel example.

stimmer:
It's similar. Basically in free running mode the SAM3X cycles through all enabled channels. So we just enable 2 channels and wait for 2 conversions each time:

void setup() {

Serial.begin(9600);
  int t=analogRead(0);

ADC->ADC_MR |= 0x80; // these lines set free running mode on adc 7 and adc 6 (pin A0 and A1 - see Due Pinout Diagram thread)
  ADC->ADC_CR=2;
  ADC->ADC_CHER=0xC0; // this is (1<<7) | (1<<6) for adc 7 and adc 6                                     
}

void loop() {
  int t=micros();
  int q0=0,q1=0;
  int a0,a1;
  for(int i=0;i<500000;i++){
    while((ADC->ADC_ISR & 0x80)==0);
    while((ADC->ADC_ISR & 0x80)==0);// wait for two conversions
    a0=ADC->ADC_CDR[7];              // read data on A0 pin
    a1=ADC->ADC_CDR[6];              // read data on A1 pin
    q0+=a0;
    q1+=a1;
  }
  t=micros()-t;
  Serial.print("500000 pairs of conversions in ");Serial.print(t);Serial.println(" micros");
  Serial.print("A0 total:");Serial.println(q0);
  Serial.print("A1 total:");Serial.println(q1);
}




Obviously there's only one ADC which is shared by the 2 channels so the effective sample rate halves. This means that you will lose some accuracy, because internally the chip is having to switch between two different input signals 1 million times per second, and that won't be as good as it tracking the same input all the time like in the 1-channel example.

This is also working.
But I have some doubts about it:

  • Why on the
while((ADC->ADC_ISR & 0x80)==0);
    while((ADC->ADC_ISR & 0x80)==0);// wait for two conversions

line, you use the same pin of the ADC to compare?

Wouldn't it be:

while((ADC->ADC_ISR & 0x80)==0);
    while((ADC->ADC_ISR & 0x40)==0);// wait for two conversions

or better:

    while((ADC->ADC_ISR & 0xC0)==0);// wait for two conversions (pin A0[7]  and A1[6])

??

On the other hand, to reduce Jitter and free more time for "processing" in the loop function, could it enabling ADC interrupts be a solution?
Do you know how to accomplish this? I have seen ADC_IER register in the datasheet, but I'm pretty new with Arduino Due (just a couple of days) and don't know how to start. :blush: :blush:

Thank you again

    while((ADC->ADC_ISR & 0xC0)==0);// wait for two conversions (pin A0[7]  and A1[6])

Well spotted - what it should actually be is this:

    while((ADC->ADC_ISR & 0xC0)!=0xC0);// wait for two conversions (pin A0[7]  and A1[6])

I'll go back and change my code in case anyone else copy/pastes it.

For reducing missed samples and allowing more processing time in the main loop, the biggest win comes through using peripheral DMA. This example uses DMA and interrupts - although if you are new to the Due, don't expect to understand it all at once :slight_smile:

#undef HID_ENABLED

// Arduino Due ADC->DMA->USB 1MSPS
// by stimmer
// Input: Analog in A0
// Output: Raw stream of uint16_t in range 0-4095 on Native USB Serial/ACM

// on linux, to stop the OS cooking your data: 
// stty -F /dev/ttyACM0 raw -iexten -echo -echoe -echok -echoctl -echoke -onlcr

volatile int bufn,obufn;
uint16_t buf[4][256];   // 4 buffers of 256 readings

void ADC_Handler(){     // move DMA pointers to next buffer
  int f=ADC->ADC_ISR;
  if (f&(1<<27)){
   bufn=(bufn+1)&3;
   ADC->ADC_RNPR=(uint32_t)buf[bufn];
   ADC->ADC_RNCR=256;
  } 
}

void setup(){
  SerialUSB.begin(0);
  while(!SerialUSB);
  pmc_enable_periph_clk(ID_ADC);
  adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST);
  ADC->ADC_MR |=0x80; // free running

  ADC->ADC_CHER=0x80; 

  NVIC_EnableIRQ(ADC_IRQn);
  ADC->ADC_IDR=~(1<<27);
  ADC->ADC_IER=1<<27;
  ADC->ADC_RPR=(uint32_t)buf[0];   // DMA buffer
  ADC->ADC_RCR=256;
  ADC->ADC_RNPR=(uint32_t)buf[1]; // next DMA buffer
  ADC->ADC_RNCR=256;
  bufn=obufn=1;
  ADC->ADC_PTCR=1;
  ADC->ADC_CR=2;
}

void loop(){
  while(obufn==bufn); // wait for buffer to be full
  SerialUSB.write((uint8_t *)buf[obufn],512); // send it - 512 bytes = 256 uint16_t
  obufn=(obufn+1)&3;    
}

It reads ADC data at 1 million samples/sec and outputs the data to SerialUSB. (I've only tested it on Linux and most of the time it works, sometimes it is unreliable, I don't know why). Using GNU Radio I was then able to analyse the data stream and receive a long-wave radio signal, with an LC tuned circuit into A0 as an aerial.

stimmer:

    while((ADC->ADC_ISR & 0xC0)==0);// wait for two conversions (pin A0[7]  and A1[6])

Well spotted - what it should actually be is this:

    while((ADC->ADC_ISR & 0xC0)!=0xC0);// wait for two conversions (pin A0[7]  and A1[6])

I'll go back and change my code in case anyone else copy/pastes it.

For reducing missed samples and allowing more processing time in the main loop, the biggest win comes through using peripheral DMA. This example uses DMA and interrupts - although if you are new to the Due, don't expect to understand it all at once :slight_smile:

#undef HID_ENABLED

// Arduino Due ADC->DMA->USB 1MSPS
// by stimmer
// Input: Analog in A0
// Output: Raw stream of uint16_t in range 0-4095 on Native USB Serial/ACM

// on linux, to stop the OS cooking your data:
// stty -F /dev/ttyACM0 raw -iexten -echo -echoe -echok -echoctl -echoke -onlcr

volatile int bufn,obufn;
uint16_t buf[4][256];   // 4 buffers of 256 readings

void ADC_Handler(){     // move DMA pointers to next buffer
 int f=ADC->ADC_ISR;
 if (f&(1<<27)){
  bufn=(bufn+1)&3;
  ADC->ADC_RNPR=(uint32_t)buf[bufn];
  ADC->ADC_RNCR=256;
 }
}

void setup(){
 SerialUSB.begin(0);
 while(!SerialUSB);
 pmc_enable_periph_clk(ID_ADC);
 adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST);
 ADC->ADC_MR |=0x80; // free running

ADC->ADC_CHER=0x80;

NVIC_EnableIRQ(ADC_IRQn);
 ADC->ADC_IDR=~(1<<27);
 ADC->ADC_IER=1<<27;
 ADC->ADC_RPR=(uint32_t)buf[0];   // DMA buffer
 ADC->ADC_RCR=256;
 ADC->ADC_RNPR=(uint32_t)buf[1]; // next DMA buffer
 ADC->ADC_RNCR=256;
 bufn=obufn=1;
 ADC->ADC_PTCR=1;
 ADC->ADC_CR=2;
}

void loop(){
 while(obufn==bufn); // wait for buffer to be full
 SerialUSB.write((uint8_t *)buf[obufn],512); // send it - 512 bytes = 256 uint16_t
 obufn=(obufn+1)&3;    
}




It reads ADC data at 1 million samples/sec and outputs the data to SerialUSB. (I've only tested it on Linux and most of the time it works, sometimes it is unreliable, I don't know why). Using GNU Radio I was then able to analyse the data stream and receive a long-wave radio signal, with an LC tuned circuit into A0 as an aerial.

This code is running smoother and with less jitter.
I am measuring jitter, by setting HIGH and LOW one Digital pin with an oscilloscope and the inline function:

inline void digitalWriteDirect(int pin, boolean val){
  if(val) g_APinDescription[pin].pPort -> PIO_SODR = g_APinDescription[pin].ulPin;
  else    g_APinDescription[pin].pPort -> PIO_CODR = g_APinDescription[pin].ulPin;
}

BTW, I am controlling the sample rate by using the PRESCAL register in:

ADC->ADC_MR |= 0x2680; // these lines set free running mode on adc 7 (pin A0)  [PRESCAL at  50kHz]

Is there any other (better) way to do it?

Actually, depending on if I am using one channel or two an the ADC, I need to setup a different PRESCAL, since there is only one ADC for all input pins:

if (channels_2)
  {
    ADC->ADC_MR |= 0x1280; // these lines set free running mode on adc 7 (pin A0) and adc 6(pin A1) PRESCAL at 50kHz
  }
  else
  {
    ADC->ADC_MR |= 0x2680; // these lines set free running mode on adc 7 (pin A0) PRESCAL at 50kHz
  }

On the other hand, what would the changes for two channels and 4 buffers? Is it possible to get a buffer for each channel?

Finally, I'm trying to measure performance (the same way as jitter) and for 50kHz sample rate and 512 points, the time in the interrupt is 1.8us and the free time in the loop is 10.23ms (99.98% free!!).
For 1MHz, it is still around 1.8us against 256us (it is still 99.29% free), however more jitter at the beggining of the loop is appreciated.

I think it is because of the while(obufn==bufn); instruction

It is interesting the

SerialUSB.begin(0);
 while(!SerialUSB);

Does the computer (or other device) set the speed of the port?

Your spectrogram app looks great, keep up the good work!! Congratulations.

The jitter you are measuring is not the same thing as the clock jitter that was mentioned earlier in the thread and will not affect the accuracy of the ADC readings. Clock jitter is caused by the accuracy of the timing and waveform of the master clock oscillator and there is nothing we can do about that.

The line adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST); sets the prescal value - there is also ADC_FREQ_MIN and you can pass any value in between.

It isn't possible to get a buffer per channel as far as I know. There is a tagged mode where the highest 4 bits are written with the channel the sample came from, that is useful when you enable more than one channel.

SerialUSB works as fast as it can, the baud rate is ignored. Since it's USB2.0 the port is faster than the SAM3X can throw data down it. I've had it running at over 50Mbit/sec.

stimmer:
The jitter you are measuring is not the same thing as the clock jitter that was mentioned earlier in the thread and will not affect the accuracy of the ADC readings. Clock jitter is caused by the accuracy of the timing and waveform of the master clock oscillator and there is nothing we can do about that.

The line adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST); sets the prescal value - there is also ADC_FREQ_MIN and you can pass any value in between.

It isn't possible to get a buffer per channel as far as I know. There is a tagged mode where the highest 4 bits are written with the channel the sample came from, that is useful when you enable more than one channel.

SerialUSB works as fast as it can, the baud rate is ignored. Since it's USB2.0 the port is faster than the SAM3X can throw data down it. I've had it running at over 50Mbit/sec.

I'm sorry about the jitter misunderstanding, after thinking it I knew it was a different issue.

So, about the ADC sampling in both channels, then I suppose that the samples are alternated in the buffers, right? Sorry to insist,but I am learning about the system...so, how could I set up a second channel?

Then, about the adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST) function.... Where could I find more information about this macros and functions? They are not the same as in the Arduino UNO, but pretty similar.

I think that to use a second channel, all you have to do is enable it. You are right, the samples end up alternated in the buffers.

I don't know of any proper documentation for the adc_init function. I just searched through the Arduino directory for files containing adc_init, then looked at the source files.

PakARD:

stimmer:
The jitter you are measuring is not the same thing as the clock jitter that was mentioned earlier in the thread and will not affect the accuracy of the ADC readings. Clock jitter is caused by the accuracy of the timing and waveform of the master clock oscillator and there is nothing we can do about that.

The line adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST); sets the prescal value - there is also ADC_FREQ_MIN and you can pass any value in between.

It isn't possible to get a buffer per channel as far as I know. There is a tagged mode where the highest 4 bits are written with the channel the sample came from, that is useful when you enable more than one channel.

SerialUSB works as fast as it can, the baud rate is ignored. Since it's USB2.0 the port is faster than the SAM3X can throw data down it. I've had it running at over 50Mbit/sec.

I'm sorry about the jitter misunderstanding, after thinking it I knew it was a different issue.

So, about the ADC sampling in both channels, then I suppose that the samples are alternated in the buffers, right? Sorry to insist,but I am learning about the system...so, how could I set up a second channel?

Then, about the adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST) function.... Where could I find more information about this macros and functions? They are not the same as in the Arduino UNO, but pretty similar.

This is a very good site:
http://asf.atmel.com/docs/latest/sam3x/html/group__sam__drivers__adc__group.html

Gericom:
This is a very good site:
http://asf.atmel.com/docs/latest/sam3x/html/group__sam__drivers__adc__group.html

Thank you Gericom.
Thank you stimmer.

First of all, Thanks for sharing this. My application requires using 8 analog inputs, while using the digital outputs to switch between banks of 8 sensors(10*8=80 total). It's recording a 20ms event, and I figured that 1 million samples/sec should give me plenty of time resolution(ADC accuracy is not as important). The Due seemed like the logical choice, but this low level programming is intimidating!

I'm trying to piece the code together to understand this all. Where does this function get called?

void ADC_Handler(){     // move DMA pointers to next buffer
  int f=ADC->ADC_ISR;
  if (f&(1<<27)){
   bufn=(bufn+1)&3;
   ADC->ADC_RNPR=(uint32_t)buf[bufn];
   ADC->ADC_RNCR=256;
  } 
}

I changed this to open up a0-7. Is there anything else needed to read 8 pins?
ADC->ADC_CHER=0xFF;

I can't find much information about SerialUSB. What do I use to read data that quickly? The serial monitor only goes up to 115200. Would it be easier/better to write to a SD card?

a few comments
the 1Msps is for one channel under very specific conditions
if you switch channels there is a lag (you will have to look up the details in the data sheet)

IMO best way is to program the PDC with timing generated by the PWM .. with resonable sample code from atmel
but that will not help you trying to read 80 samples .....

(i'm running at 500,000 samples / sec on one channel, in bursts of ~2k (roughly 4ms/collection ~ pause for 3ms, repeat )

can't comment on the USB , but writing to a SD card "can" be fast (i'm writing ~500kb/sec),
you need buffer space for sd card lag

With Stimmer's first bit of code I was able to do 1 Million ADC conversions/second, while switching through 8 analog inputs. When I added in some digitalwrites the speed drop was minimal. When I tried to read the data with serial.write() everything came to a screeching halt. I need to be able to write 1.5MB/s and SerialUSB.write() looks like it will work. The latency with the SD cards may slow it down too much, but I do like the idea of writing to a csv or txt file.

stimmer:
SerialUSB works as fast as it can, the baud rate is ignored. Since it's USB2.0 the port is faster than the SAM3X can throw data down it. I've had it running at over 50Mbit/sec.

I like his DMA approach much better because is frees up the processor. While the buffer is being filled I can write the data. but I can't find references on how to use SerialUSB.

stimmer:
For reducing missed samples and allowing more processing time in the main loop, the biggest win comes through using peripheral DMA. This example uses DMA and interrupts - although if you are new to the Due, don't expect to understand it all at once :slight_smile:

#undef HID_ENABLED

// Arduino Due ADC->DMA->USB 1MSPS
// by stimmer
// Input: Analog in A0
// Output: Raw stream of uint16_t in range 0-4095 on Native USB Serial/ACM

// on linux, to stop the OS cooking your data:
// stty -F /dev/ttyACM0 raw -iexten -echo -echoe -echok -echoctl -echoke -onlcr

volatile int bufn,obufn;
uint16_t buf[4][256];   // 4 buffers of 256 readings

void ADC_Handler(){     // move DMA pointers to next buffer
 int f=ADC->ADC_ISR;
 if (f&(1<<27)){
  bufn=(bufn+1)&3;
  ADC->ADC_RNPR=(uint32_t)buf[bufn];
  ADC->ADC_RNCR=256;
 }
}

void setup(){
 SerialUSB.begin(0);
 while(!SerialUSB);
 pmc_enable_periph_clk(ID_ADC);
 adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST);
 ADC->ADC_MR |=0x80; // free running

ADC->ADC_CHER=0x80;

NVIC_EnableIRQ(ADC_IRQn);
 ADC->ADC_IDR=~(1<<27);
 ADC->ADC_IER=1<<27;
 ADC->ADC_RPR=(uint32_t)buf[0];   // DMA buffer
 ADC->ADC_RCR=256;
 ADC->ADC_RNPR=(uint32_t)buf[1]; // next DMA buffer
 ADC->ADC_RNCR=256;
 bufn=obufn=1;
 ADC->ADC_PTCR=1;
 ADC->ADC_CR=2;
}

void loop(){
 while(obufn==bufn); // wait for buffer to be full
 SerialUSB.write((uint8_t *)buf[obufn],512); // send it - 512 bytes = 256 uint16_t
 obufn=(obufn+1)&3;    
}




It reads ADC data at 1 million samples/sec and outputs the data to SerialUSB. (I've only tested it on Linux and most of the time it works, sometimes it is unreliable, I don't know why). Using GNU Radio I was then able to analyse the data stream and receive a long-wave radio signal, with an LC tuned circuit into A0 as an aerial.

Thanks for this code! I've got my Due pulling 1MS/s as well. I wrote a very simple python script to display data as it is fetched from the serial port. It buffers the data in a separate thread, then plots as quickly as it can (it downsamples 1 second of data to 10k points and plots in realtime). It can also display the power spectrum of the signal by selecting Plot Options->Transform->FFT from the context menu. Requirements are python, pyserial (http://pyserial.sourceforge.net/), and pyqtgraph (http://pyqtgraph.org/). I'd like to develop this code further to provide display-triggering and controls for configuring the sample rate and channels.

import pyqtgraph as pg
import time, threading, sys
import serial
import numpy as np


class SerialReader(threading.Thread):
    """ Defines a thread for reading and buffering serial data.
    By default, about 5MSamples are stored in the buffer.
    Data can be retrieved from the buffer by calling get(N)"""
    def __init__(self, port, chunkSize=1024, chunks=5000):
        threading.Thread.__init__(self)
        # circular buffer for storing serial data until it is 
        # fetched by the GUI
        self.buffer = np.zeros(chunks*chunkSize, dtype=np.uint16)
        
        self.chunks = chunks        # number of chunks to store in the buffer
        self.chunkSize = chunkSize  # size of a single chunk (items, not bytes)
        self.ptr = 0                # pointer to most (recently collected buffer index) + 1
        self.port = port            # serial port handle
        self.sps = 0.0              # holds the average sample acquisition rate
        self.exitFlag = False
        self.exitMutex = threading.Lock()
        self.dataMutex = threading.Lock()
        
        
    def run(self):
        exitMutex = self.exitMutex
        dataMutex = self.dataMutex
        buffer = self.buffer
        port = self.port
        count = 0
        sps = None
        lastUpdate = pg.ptime.time()
        
        while True:
            # see whether an exit was requested
            with exitMutex:
                if self.exitFlag:
                    break
            
            # read one full chunk from the serial port
            data = port.read(self.chunkSize*2)
            # convert data to 16bit int numpy array
            data = np.fromstring(data, dtype=np.uint16)
            
            # keep track of the acquisition rate in samples-per-second
            count += self.chunkSize
            now = pg.ptime.time()
            dt = now-lastUpdate
            if dt > 1.0:
                # sps is an exponential average of the running sample rate measurement
                if sps is None:
                    sps = count / dt
                else:
                    sps = sps * 0.9 + (count / dt) * 0.1
                count = 0
                lastUpdate = now
                
            # write the new chunk into the circular buffer
            # and update the buffer pointer
            with dataMutex:
                buffer[self.ptr:self.ptr+self.chunkSize] = data
                self.ptr = (self.ptr + self.chunkSize) % buffer.shape[0]
                if sps is not None:
                    self.sps = sps
                
                
    def get(self, num, downsample=1):
        """ Return a tuple (time_values, voltage_values, rate)
          - voltage_values will contain the *num* most recently-collected samples 
            as a 32bit float array. 
          - time_values assumes samples are collected at 1MS/s
          - rate is the running average sample rate.
        If *downsample* is > 1, then the number of values returned will be
        reduced by averaging that number of consecutive samples together. In 
        this case, the voltage array will be returned as 32bit float.
        """
        with self.dataMutex:  # lock the buffer and copy the requested data out
            ptr = self.ptr
            if ptr-num < 0:
                data = np.empty(num, dtype=np.uint16)
                data[:num-ptr] = self.buffer[ptr-num:]
                data[num-ptr:] = self.buffer[:ptr]
            else:
                data = self.buffer[self.ptr-num:self.ptr].copy()
            rate = self.sps
        
        # Convert array to float and rescale to voltage.
        # Assume 3.3V / 12bits
        # (we need calibration data to do a better job on this)
        data = data.astype(np.float32) * (3.3 / 2**12)
        if downsample > 1:  # if downsampling is requested, average N samples together
            data = data.reshape(num/downsample,downsample).mean(axis=1)
            num = data.shape[0]
            return np.linspace(0, (num-1)*1e-6*downsample, num), data, rate
        else:
            return np.linspace(0, (num-1)*1e-6, num), data, rate
    
    def exit(self):
        """ Instruct the serial thread to exit."""
        with self.exitMutex:
            self.exitFlag = True


# Get handle to serial port
# (your port string may vary; windows users need 'COMn')
s = serial.Serial('/dev/ttyACM0')

# Create the GUI
app = pg.mkQApp()
plt = pg.plot()
plt.setLabels(left=('ADC Signal', 'V'), bottom=('Time', 's'))
plt.setYRange(0.0, 3.3)
            
# Create thread to read and buffer serial data.
thread = SerialReader(s)
thread.start()

# Calling update() will request a copy of the most recently-acquired 
# samples and plot them.
def update():
    global plt, thread
    t,v,r = thread.get(1000*1024, downsample=100)
    plt.plot(t, v, clear=True)
    plt.setTitle('Sample Rate: %0.2f'%r)
    
    if not plt.isVisible():
        thread.exit()
        timer.stop()

# Set up a timer with 0 interval so Qt will call update()
# as rapidly as it can handle.
timer = pg.QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)

# Start Qt event loop.    
if sys.flags.interactive == 0:
    app.exec_()

stimmer:
The line adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST); sets the prescal value - there is also ADC_FREQ_MIN and you can pass any value in between.

There is something I am missing.... if I want to set up an effective 20kHz or 50 KHz sampling rate using this method, what should I write?

The line adc_init(ADC, SystemCoreClock, 50000, ADC_STARTUP_FAST); feels wrong since ADC_FREQ_MIN is 1000000

Thank you