Friday, January 20, 2012

arduino mega and multiple hardware serial devices

For a project I use an arduino mega (Atmel AtMega1280 8-bit micro-controller to be precise)  to access multiple other hardware devices via hardware serial.

I'm running the Arduino stack on the hardware, more specific the arduino 1.0 release.

Now the atmega1280 (or atmega2560) has 4 hardware serial devices (called Serial, Serial1, Serial2, Serial3 in arduino) so using multiple serial devices is easy eh?

Not really.

For one use case I have a device connected on Serial1 at 57600 baud.
I want to be able to route serial traffic from that device to the Serial device connected to the PC.

Sounds easy enough, the follow arduino sketch does the job:

void setup(){
  Serial.begin
(57600);
  
Serial1.begin(57600);}
void loop() {
    
// from external device to serial console
    
if (Serial1.available() > 0) {
        
byte incomingByte = Serial1.read();
        
Serial.write(incomingByte);
    
}
    
// from serial console to external device
    
if (Serial.available() > 0) {
        
byte incomingByte = Serial.read();
        
Serial1.write(incomingByte);
  
}
}


Thing is, for short messages this works fine. However for longer messages only one direction is stable, the direction from the external device (Serial1) to the serial port (Serial).

I could run the serial port (Serial) at a slower speed then the device (Serial1). However then the other way around will start to drop bytes!

Let's have a look at HardwareSerial.cpp as provided in the arduino distribution.

It seems to work with an RX and TX buffer for each hardware serial device.

#define SERIAL_BUFFER_SIZE 64

Maybe that buffer is not large enough. The AtMega1280 I'm using has 8K SRAM and thats quite some more then I need. Let's double the buffer.

#define SERIAL_BUFFER_SIZE 128

Surely the behavior is somewhat better, but it is still dropping bytes in one direction.

Let's have a look at the write function.


size_t HardwareSerial::write
(uint8_t c){
  
int i = (_tx_buffer->head + 1) % SERIAL_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
  // ???: return 0 here instead?
  
while (i == _tx_buffer->tail)
    ;
  
  
_tx_buffer->buffer[_tx_buffer->head] = c;
  
_tx_buffer->head = i;
  
  
sbi(*_ucsrb, _udrie);
 
  
return 1;}



Look and behold:

while (i == _tx_buffer->tail)
    ;

it's a busy loop! If Serial1's transfer buffer is full the entire micro-controller will block and this keeps happening all the time.

The solution I introduced is only move data from one serial device to the other when both are ready. One should be readable (available() in arduino jargon) and the other should be writeable.

A writeable function is not available in Arduino so I added the following method to the HardwareSerial code:


bool HardwareSerial::writeable(){
  
int i = (_tx_buffer->head + 1) % SERIAL_BUFFER_SIZE;
  
return (i != _tx_buffer->tail);}


The loop from the sketch now looks like this:

void loop() {
    
if (Serial1.available() > 0 && Serial.writeable()) {
        
byte incomingByte = Serial1.read();
        
Serial.write(incomingByte);
    
}
    
if (Serial.available() > 0 && Serial1.writeable()) {
        
byte incomingByte = Serial.read();
        
Serial1.write(incomingByte);
  
}
}


This helps in the sense that it smooths the flow between the serial devices.

No more dropped bytes!