Linux纯C串口开发

news/2024/10/23 9:40:25/

为什么要用纯C语言

为了数据流动加速,实现低配CPU建立高速数据流而不用CPU干预,避免串口数据流多次反复上升到软件应用层又下降低到硬件协议层。

在这里插入图片描述

关于termios.h

麻烦的是,在 Linux 中使用串口并不是一件最简单的事情。在处理 termios.h 标头时,有许多挑剔的设置隐藏在多个字节的位字段中。本文将试图帮助解释这些设置并向您展示如何在 Linux 中正确配置串行端口。

在这里插入图片描述

一切都是文件

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

  1. /dev/ttyACM0: ACM 代表 USB 总线上的 ACM 调制解调器。 Arduino UNO(和类似的)将使用此名称出现。
  2. /dev/ttyPS0:运行基于 Yocto 的 Linux 版本的 Xilinx Zynq FPGA 将使用此名称作为 Getty 连接到的默认串行端口。
  3. /dev/ttyS0:通常情况下标准 COM 端口用的此名称。如今,由于较新的台式机和笔记本电脑没有实际的 COM 端口,这种情况已不太常见。
  4. /dev/ttyUSB0: 大多数 USB 转串口电缆将使用这样命名的文件显示。
  5. /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 位时,将解释 INTRQUITSUSP 字符。我们不希望使用串行端口,因此请清除此位:

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

输入模式(c_iflag

termios的输入流与输出流是分开配置的。因为大部分场景下输入流与输出流的配置相同,所以termios显得比较麻烦。termios 结构的 c_iflag 成员包含输入处理的低级设置。 c_iflag 成员是 int

软件流控制(IXOFFIXONIXANY

清除 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)

OXTABSONOEOT 在 Linux 中都没有定义。然而,Linux 确实有似乎相关的 XTABS 字段。当针对 Linux 进行编译时,我只是排除这两个字段,串行端口仍然可以正常工作。

VMINVTIME (c_cc)

VMIN 和 VTIME 是许多程序员在尝试在 Linux 中配置串行端口时感到困惑的根源。需要注意的重要一点是, VTIME 的含义略有不同,具体取决于 VMIN 的含义。当 VMIN 为 0 时, VTIME 指定从 read() 调用开始时的超时。但当 VMIN > 0 时, VTIME 指定从第一个接收到的字符开始算起的超时时间。让我们探索不同的组合:

  1. VMIN = 0,VTIME = 0:无阻塞,立即返回可用内容
  2. VMIN > 0,VTIME = 0:这将使 read() 始终等待字节(具体多少由 VMIN 确定),因此 read() 可能无限期阻塞。
  3. VMIN = 0,VTIME > 0:这是对任意数量的字符的阻塞读取,具有最大超时(由 VTIME 给出)。 read() 将阻塞,直到有任意数量的数据可用或发生超时。这恰好是我最喜欢的模式(也是我使用最多的模式)。
  4. 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 conventionstruct termios tty;// Read in existing settings, and handle any errorif(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 echotty.c_lflag &= ~ECHOE; // Disable erasuretty.c_lflag &= ~ECHONL; // Disable new-line echotty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSPtty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrltty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytestty.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 9600cfsetispeed(&tty, B9600);cfsetospeed(&tty, B9600);// Save tty settings, also checking for errorif (tcsetattr(serial_port, TCSANOW, &tty) != 0) {printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));return 1;}// Write to serial portunsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };write(serial_port, msg, sizeof(msg));// Allocate memory for read buffer, set size according to your needschar 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 VTIMEint 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 readint bytes;ioctl(fd, FIONREAD, &bytes);
}

ioctl() 函数将提供的指向整数 bytes 的指针写入可从串行端口读取的字节数。尽管获取和设置终端设置是通过文件描述符完成的,但这些设置适用于终端设备本身,并将影响正在使用或将要使用该终端的所有其他系统应用程序。这也意味着在文件描述符关闭后,甚至在更改设置的应用程序终止后,终端设置更改仍然存在。

作者:岬淢箫声
日期:2023年11月1日
版本:1.0
链接:http://caowei.blog.csdn.net

http://www.ppmy.cn/news/1191664.html

相关文章

Go 内存逃逸

内存逃逸&#xff08;memory escape&#xff09;是指在编写 Go 代码时&#xff0c;某些变量或数据的生命周期超出了其原始作用域的情况。当变量逃逸到函数外部或持续存在于堆上时&#xff0c;会导致内存分配的开销&#xff0c;从而对程序的性能产生负面影响。Go 编译器会进行逃…

Visual Studio Code 常用快捷键大全

Visual Studio Code 常用快捷键大全 快捷键是编码过程中经常使用&#xff0c;且能够极大提升效率的部分&#xff0c;这里给大家介绍一些VS Code中非常有用的快捷键。 打开和关闭侧边栏 Mac — Command B Windows — Ctrl B Ubuntu — Ctrl B 选择单词 Mac — Command D …

K8S运维 解决openjdk:8-jdk-alpine镜像时区和字体问题

目录 一、问题 二、解决 三、完整代码 一、问题 由于项目的Dockerfile中使用openjdk:8-jdk-alpine作为基础镜像来部署服务&#xff0c;此镜像存在一定问题&#xff0c;例如时差8小时问题&#xff0c;或是由于字体问题导致导出excel文件&#xff0c;图片处理内容为空等。 二…

Web3时代:探索DAO的未来之路

Web3 的兴起不仅代表着技术进步&#xff0c;更是对人类协作、创新和价值塑造方式的一次重大思考。在 Web3 时代&#xff0c;社区不再仅仅是共同兴趣的聚集点&#xff0c;而变成了一个价值交流和创新的平台。 去中心化&#xff1a;超越技术的革命 去中心化不仅仅是 Web3 的技术…

JVM修炼印记之初识

文章目录 JVM认识JVM的功能常见JVMHotSpot的发展历程 JVM认识 Java虚拟机&#xff08;Java Virtual Machine&#xff0c;JVM&#xff09;是一个用于执行Java字节码的虚拟计算机。它是Java语言的核心&#xff0c;可以在不同的操作系统和硬件平台上运行Java程序。 JVM负责将Java…

计算机网络之网络层(全)

网络层的功能 互联网在网络层的设计思路是&#xff0c;向上只提供简单灵活的、无连接的、尽最大努力交付的数据报服务。 路由器在能够开始向输出链路传输分组的第一位之前&#xff0c;必须先接收到整个分组&#xff0c;这种机制称为&#xff1a;存储转发机制 异构网络互连 用…

护眼台灯哪个品牌更好?超级专业的五个台灯品牌推荐

青少年的近视率持续升高&#xff0c;保护眼睛非常重要。台灯是用眼环境的必备品&#xff0c;而市面上款式多样不知如何购买。这期就来聊聊护眼台灯的选购问题&#xff01; 今天就说说照度是什么&#xff1f; 国A级和国AA级有什么区别&#xff1f; &#xff08;一&#xff0…

负载均衡深度解析:算法、策略与Nginx实践

引言 如今&#xff0c;网站和应用服务面临着巨大的访问流量&#xff0c;如何高效、稳定地处理这些流量成为了一个亟待解决的问题。负载均衡技术因此应运而生&#xff0c;它通过将流量合理分配到多个服务器上&#xff0c;不仅优化了资源的利用率&#xff0c;还大大提升了系统的…