Go Down

Topic: Arduino Due - ADC->DMA: Channel ordering in buffer (Read 824 times) previous topic - next topic

baker93

Hi,

I'm new to Arduino (and embedded programming in general) and am using the Due for a project at work. I am using slightly modified code from the user stimmer to make the Due a data acquisition device for two analog inputs, utilizing DMA and USB. I've also modified stimmer's python code slightly to export the transferred data to csv.

Am I modifying the arduino code correctly for two channels?
How do I know what data point in the transferred data corresponds to which analog input channel (A0 or A1)?

As an experiment, I plugged in a wire to A0 which introduced significant noise to only A0. I plotted the output data from the python script and it appears that the ordering of signal channels in the buffer is strangely uneven. The total samples within the period are the same between noise and no-noise (A0 vs A1) which makes sense, but I don't know why the A0 signal is split between two uneven chunks yet the A1 signal is split evenly. Any insight? -- see attached plot


My goal is to be able to parse the incoming signal from the DUE in python by analog input channel (A0, A1) and save it to a csv. 


Arduino Due code
I modified stimmer's code to activate A0 and A1. I also increased the size of the buffer from 256 to 512, seeing as how I now have two channels.

Code: [Select]

#undef HID_ENABLED

// Arduino Due ADC->DMA->USB 1MSPS
// by stimmer
// from http://forum.arduino.cc/index.php?topic=137635.msg1136315#msg1136315
// Input: Analog in A0
// Output: Raw stream of uint16_t in range 0-4095 on Native USB Serial/ACM
#define NUM_CHANNELS 2
#define ADC_CHANNELS ADC_CHER_CH7 | ADC_CHER_CH6;
#define BUFFER_SIZE 256*NUM_CHANNELS

volatile int bufn,obufn;
uint16_t bufferLength = BUFFER_SIZE;
uint16_t buf[4][BUFFER_SIZE];   // 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=BUFFER_SIZE;
  }
}

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

  ADC->ADC_CHER=ADC_CHANNELS;

  NVIC_EnableIRQ(ADC_IRQn);
  ADC->ADC_IDR=~(1<<27);
  ADC->ADC_IER=1<<27;
  ADC->ADC_SEQR1=0x00110011; // Trying to control order of conversions. Did not affect placement in buffer
  ADC->ADC_RPR=(uint32_t)buf[0];   // DMA buffer
  ADC->ADC_RCR=BUFFER_SIZE;
  ADC->ADC_RNPR=(uint32_t)buf[1]; // next DMA buffer
  ADC->ADC_RNCR=BUFFER_SIZE;
  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],BUFFER_SIZE*2); // send it - 2 bytes = 1 uint16_t
  obufn=(obufn+1)&3;   
}


Python code
I am downsampling by 500 to keep the file sizes manageable.

Code: [Select]


import time, threading, sys
import serial
import numpy as np
#import pdb

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*2, chunks=5000): #chunkSize is 1024, should i adjust to 2028 for 2 channels?
        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 logData(self, filename):
        fid = open(filename,"w+")
        return fid   
   
    def closeLog(self, fid):
        fid.close()   
   
    def writeToLogTime(self, v, fid):
        toWrite = str(v) + ','
        fid.write(toWrite)

    def writeToLogData(self, v, fid):
        toWrite = ','.join(map(str,v)) + '\n'
        fid.write(toWrite)
   
    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('COM5')

# Create thread to read and buffer serial data.
thread = SerialReader(s)
thread.start()
loghandle = thread.logData('test_500.csv')

# Calling update() will request a copy of the most recently-acquired
# samples and plot them.
def update():
    global thread, loghandle
    t,v,r = thread.get(1000*1024*2,downsample=500) #1000*1024, switching to 1000*2048
   
    return t,v,r


for i in range(0,500):
    t,v,r = update()
    if v[0] == 0:
        continue
       
    thread.writeToLogTime(int(time.time()*1000.0),loghandle)
    thread.writeToLogData(v,loghandle)
   
thread.exit()
thread.closeLog(loghandle)




ard_newbie


What ADC frequency is required for your project ?

baker93

I need to sample each channel at a minimum of 20kS/s. I would prefer to have a higher rate so I have more data to work with, but that's the base rate I need.

ard_newbie


20 K * 2 channels = 40 K Hz ----> There is no need to set the free running mode.

Trigger ADC conversions at 20 KHz (or higher) with a Timer Counter, enable 2 ADC channels, fill a buffer with a PDC DMA, set a Flag in ADC interrupt handler once a buffer is full so that the loop() knows that a SerialUSB.write() can output this buffer (make some tryings to find the right buffer size because SerialUSB is far from optimized, or use UART with a DMA).

You will find an example sketche for ADC conversions triggered at a precise frequency in one of the recent threads in the DUE sub forum. (BTW it's a good idea to avoid magic numbers).

From the PC side, I suspect that it's better to synchronise readings with the same size as writings (e.g. 256). Moreover SerialUSB is really fast and maybe too fast for your PC to be handled correctly.

Go Up