Linux驱动设备树解析说明

1.简介

主要是描述一下linux驱动中,使用的设备树解析API,在实际使用中,设备树节点,翻译为platform_device的过程中,会主动翻译 我们常见的内容,部分客制化内容,需要我们手动进行翻译解析。 主要了解内容概述:

- 1.哪些资源自动解析,比如寄存器地址、中断、时钟、DMA等等。

- 2.非标准资源,怎么使用ofproperty 接口解析。

- 3.分清楚物理地址,虚拟地址,内核操作虚拟地址,如何将物理地址、硬件中断转化为虚拟地址、软中断号。

备注: Open Firmware 开源固件,体现到linux开发上,主要是设备树(Device Tree)机制。

  • 当 CONFIG_OF=y 时,内核支持解析设备树(.dtb 文件),通过设备树获取硬件信息(如内存地址、中断号、设备兼容性等)。
  • 当 CONFIG_OF=n 时,内核不支持设备树,通常用于传统架构(如早期 x86)或无设备树的系统。 IS_ENABLED(CONFIG_OF) //OF 是 Open Firmware 的简写, 使用这个宏进行判断。

2.主动解析部分

Linux内核中,platform_device 的创建过程会自动解析设备树中的资源(比如:中断号、内存地址等等,),驱动开发工作就不需要 在probe函数中重复解析设备树。 当设备树解析生成platform_device时,内核会自动完成以下资源解析: 设备树代码示例(使用全志F1C200S):

i2c0: i2c@1C27000 { //0x1C27000为I2C0的寄存器首地址
            compatible = "allwinner,sun6i-a31-i2c"; 
            reg = <0x01C27000 0x400>;//寄存器范围
            interrupts = <7>; //i2c0中断号
            clocks = <&ccu CLK_BUS_I2C0>;
            resets = <&ccu RST_BUS_I2C0>;
            pinctrl-names = "default";
            pinctrl-0 = <&i2c0_pd_pins>; //i2c0引脚需要为pio中复用的引脚
            status = "disabled";
            #address-cells = <1>;
            #size-cells = <0>;
        };
        lradc: lradc@1c23400 {
            compatible = "allwinner,sun4i-a10-lradc-keys";
            reg = <0x01C23400 0x400>;
            interrupts = <22>;
        };

2.1 中断号解析

上面的示例中IIC的interupts表示硬件中断号 7。(硬件中断号,从芯片手册上获取) platform_device的resource数组中会包含中断资源,驱动可以通过platform_get_irq()直接获取中断号。

int irq = platform_get_irq(pdev, 0);  /* 直接获取第一个中断号,是该设备节点的第一个,如果存在多个,请注意 */

获取中断号后可以进行中断处理:

error = devm_request_irq(dev, platform_get_irq(pdev, 0),
                 sun4i_lradc_irq, 0,
                 "sun4i-a10-lradc-keys", lradc);
static irqreturn_t sun4i_lradc_irq(int irq, void *dev_id)
{
    struct sun4i_lradc_data *lradc = dev_id;
    u32 i, ints, val, voltage, diff, keycode = 0, closest = 0xffffffff;

    ints  = readl(lradc->base + LRADC_INTS);//这里将物理地址已经转换到了虚拟地址!才能操作!

    if (ints & CHAN0_KEYUP_IRQ) {//通过中断寄存器,判断是哪一位触发的,也就是何种中断发生了
        input_report_key(lradc->input, lradc->chan0_keycode, 0);
        lradc->chan0_keycode = 0;
    }

    if ((ints & CHAN0_KEYDOWN_IRQ) && lradc->chan0_keycode == 0) {
        val = readl(lradc->base + LRADC_DATA0) & 0x3f;
        voltage = val * lradc->vref / 63;
        printk("vol:(%d)_(%d)\r\n", val, voltage);

        for (i = 0; i < lradc->chan0_map_count; i++) {
            diff = abs(lradc->chan0_map[i].voltage - voltage);
            if (diff < closest) {
                closest = diff;
                keycode = lradc->chan0_map[i].keycode;
            }
        }
        lradc->chan0_keycode = keycode;
        input_report_key(lradc->input, lradc->chan0_keycode, 1);
    }
    input_sync(lradc->input);

    writel(ints, lradc->base + LRADC_INTS); //清中断标志,等待下一轮中断的发生!

    return IRQ_HANDLED;
}

中断号相关备注:(从硬件中断源-->软件中断号)

  • 解析设备树属性:of_irq_get 首先读取 interrupts 属性,拿到 “硬件中断号”(如 34)和对应的中断控制器节点(通过 interrupt-parent 属性关联,默认是系统根中断控制器);

  • 中断控制器映射:调用中断控制器驱动的 xlate 回调(不同中断控制器如 GIC、PIC 有不同实现),将 “硬件中断号” 转换为内核可识别的 软件 IRQ 号;

  • 返回软件 IRQ 号:这个软件 IRQ 号是内核全局唯一的编号(如 56),后续驱动可通过 request_irq 注册中断处理函数。

2.2 内存/IO地址解析

paltform_device的resource数组中会包含有内存资源,驱动可以通过platform_get_resource()获取。 设备树:(仍然使用上面提及的设备adc key)

lradc: lradc@1c23400 {
            compatible = "allwinner,sun4i-a10-lradc-keys";
            reg = <0x01C23400 0x400>;
            interrupts = <22>;
        };

上面寄存器地址为0x1c23400,宽度为0x400,

//常使用API
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
void __iomem *base = devm_ioremap_resource(&pdev->dev, res);

解析内存地址:

struct sun4i_lradc_data {
    struct device *dev;
    struct input_dev *input;
    void __iomem *base;
...
};

//probe里面处理
void xxx_probe(platform_device *pdev)
{
    struct sun4i_lradc_data *lradc;
    struct device *dev = &pdev->dev;
    //分配资源
    lradc = devm_kzalloc(dev, sizeof(struct sun4i_lradc_data), GFP_KERNEL);
    if (!lradc)
        return -ENOMEM;
    //获取reg的地址(从物理地址,转换到虚拟地址,注意非devm接口,要unioremap!!)
    lradc->base = devm_ioremap_resource(dev,
                  platform_get_resource(pdev, IORESOURCE_MEM, 0));
    if (IS_ERR(lradc->base))
        return PTR_ERR(lradc->base);

    //操作reg(上层),比如清除中断flag
    u32 ints  = readl(lradc->base + LRADC_INTS);
    writel(ints, lradc->base + LRADC_INTS);
}

2.3 其它标准资源

其它资源,比如:时钟, DMA通道,电源管理解析等等,驱动在probe函数中可以直接通过platform_device接口获取解析好的 资源,无需手动解析设备树

时钟:
    of_device_add_clocks() 解析clocks和clocks-name的属性
DMA:
    of_device_add_dma(), 解析dma dma-names
电源:
    解析power-domains等属性

自动解析,可以从arch_initcall_sync开始,遍历了设备树节点。 参考:(函数调用链)

arch_initcall_sync(of_platform_default_populate_init)  of_platform_populate()  遍历设备树节点并调用 of_platform_bus_create()

3.用户客制化内容

也就是非标准属性(比如自定义参数、非标准设备树结构),这个需要通过of_propertyread*()进行手动解析! 参考:(以全志F1C200S为参考)

//------fake device tree node parse test--------//
    xiaodong_fake_device_tree {
        compatible = "xiaodong_fake_device_tree_compatible";
        status="okay";
        is_fake_value;  //解析测试
        value = <200>;
        interrupt-parent = <&pio>;//中断控制器
        interrupts = <4 12 IRQ_TYPE_EDGE_FALLING>;//硬件中断号 12

        device_tree_node1 {
            node_name = "node_name_1";
            node_value = <1>;

        };
        device_tree_node2 {
            node_name = "node_name_2";
            node_value = <2>;

        };
    };
static int xiaodong_device_tree_parse_probe(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;  //first_layer_node---父节点指针,第一级开始
    struct device_node *next;               //second_layer_node---子节点指针

    //动态分配存储按键信息的内存,这个函数使用后不用手动释放,驱动卸载时会自动释放
    _device_node_info = devm_kzalloc(dev, sizeof(device_tree_node_parse_struct), GFP_KERNEL);
    if (_device_node_info == NULL) {
        printk("[%s]{%d},no memory!\r\n",__FUNCTION__, __LINE__);
        return -ENOMEM; 
    }

    //当前node节点中,子node数量
    _device_node_info->device_node_number = of_get_child_count(np); 
    printk("node_count: %d\r\n", _device_node_info->device_node_number);

    //当前node节点中,bool变量类型值获取
    _device_node_info->is_fake_value = of_property_read_bool(np, "is_fake_value");
    printk("is_fake_value: %d\r\n", _device_node_info->is_fake_value)

    //当前node节点,string变量值获取
    of_property_read_string(np, "status", &(_device_node_info->status) );
    printk("status: %s \r\n", _device_node_info->status);

    //当前node节点,u32值获取
    of_property_read_u32(np, "value", &(_device_node_info->value) );
    printk("value: %d \r\n", _device_node_info->value);

    //解析中断控制信息
    {
       int irq;
        char *data_to_irq = "dong";
        //获取第一个中断号[从设备树中获取]--从np节点中获取,第一个中断号
        irq = of_irq_get(np, 0);
        if(irq <= 0)
        {
            // dev_error(&pdev->dev, "Failed to get IRQ\r\n");
            return -ENXIO;
        }
        printk("-the soft irq: %d \r\n", irq);
        //注册中断处理函数(IRQF_SHARED,多个设备可共享同一中断号[0-可以设置为非共享中断])
        devm_request_irq(&pdev->dev, irq,my_irq_handle,
                    IRQF_SHARED, "xiaodong", data_to_irq);//dev_name(&pdev->dev)
    }

    //遍历子节点的信息
    for_each_child_of_node(np, next) 
    {
        const char *node_name = NULL;
        u32 node_value = 0;
        static int index = 1;
        //读取 string, 这是子节点里面的,也就是next节点!
        of_property_read_string(next, "node_name", &(node_name));
        of_property_read_u32(next, "node_value", &(node_value));
        printk("node[%d]-------------\r\n",index);
        printk("node_name: %s \r\n", node_name);
        printk("node_value: %d \r\n", node_value);
    }
    return 0;
}

4.总结

了解设备树,从基本流程概念开始,主要注意各类接口使用。

- 1.明确哪些设备树内部资源是标准化,系统已经解析,可以使用platform接口进行操作

- 2.对非标准资源,请注意解析接口,比如:of_propertyread*()