资讯动态

Linux内核power_supply框架解析:MTK充电驱动核心机制

发布时间:2026/10/3 15:26:13 来源:尧图企业网站定制
2. 温度与充电电量的联动逻辑1. 从整体视角看power_supply_core.c老规矩先聊清楚这个文件在整个充电链路里的位置再往下抠代码才有意义。Linux内核里的电源管理并不是每个驱动各玩各的而是有一个统一的抽象层这个抽象层的核心实现就是drivers/power_supply/power_supply_core.c。不管是高通、展锐还是MTK平台最终都要向内核注册一个或多个power supply设备让上层Android框架、用户态服务能以统一的方式读取电池电量、充电状态、电压电流、温度这些信息。我在接触MTK平台之前一直觉得这个文件就是个简单的“注册管理”真正干活的是底层的charger驱动和gauge驱动。后来调了几个月的充电问题才明白power_supply_core.c是整个电源子系统的“信息汇聚中心”所有关于电池和充电器的数据都要经过这里分发到用户态同时用户态的设置请求比如写充电电流上限也要从这里下发给底层驱动。可以说它是充电驱动和上层应用之间的数据总线。MTK的充电方案硬件上有自己的PMIC比如MT6357、MT6359、独立的充电IC如MT6370、BQ25601这类外置charger、电量计gauge等。软件层面MTK在标准power supply框架之上又封装了一层自己的charger核心逻辑比如mtk_charger.c、mtk_battery.c等。但不管外面包了多少层最终对外暴露给内核和用户态的仍然是标准的power_supply_class接口也就是power_supply_core.c这套东西。所以这篇的分析重点就很明确了把power_supply_core.c的内部机制搞透再看MTK是怎么把这个标准框架“玩出花”来的。你会发现理解了core.c你才能真正理解MTK的battery驱动为什么那样写也才能在遇到“电池信息不更新”“充电状态上报错误”“sysfs节点读不到”这类问题时快速定位到根因。这篇适合正在做BSP充电驱动的朋友、想搞明白Android电池信息从哪来的内核开发者以及被充电问题折磨的项目软件工程师。2. 核心数据结构与设计思路2.1 power_supply是一个怎样的“对象”在Linux内核里任何一个电源设备最终都可以抽象成一个struct power_supply。这个结构体定义在include/linux/power_supply.h里。里面最关键的是两个东西一个是描述电源设备静态信息的struct power_supply_desc另一个是设备运行时的状态struct power_supply_config。拿MTK的充电场景举例一颗外置charger芯片比如BQ25601它的驱动要做的事情就是填充一个power_supply_desc描述这个charger有哪些属性比如在线状态、输入电流限制、充电状态然后调用power_supply_register把它注册进内核的电源管理总线。注册完成之后/sys/class/power_supply/下面就会出现一个对应的目录比如bq25601_charger。这里有几个结构体字段我挑重要的说struct power_supply_desc { const char *name; // 电源设备名字如 battery、mtk-master-charger enum power_supply_type type; // 类型电池、USB、AC、无线充等 enum power_supply_property *properties; // 支持的所有属性列表 size_t num_properties; // 属性数量 int (*get_property)(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val); int (*set_property)(struct power_supply *psy, enum power_supply_property psp, const union power_supply_propval *val); int (*property_is_writeable)(struct power_supply *psy, enum power_supply_property psp); };name字段决定了你在sysfs里看到的目录名type决定了这个设备在Android里会被归类为电池还是充电器。属性列表是你告诉power supply核心层“我能提供哪些数据”比如POWER_SUPPLY_PROP_STATUS充电状态、POWER_SUPPLY_PROP_CAPACITY剩余电量、POWER_SUPPLY_PROP_TEMP电池温度。核心层拿到这些信息后会自动为每个属性生成对应的sysfs属性文件你注册了哪个属性sysfs里就会出现对应的节点。这个设计非常巧妙驱动只需要声明自己有什么剩下的事情core.c帮你搞定不需要每个驱动都去自己创建一堆kobject和attribute。2.2 为什么内核要再做一层“属性映射”很多刚入行的朋友会有疑问驱动里我直接写sysfs属性不行吗为什么非要通过get_property回调绕一圈原因是统一接口。如果每个驱动都自己实现attribute那么用户态程序就得针对每个驱动写不同的读取逻辑。比如一个电池驱动叫mtk_battery另一个叫bq20z75_battery如果它们暴露的节点名称不一样上层代码就没法写通用逻辑。power_supply_core.c把属性名和属性ID做了标准映射内核定义了一套枚举enum power_supply_property把“电压”“电流”“电量”“温度”这些通用属性统一编号。驱动只需实现这些属性ID对应的读函数core.c负责把sysfs里的字符串操作转换成枚举ID调用再统一格式化成字符串返回。这种设计在Linux内核里其实到处都有比如regmap、pinctrl都是类似的思路把底层硬件的多样性收敛成一套标准的接口让上层的逻辑保持稳定。MTK的充电驱动再怎么折腾到了power supply这一层最终仍然要遵守这套规则。2.3 MTK的扩展一张desc唱不完的戏MTK平台的充电驱动往往一个板子上存在多个电源相关设备主charger、从charger、无线充电、电池电量计、USB接口的供电状态等。每一个都是一个独立的power_supply设备都有自己的desc、name和属性集。比较典型的注册场景在mtk_battery.c里电池设备会注册为类似static enum power_supply_property mtk_battery_props[] { POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_CAPACITY, POWER_SUPPLY_PROP_VOLTAGE_NOW, POWER_SUPPLY_PROP_CURRENT_NOW, POWER_SUPPLY_PROP_TEMP, POWER_SUPPLY_PROP_TECHNOLOGY, ... };而charger驱动比如mtk_charger.c或者外置charger芯片的驱动注册的是另一组属性比如POWER_SUPPLY_PROP_ONLINE充电器是否插入、POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT充电电流控制等。Android框架层的HealthInfo服务会去读“电池”设备上的标准属性电量、电压、温度、状态同时会去看“充电器/USB”设备上的ONLINE属性判断充电器是否插入。如果你在MTK平台上自定义了一个新的充电协议IC没有正确注册成power_supply设备上层就永远看不到它的状态。3. 深入注册链路与回调机制3.1 power_supply_register的完整流程MTK的驱动调用power_supply_register时core.c内部做的事情远不止创建目录那么简单。我按照代码执行顺序梳理一下完整流程分配power_supply结构体内存关联desc和config。根据name字段检查是否已存在同名设备防止冲突。初始化内部用到的spinlock、workqueue重点是初始化psy_uevent_work这个工作项它是后续发送uevent事件的“发令枪”。创建一个struct device把它挂到power_supply_class类下面。这里用到了Linux设备模型所以后面sysfs的路径才会是/sys/class/power_supply/xxx。遍历desc里的属性列表在设备目录下逐个创建attribute文件。如果config里指定了supplied_to和num_supplicants则建立本设备与消费者设备之间的供电关系。这个关系会被用于执行power_supply_changed通知。最后把power_supply加入全局链表暴露给整个内核访问。这里有一个MTK平台上很容易踩的坑name冲突。MTK的充电方案里既可能有mtk-master-charger又可能有mtk-slave-charger甚至外置charger驱动里也注册了一个同名的charger设备。如果内核开启了CONFIG_POWER_SUPPLY_DEBUG你会看到类似power_supply_register: name already exists的报错而这次注册会被静默失败或者返回错误。我在实际项目中遇到过同一块主板上既有MT6370的驱动又添加了一颗外置的SW6306充电IC两边都想注册mtk-master-charger这个名字结果后注册的一方直接把系统搞到启动崩溃。排查半天最后发现是名字冲突。3.2 get_property回调是在什么上下文里执行的理解了注册流程接下来的重点就是回调机制。驱动最多只实现get_property和set_property两个函数指针power supply核心就能完成所有属性的读写。但这里有个关键问题这两个回调函数被调用的时机是什么上下文是什么常见调用时机包括用户态cat /sys/class/power_supply/battery/capacity时Android的HealthInfo服务周期性轮询电池节点时内核其他模块通过power_supply_get_property主动查询时power_supply_changed触发uevent事件用户态收到事件后回头读属性时。这些调用可能发生在进程上下文用户态read也可能发生在中断上下文或原子上下文某些内核路径调用。所以在实现get_property时千万不要在里面做耗时操作或睡眠操作否则你会亲手制造一个内核栈崩溃。MTK平台上我见过最典型的错误就是在get_property里直接调用I2C读取电量计芯片的寄存器并且没有任何超时保护。当电量计芯片总线异常挂起时整个系统的属性读取全部卡死进而导致系统重启。正确做法是底层驱动维护一份“软件缓存”用单独的工作队列定期更新缓存数据get_property只负责把缓存值返回。这也是为什么MTK的battery驱动里总有大量看门狗式的更新线程本质就是为了把耗时的硬件读取和快速的属性查询解耦。3.3 set_property与属性可写控制有读就有写。set_property接口用于用户态下发配置最常见的场景是设置充电电流限制。比如fast charger在协商成功之后用户态或内核上层需要把最大充电电流写到charger驱动里这时候走的就是set_property。但并不是所有属性都可写驱动必须通过property_is_writeable回调明确告诉core.c哪些属性允许写。如果不实现这个回调sysfs里生成的属性文件就是只读的即使你实现了set_property用户态写入时也会报权限错误。我在调试协议充电时经常用到这个机制临时在sysfs里调整充电电流验证硬件稳定性。做法就是确保对应的charger驱动实现了POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX这个属性的写权限然后echo 2000000 /sys/class/power_supply/battery/constant_charge_current_max注意MTK平台某些分支上这种直接写入可能不会生效因为上层还有mtk_charger的策略管理在拦截它会按需覆盖你的设置。这时需要确认你是写到了“底层charger”设备还是写到了“策略管理器”设备两者完全是两码事。4. sysfs与uevent数据的对外出口4.1 sysfs属性文件的生成规则power_supply_core.c里专门有一套sysfs的映射逻辑核心是一个静态表把每个enum power_supply_property映射为对应的属性名、格式和读函数。简单来说属性IDsysfs节点名说明POWER_SUPPLY_PROP_STATUSstatus充电状态Unknown/Charging/Discharging/Not charging/FullPOWER_SUPPLY_PROP_CAPACITYcapacity电量百分比一般0~100POWER_SUPPLY_PROP_VOLTAGE_NOWvoltage_now当前电压微伏uVPOWER_SUPPLY_PROP_CURRENT_NOWcurrent_now当前电流微安uAPOWER_SUPPLY_PROP_TEMPtemp电池温度十分之一摄氏度POWER_SUPPLY_PROP_ONLINEonline是否在线充电器是否插入POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAXconstant_charge_current_max最大恒流充电电流你注册了哪些属性sysfs目录下就会出现哪些节点不存在“默认全部生成”的情况。所以如果某个节点找不到很可能是驱动注册时漏了对应的属性枚举。调试时最常用的命令是ls -l /sys/class/power_supply/battery/ cat /sys/class/power_supply/battery/ueventuevent文件会把所有属性一次性全部打出来格式是属性名值。内核就是通过拼装这个字符串来发送uevent的所以你手动cat它做的事情和内核发送uevent时做的事情基本一致。4.2 power_supply_changed事件是如何广播的这是power_supply_core.c里最重要的一个对外接口Android的电池电量更新全靠它。当驱动检测到电池电压变化、电量变化、充电器插拔等情况时应该主动调用power_supply_changed让系统知道“电源状态变了”。这个函数的处理流程并不复杂但细节值得说清楚原子操作设置一个标志位标记该设备需要更新。调度psy_uevent_work到系统工作队列。工作队列执行时调用kobject_uevent发送change事件。同时调用power_supply_update_gen_leds更新LED状态并把变化同步到所有消费者supplicants。发送uevent时内核会发送一个NETLINK_KOBJECT_UEVENT消息给用户态。Android的HealthInfo服务正是监听这个netlink消息收到事件后重新读取所有电池属性更新状态栏的电池图标和锁屏电量显示。这里有个MTK平台常见的坑不要过于频繁地调用power_supply_changed。有些驱动在电压采集线程里每秒循环上报造成uevent事件风暴导致系统负载升高甚至引起上层服务ANR。MTK的battery驱动一般会做防抖处理设置最小上报间隔。底层charger驱动如果有自己的状态机也应该判断状态确实发生变化之后才触发上报。4.3 uevent里带不带属性值这是个问题内核的uevent事件本身只包含ACTIONchange和DEVPATH/devices/.../power_supply/battery这类信息属性值并非随事件一起发送。用户态收到事件后必须主动read属性文件拿到具体数值。但某些厂商的定制内核里会在uevent环境变量里附带POWER_SUPPLY_CAPACITY87之类的键值对这样可以减少用户态回读的次数。MTK的某些分支上也做过类似优化。如果你在做MTK平台的系统开发想要调试“为什么电量更新慢”第一步就要确认事件有没有发出来去内核日志里搜power_supply battery的uevent打印或者用以下命令监听adb shell # 监听内核uevent netlink需要root cat /proc/kmsg # 触发一次插拔充电器观察是否有对应事件打印如果没有事件产生那问题一定出在内核侧如果有事件但界面不更新问题就出在上层框架。5. MTK平台实战注册正确的psy设备5.1 如何让上层识别你的充电器MTK平台在BringUp阶段经常需要外挂一颗新的charger芯片。如果这颗芯片不通过power_supply框架注册Android会一直认为“充电器未插入”不管VBUS有没有电压。要让系统识别至少要做到这几点第一注册一个charger类型的power_supply设备desc-type设为POWER_SUPPLY_TYPE_USB或者根据协议设为POWER_SUPPLY_TYPE_USB_PD。第二实现POWER_SUPPLY_PROP_ONLINE属性的读取。上层就是靠这个值判断充电器是否在线。第三如果支持快充最好同时提供POWER_SUPPLY_PROP_USB_TYPE属性并实现usb_type的枚举映射。Android 10以上的框架对USB类型非常依赖比如判断是SDP、CDP、DCP还是PD。我给一颗SW6306写适配驱动时就是按这个套路来的注册完并实现online属性后插上充电器系统立刻识别为USB充电日志里也能看到uevent: POWER_SUPPLY_ONLINE1。5.2 电池设备应该从哪里读数据MTK平台的电池设备不一定只有一个。板子上有独立的gauge电量计也有PMIC内置的ADC通道做电压温度采集。大多数方案里电池的power_supply设备由battery驱动注册它的get_property读取的是gauge驱动的“软件缓存”。问题是gauge芯片如MT6357内置gauge、BQ27546、CW2217等的上报方式五花八门。有的通过I2C连续读取才能得到完整电量有的内部自带电量算法引擎通电几秒就能出真实百分比。这些差异最终都要被battery驱动屏蔽掉对外统一成一个干净的POWER_SUPPLY_PROP_CAPACITY。MTK的做法通常是在battery驱动里单独开一个adc/电量计轮询线程然后维护一份mtk_battery内部的状态结构体当用户态读取capacity时直接返回这个结构体里缓存的百分比。所以你在调MTK平台电量不准的时候第一步要看的是那个轮询线程的打印而不是直接看sysfs返回值。因为sysfs读到的是缓存缓存更新频率决定数据的实时性。我用一个表格把这几个设备在MTK平台上的典型分工列清楚设备名注册模块典型属性作用batterymtk_battery.ccapacity、status、temp、voltage_now对上层暴露电池状态mtk-master-chargermtk_charger.conline、charge_type、input_current_limit主充电器状态mtk-slave-chargermtk_charger.conline、charge_type从充电器/无线充状态gauge如mtk_gaugemtk_gauge_drv.cvoltage_now、current_now、capacity提供电池原始采样数据5.3 避免走入“越级”定制的误区很多工程师拿到MTK充电需求第一反应就是直接修改battery驱动的get_property把客户要求的自定义逻辑塞进去。比如客户要求在电量低于5%时上报的电压值要加一个偏移。这种做法短期看起来能交付但长期维护会非常痛苦因为mtk_battery的代码在每次MTK版本升级时都会大改你手工补丁很容易被覆盖。更合理的做法是充分利用power_supply框架的特性自定义属性、自定义设备、自定义下发通道。比如客户要一个“充电完成指示”完全没有必要去改POWER_SUPPLY_PROP_STATUS的语义而是在驱动里新增一个POWER_SUPPLY_PROP_xxx属性或者直接通过set_property实现控制逻辑。这样即使MTK原厂代码升级你的定制也被限定在新增代码文件里冲突面最小。这部分也是我从几次升级中血换来的经验。不要图省事直接改原厂代码宁可多花一点时间在框架之内做扩展也不要制造一个“升级即爆炸”的定时炸弹。6. 常见问题排查与调试技巧6.1 频繁上报导致系统卡顿怎么定位现象插上充电器后手机整体变卡top能看到system_server CPU占用很高主线程频繁唤醒。排查路径第一步确认uevent是否过度触发。用busybox top -H -p看系统进程里的kworker是否有大量唤醒。或者加打印统计power_supply_changed的调用次数。第二步找到谁在频繁调用。在power_supply_changed里临时加日志static void power_supply_changed(struct power_supply *psy) { dev_dbg(psy-dev, %s: explicitly update\n, __func__); ... }打开CONFIG_POWER_SUPPLY_DEBUG日志里会打出每次触发的设备和调用栈。第三步分析调用频率和触发源。MTK平台上最常见的是charger驱动的监测线程写得太激进每100ms就查一次VBUS状态状态稍微抖动就触发上报。解决方法是在驱动里做“变化检测”和“最小上报间隔”双重过滤比如300ms内的重复状态变化合并为一次上报。6.2 sysfs节点读取后阻塞怎么办现象cat /sys/class/power_supply/battery/capacity卡住不动或者返回很慢。这个问题的根源几乎都在get_property回调里做了I2C等慢速操作。解决思路是把硬件读取逻辑迁移到独立线程驱动初始化时创建一个kthread或delayed_work定期读取gauge和charger寄存器将读取结果保存在驱动私有结构体里get_property只做内存拷贝和单位换算。调完之后读取速度会从几十毫秒降到微秒级系统整体响应也顺畅很多。6.3 上层看到的状态和串口不一致现象串口日志里打印电流是2Asysfs里读到current_now却是500mA或者第一时间读不到最新值。原因通常是缓存更新和用户态读取之间存在时间差。MTK的battery驱动更新电量计的周期往往是10秒甚至更长而用户态查询可能是毫秒级。你这边刚看到2A充电下一秒底层刚好更新缓存导致读出来是历史值。排查方法对比mtk_battery的轮询线程日志和sysfs实际值确认缓存更新的时间戳。如果确实是更新周期太长调整电池电量计的轮询间隔如果是多设备同时上报导致属性被覆盖就要检查到底哪个设备注册了current_now以及框架层最终读的是哪个设备。6.4 调试命令速查表命令用途ls /sys/class/power_supply/查看平台注册了哪些电源设备cat /sys/class/power_supply/battery/uevent一次性查看电池全部属性cat /sys/class/power_supply/mtk-master-charger/charge_type查看当前充电模式echo 2 /sys/class/power_supply/battery/constant_charge_current_max设置充电电流视驱动支持而定dmesg | grep power_supply查看电源子系统的日志busybox ps -w | grep kworker查看是否有异常高频工作线程我这里特意强调的是“优先看uevent”因为uevent文件相当于把驱动注册的所有属性以及当前值一次列出来对比排错效率远比一个个cat高。配合dmesg | grep power_supply基本能覆盖大部分问题。7. 内核日志与寄存器级联调做充电驱动光会看sysfs还不够很多时候得直接看底层寄存器状态。以MT6370为例它的充电状态寄存器比如REG_CORE_STATUS反映的是硬件当前实际在做什么——是预充、恒流、恒压还是已经截止。而系统上层读到的status字段是驱动根据寄存器值翻译过来的。出现“软件状态和硬件状态不一致”时就得直接读寄存器验证。MTK平台一般可以通过adb shell下的寄存器读写工具操作比如# 在MTK平台常见用io工具直接访问I2C设备节点 io -4 -r 0x10000000当然更通用的方法是驱动里临时加打印通过I2C读取寄存器的原始值并输出。我在调试充电慢问题时就遇到过status显示正在充电但实际寄存器里CHG_EN位早就被关掉了的情况。这种问题如果不看寄存器根本找不到方向。经验总结下来就是用户态看uevent和sysfs驱动层看函数调用流硬件层看寄存器原始值三层对照才能精准定位问题。8. 写在后面的个人体会MTK平台充电驱动牵扯的东西非常多从PMIC到charger芯片从gauge到battery驱动每一层都有自己的状态机和策略。但只要你把power_supply_core.c这条主线抓牢就能在上层和底层之间建立起清晰的坐标系上层的Android框架只认标准属性下层的硬件驱动负责把数据填进标准属性里。这套抽象的价值我是在对比了多家平台的实现之后才真正体会到的。虽然代码写法风格各异但根基都是Linux内核这套power supply框架。把根基啃透不仅仅是MTK将来做高通、展讯平台也能快速上手。最后留下一个建议别急着改代码先在板子上把/sys/class/power_supply目录下所有设备的uevent全部拉一遍打印出来贴在工位上。往后你遇到的绝大多数充电问题都能从这些数据里找到线索。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑