Linux 系统中用C语言调用串口

总览

不幸的是,在Linux中使用串行端口并不是世界上最简单的事情。在处理termios.h标头时,存在许多复杂的设置,这些设置隐藏在价值多个字节的位域中。该页面试图帮助解释这些设置,并向您展示如何在Linux中正确配置串行端口。

一切都是文件

在典型的UNIX风格中,串行端口由操作系统中的文件表示。这些文件通常会弹出/dev/,并以name开头tty*。

常用名称是:

  • /dev/ttyACM0-ACM代表USB总线上的ACM调制解调器。Arduino UNO(及类似名称)将使用此名称显示。
  • /dev/ttyPS0 -运行基于Yocto的Linux构建的Xilinx Zynq FPGA将使用此名称作为Getty连接到的默认串行端口。
  • /dev/ttyS0-标准COM端口将具有此名称。如今,由于较新的台式机和笔记本电脑没有实际的COM端口,这些情况已经不太普遍了。
  • /dev/ttyUSB0 -大多数USB到串行电缆将使用这样的文件显示。
  • /dev/pts/0-伪终端。这些可以通过生成socat。
    Linux和连接的Arduino的/dev/目录的列表。 Arduino串行端口显示为/dev/ttyACMO0。
    Linux和连接的Arduino的/dev/目录的列表。Arduino串行端口显示为/dev/ttyACMO0。

要写入串行端口,请写入文件。要从串行端口读取,请从文件读取。当然,这允许您发送/接收数据,但是如何设置串行端口参数,例如波特率,奇偶校验等?这是通过特殊tty配置设置的struct。

C语言的基本设置

  • 注意
    此代码也适用于C ++。
    首先,我们要包括一些内容:
// C library headers
#include <stdio.h>
#include <string.h>

// Linux headers
#include <fcntl.h> // Contains file controls like O_RDWR
#include <errno.h> // Error integer and strerror() function
#include <termios.h> // Contains POSIX terminal control definitions
#include <unistd.h> // write(), read(), close()
然后,我们要打开串行端口设备(在下方显示为文件/dev/),保存由返回的文件描述符open():

int serial_port = open("/dev/ttyUSB0", O_RDWR);

# Check for errors
if (serial_port < 0) {
    printf("Error %i from open: %s\n", errno, strerror(errno));
}

一个你可能会看到这里的常见错误是errno = 2,和strerror(errno)回报No such file or directory。确保您具有正确的设备路径,并且该设备存在!

您可能会在这里遇到的另一个常见错误errno = 13是Permission denied。这通常是因为当前用户不属于拨出组。使用以下命令将当前用户添加到拨出组:

$ sudo adduser $USER dialout

这些组更改生效之前,您必须先注销然后重新登录。
在这一点上,我们可以从技术上对串行端口进行读写,但是由于默认配置设置不是为串行端口设计的,因此它可能无法工作。因此,现在我们将正确设置配置。
修改任何配置值时,最佳做法是仅修改您感兴趣的位,而保留该字段的所有其他位。这就是为什么你会看到使用下面&=或者|=,从来没有&或|设置位时。

配置设置

我们需要访问该termios结构才能配置串行端口。我们将创建一个新termios结构,然后使用写入串行端口的现有配置tcgetattr(),然后根据需要修改参数并使用保存设置tcsetattr()。

// Create new termios struc, we call it 'tty' for convention
// No need for "= {0}" at the end as we'll immediately write the existing
// config to this struct
struct termios tty;

// Read in existing settings, and handle any error
if(tcgetattr(serial_port, &tty) != 0) {
    printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
}

现在,我们可以tty根据需要更改的设置,如以下各节所示。

控制模式(c_cflags)

该结构的c_cflags成员termios包含控制参数字段。

  • PARENB(平价)
    如果该位置1,则启用奇偶校验位的生成和检测。大多数串行通信不使用奇偶校验位,因此,如果不确定,请清除该位。
tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
tty.c_cflag |= PARENB;  // Set parity bit, enabling parity

CSTOPB(数字停止位)
如果该位置1,则使用两个停止位。如果清除该位,则仅使用一个停止位。大多数串行通信仅使用一个停止位。

tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
tty.c_cflag |= CSTOPB;  // Set stop field, two stop bits used in communication

每字节位数

该CS字段中设置多少个数据位,每个字节通过串行端口传输。此处最常见的设置是8(CS8)。如果不确定,请绝对使用此端口,在此之前我从未使用过未使用8的串行端口(但它们确实存在)。

tty.c_cflag |= CS5; // 5 bits per byte
tty.c_cflag |= CS6; // 6 bits per byte
tty.c_cflag |= CS7; // 7 bits per byte
tty.c_cflag |= CS8; // 8 bits per byte (most common)

流量控制(CRTSCTS)

如果CRTSCTS设置了该字段,则启用硬件RTS / CTS流控制。这里最常见的设置是禁用它。当应禁用此功能时启用它可能会导致您的串行端口不接收任何数据,因为发送方将无限期地对其进行缓冲,等待您“就绪”。

tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
tty.c_cflag |= CRTSCTS;  // Enable RTS/CTS hardware flow control

CREAD和CLOCAL

设置CLOCAL将禁用调制解调器特定的信号线,例如载波检测。SIGHUP当检测到调制解调器断开连接时,还可以防止控制过程发送信号,这通常是一件好事。设置CLOCAL使我们能够读取数据(我们绝对想要!)。

tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)

本地模式(c_lflag)

  • 禁用规范模式
    UNIX系统提供输入,的两种基本模式的规范和非规范模式。在规范模式下,当收到换行符时,将处理输入。接收应用程序逐行接收该数据。在处理串行端口时,这通常是不希望的,因此我们通常要禁用规范模式。

通过以下方式禁用了规范模式:

tty.c_lflag &= ~ICANON;

同样,在规范模式下,某些字符(例如退格键)会被特殊对待,并用于编辑当前文本行(擦除)。同样,我们不希望此功能处理原始串行数据,因为它将导致特定字节丢失!

ECHO

如果该位置1,发送的字符将被回显。因为我们禁用了规范模式,所以我认为这些位实际上没有任何作用,但是以防万一,以防万一!

tty.c_lflag &= ~ECHO; // Disable echo
tty.c_lflag &= ~ECHOE; // Disable erasure
tty.c_lflag &= ~ECHONL; // Disable new-line echo

禁用信号字符

当该ISIG位置1时INTR,QUIT和会SUSP被解释。我们不希望使用串行端口,因此请清除以下位:

tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP

输入模式(c_iflag)

该结构的c_iflag成员termios包含用于输入处理的低级设置。所述c_iflag构件是一个int。

软件流控制(IXOFF,IXON,IXANY)

结算IXOFF,IXON并IXANY禁用软件流控制,这是我们不想要的:

tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl

禁用接收时字节的特殊处理

清除以下所有位将禁用对字节的任何特殊处理,因为这些字节在被串行端口接收并传递给应用程序之前。我们只想要原始数据,谢谢!

tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes

输出模式(c_oflag)

结构的c_oflag成员termios包含用于输出处理的低级设置。配置串行端口时,我们要禁用对输出字符/字节的任何特殊处理,因此请执行以下操作:

tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
// tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT IN LINUX)
// tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT IN LINUX)

双方OXTABS并ONOEOT没有在Linux的定义。但是Linux确实具有XTABS似乎相关的领域。为Linux编译时,我只排除了这两个字段,并且串行端口仍然可以正常工作。

VMIN和VTIME(c_cc)

VMIN对于试图在Linux中配置串行端口的许多程序员来说,这VTIME是一个混乱的根源。

需要注意的重要一点是,VTIME根据内容的不同,含义也有所不同VMIN。当VMIN为0时,VTIME指定从read()调用开始的超时。但是当VMIN>> 0时,VTIME指定从第一个接收到的字符开始的超时时间。

让我们探索不同的组合:

  • VMIN = 0,VTIME = 0:无阻塞,立即返回可用值
  • VMIN> 0,VTIME = 0:这将read()始终等待字节(确切地由决定多少个字节VMIN),因此read()可以无限期地阻塞。
  • VMIN = 0,VTIME> 0:这是对最大超时(由给出VTIME)的任何数字字符的阻塞读取。read()将阻塞直到有大量数据可用或发生超时为止。这恰好是我最喜欢的模式(也是我最常使用的模式)。
  • VMIN> 0,VTIME> 0:阻塞直到VMIN接收到任何字符或VTIME第一个字符过去。请注意,VTIME直到收到第一个字符,超时才会开始。
  • VMIN和VTIME都定义为type cc_t,我一直看到它是unsigned char(1个字节)的别名。这使VMIN字符数的上限为255,最大超时为25.5秒(255分秒)。

“一旦收到任何数据,立即返回”并不意味着您一次只能得到1个字节。根据操作系统延迟,串行端口速度,硬件缓冲区以及您无法直接控制的许多其他因素,您可能会收到任意数量的字节。

例如,如果我们要等待最多1秒的时间,一旦收到任何数据就返回,我们可以使用:

tty.c_cc[VTIME] = 10;    // Wait for up to 1s (10 deciseconds), returning as soon as any data is received.
tty.c_cc[VMIN] = 0;

波特率

而不是使用位字段与所有其他设置,串口波特率是通过调用函数集cfsetispeed()和cfsetospeed(),传递的一个指针tty结构和enum:

// Set in/out baud rate to be 9600
cfsetispeed(&tty, B9600);
cfsetospeed(&tty, B9600);

如果要保持UNIX兼容,则必须从以下一项中选择波特率:

B0,  B50,  B75,  B110,  B134,  B150,  B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800

如果使用GNU C库进行编译,则可以放弃这些枚举,而直接指定整数波特率,例如:

// Specifying a custom baud rate when using GNU C
cfsetispeed(&tty, 104560);
cfsetospeed(&tty, 104560);

并非所有硬件都支持所有波特率,因此如果可以选择的话,最好坚持使用上述标准BXXX速率之一。如果您不知道波特率是多少,并且尝试与第三方系统通信,请尝试B9600,然后B57600再尝试,B115200因为它们是最常用的速率。

  • 有关Linux串行端口代码示例,请参见https://github.com/gbmhunter/CppLinuxSerial。

保存termios

更改这些设置后,我们可以使用以下命令保存ttytermios结构tcsetattr():

// Save tty settings, also checking for error
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
    printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
}

读写

现在我们已经打开并配置了串行端口,我们可以对其进行读写了!

  • 写入
    通过该write()功能完成对Linux串行端口的写入。我们使用serial_port从open()上面的调用返回的文件描述符。
unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
write(serial_port, "Hello, world!", sizeof(msg));

读取

通过该read()功能进行读取。您必须为Linux提供缓冲区以将数据写入其中。

// Allocate memory for read buffer, set size according to your needs
char read_buf [256];

// Read bytes. The behaviour of read() (e.g. does it block?,
// how long does it block for?) depends on the configuration
// settings above, specifically VMIN and VTIME
int n = read(serial_port, &read_buf, sizeof(read_buf));

// n is the number of bytes read. n may be 0 if no bytes were received, and can also be negative to signal an error.

关闭串口

这很简单:

close(serial_port)

完整的例子

// C library headers
#include <stdio.h>
#include <string.h>

// Linux headers
#include <fcntl.h> // Contains file controls like O_RDWR
#include <errno.h> // Error integer and strerror() function
#include <termios.h> // Contains POSIX terminal control definitions
#include <unistd.h> // write(), read(), close()

// Open the serial port. Change device path as needed (currently set to an standard FTDI USB-UART cable type device)
int serial_port = open("/dev/ttyUSB0", O_RDWR);

// Create new termios struc, we call it 'tty' for convention
struct termios tty;

// Read in existing settings, and handle any error
if(tcgetattr(serial_port, &tty) != 0) {
    printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
}

tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
tty.c_cflag |= CS8; // 8 bits per byte (most common)
tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)

tty.c_lflag &= ~ICANON;
tty.c_lflag &= ~ECHO; // Disable echo
tty.c_lflag &= ~ECHOE; // Disable erasure
tty.c_lflag &= ~ECHONL; // Disable new-line echo
tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl
tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes

tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
// tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT ON LINUX)
// tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT ON LINUX)

tty.c_cc[VTIME] = 10;    // Wait for up to 1s (10 deciseconds), returning as soon as any data is received.
tty.c_cc[VMIN] = 0;

// Set in/out baud rate to be 9600
cfsetispeed(&tty, B9600);
cfsetospeed(&tty, B9600);

// Save tty settings, also checking for error
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
    printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
}

// Write to serial port
unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
write(serial_port, "Hello, world!", sizeof(msg));

// Allocate memory for read buffer, set size according to your needs
char read_buf [256];

// Normally you wouldn't do this memset() call, but since we will just receive
// ASCII data for this example, we'll set everything to 0 so we can
// call printf() easily.
memset(&read_buf, '\0', sizeof(read_buf);

// Read bytes. The behaviour of read() (e.g. does it block?,
// how long does it block for?) depends on the configuration
// settings above, specifically VMIN and VTIME
int num_bytes = read(serial_port, &read_buf, sizeof(read_buf));

// n is the number of bytes read. n may be 0 if no bytes were received, and can also be -1 to signal an error.
if (num_bytes < 0) {
    printf("Error reading: %s", strerror(errno));
}

// Here we assume we received ASCII data, but you might be sending raw bytes (in that case, don't try and
// print it to the screen like this!)
printf("Read %i bytes. Received message: %s", num_bytes, read_buf);

close(serial_port)

getty的问题

如果getty试图管理tty要与之进行串行通信的同一设备,它可能会引起串行通信问题。

停止getty:

getty很难停止,因为默认情况下,如果您尝试杀死进程,那么新进程将立即启动。

这些说明适用于旧版本的Linux和/或嵌入式Linux。

加载/etc/inittab您喜欢的文本编辑器。
注释掉涉及getty到您的tty设备的所有行。
保存并关闭文件。
运行命令~$ init q以重新加载/etc/inittab文件。
终止设备上所有正在运行的getty进程tty。他们现在应该死了!

进程独立占用

最好尝试同时防止其他进程对串行端口进行读/写操作。
实现此目的的一种方法是使用flock()系统调用:

#include <sys/file.h>

int main() {

    // ... get file descriptor here

    // Acquire non-blocking exclusive lock
    if(flock(fd, LOCK_EX | LOCK_NB) == -1) {
        throw std::runtime_error("Serial port with file descriptor " + 
            std::to_string(fd) + " is already locked by another process.");
    }

    // ... read/write to serial port here
}

例子

有关Linux串行端口代码示例,请参见https://github.com/gbmhunter/CppLinuxSerial(请注意,该库是用C ++而不是C编写的)。

外部资源

有关结构配置参数的官方规范,请参见http://www.gnu.org/software/libc/manual/html_node/Terminal-Modes.htmltermios。