3

I'm trying to use Arduino as a DAQ but the Serial.print() is not working as fast as I expected. How many bits are sent if you are using a int, because if I use a uint8_t, it's taking the same amount of time? How I could improve the speed of the Serial.print()? I'm already working at 115200 baud.

7
  • What does it mean "slow" how many ms? – Avamander Feb 4 '16 at 13:58
  • Its take about 204 us and its supposed to go less than that but know I know it's sending ASCII so it's sending more bits than I thought. – Arnau Perez Feb 4 '16 at 15:05
  • How many channels are you acquiring? See my modified answer for more comments – frarugi87 Feb 4 '16 at 15:48
  • I'm already taking samples at 50 kHz and the resolution is enough and Im acquiring data from all 6 analog pins. And what version of "timerone.h" is better? – Arnau Perez Feb 4 '16 at 15:55
  • Please see github.com/mikaelpatel/Cosa/blob/master/examples/Benchmarks/… this might give some hints on how to boost the performance. – Mikael Patel Feb 4 '16 at 16:05
4

I need a sample rate of 6 kHz

OK, so that means you need to sample and send every 1/6000 seconds (167 µs).

I have a lengthy discussion about the ADC hardware. Amongst other things is a table of conversion times for the different ADC prescalers, assuming a 16 MHz clock:

Prescaler
    2    * 13 * 1/16E6  = 0.000001625 (  1.625 µs)
    4    * 13 * 1/16E6  = 0.00000325  (  3.25  µs)
    8    * 13 * 1/16E6  = 0.0000065   (  6.5   µs)
   16    * 13 * 1/16E6  = 0.000013    ( 13     µs)
   32    * 13 * 1/16E6  = 0.000026    ( 26     µs)
   64    * 13 * 1/16E6  = 0.000052    ( 52     µs)
  128    * 13 * 1/16E6  = 0.000104    (104     µs)

If you go for maximum resolution (10 bits) you need a prescaler of 128, which takes 104 µs. The number of conversions you can do a second (taking the inverse of the above) is:

Prescaler  Conversions/sec
  2           615,385
  4           307,692
  8           153,846
 16            76,923
 32            38,462
 64            19,231
128             9,615

You also need to take into account the transmission time. At 115200 baud you can transmit one byte every 1/11520 seconds (86.8 µs) so therefore two bytes (16 bits) would take 173.6 µs.

If you sample then transmit you will therefore take, per sample:

104 + 174 = 278 µs

This is too slow if you want to achieve it inside 167 µs.


You can sample asynchronously (in the background) so you transmit one sample while taking the next one, but you are still limited by the transmission speed of serial, so it would still take 174 µs per sample.

In fact, fiddling with the prescaler isn't really the issue here because the serial transmission time is the limiting factor.

Forget about the ADC and how it works for the moment. At 115200 baud you can't transmit 2 x 6000 bytes per second. That is simply because the maximum is 11520 bytes per second.

2
  • 2
    Actually, the serial library (of course I'm speaking about the hardware one) sends the data asynchronously, in the serial ISR. So serial transmission and main loop (with the ADC acquisition) can be considered as two "parallel" tasks. So the sample time will be max(104,174), so 174us. Of course there is a bit of overhead (go to ISR, exit from ISR, perform ISR) but as a first approximation the sample rate here is not the sum of the two, but the highest – frarugi87 Feb 5 '16 at 9:41
  • 2
    You are quite right, of course! (What was I thinking when I wrote that?) - still we are both in agreement that the limiting factor is the baud rate. The Arduino hardware is capable of faster baud rates, I'm not sure about the receiving side. – Nick Gammon Feb 5 '16 at 10:03
2

By not using Serial.print?

Serial.print will format the number as a string of ASCII characters. The number of characters depends on the size of the number. Yes, that is very wasteful.

If you are sending values between 0 and 255 (uint8_t) you can just send the raw value as a single ASCII character, since there are 256 of those.

Serial.write(myVal);

For anything greater than 255, i.e., you want to send an int or uint16_t then you will have to come up with some better way. You can't send one byte for the full value, so you would have to send it as two bytes:

Serial.write((myVal >> 8) & 0xFF);
Serial.write(myVal & 0xFF);

HOWEVER... how do you know which byte coming in is which? It's very very easy to get out of sync and not get the right values when you read the data in. So you need some way of "packetizing" the data so that you know "This is a correct set of values". Just how you do that is entirely up to you - do you just wrap each pair of bytes in a special set of other bytes? Nah - that becomes as wasteful as Serial.print. Instead it's better to buffer up a batch of values and send them as a block with just a few extra bytes to define the packet.

One example of doing that is my ICSC library. Take a look at it and you can see how you can craft a packet to send lots of data as one stream of data.

2
  • In fact, I'm using 10 bits in the analogread() but I can't give up that resolution in orde to get more speed. – Arnau Perez Feb 4 '16 at 15:03
  • 1
    The 10th bit is usually just quatisation noise, so you only have 9 bits of real information. So you would in fact only be losing 1 bit of information by going from 10 to 8 bits. – Majenko Feb 4 '16 at 16:01
1

Serial.print sends the ASCII representation of the data. So:

uint8_t data8 = 102;
uint32_t data32 = 102;
char datastring[] = "102";
Serial.print(each_of_the_previous_data);

each of these are sent exactly in the same way, i.e. three bytes ('1', '0', '2', so 0x31 0x30 0x32).

This, as you noticed, is highly inefficient. What I suggest you is to send them in binary mode, so

uint8_t *convertToByteArrayLittleEndian(uint8_t *buffer, int32_t val)
{
    uint8_t i;
    for (i = 0; i < 4; i++)
    {
        buffer[i] = val & 0xFF;
        val = val >> 8;
    }
    return buffer;
}
uint8_t *convertToByteArrayBigEndian(uint8_t *buffer, int32_t val)
{
    uint8_t i;
    for (i = 0; i < 4; i++)
    {
        buffer[3-i] = val & 0xFF;
        val = val >> 8;
    }
    return buffer;
}

uint8_t supportArray[4];
uint8_t data8 = 102;
uint32_t data32 = 102;
char datastring[] = "102";
Serial.write(data8); // Sends 0x64 = 100
Serial.print(convertToByteArrayLittleEndian(supportArray,data32),4); // Sends 0x64 0x00 0x00 0x00
Serial.print(convertToByteArrayBigEndian(supportArray,data32),4); // Sends 0x00 0x00 0x00 0x64
Serial.print(datastring); // Sends 0x31 0x30 0x32

I had to use an helper function to convert from int to byte array, since Serial.write does not support directly an int. You can choose if you want to send the data in big endian or little endian format.

As you can see, in binary mode the amount of data exchanged varies.

Just three more things.

1) you probably can go higher in frequency (maybe 1Mbit/s) with the serial. Just try it out

2) If you decide to use binary mode, maybe you will need some synchronization (i.e. a start byte) to ensure you are not listening a stream from a "middle" point. There are various techniques (prevent use of some bits in the stream, use some forbidden values and escape it)...

3) If you want to go faster, I suggest you to switch peripheral. For instance use SPI (of course you won't be able to use a common USB-UART interface, but you can create a native USB dongle with an SPI interface to get the connection)

EDIT:

According to other info from the OP:

The data is 10 bits. Let's split it into two 5bits data, so we can use the upper 3 bits for sending sync info (to understand which byte comes first). The required rate is 6kHz.

I made some calculations, and it looks like that you can use this code:

#include "TimerOne.h"

bool timerHasBeenTriggered = false;

void setup()
{
    Timer1.initialize(167);         // initialize timer1, and set a 6kHz frequency
    Timer1.attachInterrupt(callback);
    Serial.begin(#HIGHER_THAN_115200#);
}

void callback()
{
    timerHasBeenTriggered = true;
}

void loop()
{
    int16_t val = analogRead(ANALOG_PIN); // takes 104us
    Serial.write((val & 0x1F) | 0xA0); // Send lower 5 bits, upper 3 set to 101
    Serial.write((val >> 5) & 0x1F); // Send lower 5 bits, upper 3 set to 000
    while (!timerHasBeenTriggered);
    timerHasBeenTriggered = false;
}

Here the loop has an execution time of around 104us (serial calls just store the values in a queue). The vast majority of the time is spent acquiring from a channel.

I used a timer to trigger the next acquisition so it can be more precise (and the millis() function is useless, since you want to trigger it 6 times per millisecond).

From the PC you will receive two bytes. The first one is in the form 0b101XXXXX, the second 0b000YYYYY. You will only have to

  1. wait for a byte starting with 101
  2. save the other 5 bits (XXXXX)
  3. get the next byte and check it starts with 000
  4. the data will be just the concatenation of the two 5-bits chunks: YYYYYXXXXX

Note that the limiting factor of the timing is the analogRead function. If you want to go faster (or to acquire from multiple channels) you will need to change the ADC clock prescaler. This, however, will make your ADC frequency higher than 200kHz, which is not recommended (you will get less precise data).

3
  • But if I do all of that it should be done for each sample? I need a sample rate of 6 kHz and I'm not really sure if is that posible. – Arnau Perez Feb 4 '16 at 14:56
  • 6 kHz only, or 6 kHz times 6 pins? – Nick Gammon Feb 5 '16 at 3:51
  • What do you mean by "each sample"? you will be able to acquire from just ONE analog channel, since EACH analog reading takes about 100us. Moreover serial baud rate needs to be higher than 115200, since with 115200 you will be able to send two chars in 174us, which means 5.7kHz. It doesn't matter how much faster, since sending and reading are performed in parallel.. – frarugi87 Feb 5 '16 at 9:38
1

If your signal of interest is of short-enough duration, you might get by by collecting the data into memory, then dumping it at whatever speed you can. If you're using an Uno or other AtMega328 based Arduino, the RAM size, and therefore, your buffer, will be quite limited. Storing the first value as int and subsequent values as one-byte differences can help.

An on-board memory device, like an FRAM, could buffer 8K or 32K bytes (or half as many ints).

Or the same differences technique can be used to send one-byte binary differences and improve your sample transmission rate. It's easy to get out of sync, though. This won't be too reliable for continuous or long data streams.

Otherwise, you're just baud-bound. How fast can you drive the serial output and the receiver still be able to keep up? You're pushing the edges of what the device can do. It's an interesting problem and possibly fun to solve, but I'm reminded of the old adage about alligators and needing to drain the swamp! :)

1

Some benchmarking of Serial.print() and comparing to clib functions (itoa/ltoa) might give some insight. First, printing int and long int with DEC base using Arduino core Print functions (Arduino IDE 1.6.7, Arduino Mega2560).

Serial.print(12345, DEC): 240 us
Serial.print(12345678L, DEC): 388 us

Second, use HEX base.

Serial.print(0x1234, HEX): 204 us
Serial.print(0x12345678L, HEX): 384 us

Third, use the AVR clib functions to convert number to a string and then print the string. 10 and 16 base.

Serial.print(itoa(12345, buf, 10)): 104 us
Serial.print(ltoa(12345678L, buf, 10)): 240 us

Serial.print(itoa(0x1234, buf, 16)): 80 us
Serial.print(ltoa(0x12345678L, buf, 16)): 240 us

Cheers!


Below is the test sketch:

void setup()
{
  uint32_t start, stop, us;
  char buf[16];
  Serial.begin(9600);

  start = micros();
  Serial.print(12345, DEC);
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(12345, DEC): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

  start = micros();
  Serial.print(12345678L, DEC);
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(12345678L, DEC): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

  start = micros();
  Serial.print(0x1234, HEX);
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(0x1234, HEX): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

  start = micros();
  Serial.print(0x12345678L, HEX);
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(0x12345678L, HEX): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

  start = micros();
  Serial.print(itoa(12345, buf, 10));
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(itoa(12345, buf, 10)): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

  start = micros();
  Serial.print(ltoa(12345678L, buf, 10));
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(ltoa(12345678L, buf, 10)): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

  start = micros();
  Serial.print(itoa(0x1234, buf, 16));
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(itoa(0x1234, buf, 16)): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

  start = micros();
  Serial.print(ltoa(0x12345678L, buf, 16));
  stop = micros();
  us = stop - start;
  Serial.println();
  Serial.print(F("Serial.print(ltoa(0x12345678L, buf, 16)): "));
  Serial.print(us);
  Serial.println(F(" us"));
  Serial.flush();

}

void loop()
{
}

The clib itoa/ltoa are regarded to be relative slow and there are proposals to improve performance. Please see, for instance, this bug report.

1
  • The bug report is interesting, but it looks to me like a speed/size trade-off: bigger code and faster execution. It is quite common on AVRs to optimize for size instead. – Edgar Bonet Feb 7 '16 at 9:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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