设备树
- [Linux LED 驱动的三种开发方式 ------ 完整实践指南](#Linux LED 驱动的三种开发方式 —— 完整实践指南)
- [1. 设备树](#1. 设备树)
-
- [1.1 概念](#1.1 概念)
- [1.2 设备树语法](#1.2 设备树语法)
-
- [1.2.1 几个相关的文件](#1.2.1 几个相关的文件)
- [1.2.2 设备树文件编译和反编译](#1.2.2 设备树文件编译和反编译)
- [1.2.3 设备树基础语法](#1.2.3 设备树基础语法)
- [1.3 设备树中的节点如何在驱动中被使用](#1.3 设备树中的节点如何在驱动中被使用)
-
- [1.3.1 设备树的转换过程](#1.3.1 设备树的转换过程)
- [1.3.2 在驱动中如何使用设备树节点](#1.3.2 在驱动中如何使用设备树节点)
- [2. 使用设备树实现led驱动](#2. 使用设备树实现led驱动)
-
- [2.1 在设备树文件中添加led的信息](#2.1 在设备树文件中添加led的信息)
- [2.2 编写平台总线的pdrv层代码](#2.2 编写平台总线的pdrv层代码)
- [3. pinctl子系统和gpio子系统的使用](#3. pinctl子系统和gpio子系统的使用)
-
- [3.1 pinctl子系统](#3.1 pinctl子系统)
- [3.2 gpio子系统](#3.2 gpio子系统)
- [3.3 使用gpio子系统实现led驱动](#3.3 使用gpio子系统实现led驱动)
-
- [3.3.1 gpio方式一 (旧版gpio函数)](#3.3.1 gpio方式一 (旧版gpio函数))
- [3.3.2 gpio方式二(新版gpio函数)](#3.3.2 gpio方式二(新版gpio函数))
- 补充:构建与部署流程
Linux LED 驱动的三种开发方式 ------ 完整实践指南
重点提示:本文针对 STM32MP1 平台梳理 Linux LED 驱动的三种常见实现思路,重点放在 设备树(Device Tree) 驱动方式 ,补全缺漏的步骤、关键配置以及常见踩坑点,使整个流程从 DTS 描述、内核匹配到驱动编写、调试、部署都形成闭环。
本文详细介绍了Linux LED驱动的三种开发方式,重点探讨了设备树(Device Tree)驱动的实现方案。针对STM32MP1平台,文章梳理了从DTS描述、内核匹配到驱动编写调试的完整闭环流程,并对比了传统方式、平台总线方式和设备树方式的优缺点。特别强调了设备树实现的关键在于硬件描述与驱动逻辑的解耦,通过 DTS/DTSI/DTB 文件体系实现硬件配置的可维护性和复用性。本文还提供了设备树语法详解、常用命令示例以及开发注意事项,为嵌入式Linux开发者提供了LED驱动开发的实践指南。
三种驱动开发方式:
| 开发方式 | 典型场景 | 硬件信息获取途径 | 主要优缺点 |
|---|---|---|---|
| 传统方式 (board file) | 老旧内核 / 无设备树的早期平台 | 在 arch/arm/mach-xxx/board-xxx.c 等文件中硬编码 |
代码耦合、难维护;但逻辑简单 |
| 平台总线方式 (Platform bus 固定注册) | x86/ARM 早期平台,硬件较固定 | 在板级文件中调用 platform_device_register() 传递 resource |
资源与驱动分离,仍需修改内核源码 |
| 设备树方式 (Device Tree) | 主流 ARM SoC (3.x+ 内核) | Bootloader 加载 DTS→DTB→内核解析 |
硬件描述与驱动逻辑彻底解耦,易维护、可复用 |

设备树实现led驱动:

1. 设备树
为什么要用 Device Tree?
去硬编码:ARM SoC 板级差异巨大,内核合入困难。Device Tree 让硬件信息由
Bootloader传入,内核只关心逻辑。复用性强:同一驱动可在多个平台复用,仅修改 DTS 。
可维护性:硬件工程师可独立维护 DTS,驱动开发者专注逻辑。
DTS/DTSI/DTB 关系与编译:

| 文件类型 | 作用 | 位置示例 |
|---|---|---|
*.dtsi |
共享片上资源(SoC 通用部分) | arch/arm/boot/dts/stm32mp151.dtsi |
*.dts |
板级差异 (外设、Pin 配置) | arch/arm/boot/dts/stm32mp157a-dk1.dts |
*.dtb |
DTS 编译后的二进制 | arch/arm/boot/dts/*.dtb |
常用命令
bash
# 编译 DTS
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157a-fsmp1a.dtb
# 手动使用 dtc
./scripts/dtc/dtc -I dts -O dtb -o out.dtb arch/arm/boot/dts/xxx.dts
# 反编译便于阅读
./scripts/dtc/dtc -I dtb -O dts -o out.dts out.dtb
1.1 概念
在传统 Linux 内核中,ARM 架构的板极硬件细节过多地被硬编码在 arch/arm/plat-xxx 和 arch/arm/mach-xxx,
比如板上的 platform 设备、resource、i2c_board_info、spi_board_info 以及各种硬件的 platform_data,
这些板级细节代码对内核来讲只不过是垃圾代码。而采用 Device Tree 后,许多硬件的细节可以直接透过它传递给 Linux,而不再需要在 kernel 中进行大量的冗余编码。导致 ARM 的 merge 工作量较大。
之后经过 linux 团队一些讨论,对 ARM 平台的相关 code 做出如下相关规范调整,这个也正是引入 DTS 的原因。
1、ARM 的核心代码仍然保存在 arch/arm 目录下
2、ARM SoC core architecture code 保存在 arch/arm 目录下
3、ARM SOC 的周边外设模块的驱动保存在 drivers 目录下
4、ARM SOC 的特定代码在 arch/arm/mach-xxx 目录下
5、ARM SOC board specific 的代码被移除,由 DeviceTree 机制来负责传递硬件拓扑和硬件资源信息。
本质上,Device Tree 改变了原来用 code 方式将硬件配置信息嵌入到内核代码的方法,改用 bootloader 传递一个 DB 的形式。
对于嵌入式系统,在系统启动阶段,bootloader 会加载内核并将控制权转交给内核。
在 devie tree 中,可描述的信息包括:
1、CPU 的数量和类别
2、内存基地址和大小
3、总线和桥
4、外设连接
5、中断控制器和中断的使用情况
6、GPIO 控制器和 GPIO 使用情况
7、clock 控制器和 clock 使用情况
设备树基本就是一棵电路板上的 CPU、总线、设备组成的树,Bootloader 会将这棵树传递给内核,然后内核来识别这棵树,并根据它展开出 Linux 内核中的 platform_device、i2c_client、spi_device 等设备,而这些设备用到的内存、IRQ 等资源,也被传递给内核,内核会将这些资源绑定给展开的相应设备。
Linux 内核从 3.x 开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。比如在 ARM Linux 内,一个.dts(devicetree source )文件对应一个 ARM 的 machine,一般放置在内核的 "arch/arm/boot/dts/" 目录内,比如 stmp1a-dk1 参考板的板级设备树文件就是 "arch/arm/boot/dts/ stm32mp157a-dk1.dts"。这个文件可以通过 make dtbs 命令编译成二进制的 .dtb 文件供内核驱动使用。
1.2 设备树语法
1.2.1 几个相关的文件
// 在linux内核中,设备树文件一般存放在:arch/arm/boot/dts/
1》设备树源文件
dts:设备树源文件----硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份例如stm32mp157a-dk1.dts,一般在 Linux 源码中存在大量的dts文件,对于 arm 架构可以在arch/arm/boot/dts找到相应的dts,一个dts文件对应一个 ARM 的 machie。2》设备树头文件
dtsi:设备树头文件---值得一提的是,对于一些相同的 dts 配置可以抽象到dtsi文件中,然后类似于 C 语言的方式可以 include 到dts文件中,对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置。3》设备树编译工具
dtc:是编译dts的工具,可以在 Ubuntu 系统上通过指令apt-get install device-tree-compiler安装dtc工具,不过在内核源码scripts/dtc路径下已经包含了dtc工具;4》设备树编译生成的文件
dtb(Device Tree Blob):设备树源文件编译后生成的文件 -----dts经过dtc编译之后会得到dtb文件,dtb通过Bootloader引导程序加载到内核。所以Bootloader需要支持设备树才行;Kernel也需要加入设备树的支持;
设备树的结构:
c
/dts-v1/;
/ {
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "second string";
// hex is implied in byte arrays. no '0x' prefix is required
a-byte-data-property = [01 23 34 56];
child-node1 {
first-child-property;
second-child-property = <1>;
a-string-property = "Hello, world";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 {
};
};
};
device tree 的基本单元是 node。这些 node 被组织成树状结构,除了 root node,每个node 都只有一个 parent。一个 device tree 文件中只能有一个 root node。
每个 node 中包含了若干的
property/value来描述该 node 的一些特性。每个 node 用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该 node 没有 reg 属性(后面会描述这个
property),那么该节点名字中必须不能包括@和unit-address。unit-address的具体格式是和设备挂在那个bus上相关。例如对于
cpu,其unit-address就是从 0 开始编址,以此加一。而具体的设备,例如以太网控制器,其unit-address就是寄存器地址。root node的node name是确定的,必须是"/"。
也就是说设备树源文件的结构为:1 个 root 节点
"/";root 节点下面含一系列子节点,"
node1" and "node2"节点
node1和下又含有一系列子节点,"child-node1" and "child-node2"各个节点都有一系列属性
这些属性可能为空,如 an-empty-property 可能为字符串,如 a-string-property 可能为字符串树组,如 a-string-list-property 可能为 Cells(由 u32 整数组成),如 second-child-property
1.2.2 设备树文件编译和反编译
在一个 dts 文件中个,经常会包含许多 dtsi 文件,有时候 dtsi 会嵌套很深,此时不利于我们对设备树文件的阅读和理解,这是可以将编译好的 dtb文件,反编译为一个完整的 dts 文件,便于阅读和理解:
编译:
bash
./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts //将xxx.dts 编译为 tmp.dtb
反编译:
bash
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb //将xxx.dtb 编译为 tmp.dts
bash
// 如果要自己向设备树文件中添加结点:
1,参考内核提供的文档:Documentation/devicetree/bindings/
2, 参考同类型的单板的设备树文件
3,网上搜索
4,最后,可以通过研究内核驱动,驱动需要什么数据,就添加对应数据节点
1.2.3 设备树基础语法
1》设备树节点语法:
c
一个 node 被定义成如下格式:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
}
"[]"表示 option可选,因此可以定义一个只有 node name 的空节点,label 方便在 dts 文件中引用
一个结点由属性和子结点组成,对于属性来说,它的值可以是:
text string(以 null 结束),以双引号括起来,
如:string-property = "a string";
cells 是 32 位无符号整形数,以尖括号括起来
如:cell-property = <0xbeef 123 0xabcd 1234>;
binary data 以方括号括起来
如:binary-property = [0x01 0x23 0x45 0x67];
不同类型数据可以在同一个属性中存在,以逗号分格,
如:mixed-property = "a string", [0x01 0x23 0x45 0x67],<0x12345678>;
多个字符串组成的列表也使用逗号分格,
如:string-list = "red fish","blue fish";
2》设备树节点中的特殊属性
c
/ {
model = "HQYJ FS-MP1A Discovery Board";
compatible = "st,stm32mp157a-dk1", "st,stm32mp157", "hqyj,fsmp1a";
aliases {
ethernet0 = ðernet0;
serial0 = &uart4;
serial5 = &usart3;
};
chosen {
stdout-path = "serial0:115200n8";
};
... ...
};
model 属性值是,它指定制造商的设备型号。推荐的格式是:"manufacturer,model",其中 manufacturer 是一个字符串描述制造商的名称,而型号指定型号。
compatible 属性值是,指定了系统的名称,是一个字符串列表,它包含了一个"<制造商>,<型号>"形式的字符串。重要的是要指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突。
chosen 节点不代表一个真正的设备,但功能与在固件和操作系统间传递数据的地点一样,如根参数,取代以前 bootloader 的启动参数,控制台的输入输出参数等。
port {
#address-cells = <1>; //表示子节点中reg属性中,使用几个u32整数来描述地址
#size-cells = <0>; //表示子节点中reg属性中,使用几个u32整数来描述空间大小
ltdc_ep0_out: endpoint@0 {
reg = <0>;
remote-endpoint = <&sii9022_in>;
};
};
例如:
cpus {
#address-cells = <1>; //子节点中地址用一个u32表示
#size-cells = <0>; //表示子节点中用几个u32表示内存大小
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>; //0代表cpu的地址(编号),没有大小
clocks = <&rcc CK_MPU>;
clock-names = "cpu";
operating-points-v2 = <&cpu0_opp_table>;
nvmem-cells = <&part_number_otp>;
nvmem-cell-names = "part_number";
};
}
/ {
#address-cells = <1>; //子节点中地址用一个u32表示
#size-cells = <1>; //子节点中大小用一个u32表示
...
timers2: timer@40000000 {
compatible = "st,stm32-timers";
reg = <0x40000000 0x400>; //0x40000000表示地址,0x400表达大小
};
}
3》状态
c
device tree 中的 status 标识了设备的状态,使用 status 可以去禁止设备或者启用设备,看下设备树规范中的 status 可选值。
值 描述
"okay" 表示设备正在运行
"disabled" 表示该设备目前尚未运行,但将来可能会运行
"fail" 表示设备无法运行。在设备中检测到严重错误,确实如此没有修理就不可能投入运营
"fail-sss" 表示设备无法运行。在设备中检测到严重错误,它是没有修理就不可能投入运营。值的 sss 部分特定于设备并指示检测到的错误情况。
4》引用节点的方式
c
第一种:phandle : //节点中的phandle属性,它的取值必须唯一
pic@10000000{ //中断控制器结点
phandle = <1>;
interrupt-controller;
}
device-node{
interrupt-parent = <1>; //通过节点的phandle属性引用节点,表示该设备的中断控制器为 pic
};
第二种:别名/标签
PIC:pic@10000000{ //中断控制器结点
interrupt-controller;
}
device-node{
interrupt-parent = <&PIC>; //通过标签引用节点,表示该设备的中断控制器为 pic,使用标签引用本质上也是使用phandle来引用,在编译dts为dtb时,编译器dtc会在dtb中插入phandle属性
};
1.3 设备树中的节点如何在驱动中被使用
Bootloader把 DTB 地址传给内核。- 内核启动阶段通过
unflatten_device_tree()把 DTB 转成struct device_node树 (of_root)。 of_platform_default_populate()/ 驱动子系统遍历device_node,生成对应platform_device。- 平台驱动注册 (
platform_driver_register) 后,通过platform_match()依序匹配:pdev->driver_overrideof_match_table与节点compatibleid_tabledrv->name
关键提醒:
若忘记在驱动的 .
driver.of_match_table填写兼容表,设备树方式将无法匹配,probe()不会执行。可以通过
dmesg | grep -i of:platform或grep关键信息确认设备是否成功创建为platform_device。
1.3.1 设备树的转换过程
1》第一步
c
*.dts ----编译 : *.dtb文件 ----- 内核:将dtb中每一个节点转换为: struct device_node结构体形式, 设备树就被转换为一个关于device_node的一棵树
device_node这棵树的根节点: of_root
2》第二步:
c
内核代码在使用设备树中的节点时,会将device_node转换为platform_device
在设备树中有些节点可以转换为platform_device,有些节点不能转换为platform_device
//根节点下含有compatible属性的子节点
compatible属性含有特殊的值:" simple-bus", "simple-mfd","isa","arm,amba-bus"之一
这些节点可以转换为platform_device
i2c的子节点,spi的子节点等
//如何转换
device_node中的reg属性会转化为platform_device中的内存资源
device_node中的interrupts属性会转化为platform_device中的中断资源
其他属性需要通过内核中的函数从device_node中获取
struct platform_device
|
struct device dev;
|
struct device_node *of_node; ---通过该指针可以获取属性中的值
//pdrv如何和设备树节点匹配
在pdrv中有多个匹配的接口,内核会根据顺序依次去匹配
static int platform_match(struct device *dev, struct device_driver *drv)
1,比较pdev->driver_override 和 drv->name
return !strcmp(pdev->driver_override, drv->name);
2,比较 dev->of_node节点中的compatible属性值和 drv->of_match_table //设备树节点compatible属性值和of_match_table匹配
if (of_driver_match_device(dev, drv))
3, 比较pdev->name 和 pdrv->id_table //pdev的name和id_table匹配
return platform_match_id(pdrv->id_table, pdev) != NULL;
4,比较pdev->name 和 pdrv->drv.name //pdev的name 和 pdrv父类drv中的name匹配
return (strcmp(pdev->name, drv->name) == 0);
}
1.3.2 在驱动中如何使用设备树节点
c
dtb ----> device_node--->platform_device
设备树结点操作函数:
操作设备树的函数在下面这些头文件中有声明
peter@ubuntu:~/fs-mp157/linux/linux-stm32mp-5.4.31-r0/linux-5.4.31/include/linux$ ls of*
of_address.h of_device.h of_fdt.h of_graph.h of_iommu.h of_mdio.h of_pci.h of_platform.h
of_clk.h of_dma.h of_gpio.h of.h of_irq.h of_net.h of_pdt.h of_reserved_mem.h
of.h ----操作设备树的常用函数
of_address.h -----地址相关的函数,比如:获取 reg属性中的地址,size值
of_gpio.h ---- GPIO相关的操作函数
of_platform.h --- 将device_node转换platform_device时用到的函数
例如:
struct platform_device *of_device_alloc(struct device_node *np,const char *bus_id,struct device *parent);
struct platform_device *of_find_device_by_node(struct device_node *np);
1》找结点
c
//根据路径找结点,比如: "/"根节点, "/memory" -- 对应的是memory节点
static inline struct device_node *of_find_node_by_path(const char *path)
//根据名称找结点,节点中要定义name属性
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
//根据节点类型找结点,节点中定义了device_type属性
struct device_node *of_find_node_by_type(struct device_node *from, const char *type);
//根据compatible属性找
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, char *compat);
//根据节点中的phandle属性找
struct device_node *of_find_node_by_phandle(phandle handle);
2》找结点中的属性
c
1)获取属性的结构体指针
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
//返回属性结构体指针:struct property *
2)获取属性的值
static void *of_get_property(const struct device_node *node,const char *name, int *lenp)
//返回属性值
3)获取属性值的元素个数
int of_property_count_elems_of_size(const struct device_node *np,const char *propname, int elem_size);
//根据名字找到节点的属性,确定属性的值有多少个元素
在设备树中,节点应该是:
xxx_node{
xxx_pp_name = <0x54004000 0x400> <0x5000C000 0x400>;
};
of_property_count_elems_of_size(np,"xxx_pp_name",8); //返回的个数为:2
of_property_count_elems_of_size(np,"xxx_pp_name",4); //返回的个数为:4
4)读取属性的整型值u32/u64
int of_property_read_u32(const struct device_node *np, const char *propname,u32 *out_value)
int of_property_read_u64(const struct device_node *np,const char *propname, u64 *out_value);
例如:
xxx_node{
name1 = <0x54004000>;
name2 = <0x54004000 0x00004000>;
};
of_property_read_u32(np,"name1",&val) , val1的值为:0x54004000
of_property_read_u64(np,"name2",&val2), val2的值为:0x00004000 54004000
5)获取某个u32的值
int of_property_read_u32_index(const struct device_node *np,char *propname, u32 index, u32 *out_value);
int of_property_read_u64_index(const struct device_node *np,char *propname, u32 index, u64 *out_value);
例如:
xxx_node{
name = <0x54004000 0x5400c000>;
};
of_property_read_u32_index(np,"name",1,&val); val值为:0x5400c000
6)读取数组的值
int of_property_read_variable_u8_array(const struct device_node *np,
char *propname, u8 *out_values,size_t sz_min, size_t sz_max);
int of_property_read_variable_u16_array(const struct device_node *np,
const char *propname, u16 *out_values,size_t sz_min, size_t sz_max);
int of_property_read_variable_u32_array(const struct device_node *np,
const char *propname,u32 *out_values,size_t sz_min,size_t sz_max);
int of_property_read_variable_u64_array(const struct device_node *np,
const char *propname,u64 *out_values,size_t sz_min,size_t sz_max);
例如:
xxx_node{
name = <0x54004000 0x5400c000>;
};
char val[10];
of_property_read_variable_u8_array(np,"name",val,1,10);
val中的值为:{0x00,0x40,0x00,0x54,0x00,0xc0,0x00,0x54}
of_property_read_variable_u16_array(np,"name",val,1,10);
val中的值为:{0x4000,0x5400,0xc000,0x5400};
7) 读字符串
int of_property_read_string(const struct device_node *np,char *propname,const char **out_string);
8)获取字符串数组
#define of_property_for_each_string(np, propname, prop, s) \
for (prop = of_find_property(np, propname, NULL), \
s = of_prop_next_string(prop, NULL); \
s; \
s = of_prop_next_string(prop, s))
例如:
of_property_for_each_string(np, "string_array", prop, str) //循环获取属性的多个字符串值
printk("str = %s\n",str);
9)获取32位整形数组
#define of_property_for_each_u32(np, propname, prop, p, u) \
for (prop = of_find_property(np, propname, NULL), \
p = of_prop_next_u32(prop, NULL, &u); \
p; \
p = of_prop_next_u32(prop, p, &u))
例如:
of_property_for_each_u32(np, "ages", prop, p, val) //循环获取32位整数数组的元素
printk("val = %d\n",val);
例如:在设备树文件中创建一个节点,如下:
1》在设备树文件 arch/arm/boot/dts/stm32mp157a-fsmp1a.dts 添加节点
c
/{
farsight:stm32mp157_test_dev@54004000{
compatible = "stm32mp157,test_dev";
reg = <0x54004000 0x400 0x5400c000 0x1000>;
string_array = "jack","rose";
ages = <23 21>;
bin = [080b0507];
hello;
sun{
sun_name = "robin";
age = <12>;
};
};
};
&farsight{
ages = <32 27>;
};
2》编译设备树文件,并更新
c
make ARCH=arm -j4 stm32mp157a-fsmp1a.dtb LOADADDR=0xC2000040
cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb /tftpboot/
3》编写驱动代码 ---实现内核中的 pdrv 模块
c
int test_devtree_pdrv_probe(struct platform_device * pdev)
{
struct resource *res1,*res2;
const struct device_node *np = pdev->dev.of_node;
int len,*p,val,age;
char *str,*name;
struct property *prop;
u8 b[4];
struct device_node * sub_node;
printk("---------^_^ %s--------------\n",__FUNCTION__);
//获取平台资源
res1 = platform_get_resource(pdev, IORESOURCE_MEM, 0);
res2 = platform_get_resource(pdev, IORESOURCE_MEM, 1);
printk("res1->start = %x\n",res1->start);
printk("res2->start = %x\n",res2->start);
//获取设备数节点中的自定义属性
str = (char*)of_get_property(np, "string_array", &len); //获取属性的一个字符串值
printk("str = %s,len = %d\n",str,len);
//prop_str = of_find_property(np, "string_array", &len);
of_property_for_each_string(np, "string_array", prop, str) //循环获取属性的多个字符串值
printk("str = %s\n",str);
of_property_for_each_u32(np, "ages", prop, p, val) //循环获取32位整数数组的元素
printk("val = %d\n",val);
of_property_read_u8_array(np, "bin", b, 4);
printk("bin = %x %x %x %x\n",b[0],b[1],b[2],b[3]);
//获取子节点
sub_node = of_find_node_by_name(np, "sun");
if(sub_node){
of_property_read_string(sub_node, "sun_name", (const char * *)&name);
of_property_read_u32(sub_node, "age", (u32 *)&age);
printk("name = %s,age = %d\n",name,age);
}
return 0;
}
2. 使用设备树实现led驱动
2.1 在设备树文件中添加led的信息
在设备树文件 arch/arm/boot/dts/stm32mp157a-fsmp1a.dts 中添加结点:
c
led_test1@0x54004000{
compatible = "stm32mp157,led_test1";
reg = <0x54004000 0x400>;
dev-name = "led02"; //设备结点名称
mode_clear = <0x3f>; //0x3f
mode_data = <0x15>; //0x15
shift = <5>; //5
odr = <0x7>; //0x7
minor = <8>;
};
// 编译,并更新设备树文件
bash
make ARCH=arm -j4 stm32mp157a-fsmp1a.dtb LOADADDR=0xC2000040
cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb /tftpboot/
2.2 编写平台总线的pdrv层代码
1》创建匹配table
c
const struct of_device_id led_of_match_table[] = {
{.compatible = "stm32mp157,led_test1"},
};
//1,实例化pdrv对象
struct platform_driver led_pdrv = {
.probe = led_pdrv_probe,
.remove = led_pdrv_remove,
.driver = {
.name = "stm32mp157_led", //必须要赋值:ls /sys/bus/platform/drivers/stm32mp157_led/
.of_match_table = led_of_match_table,
},
};
2》在 probe 中获取设备树结点属性信息
c
int led_pdrv_probe(struct platform_device *pdev)
{
int ret;
struct resource *res1;
char *name;
int minor;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//1,申请全局设备对象空间
led_dev = kzalloc(sizeof(*led_dev), GFP_KERNEL);
if(!led_dev){
printk("kzalloc error");
return -ENOMEM;
}
//获取device_node结点
led_dev->np = pdev->dev.of_node;
of_property_read_u32(led_dev->np, "mode_data", &led_dev->mode_data);
of_property_read_u32(led_dev->np, "mode_clear", &led_dev->mode_clear);
of_property_read_u32(led_dev->np, "odr", &led_dev->odr);
of_property_read_u32(led_dev->np, "shift", &led_dev->shift);
of_property_read_string(led_dev->np, "dev-name", (const char * *) &name);
of_property_read_u32(led_dev->np, "minor", &minor);
//2,初始化杂项设备对象
led_dev->misc.fops = &led_fops; //设备操作对象地址
led_dev->misc.minor = minor; //次设备号
led_dev->misc.name = name; //设备结点名称
//3,注册杂项设备对象
ret = misc_register(&led_dev->misc);
if(ret < 0){
printk("misc_register error\n");
goto err_kfree;
}
//获取平台资源
res1 = platform_get_resource(pdev, IORESOURCE_MEM, 0); //资源编号从0开始,表示同类型资源编号
if(!res1){
printk("platform_get_resource error\n");
goto err_misc_deregister;
}
printk("res1->start = %x\n",res1->start);
//4,硬件初始化
led_dev->gpioz = ioremap(res1->start,res1->end-res1->start+1);
if(!led_dev->gpioz){
printk("ioremap error\n");
ret = PTR_ERR(led_dev->gpioz);
goto err_misc_deregister;
}
return 0;
err_misc_deregister:
misc_deregister(&led_dev->misc);
err_kfree:
kfree(led_dev);
return ret;
}
3》在接口中使用获取的属性信息
c
int led_drv_open(struct inode *inode, struct file *filp)
{
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//将gpio设置为输出模式
led_dev->gpioz->MODER &= ~(led_dev->mode_clear << led_dev->shift *2);
led_dev->gpioz->MODER |= led_dev->mode_data << led_dev->shift *2;
return 0;
}
ssize_t led_drv_write(struct file *filp, const char __user *buf, size_t size, loff_t *flags)
{
int ret;
int value;
printk("-----------^_^ %s-------------\n",__FUNCTION__);
//将应用数据转为内核数据
ret = copy_from_user(&value, buf, size);
if(ret > 0){
printk("copy_from_user error\n");
return -EINVAL;
}
//判断应用传递的数据 1---开灯,0 --- 关灯
if(value){
//开灯
led_dev->gpioz->ODR |= led_dev->odr << led_dev->shift;
}else{
//关灯
led_dev->gpioz->ODR &= ~(led_dev->odr << led_dev->shift);
}
return size;
}
3. pinctl子系统和gpio子系统的使用
3.1 pinctl子系统
pinctl子系统 ------ 设置引脚的复用和电气属性。
传统的对引脚复用和电气属性设置都是直接对寄存器操作,完成IO的初始化,这种方法很繁琐,而且容易出问题(比如 pin 功能冲突)。在 Linux 系统中使用这种繁琐的操作不现实,所以就有了pinctl子系统。
(简单的说就是不用自己去设置引脚复用和电气属性了,只要在设备树中添加相应的节点并描述,pinctrl系统就会帮我们设置(它是内核中的一段程序))
pinctrl 子系统会干这些事:
获取设备树中
pin信息。根据获取到的
pin信息来设置引脚复用。根据获取到的
pin信息来设置电气属性(比如上/下拉、速度、驱动能力等)。对于使用者来讲,只需要在设备树里面设置好某个
io的相关属性即可,其他的初始化工作均由pinctrl子系统来完成,pinctrl子系统源码目录为drivers/pinctrl。
要想使用 pinctl,那么就需要在设备树中对 pin 的信息描述,pinctl 子系统根据描述信息来配置pin的功能,一般会在设备树下为一组 io (通常是某一个外设上使用的所有io在一个节点中配置)创建一个节点来描述
c
stm32mp151.dtsi:
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp157-pinctrl";
ranges = <0 0x50002000 0xa400>;
interrupt-parent = <&exti>;
st,syscfg = <&exti 0x60 0xff>;
hwlocks = <&hsem 0 1>;
pins-are-numbered;
gpioa: gpio@50002000 {
gpio-controller; //表示当前节点可以作为gpio控制器,管理多个gpio引脚
#gpio-cells = <2>; //子节点描述gpio引脚时,必须用2个32整数
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x0 0x400>;
clocks = <&rcc GPIOA>;
st,bank-name = "GPIOA";
status = "disabled";
};
。。。。
}
stm32mp15xx-fsmp1x.dtsi
&pinctrl {
// 在pinctrl节点中加入wifi的引脚的复用功能的配置信息
sdmmc3_b4_wifi_pins_a: sdmmc3-b4-wifi-0 {
pins1 {
pinmux = <STM32_PINMUX('F', 0, AF9)>, /* SDMMC3_D0 */
<STM32_PINMUX('F', 4, AF9)>, /* SDMMC3_D1 */
<STM32_PINMUX('D', 5, AF10)>, /* SDMMC3_D2 */
<STM32_PINMUX('D', 7, AF10)>, /* SDMMC3_D3 */
<STM32_PINMUX('D', 0, AF10)>; /* SDMMC3_CMD */
slew-rate = <1>;
drive-push-pull;
bias-pull-up;
};
}
sdmmc3_b4_od_wifi_pins_a: sdmmc3-b4-od-wifi-0 {
pins1 {
pinmux = <STM32_PINMUX('F', 0, AF9)>, /* SDMMC3_D0 */
<STM32_PINMUX('F', 4, AF9)>, /* SDMMC3_D1 */
<STM32_PINMUX('D', 5, AF10)>, /* SDMMC3_D2 */
<STM32_PINMUX('D', 7, AF10)>; /* SDMMC3_D3 */
slew-rate = <1>;
drive-push-pull;
bias-pull-up;
};
}
sdmmc3_b4_sleep_wifi_pins_a: sdmmc3-b4-sleep-wifi-0 {
pins {
pinmux = <STM32_PINMUX('F', 0, ANALOG)>, /* SDMMC3_D0 */
<STM32_PINMUX('F', 4, ANALOG)>, /* SDMMC3_D1 */
<STM32_PINMUX('D', 5, ANALOG)>, /* SDMMC3_D2 */
<STM32_PINMUX('D', 7, ANALOG)>, /* SDMMC3_D3 */
<STM32_PINMUX('G', 15, ANALOG)>, /* SDMMC3_CK */
<STM32_PINMUX('D', 0, ANALOG)>; /* SDMMC3_CMD */
};
}
};
// 设备节点中使用配置好的 pin 的功能
&sdmmc3 {
arm,primecell-periphid = <0x10153180>;
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc3_b4_wifi_pins_a>;
pinctrl-1 = <&sdmmc3_b4_od_wifi_pins_a>;
pinctrl-2 = <&sdmmc3_b4_sleep_wifi_pins_a>;
。。。。
};
3.2 gpio子系统
当在pinctrl中将引脚的复用功能设置为 gpio 功能时,此时可以使用 gpio 子系统。
gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数。
gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API 函数来操作 GPIO , Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
c
ov5640: camera@3c {
compatible = "ovti,ov5640";
reg = <0x3c>;
clocks = <&clk_ext_camera>;
clock-names = "xclk";
DOVDD-supply = <&v2v8>;
powerdown-gpios = <&gpioa 4 (GPIO_ACTIVE_HIGH | GPIO_PUSH_PULL)>; //引脚pinctrl中的gpio的复用功能
reset-gpios = <&gpioa 3 (GPIO_ACTIVE_LOW | GPIO_PUSH_PULL)>; //引脚pinctrl中的gpio的复用功能
用两个32位整数描述gpio,第一个参数表示具体的引脚编号,第二个参数表示初始电平
/* Bit 0 express polarity */
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1
在驱动中,内核 gpio 子系统中提供相关操作 gpio 的函数,通过这些函数可以获取 gpio 信息,进而对 gpio 进行操作
c
//旧版的gpio子系统函数
static inline int gpio_request(unsigned gpio, const char *label) //获取gpio引脚使用权
static inline void gpio_free(unsigned gpio) //释放gpio引脚使用权
static inline int gpio_direction_input(unsigned gpio) //将gpio设置为输入
static inline int gpio_direction_output(unsigned gpio, int value) //将gpio设置为输出,同时输出高低电平
static inline int gpio_get_value(unsigned gpio) //获取gpio引脚的数据
static inline void gpio_set_value(unsigned gpio, int value) //设置gpio引脚的数据
//新版的gpio子系统函数
struct gpio_desc *gpiod_get(struct device *dev,const char *con_id, enum gpiod_flags flags)//获取gpio引脚对象指针
static inline void gpiod_put(struct gpio_desc *desc) //释放gpio引脚对象
int gpiod_direction_input(struct gpio_desc *desc); //将gpio设置为输入
int gpiod_direction_output(struct gpio_desc *desc, int value); //将gpio设置为输出,同时输出高低电平
int gpiod_get_value(const struct gpio_desc *desc); //获取gpio引脚的数据
void gpiod_set_value(struct gpio_desc *desc, int value); //设置gpio引脚的数据
//获取gpio引脚对象指针
struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id,enum gpiod_flags flags);
void devm_gpiod_put(struct device *dev, struct gpio_desc *desc); //释放gpio引脚对象
3.3 使用gpio子系统实现led驱动
3.3.1 gpio方式一 (旧版gpio函数)
1》在设备树中添加节点
c
led_test2@54004000{
compatible = "stm32mp157,led_test2";
led_minor = <5>;
led_name = "led02";
gpios = <&gpioz 5 0>,<&gpioz 6 0>,<&gpioz 7 0>;
};
注意:
gpios = <&gpioz 5 0>,<&gpioz 6 0>,<&gpioz 7 0>;
也可以写成下面的形式:
gpios = <&gpioz 5 0 &gpioz 6 0 &gpioz 7 0>;
或
gpios = <&gpioz 5 GPIO_ACTIVE_HIGH &gpioz 6 GPIO_ACTIVE_HIGH &gpioz 7 GPIO_ACTIVE_HIGH>;
2》在内核驱动中:
c
在probe中:
获取硬件数据
np = pdev->dev.of_node;
of_property_read_u32(np,"led_minor",&minor);
of_property_read_string(np,"led_name", (const char * *)&name);
获取gpio编号
led_dev->gpioz_5 = of_get_gpio(np, 0);
led_dev->gpioz_6 = of_get_gpio(np, 1);
led_dev->gpioz_7 = of_get_gpio(np, 2);
在write中输入或输出数据
ssize_t led_pdrv_write(struct file *filp, const char __user * buf, size_t size, loff_t *flags)
{
int ret;
int value;
printk("----------^_^ %s------------\n",__FUNCTION__);
ret = copy_from_user(&value, buf, size);
if(ret){
printk("copy_from_user error\n");
return -EINVAL;
}
gpio_request(led_dev->gpioz_5, "gpioz_5");
gpio_request(led_dev->gpioz_6, "gpioz_6");
gpio_request(led_dev->gpioz_7, "gpioz_7");
if(value){ //开灯
gpio_direction_output(led_dev->gpioz_5, 1);
gpio_direction_output(led_dev->gpioz_6, 1);
gpio_direction_output(led_dev->gpioz_7, 1);
}else{ //灭灯
gpio_direction_output(led_dev->gpioz_5, 0);
gpio_direction_output(led_dev->gpioz_6, 0);
gpio_direction_output(led_dev->gpioz_7, 0);
}
gpio_free(led_dev->gpioz_5);
gpio_free(led_dev->gpioz_6);
gpio_free(led_dev->gpioz_7);
return size;
}
3.3.2 gpio方式二(新版gpio函数)
1》设备树中添加节点
c
led_test3@54004000{
compatible = "stm32mp157,led_test3";
led_minor = <6>;
led_name = "led04";
led-gpios = <&gpioz 5 0>,<&gpioz 6 0>,<&gpioz 7 0>;
};
2》内核驱动代码:
c
在probe中:
获取硬件数据
np = pdev->dev.of_node;
of_property_read_u32(np,"led_minor",&minor);
of_property_read_string(np,"led_name", (const char * *)&name);
获取gpio引脚描述对象
led_dev->gpioz_5 = devm_gpiod_get_index(&pdev->dev, "led",0, GPIOD_OUT_LOW);
led_dev->gpioz_6 = devm_gpiod_get_index(&pdev->dev, "led",1, GPIOD_OUT_LOW);
led_dev->gpioz_7 = devm_gpiod_get_index(&pdev->dev, "led",2, GPIOD_OUT_LOW);
在write中操作gpio
ssize_t led_pdrv_write(struct file *filp, const char __user * buf, size_t size, loff_t *flags)
{
int ret;
int value;
printk("----------^_^ %s------------\n",__FUNCTION__);
ret = copy_from_user(&value, buf, size);
if(ret){
printk("copy_from_user error\n");
return -EINVAL;
}
if(value){ //开灯
gpiod_direction_output(led_dev->gpioz_5, 1);
gpiod_direction_output(led_dev->gpioz_6, 1);
gpiod_direction_output(led_dev->gpioz_7, 1);
}else{ //灭灯
gpiod_direction_output(led_dev->gpioz_5, 0);
gpiod_direction_output(led_dev->gpioz_6, 0);
gpiod_direction_output(led_dev->gpioz_7, 0);
}
return size;
}
补充:构建与部署流程
- 配置内核
make menuconfig→ 选中CONFIG_LED_STM32MP157_PDEV为模块/内建。
- 编译内核 & DTS
bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
make ARCH=arm CROSS_COMPILE=... stm32mp157a-fsmp1a.dtb
- 更新
Boot媒体- 通过
tftp/scp把新zImage、dtb拷贝到板子。 U-Boot中设置正确加载地址后启动。
- 通过
- 模块加载
- 若以模块形式:
insmod led_stm32mp157.ko。 - 通过
/dev/led02写入1/0测试。
- 若以模块形式:
综上。我们可以优先采用 设备树 + pinctrl + gpiod 的组合,实现最小硬件依赖的 LED 驱动。在 严格保持 compatible 字符串一致性 ,并在驱动中提供 MODULE_DEVICE_TABLE(of, ...) 便于模块自动加载;合理使用 devm_ 接口*,让内核自动托管资源,减少内存泄漏风险。留意调试问题先查 DTS → device_node → platform_device → driver 匹配 的四步链路。对于多模式 LED(默认、睡眠、低功耗),可以扩展 pinctrl 状态,并在 suspend/resume 中切换。
通过将本文中的示例代码与流程串联,我们可以迅速从 "添加一个 DTS 节点" 到 "写入 /dev/ledXX 开关 LED" 的完整流程。