4、串口驱动初始化:module_init入口、uart_register_driver、uart_add_one_port调用链

串口驱动的初始化,说白了就是告诉内核:“嘿,我这儿有个串口,你管一下。” 这个过程看似简单,但里面藏着不少坑。我刚开始做驱动移植时,就曾因为搞混了注册顺序,导致系统启动时串口死活不干活。

今天咱们就顺着 module_init 这个入口,一步步拆解 uart_register_driveruart_add_one_port 这两个核心函数。你想想看,整个串口子系统就是靠它们搭起来的。

4.1 入口函数:module_init

每个驱动模块都有一个入口。对于串口驱动,通常长这样:

static int __init my_uart_init(void)
{
    int ret;

    ret = uart_register_driver(&my_uart_driver);
    if (ret)
        return ret;

    ret = uart_add_one_port(&my_uart_driver, &my_uart_port);
    if (ret)
        uart_unregister_driver(&my_uart_driver);

    return ret;
}
module_init(my_uart_init);

嗯,这里要注意 __init 这个标记。它告诉内核:这个函数初始化完就可以扔了,省点内存。我见过有人把 __init 漏掉,虽然也能跑,但总觉得不够优雅。

核心逻辑:先注册驱动,再添加端口。顺序不能乱,否则内核会一脸懵。

4.2 uart_register_driver:注册串口驱动

这个函数是串口子系统的“户口登记处”。它把我们的驱动信息告诉内核,让内核知道有这么个驱动存在。

函数原型:

int uart_register_driver(struct uart_driver *drv);

参数 drv 是个结构体,里面装着驱动的“身份证”:

成员 说明 我的经验
driver_name 驱动名字,比如 "my_uart" 别太长,16字节以内
dev_name 设备名,比如 "ttyS" 这个会生成 /dev/ttyS0 这样的节点
nr 支持的最大端口数 我一般设成 1,除非你有多个串口
cons 控制台操作结构体 如果要做控制台,这里不能为空

注册成功后,内核会在 /proc/tty/drivers 里看到你的驱动。我曾经调试时,第一件事就是去查这个文件,确认驱动有没有注册上。

小技巧:注册失败时,先检查 driver_name 是否和已有的冲突。我遇到过两次,都是因为名字重复导致注册失败。

4.3 uart_add_one_port:添加一个串口端口

驱动注册完了,接下来就是添加具体的端口。说白了,就是告诉内核:“我这个串口,物理地址在哪,中断号是多少,怎么操作它。”

函数原型:

int uart_add_one_port(struct uart_driver *drv, struct uart_port *port);

uart_port 结构体是核心,它描述了串口硬件的所有细节:

  • mapbase:物理基地址。我习惯先读芯片手册,确认地址范围。
  • membase:虚拟基地址。这是 ioremap 后的结果。
  • irq:中断号。这个错了,串口只能轮询,效率极低。
  • uartclk:串口时钟频率。波特率全靠它算,设错了波特率就不准。
  • fifosize:FIFO 大小。我一般设 16,除非芯片支持更大的。
  • ops:操作函数集。这是灵魂,后面会细讲。

注意:添加端口时,内核会调用 ops->request_port 来申请资源。如果这个函数没实现,或者实现有 bug,端口就加不上去。我曾经在 request_port 里忘了检查返回值,结果系统启动时直接 panic。

4.4 调用链的完整流程

为了让你看得更清楚,我画了一张流程图:

串口驱动初始化调用链 module_init(my_uart_init) uart_register_driver(&my_uart_driver) 成功? 返回错误 uart_add_one_port(&my_uart_driver, &port) 成功? 注销驱动并返回错误 初始化成功,返回0

从图上你能看到,整个流程就是一条直线。但实际项目中,往往会在 uart_add_one_port 之前做很多准备工作,比如:

  • ioremap 物理地址
  • 申请中断
  • 初始化硬件寄存器
  • 设置波特率

我个人习惯把这些准备工作放在一个单独的函数里,比如 my_uart_port_init,这样 module_init 看起来清爽很多。

4.5 避坑指南

做串口驱动移植,有几个坑我踩过,分享给你:

坑1:忘记调用 uart_unregister_driver。如果 uart_add_one_port 失败,一定要把之前注册的驱动注销掉,否则下次 insmod 时会报错。

坑2:uart_port 结构体没清零。我建议用 memset 或者 kzalloc 分配,否则未初始化的成员会导致奇怪的问题。

坑3:中断号搞错。我遇到过硬件手册写的中断号和实际设备树里的不一样,结果串口只能轮询,CPU 占用率直接飙到 100%。

嗯,说到设备树,现在很多平台都用设备树来描述硬件。这时候 uart_add_one_port 的参数要从设备树里解析出来。我习惯在 probe 函数里做这件事,而不是在 module_init 里硬编码。

4.6 小结

串口驱动的初始化,说白了就是三步走:

  1. 写一个 module_init 入口函数
  2. 调用 uart_register_driver 注册驱动
  3. 调用 uart_add_one_port 添加端口

每一步都有对应的错误处理,千万别偷懒。我刚开始做的时候,总觉得错误处理是浪费时间,后来被坑了几次,才老老实实把每个返回值都检查一遍。

下一节,咱们会深入 uart_port 结构体,看看里面的 ops 函数集到底该怎么实现。那是串口驱动的灵魂所在。


专注资料整理