3、设备树与平台设备:设备树中UART节点绑定、reg与interrupts属性解析、platform_driver注册流程

好,咱们今天聊聊设备树和平台设备。说实话,这部分内容在串口驱动移植里,是绕不开的核心。你想想看,内核怎么知道你的UART挂在哪?中断号是多少?波特率怎么配?这些信息,都得靠设备树来传递。

我个人习惯,拿到一块新板子,第一件事就是看它的设备树文件。嗯,这比看原理图还快。因为设备树里已经把硬件资源抽象出来了,你只要读懂它,驱动怎么写心里就有数了。

3.1 设备树中UART节点绑定

设备树里描述UART,其实就是一个节点。比如我手头这块i.MX6ULL的板子,它的UART1节点长这样:

uart1: serial@02020000 {
    compatible = "fsl,imx6ul-uart", "fsl,imx21-uart";
    reg = <0x02020000 0x4000>;
    interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_UART1_IPG>,
             <&clks IMX6UL_CLK_UART1_SERIAL>;
    clock-names = "ipg", "per";
    status = "disabled";
};

这里有几个关键点:

  • compatible:驱动匹配的关键字。内核通过它找到对应的驱动。我建议你写兼容性字符串时,尽量带上芯片型号,比如"fsl,imx6ul-uart",这样更精确。
  • reg:描述寄存器地址和长度。后面细讲。
  • interrupts:中断号。这个也容易踩坑。
  • clocks:时钟源。串口要工作,时钟必须配好。
  • status:默认是"disabled",需要你在板级文件里改成"okay"。

重要提醒:设备树里的节点名(比如"serial@02020000")其实不重要,内核匹配驱动只看compatible属性。但为了可读性,我建议你按规范来写。

3.2 reg与interrupts属性解析

这两个属性,是设备树里最常用的。咱们一个一个说。

reg属性

reg描述的是硬件寄存器地址范围。格式是:reg = <地址1 长度1 地址2 长度2 ...>。对于UART来说,通常只有一个地址段。

比如上面那个例子:reg = <0x02020000 0x4000>。意思是UART1的寄存器基地址是0x02020000,地址空间大小是0x4000(16KB)。

我在项目中遇到过一个问题:某款芯片的UART寄存器地址是连续的,但设备树里只给了基地址,没给长度。结果驱动里访问寄存器时,内核报了个"ioremap失败"。后来查了芯片手册,发现地址空间其实只有0x1000,但设备树里写的是0x4000。嗯,这种问题很隐蔽,你得仔细对一下手册。

小技巧:写reg属性时,长度最好和芯片手册保持一致。多写一点没关系,但少写了会导致驱动访问越界。

interrupts属性

interrupts描述的是中断信息。格式取决于中断控制器。对于ARM GIC(通用中断控制器),格式通常是:

interrupts = <中断类型 中断号 触发方式>

其中:

  • 中断类型:GIC_SPI(共享外设中断)或GIC_PPI(私有外设中断)。UART通常用SPI。
  • 中断号:硬件中断号。注意,这个号是芯片手册里定义的,不是Linux里的IRQ号。
  • 触发方式:IRQ_TYPE_LEVEL_HIGH(高电平触发)或IRQ_TYPE_EDGE_RISING(上升沿触发)。UART一般用电平触发。

我曾经踩过一个坑:某款芯片的UART中断号在设备树里写的是26,但芯片手册里写的是26+32=58。为什么?因为GIC把SPI中断号从32开始编号,设备树里写的是硬件中断号,内核会自动加上32。如果你在驱动里直接写26,那肯定不对。

注意:不同中断控制器,interrupts属性的格式不一样。比如老式的中断控制器可能只有中断号,没有类型和触发方式。一定要看芯片手册和内核文档。

3.3 platform_driver注册流程

设备树解析完了,接下来就是驱动的事了。串口驱动通常注册为platform_driver。流程其实不复杂,我画个图你就明白了。

platform_driver注册流程 1. 定义platform_driver struct platform_driver 2. 填充probe/remove 实现操作函数 3. 注册platform_driver platform_driver_register() 4. 内核匹配设备树节点 of_match_table → compatible匹配 5. probe()函数被调用 解析reg、interrupts → ioremap → request_irq → 初始化硬件

流程其实就这五步。但实际写代码时,有几个细节要注意。

3.3.1 定义platform_driver

先看一个典型的串口platform_driver定义:

static const struct of_device_id imx_uart_dt_ids[] = {
    { .compatible = "fsl,imx6ul-uart", },
    { .compatible = "fsl,imx21-uart", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);

static struct platform_driver imx_uart_platform_driver = {
    .probe  = imx_uart_probe,
    .remove = imx_uart_remove,
    .driver = {
        .name   = "imx-uart",
        .of_match_table = imx_uart_dt_ids,
        .pm     = &imx_uart_pm_ops,
    },
};

这里有个关键点:of_match_table。它告诉内核,这个驱动支持哪些设备树节点。内核在启动时,会遍历所有设备树节点,找到compatible匹配的节点,然后调用probe函数。

核心逻辑:设备树里的compatible字符串,必须和驱动里的of_device_id数组中的某个字符串完全一致。大小写、标点符号都不能错。我曾经因为少写了一个逗号,折腾了一下午。

3.3.2 probe函数里做什么

probe函数是驱动的入口。它要做的事情,说白了就是:

  1. 获取设备树资源:用platform_get_resource()获取reg和interrupts。
  2. 映射寄存器:用devm_ioremap_resource()把物理地址映射到虚拟地址。
  3. 申请中断:用devm_request_irq()注册中断处理函数。
  4. 初始化硬件:设置波特率、数据位、停止位等。
  5. 注册串口设备:调用uart_add_one_port()把串口注册到内核。

我建议你写probe函数时,尽量用devm_系列函数。它们会自动管理资源释放,省得你在remove函数里一个个清理。

3.3.3 注册与注销

最后,在模块入口和出口函数里注册和注销platform_driver:

static int __init imx_uart_init(void)
{
    return platform_driver_register(&imx_uart_platform_driver);
}

static void __exit imx_uart_exit(void)
{
    platform_driver_unregister(&imx_uart_platform_driver);
}

module_init(imx_uart_init);
module_exit(imx_uart_exit);

嗯,这里有个小细节:module_initmodule_exit宏,它们告诉内核模块的入口和出口。如果你忘了写,模块加载时什么都不会发生。

个人经验:调试串口驱动时,我习惯先在probe函数里加个dev_info()打印,确认驱动是否被正确匹配。如果没打印,说明设备树和驱动的compatible没对上。

3.4 总结一下

设备树和platform_driver的关系,说白了就是:设备树描述硬件,驱动操作硬件。内核通过compatible属性把它们连起来。

你在移植串口驱动时,只要记住三点:

  • 设备树节点要写对compatible、reg、interrupts
  • 驱动里要定义of_match_table,字符串和设备树一致
  • probe函数里用platform_get_resource获取资源

做到这三点,串口驱动基本就能跑起来了。剩下的就是调试细节,比如波特率对不对、中断能不能触发。这些咱们后面再聊。

最后提醒:不同芯片的UART控制器差异很大。比如有些芯片的UART有FIFO,有些没有。有些支持DMA,有些不支持。设备树里可能还需要额外的属性来描述这些特性。一定要仔细看芯片手册和内核里的绑定文档(Documentation/devicetree/bindings/)。