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 提供了两种中断处理模式:

  1. 传统模式:驱动在 startup 中调用 request_irq 注册自己的中断处理函数,然后在处理函数中读取中断状态寄存器,判断中断类型(接收、发送、错误、状态变化),并调用 serial_core 提供的辅助函数(如 uart_insert_char、uart_handle_sysrq_char)。
  2. 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 驱动分层架构,以及中断处理路径:

Linux UART 驱动分层架构与中断处理路径 Linux UART 驱动分层架构与中断处理路径 用户空间 (应用程序 / dev/ttyS*) TTY 层 (tty_driver / tty_port / ldisc) serial_core 层 (uart_port / uart_ops) 端口管理 | 操作抽象 | TTY 适配 | 中断分发 uart_ops 回调 (startup/shutdown/start_tx/handle_irq) 硬件驱动层 (寄存器读写 / DMA 配置 / 中断处理) UART 硬件 (FIFO / DMA 控制器 / 中断控制器) 中断路径 硬件中断 → handle_irq → serial_core 分发

4.6 中断处理流程:从硬件到 TTY 层

当 UART 硬件产生中断时,完整的中断处理流程如下:

  1. 硬件触发:UART 控制器检测到接收 FIFO 达到阈值、发送 FIFO 为空、或发生错误,向中断控制器(GIC)发送中断信号。
  2. 内核中断处理:CPU 响应中断,调用驱动注册的 irq_handler(或 serial_core 的统一入口)。
  3. 中断类型判断:读取中断状态寄存器(如 IIR),判断中断来源(接收、发送、错误、调制解调器状态)。
  4. 数据接收:对于接收中断,从 FIFO 或 DMA 缓冲区读取数据,调用 uart_insert_char 将数据放入 TTY 翻转缓冲区。
  5. 错误处理:对于溢出、帧错误、奇偶校验错误,记录统计信息,必要时清空 FIFO 或重启 DMA。
  6. 唤醒 TTY 层:调用 tty_flip_buffer_push 通知 TTY 层有数据可读,最终传递到用户空间。
DMA 传输完成中断的特殊性: 在 DMA 模式下,接收中断不再是每个字节触发,而是 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 阈值或增加缓冲区深度。

4.8 本章小结