学习笔记——设备树基础

一、设备树(Device Tree)基础

1. 设备树的作用

  • 描述硬件信息(如内存、外设、中断、时钟等),使 Linux 内核可以动态识别硬件,无需重新编译内核。

  • 设备树文件分为两种:

    • .dts:设备树源文件(可编辑的文本文件)。

    • .dtb:设备树二进制文件(编译后的二进制文件,供内核解析)。

2. 设备树的基本结构

  • 设备树是一个树状结构,最顶层是根节点 /

  • 每个节点可以包含子节点,每个节点有若干属性(键值对)。

  • 节点中可以定义:

    • compatible:用于与驱动程序匹配的标识符。

    • reg:描述设备寄存器的地址和大小。

    • status:设备状态,如 "okay""disabled"

3. 设备树节点示例

复制代码
ptled {
    compatible = "pt-led";
    name = "led";
    status = "okay";
    reg = <0x020E0068 0x04
           0x020E02F4 0x04
           0x0209C004 0x04
           0x0209C000 0x04>;
};
  • #address-cells#size-cells:用于描述 reg 属性中地址和长度的单元数(默认不写时继承父节点)。

  • compatible:内核驱动程序通过此属性匹配设备。

4. 设备树编译

  • 编译所有设备树文件:

    bash

    复制代码
    make dtbs
  • 编译单个设备树文件(如 imx6ull.dts):

    bash

    复制代码
    make imx6ull.dtb

二、设备树与驱动匹配机制(Platform Driver)

1. 匹配依据

  • 设备树节点中的 compatible 属性与驱动中的 compatible 字符串匹配。

  • 若匹配成功,内核会调用驱动中的 probe 函数。

  • 如果没有 compatible,也可以用 name 匹配(但通常不推荐)。

2. 示例:设备树与驱动的对应关系

设备树节点(如 ptled_sub):

dts

复制代码
ptled_sub {
    compatible = "pt-led-sub";
    ptled-gpio = <&gpio1 3 GPIO_ACTIVE_HIGH>;
};
驱动代码(如 led_dts_platform.c):
复制代码
static struct of_device_id led_table[] = {
    {.compatible = "pt-led-sub"},
    {}
};

匹配成功 → 执行 probe 函数。

三、GPIO 子系统

1. 作用

  • Linux 内核为 GPIO 操作提供统一的接口,方便开发者管理引脚,无需直接操作寄存器。

2. 常用函数接口

函数 说明
gpio_request() 申请 GPIO 引脚
gpio_direction_output() 设置为输出模式
gpio_direction_input() 设置为输入模式
gpio_set_value() 设置输出电平
gpio_get_value() 读取输入电平
gpio_free() 释放 GPIO 引脚

3. 使用示例(在 probe 中)

复制代码
led_gpio = of_get_named_gpio(pdts, "ptled-gpio", 0);
gpio_request(led_gpio, "led");
gpio_direction_output(led_gpio, 1);  // 初始化为高电平
gpio_set_value(led_gpio, 0);         // 拉低,点亮 LED

四、驱动代码分析

1. led_dts.c(直接映射寄存器版本)

  • 通过设备树读取寄存器地址,使用 ioremap 映射到虚拟地址。

  • 直接操作寄存器控制 LED。

(1)详细代码分析
1> 头文件和宏定义
cpp 复制代码
#include <linux/init.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/export.h>
#include <linux/types.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <asm/string.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/of.h>

#define DEV_NAME "led"
static volatile unsigned long * iomuxc_mux_ctl;
static volatile unsigned long * iomuxc_pad_ctl;
static volatile unsigned long * gpio1_gdir;
static volatile unsigned long * gpio1_dr;

#define MAGIC_NUM 'x' 
#define LED_ON 0
#define LED_OFF 1
#define CMD_LED_ON    _IO(MAGIC_NUM, LED_ON)
#define CMD_LED_OFF _IO(MAGIC_NUM, LED_OFF)
  • 包含必要的内核头文件,定义设备名和宏

  • volatile unsigned long *:定义指针,用于映射硬件寄存器

  • _IO(MAGIC_NUM, LED_ON):定义ioctl命令,MAGIC_NUM是魔数,用于区分不同设备

2> LED控制函数
cpp 复制代码
static void led1_init(void)
{
    // 复用功能 - 将引脚配置为GPIO功能
    *iomuxc_mux_ctl = 0x05;
    // 电气特性 - 配置引脚的电气参数
    *iomuxc_pad_ctl = 0x10B0;
    // 引脚方向 - 设置为输出模式
    *gpio1_gdir |= (1 << 3);
}
解释:初始化LED引脚

步骤1:配置引脚复用为GPIO(不再是其他功能)

步骤2:配置上下拉电阻、驱动能力等电气特性

步骤3:将GPIO1的第3位设为输出模式

static void led_on(void)
{
    *gpio1_dr &= ~(1 << 3);  // 第3位清零,LED亮
}

static void led_off(void)
{
    *gpio1_dr |= (1 << 3);   // 第3位置1,LED灭
}
解释:通过数据寄存器控制LED

&=:清零操作

|=:置位操作
3> 文件操作函数
cpp 复制代码
static int open(struct inode * inode, struct file * file)
{
    led1_init();  // 打开设备时初始化LED
    printk("led  open\n");
    return 0;
}
  • 用户调用open()时执行

  • 初始化LED硬件

cpp 复制代码
static ssize_t write(struct file * file, const char __user * buf, size_t size, loff_t * loff)
{
    char data[20] = {0};
    long len = sizeof(data) < size ? sizeof(data) : size;
    long ret = copy_from_user(data, buf, len);  // 从用户空间复制数据
    
    if(!strcmp(data, "led_on"))
        led_on();
    else if(!strcmp(data, "led_off"))
        led_off();
    else
        ret = -EINVAL;    

    printk("led  write\n");
    return ret;
}
  • 用户写入字符串控制LED

  • copy_from_user():将数据从用户空间复制到内核空间

  • 判断字符串内容执行相应操作

cpp 复制代码
static long ioctl(struct file * file, unsigned int cmd, unsigned long args)
{
    long ret = 0;
    switch(cmd)
    {
        case CMD_LED_ON:
            led_on();
            break;
        case CMD_LED_OFF:
            led_off();
            break;
        default:
            ret = -EINVAL;  // 无效命令
            break;
    }
    return ret;
}
  • 通过ioctl命令控制LED

  • 更标准化的控制方式

4> 文件操作结构体和杂项设备
cpp 复制代码
static struct file_operations fops = 
{
    .owner = THIS_MODULE,
    .open = open,
    .read = read,
    .write = write,
    .unlocked_ioctl = ioctl,
    .release = close
};

static struct miscdevice misc_dev =
{
    .minor = MISC_DYNAMIC_MINOR,  // 动态分配次设备号
    .name = DEV_NAME,             // 设备名
    .fops = &fops                 // 文件操作函数
};
  • 定义设备操作接口

  • file_operations:告诉内核这个设备支持哪些操作

  • miscdevice:注册为杂项设备

5> 驱动初始化和设备树
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};
    
    // 1. 注册杂项设备
    int ret = misc_register(&misc_dev);
    if(ret)
        goto err_misc_register;

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

    // 3. 读取设备树属性
    ret = of_property_read_string(pdts, "compatible", &pcom);
    if(ret < 0)
        goto err_of_property_read;
    printk("led pcom = %s\n", pcom);
    
    // 4. 读取寄存器地址
    ret = of_property_read_u32_array(pdts, "reg", led_array, 8);
    if(ret < 0)
        goto err_of_property_read;
    
    // 5. 映射寄存器到虚拟地址
    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]);

    return 0;
}

关键步骤:

  • 注册设备

  • 从设备树查找节点

  • 读取寄存器地址

  • 使用ioremap映射物理地址到虚拟地址

6> 驱动卸载
cpp 复制代码
static void __exit led_exit(void)
{
    iounmap(iomuxc_mux_ctl);  // 取消映射
    iounmap(iomuxc_pad_ctl);
    iounmap(gpio1_gdir);
    iounmap(gpio1_dr);
    misc_deregister(&misc_dev);  // 注销设备
}
(2)关键步骤
  1. 读取设备树节点 /ptled

  2. 获取 reg 属性中的地址和长度。

  3. 使用 ioremap 映射到内核虚拟地址。

  4. open 中初始化引脚。

  5. write/ioctl 中控制 LED。

2. led_dts_platform.c(使用 GPIO 子系统版本)

  • 通过设备树获取 GPIO 编号。

  • 使用 GPIO 子系统函数控制 LED。

(1)详细代码分析
1> 头文件和宏定义
cpp 复制代码
#include <linux/of_gpio.h>
#include <linux/gpio.h>  // GPIO子系统头文件

static int led_gpio;  // GPIO编号

static void led1_init(void)
{
	gpio_direction_output(led_gpio, LED_OFF);  // 配置为输出,初始为高电平
}

static void led_on(void)
{
	gpio_set_value(led_gpio, LED_ON);  // 设置低电平
}

static void led_off(void)
{
	gpio_set_value(led_gpio, LED_OFF);  // 设置高电平
}
  • 对比:不再直接操作寄存器,而是调用GPIO子系统函数
3> 平台驱动结构
cpp 复制代码
static int probe(struct platform_device * pdev)
{
	struct device_node * pdts;
	
	// 1. 注册杂项设备
	int ret = misc_register(&misc_dev);
	if(ret)
		goto err_misc_register;
	
	// 2. 查找设备树节点
	pdts = of_find_node_by_path("/ptled_sub");
	if(NULL == pdts)
	{
		ret = PTR_ERR(pdts);
		goto err_of_find;
	}

	// 3. 获取GPIO编号
	led_gpio = of_get_named_gpio(pdts, "ptled-gpio", 0);	
	if(led_gpio < 0)
	{
		ret = led_gpio;
		goto err_of_find;
	}
	
	// 4. 申请GPIO
	ret = gpio_request(led_gpio, "led");
	if(ret < 0)
		goto err_gpio_request;

	// 5. 配置为输出
	gpio_direction_output(led_gpio, LED_OFF);

	return 0;
}
4> 平台驱动匹配表
cpp 复制代码
static struct of_device_id led_table[] = 
{
	{.compatible = "pt-led-sub"},  // 与设备树中的compatible匹配
	{}
};

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

static int __init led_driver_init(void)
{
	return platform_driver_register(&pdrv);  // 注册平台驱动
}
(2) 驱动匹配流程
复制代码
设备树节点 -> compatible="pt-led-sub"
            ↓
驱动匹配表 -> .compatible="pt-led-sub"
            ↓
匹配成功 -> 执行probe函数
            ↓
probe中初始化硬件
(2)关键步骤:
  1. 定义 platform_driver,包含 proberemove

  2. probe 中:

    • 注册 miscdevice

    • 通过 of_get_named_gpio 获取 GPIO 编号。

    • 使用 gpio_requestgpio_direction_output 初始化。

  3. remove 中释放资源。

3. 两种版本的对比

特性 led_dts.c led_dts_platform.c
硬件控制方式 直接映射寄存器 使用GPIO子系统
驱动架构 简单杂项设备 平台驱动框架
设备树匹配 手动解析节点 自动匹配compatible
可移植性 差(硬件相关) 好(使用标准接口)
代码复杂度 简单直接 较复杂但规范
适用场景 学习、简单设备 实际项目、复杂设备

五、文件操作接口(File Operations)

1. 常用操作

  • open:打开设备时调用(如初始化 LED)。

  • write:写入数据时调用(如接收字符串 "led_on"/"led_off")。

  • ioctl:用于控制命令(如 CMD_LED_ONCMD_LED_OFF)。

  • release:关闭设备时调用(如关闭 LED)。

2. 用户空间与内核空间数据交换

  • copy_from_user:从用户空间复制数据到内核空间。

  • copy_to_user:从内核空间复制数据到用户空间。

六、驱动注册与注销

1. 杂项设备(Misc Device)

  • 使用 misc_register 注册一个杂项设备。

  • 自动分配次设备号,主设备号为 10。

  • 适用于简单的字符设备驱动。

2. 平台驱动(Platform Driver)

  • 使用 platform_driver_register 注册平台驱动。

  • 通过 compatible 与设备树匹配。

  • probe 中完成设备初始化。

七、小结

  • 设备树:描述硬件,实现驱动与硬件分离。

  • GPIO 子系统:提供统一接口操作 GPIO。

  • 驱动匹配 :通过 compatible 匹配设备树节点与驱动。

  • 文件操作:提供用户空间控制设备的接口。

  • 驱动注册 :通过 miscdeviceplatform_driver 注册驱动。

这样整理的知识点系统且清晰,适合零基础学习者理解设备树与驱动开发的基本流程。

相关推荐
Gary Studio6 小时前
rk芯片驱动编写
linux·学习
mango_mangojuice6 小时前
Linux学习笔记(make/Makefile)1.23
java·linux·前端·笔记·学习
想进个大厂6 小时前
代码随想录day37动态规划part05
算法
sali-tec6 小时前
C# 基于OpenCv的视觉工作流-章22-Harris角点
图像处理·人工智能·opencv·算法·计算机视觉
devmoon6 小时前
运行时(Runtime)是什么?为什么 Polkadot 的 Runtime 可以被“像搭积木一样”定制
开发语言·区块链·智能合约·polkadot·runtmie
时艰.6 小时前
Java 并发编程 — 并发容器 + CPU 缓存 + Disruptor
java·开发语言·缓存
Harvey9036 小时前
通过 Helm 部署 Nginx 应用的完整标准化步骤
linux·运维·nginx·k8s
子春一7 小时前
Flutter for OpenHarmony:构建一个 Flutter 四色猜谜游戏,深入解析密码逻辑、反馈算法与经典益智游戏重构
算法·flutter·游戏
忆~遂愿7 小时前
GE 引擎进阶:依赖图的原子性管理与异构算子协作调度
java·开发语言·人工智能