Linux下应用层操作UART的四种方式
点击左上方蓝色“一口Linux”,选择“设为星标”
作者: 亚洲程序员盟主
串口文件
在linux中,针对所有的周边设备都提供了设备文件供用户访问,所以如果要访问串口,只要打开相关的设备文件即可。
在LInux下串口文件是位于/dev下的
-
COM1串口一为/dev/ttyS0
-
COM2串口2为/dev/ttyS1
或者
-
COM1串口一为/dev/ttyUSB0
-
COM2串口2为/dev/ttyUSB1
命令查询串口:
~$ ls /dev/ttyS*
/dev/ttyS0 /dev/ttyS12 /dev/ttyS16 /dev/ttyS2 /dev/ttyS23 /dev/ttyS27 /dev/ttyS30 /dev/ttyS6
/dev/ttyS1 /dev/ttyS13 /dev/ttyS17 /dev/ttyS20 /dev/ttyS24 /dev/ttyS28 /dev/ttyS31 /dev/ttyS7
/dev/ttyS10 /dev/ttyS14 /dev/ttyS18 /dev/ttyS21 /dev/ttyS25 /dev/ttyS29 /dev/ttyS4 /dev/ttyS8
/dev/ttyS11 /dev/ttyS15 /dev/ttyS19 /dev/ttyS22 /dev/ttyS26 /dev/ttyS3 /dev/ttyS5 /dev/ttyS9
方法1:轮询
1. 打开串口
fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1)
{
perror("open_port: Unable to open serial port");
return -1;
}
2. 配置串口
tcgetattr(fd, &options);
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS;
tcsetattr(fd, TCSANOW, &options);
其中,tcgetattr 和 tcsetattr 函数用于获取和设置串口参数。cfsetispeed 和 cfsetospeed 函数用于设置串口的输入和输出波特率,这里设置为 115200。options.c_cflag 表示控制标志位,用于配置串口控制参数,具体含义如下:
-
CLOCAL:忽略调制解调器的状态线,只允许本地使用串口。
-
CREAD:允许从串口读取数据。
-
PARENB:启用奇偶校验。&= ~PARENB则为禁用校验。
-
CSTOPB:使用两个停止位而不是一个。&= ~CSTOPB停止位为1。
-
CSIZE:表示字符长度的位掩码。在这里设置为 0,表示使用默认的 8 位数据位。
-
CS8:表示使用 8 位数据位。
-
CRTSCTS:启用硬件流控制,即使用 RTS 和 CTS 状态线进行流控制。
在示例程序中,我们将 CLOCAL 和 CREAD 标志位置为 1,表示允许本地使用串口,并允许从串口读取数据。我们将 PARENB、CSTOPB 和 CRTSCTS 标志位都设置为 0,表示不启用奇偶校验、使用一个停止位和禁用硬件流控制。最后,我们将 CSIZE 标志位设置为 0,然后将 CS8 标志位设置为 1,以表示使用 8 位数据位。
3. 读写
read(fd, buf, sizeof(buf)); // 返回接收个数
write(fd, buf, strlen(buf)); // 返回发送长度,负值表示发送失败
4. 关闭串口
close(fd);
完整示例
int open_port(const char *port)
{
int fd;
struct termios options;
// 打开串口设备
fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1) {
perror("open_port: Unable to open serial port");
return -1;
}
// 配置串口参数
tcgetattr(fd, &options);
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS;
tcsetattr(fd, TCSANOW, &options);
return fd;
}
int main()
{
int fd;
char buf[255];
int n;
// 打开串口设备
fd = open_port("/dev/ttyUSB0");
if (fd == -1) {
printf("open err\n");
exit(1);
}
while (1)
{
// 读取串口数据
n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Received: %.*s\n", n, buf);
}
// 发送串口数据
strcpy(buf, "Hello, world!\n");
n = write(fd, buf, strlen(buf));
if (n < 0) {
perror("write failed\n");
}
usleep(10 * 1000);
}
// 关闭串口设备
close(fd);
printf("close uart\n");
return 0;
}
方法2:中断读取示例
上面给出的串口示例是使用轮询的方式读取串口数据,这种方式在某些场景下可能会占用大量 CPU 资源。实际上,对于 Linux 系统来说,还可以使用中断方式接收串口数据,这样可以大大减少 CPU 的占用率,并且能够更快地响应串口数据。
要使用中断方式接收串口数据,可以使用 select 函数来监听串口文件描述符的可读事件。当串口数据可读时,select 函数将返回,并且可以调用 read 函数来读取串口数据。这种方式可以避免轮询操作,只有在串口数据可读时才会执行读取操作,因此能够减少 CPU 的占用率。
以下是一个简单的使用中断方式接收串口数据的示例程序:
#include
#include
#include
#include
#include
#include
int main() {
int fd;
struct termios options;
fd_set rfds;
// 打开串口设备
fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("open");
return -1;
}
// 配置串口参数
tcgetattr(fd, &options);
options.c_cflag = B9600 | CS8 | CLOCAL | CREAD;
options.c_iflag = IGNPAR;
options.c_oflag = 0;
options.c_lflag = 0;
options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 1;
tcsetattr(fd, TCSANOW, &options);
while (1) {
// 使用 select 函数监听串口文件描述符的可读事件
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);
// 读取串口数据
char buf[256];
int n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Received data: %.*s\n", n, buf);
}
}
// 关闭串口设备
close(fd);
return 0;
}
需要注意的是,在使用中断方式接收串口数据时,需要对串口文件描述符设置为非阻塞模式,以便在 select 函数返回时立即读取串口数据。可以使用 fcntl 函数来设置文件描述符的标志位,如下所示:
// 设置串口文件描述符为非阻塞模式
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
方法3:信号的方式接收数据
#include
#include
#include
#include
#include
#include
int fd;
void sigio_handler(int sig) {
char buf[256];
int n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Received data: %.*s\n", n, buf);
}
}
int main() {
struct termios options;
struct sigaction sa;
// 打开串口设备
fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("open");
return -1;
}
// 配置串口参数
tcgetattr(fd, &options);
options.c_cflag = B9600 | CS8 | CLOCAL | CREAD;
options.c_iflag = IGNPAR;
options.c_oflag = 0;
options.c_lflag = 0;
options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 1;
tcsetattr(fd, TCSANOW, &options);
// 设置串口文件描述符为异步通知模式
/* 将串口文件描述符设置为当前进程的拥有者,从而接收该文件描述符相关的信号。*/
fcntl(fd, F_SETOWN, getpid());
int flags = fcntl(fd, F_GETFL, 0); // 先获取当前配置, 下面只更改O_ASYNC标志
/* 将串口文件描述符设置为非阻塞模式,从而允许该文件描述符异步地接收数据和信号。*/
fcntl(fd, F_SETFL, flags | O_ASYNC);
// 设置 SIGIO 信号的处理函数
sa.sa_handler = sigio_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
/* 设置了 SIGIO 信号的处理函数为 sigio_handler,从而在该信号被触发时读取串口数据并进行处理。*/
sigaction(SIGIO, &sa, NULL);
while (1) {
// 等待 SIGIO 信号
sleep(1);
}
// 关闭串口设备
close(fd);
return 0;
}
上述代码中,使用了 fcntl 函数将串口文件描述符设置为异步通知模式,并使用 SIGIO 信号来通知程序串口数据已经可读。当程序接收到 SIGIO 信号时,会调用 sigio_handler 函数来读取并处理串口数据。
在这段代码中, sigemptyset(&sa.sa_mask); 的作用是将信号处理函数在执行时要屏蔽的信号集合清空,即将其设置为空集。
每个进程都有一个信号屏蔽字,它表示了当前被阻塞的信号集合。当一个信号被阻塞时,它将被加入到信号屏蔽字中,而当信号被解除阻塞时,它将被从信号屏蔽字中移除。如果信号处理函数在执行时需要屏蔽其他的信号,则可以使用 sigaddset 等函数将需要屏蔽的信号添加到信号屏蔽字中。但是,在本例中,我们需要处理的信号是SIGIO,它通常不需要被屏蔽,因此我们使用 sigemptyset 函数将信号屏蔽字清空,以确保在处理SIGIO信号时不会屏蔽任何其他信号。
在Linux系统中,使用 sigaction 函数注册信号处理函数时,可以设置一些标志来指定信号处理的行为。例如,可以使用 SA_RESTART 标志来指定当系统调用被信号中断时自动重启该系统调用。在本例中,由于我们并不需要设置任何标志,因此将 sa.sa_flags 字段设置为0即可。这表示信号处理函数不需要任何特殊的行为,只需要按照默认的方式处理信号即可。
方法4:使用线程接收串口数据:
#include
#include
#include
#include
#include
#include
void *read_thread(void *arg) {
int fd = *(int *)arg;
char buf[256];
int n;
while (1) {
// 读取串口数据
n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Received data: %.*s\n", n, buf);
}
}
return NULL;
}
int main() {
int fd;
struct termios options;
pthread_t tid;
// 打开串口设备
fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("open");
return -1;
}
// 配置串口参数
tcgetattr(fd, &options);
options.c_cflag = B9600 | CS8 | CLOCAL | CREAD;
options.c_iflag = IGNPAR;
options.c_oflag = 0;
options.c_lflag = 0;
options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 1;
tcsetattr(fd, TCSANOW, &options);
// 创建读取线程
if (pthread_create(&tid, NULL, read_thread, &fd) != 0) {
perror("pthread_create");
return -1;
}
while (1) {
// 主线程的其他处理逻辑
sleep(1);
}
// 关闭串口设备
close(fd);
return 0;
}
上述代码中,创建了一个读取线程,不断读取串口数据并进行处理。主线程可以在读取线程运行的同时进行其他处理逻辑。
这是一口君的新书,感谢大家支持!
end
一口Linux
关注,回复【 1024 】海量Linux资料赠送
精彩文章合集
文章推荐