嵌入式Linux中的LED驱动控制(设备树方式)(续)

嵌入式Linux中的LED驱动控制(设备树方式)”一文通过设备树方式实现了在野火STM32MP157开发板上对三个LED灯的控制,这里来讨论一下设备树的原理。

设备树用于描述一个硬件平台的硬件资源,它经由bootloader传递到内核,内核就可从设备树中获取到硬件信息。设备树描述硬件资源时有两个特点,其一,设备树以“树状”结构来描述硬件资源。其二,设备树可以像头文件那样,一个设备树文件可以包含(引用)另外一个设备树文件,以实现设备树内容的共享重用。

设备树的代码是一种基于文本描述的形式,其源码文件以.dts为扩展名,一般一个.dts文件对应一个硬件平台。源码文件一般位于Linux源码的“/arch/arm/boot/dts”目录下。设备树目标文件则以.dtb为扩展名,是设备树源码经过编译后生成的二进制文件,它可以直接被内核获取。另外,DTC是设备树源码的编译工具,一般需要手动安装这个编译工具。

设备树的源码一般分为三个部分,即包含头文件部分,设备树节点部分和设备树节点追加内容部分。设备树由一个根节点和多个子节点组成,子节点还可以继续包含其他子节点。每个节点由一对大括号“{}”表示,而“/ {⋯};”表示“根节点”(注意最后以分号结尾),一个设备树只允许有一个根节点,不同文件中的根节点最终会合并为一个根节点。在根节点内部会形成多个子节点,如“aliases {⋯}”、“chosen{⋯}”、“memory {⋯}”等。

设备树中的每个节点都按照如下的形式约定命名。

node-name@unit-address{
    属性1 = ⋯
    属性2 = ⋯
    属性3 = ⋯
    子节点⋯
};

node-name用于指定节点的名称,它的长度为1至31个字符。节点名应当使用大写或小写字母开头,并且能够描述设备类别。根节点没有节点名,它直接使用“/”指代根节点。unit-address用于指定“单元地址”,它的值要和节点“reg”属性的第一个地址一致。如果节点没有“reg”属性值,可以直接省略“@unit-address”部分。

节点标签:节点名的简写,作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。

节点路径:指从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点,不同层次的设备树节点名称可以相同,同层次的设备树节点名称要唯一。

节点属性:指在节点“{}”之间包含的内容,通常情况下一个节点有多个属性,这些属性信息是要传递到内核的“板级硬件描述信息”,在驱动程序中会通过一些API函数来获取这些信息。编写设备树最主要的内容就是编写这些节点属性,通常情况下一个节点代表一个设备。有些节点属性是所有节点共有的,而有些属性只作用于特定的节点。节点属性分为标准属性和自定义属性,标准属性的属性名是固定的,自定义属性的属性名可按照要求自行定义。

以下是一些重要的节点属性:

compatible属性,其值由一个或多个字符串组成,有多个字符串时使用“,”分隔开。设备树中每一个表示设备的节点都要有一个compatible属性,compatible是系统用来决定绑定设备驱动的关键。compatible属性也是用来查找节点的方法之一(另外还可以通过节点名或节点路径查找指定节点)。例如,在系统初始化platform总线上的设备时,会根据设备节点的”compatible”属性与驱动of_match_table中对应的compatible值进行匹配,匹配成功就加载对应的驱动。

status属性,用于指示设备的“操作状态”,通过status可以禁止设备(disabled)或启用设备(okay),默认情况下status属性设备是使能的。

reg属性,描述设备资源在其父总线定义的地址空间内的地址,通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度,在特定情况下也会有不同的含义。reg属性值由一串数字组成,ret属性的书写格式为reg = < cells cells cells cells cells cells⋯>,长度根据实际情况而定,这些数据分为地址数据(地址字段),长度数据(大小字段),例如reg = <0x900000 0x4000>,其中0x9000000表示的是地址,0x4000表示的是地址长度,这里的reg属性指定了起始地址为0x9000000,长度为0x4000 的一块地址空间。

#address-cells和#size-cells属性要同时存在,它们用在有子节点的设备节点,用于设置子节点的“reg”属性的“格式”。#address-cells用于指定子节点reg属性“地址字段”所占的长度(单元格cells 的个数)。#size-cells用于指定子节点reg 属性“大小字段”所占的长度(单元格cells 的个数)。例如像reg = <0x9000000 x4000>这样的形式,应该设置成#address-cells = <1>,#address-cells = <1>。

model属性,用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,也可以自定义。该属性为非必要属性。

ranges属性,提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是ranges = < 子地址, 父地址, 转换长度>。如果父地址空间和子地址空间相同则无需转换,可以让renges的内容为空。 

name属性用于指定节点名,在旧的设备树中它用于确定节点名,现在的设备树已经不用了。

device_type属性也是一个很少用的属性,只用在CPU和内存的节点上。

下面来看两类特殊的子节点。

1、aliases子节点,它的作用就是为其他节点起一个别名,如下面的示例。

aliases {
  ethernet0 = &ethernet0;
  serial0 = &uart4;
  serial1 = &usart1;
  serial2 = &usart2;
  serial3 = &usart3;
}

以上面的“serial0 = &uart4;”为例,“serial0”是一个节点的名字,设置别名后可以使用“serial0”来指代uart4节点,这与节点标签有点类似。在设备树中更多的是为节点添加标签,而不太使用节点别名,别名的作用是为了“快速找到设备树节点”。在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。也可以使用别名“一步到位”找到节点。

2、chosen子节点,它位于根节点下,它不代表实际硬件,主要用于给内核传递参数,如下面的示例。

chosen {
  stdout-path = "serial0:115200n8";
};

上面只设置了“stdout-path =”serial0:115200n8”;”一条属性,表示系统标准输出stdout使用串口serial0,并指定了波特率、校验、位数等参数。此外,chosen节点还可做为uboot向Linux内核传递配置参数的“通道”。

子节点名称前如果多加了一个“&”符号,表示该节点是向已经存在的子节点追加数据,如“&cpu0 {⋯}”表示向已经存在的cpu0这个子节点中追加信息。这些源码并不一定包含在根节点“/{⋯}”内,它们本身并不是一个新的节点,只是向原有的节点追加内容,被追加的节点可能定义在当前文件中,也可能定义在当前文件所包含的其他设备树文件中。

以上是设备树相关内容的讨论,接下来再讨论一下与设备树配套的平台驱动的相关内容。

在驱动中,用一个名为device_node的结构体来描述设备树中的信息,该结构体的形式如下。

struct device_node {
  const char *name;
  const char *type;
  phandle phandle;
  const char *full_name;
  struct fwnode_handle fwnode;
  struct property *properties;
  struct property *deadprops; /* removed properties */
  struct device_node *parent;
  struct device_node *child;
  struct device_node *sibling;
  #if defined(CONFIG_OF_KOBJ)
  struct kobject kobj;
  #endif
  unsigned long _flags;
  void *data;
  #if defined(CONFIG_SPARC)
  const char *path_component_name;
  unsigned int unique_id;
  struct of_irq_controller *irq_trans;
  #endif
};

上述中,name为节点中属性为name的值。type为节点中属性为device_type的值。full_name为节点的名字,在device_node结构体后面放一个字符串,full_name指向它。properties为链表,连接该节点的所有属性。parent指向父节点。child指向子节点。sibling指向兄弟节点。

驱动如何从设备树的设备节点获取需要的数据?其实Linux内核提供了一组用于从设备节点获取资源(设备节点中定义的属性)的函数,这些函数均以of_ 开头,一般称为OF操作函数。

1、根据节点路径寻找节点函数:struct device_node *of_find_node_by_path(const char *path),参数path指定节点在设备树中的路径。如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。

2、根据节点名字寻找节点函数:struct device_node *of_find_node_by_name(struct device_node *from, const char *name),参数from指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。参数 name为要寻找的节点名。如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。

3、根据节点类型寻找节点函数:struct device_node *of_find_node_by_type(struct device_node *from, const char *type),参数from指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。参数type为要查找节点的类型,这个类型就是device_node-> type。返回值为device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

4、根据节点类型及compatible属性寻找节点函数:struct device_node *of_find_compatible_node(struct device_node from, const char *type, const char *compatible),参数from指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。参数type为要查找节点的类型,这个类型就是device_node-> type。参数compatible为要查找节点的compatible属性,相比of_find_node_by_type函数增加了一个compatible属性作为筛选条件。返回值为device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

5、根据匹配表寻找节点函数:static inline struct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match),参数from指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。参数matches为源匹配表,查找与该匹配表想匹配的设备节点。参数of_device_id是一个结构体,原型如下。

struct of_device_id {
  char name[32];
  char type[32];
  char compatible[128];
  const void *data;
};

上述中,name为节点中属性为name的值。type为节点中属性为device_type的值。compatible为节点的名字,在device_node结构体后面放一个字符串,full_name指向它。data为链表,连接该节点的所有属性该函数的返回值为device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

6、查找父节点函数:struct device_node *of_get_parent(const struct device_node *node),参数node指定要查找哪个节点的父节点。返回值为device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

7、查找子节点函数:struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev),参数node指定要查找哪个节点的子节点。参数prev为前一个子节点,查找的是prev节点之后的节点。这是一个迭代查寻过程,例如寻找第二个子节点,这里就要填第一个子节点。参数为NULL表示寻找第一个子节点。返回值为device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

以是7个函数有一个共同特点,即返回值类型相同。只要找到了节点就会返回节点对应的device_node结构体,在驱动程序中通过这个device_node来获取设备节点的属性信息,并查找它的父、子节点等等。函数of_find_node_by_path与后面六个不同,它是通过节点路径寻找节点的,而“节点路径”是从设备树源文件(.dts) 中的到的。中间四个函数是根据节点属性在某一个节点之后查找符合要求的设备节点,这个“某一个节点”是设备节点结构体(device_node),也就是说这个节点是已经找到的。最后两个函数与中间四个类似,只不过最后两个没有使用节点属性而是根据父、子关系查找。

下面来看节点属性结构体,名称为property,该结构体的形式如下。

struct property {
  char *name;
  int length;
  void *value;
  struct property *next;
  #if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
  unsigned long _flags;
  #endif
  #if defined(CONFIG_OF_PROMTREE)
  unsigned int unique_id;
  #endif
  #if defined(CONFIG_OF_KOBJ)
  struct bin_attribute attr;
  #endif
};

上述中,name为属性名,length为属性长度,value为属性值,next为下一个属性。

1、查找节点属性函数:struct property *of_find_property(const struct device_node *np, const char *name, int *lenp),参数np指定要获取那个设备节点的属性信息,参数name为属性名,参数lenp为获取得到的属性值的大小,这个指针作为输出参数,这个参数被填充的值是实际获取得到的属性大小。返回值为property类型的结构体指针,失败返回NULL。从这个结构体中可以得到想要的属性值。

2、读取整型属性函数:int of_property_read_uX_array(const struct device_node *np, const char *propname, uX *out_values, size_t sz),注意这种函数一共有4个类型,以X值(8、16、32、64)不同来区分,其他都一样。对数np指定要读取那个设备节点结构体,也就是说读取那个设备节点的数据。参数propname指定要获取设备节点的哪个属性。参数out_values是一个输出参数,是函数的“返回值”,保存读取得到的数据。参数sz是一个输入参数,它用于设置读取的长度。返回值,成功返回0,错误返回错误状态码(非零值),EINVAL(属性不存在),-ENODATA(没有要读取的数据),-EOVERFLOW(属性值列表太小)。

3、简化后的读取整型属性函数:int of_property_read_uX (const struct device_node *np, const char *propname, uX *out_values),这种函数也有4个类型,以X值(8、16、32、64)不同来区分,其他都一样。

4、读取字符串属性函数1:int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string),参数np指定要获取那个设备节点的属性信息,参数propname为属性名,参数out_string获取得到字符串指针,这是一个“输出”参数,带回一个字符串指针。也就是字符串属性值的首地址。这个地址是“属性值”在内存中的真实位置,也就是说我们可以通过对地址操作获取整个字符串属性(一个字符串属性可能包含多个字符串,这些字符串在内存中连续存储,使用’0’分隔)。该函数成功时返回0,失败返回时错误状态码。

5、读取字符串属性函数2:int of_property_read_string_index(const struct device_node *np, const char *propname, int index, const char **out_string),相比前面的函数增加了参数index,它用于指定读取属性值中第几个字符串,index从零开始计数。第一个函数只能得到属性值所在地址,也就是第一个字符串的地址,其他字符串需要我们手动修改移动地址,非常麻烦,推荐使用第2个函数。

6、内存映射相关of函数:void __iomem *of_iomap(struct device_node *np, int index),参数np指定要获取那个设备节点的属性信息。参数index用于指定映射哪一段(reg属性包含多段),标号从0开始。返回值,若成功得到转换得到的地址,失败则返回NULL。

7、获取地址的of函数:int of_address_to_resource(struct device_node *dev, int index, struct resource *r),参数np指定要获取那个设备节点的属性信息。参数index用于指定映射哪一段(reg属性包含多段),标号从0开始。参数r是一个resource结构体指针,是“输出参数”用于返回得到的地址信息。该函数成功返回0,失败返回错误状态码。

以上就是设备树配套的平台驱动的相关内容。下面来看一下“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的具体实现过程。

先是在设备树中加入了三个LED的节点,如下。

rgb_led{
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "fire,rgb_led";
        ranges;
        //红色LED节点
        led_red@0x50002000{
            compatible = "fire,led_red";
            reg = < 0x50002000 0x00000004
                    0x50002004 0x00000004
                    0x50002008 0x00000004
                    0x5000200C 0x00000004
                    0x50002018 0x00000004
                    0x50000A28 0x00000004 >;
            status = "okay";
        };
        //绿色LED节点
        led_green@0x50008000{
            compatible = "fire,led_green";
            reg = < 0x50008000 0x00000004
                    0x50008004 0x00000004
                    0x50008008 0x00000004
                    0x5000800C 0x00000004
                    0x50008018 0x00000004 >;
            status = "okay";
        };
        //蓝色LED节点
        led_blue@0x50003000{
            compatible = "fire,led_blue";
            reg = < 0x50003000 0x00000004
                    0x50003004 0x00000004
                    0x50003008 0x00000004
                    0x5000300C 0x00000004
                    0x50003018 0x00000004 >;
            status = "okay";
        };
};

以上在设备树的根下,新建了一个名为rgb_led的节点,然后在该节点内定义了#address-cells和#size-cells属性,即指定在后面的reg属性中,“地址字段”和“大小字段”所占的长度均为1个字(32位)。然后定义一个compatible,用于和平台驱动匹配。随后指定一个空的ranges属性(不能省略)。最后定了红、绿、蓝3个子节点,并给出了连接三个LED引脚的物理地址。

在子节点内部,最重要的是reg属性。以红色LED为例,它接在PA引脚上,其基址为0x50002000,每个寄存器的偏移量为4字节(32位)。所以,从0x50002000到0x50002018,分别表示了PA端口MODER、OTYPER、OSPEEDR、PUPDR、BSRR四个寄存器的地址(寄存器的具体内容可参看“嵌入式Linux中的LED驱动控制(续)”一文) 。最后一个地址0x50000A28则表示端口时钟管理寄存器RCC_MP_AHB4ENSETR,它只用设置一次,所以在后面的绿、蓝子节点中并没有设这个地址。每个子节点的地址要与reg属性中的第一个值一致。最后,三个子节点中,都设置了status状态为启用。

接下来看平台驱动,在驱动程序的入口函数中,向内核注册了一个platform_driver结构体,名称为led_platform_driver(有关platform平台的讨论可参看“嵌入式Linux中platform平台设备模型的框架(实现LED驱动)”一文)。在该结构体的driver成员中,指定了匹配表of_match_table为一个of_device_id型结构体,名为rgb_led[],在rgb_led[]中定义了一个compatible成员,它的值与设备树中rgb_led节点下的compatible属性值要一致,匹配成功会触发led_pdrv_probe函数执行。

在led_pdrv_probe函数中,先调用of_find_node_by_path("/rgb_led")在设备树中查找rgb_led节点,找到后把该节点信息赋值给rgb_led_device_node。然后通过rgb_led_device_node节点,再继续调用of_find_node_by_name函数查找红、绿、蓝3个子节点,找到后赋值给相应的device_node变量。然后通过调用of_iomap函数把找到的设备子节点中的地址与相应的寄存器变量映射起来。接下来的做法就和“嵌入式Linux中platform平台设备模型的框架(实现LED驱动)”一文中基本一样了,只不过解除映射放在了led_pdrv_remove函数中来进行。

posted @ 2024-07-10 23:56  fxzq  阅读(137)  评论(0编辑  收藏  举报