在 Linux 系统中,串口(UART/Serial Port)通信通常可以通过轮询(Polling)或中断(Interrupt)方式实现,中断方式效率更高,因为它允许 CPU 在等待数据时执行其他任务,直到硬件触发中断才处理数据。
以下是关于 Linux 串口中断的详细说明,包括:
- 用户空间如何通过中断方式读写串口
- 内核空间如何实现串口中断驱动
- 常见问题与调试技巧
用户空间:使用中断方式读写串口
在用户空间,Linux 提供了两种主要机制来实现“非阻塞”或“事件驱动”的串口通信:
使用 select() / poll() / epoll()
这是最常用、最推荐的方式,虽然它不是真正的“硬件中断”,但它通过内核的事件通知机制模拟了中断行为,避免忙等待。
示例代码(使用 select):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/select.h>
#include <string.h>
int open_serial_port(const char device) {
int fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
perror("open");
return -1;
}
return fd;
}
int main() {
int fd = open_serial_port("/dev/ttyS0");
if (fd < 0) return 1;
// 配置串口(略,实际需设置波特率、数据位等)
struct termios tty;
tcgetattr(fd, &tty);
cfsetispeed(&tty, B9600);
cfsetospeed(&tty, B9600);
tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag &= ~CRTSCTS;
tty.c_cflag |= CREAD | CLOCAL;
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tty.c_iflag &= ~(IXO
N | IXOFF | IXANY);
tty.c_iflag &= ~(INLCR | ICRNL | IGNCR);
tty.c_oflag &= ~OPOST;
tty.c_cc[VMIN] = 1;
tty.c_cc[VTIME] = 0;
tcsetattr(fd, TCSANOW, &tty);
fd_set readfds;
struct timeval timeout;
while (1) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret < 0) {
perror("select");
break;
} else if (ret == 0) {
printf("Timeoutn");
continue;
}
if (FD_ISSET(fd, &readfds)) {
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Received %d bytes: ", n);
fwrite(buf, 1, n, stdout);
printf("n");
}
}
}
close(fd);
return 0;
}
✅ 优点:简单、可移植、无需修改内核。
❌ 缺点:不是真正的硬件中断,仍由内核调度。
使用 SIGIO 信号(真实中断通知)
Linux 支持通过 F_SETSIG 和 FASYNC 实现基于信号的中断通知。
示例代码(使用 SIGIO):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <signal.h>
#include <string.h>
volatile sig_atomic_t got_data = 0;
void sigio_handler(int sig) {
got_data = 1;
}
int main() {
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
perror("open");
return 1;
}
// 配置串口(同上,略)
// 设置进程组 ID 以便接收 SIGIO
fcntl(fd, F_SETOWN, getpid());
// 启用异步 I/O 和信号通知
int flags = f
cntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
// 设置信号类型为 SIGIO
fcntl(fd, F_SETSIG, SIGIO);
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigio_handler;
sigaction(SIGIO, &sa, NULL);
char buf[256];
while (1) {
if (got_data) {
got_data = 0;
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Received: ");
fwrite(buf, 1, n, stdout);
printf("n");
}
}
usleep(10000); // 避免忙等待
}
close(fd);
return 0;
}
⚠️ 注意:
SIGIO需要内核支持,并且某些嵌入式平台可能未启用。
内核空间:实现串口中断驱动
如果你需要编写 Linux 内核模块来处理串口中断,你需要实现一个字符设备驱动,并注册中断服务程序(ISR)。
基本步骤:
- 分配并初始化
uart_port结构。 - 注册中断处理函数。
- 实现
uart_ops中的.tx_empty、.set_termios等回调。 - 在中断服务程序中读取 FIFO 数据,并通过
tty_flip_buffer_push()提交给 TTY 层。
示例:简化的中断处理函数
static irqreturn_t my_uart_isr(int irq, void dev_id) {
struct my_uart_port port = dev_id;
unsigned int status;
// 读取状态寄存器
status = readl(port->membase + UART_REG_LSR);
if (status & UART_LSR_DR) {
// 数据就绪,从数据寄存器读取
unsigned char data = readb(port->membase + UART_REG_DATA);
// 将数据放入 TTY 翻转缓冲区
tty_insert_flip_string(&port->tty->port, &data, 1);
tty_flip_buffer_push(&port->tty->port);
}
if (status & UART_LSR_THRE) {
//
发送寄存器为空,可以继续发送
if (!uart_circ_empty(&port->tty->port.xmit)) {
// 发送下一个字节
unsigned char ch = port->tty->port.xmit[port->tty->port.xmit_head];
writeb(ch, port->membase + UART_REG_DATA);
port->tty->port.xmit_head = (port->tty->port.xmit_head + 1) & (UART_XMIT_SIZE - 1);
}
}
return IRQ_HANDLED;
}
注册中断:
int ret = request_irq(irq_number, my_uart_isr, IRQF_SHARED, "my_uart", port);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %dn", irq_number);
return ret;
}
常见问题与调试
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 收不到中断 | 中断未使能、引脚复用错误 | 检查设备树(DTS)中 UART 引脚配置 |
| 数据错乱 | 波特率不匹配、FIFO 未清空 | 检查波特率设置,清除 FIFO |
| 中断频繁触发 | 噪声干扰、未屏蔽错误中断 | 检查 LSR 寄存器,屏蔽错误中断 |
| 用户空间收不到数据 | TTY 层未正确推送 | 检查 tty_flip_buffer_push() 是否调用 |
- 用户空间:推荐使用
select()/poll()实现高效非阻塞 I/O,或使用SIGIO实现信号驱动。 - 内核空间:需实现完整的 UART 驱动框架,注册中断服务程序,并通过 TTY 层提交数据。
- 调试工具:
stty、minicom、cat /proc/interrupts、dmesg等。
如需进一步深入某一部分(如设备树配置、DMA 支持、低功耗模式等),欢迎继续提问!
首发原创文章,作者:世雄 - 原生数据库架构专家,如若转载,请注明出处:https://idctop.com/article/487158.html



