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.
#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.
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)
