news 2026/10/21 18:00:09

基于kernel 5.15的USB状态更新与充电事件上报机制解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于kernel 5.15的USB状态更新与充电事件上报机制解析

1. 项目背景与整体拆解:从USB插拨到充电事件上报

做了这些年BSP和内核驱动,我越来越觉得,真正折磨人的往往不是某个算法或者框架本身,而是硬件状态和软件事件这两条线之间的对接。最近我刚好在基于kernel 5.15的一套移动平台上,处理了一个典型的“USB状态更新充电事件”问题:USB口的状态变化没有正确驱动PMIC充电单元更新,导致上层拿到的充电事件要么不来,要么来了也是错的。

字面上看,这个问题就是:当用户把USB线插入设备,或者拔掉设备,内核能不能及时把物理连接状态翻译成一个充电器驱动能理解的“输入源类型”,再通过power_supply框架把这个类型和充放电状态以事件的形式通知出去。这看起来是个小功能,但在手机、平板、IoT设备上牵一发动全身。上层App收到“正在充电”事件后,才会限制某些高耗电功能,比如关掉后台大流量下载、降低屏幕亮度;系统服务收到“拔电”事件后,才会从性能模式切回省电策略。所以这条链路是否通畅,直接影响用户的充电体验和整机功耗表现。

在kernel 5.15上做这件事,比在4.x上多了一些需要重新梳理的地方。5.15是Android GKI的核心版本之一,内核把很多通用驱动框架重新做了分层,平台相关的驱动如果还按老方式直接挂在某一个Kconfig下,很容易出现编译过了但运行时不工作的情况。我在实际项目中就遇到过:extcon和typec两套体系同时存在,充电器驱动订阅了某一个事件源,但实际插拔时另一个事件源在传播,导致状态永远不对。

这篇文章的读者,我默认是BSP驱动工程师、内核开发人员,或者被充电相关Bug缠身的应用开发者。文中我会从机制讲到实操,再给出我实际调试时的现场记录和排错清单。如果你想临时修一个充电图标不显示的问题,可以直接跳到第四部分看排查表;如果你想把这条链路彻底搞懂,建议顺着看完。

1.1 核心链路参与方:USB控制器、PMIC充电单元、power_supply框架

先把术语和角色对齐,后面聊起来会高效很多。

第一参与方是USB控制器。在绝大多数移动SoC里,USB控制器同时支持device模式、host模式和OTG,常用驱动是dwc3。dwc3关注的是USB协议本身,包括连接、复位、枚举、挂起唤醒。当插上充电器时,dwc3不一定真的完成USB枚举——很多充电头根本没有数据传输能力,只会拉高VBUS并往D+/D-上加偏置电压。这时候USB控制器可能只产生一个连接中断,然后停在未枚举状态。

第二参与方是PMIC里面的充电管理单元。它掌握着电池充电路径,例如输入过压保护、输入限流、预充电、恒流、恒压这些流程都由它完成。PMIC内部会配置一组与充电状态相关的寄存器,像是输入源类型、充电状态机等。大部分PMIC还自带VBUS检测和充电状态中断,能够独立感知“外部电源接入了”,但是感知不到接入的是PC、充电头还是快充适配器。

第三参与方是内核的power_supply框架。它屏蔽了底层硬件的差异,把充电器、电池统一抽象成类设备。用户空间看到的是/sys/class/power_supply/下面的一个个节点,例如usb、ac、main_battery。充电器驱动在检测到输入源变化后,会更新这些节点的属性并调用power_supply_changed(),触发uevent广播。上层框架只要监听uevent就能刷新状态。

这三者之间的关系可以概括为:USB控制器和PHY负责“识别”,PMIC负责“执行”,power_supply负责“广播”。“USB状态更新充电事件”这件事,本质上就是把第一方识别到的客观事实,翻译成第三方能广播给用户空间的充电事件。

1.2 kernel 5.15带来的变数:GKI模块化与Type-C新老框架并存

要理解kernel 5.15上这个题目为什么值得单独写一篇,还得说清楚GKI和Type-C的变数。

GKI把内核核心部分固定下来,厂商只能以内核模块形式加入驱动。这在逻辑上更干净,但也带来实际约束:一些原本通过内建符号直接调用的逻辑,现在必须使用EXPORT_SYMBOL接口;一些驱动加载顺序受模块加载依赖制约。充电器驱动如果依赖USB状态通知,而这个通知来自另一个晚加载的模块,就会出现注册notifier时对方还没就绪、然后一直等不到状态更新的情况。

Type-C框架的引入同样值得注意。以前很多设备还在用micro-B口,USB状态基本靠VBUS和D+/D-识别,逻辑相对简单;现在Type-C口带CC引脚,状态检测既可以通过CC引脚的电阻实现,也可以通过TCPC用I2C访问。kernel 5.15里,完整的软件栈包括typec_class、typec_connector_class、tcpm、tcpc,外加上层端口管理。这就形成了一套和老的extcon并行的新传播路径。适配时稍不注意,充电器驱动订阅的是extcon的上报,但硬件状态实际走的是typec框架,两条路永远不碰头。

所以,这篇文章既讲通用机制,也讲我在kernel 5.15平台上的实际落地做法。下面从机制开始,逐步进入实操。

2. USB状态检测与充电事件的机制拆解

2.1 物理插入到“接入类型确认”的完整过程

进入代码前,先模拟一遍插入充电头的物理过程。

当你把充电线插入设备,首先是VBUS线接触,PMIC和USB PHY都会看到VBUS电压开始上升。VBUS上升不是瞬间完成的,因为线上有电容和接地寄生,会有一个从几百微秒到几毫秒不等的爬升过程。硬件上,这个上升沿通常会被两个位置捕获:USB PHY的VBUS检测比较器,以及PMIC的VBUS GPIO或ADC通道。

接下来是角色判断。如果是普通USB口,驱动会进入BC1.2握手流程。检测原理大致是:先看D+和D-是否有短路关系。DCP专用充电端口的D+和D-之间通常是短路的;CDP则是先把D+拉高,看D-有没有同步响应;SDP是标准电脑口,D+/D-电压由USB host侧控制。通过一组时序状态机,内核驱动最终得到接入类型。

如果是Type-C口,判断则由CC引脚上的电阻决定。接入的受电方和供电方各自通过CC引脚的上拉或下拉电阻来表达自己的角色和电流能力。Type-C控制器解码这些电阻值,得到“能提供多少电流”的初始能力,再通过PD协商进一步获取上报的功率档位。

这类信息在驱动里的最终形态,不是一个单一标志,而是一组状态。常见的有:连接事件、cable方向、USB设备角色、充电器类型、电流能力。只有把这一组状态全部确认完,才能可靠地生成充电事件。

2.2 充电器类型与充电参数的关系

充电器驱动在拿到类型后到底要干什么?这需要把类型对应的参数说清楚。

在USB BC1.2规范里,充电端口分成三类。

  • SDP(Standard Downstream Port):标准USB端口,最大输出一般500mA,主要用于数据传输,电流非常有限。
  • CDP(Charging Downstream Port):既可以传输数据,又能提供最大1.5A左右的充电电流。
  • DCP(Dedicated Charging Port):专用于充电,数据线短接或不接,能提供1.5A甚至更高的电流。

除此之外,现在的设备还面临PD协议。PD协商成功之后,电流能力不再是一档固定值,而是可动态协商的变量。例如一个30W适配器,可能协商出5V/3A、9V/3A、12V/2.5A等不同档位。这个信息最终也要反馈给充电器驱动,用于设置输入限流和目标电压。

充电器驱动拿到这些信息之后,PMIC的输入限流寄存器、输入过压阈值和动态电源路径管理选项就会跟着调整。比如识别到SDP,系统可能把输入限流设置到500mA以内,甚至提示“慢速充电”;识别到DCP,则按高电流充电配置。如果USB状态没有更新或者类型判断错了,充电参数自然就是错的。实际项目里最常见的现象是插上快充头却只有普通慢充速度,多半就发生在这一环。

2.3 为什么kernel 5.15里这条链路更“脆弱”

把机制讲完,回答一下为什么kernel 5.15上问题更容易暴露。我的观点有三个。

第一,模块化导致依赖关系复杂。GKI框架下,模块加载顺序不完全由链接顺序控制,而是由加载时的依赖关系决定。如果上游模块要监听下游模块的状态,而下游还没加载完,notifier注册就会失败。很多驱动为了规避这个问题,用了延迟注册或轮询,反而增加了调试难度。

第二,接口多样化。老的extcon、新的Type-C子系统、PMIC自带的VBUS中断,三条路径都可以上报充电器的存在。但它们上报的数据格式、时机、去抖策略不一致。同一个平台如果同时开启这些路径,又没有协调好,就会出现事件重复上报、状态覆盖等怪问题。

第三,5.15内核里power_supply框架做了不少调整。属性更新的并发控制更严格,如果驱动里用了不规范的原子操作或漏了锁,往往不会编译报错,而是在实际运行中随机丢uevent。这也是调试时最让人头疼的部分。

3. 在kernel 5.15上打通USB状态与充电事件的实操记录

3.1 确认基本软件框架与配置

拿到一个可疑的充电事件丢包问题,我习惯先做三件事:确认内核配置、确认设备树描述、确认驱动加载顺序。这一步能过滤掉一半的问题。

内核配置方面,建议检查这几个开关是否打开:

  • CONFIG_USB_DWC3:USB控制器驱动
  • CONFIG_EXTCON:外部连接器框架
  • CONFIG_TYPEC、CONFIG_TCPM:Type-C控制器框架
  • CONFIG_POWER_SUPPLY:电源抽象框架
  • 平台配套的充电器驱动Kconfig

设备树方面,USB控制器节点、PHY节点、Type-C端口控制器节点和PMIC充电器子节点的关系要理清楚。充电器子节点通常挂在PMIC的I2C下,它和USB节点之间不一定直接引用,而是通过一个extcon或typec的phandle来绑定。我在实际项目里看到过设备树里把phandle写错或漏写的情况,导致驱动加载正常、日志正确,但状态事件就是过不来。

驱动加载顺序可以通过启动日志确认。在log里搜dwc3、typec、pmic charger这几类驱动的probe时间点,如果发现充电器驱动先于USB状态源注册,就要引起警惕。

3.2 在驱动代码里追踪状态更新路径

一次完好的状态更新路径,在日志里应该有迹可循。我先描述一条标准路径,再结合系统调试手段说明如何验证。

假设平台用的是Type-C口,流程大致是这样:

  1. 插入充电器,TCPC通过I2C中断报告CC状态变化。
  2. tcpm状态机运行,在收到CC和电压信息后确认连接。
  3. 连接确认后,Type-C框架会通过typec_connector_class报告Port Partner信息,同时,如果配置了extcon桥接,它会触发extcon_set_state。
  4. 充电器驱动里注册了extcon notifier或者typec notifier,回调被触发,驱动读取当前cable类型。
  5. 驱动根据类型更新power_supply属性,比如online、status,然后调用power_supply_changed()。
  6. uevent事件带上“POWER_SUPPLY_STATUS/ONLINE”等内容广播到用户空间。

查看日志时,重点关注“type_attach”、“USB_ENTER”、“charger online”这类关键字。各平台日志关键字差异很大,但思路是顺着事件源往外看。如果你在日志里看到了上游的type attach,却没有下游的charger online,那说明断链在notifier回调或属性更新之前。

3.3 上报事件的代码路径怎么补

如果发现链路确实断了,而且断点在状态订阅这一步,就需要补代码。下面是一个简化但结构完整的示例模板,适合在vendor charger驱动里实现事件订阅和上报。

static struct notifier_block usb_nb = { .notifier_call = usb_state_notifier, }; static int usb_state_notifier(struct notifier_block *nb, unsigned long state, void *data) { struct device *dev = usb_chg_psy.dev; enum power_supply_type chg_type; if (state & EXTCON_USB) { chg_type = detect_input_source_type(); usb_chg_psy.properties[POWER_SUPPLY_PROP_ONLINE] = 1; usb_chg_psy.properties[POWER_SUPPLY_PROP_STATUS] = POWER_SUPPLY_STATUS_CHARGING; power_supply_changed(&usb_chg_psy); } else { usb_chg_psy.properties[POWER_SUPPLY_PROP_ONLINE] = 0; power_supply_changed(&usb_chg_psy); } return NOTIFY_OK; }

这只是示意,真实驱动里还会涉及锁、去抖、并发控制和寄存器操作。但核心思路是三步:拿到状态、更新属性、通知变化。如果平台已经有类似notifier但没生效,检查注册时机和返回值的处理。notifier回调一定要返回标准值,比如NOTIFY_OK或NOTIFY_DONE,返回值错误会导致后续notifier链中断。

power_supply_changed()触发后,内核会通过kobject_uevent_env()广播uevent。广播是否成功可以在调试期间用监听netlink uevent的方式验证。实际开发板上,我通常还会同时cat一下/sys/class/power_supply/usb/online,看看属性值是否已经更新。如果属性更新了但没有uevent,问题多半出在kset或kobject注册上面。

3.4 验证修复效果的完整步骤

当你改完代码,建议按这个顺序验证:

  1. 编译内核模块并更新。
  2. 插入电脑(SDP)确认枚举和“正在充电”事件。
  3. 插上DCP充电头确认充电类型切换。
  4. 验证Type-C口插入时是否还能持续监听PD协商结果的变化。
  5. 反复快速插拔,观察是否有状态滞留或者双重上报。

验证修复效果时,反复快速插拔最能暴露问题。正常插拔都能过关,但连续插拔十几次后就会偶现状态不对。这一般是内核work queue延迟或者notifier回调里处理了耗时操作导致的。在驱动里别做长时间阻塞操作,能用线程的不要用硬中断上下文。

4. 实际调试中遇到的典型问题与排查思路

到了这一段,我把这次调试过程中真正碰到的几个问题列出来。每个问题给出现象、初步判断和最终定位,方便你对照排查。

4.1 现象一:插上充电器完全不进入充电状态

先看dmesg,搜索VBUS相关日志。多数情况下有中断日志,但没有后续充电器online日志。

我遇到的一次根因是Type-C控制器已经识别到CC连接,但因为DTS里没有把Type-C端口的中断及时映射到充电器驱动的事件源,导致tcpm上报之后没有人消费。最终修改是在充电器驱动的notifier注册路径上增加对typec事件的监听,而不是只监听老的extcon。

排查要点:确认事件源是谁、notifier注册没有、回调是否被调用。

4.2 现象二:能充电但功率始终是普通档

这个问题的典型原因是充电器类型识别卡在SDP。插的是DCP或快充头,但内核认为接入的是普通USB口。

我用电流仪确认了实际输入电流只有500mA,然后在sysfs里读到充电器类型确实错误。进一步定位发现是BC1.2检测的时序没有走完,因为检测依赖的D+/D-电压采样线程被系统调度延迟,错过了握手窗口。

这类问题不能靠乱调时间窗,更合理的做法是把检测线程优先级提高或改成wakeup高优先级work。同时检查一下检测完成后有没有把结果正确写回PMIC寄存器。

4.3 现象三:拔掉USB后状态仍然停留在“充电中”

这类问题最烦人,因为线下很难稳定复现。我用ftrace抓到一次现场后发现,power_supply_changed()被调用了,但uevent广播前状态属性又被后面一个慢的callback覆盖回去。

究其原因,是拔电瞬间USB控制器和PMIC两个中断几乎同时产生,两个notifier回调并发执行,由于没有加锁,导致后执行的旧值覆盖了新值。解决办法:在充电器驱动的状态更新入口统一加互斥,并且对VBUS下降沿做去抖。

去抖时间可以定在80ms左右,既能保证插拔响应速度,又能滤掉接触不良产生的毛刺。去抖时间不是越长越好,太长会导致用户拔线后充电图标延迟消失,反而产生新投诉。

4.4 常见问题速查表

现象可能原因优先检查项
插入不充电事件源notifier未注册dmesg、extcon/typec状态
有日志但无ueventpower_supply注册不完整/sys/class/power_supply下节点
电流档位偏低充电器类型识别错误sysfs读取charge_type
拔线状态残留并发覆盖/去抖不足锁、去抖处理
偶发不更新workqueue延时阻塞操作、优先级

这张表我贴在工位上很久了,每次排查充电问题都会先扫一遍。

5. 基于kernel 5.15调试的几条避坑建议

5.1 善用sysfs快速确认静态状态

在手机UI上看不到具体值,但开发板上可以通过sysfs快速验证。

查看所有供电设备节点:

ls /sys/class/power_supply/

读取USB充电器状态:

cat /sys/class/power_supply/usb/online cat /sys/class/power_supply/usb/status cat /sys/class/power_supply/battery/status

这几个节点是否更新、是否同步,能帮你快速缩小问题范围。如果online已经是1但界面不亮,问题可能更靠上层;如果online始终是0,就专心看内核驱动链路。

5.2 调试时先关掉用户空间的干扰

Android系统上层有Healthd、电池服务、Settings等多个模块会响应uevent。它们的行为会掩盖一部分内核问题。遇到纯充电状态Bug时,建议先在开发板上关掉不必要的上层服务,甚至直接用串口在内核态观察。否则你看到的现象是“App没刷新”,但实际是内核本来就发错事件。

5.3 代码审查时重点看并发与锁

内核对充电事件的处理几乎都是异步的。VBUS中断、TCPC中断、用户态读操作随时可能并发。驱动里凡是读写充电状态共享变量,都应该有明确的内存序和锁保护。review代码时我首先看的是:两个notifier回调是否可能同时写同一个属性;power_supply_changed()前后是否持锁。这两个点基本能覆盖大多数偶发Bug。

5.4 用好ftrace对比前后时间线

最后分享一个经验:调试这类靠事件驱动的问题,肉眼阅读dmesg是低效的。我第一次定位拔电状态残留问题,就是靠ftrace把notifier回调、workqueue执行和power_supply_changed()的时间点拉出来对比,才确认了两个回调的先后顺序。建议在项目里提前把tracepoints跑通,需要时能省半天时间。后来我整理这套处理流程时,又把这个办法用在了USB状态更新与充电事件的时间对齐上,确实能把一类模糊的问题变得可观测、可验证。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 17:57:21

快马AI智能诊断实战:三分钟定位修复应用启动失败

1. 启动失败:开发者最熟悉的陌生人你有没有过这样的三分钟:凌晨两点的部署窗口,测试环境连续三次起不来,控制台输出就一行Application failed to start,后面跟着一串看不懂的堆栈片段,群里已经有人开始连环…

作者头像 李华
网站建设 2026/10/21 17:57:07

近红外无创血糖检测的工程落地难点与解决方案

简介:本资源是一份面向生物医学工程、嵌入式系统及智能医疗方向高校师生与研发人员的学术型技术文档,聚焦近红外光无创血糖检测这一临床痛点问题,提供从原理建模到硬件验证的完整技术路径。全文基于PPG信号采集、朗伯-比尔定律定量分析、最小…

作者头像 李华
网站建设 2026/10/21 17:56:59

wire_api选chat还是responses?API语义边界决策指南

1. 标题背后的真实战场:不是语法选择,而是API语义边界的认知重构看到这个标题——“Codex 接中转站,wire_api 填 chat 还是 responses?”——我第一反应不是查文档,而是放下手头正在调试的接口,把笔记本翻到…

作者头像 李华
网站建设 2026/10/21 17:56:39

杰理692N SDK开发入门:从源码结构到跑通第一个音频工程

简介:这份杰理692N开发入门教程SDK源码面向蓝牙、Wi-Fi等无线通信方向的嵌入式开发者与初学者,帮助其快速搭建692N芯片的开发环境并理解SDK整体架构。压缩包共647个文件,约40.08MB,以241个h头文件与127个c源文件为核心&#xff0c…

作者头像 李华
网站建设 2026/10/21 17:56:09

MBA毕业论文救命指南:10类AI工具全流程实战分享

如果你正在MBA毕业论文的deadline边缘反复横跳,白天上班、晚上码字,周末还要被文献综述按在地上摩擦,那这篇内容应该能直接拉你一把。我前前后后带过不少MBA在读的朋友和同事,见过太多人不是不努力,而是把大量时间耗在…

作者头像 李华
网站建设 2026/10/21 17:55:58

技术博文高效写作:从信息模板到SEO友好呈现

请提供以下信息,我会基于它们为您生成高质量博文: 项目标题: [必填,例如“某开发框架部署实录”] 项目正文: [选填,您的原始零散描述、踩坑记录、需求背景等] 关键词: [选填,用逗号分隔] 摘要描述: [选填&#xff0c…

作者头像 李华