
最近在调一块带PMIC和音频Codec的板子被设备树里密密麻麻的reg、interrupts、gpio配置折腾得不轻。核心痛点很清楚一颗芯片同时承担稳压、充电、音频、LED驱动、ADC采样传统做法给每个功能单独建驱动节点结果就是中断资源争抢、寄存器映射重复做、设备树里塞满互不相关的属性。后来把MFD子系统和syscon API吃透之后整个代码结构清爽了很多。这篇文章把这两个东西从原理到实战完整讲一遍适合正在写BSP或者被多功能芯片折磨的嵌入式Linux工程师参考新手也能看懂思路老手可以直接跳去核对API细节和避坑清单。1. 为什么需要MFD子系统一颗芯片引发的代码灾难1.1 没有MFD的时代驱动是怎么“硬写”的先说一个典型场景。某颗电源管理芯片集成了三路DCDC、五路LDO、一个RTC、一个12位ADC、一个看门狗。如果按部就班地给每个功能各写一个platform_driver就会踩进一个很尴尬的坑这些子功能共享同一个物理中断引脚也需要访问同一片寄存器区域。你不可能让五个驱动都去request_threaded_irq同一个中断号也不可能让五个驱动各自ioremap同一段物理内存然后指望它们在寄存器读写上不打架。最常见的“土办法”是做一个超级驱动内部自己发一个子驱动列表然后自己管probe顺序、自己管中断分发。听上去可行但一旦子功能之间有依赖比如ADC需要LDO先上电或者其中一个子设备需要单独热插拔控制这套自研框架很快就失控了。更别提设备树里没法清晰表达“这颗芯片有这些子设备”的层级关系。1.2 MFD的核心设计理念MFDMulti-Function Device子系统就是Linux内核为了解决上述问题而生的一个驱动框架。它的核心思路非常朴素把一颗多功能芯片抽象成一个MFD设备由一个MFD核心驱动负责初始化芯片公共资源寄存器映射、中断、时钟、电源然后利用Linux设备模型把每个功能单元注册成独立的platform_device子设备。每个子设备由各自的驱动负责彼此逻辑解耦互不干扰。这个设计的价值体现在三个层面资源集中管理寄存器映射、中断解析、电源控制都在MFD核心驱动里做一次子设备驱动不需要关心这些公共资源的细节。设备模型树形化设备树里用compatible#address-cells#size-cells的节点关系把一个芯片清晰地表达成一个父节点挂多个子节点的结构。解耦与复用MFD核心驱动只关注“如何把芯片初始化好并拆出子设备”子设备驱动只关注“我这个功能怎么实现”。两边改一边不影响另一边。这套设计让众多PMIC、音频Codec、多功能SoC控制模块比如Intel的IPC设备、各种融合型传感器芯片都能用同一套机制管理子设备内核对驱动编写者提供的口子非常统一。1.3 入口struct mfd_cell与mfd_add_devicesMFD框架里最关键的数据结构是struct mfd_cell。它定义在include/linux/mfd/core.h里描述的是“一个子设备长什么样”。struct mfd_cell { const char *name; int id; void __iomem *resources; int num_resources; void (*probe)(struct platform_device *pdev); void (*remove)(struct platform_device *pdev); ... struct resource *resources; int num_resources; ... };你不需要把子设备驱动的probe函数直接写在这个结构体里。name字段决定了MFD框架注册子设备时使用的platform_device.name之后平台总线会根据这个名字去匹配对应的platform_driver。换句话说MFD框架把mfd_cell里填的子设备名称和实际设备驱动中的driver.name一匹配probe就自动触发。真正把子设备加入内核的API是mfd_add_devices或devm_mfd_add_devicesint devm_mfd_add_devices(struct device *dev, int id, const struct mfd_cell *cells, int n_devs, struct resource *mem_base, int irq_base, struct irq_domain *irq_domain, void *domain_data);devm_版本的好处是如果MFD核心驱动自身探测失败或设备移除内核会自动帮你把子设备全部释放不需要自己做繁琐的清理工作。1.4 设备树中的体现设备树里一颗多功能芯片通常是这样表达的pmic: pmic0x34 { compatible vendor,pmic-model; reg 0x34; #address-cells 1; #size-cells 0; regulator0 { compatible vendor,pmic-dcdc; reg 0; }; adc1 { compatible vendor,pmic-adc; reg 1; }; };MFD核心驱动在probe阶段读到父节点后调用devm_mfd_add_devices为regulator0和adc1分别创建platform_device。子设备驱动只需要关心自己的compatible字符串和reg编号不需要知道父芯片的I2C地址、中断号等全局信息。2. MFD核心数据结构与驱动开发实战2.1 mfd_cell结构体逐字段拆解日常开发中mfd_cell里常填的字段就那几个但用错会出大问题。逐个说一下。name子设备的名称匹配platform_driver.driver.name。注意它是通过platform总线匹配的所以子设备驱动一定是platform_driver不是I2C或SPI驱动。id子设备的实例ID。填PLATFORM_DEVID_AUTO可以让内核自动分配ID避免多个同型号芯片挂在同一总线上时设备名冲突。如果只有一颗芯片可以固定填某个值或者填-1。resources / num_resources子设备需要用到的寄存器res、中断res等。这里会通过platform_get_resource()接口被子设备驱动读取。of_compatible如果子设备想在设备树中有独立节点并通过compatible匹配可以填这个字段配合of_platform_populate把device_node传到子设备。pm_runtime_no_callbacks / pm_runtime_of针对运行时电源管理的一些控制位属于进阶内容常规驱动很少用。请注意mfd_cell里的resources和num_resources是资源信息的来源但很多老派MFD驱动会直接在设备树里给子设备节点分配reg属性两者的解析路径不一样前者走platform_get_resource后者走of_property_read_*platform_get_resource_byname之类。实际项目中我推荐能放在设备树里的就用设备树代码里硬编码resources数组只适合那些节点不方便改动的情况。2.2 MFD核心驱动骨架从probe到子设备注册一个最简单的MFD核心驱动probe函数长这样static const struct mfd_cell pmic_devs[] { { .name pmic-regulator, .resources regulator_resources, .num_resources ARRAY_SIZE(regulator_resources), }, { .name pmic-adc, .resources adc_resources, .num_resources ARRAY_SIZE(adc_resources), }, }; static int pmic_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct pmic_data *pmic; void __iomem *base; int ret; pmic devm_kzalloc(client-dev, sizeof(*pmic), GFP_KERNEL); if (!pmic) return -ENOMEM; /* 初始化公共资源regmap、中断等 */ pmic-regmap devm_regmap_init_i2c(client, pmic_regmap_cfg); if (IS_ERR(pmic-regmap)) return PTR_ERR(pmic-regmap); i2c_set_clientdata(client, pmic); irq_base ...; /* 分配并初始化子中断域 */ ret devm_mfd_add_devices(client-dev, id, pmic_devs, ARRAY_SIZE(pmic_devs), NULL, 0, NULL); if (ret) return ret; return 0; }这段代码揭示了MFD框架的三个关键流程MFD核心驱动先初始化一个regmap这个regmap之后会被子设备驱动通过dev_get_regmap(dev-parent, NULL)拿到。MFD核心驱动处理公共中断并通过irq_domain把子设备需要的虚拟中断号映射好。devm_mfd_add_devices把pmic_devs里的每个cell注册为平台设备之后子设备驱动依次probe。2.3 子设备驱动的编写要点子设备驱动写法就是一个普通platform驱动。比如PMIC内部的ADC子驱动static int pmic_adc_probe(struct platform_device *pdev) { struct regmap *regmap dev_get_regmap(pdev-dev.parent, NULL); struct resource *res platform_get_resource(pdev, IORESOURCE_MEM, 0); struct pmic_adc_data *adc; int ret; if (!regmap) return -ENODEV; adc devm_kzalloc(pdev-dev, sizeof(*adc), GFP_KERNEL); if (!adc) return -ENOMEM; adc-base res-start; platform_set_drvdata(pdev, adc); ret devm_request_irq(pdev-dev, irq, pmic_adc_isr, 0, pmic-adc, adc); if (ret) return ret; return 0; } static struct platform_driver pmic_adc_driver { .probe pmic_adc_probe, .driver { .name pmic-adc, }, }; module_platform_driver(pmic_adc_driver);这里最关键的代码是dev_get_regmap(pdev-dev.parent, NULL)它从父设备MFD核心设备那里拿到了共享regmap。你不应该在子设备驱动里再重新创建regmap那样会重复映射同一片硬件寄存器缓存同步会出大问题。我第一次写子设备驱动时犯过一个低级错误直接在ADC子驱动里用devm_ioremap_resource去映射芯片寄存器结果发现和父驱动里的regmap完全不是一套机制读写互相干扰调试了整整两天才明白需要消除重复映射改用dev_get_regmap从父设备继承寄存器访问能力。3. syscon API系统控制寄存器的统一访问之道3.1 syscon到底解决了什么问题MFD解决了“一颗芯片多个功能”的拆分问题但嵌入式SoC里还有一类很棘手的情况有些寄存器零零散散地位于一个“系统控制模块”里既不属于某个外设又被多个驱动共同需要。举个例子某颗SoC的USB PHY需要校准电阻值这个校准值存在系统控制寄存器Block的偏移0x120处同时以太网MAC想要读取芯片版本号版本号在偏移0x014处。如果让每个驱动各自ioremap同一块物理地址就会遇到两个问题一是映射重复导致TLB浪费二是没有统一缓存机制多个driver读同一寄存器可能出现缓存不一致。sysconSystem Controller就是为了解决这类共享寄存器访问而生的。它本质上是一个极简的MFD driver它做的事情包括将设备树中标为syscon兼容性的节点统一初始化成regmap。把这些regmap注册到内核维护的一个全局链表中后续其他驱动可以用syscon_regmap_lookup_by_*系列函数查到对应的regmap。提供标准的regmap读写接口给使用方缓存和加锁机制由regmap框架搞定。这极大简化了“我要读一个系统控制寄存器”这种需求不再需要自己维护ioremap地址也不用担心多个驱动并发访问同一寄存器的同步问题。3.2 syscon内核实现一个极简驱动的内部逻辑syscon的实现在drivers/mfd/syscon.c核心思想不复杂注册一个platform_driver匹配设备树上的syscon兼容字符串然后在probe函数里读reg属性创建regmap并且把regmap存到哪里呢存到了一个以device_node指针为索引的全局链表里。后续syscon_regmap_lookup_by_*函数就是在链表里根据不同的key查找regmap。注意内核里有两个“syscon”容易混淆drivers/mfd/syscon.c实现的是通用的syscon driver创建regmap并管理全局链表。include/linux/mfd/syscon.h里导出的是给驱动使用的API接口。从使用者的角度我们只关心这几个接口怎么用好。3.3 syscon API使用详解三类查找方法syscon API的核心就是查找regmap重点掌握三个函数struct regmap *syscon_node_to_regmap(struct device_node *np); struct regmap *syscon_regmap_lookup_by_compatible(const char *s); struct regmap *syscon_regmap_lookup_by_phandle(struct device_node *np, const char *property);syscon_node_to_regmap最底层的接口传入一个已经解析出的设备树节点返回这个节点对应的regmap。如果该节点没有被注册为syscon返回ERR_PTR(-ENODEV)。调用前一般需要先of_find_node_by_path或of_parse_phandle拿到节点。syscon_regmap_lookup_by_compatible通过compatible字符串直接查找。比如设备树里有compatible rockchip,grf的节点你在驱动里用这个函数传入相同字符串就能拿到regmap。适合那种一个SoC里只有一个该类型控制模块的场景。syscon_regmap_lookup_by_phandle当你的外设设备树节点中通过phandle属性指向某个系统控制节点时这个方法最自然。避免了硬编码compatible带来的“不知道自己属于哪个实例”的问题。一个常见的使用姿势是在设备树里给外设节点添加一个引用属性usb_phy { compatible vendor,usb-phy; syscon-phyctrl phy_ctrl; ... };然后在驱动里static int usb_phy_probe(struct platform_device *pdev) { struct regmap *regmap; unsigned int val; int ret; regmap syscon_regmap_lookup_by_phandle(pdev-dev.of_node, syscon-phyctrl); if (IS_ERR(regmap)) { dev_err(pdev-dev, failed to get syscon regmap: %ld\n, PTR_ERR(regmap)); return PTR_ERR(regmap); } /* 读取0x120处的校准值 */ ret regmap_read(regmap, 0x120, val); if (ret) return ret; val 0x3f; /* 根据数据手册取对应bit */ ... }3.4 为什么syscon返回的是regmap而不是io地址很多刚接触syscon的朋友会疑惑我直接用of_iomap不是更简单吗确实如果你只有一个驱动访问一段寄存器of_iomap最直接。但一旦涉及多个驱动共享regmap的价值就体现出来了缓存机制regmap可以配置regmap_config.cache_type对于只读寄存器或写1清零寄存器缓存能减少对硬件寄存器频繁访问带来的性能和一致性问题。并发控制regmap默认自带lock可以是spinlock或mutex保证多驱动并发读写时寄存器操作是原子的。总线抽象虽然syscon底层多用regmap_init_mmio但你完全可以写一个基于I2C或SPI的syscon节点这样子系统里的API完全复用。所以我通常建议只要设备树节点声明了syscon并且你确定这块寄存器有多个使用者就只走syscon API访问不要同时用of_iomap映射同一段物理空间否则两个机制各管各的迟早出同步问题。4. MFD与syscon如何协同典型硬件场景实战4.1 一个完整的实战场景PMIC SoC控制块协同我在项目中经常遇到这样的硬件一颗PMIC通过I2C接入SoCPMIC内部集成LDO、ADC和充电逻辑而这些功能的某些使能位却放在SoC的系统控制模块寄存器里。也就是说PMIC子设备驱动要同时操作两条总线一条是PMIC自身的I2C寄存器空间另一条是SoC系统控制模块的寄存器空间。如果不理解MFD和syscon的协作关系代码会写成PMIC MFD核心驱动初始化I2C regmapADC子驱动既访问PMIC内部的regmap又自己ioremap SoC控制模块。这种写法非常危险SoC控制模块那段的访问完全裸奔没有锁、没有缓存机制。正确的做法是把SoC控制模块注册成syscon子驱动通过syscon API访问系统控制寄存器。4.2 设备树编写syscon和MFD节点如何搭配假设SoC控制模块地址是0x10000000大小0x1000设备树里可以这样写soc_ctrl: syscon10000000 { compatible vendor,soc-ctrl, syscon; reg 0x10000000 0x1000; #address-cells 1; #size-cells 1; };注意这里compatible里同时出现了两个值。第一个vendor,soc-ctrl是自定义的专用兼容名第二个syscon是让内核syscon driver来接管这个节点。很多SoC实际设备树里会写成compatible syscon, simple-mfd这种写法适合一块区域既要做系统控制寄存器又要挂一些子功能设备的情况。判断依据是如果这个节点只是纯粹的共享寄存器块就用syscon如果这块区域里还要创建子设备节点就加上simple-mfd。而PMIC的节点是挂在I2C总线下面和syscon节点没有从属关系。需要跨节点引用时通过phandle属性把两边联系起来i2c0 { pmic34 { compatible vendor,pmic; reg 0x34; syscon-enable soc_ctrl; ... }; };4.3 驱动代码里如何协作PMIC MFD核心驱动在probe时通过syscon_regmap_lookup_by_phandle拿到SoC控制模块的regmap把它存到drvdata里。然后创建子设备时把该regmap指针通过platform_set_drvdata或者直接在子设备驱动里再用一次syscon_regmap_lookup_by_phandle获取。两种方式都可行但推荐后者原因是子设备驱动不依赖父驱动的具体数据结构自包含性更强。一个常用写法是在子设备驱动里自己解析phandlestatic int pmic_ldo_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct regmap *soc_regmap; struct regmap *pmic_regmap; unsigned int val; pmic_regmap dev_get_regmap(dev-parent, NULL); if (!pmic_regmap) return -ENODEV; soc_regmap syscon_regmap_lookup_by_phandle(dev-of_node, syscon-enable); if (IS_ERR(soc_regmap)) { dev_err(dev, missing syscon-enable phandle\n); return PTR_ERR(soc_regmap); } if (of_property_read_u32_index(dev-of_node, reg, 0, val)) return -EINVAL; ... }这里子设备通过dev-of_node找到自己对应的设备树节点并解析节点上的syscon-enable属性。属性值指向SoC控制模块从而拿到同一个regmap。整个过程没有跨驱动传递裸指针依赖的是设备树的标准引用机制代码移植性很好。4.4 参数选择背后的逻辑有些开发者会问一个PMIC子设备驱动里已经拿到父驱动的regmap了为什么不能通过在mfd_cell里直接塞一个struct regmap *传给子设备这当然也可以但缺点是mfd_cell的driver_data一旦放了这个指针子设备驱动和父驱动就产生了强耦合。如果SoC上有多颗PMIC每个PMIC子驱动拿到的父regmap不一样依然是强绑定。设备树里通过phandle引用本身就是Linux设备模型的标准做法调试起来更直观。所以在有设备树的系统里我优先推荐所有跨设备共享的寄存器访问都通过设备树phandle属性传递而不是在代码里硬编码或通过mfd_cell传递。5. 常见问题与调试技巧实录5.1 syscon注册失败probe顺序引发的血案工作中遇到过最典型的坑是某个驱动在module_init阶段就调用syscon_regmap_lookup_by_compatible结果返回-EPROBE_DEFER但调用方没有正确处理这个错误码直接把probe判失败了。syscon驱动本身是一个platform_driver它注册完、执行probe需要在内核设备模型初始化之后。如果在arch_initcall或更早阶段就尝试查找syscon那几乎必然失败。正确的做法是所有使用syscon的驱动都应该在probe回调里做查找操作查找失败返回-EPROBE_DEFER时驱动框架会稍后重新probe直到syscon就绪。一个非常容易忽视的问题syscon_regmap_lookup_by_compatible返回的错误码是ERR_PTR类型不是NULL。新人在判断时写if (!regmap)是完全错误的必须用IS_ERR(regmap)。遇到这个问题时系统日志不会有明显错误只会在后续regmap操作时报空指针。5.2 regmap操作与直接ioremap混用导致的寄存器错乱在一开始提到的USB PHY例子里如果a驱动通过syscon regmap读取校准寄存器而b驱动直接ioremap同一块物理内存并写寄存器那么regmap内部缓存的寄存器值可能和硬件真实值不一致造成“明明写进去了过一会儿又变回去”的诡异现象。这个问题不是每次都复现一旦出现极难排查。调试思路是检查代码里对同一物理地址的映射点用/proc/iomem查看是否有多个条目指向同一区域。若有就需要统一走regmap机制把ioremap改成syscon_regmap_lookup_by_* regmap_read/write。5.3 MFD子设备不probe的排查套路我用devm_mfd_add_devices注册子设备后发现子设备驱动完全不probe排查步骤一般是检查/sys/bus/platform/devices下是否有对应名称的设备节点。若没有说明mfd_add_devices没创建成功大概率是mfd_cell.name填错或者资源冲突。检查arch/arm/boot/dts里的节点是否存在以及子设备节点是否被正确解析。用了of_compatible字段时确认设备树节点存在且compatible一致。用dmesg看是否出现Failed to create ...相关日志。有一次排查了很久才发现是mfd_cell的num_resources多填了一项导致资源解析越界子设备注册直接失败。所以填资源时最好用ARRAY_SIZE()宏别手算。5.4 实战调试手段从devmem到trace调试MFD/syscon寄存器问题时我常用的工具按优先级排列devmem快速读一段物理地址。但要注意devmem绕过了regmap缓存只能看硬件真实值不适合验证regmap缓存逻辑。trace_regmap内核有regmap的tracepoint接口打开/sys/kernel/tracing下相关事件可以跟踪每个寄存器的读写路径。perf probe给特定的regmap函数加动态探针比如regmap_update_bits可以打印寄存器地址和值。实际经验告诉我最有效的还是先用设备树把每个节点的reg/compatible核对清楚再上工具。MFD和syscon的问题很大比例出在设备树描述不严谨上而不是代码逻辑本身。5.5 关于冷却与性能的另一个坑syscon regmap默认用的锁是spinlock还是mutex取决于regmap_config的配置。如果你在中断上下文比如ISR里调用regmap_update_bits而regmap配置了mutex锁就会导致睡眠在原子上下文中的内核panic。遇到这种情况要么改用regmap的fast_io标志内部使用spinlock要么把寄存器访问延后到threaded irq或workqueue里。尤其在使用syscon进行中断使能/屏蔽操作时这个坑特别容易踩。我记得有一次一个网卡驱动的ISR里直接通过syscon regmap去清中断标记结果系统启动时偶发死锁查了一整天才发现是锁类型不匹配。如果你也有类似需求记得在创建regmap时设置fast_io true或者干脆把中断处理函数注册成request_threaded_irq。6. 从MFD到syscon升级你的BSP设计思维写到这里我想再聊一点超越API层面的体会。初学嵌入式Linux时很多人包括我自己都习惯于“一个外设一个driver”的线性思维芯片有什么功能就写多少个驱动每个驱动自己ioremap、自己request_irq。但实际上硬件设计远不止这么简单尤其是现代SoC和PMIC跨模块寄存器访问太常见了。MFD和syscon恰好是内核给出的两个标准化解法MFD解决“一个芯片多个功能”的设备拆分问题syscon解决“多个驱动共享一段寄存器”的并发访问问题。把它们结合起来使用的场景几乎覆盖了我工作中八成以上的BSP开发需求。比如高通平台的RPMResource Power Manager、Rockchip的GRFGeneral Register File、各种集成了Audio Codec和PMU的复合芯片基本都能看到MFD driver syscon的组合。理解了这两套机制你再看那些平台厂商的BSP代码会突然觉得非常通透不再是东一榔头西一棒子。一个值得养成的习惯是拿到新板子先花半小时梳理SoC里的系统控制寄存器和多功能芯片分布在设备树里把所有syscon节点和MFD节点规划好。规划阶段多花的时间会在后续每个驱动开发中成倍赚回来。我自己有几次赶工期没有做这一步后面发现一个驱动要访问系统控制寄存器、另一个驱动又要访问再回头改设备树结构反而浪费更多时间。6.1 如果你准备自研一个MFD驱动推荐路径如果你的项目需要自己写MFD驱动我建议按这个顺序逐步验证先写一个只包含mfd_cell和一个空子设备的骨架驱动确保子设备能成功注册。为子设备添加一种资源访问比如从父驱动继承regmap验证数据通路。再添加中断和电源管理相关的复杂逻辑逐步build up。在这个过程中多利用devm_系列接口比如devm_mfd_add_devices、devm_regmap_init_i2c等这样异常路径处理会简单很多不易泄漏。6.2 syscon在设备树中的常见变体最后补充一个设备树中常见的syscon节点变体sysconf100000 { compatible syscon; reg 0x0f100000 0x1000; /* 可以在下面挂一些mfd子设备 */ ipu0 { compatible vendor,ipu; reg 0x0f100000 0x100; }; };注意这种写法下syscon驱动会把整段0x0f100000~0x0f101000都映射成一个regmap而ipu0这个子设备节点不一定会被自动创建为platform设备除非你在syscon驱动里手动调用of_platform_populate或compatible里带了simple-mfd。所以在设备树中看到syscon, simple-mfd时这意味着“这是一个系统控制寄存器块同时又自带一些子设备”。理解这个组合对你读各种厂商设备树源码会很有帮助。我在实际项目中的体会是MFD子系统与syscon API好像是嵌入式Linux驱动开发里最容易被忽视、却最见功底的两个模块。很多老手写外设驱动写得行云流水但一到“多个驱动怎么共享一段寄存器”或“一颗芯片怎么拆成多个子驱动”就含糊。其实这两套机制并不复杂关键是理解内核设备模型是怎么组织硬件资源的MFD是拆分syscon是共享拆分让代码结构清晰共享让数据通路统一。熟练掌握它们你的BSP代码会变得干净、可维护也正是这十几年里我反复使用的两个利器。