【Linux 驱动开发】五. 设备树

设备树

  • [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-xxxarch/arm/mach-xxx

比如板上的 platform 设备、resourcei2c_board_infospi_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_devicei2c_clientspi_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-addressunit-address 的具体格式是和设备挂在那个 bus 上相关。

例如对于 cpu,其 unit-address 就是从 0 开始编址,以此加一。而具体的设备,例如以太网控制器,其 unit-address 就是寄存器地址。root nodenode 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 = &ethernet0;
         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 设备树中的节点如何在驱动中被使用

  1. BootloaderDTB 地址传给内核。
  2. 内核启动阶段通过 unflatten_device_tree() 把 DTB 转成 struct device_node 树 (of_root)。
  3. of_platform_default_populate() / 驱动子系统遍历 device_node,生成对应 platform_device
  4. 平台驱动注册 (platform_driver_register) 后,通过 platform_match() 依序匹配:
    • pdev->driver_override
    • of_match_table 与节点 compatible
    • id_table
    • drv->name

关键提醒:

若忘记在驱动的 .driver.of_match_table 填写兼容表,设备树方式将无法匹配,probe() 不会执行。

可以通过 dmesg | grep -i of:platformgrep 关键信息确认设备是否成功创建为 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;
}

补充:构建与部署流程

  1. 配置内核
    • make menuconfig → 选中 CONFIG_LED_STM32MP157_PDEV 为模块/内建。
  2. 编译内核 & DTS
bash 复制代码
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
make ARCH=arm CROSS_COMPILE=... stm32mp157a-fsmp1a.dtb
  1. 更新 Boot 媒体
    • 通过 tftp/scp 把新 zImagedtb 拷贝到板子。
    • U-Boot 中设置正确加载地址后启动。
  2. 模块加载
    • 若以模块形式: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" 的完整流程。

相关推荐
岱宗夫up2 小时前
基于ROS的视觉导航系统实战:黑线循迹+激光笔跟随双模态实现(冰达机器人Nano改造)
linux·python·机器人·ros
YouEmbedded2 小时前
解码内部集成电路(IIC)与OLED屏
stm32·0.96寸oled·硬件iic·软件模拟iic·图片取模·汉字取模
开开心心_Every3 小时前
PDF转图片工具推荐:免费支持批量转换
linux·运维·服务器·spring boot·edge·pdf·powerpoint
jghhh013 小时前
基于上海钜泉科技HT7017单相计量芯片的参考例程实现
科技·单片机·嵌入式硬件
春日见3 小时前
Docker中如何删除镜像
运维·前端·人工智能·驱动开发·算法·docker·容器
郝学胜-神的一滴3 小时前
Python中的with语句与try语句:资源管理的两种哲学
linux·服务器·开发语言·python·程序人生·算法
恶魔泡泡糖3 小时前
51单片机外部中断
c语言·单片机·嵌入式硬件·51单片机
zhangrelay4 小时前
如何让手机电脑流畅飞起低碳节能性能拉满-软件安装篇-ESR-Extended Support Release-延长支持版-LTS
linux·运维·笔记·学习
意法半导体STM324 小时前
【官方原创】如何基于DevelopPackage开启安全启动(MP15x) LAT6036
javascript·stm32·单片机·嵌入式硬件·mcu·安全·stm32开发