[Home] [Downloads] [Search] [Help/forum]

Gammon Software Solutions forum

See www.mushclient.com/spam for dealing with forum spam. Please read the MUSHclient FAQ!

[Folder]  Entire forum
-> [Folder]  Electronics
. -> [Folder]  Microprocessors
. . -> [Subject]  RS485 communications
Home  |  Users  |  Search  |  FAQ
Username:
Register forum user name
Password:
Forgotten password?

RS485 communications

Postings by administrators only.

[Refresh] Refresh page


Posted by Nick Gammon   Australia  (19,502 posts)  [Biography] bio   Forum Administrator
Date Mon 14 Nov 2011 11:48 PM (UTC)  quote  ]

Amended on Tue 06 Aug 2013 10:11 PM (UTC) by Nick Gammon

Message
This post describes how you can connect multiple Arduinos together via an RS485 connection.

This is useful in situations where you need to connect devices together over longer distances than I2C or SPI can handle.

The RS485 protocol is described here:

http://en.wikipedia.org/wiki/Rs485

Basically it is an electrical protocol which allows you to communicate over long wires because it is "balanced". The graphic below shows this:



The "A" line shows a series of 0 and 1 bits, whilst the "B" line below is the inverse of the "A" line. This is more resistant to noise than simply having one line and ground, as small glitches of noise might be interpreted as data. This is because a "1" is when line A is higher than line B, and a "0" is when line B is higher than line A.

To use RS485 with an Arduino we need an RS485 "transceiver" (transmitter/receiver) chip. These cost a couple of dollars and come in various formats. I used the DIP-8 style, which was easy to breadboard with.

Electrical connection


Below is how I wired the transceiver chip up:



It needs power and ground, plus Rx/Tx connections. Pin 1 of the chip receives data from the A/B wires, and pin 4 is used to transmit data, provided DE (pin 3) is high. To avoid contention only one device should be transmitting at once. The easiest way to arrange this is to design a single master / multiple slaves configuration. Then the master can make a request to a particular slave, and then wait for a response. An example of that is shown below.

The 680 ohm resistors are there to make sure that the A/B lines are in a "standard" state (A on, B off) if no tranceiver is configured for output at a particular time, thus avoiding noise from floating lines.


Error checking protocol


The prudent designer would be worried about simply interpreting any incoming data as valid, without reasonable error checks. Noise on the line, or a device being connected or disconnected half-way through a transmission, could be interpreted as valid data, when it isn't.

Hence I have written a small library that has the following features:


  • Handles "packets" of between 1 and 255 bytes.

  • Uses a "begin packet" character (Start of Text, STX, 0x02) to reliably indicate that a packet is starting.

  • Uses an "end packet" character (End of Text, ETX, 0x03) to reliably indicate that a packet is ending.

  • Each data byte (other than STX/ETX) is sent in a "doubled/inverted" form. That is, each nibble (4 bits) is sent twice, once normally, and once inverted. Thus the only valid values for each nibble are:

    
    0F, 1E, 2D, 3C, 4B, 5A, 69, 78, 87, 96, A5, B4, C3, D2, E1, F0
    


    The inverse (ones complement) of 0 is F, hence 0 becomes 0F. The inverse of 1 is E, hence 1 becomes 1E. And so on.

    This guards somewhat against "bursts" of noise. A burst of either 0s or 1s is unlikely to corrupt a byte preserving this normal/inverse relationship. Also there are only 16/256 valid combinations, so noise has only a 6% chance of becoming a valid byte.

    Because of this, also, the STX and ETX characters cannot appear in ordinary data (they are not one of the 16 valid values).

  • Each packet is followed by a CRC (cyclic redundancy check). This is a further test that the packet was received completely. It guards against noise, or possibly some bytes just becoming missing.


The library is available from:

http://www.gammon.com.au/Arduino/RS485_protocol.zip

Just unzip into your Arduino "libraries" folder (and restart the IDE).

Callback functions


The library was written to allow for various hardware interfaces (eg. software serial, hardware serial, I2C). Thus when using it you supply three "callback" functions which have the job of doing the actual sending/receiving.

For hardware serial, they might look like this:


  void fWrite (const byte what)
    {
    Serial.write (what);  
    }
    
  int fAvailable ()
    {
    return Serial.available ();  
    }
  
  int fRead ()
    {
    return Serial.read ();  
    } 


For software serial, you might use:


  #include <SoftwareSerial.h>
  
  SoftwareSerial rs485 (2, 3);  // receive pin, transmit pin
    
  void fWrite (const byte what)
    {
    rs485.write (what);  
    }
    
  int fAvailable ()
    {
    return rs485.available ();  
    }
  
  int fRead ()
    {
    return rs485.read ();  
    }


Master


It is your responsibility to turn on the "write enable" pin before and after doing a "send". This configures the RS485 chip to allow writing to the network. An example master is:


#include "RS485_protocol.h"
#include <SoftwareSerial.h>

const byte ENABLE_PIN = 4;
const byte LED_PIN = 13;

SoftwareSerial rs485 (2, 3);  // receive pin, transmit pin

// callback routines
  
void fWrite (const byte what)
  {
  rs485.write (what);  
  }
  
int fAvailable ()
  {
  return rs485.available ();  
  }

int fRead ()
  {
  return rs485.read ();  
  }

void setup()
{
  rs485.begin (28800);
  pinMode (ENABLE_PIN, OUTPUT);  // driver output enable
  pinMode (LED_PIN, OUTPUT);  // built-in LED
}  // end of setup
  
byte old_level = 0;

void loop()
{

  // read potentiometer
  byte level = analogRead (0) / 4;
  
  // no change? forget it
  if (level == old_level)
    return;
      
  // assemble message
  byte msg [] = { 
     1,    // device 1
     2,    // turn light on
     level // to what level
  };

  // send to slave  
  digitalWrite (ENABLE_PIN, HIGH);  // enable sending
  sendMsg (fWrite, msg, sizeof msg);
  digitalWrite (ENABLE_PIN, LOW);  // disable sending

  // receive response  
  byte buf [10];
  byte received = recvMsg (fAvailable, fRead, buf, sizeof buf);
  
  digitalWrite (LED_PIN, received == 0);  // turn on LED if error    
  
  // only send once per successful change
  if (received)
    old_level = level;

}  // end of loop


This example demonstates how you might command a light in some other part of the house to dim up/down. It reads a potentiometer connected to pin A0 (with the other sides of the pot connected to +5V and Gnd). This gives an analog reading which is then sent to the slave.

We use a 3-byte message format:


  • Address of slave (eg. 1 to 255)
  • Command (eg. 2 = turn light on)
  • Parameter (eg. 128 = half level)


Then we wait for a response from the slave to confirm it got the message. If not, we turn on an "error" LED.

Slave


The code for the slave could be:


#include <SoftwareSerial.h>
#include "RS485_protocol.h"

SoftwareSerial rs485 (2, 3);  // receive pin, transmit pin
const byte ENABLE_PIN = 4;

void fWrite (const byte what)
  {
  rs485.write (what);  
  }
  
int fAvailable ()
  {
  return rs485.available ();  
  }

int fRead ()
  {
  return rs485.read ();  
  }
  
void setup()
{
  rs485.begin (28800);
  pinMode (ENABLE_PIN, OUTPUT);  // driver output enable
}

void loop()
{
  byte buf [20];
  
  byte received = recvMsg (fAvailable, fRead, buf, sizeof (buf) - 1);
  
  if (received)
    {
    if (buf [0] != 1)
      return;  // not my device
      
    if (buf [1] != 2)
      return;  // unknown command
    
    byte msg [] = {
       0,  // device 0 (master)
       3,  // turn light on command received
    };
    
    delay (1);  // give the master a moment to prepare to receive
    digitalWrite (ENABLE_PIN, HIGH);  // enable sending
    sendMsg (fWrite, msg, sizeof msg);
    digitalWrite (ENABLE_PIN, LOW);  // disable sending
    
    analogWrite (11, buf [2]);  // set light level
   }  // end if something received
   
}  // end of loop


The slave simply loops looking for incoming data. The library returns a non-zero "received count" if a valid message is received. Then the slave checks the address (first byte of the message) to see if it is addressed to it (rather than a different slave). If not, it ignores the message.

Then if checks for a valid command (eg. 2 = turn light on). If not, it ignores it.

Finally if it passes these checks, it sends back a response. A small delay (of 1 mS) is inserted to give the master time to prepare for a response. That way the master knows the slave is alive, and responding.

Flushing the output


A small "gotcha" caught me when testing with hardware serial. The following code didn't work properly:


 digitalWrite (ENABLE_PIN, HIGH);  // enable sending
 sendMsg (fWrite, msg, sizeof msg);
 digitalWrite (ENABLE_PIN, LOW);  // disable sending


Whilst it worked fine with software serial, the code above "turns off" the RS485 chip too quickly, because the last byte is still being sent from the serial hardware port.

A couple of solutions worked:

  
 digitalWrite (ENABLE_PIN, HIGH);  // enable sending
 sendMsg (fWrite, msg, sizeof msg);
 delayMicroseconds (660);
 digitalWrite (ENABLE_PIN, LOW);  // disable sending  


I'm not very happy with hard-coded delays. Too low and it is still too quick, too high and the slave is responding before you turn the transmitter off. The exact value has to be carefully tuned, and depends very much on the baud rate in use. The value required appears to be appromately two character times. So for 28800 baud, one character time is 1/2880 which is 347 uS. Doubling that gives about 690 uS.

Another approach is:


 digitalWrite (ENABLE_PIN, HIGH);  // enable sending
 sendMsg (fWrite, msg, sizeof msg);
 
 while (!(UCSR0A & (1 << UDRE0)))  // Wait for empty transmit buffer
     UCSR0A |= 1 << TXC0;  // mark transmission not complete
 while (!(UCSR0A & (1 << TXC0)));   // Wait for the transmission to complete

  digitalWrite (ENABLE_PIN, LOW);  // disable sending  


This requires fiddling with hardware registers (and the exact ones depend on whether you are using Serial, Serial1, Serial2 and so on). The first loop waits for the hardware chip's buffer to empty, at the same time setting the "transmission not complete" flag. The second loop waits for the final byte to be clocked out by the hardware.

Termination resistors


If the transceivers are not at the ends of the cable termination resistors are probably necessary. Something like 120 ohms, connected between the A and B cables, at each end only, stop the signal reflecting back along the cable.


Conclusion


This setup seemed to work pretty well. Transmission was over ordinary "speaker cable". It wasn't twisted pair or shielded. At 28800 baud it took about 7 mS to send the command and receive a response. Turning the knob resulted in light on the second Arduino smoothly changing value "instantly" (to human appearances).


[EDIT] Amended 7 August 2013 to change from NewSoftSerial to SoftwareSerial.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (19,502 posts)  [Biography] bio   Forum Administrator
Date Reply #1 on Mon 03 Dec 2012 11:12 PM (UTC)  quote  ]

Amended on Mon 03 Dec 2012 11:15 PM (UTC) by Nick Gammon

Message
Non-blocking version


I have made a "non-blocking" version of this library. The protocol is the same, but it lets you handle an incoming packet a byte at a time. This could be handy where you have multiple incoming devices.

This library is available from:

http://www.gammon.com.au/Arduino/RS485_non_blocking.zip


Sending sketch



#include <RS485_non_blocking.h>

size_t fWrite (const byte what)
{
  return Serial.write (what);  
}

RS485 myChannel (NULL, NULL, fWrite, 0);

void setup ()
{
  Serial.begin (115200);
  myChannel.begin ();
}  // end of setup

const byte msg [] = "Hello world";

void loop ()
{
  myChannel.sendMsg (msg, sizeof (msg));
  delay (1000);   
}  // end of loop


There is now a RS485 class (which I made an instance of called myChannel). You supply in the constructor a "read", "available" and "write" callback. If you are only writing you don't need the read or available callbacks, as illustrated above. You also supply a maximum buffer length (this only applies for reading).

Receiving sketch



#include <RS485_non_blocking.h>

 int fAvailable ()
   {
   return Serial.available ();  
   }
 
 int fRead ()
   {
   return Serial.read ();  
   }
 

RS485 myChannel (fRead, fAvailable, NULL, 20);

void setup ()
  {
  Serial.begin (115200);
  myChannel.begin ();
  }  // end of setup

void loop ()
  {
  if (myChannel.update ())
    {
    Serial.write (myChannel.getData (), myChannel.getLength ()); 
    Serial.println ();
    }
  }  // end of loop


In this sketch the loop function calls myChannel.update () which gradually assembles the incoming packet into a buffer inside the class instance. You need to call myChannel.begin () in setup to actually allocate the required amount of memory for it.

Once myChannel.update () returns true, a packet is ready, and you can then access its data, and the length of its data.

There are some other functions in the class which tell you whether a packet is in progress, when it started, how many errors have occurred, and so on.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


32,637 views.

Postings by administrators only.

[Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

[Home]

Written by Nick Gammon - 5K

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( http://www.gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Web site powered by FutureQuest.Net]