Go Down

Topic: speed of analogRead (Read 18436 times) previous topic - next topic

ssclark88

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?
Code: [Select]

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?

ralphnev

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

ssclark88

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.


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.

lcampagn


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 :)

Code: [Select]
#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.

Code: [Select]

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_()


PakARD



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

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.
Due VGA library - http://arduino.cc/forum/index.php/topic,150517.0.html

PakARD


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.

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 :)

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
Due VGA library - http://arduino.cc/forum/index.php/topic,150517.0.html

PakARD


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 :)

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.


DrRobertPatzke

#24
Jun 28, 2013, 07:56 pm Last Edit: Jun 28, 2013, 08:38 pm by DrRobertPatzke Reason: 1
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.

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 ??


madrang

#26
Jan 22, 2014, 06:29 pm Last Edit: Jan 22, 2014, 06:36 pm by madrang Reason: 1
Code: [Select]
 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.

Code: [Select]
while(obufn==bufn); // wait for buffer to be full

This line should be

Code: [Select]
while((obufn + 1)%4==bufn); // wait for buffer to be full

Also why start at buffer 1  ??
Code: [Select]
bufn=obufn=1;

Would be better to start at 0.
Code: [Select]
bufn=1;
obufn=0;


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

Adonisleal

Well spotted - what it should actually be is this:
Code: [Select]
    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 :)

Code: [Select]
#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?

Go Up