设备树三:资源解析【转】

转自:https://zhuanlan.zhihu.com/p/146110047

内核版本

  • linux-v5.6

参考资料

  • Documentation/devicetree/
  • devicetree-specification-v0.3.pdf
  • arm64体系架构
  • 蜗窝系列博客(http://www.wowotech.net

linux系统和device在运行过程中使用到很多资源:

  • memory
  • cpus
  • gpio
  • 设备资源(irq、iomem等)

这些资源都在设备树中进行了描述,本文跟读linux内核是如何从设备树中获取到这些资源的。

一、memory

设备树规格书中对memory节点的说明如下:

A memory device node is required for all devicetrees and describes the physical memory layout for the system. If a system has multiple ranges of memory, multiple memory nodes can be created, or the ranges can be specified in the reg property of a single memory node.

可见,memory节点描述了系统物理内存的layout,是设备树文件必需的节点。

1.1 设备树定义

下面以qemu模拟的arm64(armv8)板卡的dts为例:

/ {
    #size-cells = <0x00000002>;
    #address-cells = <0x00000002>;
    compatible = "linux,dummy-virt";
    memory@40000000 {
        reg = <0x00000000 0x40000000 0x00000000 0x80000000>;
        device_type = "memory";
    };
}

因为是64-bits系统,所以分别需要用两个cells描述memory的address和size,从dts看:

  • address:0x0000 0000 4000 0000,即起始地址为1G,该值为DDR在CPU地址映射中的起始地址
  • size:0x0000 0000 8000 0000,即大小为2G,与qemu启动内核时候传入的参数(-m 2048)是相符的

在真实的系统中,往往由bootloader(如uboot)引导启动linux内核,在bootloader中会检测板卡上实际DDR的address和size,然后修改dtb文件(即修改该memory节点,如dtb中未定义memory节点,则会创建memory节点),将实际内存layout情况告知内核。

1.2 解析流程

在设备树系列文章的第一篇其实已经提到了memory资源的解析,路径如下:

start_kernel
--> setup_arch
    --> setup_machine_fdt(__fdt_pointer);
        --> early_init_dt_scan(dt_virt)
            --> early_init_dt_scan_nodes
                --> early_init_dt_scan_memory

early_init_dt_scan_memory()函数实现如下:

/*  */
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
                                     int depth, void *data)
{
        const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
        const __be32 *reg, *endp;

        /* of_scan_flat_dt中会对每个node调用early_init_dt_scan_memory函数,
         * 此处只对device_type属性为memory的node进行处理
         */
        if (type == NULL || strcmp(type, "memory") != 0)
                return 0;

        /* memory address、size信息存放在linux,usable-memory或者reg属性中
         * l: 返回property value的长度,单位为字节
         */
        reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
        if (reg == NULL)
                reg = of_get_flat_dt_prop(node, "reg", &l);
        if (reg == NULL)
                return 0;
        /* endp: 指向dtb中该property value结束处的offset */
        endp = reg + (l / sizeof(__be32));
        /* dt_root_addr_cells、dt_root_size_cells为根节点下的#size-cells、
         * #address-cells属性值(在1.1小节举例的qemu arm64的dts中该值为2、2)
         */
        while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
                u64 base, size;
                /* 此处解析设备树中的memory address和size,以1.1小节的举例为例:
                 * base: 0x0000 0000 4000 0000
                 * size: 0x0000 0000 8000 0000
                 */
                base = dt_mem_next_cell(dt_root_addr_cells, &reg);
                size = dt_mem_next_cell(dt_root_size_cells, &reg);
                /* 将该块内存加入内存子系统进行管理 */
                early_init_dt_add_memory_arch(base, size);
        }
}

early_init_dt_add_memory_arch()函数实现如下:

void __init __weak early_init_dt_add_memory_arch(u64 base, u64 size)
{
        /* #define MIN_MEMBLOCK_ADDR       __pa(PAGE_OFFSET)
         * PHYS_OFFSET: 内核镜像在DDR上的起始物理地址
         * TEXT_OFFSET: 其实内核镜像真实被加载到的地址为(PHYS_OFFSET+TEXT_OFFSET),
         *              TEXT_OFFSET大小这块区域为保留区域,一般用于存放页表或者bootlaoder
         *              和kernel间参数的传递
         * PAGE_OFFSET: 内核启动过程中,物理地址PHYS_OFFSET会线性映射到虚拟地址PAGE_OFFSET
         * 所以这里的MIN_MEMBLOCK_ADDR即是PHYS_OFFSET。
         */
        const u64 phys_offset = MIN_MEMBLOCK_ADDR;
        /* 忽略掉不足一页(PAGE_SIZE)的内存,详见commit 6072cf567a2be
         * (of: ignore sub-page memory regions)
         */
        if (size < PAGE_SIZE - (base & ~PAGE_MASK)) {
                pr_warn("Ignoring memory block 0x%llx - 0x%llx\n",
                        base, base + size);
                return;
        }
        /* 如果base没有页对齐,则进行对齐操作 */
        if (!PAGE_ALIGNED(base)) {
                size -= PAGE_SIZE - (base & ~PAGE_MASK);
                base = PAGE_ALIGN(base);
        }
        /* size也进行对齐 */
        size &= PAGE_MASK;

        if (base > MAX_MEMBLOCK_ADDR) {
                pr_warn("Ignoring memory block 0x%llx - 0x%llx\n",
                        base, base + size);
                return;
        }
        /* memory区域超过物理内存最大地址,则修正size值 */
        if (base + size - 1 > MAX_MEMBLOCK_ADDR) {
                pr_warn("Ignoring memory range 0x%llx - 0x%llx\n",
                        ((u64)MAX_MEMBLOCK_ADDR) + 1, base + size);
                size = MAX_MEMBLOCK_ADDR - base + 1;
        }
        /* memory区域在内核镜像物理地址之前,则忽略该memory区域 */
        if (base + size < phys_offset) {
                pr_warn("Ignoring memory block 0x%llx - 0x%llx\n",
                        base, base + size);
                return;
        }
        /* base在内核镜像物理地址之前,则修正base和size */
        if (base < phys_offset) {
                pr_warn("Ignoring memory range 0x%llx - 0x%llx\n",
                        base, phys_offset);
                size -= phys_offset - base;
                base = phys_offset;
        }
        /* 调用MEMBLOCK内存分配器接口,增加一个新的memblock region,
         * 将该memory加入内存管理系统 
         */
        memblock_add(base, size);
}

memblock_add()实现如下:

int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{       
        phys_addr_t end = base + size - 1;
        /* _RET_IP_: 该宏调用了内建函数__builtin_return_address(0)
         *              0: 返回当前函数的返回地址
         *              1: 返回当前函数调用者的返回地址
         * 此处即是返回当前函数的返回地址
         */
        memblock_dbg("%s: [%pa-%pa] %pS\n", __func__,
                     &base, &end, (void *)_RET_IP_);
        /* 1、MEMBLOCK内存分配器使用struct memblock维护了两种内存,memblock.memory维护
         *   着可用物理内存,memblock.reserved维护着预留内存;
         * 2、memblock定义见mm/memblock.c #110
         * 3、该函数具体过程本文不详细讨论
         */
        return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
}

二、cpus

设备树规格书中对cpus的说明如下:

A /cpus node is required for all devicetrees. It does not represent a real device in the system, but acts as a container for child cpu nodes which represent the systems CPUs.

可见,cpus也是设备树必需的一个节点,其下的cpu子节点代表系统中的cpu。

2.1 设备树定义

还是以qemu模拟的arm64(armv8)板卡为例,cpus的节点定义如下:

/ {
    psci {
        migrate = <0xc4000005>;
        cpu_on = <0xc4000003>;
        cpu_off = <0x84000002>;
        cpu_suspend = <0xc4000001>;
        method = "hvc";
        compatible = "arm,psci-0.2", "arm,psci";
    };
    cpus {
        #size-cells = <0x00000000>;
        #address-cells = <0x00000001>;
        cpu@0 {
            reg = <0x00000000>;
            enable-method = "psci";
            compatible = "arm,cortex-a57";
            device_type = "cpu";
        };
        cpu@1 {
            reg = <0x00000001>;
            enable-method = "psci";
            compatible = "arm,cortex-a57";
            device_type = "cpu";
        };
    };  
};

2.1.1 reg

根据文档Documentation/devicetree/bindings/arm/cpus.yaml中的描述,因#address-cells的属性值为0x0000 0001,所以reg属性值的[23:0]被设置成MPIDR_EL1寄存器的[23:0]位的值,其余位填充为0;

MPIDR_EL1是多核标志寄存器,具体描述见ARM文档:《ARM Cortex-A57 MPCore Processor Technical Reference Manual》,该寄存器各个位的分配如下:

[1:0]位表示当前簇(cluster)下的cpu id号,所以reg属性值0x0000 0000代表系统中的0号cpu(主cpu),0x0000 0001代表系统中的1号cpu。

2.1.2 enable-method

enable-method属性值表示cpu启动方法,在64-bits的armv8系统中,主要有两种方法:

  • spin-table
  • psci

本文举例的板卡使用的psci(Power State Coordination Interface)方法,psci是由arm定义的电源管理接口规范,linux使用它来进行各种以cpu为中心的电源操作。

使用psci时还需要定义一个psci节点,该节点详细介绍可见Documentation/devicetree/bindings/arm/psci.yaml文档;其中的method属性值代表调用psci功能的方法,一共有两种取值:

  • smc
  • hvc

smc、hvc均为arm从低异常等级向更高异常等级请求服务的指令,smc陷入EL3,hvc陷入EL2,linux通过这两种指令进入不同的异常等级,进而调用不同的psci实现;

想进一步了解异常等级的可以去看arm白皮书,下图是arm64的异常等级划分情况:

2.2 解析流程

解析cpus资源时,内核已经完成了对dtb文件处理,所以可以直接使用内核的device_node、property数据结构拓扑进行节点、属性的获取。

代码路径如下:

start_kernel
--> setup_arch
    --> psci_dt_init();
        cpu_read_bootcpu_ops();
        smp_init_cpus();

2.2.1 psci_dt_init

psci_dt_init()函数用于进行psci的初始化,大致过程为通过psci_of_match获取到匹配的psci节点,然后调用对应的psci初始化函数。

static const struct of_device_id psci_of_match[] __initconst = {
        { .compatible = "arm,psci",     .data = psci_0_1_init},
        { .compatible = "arm,psci-0.2", .data = psci_0_2_init},
        { .compatible = "arm,psci-1.0", .data = psci_1_0_init},
        {},
};
int __init psci_dt_init(void)
{
        np = of_find_matching_node_and_match(NULL, psci_of_match, &matched_np);
        init_fn = (psci_initcall_t)matched_np->data;
        ret = init_fn(np);
}

2.2.2 cpu_read_bootcpu_ops

在多核心处理器中,cpu0为主cpu核心,也称为bootstrap processor,负责系统的引导、加载和初始化;其他cpu称为application processor,内核初始化过程中会对其进行enable操作,此后各个cpu的使用基本无区别(系统关机也由cpu0完成)。

cpu_read_bootcpu_ops()函数用于获取cpu0的enable_method,并将对应的操作集记录在cpu_ops[0]中。

static inline void __init cpu_read_bootcpu_ops(void)
{
        cpu_read_ops(0);
}
int __init cpu_read_ops(int cpu)
{
        /* 读取cpu节点中的enable-method属性值,本示例中为"psci" */
        const char *enable_method = cpu_read_enable_method(cpu);
        /* 根据enable_method获取到对应的操作函数集:cpu_psci_ops */
        cpu_ops[cpu] = cpu_get_ops(enable_method);
}

cpu_psci_ops定义如下:

/* arch/arm64/kernel/psci.c */
const struct cpu_operations cpu_psci_ops = {
        .name           = "psci",
        .cpu_init       = cpu_psci_cpu_init,
        .cpu_prepare    = cpu_psci_cpu_prepare,
        .cpu_boot       = cpu_psci_cpu_boot,
#ifdef CONFIG_HOTPLUG_CPU
        .cpu_can_disable = cpu_psci_cpu_can_disable,
        .cpu_disable    = cpu_psci_cpu_disable,
        .cpu_die        = cpu_psci_cpu_die,
        .cpu_kill       = cpu_psci_cpu_kill,
#endif
};

可见,该ops支持cpu的init、prepare、boot等各种操作。

2.2.3 smp_init_cpus

smp_init_cpus()函数负责解析cpus节点,并enable 0号cpu以外的其他cpus。

首先看其中的节点解析函数of_parse_and_init_cpus():

static void __init of_parse_and_init_cpus(void)
{
        struct device_node *dn;

        /* 1、获取cpus节点
         * 2、遍历cpus节点下node name为"cpu"或者device_type属性值为"cpu"的子节点
         */
        for_each_of_cpu_node(dn) {
                /* 获取reg属性值 */
                u64 hwid = of_get_cpu_mpidr(dn);

                if (hwid == INVALID_HWID)
                        goto next;
                /* 如果有重复的cpu id号,则跳过该cpu(从cpu1开始检查,不与cpu0进行比较) */
                if (is_mpidr_duplicate(cpu_count, hwid)) {
                        pr_err("%pOF: duplicate cpu reg properties in the DT\n",
                                dn);
                        goto next;
                }
                /* cpu0情况 */
                if (hwid == cpu_logical_map(0)) {
                        /* 重复cpu编号,跳过 */
                        if (bootcpu_valid) {
                                pr_err("%pOF: duplicate boot cpu reg property in DT\n",
                                        dn);
                                goto next;
                        }

                        bootcpu_valid = true;
                        /* 将cpu与numa node进行映射 */
                        early_map_cpu_to_node(0, of_node_to_nid(dn));
                        continue;
                }
                /* cpu个数超出最大数量限制,跳过 */
                if (cpu_count >= NR_CPUS)
                        goto next;

                pr_debug("cpu logical map 0x%llx\n", hwid);
                /* 将读取到的reg属性值记录在cpu_logical_map数组中 */
                cpu_logical_map(cpu_count) = hwid;

                early_map_cpu_to_node(cpu_count, of_node_to_nid(dn));
next:
                cpu_count++;
        }
}

然后看使能其他cpus的smp_cpu_setup()函数:

static int __init smp_cpu_setup(int cpu)
{       /* 获取enable_method对应的操作函数集 */
        if (cpu_read_ops(cpu))
                return -ENODEV;
        /* 执行操作集中的init函数,初始化该cpu */
        if (cpu_ops[cpu]->cpu_init(cpu))
                return -ENODEV;
        /* 将当前cpu记录到bitmap变量__cpu_possible_mask中,该变量表示系统中可运行
         * 状态的cpus
         */
        set_cpu_possible(cpu, true);

        return 0;
}

最后看完整的smp_init_cpus()函数就比较清晰了:

void __init smp_init_cpus(void)
{       /* 解析cpus节点 */
        of_parse_and_init_cpus();
        /* nr_cpu_ids为NR_CPUS,即cpus的最大数量,默认为256 */
        if (cpu_count > nr_cpu_ids)
                pr_warn("Number of cores (%d) exceeds configured maximum of %u -
                        clipping\n", cpu_count, nr_cpu_ids);
        /* cpu0会在setup_arch之前进行一定的初始化:
         *      start_kernel
         *          该函数会读取MPIDR_EL1寄存器的值,并将该值记录在cpu_logical_map[0]中
         *      --> smp_setup_processor_id
         * 若dts中定义的cpu0的reg值与实际读取到的MPIDR_EL1的值不一致,则不继续进行其他cpus的
         * enable操作
         */
        if (!bootcpu_valid) {
                pr_err("missing boot CPU MPIDR, not enabling secondaries\n");
                return;
        }
        /* 只处理[1, nr_cpu_ids-1]范围的cpu,超出的cpu直接忽略 */
        for (i = 1; i < nr_cpu_ids; i++) {
                if (cpu_logical_map(i) != INVALID_HWID) {
                        /* enable其他cpus,若失败返回非0值,那么设置对应的cpu_logical_map
                         * 数组成员为INVALID_HWID
                         */
                        if (smp_cpu_setup(i))
                                cpu_logical_map(i) = INVALID_HWID;
                }
        }
}

三、gpio

在嵌入式系统的开发过程中,涉及到最多的就是gpio口的配置和使用了;查看soc的datasheet,可以发现很多function io都存在复用情况:

比如上图的GPIO1_B7引脚,即可以配置成普通的gpio口,也可以配置成spi3的miso口,还可以配置成i2c0的sda口;当配置成不同功能时,该pin脚也就由对应的gpio controller、spi controller和i2c controller进行控制;有些gpio功能的引脚还可以配置成中断特性,可以用于触发中断;而负责配置这些功能的硬件,即为pin controller。

本章主要查看这些配置在设备树中的描述,并跟读kernel的解析流程。

3.1 设备树定义

以高通sdm845的dts为例:

/ {
    spi8: spi@a80000 {
        compatible = "qcom,geni-spi";
        pinctrl-names = "default", "sleep";
        pinctrl-0 = <&qup_spi8_default, &qup_spi8_cs_active>;
        pinctrl-1 = <&qup_spi8_sleep, &qup_spi8_cs_sleep>;
    };
};

然后是pin controller相关节点的定义:

&soc {
    tlmm: pinctrl@3400000 {
        compatible = "qcom,sdm845-pinctrl";
        qup_spi8_default: qup-spi8-default {
            /* pin multiplexing */
            mux {
                pins = "gpio90";
                function = "gpio";
            };   
            /* pin configuration */
            config {
                pins = "gpio90";
                drive-strength = <12>;
                bias-disable = <0>; 
                input-enable;
            };   
        };
    };
};

可以看出,外设节点中一般有两种属性:

  • pinctrl-names:state列表,state的定义和电源管理相关,比如设备active时候的引脚配置和设备sleep时候的引脚配置是不同的;
  • pinctrl-x:phandle列表,每个phandle指向一个pin脚配置(pin configuration)

在pin脚配置节点中一般有几种通用的属性:

  • pins:pin group
  • function:配置的功能,如gpio、spi等

还有一些驱动能力drive-strength、上下拉电阻pull-up/down等的属性;当然这些属性名称都是可以自定义的,因为pinctrl节点一般由各个厂商自己的pinctrl驱动文件进行解析,比如树梅派4b中,pins和function属性名称变为了brcm,pins和brcm,function。

3.2 重要数据结构

3.2.1 pin control state holder相关数据结构

pinctrl-names可描述设备的多种电源管理状态,每种状态对应不同的pin脚配置,kernel使用struct pinctrl结构体来管理一个设备的所有状态:

/* drivers/pinctrl/core.h */
struct pinctrl {
        /* 系统中所有设备的pin脚配置(pin control state holder)挂入
         * 一个全局链表pinctrl_list中
         */
        struct list_head node;
        /* 该pin control state holder对应的设备 */
        struct device *dev;
        /* 该设备所有的状态挂入该链表 */
        struct list_head states;
        /* 当前的设备状态 */
        struct pinctrl_state *state;
        /* pins与function、config的mapping table */
        struct list_head dt_maps;
        /* 引用计数 */
        struct kref users;
};

描述具体状态的结构体为struct pinctrl_state:

struct pinctrl_state {
        /* 挂入pinctrl->states链表 */
        struct list_head node;
        /* 该state的名称,如default、sleep、active等 */
        const char *name;
        /* 该状态的所有pin脚设置挂入该链表 */
        struct list_head settings;
};

描述pin脚设置的结构体为struct pinctrl_setting:

enum pinctrl_map_type {
        PIN_MAP_TYPE_INVALID,
        PIN_MAP_TYPE_DUMMY_STATE,
        PIN_MAP_TYPE_MUX_GROUP,     // 功能复用设置
        PIN_MAP_TYPE_CONFIGS_PIN,   // 单一pin脚特性设置
        PIN_MAP_TYPE_CONFIGS_GROUP, // pin group特性设置
};
struct pinctrl_setting {
        /* 挂入pinctrl_state->settings链表 */
        struct list_head node;
        /* setting类型 */
        enum pinctrl_map_type type;
        /* 对应的pin controller设备 */
        struct pinctrl_dev *pctldev;
        /* 使用该state的设备名称 */
        const char *dev_name;
        union {
                /* 引脚功能设置 */
                struct pinctrl_setting_mux mux;
                /* 引脚特性设置 */
                struct pinctrl_setting_configs configs;
        } data;
};

两种设置结构体定义如下:

/* setting data for MAP_TYPE_MUX_GROUP */
struct pinctrl_setting_mux {
        unsigned group;
        unsigned func;
};
/* setting data for MAP_TYPE_CONFIGS_* */
struct pinctrl_setting_configs {
        unsigned group_or_pin;
        unsigned long *configs;
        unsigned num_configs;
};

3.2.2 device tree相关数据结构

deivce tree中的pins与funtiuon、configs的映射关系通过struct pinctrl_map进行建立:

struct pinctrl_map {
        const char *dev_name;
        const char *name;
        enum pinctrl_map_type type;
        const char *ctrl_dev_name;
        union {
                struct pinctrl_map_mux mux;
                struct pinctrl_map_configs configs;
        } data;
};

3.2.3 device与pinctrl

在struct device结构体中也有pinctrl相关的成员:

struct device {
    #ifdef CONFIG_PINCTRL
        struct dev_pin_info     *pins;
    #endif
};
struct dev_pin_info {
        struct pinctrl *p;
        struct pinctrl_state *default_state;
        struct pinctrl_state *init_state;
#ifdef CONFIG_PM
        struct pinctrl_state *sleep_state;
        struct pinctrl_state *idle_state;
#endif
};

3.3 解析流程

在统一设备模型中,当device和driver匹配时,都会调用到really_probe()函数:

static int really_probe(struct device *dev, struct device_driver *drv)
{
        /* If using pinctrl, bind pins now before probing
         * 对当前device涉及的pin脚进行pin contrl设置,其间会对设备树节点进行解析
         */
        ret = pinctrl_bind_pins(dev);
        if (ret)
                goto pinctrl_bind_failed;

        if (dev->bus->probe) {
                /* 进入对应的probe函数中 */
                ret = dev->bus->probe(dev);

        switch (ret) {
        /* 此情况下,会触发延迟probe机制,原因是某些设备的probe需要依赖其他设备的probe完成
         * 详细情况可见commit 62a6bc3a1e4f4(driver: core: Allow subsystems to continue 
         * deferring probe),以及lwn文章:https://lwn.net/Articles/662820/ (Device 
         * dependencies and deferred probing)
         */
        case -EPROBE_DEFER:
                /* Driver requested deferred probing */
                dev_dbg(dev, "Driver %s requests probe deferral\n", drv->name);
                driver_deferred_probe_add_trigger(dev, local_trigger_count);
                break;
        }
}

3.3.1 pinctrl_dt_to_map

本文只跟读解析设备树节点的部分:

pinctrl_bind_pins
--> dev->pins->p = devm_pinctrl_get(dev);
    --> p = pinctrl_get(dev);
        --> return create_pinctrl(dev, NULL);
            --> ret = pinctrl_dt_to_map(p, pctldev);

主要的设备树解析流程在pinctrl_dt_to_map()函数中:

int pinctrl_dt_to_map(struct pinctrl *p, struct pinctrl_dev *pctldev)
{
        /* 当前probe的设备的device node */
        struct device_node *np = p->dev->of_node;
        /* For each defined state ID */
        for (state = 0; ; state++) {
                /* 查找名称为pinctrl-x的属性,如pinctrl-0、pinctrl-1等 */
                propname = kasprintf(GFP_KERNEL, "pinctrl-%d", state);
                /* 从当前np下的properties链表中获取该属性 */
                prop = of_find_property(np, propname, &size);
                kfree(propname);
                if (!prop) {
                        /* 如果没找到pinctrl-0,则直接返回 */
                        if (state == 0) {
                                of_node_put(np);
                                return -ENODEV;
                        }
                        /* 如果每找到pinctrl-x(x>0),则跳出循环,停止查找 */
                        break;
                }
                /* 获取属性值,即pin configuration数组 */
                list = prop->value;
                /* 计算一共有多少个pin configuration节点 */
                size /= sizeof(*list);

                /* pinctrl-names属性值为一个string数组,此处获取第state个成员到statename
                 * 比如在3.1示例中,第0个为"default",即statename = "default"
                 */
                ret = of_property_read_string_index(np, "pinctrl-names",
                                                    state, &statename);
                /* 如果没找到对应的pinctrl-names属性值,则直接使用princtrl-x中的x
                 * 作为statename
                 */
                if (ret < 0)
                        statename = prop->name + strlen("pinctrl-");
                /* For every referenced pin configuration node in it */
                for (config = 0; config < size; config++) {
                        /* 1、在dts中,可以为device node添加label,如3.1示例中的
                         *    qup_spi8_default,然后可以通过&qup_spi8_default的方式进行引用;
                         * 2、这种情况下,在使用dtc进行编译时,会在qup-spi8-default节点下
                         *    生成phandle属性,属性值为u32整数,每个phandle值都是独一无二的,
                         *    如phandle = <0x00008000>;
                         * 3、&qup_spi8_default即等于0x00008000
                         * 此处即是以大端模式读取该phandle值,该值指向pin configuration节点
                         */
                        phandle = be32_to_cpup(list++);

                        /* of_find_node_by_phandle()函数内部流程:
                         * 1、首先计算哈希计算后的handle_hash,然后查看是否存在
                         *    对应的缓存phandle_cache[handle_hash],存在则返回缓存的pin 
                         *    configuration node
                         * 2、如果没有找到,则从root节点开始遍历,查找np->phandle与phandle
                         *    相等的node,找到后将该node加入phandle_cache缓存,并返回该node;
                         * 3、如果步骤2没找到,则返回NULL
                         */
                        np_config = of_find_node_by_phandle(phandle);
                        if (!np_config) {
                                ret = -EINVAL;
                                goto err;
                        }

                        /* 解析具体的pin configuration node */
                        ret = dt_to_map_one_config(p, pctldev, statename,
                                                   np_config);
                }
        }
}

3.3.2 dt_to_map_one_config

dt_to_map_one_config()函数实现如下:

static int dt_to_map_one_config(struct pinctrl *p,
                                struct pinctrl_dev *hog_pctldev,
                                const char *statename,
                                struct device_node *np_config)
{
        /* Find the pin controller containing np_config */
        np_pctldev = of_node_get(np_config);
        for (;;) {
                np_pctldev = of_get_next_parent(np_pctldev);
                if (!np_pctldev || of_node_is_root(np_pctldev)) {
                        of_node_put(np_pctldev);
                        /* 触发延迟probe */
                        if (IS_ENABLED(CONFIG_MODULES) && !allow_default)
                                return driver_deferred_probe_check_state_continue(p->dev);

                        return driver_deferred_probe_check_state(p->dev);
                }
                /* 遍历全局链表pinctrldev_list,寻找匹配的struct pinctrl_dev,
                 * 即对应的pin controller设备
                 */
                pctldev = get_pinctrl_dev_from_of_node(np_pctldev);
                /* 找到后跳出循环 */
                if (pctldev)
                        break;
        }
        of_node_put(np_pctldev);

        /*
         * 调用pin controller驱动中的dt_node_to_map实现对设备树中的pin脚配置
         * 节点进行解析(这也是为什么本章开头说pins、function等属性名称其实可以由厂商自定义)
         */
        ops = pctldev->desc->pctlops;
        ret = ops->(pctldev, np_config, &map, &num_maps);

        /* Stash the mapping table chunk away for later use */
        return dt_remember_or_free_map(p, statename, pctldev, map, num_maps);
}

kernel提供了通用的dt_node_to_map函数实现,有些厂商的pin controller驱动会直接调用该通用实现:

/* drivers/pinctrl/qcom/pinctrl-msm.c */
static const struct pinctrl_ops msm_pinctrl_ops = {
        .dt_node_to_map         = pinconf_generic_dt_node_to_map_group,
};

pinconf_generic_dt_node_to_map_group()函数即为通用函数,实现如下:

static inline int pinconf_generic_dt_node_to_map_group(
                struct pinctrl_dev *pctldev, struct device_node *np_config,
                struct pinctrl_map **map, unsigned *num_maps)
{
        return pinconf_generic_dt_node_to_map(pctldev, np_config, map, num_maps,
                        PIN_MAP_TYPE_CONFIGS_GROUP);
}
/* drivers/pinctrl/pinconf-generic.c 
 * pctldev:dt_to_map_one_config中找到的pin controller设备
 * np_config:pinctrl-x中指向的pin configuration节点
 */
int pinconf_generic_dt_node_to_map(struct pinctrl_dev *pctldev,
                struct device_node *np_config, struct pinctrl_map **map,
                unsigned *num_maps, enum pinctrl_map_type type)
{
        unsigned reserved_maps;
        struct device_node *np;
        int ret;

        reserved_maps = 0;
        *map = NULL; 
        *num_maps = 0;
        /* 先搜索当前pin configuration节点有没有pins属性,如没有,则返回0;
         * 3.1小节举例中,pins属性在子节点mux和config中,所以此处返回0
         */
        ret = pinconf_generic_dt_subnode_to_map(pctldev, np_config, map,
                                                &reserved_maps, num_maps, type);
        if (ret < 0)
                goto exit;

        /* 遍历当前pin configuration节点下所有有效的节点,并尝试解析pins、function等属性 */
        for_each_available_child_of_node(np_config, np) {
                ret = pinconf_generic_dt_subnode_to_map(pctldev, np, map,
                                        &reserved_maps, num_maps, type);
                if (ret < 0)
                        goto exit;
        }
        return 0;
}
EXPORT_SYMBOL_GPL(pinconf_generic_dt_node_to_map);

具体解析函数为pinconf_generic_dt_subnode_to_map():

int pinconf_generic_dt_subnode_to_map(struct pinctrl_dev *pctldev,
                struct device_node *np, struct pinctrl_map **map,
                unsigned *reserved_maps, unsigned *num_maps,
                enum pinctrl_map_type type)
{
        ret = of_property_count_strings(np, "pins");
        if (ret < 0) {
                ret = of_property_count_strings(np, "groups");
                if (ret < 0)
                        /* skip this node; may contain config child nodes 
                         * 如果当前节点下pins和groups属性均不存在,则跳过该节点
                         */
                        return 0;
        } 
        /* 保存pins个数 */
        strings_count = ret;
        /* 获取function属性值,并保存在function变量中 */
        ret = of_property_read_string(np, "function", &function);
        /* 1、解析当前节点下的config相关属性,并将属性名称与属性值打包存放在configs
         *    数组中
         * 2、num_configs保存着config相关属性的数量
         */
        ret = pinconf_generic_parse_dt_config(np, pctldev, &configs,
                                              &num_configs);

        reserve = 0;
        if (function != NULL)
                reserve++;
        if (num_configs)
                reserve++;
        reserve *= strings_count;
        /* 重新krealloc空间 */
        ret = pinctrl_utils_reserve_map(pctldev, map, reserved_maps,
                        num_maps, reserve);
        /* np: 当前设备节点
         * subnode_target_type:pins或者groups
         * prop:在of_property_for_each_string作为中间变量
         * group:每个pins\groups属性值,如3.1中的"gpio90"
         * #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, subnode_target_type, prop, group) {
                if (function) {
                        /* 将pins与function建立映射关系,即对map(struct pinctrl_map)
                         * 中的各个成员进行赋值
                         */
                        ret = pinctrl_utils_add_map_mux(pctldev, map,
                                        reserved_maps, num_maps, group,
                                        function);
                }

                if (num_configs) {
                        /* 将pins与此前解析出的configs建立映射关系,即对map
                         * (struct pinctrl_map)中的各个成员进行赋值
                         */
                        ret = pinctrl_utils_add_map_configs(pctldev, map,
                                        reserved_maps, num_maps, group, configs,
                                        num_configs, type);
                }
        }
}
EXPORT_SYMBOL_GPL(pinconf_generic_dt_subnode_to_map);

下面看下pinconf_generic_parse_dt_config()函数的具体实现:

static const struct pinconf_generic_params dt_params[] = {
        { "bias-bus-hold", PIN_CONFIG_BIAS_BUS_HOLD, 0 },
        { "bias-disable", PIN_CONFIG_BIAS_DISABLE, 0 },
        { "bias-high-impedance", PIN_CONFIG_BIAS_HIGH_IMPEDANCE, 0 },
        { "bias-pull-up", PIN_CONFIG_BIAS_PULL_UP, 1 },
        /* 等等 */
};
int pinconf_generic_parse_dt_config(struct device_node *np,
                                    struct pinctrl_dev *pctldev,
                                    unsigned long **configs,
                                    unsigned int *nconfigs)
{
        /* dt_params为默认的config项 */
        parse_dt_cfg(np, dt_params, ARRAY_SIZE(dt_params), cfg, &ncfg);
        if (pctldev && pctldev->desc->num_custom_params &&
                pctldev->desc->custom_params)
                /* custom_params为用户自定义的config项 */
                parse_dt_cfg(np, pctldev->desc->custom_params,
                             pctldev->desc->num_custom_params, cfg, &ncfg);
        /* 将获取到的config值和数量拷贝返回 */
        *configs = kmemdup(cfg, ncfg * sizeof(unsigned long), GFP_KERNEL);
        *nconfigs = ncfg;
}

parse_dt_cfg()函数是实际干活的:

static void parse_dt_cfg(struct device_node *np,
                         const struct pinconf_generic_params *params,
                         unsigned int count, unsigned long *cfg,
                         unsigned int *ncfg)
{
        for (i = 0; i < count; i++) {
                u32 val;
                int ret;
                const struct pinconf_generic_params *par = &params[i];
                /* 读取属性值到val中 */
                ret = of_property_read_u32(np, par->property, &val);
                /* 如果没有指定属性值,则使用默认值 */
                if (ret)
                        val = par->default_value;
                /* 将属性名称(如PIN_CONFIG_BIAS_BUS_HOLD)与具体值打包,并
                 * 存放到cfg数组中
                 */
                cfg[*ncfg] = pinconf_to_config_packed(par->param, val);
                (*ncfg)++;
        }
}

至此,device对应的pin configuration节点中的信息以全部解析完成,并建立的映射关系;此后这些信息可以方便的在GPIO子系统中进行使用。

四、platform设备资源

在platform_device驱动中,经常使用platform_get_resource()接口获取设备资源,常见的资源的类型定义如下:

/* include/linux/ioport.h */
/* port-mapped IO资源,cpu需使用专门的指令进行访问 */
#define IORESOURCE_IO           0x00000100      /* PCI/ISA I/O ports */
/* memory-mapped IO资源,在芯片手册的地址映射章节可以看到,除了DDR RAM,cpu还映射了很多
 * io设备的地址到cpu总线上,这样当cpu访问某个内存地址时,可能是物理内存,也可能是某个IO设备
 */
#define IORESOURCE_MEM          0x00000200
#define IORESOURCE_IRQ          0x00000400

4.1 设备树定义

还是以qemu模拟的arm64板卡为例:

/ {
    interrupt-parent = <0x00008001>;
    #size-cells = <0x00000002>;
    #address-cells = <0x00000002>;
    compatible = "linux,dummy-virt";
    pl061@9030000 {
        phandle = <0x00008003>;
        clock-names = "apb_pclk";
        clocks = <0x00008000>;
        interrupts = <0x00000000 0x00000007 0x00000004>;
        gpio-controller;
        #gpio-cells = <0x00000002>;
        compatible = "arm,pl061", "arm,primecell";
        reg = <0x00000000 0x09030000 0x00000000 0x00001000>;
    };
    intc@8000000 {
        phandle = <0x00008001>;
        compatible = "arm,cortex-a15-gic";
        interrupt-controller;
        #interrupt-cells = <0x00000003>;
    };
};

上图中的pl061为gpio controller,也是作为platform_device加入设备模型的,主要关注reg和interrupts属性:

其中的reg属性在设备树规格书中的描述如下:

The reg property describes the address of the device’s resources within the address space defined by its parent bus. Most commonly this means the offsets and lengths of memory-mapped IO register blocks, but may have a different meaning on some bus types. Addresses in the address space defined by the root node are CPU real addresses.

通常情况下描述了memory-mapped IO资源的地址偏移和大小,在本示例中:

  • address:0x0000 0000 0903 0000
  • size:0x0000 0000 0000 1000

interrupts属性描述如下:

The interrupts property of a device node defines the interrupt or interrupts that are generated by the device. The value of the interrupts property consists of an arbitrary number of interrupt specifiers. The format of an interrupt specifier is defined by the binding of the interrupt domain root.

interrupts属性定义了设备的硬件中断号偏移,根据中断控制器的不同,有些还会在该属性中定义中断触发类型等;在本示例中:

  • 中断类型:0x00000000,GIC_SPI,即共享外设中断
  • 硬件中断号:0x00000007,一般GIC_SPI从32号开始计数,所以硬件中断号应该为32+7,即39号
  • 触发类型:0x00000004,高电平触发

内核定义的触发类型如下:

/* include/linux/interrupt.h */
#define IRQF_TRIGGER_NONE       0x00000000
#define IRQF_TRIGGER_RISING     0x00000001
#define IRQF_TRIGGER_FALLING    0x00000002
#define IRQF_TRIGGER_HIGH       0x00000004
#define IRQF_TRIGGER_LOW        0x00000008

4.2 重要数据结构

kernel使用struct resource结构体对资源进行描述:

struct resource {
        resource_size_t start;
        resource_size_t end;
        const char *name;       // 资源名称
        unsigned long flags;    // 资源类型,如IORESOURCE_MEM、IORESOURCE_IRQ
        unsigned long desc;     // 资源描述,在commit 43ee493bde78d(resource: 
                                // Add I/O resource descriptor)中加入
        struct resource *parent, *sibling, *child;
};

4.3 解析流程

在创建platform_device的过程(详细流程见设备树系列文章的第二篇)中,会将当前节点下的io和irq资源一并解析并填充到platform_device结构体中,函数路径为:

of_platform_default_populate
--> of_platform_populate
    --> of_platform_bus_create
        --> of_platform_device_create_pdata
            --> of_device_alloc

of_device_alloc()函数实现如下:

/* drivers/of/platform.c */
struct platform_device *of_device_alloc(struct device_node *np,
                                  const char *bus_id,
                                  struct device *parent)
{
        /* count the io and irq resources */
        /* 第一次调用of_address_to_resource()函数,用于统计io资源的个数(解析reg属性值) */
        while (of_address_to_resource(np, num_reg, &temp_res) == 0)
                num_reg++;
        /* 统计irq资源的个数 */
        num_irq = of_irq_count(np);

        /* Populate the resource table */
        if (num_irq || num_reg) {
                /* 为res数组动态分配内存 */
                res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL);

                dev->num_resources = num_reg + num_irq;
                dev->resource = res;
                for (i = 0; i < num_reg; i++, res++) {
                        /* 第一次调用of_address_to_resource()函数,实际解析io资源,
                         * 并将相关信息存储在res数组中
                         */
                        rc = of_address_to_resource(np, i, res);
                        WARN_ON(rc);
                }
                /* 实际解析irq资源,并将相关信息存储在res数组中 */
                if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
                        pr_debug("not all legacy IRQ resources mapped for %pOFn\n",
                                 np);
        }
}
EXPORT_SYMBOL(of_device_alloc);

主要的解析函数为of_address_to_resource()和of_irq_to_resource_table()。

4.3.1 of_address_to_resource

int of_address_to_resource(struct device_node *dev, int index,
                           struct resource *r)
{
        const __be32    *addrp;
        u64             size;
        unsigned int    flags;
        const char      *name = NULL;
        /* 获取资源的address、size以及flags */
        addrp = of_get_address(dev, index, &size, &flags);
        if (addrp == NULL)
                return -EINVAL;

        /* 读取reg-names属性,该属性可选,如未定义(如本示例),则会使用dev->full_name */
        of_property_read_string_index(dev, "reg-names", index, &name);
        /* 将资源信息填充到res数组成员中 */
        return __of_address_to_resource(dev, addrp, size, flags, name, r);
}
EXPORT_SYMBOL_GPL(of_address_to_resource);

of_get_address()函数实现如下:

const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,
                    unsigned int *flags)
{
        /* Get parent & match bus type */
        parent = of_get_parent(dev);
        if (parent == NULL)
                return NULL;
        /* of_busses定义在drivers/of/address.c中,默认情况如下:
         *      {
         *              .name = "default",
         *              .addresses = "reg",
         *              .match = NULL,
         *              .count_cells = of_bus_default_count_cells,
         *              .map = of_bus_default_map,
         *              .translate = of_bus_default_translate,
         *              .get_flags = of_bus_default_get_flags,
         *      },
         * 本示例匹配的即为该默认情况
         */
        bus = of_match_bus(parent);
        /* 获取父节点的#size-cells和#address-cells,并保存到na和ns中 */
        bus->count_cells(dev, &na, &ns);
        of_node_put(parent);

        /* 获取"reg"或者"assigned-addresses"属性值 */
        prop = of_get_property(dev, bus->addresses, &psize);
        /* 每个cell都是u32类型,即4个字节,此处计算cell的个数,本示例中为4 */
        psize /= 4;
        /* 描述一个资源所需的size,本示例中为2+2,即4 */
        onesize = na + ns;
        /* 本示例reg只描述了一个资源,该循环之执行一次 */
        for (i = 0; psize >= onesize; psize -= onesize, prop += onesize, i++)
                if (i == index) {
                        if (size)
                                /* 读取size */
                                *size = of_read_number(prop + na, ns);
                        if (flags)
                                /* 读取flags
                                 * 本示例情况:of_bus_default_get_flags函数
                                 *           只返回IORESOURCE_MEM
                                 */
                                *flags = bus->get_flags(prop);
                        return prop;
                }
        return NULL;
}
EXPORT_SYMBOL(of_get_address);

__of_address_to_resource()函数实现如下:

static int __of_address_to_resource(struct device_node *dev,
                const __be32 *addrp, u64 size, unsigned int flags,
                const char *name, struct resource *r)
{
        u64 taddr;

        /* 针对IORESOURCE_MEM、IORESOURCE_IO对address做不同的转换处理,
         * 转换后的地址返回到taddr变量
         */
        if (flags & IORESOURCE_MEM)
                /* platform_device情况直接返回reg值,如果是其他总线上的设备,比如
                 * spi设备,reg此时表示的是在spi总线上的地址,需要转换成cpu地址映射
                 * 上的地址
                 */
                taddr = of_translate_address(dev, addrp);
        else if (flags & IORESOURCE_IO)
                taddr = of_translate_ioport(dev, addrp, size);
        else
                return -EINVAL;

        memset(r, 0, sizeof(struct resource));
        /* 填充resource结构体 */
        r->start = taddr;
        r->end = taddr + size - 1;
        r->flags = flags;
        r->name = name ? name : dev->full_name;

        return 0;
}

4.3.2 of_irq_to_resource_table

/* drivers/of/irq.c */
int of_irq_to_resource_table(struct device_node *dev, struct resource *res,
                int nr_irqs)
{
        int i;

        for (i = 0; i < nr_irqs; i++, res++)
                if (of_irq_to_resource(dev, i, res) <= 0)
                        break;

        return i;
}
EXPORT_SYMBOL_GPL(of_irq_to_resource_table);

实际干活的是of_irq_to_resource()函数:

int of_irq_to_resource(struct device_node *dev, int index, struct resource *r)
{
        /* 解析设备树interrupts属性,获取硬件中断号,分配一个软件中断号与之建立映射,
         * 并返回该软件中断号
         */
        int irq = of_irq_get(dev, index);
        /* 填充resource结构体 */
        if (r && irq) {
                const char *name = NULL;
                memset(r, 0, sizeof(*r));
                of_property_read_string_index(dev, "interrupt-names", index,
                                              &name);
                r->start = r->end = irq;
                /* 将中断触发类型和IORESOURCE_IRQ打包到resource->flags中 */
                r->flags = IORESOURCE_IRQ | irqd_get_trigger_type(irq_get_irq_data(irq));
                r->name = name ? name : of_node_full_name(dev);
        }
        /* 返回软件中断号 */
        return irq;
}
EXPORT_SYMBOL_GPL(of_irq_to_resource);

of_irq_get()函数实现如下:

int of_irq_get(struct device_node *dev, int index)
{
        /* 解析设备树,读取interrupts属性值,对属性值的解析涉及中断子系统,本文不详细讨论
         *      res = of_property_read_u32_index(device, "interrupts",
         *                                       (index * intsize) + i,
         *                                       out_irq->args + i);
         */
        rc = of_irq_parse_one(dev, index, &oirq);

        domain = irq_find_host(oirq.np);
        /* 分配软件中断号,并与硬件中断号建立映射,详细流程涉及中断子系统,本文
         * 不详细讨论
         */
        return irq_create_of_mapping(&oirq);
}
EXPORT_SYMBOL_GPL(of_irq_get);

4.4 使用platform设备资源

4.4.1 使用io resource

获取resource结构体:

/* pdev:struct platform_device
 * IORESOURCE_MEM:memory-mapped IO
 * 0:index,第几个资源
 */
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

如果定义了reg-names属性,也可以使用如下接口函数:

/* res_name:reg-names */
struct resource *res = platform_get_resource_byname(pdev, IORESOURCE_MEM, res_name);

当需要进行读写操作时,需要将res中保存的内存区映射到虚拟地址空间:

void __iomem *virt = ioremap(res->start, resource_size(res));

4.4.2 使用irq resource

获取irq resource与获取io resource的步骤大致相同:

struct resource *res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
或者
/* res_name:interrupt-names
 * res->start:软件中断号,可直接用于request irq
 */
struct resource *res = platform_get_resource_byname(pdev, IORESOURCE_IRQ, res_name);

而后使用request_irq、request_threaded_irq等接口函数注册即可。

五、小结

本文只侧重于设备树中各种资源的解析流程,对涉及到的cpu管理位图、内存管理、gpio子系统、中断子系统等都是浅尝辄止,在后续的文章中,会一一对内核中的这些子系统进行跟读。

发布于 2020-06-05 16:13

原文地址:https://www.cnblogs.com/sky-heaven/p/15941369.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


这篇文章主要介绍“基于nodejs的ssh2怎么实现自动化部署”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“基于nodejs...
本文小编为大家详细介绍“nodejs怎么实现目录不存在自动创建”,内容详细,步骤清晰,细节处理妥当,希望这篇“nodejs怎么实现目录不存在自动创建”文章能帮助大...
这篇“如何把nodejs数据传到前端”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这...
本文小编为大家详细介绍“nodejs如何实现定时删除文件”,内容详细,步骤清晰,细节处理妥当,希望这篇“nodejs如何实现定时删除文件”文章能帮助大家解决疑惑...
这篇文章主要讲解了“nodejs安装模块卡住不动怎么解决”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来...
今天小编给大家分享一下如何检测nodejs有没有安装成功的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文...
本篇内容主要讲解“怎么安装Node.js的旧版本”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎...
这篇“node中的Express框架怎么安装使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家...
这篇文章主要介绍“nodejs如何实现搜索引擎”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“nodejs如何实现搜索引擎...
这篇文章主要介绍“nodejs中间层如何设置”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“nodejs中间层如何设置”文...
这篇文章主要介绍“nodejs多线程怎么实现”,在日常操作中,相信很多人在nodejs多线程怎么实现问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法...
这篇文章主要讲解了“nodejs怎么分布式”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“nodejs怎么分布式”...
本篇内容介绍了“nodejs字符串怎么转换为数组”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情...
这篇文章主要介绍了nodejs如何运行在php服务器的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇nodejs如何运行在php服务器文章都...
本篇内容主要讲解“nodejs单线程如何处理事件”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“nodejs单线程如何...
这篇文章主要介绍“nodejs怎么安装ws模块”,在日常操作中,相信很多人在nodejs怎么安装ws模块问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法...
本篇内容介绍了“怎么打包nodejs代码”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!
本文小编为大家详细介绍“nodejs接收到的汉字乱码怎么解决”,内容详细,步骤清晰,细节处理妥当,希望这篇“nodejs接收到的汉字乱码怎么解决”文章能帮助大家解...
这篇“nodejs怎么同步删除文件”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇...
今天小编给大家分享一下nodejs怎么设置淘宝镜像的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希