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.

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):
- Arduino pro mini is the cheapest way to get started
with ATmega328P. In case one also wants an USB bridge Arduino nano
might be interesting too. For larger projects there are boards built
around the ATMega2560 that provide up to four USARTs
and a whole bunch of I/O pins.
- MAX485 breakout boards that contain all necessary
components for the RS485 bus driver.

The steps that will be taken are:
- Configuring the serial port
- How one can queue data for input and output
- How one could control the data direction pin of the RS485 transceivers for half
duplex operation.
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
- receive complete
USART_RX_vect
. This routine is triggered whenever a data
word (in our case a byte) has been completly received.
- transmit complete
USART_TX_vect
. This will be triggered after the transceiver
has finished itās work. In our case this will happen after we stopped transmitting
data and the stop bits at the end have been transmitted correctly. Weāll use
this ISR to disable the RS485 transceiver.
- data register empty
USART_UDRE_vect
is used to request new data out of
the ring buffers to be pushed into the UDRn
register.
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:
- The buffer is full (
head pointer + 1
equals to the tail pointer)
- The buffer is empty (head and tail pointer are equal)
Basically a ringbuffer can have the following properties:
- Is data available (Verify if head is unequal to tail)
- Is data writable (Verify that head+1 is unequal to tail)
- Write data
- Read data
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