Linux内核驱动——设备树原理与应用

目录

一、设备树介绍

[1.1 设备树基础概念](#1.1 设备树基础概念)

[1.2 设备树相关文件](#1.2 设备树相关文件)

[1.3 编译命令](#1.3 编译命令)

二、设备树节点结构分析

三、引脚控制配置

[四、Linux 内核驱动设计](#四、Linux 内核驱动设计)

[4.1 驱动框架选择](#4.1 驱动框架选择)

[4.1.1 传统字符设备驱动](#4.1.1 传统字符设备驱动)

[4.1.2 基于 Platform 的标准驱动](#4.1.2 基于 Platform 的标准驱动)

[4.2 GPIO 子系统的使用](#4.2 GPIO 子系统的使用)

五、驱动与设备树的匹配机制

[5.1 匹配条件](#5.1 匹配条件)

[5.2 备选匹配方式](#5.2 备选匹配方式)

六、完整驱动流程

七、总结


一、设备树介绍

1.1 设备树基础概念

设备树(Device Tree)是 Linux 内核引入的一种描述硬件结构的数据结构,主要用于嵌入式系统中。它将硬件描述与内核代码分离,使得内核可以更灵活地支持不同硬件平台。
Linux设备树

设备树的特点:

  • 以 "树状" 结构描述硬件资源。
    • 例如本地总线为树的 "主干" 在设备树里面称为 "根节点", 挂载到本地总线的 IIC 总线、SPI 总线、UART 总线为树的 "枝干" ,在设备树里称为 "根节点的子节点", IIC 总线下的IIC 设备不止一个,这些 "枝干" 又可以再分。
  • 设备树可以像头文件(.h文件)那样,一个设备树文件引用另外一个设备树文件, 这样可以实现 "代码" 的重用。
    • 例如多个硬件平台都使用 IMX6ULL 作为主控芯片, 那么我们可以将 IMX6ULL 芯片的硬件资源写到一个单独的设备树文件里面,一般使用 ".dtsi" 后缀, 其他设备树文件直接使用 "#include xxx.dtsi" 引用即可。

1.2 设备树相关文件

  • **.dts:**设备树源文件(Device Tree Source),是人类可读的文本格式。
  • **.dtb:**设备树二进制文件(Device Tree Blob),由 .dts 编译而来,供内核解析使用。

1.3 编译命令

bash 复制代码
make dtbs        # 编译所有设备树
make xxx.dtb      # 编译特定的xxx.dts文件

二、设备树节点结构分析

以 pt.dts 中的 LED 节点为例:

复制代码
ptled {
    #address-cells = <1>;
	#size-cells = <1>;
	compatible = "pt-led";
	name1 = "led";
	status = "okay";
	reg = <0x020E0068 0x04
		   0x020E02F4 0x04
		   0x0209C004 0x04
		   0x0209C000 0x04>;	
};

ptled_sub {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "pt-led-sub";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ptled>;
    ptled-gpio = <&gpio1 3 GPIO_ACTIVE_HIGH>;
    status = "okay";
};

关键属性说明:

属性 作用
#address-cells 地址的单元数量
#size-cells 大小的单元数量
compatible 驱动匹配的关键字段,必须与驱动中的 of_match_table 匹配
pinctrl-* 描述引脚复用配置,指向具体的 pinmux 配置节点
gpio-* 定义 GPIO 引脚信息,包括控制器和编号
status 表示设备状态,"okay" 表示启用,"disabled" 表示禁用
reg 设备寄存器地址和大小

三、引脚控制配置

在设备树中,引脚功能通过 pinctrl 节点定义:

复制代码
pinctrl_ptled: ptledgrp {
    fsl,pins = <
        MX6UL_PAD_GPIO1_IO03__GPIO1_IO03     0x10B0 /* pt led */
    >;
};
  • MX6UL_PAD_GPIO1_IO03__GPIO1_IO03:表示将该引脚配置为普通 GPIO 输出模式。
  • 0x10B0:寄存器配置值,决定电平、上拉等特性。

四、Linux 内核驱动设计

4.1 驱动框架选择

根据是否使用平台总线,分为两种方式:

4.1.1 传统字符设备驱动

这种方式直接从设备树中获取寄存器地址,然后通过 ioremap 映射到内核空间,最后直接操作寄存器。

优点

  • 实现简单直接
  • 不需要注册 platform 设备

缺点

  • 代码耦合度高
  • 不符合 Linux 内核的驱动模型规范
  • 难以进行设备的热插拔管理

关键代码分析

cpp 复制代码
static int __init led_init(void)
{
	struct device_node * pdts;
	const char * pcom = NULL;
	const char * pname = NULL;
	int i = 0;
	unsigned int led_array[8] = {0};
	int ret = misc_register(&misc_dev);
	if(ret)
		goto err_misc_register;

    // 查找设备树节点
	pdts = of_find_node_by_path("/ptled");	
	if(NULL == pdts)
	{
		ret = PTR_ERR(pdts);
		goto err_find_node;
	}

	ret = of_property_read_string(pdts, "compatible", &pcom);
	if(ret < 0)
		goto err_of_property_read;
	printk("led pcom = %s\n", pcom);
	
	ret = of_property_read_string(pdts, "name1", &pname);
	if(ret < 0)
		goto err_of_property_read;
	printk("led pname = %s\n", pname);

    // 读取寄存器地址数组
	ret = of_property_read_u32_array(pdts, "reg", led_array, sizeof(led_array) / sizeof(led_array[0]));
	if(ret < 0)
		goto err_of_property_read;
	for(i = 0; i < sizeof(led_array) / sizeof(led_array[0]); i+=2)
	{
		printk("0x%x\t0x%x\n", led_array[i], led_array[i + 1]);
	}
	
    // 映射寄存器到内核空间
	iomuxc_mux_ctl = ioremap(led_array[0], led_array[1]);
	iomuxc_pad_ctl = ioremap(led_array[2], led_array[3]);
	gpio1_gdir = ioremap(led_array[4], led_array[5]);
    gpio1_dr = ioremap(led_array[6], led_array[7]);

	printk("######################### misc  led_init\n");
	return 0;

err_of_property_read:
	printk("ptled of_property_read   failed\n");

err_find_node:
	printk("ptled find node failed\n");

err_misc_register:
	printk("misc led_init  failed ret = %d\n", ret);
	misc_deregister(&misc_dev);
	return ret;	
}

这种方式直接读取设备树中定义的寄存器地址,然后通过 ioremap 映射到内核空间,最后直接操作寄存器。

4.1.2 基于 Platform 的标准驱动

这种方式使用了 Linux 内核的 platform 驱动模型,更加规范和灵活。

优点

  • 符合 Linux 内核驱动模型规范
  • 便于设备的热插拔管理
  • 代码结构清晰,易于维护
  • 可以利用内核提供的 GPIO 子系统,无需直接操作寄存器

缺点

  • 实现相对复杂
  • 需要了解 platform 驱动模型

关键代码分析

cpp 复制代码
static int probe(struct platform_device * pdev)
{
	struct device_node * pdts;
	int ret = misc_register(&misc_dev);
	if(ret)
		goto err_misc_register;
	
    // 查找设备树节点
	pdts = of_find_node_by_path("/ptled_sub");
	if(NULL == pdts)
	{
		ret = PTR_ERR(pdts);
		goto err_of_find;
	}

    // 通过设备树获取GPIO编号
	led_gpio = of_get_named_gpio(pdts, "ptled-gpio", 0);	
	if(led_gpio < 0)
	{
		ret = led_gpio;
		goto err_of_find;
	}
	
    // 申请GPIO并设置为输出
	ret = gpio_request(led_gpio, "led");
	if(ret < 0)
		goto err_gpio_request;
	gpio_direction_output(led_gpio, LED_OFF);

	printk("#########################  led_driver  probe\n");
	return 0;
err_gpio_request:
	printk("#########################  led_driver gpio_request\n");
err_of_find:
	printk("#########################  led_driver find node failed\n");
err_misc_register:
	misc_deregister(&misc_dev);
	printk("#########################  led_driver misc register ret = %d\n", ret);
	return ret;
}

这种方式通过 of_get_named_gpio 从设备树中获取 GPIO 编号,然后使用 GPIO 子系统提供的函数进行操作,无需直接操作寄存器。

4.2 GPIO 子系统的使用

在 platform 驱动模型中,可以使用 GPIO 子系统来操作 GPIO,而不需要直接操作寄存器。

cpp 复制代码
gpio_request();          // 请求 GPIO
gpio_direction_output(); // 设置输出方向
gpio_set_value();        // 写入电平
gpio_get_value();        // 读取电平
gpio_free();             // 释放资源

所有 GPIO 操作都应通过这些 API 进行,避免直接操作寄存器。

通过设备树获取GPIO:

cpp 复制代码
led_gpio = of_get_named_gpio(pdts, "ptled-gpio", 0);

这个函数从设备树节点中获取 GPIO 编号,参数 "ptled-gpio" 对应设备树中的属性名。

五、驱动与设备树的匹配机制

5.1 匹配条件

驱动与设备树节点的绑定依赖于 compatible 字段:

cpp 复制代码
static const struct of_device_id led_table[] = {
    {.compatible = "pt-led-sub"},
    {}
};

static struct platform_driver pdrv = {
    .probe = probe,
    .remove = remove,
    .driver = {
        .name = DEV_NAME,
        .of_match_table = led_table,
    },
};

当内核加载时,会遍历所有设备树节点,查找 compatible = "pt-led-sub" 的节点,并调用对应的 probe() 函数。

5.2 备选匹配方式

如果没有 compatible 属性,也可以使用 name 字段进行匹配:

cpp 复制代码
if (of_property_read_string(pdt, "name1", &pname) == 0 && strcmp(pname, "led") == 0)

但这种方式不推荐,因为缺乏标准化且灵活性差。

六、完整驱动流程

  1. 编写设备树(dts/xx.dts)
    1. 定义外设节点
    2. 设置 compatible, gpio, pinctrl
  2. 在 dts/Makefile 中新增 xx.dtb
  3. 使用 make dtbs 指令编译为 xx.dtb 文件
  4. 将 xx.dtb 文件放置在 tftpboot 文件夹下
  5. 编写驱动程序(char/xxx.c)
    1. 使用 platform_driver 或 misc_device
    2. 通过 of_* 系列函数获取设备树信息
    3. 调用 GPIO API 控制硬件
    4. 使用 platform_driver_register 注册驱动
  6. 在 char/Makefile 中新增 xxx.o
  7. 在 char/Kconfig 中新增 config XXX ...
    • 注意将模块类型设置为 tristate
    • 将名称设置为 "This is my xxx!"
  8. 使用 make menuconfig 指令配置内核
    1. 取消选择自己添加的其余程序
    2. 将新增的 xxx 设置为 M,表示允许选择编译为模块
  9. 使用 make modules 指令编译生成 xxx.ko 文件
  10. 将 xxx.ko 文件放置在根目录 rootfs 下
  11. 在根目录下编写应用程序(rootfs/xxxx.c)
  12. 使用 arm-linux-gnueabihf-gcc xxxx.c -o xxxx 指令编译为可执行程序
  13. 启动 ARM 开发板,输入 setenv bootargs ... 命令
  14. 输入 tftp 0x80800000 zImage 和 tftp 0x83000000 xx.dtb 命令
  15. 输入 bootz 0x80800000 - 0x83000000 命令完成启动
  16. 输入 insmod xxx.ko 命令加载模块
  17. 输入 ./xxxx 命令执行应用程序

七、总结

设备树是 Linux 内核中描述硬件的重要机制,它将硬件描述与内核代码分离,提高了内核的可移植性和可维护性。在开发设备驱动时,应该优先使用 platform 驱动模型和 GPIO 子系统,而不是直接操作寄存器。

相关推荐
Trouvaille ~2 小时前
【Linux】进程间关系与守护进程详解:从进程组到作业控制到守护进程实现
linux·c++·操作系统·守护进程·作业·会话·进程组
国科安芯2 小时前
火箭传感器控制单元的抗辐照MCU选型与环境适应性验证
单片机·嵌入式硬件·架构·risc-v·安全性测试
BlackQid2 小时前
深入理解指针Part5——回调函数及应用
c语言
Fcy6482 小时前
Linux下 进程(二)(进程状态、僵尸进程和孤儿进程)
linux·运维·服务器·僵尸进程·孤儿进程·进程状态
日拱一卒——功不唐捐2 小时前
字符串匹配:暴力法和KMP算法(C语言)
c语言·算法
ZFB00012 小时前
【麒麟桌面系统】V10-SP1 2503 系统知识——救援模式显示异常
linux·kylin
第七序章2 小时前
【Linux学习笔记】初识Linux —— 理解gcc编译器
linux·运维·服务器·开发语言·人工智能·笔记·学习
迎仔2 小时前
A-总览:GPU驱动运维系列总览
linux·运维
tiantangzhixia2 小时前
Master PDF Linux 平台的 5.9.35 版本安装与自用
linux·pdf·master pdf