一、设备树(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)关键步骤
-
读取设备树节点
/ptled。 -
获取
reg属性中的地址和长度。 -
使用
ioremap映射到内核虚拟地址。 -
在
open中初始化引脚。 -
在
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)关键步骤:
-
定义
platform_driver,包含probe和remove。 -
在
probe中:-
注册
miscdevice。 -
通过
of_get_named_gpio获取 GPIO 编号。 -
使用
gpio_request和gpio_direction_output初始化。
-
-
在
remove中释放资源。
3. 两种版本的对比
| 特性 | led_dts.c |
led_dts_platform.c |
|---|---|---|
| 硬件控制方式 | 直接映射寄存器 | 使用GPIO子系统 |
| 驱动架构 | 简单杂项设备 | 平台驱动框架 |
| 设备树匹配 | 手动解析节点 | 自动匹配compatible |
| 可移植性 | 差(硬件相关) | 好(使用标准接口) |
| 代码复杂度 | 简单直接 | 较复杂但规范 |
| 适用场景 | 学习、简单设备 | 实际项目、复杂设备 |
五、文件操作接口(File Operations)
1. 常用操作
-
open:打开设备时调用(如初始化 LED)。 -
write:写入数据时调用(如接收字符串 "led_on"/"led_off")。 -
ioctl:用于控制命令(如CMD_LED_ON、CMD_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匹配设备树节点与驱动。 -
文件操作:提供用户空间控制设备的接口。
-
驱动注册 :通过
miscdevice或platform_driver注册驱动。
这样整理的知识点系统且清晰,适合零基础学习者理解设备树与驱动开发的基本流程。