为什么要用纯C语言
为了数据流动加速,实现低配CPU建立高速数据流而不用CPU干预,避免串口数据流多次反复上升到软件应用层又下降低到硬件协议层。
关于termios.h
麻烦的是,在 Linux 中使用串口并不是一件最简单的事情。在处理 termios.h
标头时,有许多挑剔的设置隐藏在多个字节的位字段中。本文将试图帮助解释这些设置并向您展示如何在 Linux 中正确配置串行端口。
一切都是文件
在典型的 UNIX 风格中,串行端口由操作系统中的文件表示。这些文件通常在 /dev/ 中弹出,并以名称 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 生成。
下图展示了一块常见的开发板提供的串口设备:
要写入串行端口,请写入文件。要从串行端口读取,请从文件中读取。当然,这允许您发送/接收数据,但是如何设置串口参数,例如波特率、奇偶校验等。这是由特殊的 tty 配置 struct 设置的。
开发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
。这通常是因为当前用户不属于dialout
组的一部分而发生。使用以下命令将当前用户添加到 dialout
组:
sudo adduser $USER dialout
上述命令没有立即生效。您可以选择注销并重新登录,也可以使用其它工具让它立即生效。
此时,我们可以从技术上读取和写入串行端口,但它可能不起作用,因为默认配置设置不是为串行端口使用而设计的。所以现在我们将正确设置配置。
修改任何配置值时,最佳做法是仅修改您感兴趣的bit位,并保持字段的所有其它bit位不变。这就是为什么您会在下面看到设置位时使用 &=
或 |=
,而不是 =
。
串口启动配置
我们需要访问 termios
结构才能配置串行端口。我们将创建一个新的 termios
结构体,然后使用 tcgetattr()
将串口的现有配置写入其中,然后根据需要修改参数并使用 tcsetattr()
// Create new termios struct, 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
// NOTE: This is important! POSIX states that the struct passed to tcsetattr()
// must have been initialized with a call to tcgetattr() overwise behaviour
// is undefined
if(tcgetattr(serial_port, &tty) != 0) {
printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
}
我们现在可以根据需要更改 tty 的设置,如以下部分所示。在我们开始之前,如果您感兴趣的话,这里是 termios 结构的定义(从 termbits.h 中提取):
struct termios {
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 */
};
串口参数配置c_cflags
termios
结构的 c_cflag
成员包含控制参数字段。
PARENB (Parity)
如果设置该位,则启用奇偶校验位的生成和检测。大多数串行通信不使用奇偶校验位,因此如果您不确定,请清除该位。
tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
tty.c_cflag |= PARENB; // Set parity bit, enabling parity
CSTOPB (停止位)
如果设置该位,则使用两个停止位。如果该位被清除,则仅使用一个停止位。大多数串行通信仅使用一个停止位。
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<number>
字段设置通过串行端口每个字节传输多少数据位。这里最常见的设置是 8 ( CS8
)。如果你不确定的话,一定要使用这个,我以前从来没有使用过串口,之前没有使用过8(但它们确实存在)。在使用 &= ~CSIZE
设置任何大小位之前,您必须清除所有大小位。
tty.c_cflag &= ~CSIZE; // Clear all the size bits, then use one of the statements below
tty.c_cflag |= CS5; // 5 bits 每字节
tty.c_cflag |= CS6; // 6 bits 每字节
tty.c_cflag |= CS7; // 7 bits 每字节
tty.c_cflag |= CS8; // 8 bits 每字节 (most common)
CRTSCTS(硬件流控制)
如果设置了 CRTSCTS
字段,则启用硬件RTS/CTS
流控制。这是当端点之间有两条额外的电线时,用于在数据准备好发送/接收时发出信号的情况。这里最常见的设置是禁用它。在应该禁用它的时候启用它可能会导致您的串行端口接收不到数据,因为发送者将无限期地缓冲它,等待您“准备好”。少于3根线的串口一定没有这个功能,应该禁用。
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
信号,这通常是一件好事。设置 CREAD
允许我们读取数据(我们绝对想要这样!)。
tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
c_lflag
禁用规范模式
UNIX系统提供两种基本的输入模式:规范模式和非规范模式。在规范模式下,当收到新行字符时处理输入。接收应用程序逐行接收该数据。在处理串行端口时,这通常是不受欢迎的,因此我们通常希望禁用规范模式。禁用规范模式:
tty.c_lflag &= ~ICANON;
此外,在规范模式下,某些字符(例如退格键)会被特殊处理,用于编辑当前文本行(擦除)。同样,如果处理原始串行数据,我们不希望使用此功能,因为它会导致特定字节丢失!
回应(Echo)
如果设置了该位,发送的字符将被回显。因为我们禁用了规范模式,所以我认为这些位实际上没有做任何事情,但以防万一禁用它们也没有什么坏处!串口默认启用了这个模式,因为测试硬件的正确性经常需要TX/RX短接。
tty.c_lflag &= ~ECHO; // Disable echo
tty.c_lflag &= ~ECHOE; // Disable erasure
tty.c_lflag &= ~ECHONL; // Disable new-line echo
禁用信号字符
当设置 ISIG
位时,将解释 INTR
、 QUIT
和 SUSP
字符。我们不希望使用串行端口,因此请清除此位:
tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP
输入模式(c_iflag
)
termios
的输入流与输出流是分开配置的。因为大部分场景下输入流与输出流的配置相同,所以termios
显得比较麻烦。termios
结构的 c_iflag
成员包含输入处理的低级设置。 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
)
termios
结构的 c_oflag
成员包含输出处理的低级设置。配置串行端口时,我们希望禁用对输出字符/字节的任何特殊处理,因此请执行以下操作:
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 和 VTIME 是许多程序员在尝试在 Linux 中配置串行端口时感到困惑的根源。需要注意的重要一点是, 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 都定义为类型 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 :
// 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
Linux 的某些实现提供了一个辅助函数 cfsetspeed() ,它同时设置输入和输出速度:
cfsetspeed(&tty, B9600);
自定义波特率
由于您现在完全意识到配置 Linux 串行端口并非小事,因此您可能不会因为设置自定义波特率同样困难而感到困惑。没有可移植的方法来执行此操作,因此请准备好尝试以下代码示例,以了解哪些内容适用于您的目标系统。
GNU/Linux 方法
如果您使用 GNU C 库进行编译,则可以放弃上面的标准枚举,只需直接为 cfsetispeed()
和 cfsetospeed()
指定整数波特率,例如:
// Specifying a custom baud rate when using GNU C
cfsetispeed(&tty, 104560);
cfsetospeed(&tty, 104560);
termios2方法
此方法依赖于使用 termios2 结构,该结构类似于 termios 结构,但功能明显更多。我不确定 termios2 到底是在什么 UNIX 系统上定义的,但如果是的话,它通常是在 termbits.h 中定义的(它是在我正在做的带有 GCC 系统的 Xubuntu 18.04 上)这些测试):
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 */
};
这与普通的旧 termios 非常相似,除了添加了 c_ispeed 和 c_ospeed 。我们可以使用这些来直接设置自定义波特率!我们几乎可以以与 termios 完全相同的方式设置除波特率之外的所有内容,除了从文件描述符读取/写入终端属性之外 - 而不是使用 tcgetattr() 和 tcsetattr() 我们必须使用 ioctl() 。
让我们首先更新我们的包含,我们必须删除 termios.h 并添加以下内容:
// #include <termios.h> This must be removed!
// Otherwise we'll get "redefinition of ‘struct termios’" errors
#include <sys/ioctl.h> // Used for TCGETS2/TCSETS2, which is required for custom baud rates
struct termios2 tty;
// Read in the terminal settings using ioctl instead
// of tcsetattr (tcsetattr only works with termios, not termios2)
ioctl(fd, TCGETS2, &tty);
// Set everything but baud rate as usual
// ...
// ...
// Set custom baud rate
tty.c_cflag &= ~CBAUD;
tty.c_cflag |= CBAUDEX;
// On the internet there is also talk of using the "BOTHER" macro here:
// tty.c_cflag |= BOTHER;
// I never had any luck with it, so omitting in favour of using
// CBAUDEX
tty.c_ispeed = 123456; // What a custom baud rate!
tty.c_ospeed = 123456;
// Write terminal settings to file descriptor
ioctl(serial_port, TCSETS2, &tty);
请阅读上面关于 BOTHER 的评论。也许在你的系统上这个方法会起作用!
并非所有硬件都支持所有波特率,因此如果可以选择,最好坚持使用上述标准 BXXX 速率之一。如果您不知道波特率是多少,并且尝试与第三方系统通信,请尝试 B9600 ,然后 B57600 ,然后 B115200 因为它们是最常见的波特率。
使配置生效
更改这些设置后,我们可以使用 tcsetattr()
传递 tty termios 结构到硬件:
// Save tty settings, also checking for error
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
}
读写串口数据
现在我们已经打开并配置了串口,我们可以对其进行读写了!
对Linux串口的写入是通过 write() 函数完成的。我们使用上面调用 open() 返回的 serial_port 文件描述符。
unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
write(serial_port, msg, 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()
int main() {
// 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 struct, 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));
return 1;
}
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 &= ~CSIZE; // Clear all bits that set the data size
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));
return 1;
}
// Write to serial port
unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
write(serial_port, msg, 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));
return 1;
}
// 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);
return 0; // success
};
独占串口设备
谨慎的做法是尝试阻止其他进程同时读取/写入串行端口。实现此目的的一种方法是使用 flock() 系统调用
if(flock(fd, LOCK_EX | LOCK_NB) == -1) {
//输出错误消息
}
获取RX有多少个字节可读取
您可以将 FIONREAD 与 ioctl() 一起使用来查看串行端口 1 的操作系统输入(接收)缓冲区中是否有任何可用字节。这在轮询式方法中非常有用,其中应用程序在尝试读取字节之前定期检查字节。
#include <unistd.h>
#include <termios.h>
int main() {
// ... get file descriptor here
// See if there are bytes available to read
int bytes;
ioctl(fd, FIONREAD, &bytes);
}
ioctl() 函数将提供的指向整数 bytes 的指针写入可从串行端口读取的字节数。尽管获取和设置终端设置是通过文件描述符完成的,但这些设置适用于终端设备本身,并将影响正在使用或将要使用该终端的所有其他系统应用程序。这也意味着在文件描述符关闭后,甚至在更改设置的应用程序终止后,终端设置更改仍然存在。
作者: | 岬淢箫声 |
日期: | 2023年11月1日 |
版本: | 1.0 |
链接: | http://caowei.blog.csdn.net |