[What]linux -> 设备树基本操作

参考书籍: Linux设备驱动开发详解 参考文档: /Documentation/devicetree/booting-without-of.txt

设备树的基本操作

设备树将板级信息通过文本文件的形式与内核分离开来,达到灵活配置的目的,而不用修改相应的驱动代码.

设备树由一系列被命名的节点(Node)和属性(Property)组成,而节点本身可以包含子节点.所谓的属性,其实就是成对出现的名称和值. 在设备树中,可以描述的信息包括(原先这些信息大多被硬编码在内核中):

  • CPU 的数量和类别
  • 内存基地址和大小
  • 总线和桥
  • 外设连接
  • 中断控制器和中断使用情况
  • GPIO控制器和GPIO使用情况
  • 时钟控制器和时钟使用情况

它基本上就是画一棵电路板上CPU,总线,设备组成的树, Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出linux内核中 的 platform_device, i2c_client, spi_device等设备,而这些设备用到的内存,IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应设备.

既然设备树包含了这些板级信息,可以知道:对于同一个内核代码,我们只需要修改设备树便可以支持不同的开发板。

  • 当然,内核中包含了这些开发板的驱动代码。

设备树的组成和结构

DTS,DTC和DTB

DTS(Device Tree Source)

.dts 是一种ASCII文本格式的设备树描述,易于阅读.对于ARM而言,一般放在 arch/arm/boot/dts/ 中.

由于同一个SOC可以应用于许多不同的设备,所以需要提炼出SOC公用的部分,使用 .dtsi 文件,类似于C语言的头文件.并且此文件也可以包含其他的 .dtsi 文件. 比如几乎所有的 ARM SOC 的 .dtsi 文件都引用了 skeleton.dtsi 文件. 包含格式:

/include/ "system-conf.dtsi"

设备树结构的模板:

/{
    /**
     * @brief device tree example
     */
    node1{
      //<property> = <value>
      a-string-property = "A string";
      a-string-list-property = "first string", "second string";
      a-byte-data-property = [0x01 0x23 0x34 0x56];
      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>;
    child-node1{
    };
    };
};
  1. 每个文件中的设备树都是以根节点"/"为起始(或引用其他的label),根节点可以包含多个子节点,子节点也可以包含子节点.节点的基本元素就是属性.
    • 一个节点的完整名字就是其节点路径
  2. 属性的定义非常灵活
    • 可以只有属性名,没有对应的值
    • 值可以是字符串、字符串数组、整数、整数数组、16进制、16进制数组
  3. 注释和c规则一致
/ {
  property1 = "string_value";	/* define a property containing a 0
                              * terminated string
                              */

  property2 = <0x1234abcd>;	/* define a property containing a
                            * numerical 32-bit value (hexadecimal)
                            */

  property3 = <0x12345678 0x12345678 0xdeadbeef>;
                                /* define a property containing 3
                                 * numerical 32-bit values (cells) in
                                 * hexadecimal
                                 */
  property4 = [0x0a 0x0b 0x0c 0x0d 0xde 0xea 0xad 0xbe 0xef];
                                /* define a property whose content is
                                 * an arbitrary array of bytes
                                 */

  childnode@address {	/* define a child node named "childnode"
                                 * whose unit name is "childnode at
                                 * address"
                                 */

    childprop = "hello\n";      /* define a property "childprop" of
                                 * childnode (in this case, a string)
                                 */
  };
};

假设硬件拓扑如下:

1个双核ARM Cortex-A9 32位处理器;ARM本地总线上的内存映射区域分布有两个串口(分别位于0x101f1000 和 0x101f2000),GPIO控制器(位于0x101f3000), SPI控制器(位于0x10170000),中断控制器(位于0x10140000)和一个外部总线桥;外部总线桥上又连接了 SMC SMC91111 以太网(位于 0x10100000), I2C控制器(位于0x10160000), 64MB NOR Flash(位于0x30000000);外部总线桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338 实时钟(I2C地址为0x58).

/{
  //指定生产厂商和对应的产品
  compatible = "acme,coyotes-revenge";
  #address-cells = <1>;
  #size-cells = <1>;
  interrupt-parent = <&intc>;
  //一般指定CPU只需要通过compatible匹配即可,其构架的具体信息一般在芯片相关代码中配置了
  cpus{
    #address-cells = <1>;
    #size-cells = <0>;
    cpu@0{
      compatible = "arm,cortex-a9";
      reg = <0>;
    };
    cpu@1{
      compatible = "arm,cortex-a9";
      reg = <1>;
    };
  };
  serial@0x101f1000{
    compatible = "arm,pl0111";
    reg = <0x101f1000 0x1000>;
    interrupts = <1 0>;
  };
  serial@0x101f2000{
    compatible = "arm,pl0111";
    reg = <0x101f2000 0x1000>;
    interrupts = <2 0>;
  };
  gpio@101f3000{
    compatible = "arm,pl1061";
    reg = <0x101f3000 0x1000
           0x101f4000 0x0010>;
    interrupts = <3 0>;
  };
  intc:interrupt-controller@10140000{
    compatible = "arm,pll90";
    reg = <0x10140000 0x1000>;
    interrupt-controller;
    interrupt-cells = <2>;
  };
  spi@10170000{
    compatible = "arm,pl022";
    reg = <0x10170000 0x1000>;
    interrupts = <4 0>;
  };
  external-bus{
    #address-cells = <2>;
    #size-cells = <1>;
    ranges = < 0 0 0x10100000 0x10000 //Chipselect 1, Ethernet
               1 0 0x10160000 0x10000 //Chipselect 2, i2c controller
               2 0 0x30000000 0x1000000>; //Chipselect 3, NOR Flash

    ethernet@0,0{
      compatible = "smc, smc91c111";
      reg = <0 0 0x1000>;
      interrupts = <5 2>;
    };
    i2c@1,0{
      compatible = "acme,a1234-i2c-bus";
      #address-cells = <1>;
      #size-cells = <0>;
      reg = <1 0 0x1000>;
      interrupts = <6 2>;
      rtc@58{
        compatible = "maxim,ds1338";
        reg = <58>;
        interrupts = <7 3>;
      };
    };
    flash@2,0{
    compatible = "samsung,k8f1315ebm","cfi-flash";
    reg = <2 0 0x40000000>;
    };
  };
};

DTC(Device Tree Compiler)

DTC是将 .dts 文件编译为 .dtb 的工具.其源代码位于 scripts/dtc,在内核使能了设备树的情况下, 此工具会被编译出来.

  • DTC 也可以在ubuntu中安装 sudo apt install device-tree-compiler.

对于arm而言,路径 arch/arm/boot/dts/Makefile 中指出了对应不同SOC所需要哪些 .dtb 文件。

  • 工具在编译 dts 时会检查dts的格式

也可以单独编译设备树: make dtbs (需要先设置 ARCH 以让其分析Makefile找到对应需要的dtb),或者反编译:

#  Input formats:
#  -------------
#
#     - "dtb": "blob" format, that is a flattened device-tree block
#       with header all in a binary blob.
#     - "dts": "source" format. This is a text file containing a
#       "source" for a device-tree. The format is defined later in this
#        chapter.
#     - "fs" format. This is a representation equivalent to the
#        output of /proc/device-tree, that is nodes are directories and
#	properties are files
#
# Output formats:
# ---------------
#
#     - "dtb": "blob" format
#     - "dts": "source" format
#     - "asm": assembly language file. This is a file that can be
#       sourced by gas to generate a device-tree "blob". That file can
#       then simply be added to your Makefile. Additionally, the
#       assembly file exports some symbols that can be used.
#dtc [-I <input-format>] [-O <output-format>] [-o output-filename] [-V output_version] input_filename

./scripts/dtc/dtc -I dtb -O dts -o xxx.dts /arch/arm/boot/dts/xxx.dtb

DTB(Device Tree Blob)

.dtb文件是由.dts文件通过DTC工具编译过来的,此文件可以被linux内核和u-boot识别. 此文件可以与内核镜像单独存放,也可以与内核镜像打包.编译内核时,使能 CONFIG_ARM_APPENDED_DTB 便可以打包.

可以使用内核自带的脚本将dtb转为dts以验证是否调用了正确的设备树:

./scripts/dtc/dtc -I dtb -O dts -o devicetree.dts devicetree.dtb

绑定(Binding)

设备树针对不同设置的绑定说明文档位于 Documentation/devicetree/bindings, 在使用内核提供的驱动时,需要查看此文件以知道哪些属性是必须的,哪些是可选的。

Linux内核下的 scripts/checkpath.pl 会运行一个检查,如果有人在设备树中新添加了 compatible 字符串,而没有添加相应的文档进行解释, checkpatch 程序会报出警告 =UNDOCUMENTED_DT_STRINGDT compatible string xxx apperars un-documented.

Bootloader

在u-boot中使能设备树使用: CONFIG_OF_LIBFDT

以前u-boot将启动参数发送给内核使用的是 ATAGS 方式:

  • r0 : 0
  • r1 : Machine type number
  • r2 : 存储tagged list的物理内存地址

现在一般使用设备树来传递启动参数:

  • r0 : 0
  • f1 : 一类SOC号
  • f2 : 存储device-tree的物理内存地址(需要64位对齐)

kernel通过区分 ATAG 和 device-tree 的标记来知道当前传递的是哪种类型。

对于arm而言,它们具有共用的设备树文件为 skeleton.dtsi ,此文件规定了最基本的几个节点:

  • chosen : 传递给内核的启动参数、控制命令等
  • aliases : 对一些节点的重命名
  • memory : 对系统物理内存的说明
  • model : 当前目标板名称

比如zynq下对这几个节点的填充如下:

/include/ "skeleton.dtsi"
/include/ "zynq-7000.dtsi"
/include/ "pcw.dtsi"
/include/ "pl.dtsi"

/ {
  model = "kc_sdr";
  aliases {
    serial0 = &uart1;
    spi0 = &qspi;
  };
  chosen {
    bootargs = "console=ttyPS0,460800 earlyprintk";
    stdout-path = "serial0:460800ns";
  };
  memory {
    device_type = "memory";
    reg = <0x0 0x40000000>;
  };
};

根节点兼容性

前面的根节点兼容性为 : compatible = "acme,coyotes-revenge";定义了整个系统的名称,它的组织形式为: <manufacturer>,<model>.

Linux内核通过此兼容性来判断启动的是哪类设备,此属性一般包括两个及以上的兼容性字符串,首个兼容性字符串是板子级别的名称,后面一个兼容性是芯片级别(或者芯片系列级别)的名字.

  • 比如在 zynq-zc702.dts 中: compatible = "xlnx,zynq-zc702","xlnx,zynq-7000";
  • 对于根节点兼容性的设置,一般也会有对应bind文档,比如 /Documentation/devicetree/bindings/arm 中具有对arm类各SOC的兼容说明
Xilinx Zynq Platforms Device Tree Bindings

Boards with Zynq-7000 SOC based on an ARM Cortex A9 processor
shall have the following properties.

Required root node properties:
    - compatible = "xlnx,zynq-7000";

在Linux2.6内核中,是通过匹配 bootloader 传入的 MACHINE_ID 来执行相应的初始化函数. 在引入设备树后,就是通过根节点兼容性和内核中的 .dt_compat 来实现匹配.

//file arch/arm/mach-zynq/common.c
static const char * const zynq_dt_match[] = {
"xlnx,zynq-7000",
NULL
};

DT_MACHINE_START(XILINX_EP107, "Xilinx Zynq Platform")
/* 64KB way size, 8-way associativity, parity disabled */
.l2c_aux_val = 0x00400000,
.l2c_aux_mask = 0xffbfffff,
.smp = smp_ops(zynq_smp_ops),
.map_io = zynq_map_io,
.init_irq = zynq_irq_init,
.init_machine = zynq_init_machine,
.init_late = zynq_init_late,
.init_time = zynq_timer_init,
.dt_compat = zynq_dt_match,
.reserve = zynq_memory_init,
MACHINE_END

Linux 倡导针对多个SOC,多个电路板的通用DT设备,即一个设备的 .dt_compat 包含多个电路板的.dts文件的根节点兼容属性字符串.之后如果这多个电路板的初始化序列不一样,可以 通过 int of_machine_is_compatible(const char *compat) API判断具体的电路板是什么.

static int exynos_cpufreq_probe(struct platform_device *pdev)
{

int ret = -EINVAL;
exynos_info = kzalloc(sizeof(*exynos_info), GFP_KERNEL);
if(!exynos_info)
return -ENOMEM;
exynos_info->dev = &pdev->dev;

if(of_machine_is_compatible("samsung,exynos4210")){
exynos_info->type = EXYNOS_SIC_4210;
ret = exynos4210_cpufreq_init(exynos_info);
}else if(of_machine_is_compatible("samsung,exynos4212")){
exynos_info->type = EXYNOS_SIC_4212;
ret = exynos4x12_cpufreq_init(exynos_info);
}
...
}

设备节点兼容性

设备兼容属性用于驱动和设备绑定,第一个字符串表示节点代表的确切设备,形式为: "<manufacturer>,<model>",后面的字符串表明可以兼容的其他设备. 比如: compatible = "arm,vexpress-flash", "cfi-flash"; 设备节点的兼容性和根节点的兼容性是类似的,都是从具体到抽象.

驱动需要与.dts中描述的设备节点匹配,从而使驱动的probe()函数执行.驱动需要添加OF匹配列表(struct of_device_id).

一个驱动可以在 of_match_table 中兼容多个设备,使用如下API来判断具体的设备是什么:

int of_device_is_compatible(const struct device_node *device,const char *compat);

if (immr_node && (of_device_is_compatible(immr_node,"fsl,mpc8315-immr") ||
of_device_is_compatible(immr_node, "fsl,mpc8308-immr")))
clrsetbits_be32(immap + MPC83XX_SCCR_OFFS,
MPC8315_SCCR_USB_MASK,
MPC8315_SCCR_USB_DRCM_01);
else
clrsetbits_be32(immap + MPC83XX_SCCR_OFFS,
MPC83XX_SCCR_USB_MASK,
MPC83XX_SCCR_USB_DRCM_11);

除了使用 of_device_is_compatible() 以外,还可以使用 私有数据绑定的方法 来匹配不同的设备.

struct l2c_init_data{
const char *type;
unsigned num_lock;
.....
};
#define L2C_ID(name, fns) {.compatible = name, .data = (void *)&fns}
static const struct of_device_id l2x0_ids[] __initconst = {
L2C_ID("arm,l210-cache", of_l2c210_data),
L2C_ID("arm,l220-cache", of_l2c220_data),
....
};
int __init l2x0_of_init(u32 aux_val, u32 aux_mask)
{

const struct l2c_init_data *data;
struct device_node *np;
np = of_find_matching_node(NULL, l2x0_ids);
if(!np)
return -ENODEV;
....
data = of_match_node(l2x0_ids, np)->data;
};

通过这种方法,驱动可以把与某个设备兼容的私有数据寻找出来,体现了一种面向对象的设计思想,避免了大量的 if,else.

设备节点及lable命名

节点命名的格式: <name>[@<unit-address>]; <>中的内容是必选,[]中作为可选.

  • <name> 为ASCII 字符串,多个同类设备节点的name可以一样,但unit-address要不一样.
  • @<unit-address> 为设备的起始地址.也经常在对应节点的 reg 属性中给出.
    • 对于挂在内存空间的设备,此地址直接代表在内存中的地址.
    • 对于挂在I2C总线上的外设,@后面一般跟的是从设备的I2C地址.

注意:节点名和属性名(@符号左边的字符)不能超过31个字符

可以给设备节点添加 label,之后可以通过 &label 的形式访问这个节点以获取该节点的设备地址(通过phandle,pointer handle进行的).

  • 比如在音频machine 驱动中的,设备树中节点定义
audio_speaker{
    compatible = "zynq, audio_speaker";
    audio-codec = <&ssm2518_label>;
    cpu-dai = <&audio_i2s_label>;
};
  • 为了能够获取codec和platform节点,在machine driver 的 probe 函数中需要如下操作:
static int audio_speaker_probe(struct platform_device *pdev)
{

int rc = 0;

.....
struct device_node *of_node = pdev->dev.of_node;
if(of_node == NULL)
{
return -ENXIO;
}
//获取设备树节点
audio_speaker_link.codec_of_node = of_parse_phandle(of_node, "audio-codec" , 0);
audio_speaker_link.cpu_of_node = of_parse_phandle(of_node, "cpu-dai" , 0);

//得到设备
struct device *codec_dev = &of_find_device_by_node(
audio_speaker_link.codec_of_node)->dev;

struct device *cpu_dev = &of_find_device_by_node(
audio_speaker_link.cpu_of_node)->dev;
}
  • 同时设备树支持C语言的预处理过程,所以设备树中可以包含头文件并使用宏定义.

地址编码

address-cells,size-cells

#address-cells = <num>;
#size-cells = <num>;

代表 reg 属性的 每一对表现格式,是由多长地址配多大范围.

  • 比如当两个都为1时,代表32位地址配32位大小,当 #address-cells=2 #size-cells=1 代表64位地址配32位大小

注意: 在当前节点下所设置的这两个属性, 只影响到子节点.

reg

  • 格式: reg = <address1 length1 [address2 length2] [address3 length3] ..>;
    • 其中 address length 代表设备的起始地址及其使用范围.address 为一个或多个的32位整型(即 cell),length则意味着从 address 到 address+length-1.

ranges

如果设备经过总线桥与CPU连接,其 address 往往需要经过转换才能对应CPU的内存映射. 如之前的 external-bus 所示:

ranges = <0 0 0x10100000 0x10000
          1 0 0x10160000 0x10000
          2 0 0x30000000 0x1000000>;

ranges 是地址转换表,其中的每个项目是一个子地址,父地址以及在子地址空间的大小映射. 映射表中的子地址,父地址分别采用子地址空间的 #address-cells 和父地址空间的 #address-cells.

对于本例而言,子地址空间的 #address-cells = 2,父地址空间的 #address-cells = 1,因此 0 0 0x10100000 0x10000 的前2个cell为 external-bus 桥后external-bus上片选0偏移为0,第3个cell表示external-bus上片选0偏移0的地址空间被映射到CPU 的本地总线的 0x10100000 位置,第4个cell表示映射的大小为0x10000.

中断连接

对于中断控制器而言,它提供如下属性:

  • interrupt-controller 此属性为空,中断控制器应该加上此属性表明自己的身份
  • #interrupt-cells = <num> 与#address-cells 和 #size-cells 相似,表明连接此中断控制器的设备的中断属性的cell大小

对于普通设备,与中断相关的属性还包括:

  • interrupt-parent = <&intc> 指定此设备所依附的中断控制器的 phandle,当节点没有此属性时,则从父节点继承.
  • interrupt = <val> 指定中断号,触发方式等.其值的个数由 interrupt-cells 指定,而具体的意义由驱动决定.在相应的绑定文档也会说明
    • 一个设备还可能会用到多个中断号.对于ARM GIC而言,若某设备使用了 SPI的168,169号两个中断,且都是高电平触发,则定义为 interrupts = <0 168 4>,<0 169 4>;

获取中断号可以通过 platform_get_irq 直接获取,也可以通过名称获取,如下:

edma0: dma-controller@40018000{
#dma-cells = <2>;
compatible = "fsl,vf610-edma";
reg = <0x40018000 0x2000>,
<0x40024000 0x1000>,
<0x40025000 0x1000>;
interrupts = <0 8 IRQ_TYPE_LEVEL_HIGH>,
<0 9 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "edma-tx","edma-err";
dma-channels = <32>;
clock-names = "dmamux0","dmamux1";
....
};
static int
fsl_edma_irq_init(struct platform_device *pdev,struct fsl_edma_engine *fsl_edma)
{

fsl_edma->txirq = platform_get_irq_byname(pdev,"edma-tx");
fsl_edma->errirq = platform_get_irq_byname(pdev,"edma-err");
}

GPIO,时钟连接

GPIO

对于CPIO控制器而言,其对应的设备节点需要声明 gpio-controller 属性,并设置 #gpio-cells 大小. #gpio-cells 中第一个cell为GPIO号,第2个为GPIO极性.

gpio0: gpio@e00a000{
  compatible = "xlnx,zynq-gpio-1.0";
  #gpio-cells = <2>;
  #interrupt-cells = <2>;
  clocks = <&clkc 42>;
  gpio-controller;
  interrupt-controller;
  interrupt-parent = <&intc>;
  interrupts = <0 20 4>;
  reg = <0xe000a000 0x1000>;
};

使用GPIO设备则通过定义命名 xxx-gpios 属性来引用GPIO控制器的设备节点,

sdhci@c8000400{
status = "okay";
cd-gpios = <&gpio01 0>;
wp-gpios = <&gpio02 0>;
power-gpios = <&gpio03 0>;
bus-width = <4>;
};

设备驱动通过如下方法来获取GPIO:


//在.dts和设备驱动不关心GPIO名字的情况下,也可以通过of_get_gpio() 获取
static inline int of_get_gpio(struct device_node *np,int index);

static inline int of_get_named_gpio(struct device_node *np, const char *propname, int index);

cd_gpio = of_get_named_gpio(np, "cd-gpios", 0);
wp_gpio = of_get_named_gpio(np, "wp-gpios", 0);
power_gpio = of_get_named_gpio(np, "power-gpios", 0);

时钟

与GPIO类似,时钟控制器的节点被使用时钟的模块引用:

clocks = <&clks 138>,<&clks 140>,<&clks 141>;//数字与相应时钟驱动中的CLK表的顺序对应
clock-names = "uart","general","noc";

而驱动中则使用上述的clock-names属性作为clk_get()或devm_clk_get()的第二个参数来申请时钟:

devm_clk_get(&pdev-dev, "general");

CLK表作为宏定义到了 arch/arm/boot/dts/include/dt-bindings/clock 中,所以设备树也可以使用宏来引用.

常用API

寻找节点

//一般from和type为NULL,查找与compatible匹配的节点
struct device_node *of_find_compatible_node(struct device_node *from, const char *type,
const char *compatible)
;

读取属性

//从节点np处获取propname属性的值并存储于缓存 out_values
int of_property_read_u8_array(const struct device_node *np,const char *propname,
u8 *out_values, size_t sz)
;

int of_property_read_u16_array(const struct device_node *np,const char *propname,
u16 *out_values, size_t sz)
;

int of_property_read_u32_array(const struct device_node *np,const char *propname,
u32 *out_values, size_t sz)
;

int of_property_read_u64_array(const struct device_node *np,const char *propname,
u64 *out_values, size_t sz)
;



//从节点np处获取propname属性的一个值并存储于缓存 out_values
int of_property_read_u8(const struct device_node *np,const char *propname,
u8 *out_values)
;

int of_property_read_u16(const struct device_node *np,const char *propname,
u16 *out_values)
;

int of_property_read_u32(const struct device_node *np,const char *propname,
u32 *out_values)
;

int of_property_read_u64(const struct device_node *np,const char *propname,
u64 *out_values)
;


//获取字符串
int of_property_read_string(struct device_node *np, const char *propname, const char **outstring);
int of_property_read_string_index(struct device_node *np, const char *propname,
int index,const char **outstring)
;


//获取bool值,属性存在返回true
static inline bool of_property_read_bool(const struct device_node *np, const char *propname);

内存映射

//通过设备节点进行设备的内存映射,可以代替 ioremap()
void __iomem *of_iomap(struct device_node *node, int index);

//通过设备节点获取对应内存的资源
int of_address_to_resource(struct device_node *dev,int index, struct resource *r);

解析中断

//通过设备树获得设备中断号
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

获取节点对应的 platform_device

struct platform_device *of_find_device_by_node(struct device_node *np);
//获取 platform_device 对应的节点
static int sirfsoc_dma_probe(struct platform_device *op)
{

struct device_node *dn = op->dev.of_node;
}

用户空间访问

为了能够验证设备树的加载实际情况,可以查看 /sys/firmware/devicetree/base/ 文件夹下的树形结构。

同时在 /sys/bus/*/devices/**/ 下的 of_node 文件会产生一个与设备树文件的符号链接,并且当此设备与对应的驱动绑定后,会有一个 driver 文件的符号链接指向驱动文件

Last Updated 2018-11-21 Wed 07:59.
Render by hexo-renderer-org with Emacs 26.1 (Org mode 9.1.14)