Hello, I've been researching ADC sampling on the Arduino, and after reading a lot I came with the following codes for the Arduino (in my case, a Nano clone with a CH340 serial-USB converter) and for a VERY SIMPLE 'scope' app on Processing (don't hit me too hard, I'm not a professional programmer in any way lol).
I'm using a sine wave ( 0.5 - 4.5Vpp) with variable frequency as input. After doing a lot of optimizations, I've been able to reach a reasonably good sampling of around 16 data points for each cycle of a 4kHz sine wave, which I think it's ok from what I've been reading. After that, there's too few data points and the sampling starts to distort and become unusable.
I'll state what I think it's happening on both sides based on the codes.
A) Arduino side: I disabled interrupts, programmed the USART to send 8N1 data, programmed the UART to send at 2Mbps, programmed the ADC prescaler to 16 (which I read it's the minimum speed without distorting conversion), and just 'spit' bits at the USART as soon as 1) the AD finishes converting, 2) The USART buffer is available:
#define BAUD 1000000
#define FCPU 16000000UL
#define BRC ((FCPU/16/BAUD) - 1)
void UART_Init(){
UBRR0H = (unsigned char)(BRC >> 8);
UBRR0L = (unsigned char) BRC;
bitSet(UCSR0A, U2X0);
UCSR0B = (0<<RXEN0) |(1<<TXEN0) | (0 << RXCIE0);
UCSR0C = (1 << UCSZ01) | (3<<UCSZ00);
}
int main() {
uint32_t ct;
cli();//interrupts disable
UART_Init(); // initialize the UART
// prescaler AD=16
ADCSRA = 0b10000100;
// Channel
ADMUX = 0b01000001;
//while(ct<=4096) {
while(1) {
ADCSRA |= 0b00010000;
ADCSRA |= bit(ADSC);
while( !( ADCSRA & 0b00010000));
while( !( UCSR0A & (1<<UDRE0)) );
UDR0 = (ADC >> 8) & 0x03;
while( !( UCSR0A & (1<<UDRE0)) );
UDR0 = (ADC & 0x00FF)+4;
//ct++;
}
}
A little explanation : on the last UDR0 = (ADC & 0x00FF)+4 line, I sum 4 to the value so I can differentiate which byte is the High AD byte (which ranges from 0 to 3 only), and the lower byte. I did that because sometimes, at this speed some data is just lost on the line, and when this happens it 'flips' the code on Processing to use high bytes as low and low as high. Doing this, I alway have a way to know which bytes are high (0 to 3) or low (anything else). Since I'm using a 4,5Vpp sine, I never reach 1024 as a value for the AD too.
B) Processing side: I create a 'data in' buffer and program the serial library to call the 'serial event' routine every 'chunkSize' bytes to fill this buffer; in parallel, I created a circular (ring) bigger buffer to receive the data from the 'data in' buffer, and I use this buffer to draw the samples on screen non-stop. Since the 'data in' and the ring buffer are being filled asynchronously, the program can deal with the speed the bytes are coming (2Mbps) without having to wait for the drawing, so everything that comes via serial is shown real-time on the screen:
import processing.serial.*;
Serial myPort; // The serial port
int dataBufferSize=32768;
int[] dataBuffer = new int[dataBufferSize];
int chunkSize=4096;
byte[] inBuffer = new byte[chunkSize];
int bytesRead=0;
int x, y, ly = 0;
int r1, r2 ,vh, vl;
int trgValue = 350;
boolean trg1=false;
int step=20;
long t1,t2;
int k, j, p, writeBufferPos=0;
void setup() {
myPort = new Serial(this, Serial.list()[2], 2000000);
myPort.buffer(chunkSize);
size(1024, 512);
background(0, 0, 0);
stroke(255);
frameRate(20000); // Obviously impossible, just to assure maximum screen refreshing
ly = height/2;
}
void draw() {
for (j=writeBufferPos; j<writeBufferPos+bytesRead; j=j+2) {
if (j < dataBufferSize) { p = j; } else { p = j - dataBufferSize; }
r1=dataBuffer[p];
if (p+1 < dataBufferSize) { r2=dataBuffer[p+1]; } else { r2=dataBuffer[0]; }
if (r1 >= 0 && r1 <=3 && r2>=4) { vh=r1; vl=r2; }
else if (r1 >= 0 && r1 <=3 && r2<0) { vh=r1; vl=r2+256; }
else if ((r2 >= 0 && r2 <=3 && r1>=4) || (r2 >= 0 && r2 <=3 && r1<0)) { j++; } // Discard sample
y = 512 - ((vh << 8 | vl) >> 1);
if (y > 511) { y=511; }
if (y < 0) { y=0; }
if (y == trgValue && y>ly) { trg1 = true; } // Very simple triggering, ramp-up
if ( trg1 == true) {
stroke(255, 255, 255);
for (p=1; p<=step; p++) { line(x+p, 0, x+p, 512); }
stroke(0, 0, 0);
line(x, ly, x+step, y);
stroke(255, 0, 0);
circle(x+step,y,3);
x=x+step;
}
ly=y;
if (x >= width) {
x = 0;
trg1 = false;
}
}
}
void serialEvent(Serial p) {
print("Serial Event called - buffer = "); println(chunkSize);
t1 = System.nanoTime();
bytesRead = myPort.readBytes(inBuffer);
for (k=0; k<bytesRead; k++) {
dataBuffer[writeBufferPos]=inBuffer[k];
writeBufferPos++;
if (writeBufferPos == dataBufferSize) {
writeBufferPos = 0;
t2 = System.nanoTime();
print("Circular buffer of "); print(dataBufferSize); print(" bytes filled in ");
print(t2-t1); println (" nanosec");
}
}
}
With all that, I've been able to achieve around 16 data points per cycle for a 4kHz sine wave, which I think it's pretty good as I already said. But...
There's scope projects like this one (fantastic project by the way, much respect to the coder (ZaidaTek) who did it):
Which claims to be able to achieve 100kHz sampling rate for a single channel, MUCH faster than my tests.
As far as I understand, he didn't do things too much differently at the Arduino side. In my limited understanding, the only place where things could be speed up are at windows side, but to me my buffer routines seen to be able to handle the data in speeds.
I'd appreciate if someone could give me some insights on why the speeds are SO MUCH different, and suggestions on what could be done to speed up my conversion. Thanks a lot!
Some measurings attached at 400Hz, 1kHz and 4kHz. The red circles are the data points drawn by the program (The attachments are a little bit distorted, but you can see a lot of red points at 400Hz and just a few (~16) points for each cycle at 4kHz).
Thank you!