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层。我画了个流程图,你看一眼就明白整体脉络了。
流程清楚了,我们看代码。下面是一个典型的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;
}
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上。
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_* 系列不一定全都有。
/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 里的具体实现了。