Emulating a serial port on UNIX
Sometimes a project presents an assortment of constraints that leads to an unusual problem. We were working on an embedded Linux project with a need to communicate with a sensor that uses the Modbus RTU protocol over RS-485. Not wanting to re-implement a standard protocol, we found libmodbus, a free software library. This looked good, except for one problem. The library expects to talk to a serial port but we were using an RS-485 driver chip with an SPI interface, like this:
Arguably the ‘proper’ thing to do would be to write a device driver that provided a serial-port-like interface. On the other hand, this is a proof of concept embedded device already doing plenty of low level hardware control in the user application. The project was being developed for a hard deadline and pragmatic solutions were required.
The obvious option was simply to drop libmodbus and implement our own Modbus library. This would be reasonable. Modbus is not very complicated and we would only need the subset of the protocol used by our peripherals. We were wary of this temptation though; things tend to look much simpler than they really are. Instead, we took a step back and considered building an adapter.
In software pattern jargon, an adapter is a module that connects two different interfaces. In this case one side of the adapter would be the serial-port-like interface that libmodbus could use. The other side would be the module that handles the SPI / RS-485 driver chip.
There are several options for building a serial port adapter. First the ideas we didn’t use:
Named pipes – An IPC channel that appears as a file in the filesystem. This might work on systems with bidirectional pipes1 but Linux only supports unidirectional pipes.
Dummy serial port – e.g. tty0tty – A program that installs pairs of virtual serial ports that communicate with each other as if connected by a null-modem cable. These are permanently available in known locations, e.g. /dev/tnt0 <=> /dev/tnt1. This would be a good choice for communicating with a program that expects a fixed serial device name and is awkward to reconfigure.
Relay/pipe utility – e.g. socat – A program that dynamically connects two endpoints of various types.
The workable solutions listed above required things we would prefer to avoid; installing external dependencies; installing kernel modules; managing an extra process at runtime; or writing and debugging a new kernel module. It would simplify installation and usage if we could incorporate the adapter within the main application. For rapid development, we'd rather not write a lot of new code. It turns out we could satisfy both aims using a few standard POSIX functions to create a type of device called a pseudoterminal.
A pseudoterminal is a pair of connected pseudodevices2, one of which implements a teletype (TTY) interface, the other a device file. Data written to one end of the pseudoterminal may be read at the other. A pseudoterminal is dynamically created by a process usually for its own use and it does not persist after its creator exits. Pseudoterminals are typically used to implement terminal emulators and services such as ssh.
This diagram expands the CPU device to show the software design. The main application has a sensor module for high level access to the sensor. This module uses the C API of libmodbus to make Modbus message requests of the sensor. Libmodbus uses the I-can’t-believe-it’s-not-a-serial-port3 (TTY) end of the pseudoterminal to write serialised messages. Back inside our application, the adapter reads the message data from the device file end of the pseudoterminal and writes this back out to the device file representing the SPI port. The RS485 driver chip transmits the data on the bus to the sensor. The sensor generates a Modbus reply to the request which travels back along the same path in reverse.
At this point we’ll illustrate use of a pseudoterminal with snippets of C code4.
#include <unistd.h> #include <stdlib.h> #include <fcntl.h>
These headers allow use of pseudoterminals and low level I/O.
In this example we’re using the pseudoterminal to interface to libmodbus.
int fileDescriptor = posix_openpt(O_RDWR | O_NOCTTY);
Start by opening a new pseudoterminal. The options here specify a read/write device that is not the controlling terminal for the process (i.e. it is not the user interface terminal).
int result = grantpt(fileDescriptor); ... result = unlockpt(fileDescriptor);
These calls are required before using the new pseudoterminal5.
char* deviceName = ptsname(fileDescriptor); ... modbus_t* context = modbus_new_rtu(deviceName, BAUD, PARITY, DATA_BITS, STOP_BITS);
We get the device name (i.e. a file path under /dev) and pass it to libmodbus. If these two calls are not in the same function, then it would be better to make a copy of the deviceName string; this will be overwritten at the next call to ptsname.
The Modbus context can now be used to open the device and set the station address. We can send Modbus messages, but the thread calling into libmodbus will block until it gets a reply, error, or a timeout message. We will need to start a new thread to handle the other end. This thread will loop indefinitely copying the data back and forth:
ssize_t readCount = read(fileDescriptor, requestBuffer, sizeof requestBuffer); // from libmodbus /* send the request to the device over SPI */ ... /* receive a reply from the device */ ... ssize_t writeCount = write(fileDescriptor, replyBuffer, replySize); // to libmodbus
This part of the solution is potentially awkward in the general case. How do we know how many bytes should be read and written at each iteration of the loop?
In this example we are helped by the Modbus protocol which specifies timing boundaries within and between messages. The message boundaries are identified by timeout without any other protocol logic required. To read, we request a large amount of data (sizeof requestBuffer) which will be set according to the maximum possible size of a Modbus message. We are returned the actual size of the next message sent by libmodbus. To write the reply, we read bytes from the SPI device until we identify the message boundary by timeout then send this batch to libmodbus.
In other request-response protocols it might be necessary to apply some basic message parsing to identify length fields or start-stop markers. In the worst case, or to support full-duplex operation, the read and write connections can be handled separately and in parallel6.
modbus_close(context); ... result = close(fileDescriptor);
The program should dispose of the pseudoterminal by closing both ends of the connection.
When you need to emulate a serial port interface on a UNIX system, consider using a pseudoterminal.
1 There may be other problems, for example a named pipe could return an error when the client attempts to set terminal attributes such as baud rate.
2 A pseudodevice provides interfaces typical of hardware devices but has no actual hardware associated with it.
3 In UNIX systems, teletype devices and serial ports are closely related. For example, the typical naming of a hardware serial port /dev/ttyS0 assumes it will be used to connect a hardware terminal.
4 The samples omit lines of code not directly concerned with operating the pseudoterminal. Within a snippet omitted lines are indicated by ellipses. (…) This is most commonly error checking, though we do show where result codes are available by capturing returned values.
5 These library functions exist due to historical concerns about user terminal security.
6 The question still exists of how much data to transfer per iteration but this is a performance (latency and throughput) concern, not one of correctness.
Interested in other ITDev Linux blogs?
Here at ITDev, we often work on client projects that use the Linux operating system, see this search index to other Linux related blogs.
Embedded Linux Interest Group
ITDev runs an Embedded Linux Interest Group to inform and support industry. If you are interested in learning more about Linux as well as attending events and workshops, sign up to our Interest Group here:
How ITDev Can Help
As a provider of software and electronics design services, we are often working with the Linux and Android operating systems. We have extensive experience in developing, modifying, building, debugging and bring up of these operating systems be it for new developments or working with existing kernels to demonstrate new device technologies and features.
We offer advice and assistance to companies considering to use or already using Linux or Android on their embedded system. If you have any questions or would like to find out more, we would be delighted to speak to you. Email us or call us on +44 (0)23 8098 8890.