拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Linux驱动开发核心:drivers/base映射机制与调试实战

干嵌入式Linux驱动这行时间不短了每年带新人时我几乎都会说同一句话内核源码那么大drivers目录底下光文件就几千个真想入门驱动开发先别急着看某个具体芯片的驱动而是把drivers/base这个目录啃明白。它是整个设备模型的心脏也是所有“设备-驱动-总线”映射关系的源头。今天这篇学习笔记就用一次实际项目的复盘视角把drivers/base底下和“map”相关的几条主线梳理一遍——设备是怎么匹配到驱动的寄存器物理地址是怎么映射进内核虚拟地址空间的设备树里的中断号又是怎么换算成Linux中断号的顺便把我自己踩过的几个坑也一并交代清楚。我默认读这篇文章的朋友已经会用menuconfig、会编译内核、会写最简单的module_init。如果你刚摸到Linux驱动开发的门边这篇文章可以帮你把很多“半懂不懂”的代码拼图补完整如果你已经写了一阵子驱动那至少能从这里找到几个值得回查的细节。话不多说从地基开始。1. 先把地基打牢drivers/base下的“映射”到底指什么1.1 drivers/base目录里都放着哪些关键内容很多新人点开drivers/base目录会懵因为这里面几乎没有一个“看起来像设备驱动”的文件。它的核心文件我简单列一下core.c: 管理struct device、struct device_driver、struct bus_type这些基础对象的创建和生命周期。bus.c: 实现总线的注册逻辑。设备挂到总线上、驱动挂到总线上真正“配对”的时候总线回调match函数。dd.c: 这是设备与驱动绑定的核心really_probe就是从这里把probe一路执行起来的。platform.c: platform总线以及platform_get_resource、platform_match这些我们天天用的接口实现就在这里。regmap/: 一个独立的子目录提供寄存器映射框架后面专门讲。power/、firmware/、driver.c等各管一摊和本文主题相关度不高先不展开。你可以把drivers/base理解成一套“婚姻中介”设备device是找对象的征婚人驱动driver是另一边的征婚人总线bus_type就是撮合双方红娘。dd.c负责把双方拉到一起履行流程probe成功就相当于领证。1.2 实际开发中你会碰到的几种“map”我平时把驱动开发中的“映射”分成三类也对应本文后面的章节设备与驱动的匹配映射。设备树里一个节点凭什么找到对应的struct device_driver这就是匹配映射的职责。物理地址到虚拟地址的映射。外设寄存器挂在某个物理地址上CPU不能直接按物理地址访问需要先把物理地址映射到内核虚拟地址空间再通过映射后的地址读写。中断号映射。设备树里写的interrupts 0 42 4其实不是Linux最终使用的中断号中间还隔着一层irq domain的换算。另外还有一类和“map”名字直接挂钩的框架regmapregister map。它在drivers/base下面占了独立目录是专门用来统一寄存器访问的抽象层很多老驱动不用它新驱动却很常见。这四种“映射”放在一起看基本就能覆盖一个普通platform设备驱动从匹配到初始化的完整路径。2. 设备与驱动匹配映射platform_match是怎么“对号入座”的2.1 platform_match的执行顺序和优先级在嵌入式Linux里最常用的是platform总线。你写一个挂在内存映射外设上的驱动这个设备节点几乎总是platform设备。总线上的match函数就落在drivers/base/platform.c的platform_matchstatic int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev to_platform_device(dev); struct platform_driver *pdrv to_platform_driver(drv); /* 1. driver_override强制指定 */ if (pdev-driver_override) return !strcmp(pdev-driver_override, drv-name); /* 2. OF style match设备树匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 3. ACPI style match */ if (acpi_driver_match_device(dev, drv)) return 1; /* 4. id_table 匹配 */ if (pdrv-id_table) return platform_match_id(pdrv-id_table, pdev) ! NULL; /* 5. 名字兜底匹配 */ return (strcmp(pdev-name, drv-name) 0); }这段代码的优先级在面试里经常被问到实际排错时也特别有用。我的理解是内核给了不同时代的设备“两条腿走路”的兼容方案。老式平台设备没有设备树靠platform_device.name和platform_driver.driver.name硬匹配新式设备走设备树靠compatible属性匹配driver_override则是一种“强制包办”不管你是谁我指定谁就是谁。顺序上有一点值得注意设备树匹配的优先级高于id_tableid_table高于name兜底。我见过有人总以为platform_driver里只写driver.name就能匹配设备树节点其实设备树节点上如果没有compatible能对上的驱动仅靠name兜底非常容易出问题。2.2 compatible和设备树of_device_id的匹配写法设备树里最典型的写法是这样/ { #address-cells 1; #size-cells 1; mydevicef0000000 { compatible vendor,mydevice; reg 0xf0000000 0x1000; interrupts 0 42 4; }; };驱动的of_device_id表则这样写static const struct of_device_id mydevice_of_match[] { { .compatible vendor,mydevice }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydevice_of_match); static struct platform_driver mydevice_driver { .probe mydevice_probe, .remove mydevice_remove, .driver { .name mydevice, .of_match_table mydevice_of_match, }, }; module_platform_driver(mydevice_driver);这里有一个非常重要的习惯MODULE_DEVICE_TABLE(of, ...)一定要写。它的作用不仅仅是生成模块别名还可能影响设备节点自动匹配。如果驱动编译成模块时不写这一行modprobe可能无法根据设备树节点的modalias自动加载这个模块然后你会看到设备树里节点明明存在/sys/bus/platform/devices/下设备也有但就是没有对应的驱动去probe。这个问题排查起来很费时间我就栽过一次。compatible的命名规范是“厂商前缀逗号设备型号”。不要小看这个逗号的位置。写错一个字母、把逗号忘掉都会导致匹配失败。比如vendor,mydevice和vendor,mydevice 结尾多一个空格内核不会帮你trim它比对的就是字节。这类问题是驱动“死活不probe”的头号原因。2.3 driver_override和id_table几个容易被忽略的匹配入口driver_override机制在platform_match里排在第一位。它的典型使用场景是为了做兼容或者测试需要强制某个平台设备和某个驱动绑定。你可以通过sysfs操作echo mydevice /sys/bus/platform/devices/f0000000.mydevice/driver_override echo f0000000.mydevice /sys/bus/platform/drivers/mydevice/bindid_table则是传统C-Style平台设备遗留的匹配方式。在老内核里很多平台设备没有设备树直接在板级文件里定义struct platform_device然后给一个name字段。驱动侧则通过platform_driver.id_table来匹配这是从Linux 2.6时代延续下来的兼容路径。现在新开发基本都是设备树路线了但看老代码时还会遇到它。2.4 匹配映射失败的三个典型现场我总结了三个最常见的匹配失败现场compatible拼写不一致。设备树里写vender,mydevice驱动里写vendor,mydevice一个字母之差系统启动后设备就是挂着不probe。排查方法读/sys/bus/platform/devices/f0000000.mydevice/uevent看MODALIAS字段再和驱动源码里的表比对。of_match_table没赋值。.driver里只写了.name没有给.of_match_table赋值在设备树系统上基本无法匹配。module_platform_driver没有调用。或者手动写了module_init但注册的不是同一个platform_driver导致驱动表根本没进内核。这些坑都有一个共同特点内核不会报错只会在dmesg里静默地找不到匹配。如果你在dmesg里看到类似“no match for driver”的提示或者干脆什么都没有就按上面几个点逐一查。3. 地址映射ioremap和devm_ioremap_resource的正确打开方式3.1 为什么不能直接访问物理地址现代CPU有MMU内核看到的是虚拟地址空间不是物理地址。kernel在启动时会设置页表把内核镜像所在的物理内存映射到直接映射区但这不代表外设寄存器也自动映射好了。你可以这么理解物理内存像学校图书馆里的一排排书架虚拟地址是我借书时拿到的索引号。我自己宿舍里的书架可以随时看但图书馆的书架必须经过“登记”才能访问。外设寄存器就相当于图书馆深处那些不常开放的书架内核默认不会把它们放进可访问的页表里必须用ioremap这类接口主动建立映射关系。所以在设备驱动里直接对一个物理地址解引用来做寄存器读写是很危险的。它会触发一个page fault运气好就是oops运气不好可能在别的错误上下文里悄悄破坏页表。3.2 设备树reg属性到resource再到虚拟地址的链路一个完整的platform驱动开头通常长这样struct mydevice_priv { void __iomem *base; struct regmap *regmap; int irq; struct clk *clk; }; static int mydevice_probe(struct platform_device *pdev) { struct mydevice_priv *priv; struct resource *res; void __iomem *base; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; /* 1. 取出reg属性对应的resource */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -EINVAL; /* 2. 请求并映射物理地址 */ base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); priv-base base; ... platform_set_drvdata(pdev, priv); return 0; }链路的完整逻辑是设备树里的reg 0xf0000000 0x1000会被内核解析成一个struct resource类型是IORESOURCE_MEM起始地址0xf0000000长度0x1000。platform_get_resource(pdev, IORESOURCE_MEM, 0)就是把这个resource取出来。0表示取第一段MEM资源。devm_ioremap_resource分两步走先调用devm_request_mem_region向内核申请这段IO资源避免和其他驱动冲突再调用devm_ioremap做真正的页表映射。有个细节要提醒devm_ioremap_resource返回的是__iomem指针返回值必须用IS_ERR判断。我见过有人拿NULL判断这在devm_ioremap时代还能碰巧对但devm_ioremap_resource失败时返回的是ERR_PTR(-EBUSY)这类错误指针不是NULL。不判断IS_ERR直接往下走后面readl一次就崩。3.3 devm和裸ioremap应该怎么选裸的ioremap和devm_ioremap区别在于资源管理。ioremap映射后必须手动调iounmap否则probe失败或者remove时就会泄漏映射。而devm_系列接口注册了devres回调在设备生命周期结束时自动释放。我个人的习惯是优先用devm系列。尤其是现代内核里驱动可以只写probe和remove中的注销逻辑甚至有些驱动都不需要remove资源全交给devm管理。这点比老式驱动要省心太多。不过devm不是万能的。如果某个映射不是绑定在struct device的生命周期上而是需要一个独立的生命周期那就要考虑管理它是不是更合适。另外映射大块物理内存比如DMA buffer时ioremap只是映射配合remap_pfn_range之类才是正确姿势。3.4 访问IO内存的注意点readl/writel和__iomem映射完地址后最忌讳的就是拿普通指针解引用方式去读写寄存器// 错误示例 u32 val *(u32 *)priv-base; // 正确示例 u32 val readl(priv-base); writel(val | BIT(0), priv-base 0x10);原因有两个层面。第一外设寄存器很多是volatile的用普通*访问编译器可能优化掉或者缓存到CPU寄存器里。第二有些架构下IO内存和普通内存有不同的缓存属性约定readl/writel这些接口会确保按访问顺序、按正确宽度进行。__iomem标志是用来给sparse静态检查工具看的它提醒代码作者和reviewer这个指针不是普通内存不能用memcpy、不能直接解引用。arm64上ioremap映射出来通常是Device-nGnRnE属性CPU访问时不会经过cache保证每次读写都落在真实寄存器上。这也是为什么不能用普通memcpy来拷贝一段寄存器内容——你很可能读到的是缓存行里的旧值而不是硬件当前的值。4. 寄存器映射框架regmap把复杂总线操作藏起来4.1 regmap解决了什么问题在讲regmap之前先回忆一下以前写驱动的痛点。很多芯片的寄存器访问分两种路径一类挂在内存映射总线上用readl/writel访问一类挂在I2C/SPI总线上用i2c_smbus_read_byte_data、spi_write_then_read访问。同一个芯片如果同时支持I2C和SPI接口它的驱动就得写两套寄存器读写代码。这还只是麻烦的起点。更麻烦的是很多芯片的寄存器没有全部暴露在硬件寄存器位里而是需要软件维护一个缓存做一些读改写、write-only bit的处理。每个驱动都自己写一套代码重复、易错、难调试。regmap框架就是为了解决这些问题的。它用一份统一的API来访问寄存器底层无论走MMIO、I2C还是SPI上层代码不用改。devm_regmap_init_mmio就是regmap和本文前面讲到的地址映射结合点。4.2 用regmap重写一个简单设备驱动拿前面那个mydevice来说如果芯片寄存器不多使用regmap之后probe会变成这样static const struct regmap_config mydevice_regmap_cfg { .reg_bits 32, .val_bits 32, .reg_stride 4, .max_register 0x1000 / 4, .cache_type REGCACHE_RBTREE, .volatile_reg mydevice_volatile_reg, }; static int mydevice_probe(struct platform_device *pdev) { struct mydevice_priv *priv; struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); priv-regmap devm_regmap_init_mmio(pdev-dev, base, mydevice_regmap_cfg); if (IS_ERR(priv-regmap)) return PTR_ERR(priv-regmap); /* 后续访问寄存器 */ regmap_read(priv-regmap, 0x00, val); regmap_update_bits(priv-regmap, 0x10, BIT(1), BIT(1)); ... }regmap把“物理地址映射”和“寄存器语义访问”拆成了两层。前者由devm_ioremap_resource完成后者由regmap_read/write/update_bits完成。对应用层来说读改写一个寄存器只需要一个函数调用不用再手动处理读、改、写三步。使用regmap时我还要强调两点max_register要算准。它表示寄存器偏移上限单位取决于reg_bits和reg_stride。比如寄存器宽度4字节0x1000字节地址空间那么最大寄存器序号大约是0x400。算错会导致框架拒绝越界访问也能帮你提前发现设备树reg长度不对的问题。volatile_reg和cache_type要配合使用。对于每次都要读到真实硬件状态的寄存器比如中断状态寄存器必须在volatile_reg里返回true否则regmap的缓存机制可能让你读到旧值。4.3 regmap调试debugfs里直接看寄存器regmap很大的一个爽点在于调试。内核编译时开启CONFIG_DEBUG_FS然后你可以直接在debugfs里读取某个设备的寄存器现场cat /sys/kernel/debug/regmap/mydevice/registers输出里每一行就是寄存器和当前值。这个信息量比用devmem读要直观得多因为你不用自己去翻译设备树里的物理地址。/sys/kernel/debug/regmap/目录下每个regmap实例会有一个子目录里面还有name、range、access等文件。做新板子bring-up时我经常先挂上驱动然后直接看registers确认硬件复位值是不是符合预期这比隔着I2C总线去猜要高效很多。5. 中断号映射从设备树interrupt到Linux IRQ number5.1 platform_get_irq和irq_of_parse_and_map的区别设备树里描述中断时interrupts 0 42 4里的42并不一定是最终Linux中断号。它是“中断控制器视角”的中断编号要经过irq domain映射之后才能得到linux内部使用的全局IRQ number。在platform驱动里一般直接用platform_get_irqint irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(pdev-dev, failed to get irq: %d\n, irq); return irq; }而irq_of_parse_and_map是“从设备节点属性解析并映射”的底层接口platform_get_irq内部优先走设备树路径本质上会调用它。区别在于platform_get_irq把资源查询、错误处理都封装好了对驱动作者更友好。有一个容易踩的坑新版内核里platform_get_irq返回0也被视为错误。因为0号中断在多数架构上被保留用作无效中断所以代码里判断应该是if (irq 0)而不要写成if (irq 0)后返回错误也不要只判断irq 0。5.2 中断映射表怎么看/proc/interrupts系统运行起来后你可以通过/proc/interrupts查看每个中断号的映射情况cat /proc/interrupts这个文件的每一行左边是IRQ number中间是各CPU上的触发次数右边是设备和中断控制器名称。如果你的驱动注册了中断但计数没有增长说明中断根本没有触发如果计数在疯狂增长但driver没有反应可能是中断触发方式不对或者中断号不对或者request_irq时的flags和硬件实际信号不匹配。实际排查时我还会用/proc/irq/irq/目录下的文件。比如smp_affinity可以调整中断绑在哪个CPU上spurious记录了这个中断上发生的异常次数。中断风暴问题靠这几个文件能定位出大概方向。5.3 中断映射失败的排查经验写设备树时中断相关属性最容易出错我遇到过这样几类interrupt-parent没写或写错。如果设备挂在中断控制器下但父节点指定错了解析出来的中断号会莫名其妙。interrupt-cells个数不匹配。比如GIC的#interrupt-cells 3如果写成两个cell解析会失败。触发方式不匹配。设备树里写IRQ_TYPE_LEVEL_HIGH但request_irq时用了IRQF_TRIGGER_RISING或者反过来结果就是要么不触发要么触发风暴。我特别想强调一个容易被忽视的经验设备树里interrupts前的一个cell在GIC上表示SPI还是PPI不要随便写。很多人抄模板时没注意这个值导致中断号被解析到另一个CPU私有中断上然后只在某个CPU上生效或者干脆不生效。遇到这个问题时我通常会先回到设备树源文件确认#interrupt-cells的定义再手算一遍它应该落在哪个中断号范围再和/proc/interrupts对照。6. 系统起来后怎么反查各种映射关系6.1 /sys/bus/platform下的设备与驱动对应关系当你怀疑驱动是否匹配上时/sys/bus/platform/是你最好的朋友。它下面有devices和drivers两个主要目录ls /sys/bus/platform/devices/ ls /sys/bus/platform/drivers/mydevice/如果设备存在但/sys/bus/platform/devices/f0000000.mydevice/driver符号链接不存在说明设备没有绑定到驱动也就是匹配失败。如果符号链接存在再看符号链接指向的驱动目录里的driver是不是这个驱动。另外uevent文件里的MODALIAS内容也非常关键cat /sys/bus/platform/devices/f0000000.mydevice/ueventMODALIAS会输出类似of:NmydeviceTNULLCvendor,mydevice的字符串它的含义是“设备树节点名mydevicecompatible为vendor,mydevice”。你拿这个字符串去搜索内核里的模块别名是否被正确生成能排查MODULE_DEVICE_TABLE的问题。6.2 /proc/iomem看地址资源占用/proc/iomem展示的是IO资源树。驱动成功request_mem_region之后你的设备资源就会出现在这里并且名字对应你在驱动里设置的名字cat /proc/iomem | grep -i mydevice # f0000000-f0000fff : mydevice如果devm_ioremap_resource失败最常见的原因就是这一小段IO资源已经被别的驱动占用了。这个时候/proc/iomem里能看到资源被谁持有。有时候设备树里两个节点reg写重叠了而你从头到尾没有怀疑过资源冲突一查/proc/iomem立刻现形。顺带提一个/dev/mem相关的坑不要在没搞清楚地址映射的情况下用devmem到处写。devmem直接操作物理地址绕过内核的资源管理很容易把其他驱动正在用的寄存器改乱并且它看不到__iomem映射后的语义。应急排查可以用但不要把设备树里reg写错这件事指望靠devmem来绕过。6.3 devices_deferred和EPROBE_DEFER现代内核里probe会返回各种错误码其中一个很常见又容易被忽略的是-EPROBE_DEFER。意思是“本设备还没有准备好等依赖资源就绪后再试一次”。驱动可能在probe里请求时钟、请求GPIO、请求regulator时这些依赖还没有ready于是返回EPROBE_DEFER。内核会把这些被延迟probe的设备记录下来cat /sys/kernel/debug/devices_deferred输出类似f0000000.mydevice waiting for supplier 1000.clock这个文件是解决“驱动注册了但probe没跑”问题的关键。很多新人看到dmesg没有任何错误就以为驱动没编译进去结果一查devices_deferred发现是时钟依赖没就绪。遇到这种情况正确做法不是删掉-EPROBE_DEFER而是去看依赖的provider为什么没就绪。7. 常见问题排查实录和写在最后的调试心得7.1 驱动注册了却不probe从头到尾怎么查这个问题出现的频率极高我给它总结了一个排查顺序确认驱动有没有进内核。lsmod | grep mydevice或者cat /proc/modules看模块有没有被加载如果编译进内核dmesg里会有mydevice: driver initialized之类日志。确认设备有没有注册。ls /sys/bus/platform/devices/ | grep mydevice。确认设备和驱动是否绑定。ls -l /sys/bus/platform/devices/f0000000.mydevice/里有没有driver符号链接。确认有没有被defer。查/sys/kernel/debug/devices_deferred。确认匹配表内容。把/sys/bus/platform/devices/f0000000.mydevice/uevent的MODALIAS和驱动里的of_device_id逐个字符比对注意结尾空格、逗号。确认probe里没有中途return。如果probe被调用了但很快失败dmesg里会有probe of ... failed with error ...。我见过很多次设备是i2c设备却以为自己写的是platform驱动然后在/sys/bus/platform下死活找不到。所以第一步先要搞清楚你要匹配的设备到底挂在哪条总线上/sys/bus/下面看着点别只看platform。7.2 一访问寄存器就oops多半是这三类问题驱动在readl或者writel时直接oops我几乎每次都能从下面三类里找到原因映射地址有问题。devm_ioremap_resource返回了错误指针但没判断或者platform_get_resource返回NULL但没检查整个地址就是错的。这类oops通常在页面错误信息里能看到PC指针是一个荒谬的地址。偏移越界。base offset里的offset超出了reg声明的长度。比如reg 0xf0000000 0x1000但你访问base 0x2000这已经超过映射窗口一旦触发页错误就oops。把IO内存当普通内存访问。前面反复强调的memcpy、strcpy、直接解引用等操作。不管是不是真的会触发oops这种代码在架构相关的行为上都是未定义迟早会翻车。一个提高排查效率的实用经验打开内核编译时的CONFIG_DEBUG_SPINLOCK、CONFIG_DEBUG_MUTEXES不一定能帮你查oops但打开CONFIG_DEBUG_PAGEALLOC和CONFIG_USER_MODE_LINUX在某种程度上有助于暴露越界访问路径。更好用的其实还是sparse工具编译时加上C1就能扫描出很多__iomem标注错误make C1 drivers/misc/mydevice/7.3 中断申请失败看中断号和触发方式request_irq失败时/proc/interrupts里不一定会有线索但dmesg会告诉你失败原因。常见几种-EINVAL中断号无效。多半是platform_get_irq返回了负数或者中断号被解析成了0。-EBUSY中断号已经被其他驱动占用。查/proc/interrupts找是谁占的很多情况下是因为两个设备在设备树里写了同样的interrupts或者某个驱动用irq方式误注册了request_irq。申请成功但永远不触发对触发方式不敏感。这时候我会直接改request_threaded_irq调试或者临时用cat /proc/interrupts观察计数是否有变化配合irqtop看哪个中断在活跃。在调试中断时我还习惯在probe里加一个临时探针比如注册完中断后用gpio_set_value或者写一个调试寄存器人为制造一次现场信号用来验证硬件链路和软件链路是否都通了。很多时候问题不在驱动而是外设根本没有发出正确的中断电平或边沿。写在最后的调试心得前面这些内容如果只看一遍你可能还是记不住。我自己的体会是drivers/base这一套“映射”逻辑一定要配合实际的板子跑一遍才有体感。你可以在任何一块带设备树的开发板上做个小实验写一个最简单的platform驱动只做三件事——取MEM资源、ioremap、读一次寄存器打印出来然后试着改掉设备树里的compatible、改掉reg的偏移、故意漏写MODULE_DEVICE_TABLE再观察/sys/bus/platform和/proc/iomem的变化。这几步做完你对设备模型的理解会比读十篇博客都深。最后再分享一个小技巧每次写完驱动先不要急着上应用层先打开debugfs和动态调试把dev_dbg打开用echo file drivers/misc/mydevice.c p /sys/kernel/debug/dynamic_debug/control这种手段看probe流程打印。再加上前面提到的/proc/iomem、/proc/interrupts、/sys/bus/platform这几个“反查入口”90%的映射问题都能当场定位。这篇笔记其实就是我这些年在drivers/base和“map”这两个关键词上踩坑、翻源码、调板子的一个缩影希望能给你省下一些摸索时间。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门