Skip to content

HardwareSerial library sometimes sends the same data twice #120

Closed
@johnholman

Description

@johnholman

Running a Arduino Mega 2560 program that makes heavy use of interrupts and that reports by sending lines of text (about 150 characters) in the main loop via the serial port at 115200 baud, I noiticed that additional spurious characters were sent on average every 2000 lines or so. Looking at the problem in more detail, what was transmitted was a line of exactly 66 characters consisting of the first few characters of the new line followed by last part of the line that had already been sent. The next line missed the first two characters, and then following ones were normal until the next occurrence.

These symptoms seemed consistent with void HardwareSerial::_tx_udr_empty_irq(void) occasionally being invoked at a time when the transmit buffer is in fact empty (i.e. _tx_buffer_head == _tx_buffer_tail), even though this should never happen. So I added some instrumentation:

void HardwareSerial::_tx_udr_empty_irq(void)
{
  // the buffer shouldn't be empty if we get here, but ...
  if (_tx_buffer_head == _tx_buffer_tail) {
    serial_errors++;

where serial_errors is defined in HardwareSerial.h as a private member

volatile uint8_t serial_errors;

and added a method to read and print the error count as part of the main loop.

This confirmed that the problem is indeed associated with the ISR being entered at a time when the buffer is empty, as the error count increments by one each time the problem occurs.

My original code is quite complicated, but I've been able to replicate the problem with a simple testcase in which the loop prints as before while a timer interrupt runs every 0.5ms with an associated ISR running for 0.3ms. The error seems to occur more often at higher baud rates - at 500,000 baud it occurs every few seconds. I can provide the testcase if required.

As a workaround, I've added this section to the start of _tx_udr_empty_irq:

void HardwareSerial::_tx_udr_empty_irq(void)
{
  // the buffer shouldn't be empty if we get here, but ...
  if (_tx_buffer_head == _tx_buffer_tail) {
    serial_errors++;
    // empty buffer so the interrupt should be disabled. Make it so and return.
    cbi(*_ucsrb, UDRIE0);
    return;
  }
...

This fix seems to work for me, but I'm certainly not convinced that it is correct in all circumstances.

I've not been able to figure out why the ISR is ever entered in this situation. Most but not all of the errors disappear if you comment out the part in the busy wait loop in write() that calls the ISR directly when interrupts are disabled globally. (No, I'm not printing to the serial port from an ISR!) Almost all disappear if in addition you comment out the optimisation that writes directly into the output register when the buffer is empty. However I've seen the problem very occasionally (but not with the test case) even when both optimisations are taken out.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions