speed of analogRead

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

ADC_FREQ_MAX/MIN is the frequency of the ADC clock, not the sample rate. The ADC takes about 20 clocks to do a conversion (maybe exactly 20 clocks, I can't remember), so the conversion rate by this method is 50000-1000000 samples per second. To get a slower sample rate using free-running mode you could sample some channels you don't need to slow things down. But the proper way to do it is to use a timer triggered mode instead of free-running mode.

stimmer:
ADC_FREQ_MAX/MIN is the frequency of the ADC clock, not the sample rate. The ADC takes about 20 clocks to do a conversion (maybe exactly 20 clocks, I can't remember), so the conversion rate by this method is 50000-1000000 samples per second. To get a slower sample rate using free-running mode you could sample some channels you don't need to slow things down. But the proper way to do it is to use a timer triggered mode instead of free-running mode.

I see, but then is every analogread sampled in the timer interrupt? How does DMA Work with the timer?
I don't get it.

Thank you again.

It's not an interrupt, the ADC is internally wired to the timer and can use it to trigger a conversion. You can use it with DMA, it works just as you'd expect - for example if you set the timer to 1000Hz and ask the DMA to do 100 conversions you get 100 samples at a sample rate of 1000 samples/sec :slight_smile:

I used this technique in the DueVGA Waterfall example but I haven't properly tidied up the code and commented it yet so it is hard to follow.

stimmer:
It's not an interrupt, the ADC is internally wired to the timer and can use it to trigger a conversion. You can use it with DMA, it works just as you'd expect - for example if you set the timer to 1000Hz and ask the DMA to do 100 conversions you get 100 samples at a sample rate of 1000 samples/sec :slight_smile:

I used this technique in the DueVGA Waterfall example but I haven't properly tidied up the code and commented it yet so it is hard to follow.
https://github.com/stimmer/DueVGA/blob/master/extras/Waterfall/waterfall/waterfall.ino

Thank you again Stimmer. Nice piece of code.

This is working as it should (I tested it at 50kHz). The nice thing about this way is that you don't need to worry about the sampling rate when using more than one channel....just multiply the buffer size by the number of them.

I found some hints to that speed issues in Atmel's doc11106.pdf, Analog-to-Digital Converter in the SAM3S4 (searching for such a document addressing SAM3X was not successful).
They say, that STARTUP is needed for SLEEP mode and Fast Wake Up (FWUP) mode, because the ADC needs this time to get ready for conversation from SLEEP (40 us) and from FWUP (12 us) to normal mode.
As far as I understood, in normal mode there is no need for STARTUP (value may be 0).
There is a formula to calculate the sampling frequency Fs with Fadc = 1/ADC_CLOCK (programmed via PRESCAL).
Fs = Fadc/(n(STARTUP) + Tconv) with Tconv = 20 (ADC clock cycles).
This has to be devided by the number of used channels.
n(STARTUP) is not linear, it is given as a list of values in the data book of SAM3X:
0/0, 1/8, 2/16 .... 10/640 ... 12/768 ... 15/960.

It looks like as other parameters (TRANSFER, SETTLING) happen with a channel while another channel is converted, so there is no influence on the sampling frequency. TRACKTIM is a delay between conversions and may be 0.

I hope, I've read the description in doc11106.pdf correct. If I made mistakes by repeating, someone should tell us the truth.

A correction:
SETTLING and TRANSFER is not overlapping conversion, only the Tracking. Look at Figures 44-2 and 44-3 (page 1333) in the User Guide of SAM3X. Seems, that TRANSFER and SETTLING is inluded in the 20 ADC clock cycles mentioned for conversion time.
But I did not really study all the text, may be, I'll do that later, now I want to make progress with my software.

i use arduino DUE, A0 to GND, A1 to 3V3,
run STIMMER 2 analog in code
and test with arduino 1.5.4 OR arduino 1.5.5. r2 with FIX
see 500kHz OR 333kHz ??

  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;

I'm not sure, but it look like the Due is using buffer 0 for writing and reserving buffer 1 as the next buffer to be used.
If it is the case there is 2 buffer in use.

while(obufn==bufn); // wait for buffer to be full

This line should be

while((obufn + 1)%4==bufn); // wait for buffer to be full

Also why start at buffer 1 ??

bufn=obufn=1;

Would be better to start at 0.

bufn=1;
obufn=0;

Not sure if i understood the code properly, i hope someone can tell me more about this.

stimmer:
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.

Hi Stimmer

I'd like to know , how can I see the Digital signal using SerialUSB, Can I use the Serial Terminal of Arduino IDE?

kllsamui:
i use arduino DUE, A0 to GND, A1 to 3V3,
run STIMMER 2 analog in code
and test with arduino 1.5.4 OR arduino 1.5.5. r2 with FIX
see 500kHz OR 333kHz ??

I'm using Arduino 1.5 and I'm also getting only 333kHz sampling frequency with two channels.
Does anybody know what might be the reason?

Without your code, we can't speculate on the reason. Have you tried the code in reply #18 which claims to achieve 1MHz?

My application will be monitoring two analog channels. I have Arduino Zero which is the same family as the Due. I will need the free-running mode or better yet DMA. I tried copying the code examples but get errors on the compile. Are there library references I need to include to get the compile to run?

My application will be monitoring two analog channels. I have Arduino Zero which is the same family as the Due.

Yes but it is not the same.
I suggest you start your own thread in the appropriate section of the forum.

As I am trying to do something similiar, I tried to follow along your code with the datasheet of the SAM3X and I could not find some of the registers you use, such as ADC->ADC_RPR. From your code I guess that a pointer towards memory is stored there and the ADC uses this address to store the conversion result there using DMA. As I do want to send out the PIOC Register via USB instead of an ADC conversion result I would be happy to know where to find the registers you use to fully understand whats happening.

Thanks a lot,

Laura