4. Linux 内核 UART 驱动框架:serial_core、tty 层、uart_ops 结构体、中断处理入口
Linux 内核的 UART 驱动并非一个孤立的字符设备驱动,而是构建在 serial_core 与 TTY 层 之上的分层架构。理解这一框架是开发 DMA 传输完成中断与错误处理流程的前提。本章将拆解 serial_core 的核心作用、TTY 层的抽象机制、uart_ops 操作表以及中断处理入口的注册与分发逻辑。
4.1 serial_core 层:UART 驱动的中间件
serial_core(位于 drivers/tty/serial/serial_core.c)是 Linux 内核为串口驱动提供的一套通用基础设施。它位于底层硬件驱动与上层 TTY 层之间,承担以下职责:
- 端口管理:统一管理
uart_port结构体的分配、注册与注销。 - 操作抽象:定义
uart_ops回调函数表,驱动开发者只需实现底层硬件操作。 - TTY 接口适配:将 TTY 层的标准调用(如
open、close、write)转换为uart_ops中的具体函数。 - 中断分发:提供统一的中断处理入口,并根据中断类型分发到对应的处理函数。
关键数据结构:
struct uart_port 是 serial_core 层的核心,包含 I/O 基地址、中断号、DMA 通道、波特率、FIFO 配置等信息。每个物理串口对应一个 uart_port 实例。
设计意图: serial_core 层让驱动开发者无需重复实现 TTY 协议、线路规程(line discipline)等复杂逻辑,只需关注寄存器读写、中断处理、DMA 配置等硬件相关操作。
4.2 TTY 层:用户空间与内核的桥梁
TTY 层是 Linux 内核中处理终端设备的抽象层。对于 UART 驱动而言,TTY 层提供了以下能力:
| TTY 层组件 | 作用 | 与 UART 驱动的关系 |
|---|---|---|
struct tty_driver |
注册 TTY 设备,提供 open/close/write 等操作 |
serial_core 自动创建并注册 |
struct tty_port |
管理端口状态、缓冲区、流量控制 | 嵌入在 uart_port 中 |
| 线路规程 (ldisc) | 处理数据格式化、回显、信号字符 | 默认使用 N_TTY 规程 |
tty_flip_buffer |
接收数据缓冲区,供中断处理程序推送数据 | DMA 完成中断将数据推入此缓冲区 |
用户空间通过 /dev/ttyS* 或 /dev/ttyAMA* 等设备节点访问串口,所有读写操作最终通过 TTY 层转发到 serial_core,再由 uart_ops 回调进入底层硬件驱动。
4.3 uart_ops 结构体:硬件操作回调表
struct uart_ops 是驱动开发者必须实现的核心接口。它定义了 UART 硬件从初始化到数据收发的全部操作。以下是最关键的几个回调函数:
| 回调函数 | 触发时机 | 典型实现内容 |
|---|---|---|
startup |
设备打开时 | 申请中断、复位 FIFO、配置波特率、使能 DMA |
shutdown |
设备关闭时 | 释放中断、停止 DMA、清空 FIFO |
start_tx |
TTY 层有数据要发送时 | 启动发送 DMA 或写入发送 FIFO |
stop_tx |
发送暂停或完成时 | 停止发送 DMA 或关闭发送中断 |
tx_empty |
查询发送 FIFO 是否为空 | 读取状态寄存器,返回是否为空 |
set_termios |
波特率/数据位/校验位改变时 | 重新计算分频系数,配置硬件寄存器 |
handle_irq |
中断发生时(可选) | 读取中断状态寄存器,分发到对应处理函数 |
// uart_ops 结构体定义示例(位于 include/linux/serial_core.h)
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *new,
struct ktermios *old);
void (*pm)(struct uart_port *, unsigned int state,
unsigned int oldstate);
int (*handle_irq)(struct uart_port *);
// ... 其他回调
};
常见错误: 驱动开发者容易遗漏
start_tx 与 stop_tx 的配对调用。在 DMA 传输场景中,start_tx 启动 DMA 后,必须在传输完成或出错时调用 stop_tx,否则会导致 DMA 通道无法释放。
4.4 中断处理入口:从硬件中断到 serial_core 分发
UART 中断处理是驱动中最关键的路径之一。serial_core 提供了两种中断处理模式:
- 传统模式:驱动在
startup中调用request_irq注册自己的中断处理函数,然后在处理函数中读取中断状态寄存器,判断中断类型(接收、发送、错误、状态变化),并调用 serial_core 提供的辅助函数(如uart_insert_char、uart_handle_sysrq_char)。 - serial_core 统一入口模式:驱动在
uart_ops中实现handle_irq回调,serial_core 会在其注册的中断处理函数中自动调用该回调。这种方式更简洁,推荐用于新驱动。
以下是一个典型的中断处理入口实现:
// 中断处理函数示例(传统模式)
static irqreturn_t my_uart_irq_handler(int irq, void *dev_id)
{
struct uart_port *port = dev_id;
unsigned int iir; // 中断标识寄存器
// 读取中断状态,判断中断类型
iir = readl(port->membase + UART_IIR);
if (iir & UART_IIR_NO_INT) {
return IRQ_NONE; // 非本设备中断
}
// 接收中断
if (iir & UART_IIR_RX_TIMEOUT || iir & UART_IIR_RX_AVAIL) {
my_uart_rx_handler(port);
}
// 发送中断
if (iir & UART_IIR_TX_EMPTY) {
my_uart_tx_handler(port);
}
// 错误中断(溢出、帧错误、奇偶校验错误等)
if (iir & UART_IIR_ERROR) {
my_uart_error_handler(port);
}
// 调制解调器状态变化
if (iir & UART_IIR_MS_CHANGE) {
uart_handle_cts_change(port, read_msr(port));
}
return IRQ_HANDLED;
}
性能建议: 在 DMA 传输场景中,接收中断应尽量减少。理想情况下,DMA 传输完成中断(而非每个字节中断)才是数据推送的主要触发点。错误中断则必须保留,以便及时处理溢出和帧错误。
4.5 框架整体架构图
下图展示了从用户空间到硬件寄存器的完整 UART 驱动分层架构,以及中断处理路径:
4.6 中断处理流程:从硬件到 TTY 层
当 UART 硬件产生中断时,完整的中断处理流程如下:
- 硬件触发:UART 控制器检测到接收 FIFO 达到阈值、发送 FIFO 为空、或发生错误,向中断控制器(GIC)发送中断信号。
- 内核中断处理:CPU 响应中断,调用驱动注册的
irq_handler(或 serial_core 的统一入口)。 - 中断类型判断:读取中断状态寄存器(如 IIR),判断中断来源(接收、发送、错误、调制解调器状态)。
- 数据接收:对于接收中断,从 FIFO 或 DMA 缓冲区读取数据,调用
uart_insert_char将数据放入 TTY 翻转缓冲区。 - 错误处理:对于溢出、帧错误、奇偶校验错误,记录统计信息,必要时清空 FIFO 或重启 DMA。
- 唤醒 TTY 层:调用
tty_flip_buffer_push通知 TTY 层有数据可读,最终传递到用户空间。
DMA 传输完成中断的特殊性: 在 DMA 模式下,接收中断不再是每个字节触发,而是 DMA 传输完成时触发一次。此时中断处理程序需要:
1. 停止 DMA 通道。
2. 将 DMA 缓冲区中的数据一次性推入 TTY 翻转缓冲区。
3. 重新配置 DMA 描述符,准备下一次传输。
4. 处理可能发生的 DMA 错误(如总线错误、描述符错误)。
1. 停止 DMA 通道。
2. 将 DMA 缓冲区中的数据一次性推入 TTY 翻转缓冲区。
3. 重新配置 DMA 描述符,准备下一次传输。
4. 处理可能发生的 DMA 错误(如总线错误、描述符错误)。
4.7 错误中断处理:溢出、帧错误与奇偶校验
UART 硬件在接收数据时可能检测到三种常见错误,每种错误都需要在中断处理中妥善处理:
| 错误类型 | 触发条件 | 典型处理方式 |
|---|---|---|
| 溢出错误 (Overrun) | 接收 FIFO 已满,新数据到达导致数据丢失 | 清空 FIFO,增加 DMA 缓冲区大小或降低波特率 |
| 帧错误 (Frame Error) | 停止位检测失败(通常由波特率不匹配引起) | 记录错误计数,丢弃当前数据,重新同步 |
| 奇偶校验错误 (Parity Error) | 校验位与数据不匹配 | 根据线路规程配置决定是否丢弃或传递带标记的数据 |
// 错误中断处理示例
static void my_uart_error_handler(struct uart_port *port)
{
unsigned int lsr = readl(port->membase + UART_LSR);
// 溢出错误
if (lsr & UART_LSR_OE) {
port->icount.overrun++;
// 清空 FIFO,防止错误扩散
writel(UART_FCR_CLEAR_RCVR, port->membase + UART_FCR);
dev_warn(port->dev, "FIFO overrun error\n");
}
// 帧错误
if (lsr & UART_LSR_FE) {
port->icount.frame++;
dev_dbg(port->dev, "frame error detected\n");
}
// 奇偶校验错误
if (lsr & UART_LSR_PE) {
port->icount.parity++;
dev_dbg(port->dev, "parity error detected\n");
}
// 通知 TTY 层错误状态(可选)
uart_handle_break(port);
}
关键注意事项: 错误中断处理中,必须先读取错误状态寄存器,再清空 FIFO。如果顺序颠倒,错误状态会被清除,导致无法准确记录错误类型。此外,在 DMA 模式下,溢出错误可能意味着 DMA 传输速度跟不上数据到达速度,需要调整 DMA 阈值或增加缓冲区深度。