十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核片上资源管理:设备树到驱动实战全解析

Linux内核片上资源管理:设备树到驱动实战全解析 从纯软件的世界走进内核最难受的一关往往不是链表、不是调度器、也不是内存管理而是突然面对“一堆寄存器地址、一堆中断号、一堆时钟引脚”时不知道从哪下手。这篇是内核学习系列的第十四篇集中聊片上硬件资源管理。片上硬件资源不是一个独立子系统而是Linux在芯片内部所有硬件资源的描述、抽象、分配与运行时控制的总和。如果你过去写裸机程序习惯了直接往寄存器地址里写值那么切到Linux下会明显不适应地址不能随便访问了中断要申请注册时钟不归你管GPIO也得走一套框架。这篇文章会把资源管理拆成“描述层、抽象层、运行时层”三个层面配合真实驱动写法与常见问题把整条链路串起来。内容比较适合已经看完进程、内存、中断入门正准备动手写外设驱动的读者。1. 片上硬件资源管理到底在管什么1.1 资源清单Linux眼中的硬件世界片上硬件资源说穿了就是一颗SoC内部所有外设需要的东西。我习惯把资源先列一份清单再去对照内核代码思路会清爽很多。寄存器空间外设的控制寄存器、状态寄存器、FIFO等对应物理地址范围也就是MMIOMemory Mapped IO。中断外设产生事件后通知CPU的信号线对应中断号、触发方式、是否共享。时钟外设工作所需的时钟源、频率、门控开关。复位与电源域外设需要处于供电状态、复位释放状态才能正常工作。GPIO与引脚复用引脚既可以当GPIO也可以复用为UART、I2C、PWM等外设功能需要配置引脚mux和电气属性。DMA通道外设搬运数据时使用的DMA请求与通道。电源调节器regulator电压域比如vdd-supply、vcc-supply等。特殊资源GPIO中断、硬件定时器、PWM通道、ADC通道等。这些资源不是孤立的。一个最简单的UART工作至少需要寄存器地址、中断号、时钟、引脚复用、电源域同时到位。缺一个整个设备就只能在dmesg里留下一行错误然后默默退出probe。1.2 内核为什么非要“多管闲事”很多从裸机转过来的朋友第一个疑问是我直接在驱动里ioremap一下然后写寄存器不就行了为什么要过设备树、过各种framework这个问题问得好答案在于资源冲突和资源生命周期。裸机程序里整个芯片都是你一个人的你可以随意开关时钟、随意改引脚。但Linux里跑着几十个驱动任何资源都可能被多个模块共享。比如一个时钟源同时供给两个外设其中一个驱动天真地把它关了另一个外设立刻罢工。中断也一样有些中断线是多设备共享的你一个人独占处理其他设备的事件就丢了。内核还面临另外一个问题资源的休眠与唤醒。驱动在运行时可能进入suspend如果直接操作寄存器没有经过电源管理框架设备可能在休眠时被断电醒来后寄存器全部丢失系统直接挂掉。所以内核的每一个资源框架本质上都在做三件事安全访问、冲突仲裁、生命周期管理。多管闲事是为了让所有驱动在同一套规则下协作。1.3 三层模型描述层、抽象层、运行时层我建议初学的人不要一头扎进某个子系统源码而是先建立整体模型。片上硬件资源管理在内核里可以分成三个层面层面载体核心作用描述层设备树DT、ACPI把硬件有哪些资源、资源在哪里用标准化格式告诉内核抽象层driver model、platform bus、各类framework把物理资源抽象成设备、总线、驱动让驱动不关心具体平台运行时层ioremap、interrupt、clk、gpio、pinctrl、dmaengine、regulator等在驱动运行过程中申请、使用、释放资源描述层回答“有什么”抽象层回答“怎么挂”运行时层回答“怎么用”。你可以把设备树理解成一张硬件配置表内核启动时解析它生成一个个platform_device然后和驱动匹配。匹配成功后驱动通过一串API向运行时层申请资源。我见过很多人学内核外设驱动直接从网上下一个杂碎驱动代码开抄结果换个板子就废。原因就是没有搞懂三层模型。资源在哪描述、由谁解析、在哪一步变成驱动能用的数据结构这三件事理清了看任何驱动都像是在看同一套模板。2. 设备树给硬件发一张资源清单2.1 从板级文件到设备树的演进在设备树普及之前ARM Linux的内核里塞满了各种板级文件比如arch/arm/mach-xxx/xx-board.c。每出一个新板子就要改内核源码重新编译。后来社区实在受不了这种“一人一板”的开发方式引入设备树机制。设备树本质上是一种描述硬件的数据结构它不包含任何逻辑只有节点、属性和值。内核启动时由bootloader将设备树二进制dtb传入内核解析后动态生成平台设备。这样换板子只需要换dtb不用重新编译内核。很多老工程师对设备树很反感觉得又是“换汤不换药”。但在我实际做项目过程中设备树最大的好处是解耦芯片厂商提供dtsi描述SoC内部外设板卡厂商在dts里描述板级差异两者互不干扰。这也是为什么今天几乎所有主流架构都在往这个方向走。2.2 节点逐行拆解我以一个i2c挂载的温度传感器节点为例逐行拆给你看i2c0 { sensor48 { compatible vendor,temp-sensor; reg 0x48; interrupt-parent gpio1; interrupts 5 IRQ_TYPE_LEVEL_LOW; clocks clk_i2c 0; vdd-supply reg_3v3; reset-gpios gpio4 12 GPIO_ACTIVE_LOW; status okay; }; };compatible这是设备树里最重要的属性用于驱动和设备节点匹配。命名规范是“厂商,型号”内核通过它找到对应的platform_driver。reg在i2c总线子节点下这个值表示从设备地址。如果节点挂在根总线下reg一般表示寄存器物理地址和长度。interrupt-parent和interrupts描述中断资源。interrupt-parent指明中断控制器interrupts里第一个值是中断号第二个是触发类型。clocks描述该设备用到的时钟。clk_i2c 0表示phandle到clk_i2c这个时钟控制器并取它的第0路输出。vdd-supply电源资源指向一个regulator节点。reset-gpiosGPIO资源包括引脚控制器、引脚编号和有效电平。注意设备树节点本身不负责“如何使用”这些资源它只是一份清单。这份清单会被内核解析成struct resource、struct clk、struct gpio_desc等内核资源对象。2.3 驱动侧读取资源的几种常用姿势节点写好了驱动侧怎么拿最核心的是platform_get_resource家族static int sensor_probe(struct platform_device *pdev) { struct resource *res; struct device *dev pdev-dev; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; /* 推荐用devm_ioremap_resource它会在失败时打印详细错误 */ void __iomem *base devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base); /* 获取中断 */ int irq platform_get_irq(pdev, 0); if (irq 0) return irq; /* 读取普通属性 */ u32 threshold; ret device_property_read_u32(dev, vendor,threshold, threshold); if (ret) return ret; return 0; }devm_ioremap_resource是带设备资源管理的版本probe失败或驱动卸载时自动释放不用手动iounmap。device_property_read_u32这类API兼容设备树和ACPI两种硬件描述方式写驱动时优先使用。这里有一个很多人踩过的坑platform_get_resource拿到的地址是物理地址必须经过ioremap之后才能访问。如果你直接往这个地址写数据系统要么段错误要么直接卡死。在内核里物理地址不能凭空解引用必须建立页表映射。3. 资源运行时管理六大核心框架逐个过3.1 寄存器映射ioremap与regmap的正确用法ioremap是驱动访问寄存器的基础操作它把物理地址映射到内核虚拟地址空间。传统方式是request_mem_region加ioremap再在remove里release_mem_region和iounmap。现在更多推荐devm_ioremap_resource它会自动做资源冲突检测和生命周期管理。为什么需要先request_mem_region内核里每个物理地址段都应该有主避免两个驱动同时操作同一个区域。如果你不申请就直接ioremap硬件层面上可能偶尔能工作但这种驱动属于“野驱动”在真实产品里迟早出事。如果你的外设寄存器访问比较规整建议再往上走一层使用regmap框架。regmap把寄存器访问封装成统一的读写接口支持缓存、总线适配I2C、SPI、MMIO都可、多寄存器原子操作。比如你操作一个PMIC的寄存器只需要struct regmap *map devm_regmap_init_i2c(client, cfg); regmap_write(map, 0x10, 0x3f); regmap_read(map, 0x10, val); regmap_update_bits(map, 0x10, BIT(3), BIT(3));regmap最大的好处是抽象了底层访问方式I2C设备和MMIO设备在驱动代码里看起来几乎一样。而且调试方便regmap的debugfs接口能直接导出寄存器内容。3.2 中断不是每个回调都能随便睡中断资源管理大概是新手最容易翻车的地方。申请中断的核心API是request_irq或它带设备管理的版本devm_request_irqret devm_request_irq(dev, irq, sensor_isr, IRQF_TRIGGER_LOW | IRQF_SHARED, my_sensor, priv); if (ret) return ret;中断处理函数里有两类大忌。第一不能调用可能导致睡眠的函数比如kmalloc(GFP_KERNEL)、mutex_lock、msleep。因为中断上下文不是进程上下文没有进程调度可言。第二中断处理要快不要在ISR里做冗长计算耗时的东西交给中断线程化或workqueue。遇到耗时处理优先考虑request_threaded_irq或者devm_request_threaded_irq。它把处理拆成快速处理函数和内核线程处理函数前者处理紧急的寄存器操作后者处理不紧急甚至需要睡眠的逻辑。现在的内核主线里很多驱动都不再写裸ISR了直接开线程化中断省心很多。共享中断也是一个常见坑。多个设备共用一条中断线时申请时必须带IRQF_SHARED而且中断处理函数里必须第一件事检查自己设备的中断状态寄存器如果发现不是自己的中断直接返回IRQ_NONE。3.3 时钟外设的“电源开关”没那么简单很多做应用开发的人不理解为什么内核里clk API这么重要。实际上SoC上的很多外设没有时钟就是一块死硅寄存器和数据通路都不工作。时钟API的基本姿势是struct clk *clk; clk devm_clk_get(dev, spi); if (IS_ERR(clk)) return PTR_ERR(clk); ret clk_prepare_enable(clk); if (ret) return ret;注意clk_prepare_enable是两个操作合体clk_prepare可能睡眠clk_enable必须原子。如果你在原子上下文只能调用clk_enable但前提是clk已经prepare过了。所以才有devm_clk_get配合clk_prepare_enable这套组合。如果一个设备用多个时钟可以用clk_bulk接口一次性申请和使能static const char * const clk_names[] { ahb, apb }; struct clk_bulk_data clks[2]; for (int i 0; i 2; i) clks[i].id clk_names[i]; ret devm_clk_bulk_get(dev, 2, clks); ret clk_bulk_prepare_enable(2, clks);设备树里clocks clk_ahb, clk_apb驱动里用devm_clk_get时按索引或clock-names匹配。配了clock-names的节点可读性更高clocks clk_ahb, clk_apb; clock-names ahb, apb;一个真实教训I2C控制器在时钟没使能时读取寄存器读回来的值全是0xff很多新人会以为是设备没连接排查半天最后发现是clk没开或clk频率不对。时钟框架调试时记得看/sys/kernel/debug/clk/clk_summary它会列出每个时钟的开关状态、父时钟和当前频率排查看一眼就懂。3.4 GPIO与引脚控制从pinctrl到gpiodGPIO资源管理分两个层面pin control和GPIO API。pinctrl子系统负责引脚复用、上下拉、驱动强度等硬件配置GPIO子系统负责读值和输出。引脚复用的状态在设备树里这样描述pinctrl-names default, sleep; pinctrl-0 uart0_pins_default; pinctrl-1 uart0_pins_sleep;驱动侧不用手动配置引脚pinctrl子系统会在设备匹配后自动应用“default”状态在系统suspend时应用“sleep”状态。你可以在驱动里手动切换pinctrl状态devm_pinctrl_get(pdev-dev); pinctrl_select_state(pinctrl, pinctrl_sleep);GPIO操作新代码强烈推荐gpiod接口而不是老的gpio_ API。gpiod接口默认使用设备树中的命名struct gpio_desc *reset_gpio; reset_gpio devm_gpiod_get(dev, reset, GPIOD_OUT_LOW); if (IS_ERR(reset_gpio)) return PTR_ERR(reset_gpio); gpiod_set_value(reset_gpio, 1); gpiod_set_value(reset_gpio, 0);设备树里对应的属性是reset-gpios gpio4 12 GPIO_ACTIVE_LOW;这里有个容易搞混的点在设备树资源管理里GPIO编号不是全局的必须使用gpio控制器 引脚号 标志这种形式。GPIOD_OUT_LOW表示申请时默认输出低电平。有效电平由设备树里的GPIO_ACTIVE_LOW决定驱动里设置1时实际上物理引脚输出0。gpiod接口会帮你自动处理有效电平写驱动时不要自己去翻转逻辑否则低电平有效引脚会出反逻辑的bug这种bug在硬件电路上极难排查。3.5 DMA与电源管理性能与功耗的两端DMA资源管理相对复杂基本流程是struct dma_chan *chan; chan dma_request_chan(dev, rx); if (IS_ERR(chan)) return PTR_ERR(chan); struct dma_slave_config cfg { .direction DMA_DEV_TO_MEM, .src_addr (dma_addr_t)res-start, .src_addr_width DMA_SLAVE_BUSWIDTH_4_BYTES, .src_maxburst 16, }; dmaengine_slave_config(chan, cfg); struct dma_async_tx_descriptor *desc; desc dmaengine_prep_slave_single(chan, buf_dma_addr, len, DMA_DEV_TO_MEM, 0); dmaengine_submit(desc); dma_async_issue_pending(chan);DMA资源里最坑的是DMA映射。日常驱动里DMA缓冲区必须是cache一致性安全的简单场景用dma_alloc_coherent即一致性DMA映射性能和便利性兼顾dma_addr_t phys; void *cpu_addr; cpu_addr dma_alloc_coherent(dev, size, phys, GFP_KERNEL);电源管理这一块对外设驱动最重要的接口是runtime PMpm_runtime_enable(dev); pm_runtime_get_sync(dev); /* 使用完设备后 */ pm_runtime_put_sync(dev);pm_runtime_get_sync会保证设备处于活动状态如果硬件支持它还会自动开时钟、调regulator、恢复上下文。实现runtime PM的驱动一般在runtime_suspend回调里关时钟、关电源在runtime_resume回调里恢复寄存器配置。这是资源管理在功耗维度的最终形态。4. 实操手写一个platform驱动把资源串起来4.1 先搭实验环境纸上谈兵没用建议用QEMU虚拟机跑一个ARM环境比如qemu-system-arm模拟vexpress-a9或virt平台自己编一个内核然后在里面加载模块。选择QEMU的原因是不用买开发板改设备树、改驱动、调试都能在宿主机上完成。实验环境需要几步下载内核源码配置一个支持devicetree的platform比如vexpress_defconfig或multi_v7_defconfig。安装交叉编译工具链arm-linux-gnueabi-或arm-linux-gnueabihf-。制作initramfs内置一个最小busybox。写一个虚拟设备节点编译成模块在QEMU里insmod。如果你更喜欢在真机上实验也可以找一块廉价的ARM开发板但流程会更长。QEMU的好处是随便造不怕把板子弄坏。4.2 设备端写一个能描述资源的dts节点我习惯把一个完整的实验设备节点放在根节点下面避免依赖具体总线/ { mydev: mydev40000000 { compatible demo,mydev; reg 0x40000000 0x1000; interrupts 33 0; /* 注意这里依赖interrupt-parent */ interrupt-parent gic; clocks clk_24m; clock-names core; status okay; }; };这个节点描述了一个位于0x40000000、长度0x1000的寄存器块一个中断号33以及一个24MHz时钟。因为平台不同你需要根据QEMU的设备和中断控制器结构调整interrupt-parent和interrupts值。设备树写完后用dtc编译成dtb在启动QEMU时指定qemu-system-arm -M vexpress-a9 -kernel zImage -dtb myboard.dtb \ -initrd rootfs.img -nographic -append consolettyAMA0启动后在/sys/firmware/devicetree/base目录下你就能看到mydev节点。4.3 驱动端probe里如何一步步拿资源现在写一个platform_driver把资源一步步取出来#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/interrupt.h #include linux/clk.h #include linux/io.h static irqreturn_t mydev_isr(int irq, void *data) { /* 读中断状态寄存器确认设备确实产生中断 */ struct mydev_priv *priv data; u32 status readl(priv-base 0x00); if (!(status BIT(0))) return IRQ_NONE; writel(status, priv-base 0x00); /* 清中断 */ return IRQ_HANDLED; } static int mydev_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct mydev_priv *priv; struct resource *res; int irq, ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; platform_set_drvdata(pdev, priv); /* 1. 寄存器资源 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); priv-base devm_ioremap_resource(dev, res); if (IS_ERR(priv-base)) return PTR_ERR(priv-base); /* 2. 中断资源 */ irq platform_get_irq(pdev, 0); if (irq 0) return irq; ret devm_request_irq(dev, irq, mydev_isr, 0, mydev, priv); if (ret) return ret; /* 3. 时钟资源 */ priv-clk devm_clk_get(dev, core); if (IS_ERR(priv-clk)) return PTR_ERR(priv-clk); ret clk_prepare_enable(priv-clk); if (ret) return ret; dev_info(dev, mydev probed at %pR, irq %d\n, res, irq); return 0; } static int mydev_remove(struct platform_device *pdev) { struct mydev_priv *priv platform_get_drvdata(pdev); clk_disable_unprepare(priv-clk); return 0; } static const struct of_device_id mydev_of_match[] { { .compatible demo,mydev }, { } }; MODULE_DEVICE_TABLE(of, mydev_of_match); static struct platform_driver mydev_driver { .probe mydev_probe, .remove mydev_remove, .driver { .name mydev, .of_match_table mydev_of_match, }, }; module_platform_driver(mydev_driver); MODULE_LICENSE(GPL);这段驱动把寄存器、中断、时钟三类资源完整走了一遍。devm_系列函数的便利之处在remove函数里体现得最明显我们只手动关了时钟寄存器映射、中断释放都由设备资源管理自动回收。4.4 编译加载与验证编译模块时使用内核源码树的Makefile机制make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- \ M/path/to/mydev modules生成mydev.ko后放进initramfs或者通过NFS挂载加载。在目标系统里insmod mydev.ko dmesg | tail如果一切正常你会看到类似输出mydev 40000000.mydev: mydev probed at 40000000-40000fff, irq 33还可以通过/sys/bus/platform/devices/看到设备与驱动绑定关系ls /sys/bus/platform/devices/ cat /sys/bus/platform/devices/40000000.mydev/uevent如果想验证中断路径可以在驱动里主动写一个触发寄存器然后观察统计数据。这一步做完你对“资源怎么从设备树流到驱动”的整个链路就会非常清晰了。5. 常见问题与排查技巧实录5.1 PCIe桥BAR空间不足一次典型的资源分配失败这个报错在x86和ARM服务器上很常见“内核无法给PCIe桥接器分配足够的内存映射空间”也就是BAR空间分配失败。它的本质是PCIe总线地址空间资源分配冲突。PCIe桥在枚举时会根据下游设备需要的BAR大小把上游窗口划分给下游总线。如果一个PCIe桥的prefetchable或non-prefetchable窗口不够大或者桥后面的设备BAR太大就会出现分配失败。排查时我一般按顺序做四件事lspci -vvv查看各设备BAR需求。cat /proc/iomem看系统已分配的PCIe MMIO窗口。dmesg | grep -i pci查看枚举阶段的报错信息。尝试在启动参数加pcirealloc让内核重新分配BAR资源。pcirealloc是一个很有效的临时方案它允许内核重新安排桥的窗口但根本原因往往是BIOS/固件预设的资源窗口与设备实际需求不匹配或者上游桥的窗口太小。这个问题在ARM服务器上尤其明显因为很多固件对PCIe资源预留不够合理。如果你的系统支持ACPI还可以用_CRS方法观察固件上报的PCIe资源。嵌入式平台如果外设BAR特别大还需要检查CPU地址空间里分配给PCIe控制器的地址范围是否足够。5.2 ioremap失败与地址冲突devm_ioremap_resource失败时通常会在dmesg里打印资源冲突信息。最常见的两种情况物理地址超出系统地址空间范围或该地址段已被其他驱动占用。排查手段如下cat /proc/iomem | grep 40000如果这段地址显示已被占用比如标记为reserved或归属某个驱动而你的驱动又是一个独立外设那就是设备树里reg写错了或者两个节点地址重叠。还有一种是64位平台的高地址空间需要确认驱动使用的地址是物理地址还是总线地址。如果是总线地址和CPU物理地址不一样比如某些PCIe设备、某些带地址翻译的桥要检查设备的ranges属性不能直接拿设备树里的reg当物理地址用。5.3 中断申请失败request_irq返回-EINVAL或-ENOMEM时先检查这几个点中断号是否合法。设备树里interrupts的值要基于对应中断控制器的#interrupt-cells定义。如果中断控制器是GIC通常#interrupt-cells为3驱动里获取时还需要额外解析。写错中断号经常申请失败。共享中断是否带IRQF_SHARED。如果中断线已经被别的设备独占你再申请共享中断就会失败。中断标志是否匹配。比如设备硬件是低电平触发你在设备树里写了IRQ_TYPE_EDGE_RISING有时候不会立刻报错但中断永远不触发或者触发风暴。排查时看/proc/interrupts文件里面列出了每个中断号和已注册的处理函数。你的设备中断如果没有出现在里面说明申请环节已经出问题。5.4 时钟与GPIO获取失败时钟获取失败通常表现为devm_clk_get返回-ENOENT或-EINVAL。大概率是设备树里clocks的phandle写错或者clock-names与驱动里传入的名字不一致。调试时先看cat /sys/kernel/debug/clk/clk_summary | grep 24m确认这个时钟存在再检查设备的clocks属性是否正确。很多时候是dts里少写了clock-names或者驱动里用名称匹配而设备树只用了索引。GPIO获取失败的典型错误是devm_gpiod_get返回-EPROBE_DEFER。原因可能是引脚的GPIO控制器驱动还没加载完成。-EPROBE_DEFER不是真正的错误而是一种“再试一次”的信号驱动框架会在依赖资源就绪后重新调用probe。所以遇到这个返回值不要直接return -EINVAL直接返回-EPROBE_DEFER就好内核会把这个设备挂到延迟探测队列里。另一个GPIO坑是pinctrl占用冲突。如果某个引脚已经在pinctrl里被复用成I2C功能你又通过gpiod去申请它就会得到-EBUSY。这种问题只能回头改设备树的pinctrl状态把引脚配置改成GPIO模式。5.5 DMA传输异常的排查思路DMA相关的bug是资源管理里最折磨人的一类。如果DMA传输一直没有完成或者数据全乱优先级最高的怀疑对象是cache一致性问题。使用流式DMA映射dma_map_single时记得在数据传输完成后调用dma_unmap_singleCPU访问buffer前还要调用dma_sync_single_for_cpu。如果忘记做cache同步CPU和DMA看到的可能是同一块内存的不同副本。还有一个常见问题DMA缓冲区没有对齐。有些DMA控制器要求buffer地址按32字节或64字节对齐长度也要是burst size的倍数。出现随机卡死时先用一个简单、对齐的静态缓冲区测试排除buffer问题。方向参数也经常写反。DMA_DEV_TO_MEM是外设到内存DMA_MEM_TO_DEV是内存到外设搞反了不会报错但数据一动不动。另外DMA描述符用完之后要dmaengine_desc_free或复用否则踩内存是早晚的事。6. 继续深入的学习路线与工具建议6.1 用VS Code高效看内核源码内核源码体量巨大靠vim加grep硬翻效率太低。我现在的习惯是用VS Code打开内核源码目录配一个clangd或ctags索引快速跳转函数定义。推荐做三步配置安装clangd插件用bear或compile_commands.json生成编译数据库让跳转准确识别平台相关的条件编译代码。常用快捷键记牢跳转定义、查找所有引用、全局符号搜索。如果编译数据库不好生成退而求其次用ctags生成索引在.vscode里配置command搜索ctags。看源码不要摊大饼。带着问题看比如“clk_prepare_enable到底做了什么”从include/linux/clk.h头文件入手一路追到clk.c和具体厂商驱动。一个问题追完再看下一个。6.2 从一个小驱动“解剖”一个子系统我自己的学习方法是选一个完整的小驱动然后从probe函数往外延伸。比如看GPIO子系统可以先看drivers/gpio/gpiolib.c再选一个简单的gpio控制器驱动比如gpio-74x164这种整个代码量不大可以把资源申请、中断映射、pinctrl交互看得很透彻。看中断子系统时重点是irq domain。设备树里interrupt-parent绑定到中断控制器这个控制器驱动会创建irq domain硬件中断号才能翻译成内核IRQ号。不懂irq domain你会纳闷设备树里写的是5为什么/proc/interrupts里显示的是29。这一块是片上资源管理里最绕但也最值得啃的知识点。6.3 把resource管理当成系统设计来学学习资源管理真正学到的其实不是某一段API而是一套系统设计方法先定义资源描述格式再建立抽象层屏蔽差异最后用生命周期管理保证安全。这套思路放之四海而皆准不管是嵌入式Linux、RTOS还是用户态框架都能看到影子。根据自己的经验学习过程别贪多一次只深挖一类资源。今天把clk框架吃透明天再看DMA比泛泛浏览所有子系统效率高得多。每次遇到诡异的问题先记录现象再用工具缩小范围最后形成自己的排查清单。这套方法用熟了你面对任何新芯片、新外设都会有一种“不过如此”的底气。
返回列表