• 设备树机制分析


    Linux3.x版本后,arch/arm/plat-xxx和arch/arm/mach-xxx中,描述板级细节的代码(比如platform_device、i2c_board_info等)被大量取消,取而代之的是设备树,其目录位于arch/arm/boot/dts

    1.设备树的组成

    1个dts文件+n个dtsi文件,它们编译而成的dtb文件就是真正的设备树

    • soc厂商会把soc公共的特性和多块开发板公用的特性提炼为dtsi,而dts则负责描述某个具体的产品(开发板)的特性。dts直接或间接的包含多个dtsi(类似于c语言的头文件),就体现了一个完整的产品(开发板)所有的特性。

    2.设备树的结构

    下面分别是是imx6dl-hummingboard.dts以及imx6dl.dtsi文件,我们以它们为例来分析,不难发现dts文件内容很少,只有一些板级的特征,大部分公共的硬件描述都在dtsi文件中

    soc { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; interrupt-parent = <&intc>; ranges; /*省略无关代码*/ timer@00a00600 { compatible = "arm,cortex-a9-twd-timer"; reg = <0x00a00600 0x20>; interrupts = <1 13 0xf01>; clocks = <&clks IMX6QDL_CLK_TWD>; }; aips-bus@02000000 { /* AIPS1 */ compatible = "fsl,aips-bus", "simple-bus"; #address-cells = <1>; #size-cells = <1>; reg = <0x02000000 0x100000>; ranges; /*省略无关代码*/ gpio1: gpio@0209c000 { compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio"; reg = <0x0209c000 0x4000>; interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>, <0 67 IRQ_TYPE_LEVEL_HIGH>; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; }; /*省略无关代码*/ i2c1: i2c@021a0000 { #address-cells = <1>; #size-cells = <0>; compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c"; reg = <0x021a0000 0x4000>; interrupts = <0 36 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clks IMX6QDL_CLK_I2C1>; status = "disabled"; }; }; /*省略无关代码*/ }; };

    基本构造

    • {}包围起来的结构称之为节点,dts中最开头的/ {},称为根节点。节点的标准结构是xxx@yyy{…},xxx是节点的名字,yyy则不是必须的,其值为节点的地址(寄存器地址或其他地址),比如i2c1: i2c@021a0000中的就是一个i2c控制器的寄存器基地址,rtc: pcf8523@68中的就是这个rtc设备的i2c地址
    • 有关节点的地址,比如i2c@021a0000,虽然它在名字后面跟了地址,但是正式的设置是在reg属性中设置的比如:reg = <0x021a0000 0x4000>; reg的格式通常为<address length>0x021a0000是寄存器基地址,0x4000是长度。address 和length的个数是可变的,由父节点的属性#address-cells #size-cells 决定,比如节点i2c@021a0000的父节点是aips-bus@02000000,其#address-cells #size-cells均为1,所以下面的i2c节点的reg属性就有一个address 和length,而i2c节点本身#address-cells #size-cells 分别为1和0,所以其下的rtc: pcf8523@68 reg属性就只有一个0x68(i2c地址)了
    • 如果一个节点是设备节点,那么它一定要有compatible(兼容性),因为这将作为驱动和设备(设备节点)的匹配依据,compatible(兼容性)的值可以有不止一个字符串以满足不同的需求,详见下一节。而根节点的compatible也是非常重要的,也就是"fsl,imx6dl"这个字符串,因为系统启动后,将根据根节点的compatible来判断cpu信息,并由此进行初始化
    • 一般来说,每一种设备的节点属性设置都会有一些套路,比如可以设置哪些属性?属性值怎么设置?那怎么知道这些套路呢,有两种思路
    • 第一种是抄类似的dts,比如我们自己项目的平台是4412,那么就可以抄exynos4412-tiny4412.dts、exynos4412-smdk4412.dts这类相近的dts
    • 第二种是查询内核中的文档,比如Documentation/devicetree/bindings/i2c/i2c-imx.txt就描述了imx平台的i2c属性设置方法;Documentation/devicetree/bindings/fb就描述了lcd、lvds这类属性设置方法
    • 节点与节点之间的关联,通常通过“标号引用”和“包含”来实现
    • 所谓标号引用,就是在节点名称前加上标号,这样设备树的其他位置就能够通过&符号来调用/访问该节点,比如上面代码ir_recv节点中的gpio属性,就引用了gpio1标号处的节点
    • 包含则是最基本的方式,比如我们要在i2c1接口添加一个i2c外设,那么就必须要在i2c1下面添加一个节点,比如上面代码中的rtc: pcf8523@68 {} 
    • 标号引用常常还作为节点的重写方式,比如下面代码是imx6qdl.dtsi中定义的i2c节点,而前面imx6dl-hummingboard.dts中的&i2c1,就是对i2c1标号处节点的一次重写,在其内部添加了一个rtc设备
    • 如果一个节点是属性节点(即仅仅是作为属性被其他节点调用),那么它定义在哪里其实无所谓,重要的是调用的位置,比如lcd屏幕的时序,其实我们完全可以把它定义在其他犄角旮旯,然后在lcd节点下用&来调用它,这也是可以的。这有点类似于函数:在哪定义不重要,重要的是在哪调用

    属性:地址

    属性:兼容性

    属性设置的套路

    节点之间的联系

    3.内核(驱动)与节点的匹配

    首先,内核必须要知道dtb文件的地址,这由U-boot来告诉内核,详见U-boot引导内核流程分析 6节。只要内核知晓了dtb文件的地址,那么驱动就可以通过一些API任意获取设备树的内部信息

    • 对于3.x版本之后的内核,platform、i2c、spi等设备不再需要在mach-xxx中注册,驱动程序将直接和设备树里的设备节点进行配对,是通过设备节点中的compatible(兼容性)来与设备节点进行配对的,这里只做简单介绍,具体的应用详见 基于i2c子系统的驱动分析 基于platform总线的驱动分析 
    • 这里以pcf8523驱动为例,只要驱动中的of_match_table 中的compatible 值和设备节点中的compatible 相匹配,那么probe函数就会被触发。不仅i2c是这样,platform、spi等都是这个原理

    /*定义的of_match_table*/ static const struct of_device_id pcf8523_of_match[] = { { .compatible = "nxp,pcf8523" }, { } }; /*driver 结构体中的of_match_table*/ static struct i2c_driver pcf8523_driver = { .driver = { .name = DRIVER_NAME, .owner = THIS_MODULE, .of_match_table = of_match_ptr(pcf8523_of_match), }, .probe = pcf8523_probe, .id_table = pcf8523_id, };

    i2c和spi驱动还支持一种“别名匹配”的机制,就以pcf8523为例,假设某程序员在设备树中的pcf8523设备节点中写了compatible = "pcf8523";,显然相对于驱动id_table中的"nxp,pcf8523",他遗漏了nxp字段,但是驱动却仍然可以匹配上,因为别名匹配对compatible中字符串里第二个字段敏感

    4.常见属性的设置与获取

    当修改或编写驱动时,常常需要修改gpio、时钟、中断等等参数,以前都是在mach-xxx中的device设置的,现在则要在节点里设置,然后驱动用特殊的API来获取

    • 属性的获取常常在probe函数中进行,但是获取属性之前,最重要的是,确定哪个节点触发了驱动。如果一个驱动对应多个节点,那驱动可以通过int of_device_is_compatible(const struct device_node *device, const char *name)来判断当前节点是否包含指定的compatible(兼容性)

    gpio的设置与获取

    /*imx6dl.dtsigpio1控制器的定义节点*/ gpio1: gpio@0209c000 { compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio"; reg = <0x0209c000 0x4000>; interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>, <0 67 IRQ_TYPE_LEVEL_HIGH>; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; }; /*imx6qdl-sabreauto.dtsi中某个设备节点*/ max7310_reset: max7310-reset { compatible = "gpio-reset"; reset-gpios = <&gpio1 15 1>; reset-delay-us = <1>; #reset-cells = <0>; };

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 一般来说,我们把gpio属性的名字起为xxx-gpiosxxx我们可以随便起),这样驱动才能通过特定API从识别该属性,并转换成具体的gpio号
    • 该设备节点中设置了reset-gpios = <&gpio1 15 1>;这格式是什么意思呢?&gpio1 15引用了gpio1节点,故此处含义为gpio1_15这个引脚;最后一个参数1则代表低电平有效,0则为高电平有效。至于gpio1_15具体对应哪个引脚,在imx6的手册上都有详细描述
    • 其实最后一个参数(高低电平有效)不是必须的,因为gpio1节点中设置了#gpio-cells = <2>;,所以才有两个参数;某些soc的gpio节点中会设置为#gpio-cells = <1>;,那么可以不写最后一个参数
    • 驱动一般通过以下接口获取上面节点中gpio的属性。该函数第一个参数是节点,一般可以在传入probe的参数中间接获得;第二个参数是gpio属性的名字,一定要和节点属性中的xxx-gpios相同;最后一个是编号index,当节点中有n个同名的xxx-gpios时,可以通过它来获取特定的那个gpio,同一节点中gpio同名情况很少存在,所以我们都把index设为0

    gpio = of_get_named_gpio(node, "reset-gpios", index);

    • 1
    • dts和驱动都不关心gpio名字的情况下,也可直接通过以下接口来获取gpio号,这个时候编号index就十分重要了,可以指定拿取节点的第index个gpio属性

    gpio = of_get_gpio(node, index);

    • 1

    中断的设置与获取

    假设某设备节点需要一个gpio中断

    /*先确定中断所在的组*/ interrupt-parent = <&gpio6>; /*表示中断,GPIO6中的第8个IO2为触发类型,下降沿触发*/ interrupts = <8 2>;

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 而在驱动中使用 中断号 =irq_of_parse_and_map(node, index)函数返回值来得到中断号

    自定义属性的设置与获取

    所谓的自定义属性,有点类似于老内核中的platform_data,我们在设备节点中可以随意添加自定义属性,比如下面这个节点里面的属性都是我们自己定义的

    reg_3p3v: 3p3v { compatible = "regulator-fixed"; regulator-name = "3P3V"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-always-on; };

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 针对32位整形的属性,比如上面的regulator-min-microvolt,可以利用下面这个API来获取属性值,第一个参数是节点,第二个参数是属性名字,第三个是输出型参数(把读出来的值放进去)

    of_property_read_u32(node, "regulator-min-microvolt", µvolt);

    • 1
    • 类似的读取数值的API还有:

    int of_property_read_u8(const struct device_node *np, const char *propname, u8 *out_value) int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value)

    • 1
    • 2
    • 3
    • 4
    • 下列API可检查节点中某个属性是否存在,存在则返回true,不存在则返回false

    bool of_property_read_bool(const struct device_node *np, const char *propname)

    • 1
    • 当节点中存在字符串时,可以像下面那样读取,比如我们读取前面reg_3p3v节点中的字符串

    of_property_read_string(node, "regulator-name", &string)

    • 1
    • 当节点中存在数组时,可以像下面那样读取

    /*带有数组的某个节点*/ L2: cache-controller@1e00a000 { compatible = "arm,pl310-cache"; arm,data-latency = <1 1 1>; arm,tag-latency = <1 1 1>; }; /*驱动中使用API来读取数组, &data为输出型参数*/ of_property_read_u32_array(node, "arm,pl310-cache", &data, ARRAY_SIZE(data));

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    输入输出

    下面以电源 LED 灯的驱动为例,讲述如何在内核编写代码控制 GPIO 口的输出。

    首先需要在 dts (Device Tree) 文件 firefly-rk3288.dts (0930版) 或 firefly-rk3288_beta.dts (0809版) 中增加驱动的资源描述:

    1. firefly-led{
    2. compatible = "firefly,led";
    3. led-work = <&gpio8 GPIO_A2 GPIO_ACTIVE_LOW>;
    4. led-power = <&gpio8 GPIO_A1 GPIO_ACTIVE_LOW>;
    5. status = "okay";
    6. };

    这里定义了两颗 LED 灯的 GPIO 设置:

    led-work  GPIO8_A2  GPIO_ACTIVE_LOW

    led-power GPIO8_A1  GPIO_ACTIVE_LOW

    GPIO_ACTIVE_LOW 表示低电平有效(灯亮),如果是高电平有效,需要替换为 GPIO_ACTIVE_HIGH 。

    之后在驱动程序中加入对 GPIO 口的申请和控制则可:

    #ifdef CONFIG_OF

    #include <linux/of.h>

    #include <linux/of_gpio.h>

    #endif

    static int firefly_led_probe(struct platform_device *pdev)

    {

        int ret = -1;

        int gpio, flag;

    struct device_node *led_node = pdev->dev.of_node;

    gpio = of_get_named_gpio_flags(led_node, "led-power", 0, &flag);

    if (!gpio_is_valid(gpio)){

    printk("invalid led-power: %d ",gpio);

    return -1;

    }

        if (gpio_request(gpio, "led_power")) {

    printk("gpio %d request failed! ",gpio);

            return ret;

    }

    led_info.power_gpio = gpio;

    led_info.power_enable_value = (flag == OF_GPIO_ACTIVE_LOW) ? 0 : 1;

    gpio_direction_output(led_info.power_gpio, !(led_info.power_enable_value));

    ...

    on_error:

        gpio_free(gpio);

    }

    of_get_named_gpio_flags 从设备树中读取 led-power 的 GPIO 配置编号和标志,gpio_is_valid 判断该 GPIO 编号是否有效,gpio_request 则申请占用该 GPIO。如果初始化过程出错,需要调用 gpio_free 来释放之前申请过且成功的 GPIO 

    调用 gpio_direction_output 就可以设置输出高还是低电平,因为是 GPIO_ACTIVE_LOW ,如果要灯亮,需要写入 0 。

    实际中如果要读出 GPIO,需要先设置成输入模式,然后再读取值:

    int val;

    gpio_direction_input(your_gpio);

    val = gpio_get_value(your_gpio);

    下面是常用的 GPIO API 定义:

    #include <linux/gpio.h>

    #include <linux/of_gpio.h>

    enum of_gpio_flags {

    OF_GPIO_ACTIVE_LOW = 0x1,

    };

    int of_get_named_gpio_flags(struct device_node *np, const char *propname,

       int index, enum of_gpio_flags *flags);

    int gpio_is_valid(int gpio);

    int gpio_request(unsigned gpio, const char *label);

    void gpio_free(unsigned gpio);

    int gpio_direction_input(int gpio);

    int gpio_direction_output(int gpio, int v)

    复用

    如何定义 GPIO 有哪些功能可以复用,在运行时又如何切换功能呢?以 I2C4 为例作简单的介绍。

    查规格表可知,I2C4_SDA 与 GPIO7C1 的功能定义如下:

    Pad#

    func0

    func1

    I2C4_SDA/GPIO7_C1

    gpio7c1

    i2c4tp_sda

    I2C4_SCL/GPIO7_C2

    gpio7c2

    i2c4tp_scl

    /kernel/arch/arm/boot/dts/rk3288.dtsi 里有:

    i2c4: i2c@ff160000 {

    compatible = "rockchip,rk30-i2c";

    reg = <0xff160000 0x1000>;

    interrupts = <GIC_SPI 64 IRQ_TYPE_LEVEL_HIGH>;

    #address-cells = <1>;

    #size-cells = <0>;

    pinctrl-names = "default", "gpio";

    pinctrl-0 = <&i2c4_sda &i2c4_scl>;

    pinctrl-1 = <&i2c4_gpio>;

    gpios = <&gpio7 GPIO_C1 GPIO_ACTIVE_LOW>, <&gpio7 GPIO_C2 GPIO_ACTIVE_LOW>;

    clocks = <&clk_gates6 15>;

    rockchip,check-idle = <1>;

    status = "disabled";

    };

    此处,跟复用控制相关的是 pinctrl- 开头的属性:

    • pinctrl-names 定义了状态名称列表: default (i2c 功能) 和 gpio 两种状态。
    • pinctrl-0 定义了状态 0 (即 default)时需要设置的 pinctrl: i2c4_sda 和 i2c4_scl
    • pinctrl-1 定义了状态 1 (即 gpio)时需要设置的 pinctrl: i2c4_gpio

    这些 pinctrl 在 /kernel/arch/arm/boot/dts/rk3288-pinctrl.dtsi 中定义:

    / {

    pinctrl: pinctrl@ff770000 {

    compatible = "rockchip,rk3288-pinctrl";

            ...

    gpio7_i2c4 {

    i2c4_sda:i2c4-sda {

    rockchip,pins = <I2C4TP_SDA>;

    rockchip,pull = <VALUE_PULL_DISABLE>;

    rockchip,drive = <VALUE_DRV_DEFAULT>;

    //rockchip,tristate = <VALUE_TRI_DEFAULT>;

    };

    i2c4_scl:i2c4-scl {

    rockchip,pins = <I2C4TP_SCL>;

    rockchip,pull = <VALUE_PULL_DISABLE>;

    rockchip,drive = <VALUE_DRV_DEFAULT>;

    //rockchip,tristate = <VALUE_TRI_DEFAULT>;

    };

    i2c4_gpio: i2c4-gpio {

    rockchip,pins = <FUNC_TO_GPIO(I2C4TP_SDA)>, <FUNC_TO_GPIO(I2C4TP_SCL)>;

    rockchip,drive = <VALUE_DRV_DEFAULT>;

    };

    };

            ...

        }

      }

    I2C4TP_SDA, I2C4TP_SCL 的定义在 /kernel/arch/arm/boot/dts/include/dt-bindings/pinctrl/rockchip-rk3288.h 中:

    #define GPIO7_C1 0x7c10

    #define I2C4TP_SDA 0x7c11

    #define GPIO7_C2 0x7c20

    #define I2C4TP_SCL 0x7c21

    FUN_TO_GPIO 的定义在 /kernel/arch/arm/boot/dts/include/dt-bindings/pinctrl/rockchip.h 中:

    #define FUNC_TO_GPIO(m) ((m) & 0xfff0)

    也就是说 FUNC_TO_GPIO(I2C4TP_SDA) == GPIO7_C1, FUNC_TO_GPIO(I2C4TP_SCL) == GPIO7_C2 。

    0x7c11 这样的值是有编码规则的:

    7 c1 1

    | |  `- func

    | `---- offset

    `------ bank

    0x7c11 就表示 GPIO7_C1 func1, 即 i2c4tp_sda 。

    在复用时,如果选择了 "default" (即 i2c 功能),系统会应用 i2c4_sda 和 i2c4_scl 这两个 pinctrl,最终得将 GPIO7_C1 和 GPIO7_C2 两个针脚切换成对应的 i2c 功能;而如果选择了 "gpio" ,系统会应用 i2c4_gpio 这个 pinctrl,将 GPIO7_C1 和 GPIO7_C2 两个针脚还原为 GPIO 功能。

    我们看看 i2c 的驱动程序 /kernel/drivers/i2c/busses/i2c-rockchip.c 是如何切换复用功能的:

    static int rockchip_i2c_probe(struct platform_device *pdev)

    {

    struct rockchip_i2c *i2c = NULL;

    struct resource *res;

    struct device_node *np = pdev->dev.of_node;

    int ret;

    // ...

    i2c->sda_gpio = of_get_gpio(np, 0);

    if (!gpio_is_valid(i2c->sda_gpio)) {

    dev_err(&pdev->dev, "sda gpio is invalid ");

    return -EINVAL;

    }

    ret = devm_gpio_request(&pdev->dev, i2c->sda_gpio, dev_name(&i2c->adap.dev));

    if (ret) {

    dev_err(&pdev->dev, "failed to request sda gpio ");

    return ret;

    }

    i2c->scl_gpio = of_get_gpio(np, 1);

    if (!gpio_is_valid(i2c->scl_gpio)) {

    dev_err(&pdev->dev, "scl gpio is invalid ");

    return -EINVAL;

    }

    ret = devm_gpio_request(&pdev->dev, i2c->scl_gpio, dev_name(&i2c->adap.dev));

    if (ret) {

    dev_err(&pdev->dev, "failed to request scl gpio ");

    return ret;

    }

    i2c->gpio_state = pinctrl_lookup_state(i2c->dev->pins->p, "gpio");

    if (IS_ERR(i2c->gpio_state)) {

    dev_err(&pdev->dev, "no gpio pinctrl state ");

    return PTR_ERR(i2c->gpio_state);

    }

    pinctrl_select_state(i2c->dev->pins->p, i2c->gpio_state);

    gpio_direction_input(i2c->sda_gpio);

    gpio_direction_input(i2c->scl_gpio);

    pinctrl_select_state(i2c->dev->pins->p, i2c->dev->pins->default_state);

    // ...

    }

    首先是调用 of_get_gpio 取出设备树中 i2c4 结点的 gpios 属于所定义的两个 gpio:

    gpios = <&gpio7 GPIO_C1 GPIO_ACTIVE_LOW>, <&gpio7 GPIO_C2 GPIO_ACTIVE_LOW>;

    然后是调用 devm_gpio_request 来申请 gpio,接着是调用 pinctrl_lookup_state 来查找 “gpio” 状态,而默认状态 "default" 已经由框架保存到 i2c->dev-pins->default_state 中了。

    最后调用 pinctrl_select_state 来选择是 "default" 还是 "gpio" 功能。

    下面是常用的复用 API 定义:

    #include <linux/pinctrl/consumer.h>

    struct device {

    //...

    #ifdef CONFIG_PINCTRL

    struct dev_pin_info *pins;

    #endif

    //...

    };

    struct dev_pin_info {

    struct pinctrl *p;

    struct pinctrl_state *default_state;

    #ifdef CONFIG_PM

    struct pinctrl_state *sleep_state;

    struct pinctrl_state *idle_state;

    #endif

    };

    struct pinctrl_state * pinctrl_lookup_state(struct pinctrl *p, const char *name);

    int pinctrl_select_state(struct pinctrl *p, struct pinctrl_state *s);

  • 相关阅读:
    在生成安装和部署项目时出现“Unrecoverable Build Error”(不可恢复的生成错误)错误信息
    SqlHelper(带详细中文注释)
    原创企业级控件库之大数据量分页控件
    MSSql技巧之快速得到表的记录总数
    ASP.NET 程序中常用的三十三种代码
    团队项目开发"编码规范"系列文章
    信息系统项目管理系列之二:项目生命期和组织
    原创企业级控件库之组合查询控件
    C# 中的常用正则表达式总结
    asp.net页面生存周期
  • 原文地址:https://www.cnblogs.com/edver/p/7749546.html
Copyright © 2020-2023  润新知