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 子系统:
4.4 注册与注销流程
串口驱动的注册分为两个层次:先注册 uart_driver,再逐个添加 uart_port。注销时顺序相反。
4.4.1 uart_register_driver() 流程
该函数将 uart_driver 注册到 TTY 核心,主要完成以下工作:
- 分配状态数组:根据
drv->nr分配uart_state数组,每个端口一个。 - 分配 TTY 驱动:调用
tty_alloc_driver创建tty_driver,设置类型为TTY_DRIVER_TYPE_SERIAL。 - 设置 TTY 操作:将
uart_ops中的 TTY 回调(如open、close、write)挂接到tty_driver。 - 注册 TTY 驱动:调用
tty_register_driver,使/dev/ttyS*设备节点可用。 - 初始化控制台:如果
drv->cons不为 NULL,则注册控制台。
注意:
uart_register_driver 只创建 TTY 设备节点,此时端口还不能收发数据。必须等到 uart_add_one_port 完成硬件初始化后,端口才真正可用。
4.4.2 uart_add_one_port() 流程
该函数将一个 uart_port 添加到已注册的 uart_driver 中,完成硬件初始化:
- 校验参数:检查
port->line是否在0 .. drv->nr-1范围内。 - 关联 state:将
port->state指向drv->state[port->line],并设置state->uart_port = port。 - I/O 内存映射:如果设置了
UPF_IOREMAP标志,调用ioremap将port->mapbase映射到port->membase。 - 初始化 TTY 端口:调用
tty_port_init和tty_port_register_device,将端口注册到 TTY 子系统。 - 调用配置回调:如果
port->ops->config_port存在,则调用它进行硬件自动配置(如检测 FIFO 深度)。 - 启动控制台:如果该端口被用作控制台,调用
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 注册与注销流程图
下图展示了完整的注册与注销流程,以及各步骤之间的依赖关系:
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 可以验证驱动是否注册成功。