4. uart_driver 与 uart_port 结构体:核心数据结构详解、注册与注销流程

在 Linux 串口驱动框架中,uart_driver 和 uart_port 是两个最核心的数据结构。前者代表一个串口驱动实例,负责管理多个串口端口;后者代表一个具体的串口硬件端口,包含端口号、I/O 地址、中断号、FIFO 控制参数等关键信息。理解这两个结构体的字段含义以及它们的注册/注销流程,是编写稳定串口驱动的基础。

4.1 uart_driver 结构体详解

uart_driver 定义在 include/linux/serial_core.h 中,是串口驱动的顶层抽象。它负责将一组串口端口注册到 TTY 子系统,并管理驱动级别的回调函数。

字段 类型 说明
owner struct module * 驱动所属的内核模块,通常设为 THIS_MODULE
driver_name const char * 驱动名称,用于 /proc/tty/drivers 和内核日志
dev_name const char * 设备节点名称模板,如 "ttyS",最终生成 /dev/ttyS0
nr unsigned int 该驱动管理的最大端口数量
cons struct console * 关联的控制台结构体,用于早期内核打印
ops struct uart_ops * 驱动操作函数集,包含 startup、shutdown、set_termios 等
state struct uart_state * 端口状态数组,每个端口对应一个 uart_state 实例
tty_driver struct tty_driver * 底层 TTY 驱动,由 uart_register_driver 自动创建
关键点: uart_driver 本身不直接操作硬件,它更像一个“容器”,将多个 uart_port 组织起来,并提供统一的 TTY 接口。硬件相关的操作全部委托给 uart_port 和 uart_ops。

4.2 uart_port 结构体详解

uart_port 是串口驱动的“硬件视图”,每个实例对应一个物理串口。它包含了端口配置、I/O 资源、中断处理、FIFO 控制等所有硬件相关字段。

字段 类型 说明
line unsigned int 端口编号,对应 TTY 设备号,如 ttyS0 的 line=0
iotype unsigned int I/O 访问类型,如 UPIO_MEM(内存映射)、UPIO_PORT(端口 I/O)
mapbase resource_size_t 物理基地址,用于 ioremap 映射
membase void __iomem * 虚拟基地址,ioremap 后的内核虚拟地址
irq unsigned int 中断号,用于注册中断处理函数
uartclk unsigned int UART 参考时钟频率(Hz),用于波特率计算
fifosize unsigned int 硬件 FIFO 深度(字节数),如 16、64
flags unsigned long 端口标志位,如 UPF_BOOT_AUTOCONF、UPF_IOREMAP
ops struct uart_ops * 指向驱动操作函数集,通常与 uart_driver 中的 ops 相同
lock spinlock_t 端口自旋锁,保护寄存器访问和状态变更
state struct uart_state * 指向 uart_driver->state[line],用于反向查找
编程提示: 在 uart_port 初始化时,务必正确设置 iotype 和 mapbase。如果使用内存映射 I/O,驱动框架会在 uart_add_one_port 中自动调用 ioremap(前提是设置了 UPF_IOREMAP 标志)。

4.3 uart_driver 与 uart_port 的关系

下图展示了 uart_driver 与 uart_port 之间的层级关系,以及它们如何通过 uart_state 桥接 TTY 子系统:

uart_driver 与 uart_port 层级关系图 uart_driver driver_name / nr / ops / tty_driver uart_state[0 .. nr-1] 每个元素包含 tty_port、xmit 缓冲区、port 指针 uart_port[0] line=0 / irq / mapbase fifosize / uartclk / ops uart_port[1] line=1 / irq / mapbase fifosize / uartclk / ops 说明:每个 uart_port 通过 state 指针反向关联到 uart_state uart_driver 不直接持有 uart_port,而是通过 uart_state 间接管理

4.4 注册与注销流程

串口驱动的注册分为两个层次:先注册 uart_driver,再逐个添加 uart_port。注销时顺序相反。

4.4.1 uart_register_driver() 流程

该函数将 uart_driver 注册到 TTY 核心,主要完成以下工作:

  1. 分配状态数组:根据 drv->nr 分配 uart_state 数组,每个端口一个。
  2. 分配 TTY 驱动:调用 tty_alloc_driver 创建 tty_driver,设置类型为 TTY_DRIVER_TYPE_SERIAL。
  3. 设置 TTY 操作:将 uart_ops 中的 TTY 回调(如 open、close、write)挂接到 tty_driver。
  4. 注册 TTY 驱动:调用 tty_register_driver,使 /dev/ttyS* 设备节点可用。
  5. 初始化控制台:如果 drv->cons 不为 NULL,则注册控制台。
注意: uart_register_driver 只创建 TTY 设备节点,此时端口还不能收发数据。必须等到 uart_add_one_port 完成硬件初始化后,端口才真正可用。

4.4.2 uart_add_one_port() 流程

该函数将一个 uart_port 添加到已注册的 uart_driver 中,完成硬件初始化:

  1. 校验参数:检查 port->line 是否在 0 .. drv->nr-1 范围内。
  2. 关联 state:将 port->state 指向 drv->state[port->line],并设置 state->uart_port = port。
  3. I/O 内存映射:如果设置了 UPF_IOREMAP 标志,调用 ioremap 将 port->mapbase 映射到 port->membase。
  4. 初始化 TTY 端口:调用 tty_port_init 和 tty_port_register_device,将端口注册到 TTY 子系统。
  5. 调用配置回调:如果 port->ops->config_port 存在,则调用它进行硬件自动配置(如检测 FIFO 深度)。
  6. 启动控制台:如果该端口被用作控制台,调用 register_console。

4.4.3 注销流程

注销时顺序与注册相反,先移除端口,再注销驱动:

步骤 函数 说明
1 uart_remove_one_port() 取消 I/O 映射、注销 TTY 端口、释放中断(如果由驱动管理)
2 uart_unregister_driver() 注销 TTY 驱动、释放状态数组、注销控制台
常见错误: 在 uart_remove_one_port 之后,驱动不应再访问 uart_port 的 membase 或 state 字段。如果模块卸载时顺序颠倒(先注销驱动再移除端口),会导致内核崩溃。

4.5 典型注册代码模板

以下是一个典型的串口驱动注册与端口添加代码片段:

static struct uart_driver my_uart_drv = {
    .owner          = THIS_MODULE,
    .driver_name    = "my_uart",
    .dev_name       = "ttyMY",
    .nr             = 2,          // 管理 2 个端口
    .ops            = &my_uart_ops,
};

static struct uart_port my_ports[2];

static int __init my_uart_init(void)
{
    int ret, i;

    // 1. 注册 uart_driver
    ret = uart_register_driver(&my_uart_drv);
    if (ret)
        return ret;

    // 2. 初始化每个 uart_port
    for (i = 0; i < 2; i++) {
        my_ports[i].line      = i;
        my_ports[i].iotype    = UPIO_MEM;
        my_ports[i].mapbase   = MY_UART_BASE + i * 0x1000;
        my_ports[i].irq       = MY_UART_IRQ + i;
        my_ports[i].uartclk   = 48000000;
        my_ports[i].fifosize  = 64;
        my_ports[i].flags     = UPF_IOREMAP | UPF_BOOT_AUTOCONF;
        my_ports[i].ops       = &my_uart_ops;

        // 3. 添加端口
        ret = uart_add_one_port(&my_uart_drv, &my_ports[i]);
        if (ret) {
            while (--i >= 0)
                uart_remove_one_port(&my_uart_drv, &my_ports[i]);
            uart_unregister_driver(&my_uart_drv);
            return ret;
        }
    }

    return 0;
}

static void __exit my_uart_exit(void)
{
    int i;

    // 注销顺序:先移除端口,再注销驱动
    for (i = 0; i < 2; i++)
        uart_remove_one_port(&my_uart_drv, &my_ports[i]);

    uart_unregister_driver(&my_uart_drv);
}

module_init(my_uart_init);
module_exit(my_uart_exit);

4.6 注册与注销流程图

下图展示了完整的注册与注销流程,以及各步骤之间的依赖关系:

uart_driver 注册与注销流程图 注册流程 uart_register_driver 分配 uart_state 数组 注册 TTY 驱动 uart_add_one_port 端口就绪 注销流程 uart_remove_one_port 取消 I/O 映射 注销 TTY 端口 uart_unregister_driver 驱动注销完成 说明:注册时先驱动后端口,注销时先端口后驱动 如果注册过程中失败,需要回滚已添加的端口和已注册的驱动

4.7 常见问题与避坑指南

  • 端口号冲突:多个驱动使用相同的 dev_name 和 line 会导致设备节点冲突。建议在 dev_name 中加入驱动标识,如 "ttyMY"。
  • ioremap 失败:如果 mapbase 地址无效或已被占用,uart_add_one_port 会失败。务必在调用前验证物理地址的正确性。
  • 中断未释放:如果驱动在 startup 回调中自行申请中断,必须在 shutdown 或 uart_remove_one_port 中释放,否则会导致中断泄漏。
  • 并发访问:多个端口共享同一个 uart_driver 的 ops 函数集,但每个端口有自己的 lock。在操作函数中必须使用 port->lock 保护临界区。
调试建议: 在 uart_register_driver 和 uart_add_one_port 前后添加 dev_dbg 或 pr_debug 日志,输出端口号和映射地址。通过 cat /proc/tty/drivers 可以验证驱动是否注册成功。