RS485 communication using Atmel ATMega328P

10 Feb 2021 - tsp
Last update 12 Mar 2023
Reading time 21 mins

Since I had to implement an stepper driver that communicated via RS485 and also had to implement an master that gathered information from RS485 controlled vacuum gauges by Thyracont (VSH89DL) I thought it’d been a good idea to summarize the most basic information about RS485 communication using AVRs.

Why RS485

First off to answer the question why one would like to use RS485 instead of any of the other supported serial ports or local bus systems like I2C, OneWire, etc. RS485 acts as a medium distance bus system - it can cover up to 1200 meters of distance with an speed up to 12 Mbps and a maximum of 32 devices per bus system (in case their load is equal to $12 k\Omega$ as specified as a unit load). This is way beyond nearly all other commonly used bus systems. Of course if you’re building a distributed system and have options like Ethernet available (like for example on the ESP32) I’d totally take that route and use fiber optics over distances larger than 100m. But if you require a long distance bus system that is somewhat immune to noise by using differential signaling RS485 is one of the easiest and robust solutions. Basically RS485 would not need a dedicated common ground though charge accumulation and maximum potential differences are relevant in this case.

As already mentioned RS485 is a differential bus - it usually uses voltage levels against ground of -7V to +12V, receivers are sensitive to voltage differences of 200 mV or more between A and B line.

The following graphic shows the output of the breakout boards shown below on the A line (yellow), the B line (turquoise) and the calculated differential signal in violet. The offset or overlayed interference on A and B are irrelevant for the receiver. To make the graphic more readable the signals A and B have been slightly shifted.

RS485 signals on A and B as well as differential signals

The only relevant part for RS485 receivers is the differential signal.

What will be covered

This article covers the usage of a single MAX485 bus driver per device offering either transmit or receive at the same time - so half duplex communication. It will also use the embedded USART of the AVR for communication at a rather low speed (in case of the samples 57600 bits per second so way beyond the capability of RS485 but in an range that’s easily handle able by the AVRs and most importantly that is also compatible to often used Arduino bootloaders when not flashing via ICSP).

In case you want to experiment with these bus drivers breakout boards for all components are readily available (Note: These are affiliate links, this pages author profits from qualified purchases):

Max485 breakout boards readily available

The steps that will be taken are:

Configuring the serial port

Since the RS485 interface presented in this blog article is based on the AVRs universal asynchronous / synchronous receiver/transmitter (USART) first one has to do basic configuration on the serial port. This involves setting mode of operation (synchronous / asynchronous), size of a data word, number of stop bits, the usage of parity bits, etc. as well as the speed of the serial port. The AVRs USART(s) is/are pretty flexible so this requires some steps:

Using the external bus driver in half duplex mode on the other hand just requires switching it’s mode between transmit and receive - optionally using two GPIOs if one wants to be able of disabling RS485 completely.

BAUD rate

The BAUD rate is the speed at which the transmitter transmits or receives data. The naming of this rate is rather non obvious (it’s named after Emile Baudot) but basically it’s a synonym for bits per second (bps).

To configure the BAUD rate one has to initialize the UBRR0 register. The value depends on the value of the U2X0 flag in the UCSR0A register. Setting this bit will double the effective baud rate by setting a different prescaler - but only in asynchronous mode. The values for the BAUD rate register can be calculated easily, even more easy one can consult the datasheet for one of the common clock frequencies. For 16 MHz for example the datasheet cites:

[ UBRR0 = \frac{f_{cpu}}{16 * BAUD} - 1 ]
BAUD UBRR (U2X = 0) Error (U2X = 0) UBRR (U2X = 1) Error (U2X = 1)
2400 416 -0.1 pct. 832 0.0 pct.
4800 207 0.2 pct. 416 -0.1 pct.
9600 103 0.2 pct. 207 0.2 pct.
14400 68 0.6 pct. 138 -0.1 pct.
19200 51 0.2 pct. 103 0.2 pct.
28800 34 -0.8 pct. 68 0.6 pct.
38400 25 0.2 pct. 51 0.2 pct.
57600 16 2.1 pct. 34 -0.8 pct.
76800 12 0.2 pct. 25 0.2 pct.
115200 8 -3.5 pct. 16 2.1 pct.
230400 3 8.5 pct. 8 -3.5 pct.
250000 3 0 pct. 7 0.0 pct.
500000 1 0 pct. 3 0.0 pct.
1000000 0 0 pct. 1 0.0 pct.

Since the BAUD rate register is a 12 bit register it’s split into two 8 bit registers, UBBR0H and UBBR0L. As usual one should write the high before the low register:

static void setUBBR0(unsigned long int baudrate) {
	uint8_t flagU2X = 1;
	uint32_t tmpUBRR0 = (uint32_t)((F_CPU / (16 * baudrate)) - 1);

	if(ubrr0 >= 4096) {
		flagU2X = 0;
		tmpUBRR0 = (uint32_t)((F_CPU / (8 * baudrate)) - 1);
	}

	UBRR0 = tmpUBRR0;
	if(flagU2X == 0) {
		UCSR0A = UCSR0A & (~(0x02));
	} else {
		UCSR0A = UCSR0A | 0x02;
	}
}

For this blog article we’re going to operate the bus with a BAUD rate of 57600 bits per second, 1 stop bit and no parity bits:

UBRR0  = 34;
UCSR0A = 0x02;

Then we’re also going to use 8 data bits, one stop bit and run the USART in asynchronous mode as well as use the interrupts on receive complete, transmit complete and data register empty events.

UCSR0B = 0x00;
UCSR0C = 0x06;

The bits of the control registers are described below.

Control registers

There is a total of 3 control registers used during serial communication:

UCSRnA

Bit Mnemonic R/W Content
7 RXCn R USART receive complete - set when there is unread data in the receive buffer and cleared when no data is ready any more
6 TXCn R/W USART transmit complete - is set when the entire frame in transmit shift register has been written and no new data is present in UDRn. Can generate an interrupt controlled by TXCIEn bit
5 UDREn R USART data register empty - indicates the transmit buffer is ready to receive new data (1 means the buffer is empty)
4 FEn R Frame error - is set whenever a frame error has been received (i.e. mismatch for the first stop bit)
3 DORn R Data overrun - is set when an overrun is detected (i.e. the data buffer is full and even more data has been received)
2 UPEn R Parity error - set in case of any parity errors during receive if parity bits are used
1 U2Xn R/W Double transmission speed will reduce the divisor of BAUD rate divider from 16 to 8
0 MPCMn R/W Multiprocessor communication mode - this is a special receiver mode that allows some addressing of the attached MCUs

UCSRnB

Bit Mnemonic R/W Content
7 RXCIEn R/W Receive complete interrupt enabled. This interrupt will trigger whenever data is available inside the UDRn register.
6 TXCIEn R/W Transmit complete interrupt enabled - will be used to disable the transceiver at the end of a packet
5 UDRIEn R/W Data register empty interrupt enabled which is triggered whenever a transmitter is capable of enqueuing the next byte
4 RXENn R/W Receiver enable. If set the USART will listen for external traffic, read the next byte and invoke the RX interrupt whenever a byte is ready
3 TXENn R/W Transmitter enable will enable the transmitter and ask for new data using the data register empty interrupt
2 UCSZn2 R/W Character size (bit 2)
1 RXB8n R Ninth received bit when operating with 9 bit words. Must be read before reading the remaining 8 bits from UDR0
0 TXB8n W Ninth bit to transmit when operating with 9 bit words. Must be written before writing the remaining low 8 bits to UDR0

UCSRnC

Bit Mnemonic R/W Content
7 UMSELn1 R/W Mode select bit 1 (Asynchronous - 00, synchronous - 01, Master SPI - 11)
6 UMSELn0 R/W Mode select bit 0
5 UPMn1 R/W Parity mode select (00 Disabled, 10 Enabled - even, 11 Enabled - odd)
4 UPMn0 R/W Parity mode select
3 USBSn R/W Stop bit select (0: 1 stop bit, 1: 2 stop bits)
2 UCSZn1 R/W Character size select (note third bit in B register)
1 UCSZn0 R/W Character size select (note third bit in B register)
0 UCPOLn R/W Clock polarity for synchronous operation

The character size setting can be any of the following:

UCSZn2 UCSZn1 UCSZn0 Character Size
0 0 0 5-bit
0 0 1 6-bit
0 1 0 7-bit
0 1 1 8-bit
1 0 0 Reserved
1 0 1 Reserved
1 1 0 Reserved
1 1 1 9-bit

Half duplex operation

Note that we’re only using half duplex operation. Bus drivers like the MAX485 support this by being switchable between transmit and receive mode. Basically this works by supplying an transmit enable and receive enable pin. Since the receive enable pin is inverted one can attach both pins to a single output pin of the microcontroller. This allows one to toggle the bus driver between transmit and receive mode - in case one doesn’t require disabling the drivers completely.

For this blog post we’re assuming the operation is running in an half duplex master/slave mode. The basic protocol implemented allows a master to send an request that’s prefixed with an address and length byte. The addressed slave device can is then allowed to enable it’s own transmit mode (after a short duration that’s required by the transmitter to switch into receive mode) and use the same message layout - the answer is transmitted towards address 0 and is also prefixed with a length byte. This is required because other slave devices are not capable of distinguishing messages by devices from messages sent by the slave.

In case one wants to implement full duplex operation one has to use two bus drivers - they are of course allowed to listen on the same two wire pair. One could then implement some kind of CSMA/CD mechanism to prevent and detect collisions.

Since it’s sufficient for many applications and one of the requirements of the project that was designed while I also wrote this blog article in parallel (an stepper controller to be used inside a vacuum apparatus so less communication wires that pass the chamber walls has been one of the design goals - together with being grease free, some major cooling considerations, etc.) we’ll stay with half duplex operation.

Interrupt handlers

As mentioned before interrupt handlers will be called on

Ring buffers

The basic idea of the serial communication subsystem is to use two queues - one receive and one transmit queue. These queues are ring buffers - these are buffers that have a head and a tail pointer. The head pointer is used to fill the queue - new data is always put at the location where the head pointer points to. This pointer is then incremented - if it reaches the end of the queue it will wrap around to the first possible location. The tail pointer is used for reading - as long as it’s not equal to the head pointer there’s data available that can be read directly from the pointed location - wrapping works exactly the same way.

The only two exceptional states that one has to monitor are:

Basically a ringbuffer can have the following properties:

This is a datastructure that’s also usually found when programming (serial) communication interfaces in general.

The sample implementation will use a simple wrapper structure that encloses an fixed sized ringbuffer:

#ifndef SERIAL_RINGBUFFER_SIZE
	#define SERIAL_RINGBUFFER_SIZE 256
#endif

struct ringBuffer {
    volatile unsigned long int dwHead;
    volatile unsigned long int dwTail;

    volatile unsigned char buffer[SERIAL_RINGBUFFER_SIZE];
};

A simple initializer will just set head and tail to initial positions:

void ringBuffer_Init(volatile struct ringBuffer* lpBuf) {
    lpBuf->dwHead = 0;
    lpBuf->dwTail = 0;
}

The initialization should be called before the USART receiver is enabled. Now one can build simple routines that allow one to check how much data is currently enqueued and how much data can be written into the buffer:

unsigned long int ringBuffer_AvailableN(
	volatile struct ringBuffer* lpBuf
) {
    if(lpBuf->dwHead >= lpBuf->dwTail) {
        return lpBuf->dwHead - lpBuf->dwTail;
    } else {
        return (SERIAL_RINGBUFFER_SIZE - lpBuf->dwTail) + lpBuf->dwHead;
    }
}
unsigned long int ringBuffer_WriteableN(
	volatile struct ringBuffer* lpBuf
) {
    return SERIAL_RINGBUFFER_SIZE - ringBuffer_AvailableN(lpBuf);
}

Then some additional functions can be provided that allow reading and writing data to and from the ringbuffer:

unsigned char ringBuffer_ReadChar(
	volatile struct ringBuffer* lpBuf
) {
    char t;

    if(lpBuf->dwHead == lpBuf->dwTail) {
        return 0x00;
    }

    t = lpBuf->buffer[lpBuf->dwTail];
    lpBuf->dwTail = (lpBuf->dwTail + 1) % SERIAL_RINGBUFFER_SIZE;

    return t;
}

static void ringBuffer_WriteChar(
    volatile struct ringBuffer* lpBuf,
    unsigned char bData
) {
    if(((lpBuf->dwHead + 1) % SERIAL_RINGBUFFER_SIZE) == lpBuf->dwTail) {
        return; /* Simply discard data */
    }

    lpBuf->buffer[lpBuf->dwHead] = bData;
    lpBuf->dwHead = (lpBuf->dwHead + 1) % SERIAL_RINGBUFFER_SIZE;
}

It’s also helpful to provide routines that allow one to at least write a whole sequence and that allow input and output of more complex datatypes like for example 32 bit unsigned integers. For this sample it’s assumed that they’re transmitted in little endian byte order:

static void ringBuffer_WriteChars(
    volatile struct ringBuffer* lpBuf,
    unsigned char* bData,
    unsigned long int dwLen
) {
    unsigned long int i;

    for(i = 0; i < dwLen; i=i+1) {
        ringBuffer_WriteChar(lpBuf, bData[i]);
    }
}
static void ringBuffer_WriteINT32(
    volatile struct ringBuffer* lpBuf,
    uint32_t bData
) {
    ringBuffer_WriteChar(lpBuf, (unsigned char)(bData & 0xFF));
    ringBuffer_WriteChar(lpBuf, (unsigned char)((bData >> 8) & 0xFF));
    ringBuffer_WriteChar(lpBuf, (unsigned char)((bData >> 16) & 0xFF));
    ringBuffer_WriteChar(lpBuf, (unsigned char)((bData >> 24) & 0xFF));
}

static uint32_t ringBuffer_ReadINT32(
    volatile struct ringBuffer* lpBuf
) {
    unsigned char tmp[4];

    if(ringBuffer_AvailableN(lpBuf) < 4) { return 0; }

    tmp[0] = ringBuffer_ReadChar(lpBuf);
    tmp[1] = ringBuffer_ReadChar(lpBuf);
    tmp[2] = ringBuffer_ReadChar(lpBuf);
    tmp[3] = ringBuffer_ReadChar(lpBuf);

    return ((uint16_t)(tmp[0]))
           | (((uint32_t)(tmp[1])) << 8)
           | (((uint32_t)(tmp[2])) << 16)
           | (((uint32_t)(tmp[3])) << 24);
}

This allows pretty easy access to the ringbuffer. Depending on the application it’s a good idea to provide further serialization methods for any used datatype as well as a peek method to inspect the next byte(s).

The transmit and receive logic

Since the device can only be in receive or transmit mode at a single time the queue will also act as a buffer. We’re going to enable transmit mode only when we are immediately wanting to transmit data and keep the driver in receive mode the remaining time. Since devices queried for information take some time to prepare the message it’s usually sufficient to switch into receive mode after the last byte has been fully transmitted inside the UDRE empty ISR.

To allow easy switching between receive and transmit mode two functions are used. It’s assumed that the RE and DE pins of the RS485 are connected to PD3. RXD and TXD pins are hopefully obvious.

static inline void serialModeRX() {
    /*
        Set to receive mode on RS485 driver
        Toggle receive enable bit on UART, disable transmit enable bit
    */
	uint8_t oldSREG = SREG;
	cli();

    PORTD = PORTD & (~(0x08)); /* Set RE and DE to low (RE: active, DE: inactive) */
    UCSR0B = (UCSR0B & (~0xE8)) | 0x10 | 0x80; /* Disable all transmit interrupts, enable receiver, enable receive complete interrupt */
    return;

	SREG = oldSREG;
}

static inline void serialModeTX() {
    /*
        Set to transmit mode on RS485 driver
        and toggle transmit enable bit in UART
    */
	uint8_t oldSREG = SREG;
	cli();

    PORTD = PORTD | 0x08; /* Set RE and DE to high (RE: inactive, DE: active) */
    UCSR0B = (UCSR0B & (~0x90)) | 0x08 | 0x20; /* Enable UDRE interrupt handler, enable transmitter and disable receive interrupt & receiver */

	SREG = oldSREG;

    return;
}

This is enough to switch between transmit and receive mode and toggle respective interrupt states. Then one only has to implement the interrupt routines themselves. The data register empty interrupt is pretty simple - it checks if additional data is available in the queue and if it is it enqueued the new data in the data register. If no additional data is available the UDRE interrupt handler is disabled and the transceiver waits till the TX vector is raised to disable the driver and switch back into receive mode:

ISR(USART_UDRE_vect) {
    /*
        Transmit as long as data is available to transmit. If there
        is no more data we simply stop to transmit and enter receive mode
        again
    */
    cli();

    if(ringBuffer_AvailableN(&rbTX) == 0) {
        /* Disable transmit mode again ... */
        UCSR0B = UCSR0B & (~0x20);
    } else {
        /* Shift next byte to the outside world ... */
        UDR0 = ringBuffer_ReadChar(&rbTX);
    }

    sei();
}

After the required stop bits have been transmitted the AVR triggers the TX interrupt vector. On this event the extern RS485 bus driver gets disabled and the AVRs receive logic is re-enabled:

ISR(USART_TX_vect) {
    PORTD = PORTD & (~(0x08)); /* Set RE and DE to low (RE: active, DE: inactive) */
    UCSR0B = (UCSR0B & (~0xE8)) | 0x10 | 0x80; /* Disable all transmit interrupts, enable receiver, enable receive complete interrupt */
}

The receive interrupt is even more simple - just take the next byte and push it into the ringbuffer:

ISR(USART_RX_vect) {
    cli();
    ringBuffer_WriteChar(&rbRX, UDR0);
    sei();
}

Now one can simply check for new available message data in the main program loop and process that data directly out of the ringbuffer. Whenever one wants to write a response message one might enqueue that message into the ringbuffer and toggle transmit mode. The following example is taken out of a RS485 stepper driver controller (that has been design for a quantum physics experiment inside a vacuum chamber - but that had of course no influence on the code or functionality)

static unsigned char serialHandleData__RESPONSE_IDENTIFY[20] = {
    0x00, /* Address */
    20, /* Length */
    0xb7, 0x9a, 0x72, 0xe1, 0x03, 0x6a, 0xeb, 0x11, 0x45, 0x80, 0xb4, 0x99, 0xba, 0xdf, 0x00, 0xa1, /* UUID */
    0x01, 0x00 /* Version */
};


	/*
		...
		Somwhere in the code the following instructions are following:
		...
	*/

	ringBuffer_WriteChars(&rbTX, serialHandleData__RESPONSE_IDENTIFY, sizeof(serialHandleData__RESPONSE_IDENTIFY));
	serialModeTX();

immediately after calling serialModeTX the RS485 driver will be switched into transmitting mode and the data will be pushed out by the UART. Note that the receiver already has to be in receive state at this point of time. If required one might add an additional delay before transmitting.

A short note about termination and biasing

Usually each end of an RS485 bus should be terminated according to the line load as every matched impedance line should be. It’s also advisable to use weak pullups and pulldowns on the data lines (usually A is pulled to Vcc, B is pulled to GND). The exact value of these resistors depends on the bus load and used symbol speed. The termination prevents reflections and thus interference on the line, the biasing resistors provide a well defined state in case no bus transceiver is currently active so the first symbol received is not a spurious one when a transceiver switches to active mode again.

Interesting resources

This article is tagged: Programming, ANSI C, Tutorial, AVR, RS485


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support