Serial Communications in Unix

Why serial?

Serial communications are less common outside of USB on modren computers, but serial in the form of I2C and SPI are still very common in embedded systems.

When working with micro-controllers I often find myself needing to send data over a noise reistant medium and RS485 is very relible in electrically noisy enviroments.

Lets take a look at method in quesion

Modbus

When it comes to connecting to multiple devices from a single serial connection the options often used by a lot of projects are I2C, SPI with SS and CAN. All of these have their own pros and cons.

I actually prefer another protocol Modbus RTU serial over rs485 for the reasons listed below.

pros:

  • No hardware peripheral required.
  • Flexible for low power systems.
  • CRC checks to ensure data integrity(better than parity bits).
  • Multiple devices on a single twisted pair.
  • Free and open specification.
  • Availble on most automation equipment and sensors.
  • Immune to electrical noise for all practical purposes.
  • Less issues than I2C in terms of reliability.
  • Can be run over TCP and ethernet networks if needed.

Cons:

  • Older protocol
  • The calculation of the CRC with a lookup table can be a pain.
  • No parity explicitly required/defined.
  • The terms of coils and such are a bit dated now.

History

Modbus is old, really old. If you care to search for information on it you will find that it originated from schnider electric back in the 70s. From there it’s seen widespread use in automation and scada networks.

Modbus Resources

If you want to work on writing your own implimentation of it I would highly recommend reading the specifications listed below for both RTU and TCP.

How Serial devices work in UNIX

I’ll tell you a often quoted line, “everything in unix is a file”.

Although it’s not the best way to handle everything, that unix philosophy works well enough when it comes to serial devices; they simply act like regular files on the system, and work with all the same C functions.

Let’s take a look at how you might interface with a USB to RS485 converter!

Opening the port

To open a serial port on a unix machine we first need to locate the actual device, on FreeBSD or most linux distros you will find them under the /dev/ttyUSB path name. Often there is also a second directory especially for serial devices that can also be used allowing selection by the device id.

To actually open the serial device we simply use…. the open function…. If that seems a bit obvious that’s a good thing, it just points to how well designed the unix system is.

#include <stdio.h>
#include <fctrl.h>
//--SNIP--

#define SERIAL_DEV_PATH="/dev/ttyUSB0"

//takes a file descriptor, (int)
void open_port(int *serial_fd) {
    *serial_fd = open(SERIAL_DEV_PATH, O_RDWR);
    
    if(*serial_fd < 0) {
        perror("open_port");
        exit(EXIT_FAILURE)
    }
    
    return;
}

From this example that does minimal error checking and omits setting the configuration for ioctrl or termios; we can see that using the operating systems built in libraries makes dealing with serial much easier.

but we still need to setup the correct buad rate and data settings.

Termios

Termios is the more modern way to interface with serial devices. I think of it as a extra layer over the ioctrl code.

This lets of change how the hardware will run. Lets see an example.

#include <termios.h>
#include <errorno.h>
// -- SNIP --


//Accepts the termios structure defined in the header file.
void configure_serial_connection(struct termios *new_config) {
    //configure the input flags
    new_config->c_iflag = 0;
    
    //check the termios man page for the flags you need.
    new_config->c_iflag = IGNRBRK | ......
   
    //configure the output flags.
    new_config->c_oflag = ....
    
    //inter chareter timer stuff, modbus specs have char times
    //that are asocciated with the buad rates.
    new_config->c_cc[VMIN] = 0;
    new_config->c_cc[VTIME] = 200; //hundredths of a second.
    
    //setting baud rate
    if(cfsetispeed(&config, B4800) < 0)     perror("set input speed");
    
    if(cfsetospeed(&config, B4800) < 0)     perror("set output speed");
  
    //apply the settings
    if(tcsetattr(fd, TCSAFLUSH, &config) < 0)   perror("tcsetattr");
    
    return;
}

Now obviously this example leaves out a ton of stuff. We aren’t showing the data bits which would probably be 8N1 or 8N2 if you ignore parity bits.

We also don’t do any error handling for failure of function calls besides printing out the information about the errorno to the stderr stream.

but this should illustrate the concept of how opening and configuration of the serial port is done on a unix system.

Conclusion

This is all for now on this post, I may update it later at some point to have detailed code examples, and maybe even some examples in rust.

Although there are many new serial communication methods that offer a plethora of useful new features, they can often be overkill and an actual road block to making progress in a project.

Simple serial solutions for low power controllers make sense both for reliability and comparability with other hardware that you may have to interface with in the future.