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.
Shield and ground return
The datasheet for the LTC1480 shows a shield over the wires, connected at one end only. Connecting at one end would reduce earth loops in the shield.
Also an app note from TI: AN-1057 Ten Ways to bulletproof RS-485 Interfaces mentions that a ground wire should also be used, as follows:
Quote:
Although the potential difference between the data-pair conductors determines the signal without officially involving ground, the bus needs a ground wire to provide a return path for induced common-mode noise and currents, such as the receivers' input current. A typical mistake is to connect two nodes with only two wires. If you do this, the system may radiate high levels of EMI, because the common-mode return current finds its way back to the source, regardless of where the loop takes it. An intentional ground provides a low-impedance path in a known location, thus reducing emissions.
Also, Guidelines for Proper Wiring of an RS-485 (TIA/EIA-485-A) Network mentions:
Quote:
A balanced system uses two wires, other than ground, to transmit data.
Also see Application Note 847 FAILSAFE Biasing of Differential Buses.
Also see below about termination resistors.
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
Also from Github: https://github.com/nickgammon/RS485_protocol
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 [10];
byte received = recvMsg (fAvailable, fRead, buf, sizeof (buf));
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. |