4、串口驱动初始化:module_init入口、uart_register_driver、uart_add_one_port调用链
串口驱动的初始化,说白了就是告诉内核:“嘿,我这儿有个串口,你管一下。” 这个过程看似简单,但里面藏着不少坑。我刚开始做驱动移植时,就曾因为搞混了注册顺序,导致系统启动时串口死活不干活。
今天咱们就顺着 module_init 这个入口,一步步拆解 uart_register_driver 和 uart_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 调用链的完整流程
为了让你看得更清楚,我画了一张流程图:
从图上你能看到,整个流程就是一条直线。但实际项目中,往往会在 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 小结
串口驱动的初始化,说白了就是三步走:
- 写一个
module_init入口函数 - 调用
uart_register_driver注册驱动 - 调用
uart_add_one_port添加端口
每一步都有对应的错误处理,千万别偷懒。我刚开始做的时候,总觉得错误处理是浪费时间,后来被坑了几次,才老老实实把每个返回值都检查一遍。
下一节,咱们会深入 uart_port 结构体,看看里面的 ops 函数集到底该怎么实现。那是串口驱动的灵魂所在。