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。流程其实不复杂,我画个图你就明白了。
流程其实就这五步。但实际写代码时,有几个细节要注意。
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函数是驱动的入口。它要做的事情,说白了就是:
- 获取设备树资源:用
platform_get_resource()获取reg和interrupts。 - 映射寄存器:用
devm_ioremap_resource()把物理地址映射到虚拟地址。 - 申请中断:用
devm_request_irq()注册中断处理函数。 - 初始化硬件:设置波特率、数据位、停止位等。
- 注册串口设备:调用
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_init和module_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/)。