3、Linux串口驱动框架:tty子系统架构、uart_ops结构体、串口设备注册流程
好,咱们今天聊聊Linux串口驱动的核心框架。说实话,我刚入行那会儿,看到tty子系统那堆代码,头都是大的。但后来发现,只要抓住几个关键点,这东西其实没那么玄乎。
3.1 tty子系统架构:三层模型
Linux的串口驱动,说白了就是tty子系统的一部分。它分三层:
- tty核心层:对上提供统一的文件操作接口(open、read、write等),对下调用线路规程和驱动层。
- 线路规程(line discipline):负责数据加工,比如行编辑、回显、流控。默认是N_TTY。
- tty驱动层:直接操作硬件寄存器,发送接收数据。
我画了个结构图,你看一眼就明白了:
嗯,这里要注意:线路规程是个容易被忽略的环节。我遇到过好几次,应用层读写串口数据不对,查了半天发现是线路规程把数据给"加工"了。比如你发0x0D,它自动给你加个0x0A,这就是N_TTY在搞鬼。
调试技巧:如果应用层需要"裸数据",可以用TIOCSETD命令把线路规程切换为N_TTY以外的规程,或者干脆用raw模式。
3.2 uart_ops结构体:驱动的心脏
每个串口驱动都必须实现一个uart_ops结构体。说白了,这就是驱动和tty核心之间的"契约"。你实现了这些函数,核心层才能调用你。
我直接贴一段典型的定义:
static const struct uart_ops my_uart_ops = {
.tx_empty = my_uart_tx_empty,
.set_mctrl = my_uart_set_mctrl,
.get_mctrl = my_uart_get_mctrl,
.stop_tx = my_uart_stop_tx,
.start_tx = my_uart_start_tx,
.stop_rx = my_uart_stop_rx,
.enable_ms = my_uart_enable_ms,
.break_ctl = my_uart_break_ctl,
.startup = my_uart_startup,
.shutdown = my_uart_shutdown,
.set_termios = my_uart_set_termios,
.type = my_uart_type,
.release_port = my_uart_release_port,
.request_port = my_uart_request_port,
.config_port = my_uart_config_port,
.verify_port = my_uart_verify_port,
};
这里面有几个关键函数,我重点说说:
| 函数名 | 作用 | 我的经验 |
|---|---|---|
startup |
打开串口时调用,初始化硬件 | 记得检查时钟是否使能,我踩过这个坑 |
shutdown |
关闭串口时调用,释放资源 | 别忘了关中断,否则系统会挂死 |
start_tx |
启动发送 | DMA模式下要小心缓冲区管理 |
set_termios |
设置波特率、数据位、停止位等 | 波特率计算最容易出错,建议用查表法 |
避坑指南:我曾经在
set_termios里直接修改硬件寄存器,没加锁。结果应用层频繁切换波特率时,系统直接panic。记住:set_termios可能被中断上下文调用,一定要用自旋锁保护。
3.3 串口设备注册流程
一个串口设备从驱动加载到用户可见,大概走这么几步:
- 定义uart_driver结构体:包含驱动名、设备号、端口数量等信息。
- 调用uart_register_driver():向系统注册这个驱动。
- 调用uart_add_one_port():添加具体的串口端口。
- 设备节点创建:udev或mdev自动创建/dev/ttySx节点。
我写个简化版的注册流程代码:
static struct uart_driver my_uart_driver = {
.owner = THIS_MODULE,
.driver_name = "my_uart",
.dev_name = "ttyMY",
.major = TTY_MAJOR,
.minor = 64,
.nr = 4, // 支持4个串口
};
static int __init my_uart_init(void)
{
int ret;
// 第一步:注册驱动
ret = uart_register_driver(&my_uart_driver);
if (ret)
return ret;
// 第二步:逐个添加端口
for (int i = 0; i < 4; i++) {
ret = uart_add_one_port(&my_uart_driver, &my_uart_port[i]);
if (ret) {
// 清理已添加的端口
while (--i >= 0)
uart_remove_one_port(&my_uart_driver, &my_uart_port[i]);
uart_unregister_driver(&my_uart_driver);
return ret;
}
}
return 0;
}
这里有个细节:uart_add_one_port()内部会调用你实现的config_port()和request_port()。所以这两个函数必须提前准备好。
关键点:注册顺序不能乱。先注册驱动,再添加端口。卸载时顺序相反:先移除端口,再注销驱动。
3.4 设备树与平台匹配
现在的嵌入式平台,串口设备大多通过设备树描述。举个例子:
uart1: serial@10001000 {
compatible = "my,uart";
reg = <0x10001000 0x1000>;
interrupts = <0 33 4>;
clocks = <&uart_clk>;
status = "okay";
};
驱动里用of_match_table来匹配:
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "my,uart" },
{ },
};
static struct platform_driver my_uart_platform_driver = {
.probe = my_uart_probe,
.remove = my_uart_remove,
.driver = {
.name = "my_uart",
.of_match_table = my_uart_of_match,
},
};
在probe函数里,你会拿到设备树里的资源(地址、中断号等),然后初始化硬件、注册串口驱动。我个人习惯在probe里加一句打印,确认设备树匹配成功:
dev_info(&pdev->dev, "my_uart probed at 0x%llx\n", res->start);
别小看这行打印。我调试过一块板子,串口死活不工作,最后发现是设备树里status = "disabled",驱动根本没probe。加个打印,一眼就能看出来。
调试建议:在
probe和startup函数里加dev_dbg()打印,记录关键步骤。用dmesg查看,能快速定位问题出在注册阶段还是运行时。
好了,tty子系统的三层架构、uart_ops的每个回调函数、以及串口设备的完整注册流程,咱们都过了一遍。这些东西看着多,其实核心就几个结构体和函数。你写驱动时,照着这个框架填内容就行。