PLEASE NOTE: This site is no longer being actively maintained. For frequently updated BeagleBone resources please visit Beaglebone.org.

Tuesday, September 2, 2014

On 5:59 PM by Alexander Hiam
BeagleBone RS-485 Tutorial

 

Overview

The RS-485 standard defines a serial communication protocol that uses differential signalling over a twisted pair of wires as a shared bus to create local networks of up to 32 devices. The natural noise immunity of differential twisted pairs makes RS-485 well suited for spanning much greater physical lengths than other serial protocols like RS-232.

The BeagleBone isn't capable of generating the differential signals required by RS-485, but there are many RS-485 transceiver ICs available that can convert between RS-485 and TTL serial levels, enabling the BeagleBone to communicate over an RS-485 bus using its built-in UARTs. This example will show how to do so with the Logic Supply serial cape, using its on-board Zywyn ZT3485 half-duplex RS-485 transceiver connected to UART4.

What You Will Need

Hardware Setup

The RS-485 transceiver on the serial cape is half-duplex, which means it shares the same differential pair for both sending and receiving. There are also full-duplex RS-485 transceivers that use two sets of differential pairs, but that requires a slightly different bus topology and tends to be less common. In half-duplex transceivers, because data can only flow in one direction at a time, there are one or two input pins that control whether the receiver or transmitter are active. By default the serial cape has this signal (referred to as RE/DE for Receiver-Enable/Driver-Enable) connected to GPIO1_16 (P8.15), so we'll be using that pin.

Ensure that you're J7 jumper is set in the GPIO1_16 position:


Place the J8 jumper into the RS-485 position, which will connect the UART4 signals to the RS-485 transceiver:


Ensure your BeagleBone is powered off and unplugged then attach the serial cape. You can now turn on your BeagleBone and SSH in.

Software Setup

The RE/DE input requires a high level to enable the transceiver and a low level to enable the receiver. Some UARTs can be put into RS-485 mode, where they automatically generate the RE/DE signal using their RTS pin. This mode essentially inverts the standard RTS signal, which is usually low when transmitting. The built-in UARTs in the AM335x series ARM processors lack this capability, so the RE/DE signal must be generated in software. In theory this could be done from user-space, but there would be large latencies and, since the UARTs transmit asynchronously, it would be hard to tell when a transmission is complete and the RE/DE pin should be pulled low. Thankfully the BeagleBone's omap-serial Kernel driver can be configured to generate this signal for you using any available GPIO pin. We simply need to tell it to use GPIO1_16.

Note: The RS-485 mode was a fairly recent addition to the omap-serial Kernel driver, so to ensure that you have the correct driver you should be running a Debian image with a recent 3.8 Kernel. You can grab the latest Debian image from beagleboard.org/latest-images. You can check your Kernel build version with # uname -r; if you see 3.8.13-bone39 or higher you should have the correct version of the serial driver with RS-485 support. On Debian you can update your Kernel with:
# cd /opt/scripts
# git pull
# ./tools/update_kernel.sh
# reboot

Let's dive into the Kernel a bit to to see how RS-485 works in Linux; the standard steps to put a UART into RS-485 mode are:
  1. Open the special tty file
  2. Create a serial_rs485 struct and set the desired configuration values
  3. Pass the configured struct to Kernel driver by using ioctl on the open serial port file descriptor
The default serial_rs485 struct is defined in the Kernel source in include/uapi/linux/serial.h as:
struct serial_rs485 {
  __u32   flags;                  /* RS485 feature flags */
#define SER_RS485_ENABLED               (1 << 0)        /* If enabled */
#define SER_RS485_RTS_ON_SEND           (1 << 1)        /* Logical level for
                                                           RTS pin when
                                                           sending */
#define SER_RS485_RTS_AFTER_SEND        (1 << 2)        /* Logical level for
                                                           RTS pin after sent*/
#define SER_RS485_RX_DURING_TX          (1 << 4)
  __u32   delay_rts_before_send;  /* Delay before send (milliseconds) */
  __u32   delay_rts_after_send;   /* Delay after send (milliseconds) */
  __u32   padding[5];             /* Memory is cheap, new structs
                                           are a royal PITA .. */
};
The first struct member, flags, is used to enable RS-485 and configure the logic levels of the RE/DE signal. This is done by using the defined macros that follow. The next two struct members set up the amount of extra time given between enabling the driver and the start of transmission, and between the end of transmission and enabling the receiver. The last member, padding, simply allocates some extra space so more members may be added to the struct without having to allocate more memory.

The patched version of the omap-serial driver changes the macros for the flags member to enable using the BeagleBone's GPIO driver to generate the RE/DE signal, and adds one more member to hold the desired GPIO pin number:
struct serial_rs485 {
  __u32   flags;                  /* RS485 feature flags */
#define SER_RS485_ENABLED               (1 << 0)        /* If enabled */
#define SER_RS485_RTS_ON_SEND           (1 << 1)        /* Logical level for
                                                           RTS pin when
                                                           sending */
#define SER_RS485_RTS_AFTER_SEND        (1 << 2)        /* Logical level for
                                                           RTS pin after sent*/
#define SER_RS485_RX_BEFORE_TX          (1 << 3)
#define SER_RS485_USE_GPIO              (1 << 5)
  __u32   delay_rts_before_send;  /* Delay before send (milliseconds) */
  __u32   delay_rts_after_send;   /* Delay after send (milliseconds) */
  __u32   gpio_pin;               /* GPIO Pin Index */
  __u32   padding[4];             /* Memory is cheap, new structs
                                           are a royal PITA .. */
};
So to use RS-485 mode on the BeagleBone we need to follow the standard Linux RS-485 steps, using this modified struct instead of the standard one. We'll do so here in Python.

First we need to open the UART4 tty (/dev/ttyO4):
import serial

ser = serial.Serial(
    port='/dev/ttyO4', 
    baudrate=9600, 
    timeout=1,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS
)

Next we'll create the serial_rs485 struct, using Python's struct package to pack the desired values into a correctly formatted chunk of memory:
import struct

SER_RS485_ENABLED         = (1 << 0)
SER_RS485_RTS_ON_SEND     = (1 << 1)
SER_RS485_RTS_AFTER_SEND  = (1 << 2)
SER_RS485_RTS_BEFORE_SEND = (1 << 3)
SER_RS485_USE_GPIO        = (1 << 5)
 
RS485_FLAGS = SER_RS485_ENABLED | SER_RS485_USE_GPIO 
RS485_RTS_GPIO_PIN = 48

serial_rs485 = struct.pack('IIIIIIII', 
                           RS485_FLAGS,        # config flags
                           0,                  # delay in us before send
                           0,                  # delay in us after send
                           RS485_RTS_GPIO_PIN, # the pin number used for DE/RE
                           0, 0, 0, 0          # padding - space for more values 
                           )

The GPIO pin is specified using the Kernel driver's numbering scheme: GPIO1_16 stands for GPIO module 1 - pin 16, and the driver numbers pins as module*32 + pin, so GPIO1_16 is 1*32 + 16 = 48. The logic level flags are a bit confusing; basically SER_RS485_ENABLED | SER_RS485_USE_GPIO (where '|' is the bitwise OR operator) tells the driver to drive the pin high when transmitting, and to invert the RE/DE signal you would instead use SER_RS485_ENABLED | SER_RS485_USE_GPIO | SER_RS485_RTS_ON_SEND | SER_RS485_RTS_AFTER_SEND.

The first argument to the struct.pack function tells it to pack the values into 8 consecutive 4-byte unsigned values, which matches the serial_rs485 struct's 8 __u32 members. Then we pass it the 8 values, and it returns a packed string which matches the memory allocated to the serial_rs485 struct. Python has a built-in package called fcntl which includes an ioctl routine, which makes it easy to then pass this configuration to the Kernel driver:
import fcntl

TIOCSRS485 = 0x542F

fd=ser.fileno()
fcntl.ioctl(fd, TIOCSRS485, serial_rs485)

where TIOCSRS485 tells ioctl that you're enabling RS-485 mode for the given tty file descriptor. The serial driver should now be properly controlling the RS-485 transceiver!

A Quick Example

Let's put it all the pieces together and make sure it's working:
import serial, fcntl, struct, time

ser = serial.Serial(
    port='/dev/ttyO4', 
    baudrate=9600, 
    timeout=1,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS
)

# Standard Linux RS485 ioctl:
TIOCSRS485 = 0x542F

# define serial_rs485 struct per Michael Musset's patch that adds gpio RE/DE 
# control:
# (https://github.com/RobertCNelson/bb-kernel/blob/am33x-v3.8/patches/fixes/0007-omap-RS485-support-by-Michael-Musset.patch#L30)
SER_RS485_ENABLED         = (1 << 0)
SER_RS485_RTS_ON_SEND     = (1 << 1)
SER_RS485_RTS_AFTER_SEND  = (1 << 2)
SER_RS485_RTS_BEFORE_SEND = (1 << 3)
SER_RS485_USE_GPIO        = (1 << 5)

# Enable RS485 mode using a GPIO pin to control RE/DE: 
RS485_FLAGS = SER_RS485_ENABLED | SER_RS485_USE_GPIO 
# With this configuration the GPIO pin will be high when transmitting and low
# when not

# If SER_RS485_RTS_ON_SEND and SER_RS485_RTS_AFTER_SEND flags are included the
# RE/DE signal will be inverted, i.e. low while transmitting

# The GPIO pin to use, using the Kernel numbering: 
RS485_RTS_GPIO_PIN = 48 # GPIO1_16 -> GPIO(1)_(16) = (1)*32+(16) = 48

# Pack the config into 8 consecutive unsigned 32-bit values:
# (per  struct serial_rs485 in patched serial.h)
serial_rs485 = struct.pack('IIIIIIII', 
                           RS485_FLAGS,        # config flags
                           0,                  # delay in us before send
                           0,                  # delay in us after send
                           RS485_RTS_GPIO_PIN, # the pin number used for DE/RE
                           0, 0, 0, 0          # padding - space for more values 
                           )

# Apply the ioctl to the open ttyO4 file descriptor:
fd=ser.fileno()
fcntl.ioctl(fd, TIOCSRS485, serial_rs485)

# Send some bytes:
# GPIO1_16 should be low here
time.sleep(0.2)
# GPIO1_16 should be high while this is being transmitted:
ser.write("Hello, world!")
# GPIO1_16 should be low again after transmitting
time.sleep(0.2)
ser.close()

If you have access a digital oscilloscope you can easily test this by probing the UART4 TX and GPIO1_16 pins, P9.13 and P9.15 respectively. If you trigger on the rising edge of GPIO1_16, set the scope to single capture, then run the above program, you should see GPIO1_16 go high, the message transmitted on the TX pin, then GPIO1_16 returned low: