2

I use Serial.write() to transmit 53 bytes to PC. For the time measurement I use micros() (before and after the write function). There is a delay of 1s after each transmission.

The time of the Serial.write() function is 532 us with 1000000 baud rate, and 360 us with 9600 baud rate.

The Serial.write() function is clearly asynchronous because the transmission of 53 bytes with 9600 baud rate is 53*8/9600*1e6 = 44167 us. (By the way, in the case of 1000000 baud rate, it is not so obvious that the function is asynchronous.)

I use Serial.availableForWrite() before Serial.write() to confirm that there is enough space in the buffer (this returns 63 each time, the buffer size is default 64).

I don't understand these numbers. Using memcpy() to copy the 53 bytes only takes 32 us. Is copying to the serial buffer not the same as the memcpy() function? And why is there a difference in the copy times when the baud rate is different? Serial.write() is even slower with higher baud rates according to the results. Why does Serial.availableForWrite() return 63 while the buffer size is 64 (according to SERIAL_TX_BUFFER_SIZE)?

Update: Thanks for all of your answers.

I have tried another library for serial communication: https://github.com/greiman/SerialPort

It seems to be faster than the original one. I use 2 time measurements and a delay in the code, these 3 deltaTimes represents the whole step as follows:

writeTime + memcpyTime + delaymicros = computedTime != realTime

The first 2 times are measured, the delaymicros is the theoretical delay (900 us), I can compute the step time from these. This computed step time is different from realTime (the measured step time, I measure the whole step too). It seems that the additional time can be found in the delay.

SerialPort library: 100 + 30 + 900 = 1030 != 1350

writeReal = 100 + 1350 - 1030 = 430

Arduino Serial: 570 + 30 + 900 = 1500 != 1520

writeReal = 570 + 1520 - 1500 = 590

Then I measured the delaymicros time (which is 900 us in theory), the missing time can be found there. I programmed 900 us delay, but the real delay was around 1200 in the first, and 920 in the second test.

These measurements can prove the existence of the interrupts, because measuring only the write functions doesn't give all of the writing time (especially with the downloaded serial library). The downloaded library can work faster, but requires bigger tx buffer because of errors (256 works properly instead of 64).

Here is the code: I use a checkSum in the sendig function (which is 570-532 = 38 us). I use Simulink to receive and process the data.

struct sendMsg1 {
  byte errorCheck;//1

  unsigned long dT1;//4
  unsigned long dT2;//4
  unsigned long t;//4
  unsigned long plus[10];//40
};//53

sendMsg1 msg1;
byte copyMsg1[53];

unsigned long time1;
unsigned long time2;
unsigned long time3;
unsigned long time4;

void setup() {
  Serial.begin(1000000);
}

void loop() {
  sensorRead();
  sendMsg1_f();

  //time3 = micros();
  delayMicroseconds(900);
  //time4 = micros();
}

void sensorRead() {
  time3 = micros();

  msg1.t = micros();
  for (unsigned long i = 0; i < 1; i++) {
    memcpy((void*)&(copyMsg1[i*sizeof(sendMsg1)]), (void*)&msg1, sizeof(sendMsg1));
  }

  time4 = micros();
  msg1.dT2 = time4 - time3;
}

void sendMsg1_f() {
  time1 = micros();

  msg1.errorCheck = 0;
  for (int i = 0; i < sizeof(sendMsg1) - 1; i++) {
    msg1.errorCheck += ((byte*)&msg1)[i];
  }
  Serial.write((byte*)&msg1,sizeof(sendMsg1));

  time2 = micros();
  msg1.dT1 = time2 - time1;
}
2
  • Can you provide the code, that you used to get these results?
    – chrisl
    Jul 16, 2019 at 8:12
  • 1
    1 byte of buffer is lost because of ring buffer implementation. tail an head of full buffer can't point at the same index, because tail an head are on same index in case of empty buffer
    – Juraj
    Jul 16, 2019 at 8:50

3 Answers 3

2

If you take a look into the implementation:

size_t HardwareSerial::write(uint8_t c)
{
  _written = true;
  // If the buffer and the data register is empty, just write the byte
  // to the data register and be done. This shortcut helps
  // significantly improve the effective datarate at high (>
  // 500kbit/s) bitrates, where interrupt overhead becomes a slowdown.
  if (_tx_buffer_head == _tx_buffer_tail && bit_is_set(*_ucsra, UDRE0)) {
    // If TXC is cleared before writing UDR and the previous byte
    // completes before writing to UDR, TXC will be set but a byte
    // is still being transmitted causing flush() to return too soon.
    // So writing UDR must happen first.
    // Writing UDR and clearing TC must be done atomically, otherwise
    // interrupts might delay the TXC clear so the byte written to UDR
    // is transmitted (setting TXC) before clearing TXC. Then TXC will
    // be cleared when no bytes are left, causing flush() to hang
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      *_udr = c;
#ifdef MPCM0
      *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << MPCM0))) | (1 << TXC0);
#else
      *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << TXC0)));
#endif
    }
    return 1;
  }
  tx_buffer_index_t i = (_tx_buffer_head + 1) % SERIAL_TX_BUFFER_SIZE;

  // If the output buffer is full, there's nothing for it other than to 
  // wait for the interrupt handler to empty it a bit
  while (i == _tx_buffer_tail) {
    if (bit_is_clear(SREG, SREG_I)) {
      // Interrupts are disabled, so we'll have to poll the data
      // register empty flag ourselves. If it is set, pretend an
      // interrupt has happened and call the handler to free up
      // space for us.
      if(bit_is_set(*_ucsra, UDRE0))
    _tx_udr_empty_irq();
    } else {
      // nop, the interrupt handler will free up space for us
    }
  }

  _tx_buffer[_tx_buffer_head] = c;

  // make atomic to prevent execution of ISR between setting the
  // head pointer and setting the interrupt flag resulting in buffer
  // retransmission
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    _tx_buffer_head = i;
    sbi(*_ucsrb, UDRIE0);
  }

  return 1;
}

It basically answers all the questions.

If the buffer is empty and it's not sending anything, it'll send the character directly so it's synchronous (but sending the character will be much faster than overhead of function callbacks and so)

No, you cannot use memcpy, as it just replaces buffer, but it does nothing else, like actually start the data register ready interrupt nor proper setting of head/tail counters (it's round buffer, so there can be scenario you'll overwrite something outside of buffer)

And write function is called for each character separately (all other specialisation of write are using this one)

Also if the buffer is full, it'll wait until there is a space for another character.

1

Copying to the serial buffer is not the same as memcpy(), no.

The serial buffer is a circular buffer. Because of that there are lots of calculations involved in working out exactly where in the buffer to place the next character. That takes time. memcpy() just copies one block of memory directly over another. It makes no checks, and it can't wrap around like a circular buffer.

The reason why higher baud rates seem slower is because each character is taken from the buffer by an interrupt. The higher the baud rate the more often that interrupt is triggered, and so the more time is spent by the CPU in processing that interrupt to send the next character. And equally, the less time is available to the CPU to process the placing of data into to the serial buffer.

1

The function you were testing is presumably Serial.write(const uint8_t *buffer, size_t size). If you search for it in HardwareSerial.h, you will see

    using Print::write; // pull in write(str) and write(buf, size) from Print

and the implementation is in Print.cpp:

/* default implementation: may be overridden */
size_t Print::write(const uint8_t *buffer, size_t size)
{
  size_t n = 0;
  while (size--) {
    if (write(*buffer++)) n++;
    else break;
  }
  return n;
}

This is writing the bytes one by one, and for every byte, the whole code of HardwareSerial::write(uint8_t c) is being run. This includes the tests for full or empty buffer, and the arithmetic for updating _tx_buffer_head.

As stated in the comment above the code, it would have been possible to override this with a specialized implementation. In principle, you could copy the linear buffer into the ring buffer using at most two calls to memcpy(), and updating _tx_buffer_head only once. That would likely be more efficient than the current implementation, at least for buffer sizes in the range you are using (close to, but less than the ring buffer capacity).

Would it be worth it? Maybe for your use case it would. But it could also make the code more complex and require more flash. And the benefit is likely to be really small for people who write small buffers. I am not sure a pull request implementing this sort of optimization may be accepted. You may try if you feel like so.

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.