ARTICLE DETAIL

资讯详情

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

电源子系统核心power_supply_core.c与MTK充电实战

电源子系统核心power_supply_core.c与MTK充电实战 1. 为什么第二篇才轮到power_supply_core.c聊MTK的Charger驱动大多数人的第一反应是去看mtk_charger.c、mt6370_charger.c这类平台相关代码。但如果只看这些文件很容易陷入只见树木不见森林的状态——你知道充电电流在哪里配置、温控阈值在哪里修改却说不清上层用户空间看到的/sys/class/power_supply/battery/status是怎么来的更说不清power_supply_changed()这个函数调用之后Android系统的电量百分比是怎么一步步刷新的。power_supply_core.c就是打通这个认知断层的关键文件。它是Linux内核power supply子系统的核心实现承担着三件事统一抽象让所有电源设备以相同方式呈现在用户态、生命周期管理设备注册、注销、属性动态切换、事件通知电量变化时主动告知上层。MTK的charger驱动无论多复杂最终都要调用power_supply_register()把设备注册进这个框架然后通过power_supply_changed()把变化事件推出去。我最初接触这个文件是被逼的——当时遇到一个非常诡异的问题充电状态在0%到100%之间跳变排除gauge问题后发现是POWER_SUPPLY_PROP_STATUS属性在get_property()回调里因为锁竞争返回了错误值。不读core层的源码你根本不知道那个错误值在sysfs层会被如何处理。这篇文章就顺着我在MTK平台上的实际调试过程把power_supply_core.c的核心机制完整拆一遍重点放在与charger驱动直接相关的代码路径上。2. 一个文件撑起整个电源抽象core层的设计思路先捋清楚power_supply_core.c在整个内核电源框架里的位置。它属于drivers/power/supply/目录同目录下还有power_supply_sysfs.csysfs属性导出、power_supply_leds.cLED指示、power_supply_hwmon.c硬件监控接口以及各厂商的charger驱动MTK的mtk_charger.c、高通的qcom_step_chg.c等。power_supply_core.c做的事情就是管理部门级的事务分配和释放power_supply结构体、处理设备树匹配、注册/注销内核设备、协调class接口和udev事件。真正的属性读写动作是通过回调函数下发给底层驱动的。2.1 两个核心结构体的职责边界操作power_supply_core.c绕不开这两个结构体struct power_supply_desc { const char *name; enum power_supply_type type; const 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); void (*external_power_changed)(struct power_supply *psy); bool no_thermal; int (*online_changed)(struct power_supply *psy); }; struct power_supply { const struct power_supply_desc *desc; char **supplied_to; size_t num_supplicants; char **supplied_from; size_t num_supplied_from; struct device_node *of_node; struct device dev; struct work_struct changed_work; struct delayed_work deferred_register_work; atomic_t use_cnt; struct blocking_notifier_head *battery_work; spinlock_t changed_lock; bool changed; struct mutex wakeup_lock; ... };desc描述这个电源设备是什么、支持哪些属性、怎么读写属性psy是内核中的设备实体负责管理状态、工作队列、通知链。驱动开发者的主要工作就是填充desc然后把它交给core层处理这样的分层能让不同厂商的charger驱动尽量复用公共框架。MTK的charger驱动注册的是battery、mtk-master-charger、mtk-slave-charger这几个名字在mtk_charger.c里会构建对应的power_supply_desc其中properties数组指定该设备暴露哪些属性。core层拿到这个数组后会在sysfs里自动生成对应的属性节点不需要驱动手动创建。2.2 设备树的叩门砖of_node的传递MTK平台的charger设备基本都是设备树驱动probe时通过of_node拿到platform device。Core层在注册时会检查psy-of_node如果存在就把电源设备绑定到设备树节点上。static int power_supply_core_init(struct device *dev) { ... if (psy-of_node) { ret device_add(psy-dev); ... ret sysfs_create_link(psy-dev.kobj, psy-of_node-kobj, of_node); ... } }这段逻辑决定了你在/sys/devices/platform/下看到电源设备与设备树节点的对应关系。调试时判断某个charger设备是否成功解析设备树直接看sysfs里有没有of_node链接即可。3. power_supply_register从驱动probe到sysfs节点出现MTK的charger驱动调用注册函数的路径一般是static int mtk_charger_probe(struct platform_device *pdev) { ... charger-psy_desc.name mtk-master-charger; charger-psy_desc.type POWER_SUPPLY_TYPE_MAINS; charger-psy_desc.properties mtk_charger_properties; charger-psy_desc.num_properties ARRAY_SIZE(mtk_charger_properties); charger-psy_desc.get_property mtk_charger_get_property; charger-psy_desc.set_property mtk_charger_set_property; ... charger-psy power_supply_register(pdev-dev, charger-psy_desc, psy_cfg); ... }从power_supply_register()开始core层的执行链路可以分成四个步骤。3.1 第一步结构体填充与合法性检查struct power_supply *power_supply_register(struct device *parent, const struct power_supply_desc *desc, const struct power_supply_config *cfg) { return __power_supply_register(parent, desc, cfg, true); } static struct power_supply *__power_supply_register(struct device *parent, const struct power_supply_desc *desc, const struct power_supply_config *cfg, bool ws) { struct power_supply *psy; ... if (!parent) { dev_err(NULL, %s: Expected proper parent device for %s\n, __func__, desc-name); return ERR_PTR(-EINVAL); } if (!desc-get_property) { dev_err(NULL, %s: Missing get_property callback for %s\n, __func__, desc-name); return ERR_PTR(-EINVAL); } ... psy kzalloc(sizeof(*psy), GFP_KERNEL); ... device_initialize(psy-dev); ... }这里明确要求parent非空并且至少要提供get_property回调。很多刚接触驱动的同事在写测试驱动时图省事不实现get_property就想注册必然直接返回-EINVAL。MTK的charger驱动一般都会提供完整的回调接口。3.2 第二步依赖关系建立power_supply_config中最重要的字段是supplied_to和supplied_from它们描述电源设备之间的拓扑关系static struct power_supply_config battery_psy_cfg { .supplied_to (char * const *)mtk_battery_supplied_to, .num_supplicants ARRAY_SIZE(mtk_battery_supplied_to), };在MTK平台gauge驱动注册电池设备时通过supplied_to声明我会给USB/无线充电器提供状态供给而charger驱动注册时通过supplied_from声明我的供电来源是电池。Core层在__power_supply_register()里会专门处理这种依赖if (cfg) { psy-desc desc; psy-supplied_to cfg-supplied_to; psy-num_supplicants cfg-num_supplicants; psy-supplied_from cfg-supplied_from; psy-num_supplied_from cfg-num_supplied_from; ... }随后在power_supply_register()完成核心工作后调用power_supply_check_supplies()遍历所有已注册的电源设备建立双向引用。这套机制用于当某个电源设备状态变化时core层能自动通知依赖它的其他设备。3.3 第三步class设备分配与sysfs目录创建psy-dev.class power_supply_class; psy-dev.type power_supply_dev_type; dev_set_name(psy-dev, %s, desc-name); ... ret device_add(psy-dev);dev_set_name()决定了/sys/class/power_supply/下的目录名。MTK平台注册的battery、mtk-master-charger、usb这些名字都来源于此。目录创建后power_supply_sysfs.c会根据desc-properties数组自动生成属性文件。一个容易忽视的细节是如果注册时遇到-EEXIST错误往往说明同名的power_supply设备已经存在。MTK平台的gauge驱动在休眠唤醒后重新注册电池设备时容易出现这个问题需要先power_supply_unregister()旧的设备再注册新的。3.4 第四步LED与hwmon的搭便车注册完成后core层还会尝试创建LED触发器和hwmon接口ret power_supply_create_bat_triggers(psy); ... ret power_supply_create_gen_triggers(psy); ... ret power_supply_add_hwmon_sysfs(psy);LED触发器服务于充电状态LED指示hwmon接口则用于把电压、电流、温度暴露给lm-sensors等工具。MTK平台如果没配置设备树的leds节点这部分基本不起作用hwmon接口对于服务器场景意义更大手机平台一般用不上。4. 同步与异步注册什么情况会启动延迟注册power_supply_register()内部还有一个分支可以选择立即注册或延迟注册。这个设计很多人没注意到但在MTK平台初始化顺序有问题时能救命。struct power_supply *power_supply_register_no_ws(struct device *parent, const struct power_supply_desc *desc, const struct power_supply_config *cfg) { return __power_supply_register(parent, desc, cfg, false); }带_no_ws后缀的版本对应ws false即不使用延迟注册标准的power_supply_register()对应ws true会启动一个deferred_register_work。在延迟注册的workqueue里core层会给驱动一个完整的周期去完成自身的初始化。if (ws) { INIT_DELAYED_WORK(psy-deferred_register_work, power_supply_deferred_register_work); schedule_delayed_work(psy-deferred_register_work, POWER_SUPPLY_DEFERRED_REGISTER_TIME); } else { power_supply_deferred_register_work(psy-deferred_register_work.work); }POWER_SUPPLY_DEFERRED_REGISTER_TIME默认0表示直接执行某些厂商会改成200毫秒甚至更久。这么设计的原因很现实电池设备和充电设备之间互相依赖A设备注册时需要B设备已经存在而B设备的注册又可能依赖A。延迟注册在时序上给了一个缓冲窗口。MTK平台如果发现开机阶段有battery: probe defer之类的日志并且power_supply节点起来得特别慢就可以检查一下是否应该调整这个延迟时间。不过一般不建议随意增大延迟因为用户空间init脚本可能等待这些节点就绪后才继续执行。5. power_supply_changed充电状态是如何通知出去的对于Android系统的电量显示来说power_supply_changed()是把底层变化推到上层的关键入口。MTK的charger驱动在检测到充放电状态切换、电量百分比跨越阈值、温度告警时都会调用它void power_supply_changed(struct power_supply *psy) { unsigned long flags; dev_dbg(psy-dev, %s\n, __func__); spin_lock_irqsave(psy-changed_lock, flags); psy-changed true; spin_unlock_irqrestore(psy-changed_lock, flags); schedule_work(psy-changed_work); } EXPORT_SYMBOL_GPL(power_supply_changed);这里有个细节值得注意power_supply_changed()本身做的事情很少只是置位然后调度工作队列。真正的事件广播发生在power_supply_changed_work()中static void power_supply_changed_work(struct work_struct *work) { unsigned long flags; struct power_supply *psy container_of(work, struct power_supply, changed_work); dev_dbg(psy-dev, %s\n, __func__); spin_lock_irqsave(psy-changed_lock, flags); if (!psy-changed) { spin_unlock_irqrestore(psy-changed_lock, flags); return; } psy-changed false; spin_unlock_irqrestore(psy-changed_lock, flags); power_supply_update_bat_leds(psy); power_supply_update_gen_leds(psy); atomic_inc(psy-use_cnt); if (psy-desc-external_power_changed) psy-desc-external_power_changed(psy); power_supply_update_supplicants(psy); atomic_dec(psy-use_cnt); power_supply_changed_work_sync(psy); ... kobject_uevent(psy-dev.kobj, KOBJ_CHANGE, NULL); ... }一次调用背后其实做了好几层事情。5.1 LED状态更新优先工作队列一执行先是更新LED状态。对于MTK的手机平台如果启用了power_supply的LED trigger充电动画LED就是在这里被触发的。5.2 依赖链的通知external_power_changed设备如果注册时声明了external_power_changed回调core层会调用它。这个回调的特定语义是外部电源状态改变了比如USB插拔导致的电源切换。MTK的charger驱动中mtk_charger.c里会定义这个回调用来检测充电器的插拔事件。每次插入USBexternal_power_changed都会跑一遍驱动需要快速判断当前是普通USB、DCP还是PD充电器。5.3 联动通知supplicant设备power_supply_update_supplicants()是依赖链的另一个维度。它会遍历该设备的supplied_to数组对每个下游设备调用power_supply_changed()。电池作为下游设备每当上游充电器变化时它的power_supply_changed()也会被触发最终刷新电量显示。5.4 kobject_uevent把通知送出内核最后一行kobject_uevent(psy-dev.kobj, KOBJ_CHANGE, NULL)是用户空间看到变化的根本原因。uevent发出后udev/ueventd会把/sys/class/power_supply/battery/下的属性文件重新读取一遍Android的HealthService更早是BatteryService监听到事件后刷新电池信息。MTK平台高版本Android中HealthService通过uevent监听事件触发后走battery.properties读取流程。底层power_supply_changed()每触发一次可能有4~5个属性文件被重新读一遍。明白了这条链路之后就能理解事件风暴带来的后果。6. get_property返回值的语义为什么-ENODATA会让上层直接崩溃get_property是core层与底层驱动交互的桥梁。驱动实现这个回调时需要根据psp参数返回对应的值。返回值本身也有语义if (ret -ENODEV || ret -ENODATA) return ret;在power_supply_sysfs.c的属性读取路径中-ENODEV和-ENODATA有特殊处理static ssize_t power_supply_show_property(struct device *dev, struct device_attribute *attr, char *buf) { ... ret power_supply_get_property(psy, psp, value); if (ret -ENODEV || ret -ENODATA) return ret; ... }这就意味着如果驱动在某个属性上返回这两个错误码上层读这个属性会直接失败。Android的HealthService对某些属性的读取失败非常敏感可能导致电量显示为-1或者直接不显示。我自己在MTK平台上遇到过一个问题电池温度超过60度时gauge驱动在get_property中返回-ENODATA表示温度不可用。结果Android上层没有对这种情况做保护BatteryPropertiesRegistrar读到异常值后手机一直处于充电中的状态。后来我们改成返回实时温度值而不是错误码问题才解决。所以写get_property的时候要记得错误码在上层看不是什么温和的提示可能直接导致异常行为。能用数值表达的属性尽量给数值除非该属性真的不存在。7. 属性批量拷贝机制幂等原则与脏标记power_supply_core.c里还有个不太显眼但很实用的机制就是power_supply_get_battery_info()和属性批量读取。Android的HealthService读取电池信息时通常不是一个个属性单独读而是通过一个批处理流程读取所有感兴趣的属性。在power_supply_sysfs.c里能找到一个关键的设计属性读取会先尝试从内核空间直接读取如果没有缓存则调用get_property。内核空间对这个过程做了幂等处理——同一份数据多次读取不需要反复进入驱动回调。MTK平台中mtk_battery.c的gauge驱动会周期性更新内部缓存get_property回调只负责把缓存值拷贝出来不直接和硬件通信。这样一个power_supply_changed()触发后上层连续读4~5个属性底层硬件不会被打爆。如果驱动实现得比较激进每次get_property都去读硬件寄存器在高频事件下会出现显著的性能损耗。所以MTK平台的经验是需要频繁上报的属性在驱动里做缓存只有低频监控属性才在get_property里做实时读取。8. MTK双电池与快充场景下core层的特殊玩法MTK平台相比高通的差异点在于双电池方案的普遍性。mtk-charger设备下往往注册了多个power_supply实例battery是逻辑电池由gauge层聚合mtk-master-charger和mtk-slave-charger对应两个物理充电通道usb对应Type-C口。多实例带来的第一个问题是命名冲突。power_supply_registered是通过name来区分设备的如果两个充电通道都叫charger后注册的会注册失败。MTK的做法是用mtk-master-charger、mtk-slave-charger区分——这个细节恰恰说明core层本身不提供同一类型设备可以有多个实例的方便手段。多实例带来的第二个问题是事件风暴。双电池方案下每节电池的温度、电压、电流都会变化如果每个实例都频繁调用power_supply_changed()内核和用户空间之间的uevent会非常多。我实测过在某些低端平台上事件风暴会导致ueventd占满CPUSystemUI电量刷新出现明显卡顿。规避方案是事件合并在驱动层设置一个最小上报时间间隔比如2秒两个连续事件之间的时间差小于该间隔时延迟到间隔结束再上报。具体实现可以直接复用core层的changed标志位语义也可以自己在驱动里加一个last_report_jiffies判断。9. 调试利器从sysfs反推core层行为掌握了core层的原理之后调试MTK充电问题会顺手很多。列举几个我常用的手段9.1 列出所有power_supply设备ls /sys/class/power_supply/正常MTK平台上至少能看到battery、usb、mtk-master-charger、mtk-slave-charger以及可能的wireless、pc_port等。如果少了某个设备说明对应驱动probe失败结合dmesg查具体错误。9.2 查看某个设备的属性cat /sys/class/power_supply/battery/ueventuevent文件会把所有get_property能读到的属性一次性输出。这是最快速的健康检查方式。如果某个属性缺失多半是properties数组里没有包含或者get_property返回了错误码。9.3 统计uevent事件数cat /sys/kernel/debug/tracing/trace | grep power_supply_changed配合trace_events可以统计某个时间段内power_supply_changed被调用的次数对排查事件风暴很有帮助。如果事件数每秒超过10次基本就是异常。9.4 查看设备依赖关系ls -l /sys/class/power_supply/battery/supplied_from/这个目录下的软链接列出了电池的供电来源设备supplied_to则是电池供给的目标设备。如果依赖关系配错外部电源变化时电池无法收到联动通知电量显示会僵死。10. 基于core源码的两个经典Bug复盘最后分享两个与core层行为直接相关的问题排查过程帮助理解这个文件在真实调试中的价值。10.1 问题一插入充电器后电量不刷新现象是插入USB充电器后/sys/class/power_supply/battery/status一直停留在Discharging。从power_supply_changed()链路逐段排查先确认充电器设备有没有调用power_supply_changed()结果有再确认external_power_changed回调有没有触发用dev_dbg验证结果有继续确认battery的get_property里STATUS属性逻辑发现驱动根据usb设备的online属性判断是否充电但电池的supplied_from数组没有包含usb。Core层的power_supply_update_supplicants()只通知supplied_to指定的设备。充电器注册时如果supplied_to里写的是mtk-master-charger而不是battery电池就收不到联动通知status自然不刷新。修复方式就是调整充电器设备的supplied_to数组把battery加进去。10.2 问题二低概率出现电池设备变成sensor类型某次测试发现/sys/class/power_supply/battery/type偶尔显示为Unknown导致上层把电池识别成未知设备。查下来发现是驱动在probe时序不稳定时power_supply_register()的desc被提前释放了。Core层注册时使用的是desc的指针而不是拷贝内容。如果驱动在堆栈上构造desc注册函数返回后栈帧销毁后续sysfs读取时就会拿到随机的类型值。MTK官方SDK里充电驱动基本都是把desc定义在struct charger_data之类的长生命周期结构体里就是为了避免这个问题。自己写驱动时也要注意power_supply_desc和power_supply_config的生命周期都要长于注册调用。11. 写在调试路上的几句话power_supply_core.c不像charger驱动那样有大量平台相关的逻辑和联调工作但它定义了整个电源子系统对外呈现的边界。理解了这个边界再做MTK的Charger适配、快充调试、双电池同步代码视角会完全不同。个人经验是拿到一个新的MTK平台后不要急着改mtk_charger.c里的充电参数先在目标机器上跑一遍cat /sys/class/power_supply/*/uevent把每个电源设备的行为基线建立起来。后面出了问题第一反应才会是去看core层的调度逻辑而不是在charger驱动里瞎猜。下次有机会可以再聊聊power_supply_hwmon.c和power_supply_leds.c这两个子模块尤其是hwmon接口在MTK大功率快充场景下的温控联动也是踩过不少坑的。
返回列表