0

I am using a Pro Mini to read analog values and to send them via serial.

Goal of the sketch is to stream these values to Serial as fast as possible, to obtain a steady stream of at least 1000 readings per second. For this reason I reduced the ADC prescaler and I increased serial speed.

I tested sending binary values with Serial.write and it works (it's the fastest), I tested Serial.print (it's the slowest), and I tested an optimised conversion integer-to-string (found on StackOverflow) in association with Serial.write.

This optimised conversion is faster than Serial.print (it would allow me to send the full strings within the given timeframe), but it gives me wrong values. I cannot understand why it happens, the code looks fine to my not-so-expert eye.

A correct stream as sent by Serial.print would be (spaces added for clarity):

3075 \t 2800 \t 4132 \n

"Wrong values" means that the last value gets repeated for all the three columns:

4132 \t 4132 \t 4132 \n

What is the issue in my code regarding the int2str conversion?

Since I cannot get one measurement taken and sent every 500 us, I would like to use the optimised routine to get plaintext readings at 1000 us intervals.

The source code follows. I cut out the function slimADC because it was tested as working, while I left all the 3 methods for sending data via serial: binary, optimised, Serial.print.

#include <digitalWriteFast.h>

volatile unsigned long previousMicros, currentMicros;

#define SAMPLES 8
volatile unsigned int valuesX;
volatile unsigned int valuesY;
volatile unsigned int valuesZ;

char* x;
char* y;
char* z;

void setup() {
// set pins, ADC prescaler and initialise ADC
  pinModeFast(A0, INPUT);
  pinModeFast(A1, INPUT);
  pinModeFast(A2, INPUT);
  ADCSRA &= ~(bit (ADPS0) | bit (ADPS1) | bit (ADPS2)); // clear bits
  ADCSRA |= bit (ADPS0) | bit (ADPS2);  // 32: 32 us per ADC
  slimADC(0);
  slimADC(0);

// initialise Serial
  Serial.begin(250000);
  Serial.write("\n");
  Serial.write("\n");
  previousMicros = 0;
}

void loop() {
  currentMicros = micros();
  if ((currentMicros - previousMicros) < 1000) { return; }
  previousMicros = currentMicros;

// ADC readings, repeated SAMPLES times
  valuesX = 0;
  valuesY = 0;
  valuesZ = 0;
  for (uint8_t i = 1; i < SAMPLES; i++) {  // 8*3*32=768 us
    valuesX += slimADC(0);
    valuesY += slimADC(1);
    valuesZ += slimADC(2);
  }

// SERIAL TRANSMISSION

// fastest
//  Serial.write(valuesX / 256);
//  Serial.write(valuesX % 256);
//  Serial.write("\t");
//  Serial.write(valuesY / 256);
//  Serial.write(valuesY % 256);
//  Serial.write("\t");
//  Serial.write(valuesZ / 256);
//  Serial.write(valuesZ % 256);
//  Serial.write("\n");

// good
  x = int2str( valuesX );
  y = int2str( valuesY );
  z = int2str( valuesZ );
  Serial.write(x);
  Serial.write("\t");
  Serial.write(y);
  Serial.write("\t");
  Serial.write(z);
  Serial.write("\n");

// slowest
//  Serial.print(valuesX);
//  Serial.print("\t");
//  Serial.print(valuesY);
//  Serial.print("\t");
//  Serial.print(valuesZ);
//  Serial.print("\n");
}

static int slimADC(uint8_t pin) { ... }

char _int2str[7];
char* int2str( register int i ) {
  register unsigned char L = 1;
  register char c;
  register boolean m = false;
  register char b;  // lower-byte of i
  // negative
  if ( i < 0 ) {
    _int2str[ 0 ] = '-';
    i = -i;
  }
  else L = 0;
  // ten-thousands
  if( i > 9999 ) {
    c = i < 20000 ? 1
      : i < 30000 ? 2
      : 3;
    _int2str[ L++ ] = c + 48;
    i -= c * 10000;
    m = true;
  }
  // thousands
  if( i > 999 ) {
    c = i < 5000
      ? ( i < 3000
          ? ( i < 2000 ? 1 : 2 )
          :   i < 4000 ? 3 : 4
        )
      : i < 8000
        ? ( i < 6000
            ? 5
            : i < 7000 ? 6 : 7
          )
        : i < 9000 ? 8 : 9;
    _int2str[ L++ ] = c + 48;
    i -= c * 1000;
    m = true;
  }
  else if( m ) _int2str[ L++ ] = '0';
  // hundreds
  if( i > 99 ) {
    c = i < 500
      ? ( i < 300
          ? ( i < 200 ? 1 : 2 )
          :   i < 400 ? 3 : 4
        )
      : i < 800
        ? ( i < 600
            ? 5
            : i < 700 ? 6 : 7
          )
        : i < 900 ? 8 : 9;
    _int2str[ L++ ] = c + 48;
    i -= c * 100;
    m = true;
  }
  else if( m ) _int2str[ L++ ] = '0';
  // decades (check on lower byte to optimize code)
  b = char( i );
  if( b > 9 ) {
    c = b < 50
      ? ( b < 30
          ? ( b < 20 ? 1 : 2 )
          :   b < 40 ? 3 : 4
        )
      : b < 80
        ? ( i < 60
            ? 5
            : i < 70 ? 6 : 7
          )
        : i < 90 ? 8 : 9;
    _int2str[ L++ ] = c + 48;
    b -= c * 10;
    m = true;
  }
  else if( m ) _int2str[ L++ ] = '0';
  // last digit
  _int2str[ L++ ] = b + 48;
  // null terminator
  _int2str[ L ] = 0;  
  return _int2str;
}

Appendix

The bare ADC conversions take about 768 us.

With binary data I am sending via serial 3x uint16_t plus one end of line and two tabs, meaning 9 characters per reading. At 250 kbps 8N1 the minimum time required is 324 us. Using RealTerm (for Win) I can see that I get a stram of 9k chars/s, as expected.

With Serial.print I am sending 4x string[4] (readings are always between 1000 and 9999), plus tabs and newline, meaning 15 bytes per reading. RealTerm however gives me an incoming stream of 11-12k chars/s, therefore Serial.print is too slow.

The optimised int-to-string function (even without modifying it to avoid checking for negative and for >9999, not needed in my case) still sends 15 bytes per reading, and RealTerm also shows me an actual stream of 15k chars/s, as expected.

7
  • Here is a benchmark that you might find interesting; github.com/mikaelpatel/Cosa/blob/master/examples/Benchmarks/… It tries to answer the question if 4K ADC samples (and number conversions) per second is possible. Jan 22, 2017 at 18:26
  • Well I already have 24 samples (768 us) and 3 number conversions (not sure how long they take) per millisecond. Not that different. I could probably double the conversions if I reduce the number of samples (I'm taking 8 per channel), but first I want to get the rest right :) But thanks for sharing, I starred that project for future reference. I will study it if possible.
    – FarO
    Jan 22, 2017 at 21:42
  • @MikaelPatel yep, with 4 samples per acquisition and using a binary stream I get 18k chars received on the PC side for a 500 us period. It's 24 ADC conversions (32*3*4=384 us) and 18 chars sent (9*10/250000=360 us) per millisecond!
    – FarO
    Jan 22, 2017 at 22:21
  • Unfortunately with such fast serial I cannot afford to use ADC noise reduction (it would stop the IO clock used by USART), but it's not important right now.
    – FarO
    Jan 22, 2017 at 22:26
  • 1
    > It's 24 ADC conversions (32*3*4=384 us). try to run the adc in free-running model. you can enable the adc interrupt so the data is automatically saved in a buffer for you.
    – dannyf
    Jan 22, 2017 at 23:17

1 Answer 1

2

it took me a while and i'm still not 100% sure if I got you right. I think you have two questions:

1) how to speed up the entire execution?

the serial routines will be your biggest bottleneck. I would suggest that a) you speed up the baud rate; and b) you try to use a tx interrupt here.

2) why isn't the int2str() routine working?

because int2str() stores the converted string in a buffer (_int2str[]), which is subsequently destroyed by conversions. so for it to work, you should do something like this:

  serial.write(int2str(x)); serial.write("\t");
  serial.write(int2str(y)); serial.write("\t");
  serial.write(int2str(z)); serial.write("\n");

BTW, int2str() can be written a lot better than it is now.

edit: to add an alternate int2str() implementaton.

//positive and negative numbers
//int is 16-bit type
char *int2str2(int i) {
    static char _int2str[7];
    unsigned char tmp, str_len=0;       //converted string starts at _int2str[0]

    //process the sign
    if (i&0x8000/*<0*/) {
      _int2str[str_len++]='-';          //insert leading '-'
      i=-i;                             //make the number positive
    };

    //convert the next position (10,000)
    tmp = 0;                            //reset tmp
    while (i >= 10000) {
      i-=10000;                         //decrement i
      tmp+=1;                           //increment counter
    }
    _int2str[str_len++] = tmp + '0';    //form the msb

    //convert the next position (1,000)
    tmp = 0;                            //reset tmp
    while (i >= 1000) {
      i-=1000;                          //decrement i
      tmp+=1;                           //increment counter
    }
    _int2str[str_len++] = tmp + '0';    //form the msb

    //convert the next position (100)
    tmp = 0;                            //reset tmp
    while (i >= 100) {
      i-=100;                           //decrement i
      tmp+=1;                           //increment counter
    }
    _int2str[str_len++] = tmp + '0';    //form the msb

    //convert the next position (10)
    tmp = 0;                            //reset tmp
    while (i >= 10) {
      i-=10;                            //decrement i
      tmp+=1;                           //increment counter
    }
    _int2str[str_len++] = tmp + '0';    //form the msb

    //convert the lsb
    //tmp = 0;                          //reset tmp
    //while (i >= 100000ul) {
    //  i-=100000ul;                        //decrement i
    //  tmp+=1;                         //increment counter
    //}
    tmp = i;
    _int2str[str_len++] = tmp + '0';    //form the msb

    return _int2str;
}

here is what I would use. I benchmark against yours, converting numbers from 0 to 32767. total cycle count for yours is 10.5 million and 7.0 million for mine. Now, the actual performance will depend on a lot of factors, like your chips, compilers (I used IAR AVR), and optimization settings, to name a few.

Note: the routine was written as a generic one. if you are only converting 10-bit numbers (from a 10-bit adc for example), it can be greatly simplified, and execution time greatly shortened: <160K cumulative cycles converting numbers from 0 to 1023.

3
  • Part 2) was my actual question and your explanation is clear, I corrected my code and accepted your answer. Part 1) was more side information I put there to let the reader understand what I'm doing, I didn't expect any answer or significant improvements there. But thanks for letting me know about TX interrupts. Isn't Arduino using hardware UART (and interrupts) already? see arduino.cc/en/Reference/softwareSerial Concerning int2str: it's using basically the bisection method, I thought it was already quite efficient. Do you have a link for a better version? I'm quite beginner.
    – FarO
    Jan 22, 2017 at 21:53
  • 1
    the arduino uses the hardware uart module in the avr. but uses polling in its transmission -> they want for the prior transmission to end before sending the next. the usr of isr would have allowed the mcu to do other things which uart is transmitting.
    – dannyf
    Jan 22, 2017 at 22:54
  • I understand how your conversion routine works, but I'm not sure why it is faster. On average you have 5 comparisons, 11 sums, 11 value assignments per decade, while the old one has 3 comparison, 2 sums, 1 multiplication (hardware in ATmegas) and 3 assignments. On my atmega328p I measured (0 to 32767) 311072 us for the old one and 325656 us for yours. avr-gcc 4.9.2 and following options: -Os -g -flto -fuse-linker-plugin -Wl,--gc-sections -mmcu=atmega328. I guess that on ATtiny yours would be faster, since hardware multiplication is missing.
    – FarO
    Jan 22, 2017 at 23:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.