Need help on increasing serial communication speed

I am trying to extract data from each analog microphone at >20kHz using an ATSAMD51. I have managed to set up the ADC and each reading from the analog pins takes 2us (500khz). The multiplexing takes less than1us (1Mhz). Attached is my current code.

However, the current speed is limited by the Serial.println() or Serial.write(), which takes ~100us (10khz). I am wondering if there is a walk-around or any other solutions. Thanks!

#define ADC_PIN A0

uint32_t sample_count = 0;
int one_sample =0;
long t0, t;
//uint16_t num = 1024;
String adcResults;
const int selectReadPins[4] = {2,3,4,5};  
 

void ADC_Init(void)
{
  // SETUP ADC - based on Adafruit M4 ADC setup routine
  //set to 1/(1/(48000000/4) * 6) = 2,000,000 SPS -- may be above the ratings of the chip ;) but working so far.. 
  //set to 1/(1/(48000000/8) * 6) = 1,000,000 SPS -- within ratings. 
  GCLK->PCHCTRL[ADC0_GCLK_ID].reg = GCLK_PCHCTRL_GEN_GCLK1_Val | (1 << GCLK_PCHCTRL_CHEN_Pos); //use clock generator 1 (48Mhz)
  GCLK->PCHCTRL[ADC1_GCLK_ID].reg = GCLK_PCHCTRL_GEN_GCLK1_Val | (1 << GCLK_PCHCTRL_CHEN_Pos); //use clock generator 1 (48Mhz)
  Adc *adcs[] = {ADC0, ADC1};
    for(int i=0; i<2; i++){

    adcs[i]->CTRLA.bit.PRESCALER = ADC_CTRLA_PRESCALER_DIV8_Val;  //set prescale
    adcs[i]->CTRLB.bit.RESSEL = ADC_CTRLB_RESSEL_10BIT_Val;   
    adcs[i]->CTRLB.bit.FREERUN = 1; 

    while( adcs[i]->SYNCBUSY.reg & ADC_SYNCBUSY_CTRLB );  

    adcs[i]->SAMPCTRL.reg = 1;                        

    while( adcs[i]->SYNCBUSY.reg & ADC_SYNCBUSY_SAMPCTRL ); 
    
    adcs[i]->INPUTCTRL.reg = ADC_INPUTCTRL_MUXNEG_GND;   

    while( adcs[i]->SYNCBUSY.reg & ADC_SYNCBUSY_INPUTCTRL );  

    // Averaging (see datasheet table in AVGCTRL register description)
    adcs[i]->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1 |    
              ADC_AVGCTRL_ADJRES(0x0ul);   

    while( adcs[i]->SYNCBUSY.reg & ADC_SYNCBUSY_AVGCTRL );  

    adcs[i]->CTRLA.bit.ENABLE = 1; //Enable ADC
  
    while(adcs[i]->SYNCBUSY.bit.ENABLE); 
  }
  analogReference( AR_DEFAULT ) ; // Analog Reference is AREF pin (3.3v)
}

void setup()
{
  ADC_Init();

  Serial.begin(691200);
  while(!Serial);

  Serial.println("Begin ...");

  for (int i = 0; i < 4; i++) {
    pinMode(selectReadPins[i], OUTPUT);
    digitalWrite(selectReadPins[i], LOW);
  }
}

void loop()
{
  for (int i=0; i < 2; i++){
     t0 = micros();
     selectChannelOut(i);
     //analogRead();
     ADC0->SWTRIG.bit.START = 1;
     while(!ADC0->INTFLAG.bit.RESRDY);
     Serial.write((uint8_t)(ADC0->RESULT.reg>>5));  
     Serial.write((uint8_t)(ADC0->RESULT.reg & 0x1F));
     //Serial.println(ADC0->RESULT.reg);
     t = micros()-t0;
     Serial.println((float)t);
  }
}


void selectChannelOut(int channel) {
    int r0 = channel & 1;
    int r1 = channel & 2;
    int r2 = channel & 4;
    int r3 = channel & 8;
    
    digitalWrite(selectReadPins[3], (r0==0?LOW:HIGH));
    digitalWrite(selectReadPins[2], (r1==0?LOW:HIGH));
    digitalWrite(selectReadPins[1], (r2==0?LOW:HIGH));
    digitalWrite(selectReadPins[0], (r3==0?LOW:HIGH));
}

Temporarily eliminate the serial printing to see if that speeds things up.
I seriously suspect your use of all the blocking loops is the problem. The "for" and "while" are ALL blocking so nothing else can happen during that time.
Paul

Hello Paul,

When I try to time it over the below loop (multiplexing + adc), it only takes 4 us, which indicates 2us for each reading even with the for loop. But when I include the Serial.println(), the time skyrockets to 100us. I am not sure how to avoid the Serial.println() for serial communication if eventually I would like to log the data to computer. Thanks!

 t0 = micros();
  for (int i=0; i < 2; i++){
     selectChannelOut(i);
     //analogRead();
     ADC0->SWTRIG.bit.START = 1;
     while(!ADC0->INTFLAG.bit.RESRDY);
     Serial.write((uint8_t)(ADC0->RESULT.reg>>5));  
     Serial.write((uint8_t)(ADC0->RESULT.reg & 0x1F));
     //Serial.println(ADC0->RESULT.reg);
 }
 t = micros()-t0;
 Serial.println((float)t);

See the problem? The ";" at the end of that line means the entire "while" does nothing but take up some of your time. Check your code for other instances of doing nothing.
Paul

It might be necessary to wait for, say, for example, the line to be steady.

Serial is quite a slow communication method.
The way some of the high-speed device (e.g. think oscilloscope) like ADALM1K communicate is evident when you use them. There is a significant amount of input lag, but every 0.5 second or so, the graph get refreshed and include datapoints every, say, 5us.
It might be possible to allocate buffers so you can fill up that buffer quickly but because of the Serial bottleneck, transfer the buffer slowly to the host. You can take measurements for like 0.1 second and transmit all that data for as long as you would like.

Why are you sending an integer as a float? That just adds extra characters and the time needed to convert to a float.

other methods would be to log the data on the device itself (e.g. using SD card transport) but that might also be slow. Especially since you now have to deal with the FAT32 file format.
The "burst" method sound the most logical (and feasible) without just jumping up and giving it a fat pipe communication.

You are attempting to send a MASSIVE amount of serial data, which very quickly FILLS the transmit buffer, at which point one more Serial.print will simply hang the processor until another character is sent, and a space opens in the buffer for the next character. This means you are spending MOST of the processors time waiting for space in the transmit buffer to open up, and during that time you CANNOT be taking more readings.

Bottom line, Serial is simply not fast enough to transmit so much data for any length of time. If each conversion takes 4uS, and you have 3 channels, each 10bits, you want to transfer 6 bytes every 12 uS, which is ~4 MBit/Sec. Your serial connection is only capable of a bit over 500kBit/sec, under ideal conditions. So, either slow down your data rate, or use something other than Serial, like USB.

1 Like

For such a HIGH-speed application I would recommend using a Teensy 4.0 which has 1MB of RAM.
You could code a big buffer.

If I remember right teensy's can be equiped with high capacitve RAM-modules.

If you want to have such a high datarate for a longer period of time a microcontroller is simply too small to do so. Minimum would be to change to a raspberry pi or a real computer running a realtime operating-system.

https://www.bitscope.com/blog/DI/?p=DI25A

best regards Stefan

Are you using one of the Adafruit Arduino-compatible boards, where "Serial" is actually the USB connection? Or some other hardware where you are actually using a UART?

On the one hand, the USB connection should be quite fast.
On the other hand, calls to Serial.print() for USB will have higher overhead because of the nature of USB, and writing two bytes with small pauses in between is pretty much the most pessimistic usage pattern. USB will also be more heavily dependent on the receiving program. You could probably do a lot better by aggregating your samples and doing a 64byte write once every 32 samples.

In the UART case, 2 bytes ever 4us is 5Mbps, and the actual transmission time at your weird 691200bps rate is close to 30us per sample. Even 4 samples at 2 bytes each at 20kHz is about 1.6Mbps. That's a lot of data, even if sent at maximum efficiency. You might want to look at faster communications protocols (something that bypasses the overhead of simulating a serial link over USB), or even a board with "high speed" USB rather than just "full speed." (Teensy 4 would be a good choice for that reason as well. And I think Paul at PJRC is more concerned about things like "peek USB performance" than the people at Arduino.)

If you take measurement for 0.1 second as said above like the ADALM1K, then the size start to become something a bit more manageable. something like ...

Assume 2us per read, so in 1000us (1ms) you will have 500 readings. Each reading (per channel) is 16 bit so that's 1KiB per ms, or 128B per ms.
Doing 100ms (0.1 second) you will have 128KB. Not the end of world, but definitely not convenient.

I have deliberately NOT mentioned one because transferring from the microcontroller to tke ram chip takes time and the bandwidth is very limited.
On the microprocessor itself, on the other hand, the processing nodes get a very high speed bus (perhaps) to the memory, which means very high speeds.

Yes. But basically he need to get this to work. And it's shown that USB 2.0 oscilloscopes exist and they can pull it off without any noticeable side effects aside from lag between change of signal and signal showing up on the software.

Yes it should. It just look like that the way most people write the library favors ease of use a lot more than performance, as they don't really bother about data once every hour or once every ten second.

UART is not known for their outstanding speed. They are known for their robustness.

SPI you can run them with your eyes closed at over 10MHz on devices like SAMD3X8E (arduino due)

Agree. sending the timing over serial is irrelevant as the serial monitor on arduino come with time stamp and a float is pretty big (32 bit i think) so that alone triples your serial timing.

The float is being converted to ASCII, and print defaults to two decimal places, so you are sending however many characters are needed for the actual number of microseconds, plus the decimal point and two zeros, as well as the carriage return and linefeed, so a minimum of six characters for a single-digit integer. There is also the time needed to convert the integer to a float, as well as the time to convert the float to ASCII, not as significant on an ATSAMD51 as on an UNO, but still needless.

     Serial.write((uint8_t)(ADC0->RESULT.reg>>5));  
     Serial.write((uint8_t)(ADC0->RESULT.reg & 0x1F));

That looks like you are sending the upper 3 bits in the first write, and the lower 5 bits in the second write, why not just send everything as a single byte and split it at the receiving end? Sending with two writes also introduces the complication of having to maintain sync between the sender and receiver.

Maybe if you don't assume the ADC0->RESULTS.reg is 8 bit (likely 10 bit or 12 bit, depend on actual resolution of the ADC) it would make more sense. In that case it would be necessary to split it between the MSB and LSB.
However, it seems very odd for it to be shifted only 5 bits rather than all 8 bits to create a 13 bit width instead of 16 bit (3 bits are duplicates). Bit shifting is very very fast (at least, compared to serial)

Ah, I was mistaken about the first write, didn't think of it shifting in additional bits, but it seems convoluted to not just send the entire lower 8 bits, then shift and mask off the upper 5. Easier still to just tell write to send two bytes.