4. UART驱动初始化与注册:platform_driver结构体、probe函数实现、tty_port与uart_port绑定

好,我们正式开始讲UART驱动的骨架——初始化与注册。这部分说白了就是让内核知道“嘿,这里有个UART控制器,你得管它”。很多初学者一上来就调中断、调波特率,结果驱动根本没注册成功,白忙一场。

我个人习惯,写UART驱动第一步永远是先搭好platform_driver框架。为什么用platform?因为嵌入式里的UART控制器大多挂在SoC内部总线上,不是PCI或USB那种可枚举的设备。platform总线就是为这种“固定焊死在板子上”的设备准备的。

4.1 platform_driver结构体:驱动的身份证

先看一个最简的platform_driver实例。我一般把它放在文件末尾,作为驱动的入口点。

static struct platform_driver my_uart_driver = {
    .driver = {
        .name   = "my_uart",
        .of_match_table = my_uart_of_match,
    },
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
};

module_platform_driver(my_uart_driver);

这里有几个关键点:

  • .name:驱动名字,要和设备树里的compatible或platform_device.name匹配。我踩过坑,名字拼错一个字母,probe死活不调用。
  • .of_match_table:设备树匹配表。现在新板子基本都用设备树,这个字段几乎是标配。
  • .probe / .remove:驱动的灵魂。probe成功,设备才算“活”了。
小技巧: 用 module_platform_driver() 这个宏,它帮你省掉了 module_init 和 module_exit 的样板代码。我早期手写这两行,后来发现宏更干净。

4.2 probe函数实现:从硬件到软件的第一步

probe函数是驱动里最核心的代码块。它要完成三件事:获取硬件资源、初始化底层硬件、注册到tty层。我画了个流程图,你看一眼就明白整体脉络了。

UART probe 函数执行流程 probe(pdev) 入口 获取IO内存/中断/DMA资源 初始化uart_port + ops回调 tty_port 初始化与绑定 uart_register_driver / add_one_port 返回 0,驱动就绪 注意 任何一步失败, probe 必须返回负值 并清理已分配资源。

流程清楚了,我们看代码。下面是一个典型的probe函数骨架:

static int my_uart_probe(struct platform_device *pdev)
{
    struct uart_port *port;
    struct resource *res;
    int ret, irq;

    /* 1. 分配uart_port结构体 */
    port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
    if (!port)
        return -ENOMEM;

    /* 2. 获取IO内存地址 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    port->membase = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(port->membase))
        return PTR_ERR(port->membase);

    /* 3. 获取中断号 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;
    port->irq = irq;

    /* 4. 设置uart_port基本参数 */
    port->uartclk = clk_get_rate(...);   /* 从时钟框架获取 */
    port->fifosize = 64;                 /* 硬件FIFO深度 */
    port->ops = &my_uart_ops;           /* 操作函数集 */
    port->type = PORT_MY_UART;
    port->iotype = UPIO_MEM32;
    port->line = pdev->id;              /* 串口号,重要! */

    /* 5. 初始化tty_port并绑定 */
    tty_port_init(&port->state->port);
    port->state->port.ops = &my_uart_port_ops;

    /* 6. 注册到串口核心 */
    ret = uart_add_one_port(&my_uart_driver, port);
    if (ret) {
        dev_err(&pdev->dev, "failed to add uart port\n");
        return ret;
    }

    platform_set_drvdata(pdev, port);
    return 0;
}
我曾经踩过的坑: 第4步里的 port->line 一定要设置正确。它决定了用户空间看到的 /dev/ttyS* 编号。我有一次忘了赋值,结果line默认是0,两个UART都注册成ttyS0,第二个直接覆盖了第一个,调试了整整一天才发现。

4.3 tty_port与uart_port绑定:两个世界握手

很多新人搞不清tty_port和uart_port的关系。我打个比方:

  • uart_port:面向硬件。它管寄存器、FIFO、中断、波特率。这是驱动工程师的主战场。
  • tty_port:面向用户。它管打开/关闭计数、线路规程、流控策略。这是tty核心层的抽象。

两者必须绑定,数据才能从硬件一路送到用户空间的终端程序。绑定的核心代码就一行:

tty_port_init(&port->state->port);

但背后有几个细节你必须注意:

组件 职责 绑定方式
uart_port 硬件控制:寄存器读写、中断处理、DMA 通过 port->state->port 指针关联
tty_port 用户接口:open/close计数、阻塞控制、挂起 通过 tty_port_init() 初始化后自动关联
uart_state 中间层:维护DMA缓冲区、XON/XOFF状态 由串口核心在 uart_add_one_port() 时分配

你想想看,如果没有tty_port,用户层打开/dev/ttyS1时,内核根本不知道这个打开操作对应哪个硬件端口。tty_port就是那个“接线员”,把用户请求路由到正确的uart_port上。

核心原则: uart_port 负责“怎么发”,tty_port 负责“谁在收”。两者通过 uart_state 这座桥沟通。调试时如果发现 open 卡死或数据发不出去,先检查 tty_port 的引用计数和 ops 回调。

4.4 避坑指南:probe失败后的资源清理

probe函数里,每一步都可能失败。我见过最糟糕的写法是:

/* 错误示例:失败后直接return,不释放前面已分配的资源 */
port = devm_kzalloc(...);
res = platform_get_resource(...);  /* 如果这里失败,port就泄漏了? */
/* 实际上devm_*系列会自动释放,但非devm资源不会! */

嗯,这里要注意:devm_kzalloc 和 devm_ioremap_resource 是设备资源管理的,probe失败后内核会自动释放。但如果你用了 kzalloc、ioremap 或者申请了时钟、GPIO,就必须手动清理。

我个人习惯,在probe开头定义一个 goto err_xxx 标签链:

static int my_uart_probe(struct platform_device *pdev)
{
    /* ... 分配资源 ... */
    if (失败) goto err_free_port;
    /* ... 注册 ... */
    if (失败) goto err_iounmap;
    return 0;

err_iounmap:
    iounmap(port->membase);
err_free_port:
    kfree(port);
    return ret;
}

这种写法虽然老派,但可靠。特别是当你需要支持内核老版本时,devm_* 系列不一定全都有。

调试小技巧: 如果probe函数返回了0,但 /dev/ttyS* 没出现,多半是 uart_add_one_port() 内部失败了。打开 CONFIG_SERIAL_CORE_CONSOLE 和 CONFIG_SERIAL_CORE 的调试日志,看内核打印的“uart_add_one_port”相关消息。我经常靠这招定位注册失败的原因。

好,这一节的内容就这些。你只要把platform_driver搭好、probe里把uart_port和tty_port绑牢,驱动的骨架就算立住了。下一层我们就要开始填操作函数集 uart_ops 里的具体实现了。