轉自: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, ®); size = dt_mem_next_cell(dt_root_size_cells, ®); /* 將該塊內存加入內存子系統進行管理 */ 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 = ¶ms[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子系統、中斷子系統等都是淺嘗輒止,在后續的文章中,會一一對內核中的這些子系統進行跟讀。