目录
[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)
但这种方式不推荐,因为缺乏标准化且灵活性差。
六、完整驱动流程
- 编写设备树(dts/xx.dts)
- 定义外设节点
- 设置 compatible, gpio, pinctrl
- 在 dts/Makefile 中新增 xx.dtb
- 使用 make dtbs 指令编译为 xx.dtb 文件
- 将 xx.dtb 文件放置在 tftpboot 文件夹下
- 编写驱动程序(char/xxx.c)
- 使用 platform_driver 或 misc_device
- 通过 of_* 系列函数获取设备树信息
- 调用 GPIO API 控制硬件
- 使用 platform_driver_register 注册驱动
- 在 char/Makefile 中新增 xxx.o
- 在 char/Kconfig 中新增 config XXX ...
- 注意将模块类型设置为 tristate
- 将名称设置为 "This is my xxx!"
- 使用 make menuconfig 指令配置内核
- 取消选择自己添加的其余程序
- 将新增的 xxx 设置为 M,表示允许选择编译为模块
- 使用 make modules 指令编译生成 xxx.ko 文件
- 将 xxx.ko 文件放置在根目录 rootfs 下
- 在根目录下编写应用程序(rootfs/xxxx.c)
- 使用 arm-linux-gnueabihf-gcc xxxx.c -o xxxx 指令编译为可执行程序
- 启动 ARM 开发板,输入 setenv bootargs ... 命令
- 输入 tftp 0x80800000 zImage 和 tftp 0x83000000 xx.dtb 命令
- 输入 bootz 0x80800000 - 0x83000000 命令完成启动
- 输入 insmod xxx.ko 命令加载模块
- 输入 ./xxxx 命令执行应用程序
七、总结
设备树是 Linux 内核中描述硬件的重要机制,它将硬件描述与内核代码分离,提高了内核的可移植性和可维护性。在开发设备驱动时,应该优先使用 platform 驱动模型和 GPIO 子系统,而不是直接操作寄存器。