Linux serial port configuration
In the 2023 season, the Taurs team of South China Agricultural University used x3 as the main control for the motion control of wheeled robot movement for the first time. In the process of exploration, some results of scheme construction were produced and shared with friends in the community. The series is roughly divided into five parts and three parts.
- Serial port (Dbus/Sbus protocol supports remote control to send topic control robot)
- ros2control framework construction (two parts)
- usb2can module and peripheral sensor communication (two parts)
Can be achieved from the single chip computer instead of using x3 to achieve the basic control scheme to build, the series aims to help readers from zero to achieve the robot on the basis of sensor feedback “move”, the subsequent series will have a more in-depth control scheme to share, due to the limited level of the author, the writing will inevitably have flaws, Please feel free to contact me in the community or send an email to me. Thank you again to Horizon for helping Taurus team in 2023!
Overview
In the Linux operating system, all devices are abstracted into a file, and serial ports are used in a similar way. When the system detects the serial device, it will put the abstract file in the ‘/dev/’ directory, but different types of serial devices have different naming methods.
When a device that uses serial communication (such as a remote control receiver) is inserted into the Rising Sun x3, the system will name it as’ /dev/ttyS3 ‘, which means that the inserted device is a ‘standard serial device’, but if a device such as USB-485 is inserted, the system will identify it as’ /dev/ttyUSB0 ', most USB-to-serial devices Will be so identified, there are many similar different naming methods, do not go into details here.
Since it is a file, we can use file I/O to read and write operations, here I want to explain that we read and write serial files without buffering file I/O that is,open,read and write functions, rather than the standard IO library, so the programmer needs to consider the length of the cache (’ BUFFER_SIZE ') Degree.
In this paper, the baud rate is customized on the basis of serial port and the serial port hardware is used to adapt the Dbus protocol so as to realize the communication with DJI DT7&DR16 2.4GHz remote control.
Open the device (file)
Add the necessary header files first:
// Standard header file
#include <stdio.h>
#include <string.h>
// System-related header files
#include <fcntl.h> // Includes macros for how the file is opened
#include <unistd.h> // write(), read(), close()
To insert the serial device, run commands
ls -m /dev | grep tty
List the corresponding serial port devices and select the newly inserted serial port to open it.
int fd = open("/dev/ttyS3", O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0)
{
std::cout<<"[robot_dbus] Unable to open dbus\n"<<std::endl;
}
The code calls the library function open to open the corresponding file, which receives two parameters (in the case of not creating a new file), the first is the open path of the file, the second is the open mode.
Here is a list of the open modes currently in use, and interested readers can check other modes in the manual
O_RDWR: Enable read and write. My serial port device needs to send and receive two actions, so both permissions need to be enabled.
O_NOCTTY: if the path (/dev/ttyS3) references a terminal device, the device is not assigned as the control terminal of this process. If this mode is not set, the reading of the serial port is affected by the stop signal.
O_SYNC: makes a write wait happen every time.
Function returns a file descriptor, in Linux platform, the file descriptor is a unique non-negative integer, all open files can be referenced by the file description, for example, read (read), write (write) the file, are used to manipulate the open file descriptor.
About O_SYNC
Setting O_SYNC in file open mode causes each write operation to wait until data has been written to disk before returning. In Linux,wirte simply queues the data, and the actual write operation may occur at some later point, which is not allowed for real-time control systems.
When O_SYNC is used, it is known that data has been written to the device when it is returned from write, ensuring real-time data and data loss in the event of a system exception.
But at the same time, such a setting will also bring the cost of system time and clock time, and more tests should be carried out when it is really used to see whether the current scheme can meet the control needs.
Common mistakes
There are several possible reasons why the above code might be wrong
- The file path is incorrect
- Permission issues
When you encounter a related error, you can refer to the relevant header file of the error message and diagnose the error through the relevant error message.
#include <errno.h> // API for error messages
In the above code (after you add it to open) add the following code:
// Check for errors
if (serial_port < 0) {
printf("Error integer %i from open: %s\n", errno, strerror(errno));
}
If errno = 2 and strerror(errno) returns No such file or directory, check the serial port path carefully and check whether the device is properly inserted and whether the access pin is correct.
If errno = 13 and strerror(errno) returns Permission denied, the device permission may not be granted to the user. In this case, you can use this command temporarily
sudo chmod 777 /dev/ttyS3
Give the user the appropriate permissions, the formal project should use the relevant startup file to give all permissions to the user, the following article will have the relevant explanation.
Device related Settings
In Linux, the device features that can be set are located in the ‘termios’ structure, and the use of this structure requires the inclusion of the’ termios.h 'header file.
In this paper, first of all, the structure and the header file do related brief description, and then the complete implementation of the serial driver related Settings, and explain the relevant reasons.
struct termios2 {
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
The device is set up by setting the contents of the above structure and calling the function ioctl to apply to the related device.
struct termios options;
ioctl(fd, TCGETS2, &options); // Be sure to call this function to initialize the serial port configuration
The content here involves the relevant knowledge points of terminal IO in Linux, the content is relatively complicated, readers interested in this part can find relevant materials and books by themselves.
So how do you set the contents of this struct member?
In short, the input flag (c_iflag) controls the input of characters by controlling the terminal input device (such as allowing parity of the input), the output flag (c_oflag) controls the output of the driver, the control flag bit (c_cflag) controls the serial input device, and the local flag (c_lflag) affects the driver and the user Interface between.
Next, we set the relevant parameters for the serial device.
c_cflag
The serial port needs to set parity bits to verify data correctness. The parity bit tells the receiver in the data frame whether there was an error in a communication. The Settings here are often closely related to the characteristics of the communication device to be used. For details, refer to the user manual for using peripherals. The device used here is the DJI DT7&DR16 2.4GHz remote control acceptance system, so it is required to use even check, so this bit is enabled:
options.c_cflag |= PARENB;
If your device does not set this bit, clear this bit:
options.c_cflag &= ~PARENB;
The next step is to set the stop bit. If you only use one stop bit, you can clear it.
options.c_cflag &= ~CSTOPB;
If two stop bits are used, you need to:
options.c_cflag |= CSTOPB;
The significant bit in the next data bit is generally set to 8 bits
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
8bit is the most commonly used, and if the sensor used indicates another design, you can choose from the Settings below
options.c_cflag |= CS5; // 5 bits per byte
options.c_cflag |= CS6; // 6 bits per byte
options.c_cflag |= CS7; // 7 bits per byte
The next step is to set the hardware flow control to disable, which can cause your serial port to be unable to accept data if you do not have a specific need to enable it
options.c_cflag &= ~CRTSCTS;
CREAD and CLOCAL must be set to ensure that the program is not interfered with by other ports and can read data correctly.
c_lflag
The c_lflag setting determines how the serial port handles input characters
When processing serial data, it is usually possible to use non-canonical mode, which tells the driver to process the data in c by c rather than line by line.
options.c_lflag &= ~ICANON;
Enable data echo (optional)
options.c_lflag &= ~ECHO;
options.c_lflag &= ~ECHOE;
options.c_lflag &= ~ECHONL;
When dealing with serial data, symbols such as carriage returns should not have any effect on the data transmission, so make the following Settings
options.c_lflag &= ~ISIG;
c_iflag
Clear software flow control
options.c_iflag &= ~(IXON | IXOFF | IXANY);
Since we only want to receive the sent data completely, we do not want the data generated by the data to be processed, so the following Settings are made
options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);
c_oflag
The setting here is similar to the c_iflag setting, the special processing of the sent data can be disabled (carriage return, etc.):
options.c_oflag &= ~OPOST;
options.c_oflag &= ~ONLCR;
c_cc
Just configure the serial port to be in non-blocking mode, and the read()API will not do any waiting.
options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 0;
Baud rate customization
Here for the baud rate customization is more critical, if you just want a few relatively common baud rates, such as the following:
B0, B50, B75, B110, B134, B150, B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800
Call the following API:
Call the following API:
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
If you want to customize the baud rate, then you need to set c_ispeed and c_ospeed in the structure respectively, and enable the flag bit of the customized baud rate
options.c_cflag &= ~CBAUD;
options.c_ispeed = 100000;
options.c_ospeed = 100000;
At this point, the configuration of the serial port is basically complete. The following describes the complete configuration.
void usart_init(const char* serial)
{
int fd = open(serial, O_RDWR | O_NOCTTY | O_SYNC);
struct termios2 options
{
};
ioctl(fd, TCGETS2, &options);
if (fd == -1)
{
std::cout<<"[warrior_dbus] Unable to open dbus\n"<<std::endl;
}
// Even parity(8E1):
options.c_cflag &= ~CBAUD;
options.c_cflag |= BOTHER;
options.c_cflag |= PARENB; -
options.c_cflag &= ~PARODD;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_ispeed = 100000;
options.c_ospeed = 100000;
options.c_iflag &= ~(IXON | IXOFF | IXANY);
options.c_iflag &= ~IGNBRK; // disable break processing
/* set input mode (non−canonical, no echo,...) * /
options.c_lflag = 0;
options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 0;
options.c_oflag = 0; // no remapping, no delays
options.c_cflag |= (CLOCAL | CREAD); // ignore modem controls, enable reading
ioctl(fd, TCSETS2, &options);
port_ = fd;
}
read and send, call the read() and write() interfaces to unpack the protocol and send according to the protocol.