RK3588 这颗芯片这两年出镜率实在太高了,平板、开发板、边缘计算盒子、甚至一些轻量级 NAS 都在用。我最近在 RK3588 + Android 12 平台上调 Type-C PD 充电,踩了一圈 DTS 和内核驱动的坑之后,发现这块内容网上资料其实不少,但大多零散且停留在“能亮屏能跑”的阶段,真正把 PD 协商、充电通路、电池充电策略串起来的文章很少。这篇就把我实际调试中的配置、修改、日志分析和排查思路完整写出来,给正在做 RK3588 平台 PD 充电开发的朋友一个可以直接参考的底稿。
1. 项目概述与整体架构
1.1 这个项目到底在解决什么问题
RK3588 的典型应用场景决定了它离不开 Type-C PD。无论是做安卓平板还是边缘 AI 盒子,整机功耗动不动十几瓦甚至几十瓦,Type-C 口如果不走 PD 协议协商出 9V、12V、15V 甚至 20V 的高压档位,单靠 5V 供电根本扛不住系统的峰值负载。更别说产品还要兼顾数据、视频输出、USB 外设这些功能,Type-C 口本质上已经从“充电接口”变成了“多功能 IO 入口”。
但硬件上把 Type-C 口设计出来只是一半,另外一半全在软件层:DTS 里面的设备树节点要描述清楚这颗 TCPC 芯片挂在哪个 I2C 上、中断脚在哪里、支持哪些 PDO 电压电流档位,内核驱动要负责在 Type-C 线插入时完成 CC 检测、PD 协议报文交换、功率协商,然后把协商结果送到 charger 驱动,最终控制充电芯片给电池灌电流。整个过程任何一个环节掉了链子,轻则充电功率只有 5W,重则完全充不进电,甚至系统供电不足直接关机。
所以这个项目的核心问题不是“怎么让 Type-C 口工作”,而是“怎么让 Type-C 口在 PD 协议下实现预期的功率档位切换,同时保证充电流程和 Android 系统层面的电源管理无缝衔接”。当你拿到一块新的 RK3588 主板,适配 Type-C PD 充电的第一步,往往不是改代码,而是把整条链路梳理清楚。
1.2 Type-C PD 充电全链路拆解
为了后面调试不迷路,我先把 RK3588 + Android 12 下 Type-C PD 充电的整条链路拆开。从外到内依次是:
- 外部设备端:PD 适配器 / PC USB 口 / 其他 Type-C 设备,提供电源和协商对象
- Type-C 物理接口:CC1/CC2 引脚上的电阻配置决定端口角色,D+/D- 用于 USB2.0 通信,SBU 用于 DP Alt Mode
- TCPC 芯片:也就是 Type-C Port Controller,负责 CC 检测、发送接收 PD 物理层信号,常见的就是 FUSB302、TUSB422 这类,RK 平台上 FUSB302 用得最多
- TCPM:Type-C Port Manager,在内核里对应
drivers/usb/typec/tcpm/tcpm.c,负责整个 Type-C / PD 状态机调度,包括 attach/detach、SOP 报文收发、PDO 协商、电源角色切换等 - Charger 驱动:控制充电芯片输出到电池的电压和电流,常见的有 BQ25700、SY6970、SG2001 等
- Power Supply 框架:内核的
power_supply子系统,把 charger、tcpc、battery 等设备虚拟化成电源节点,Android 上层通过 sysfs 和 Health Hal 读取电量与充电状态
这里面最核心的枢纽是 TCPM,它往底层走通过tcpc_dev接口跟 TCPC 芯片打交道,往上层走通过tcpc提供的接口把协商好的电源参数传给 charger,同时还通过 typec class 和 extcon 驱动管理数据角色和供电角色。
数据流向大概是这样的:适配器插入后,TCPC 芯片检测到 CC 引脚上的 Rd 或 Rp 状态,产生中断;TCPM 开始跑 Type-C 连接状态机,在 CC 上拉/下拉电阻,做电缆检测、SRC/SNK 角色确定;然后如果双方都支持 PD,就会在 VBUS 上发送 SOP 的“source capabilities”报文,列出所有支持的 PDO(比如固定 5V/3A、9V/2A、12V/1.5A 这样的档位);RK3588 平台作为 sink 端,也会发送 sink capabilities 和请求报文,选定一个 PDO 后用 GoodCRC 确认,最终适配器端切换 VBUS 电压,完成协商。
协商完成后,TCPM 会把协商得到的电压电流写进一个typec_power_supply设备,charger 驱动通过power_supply框架读取这个值,作为输入限制写入充电芯片的输入电流/电压寄存器。这样一来,硬件上的充电电流大小,本质上是由 PD 协商结果决定的。
1.3 DTS 与内核驱动如何分工
搞清楚链路之后,DTS 和内核驱动的分工就非常简单了:DTS 描述“硬件长什么样、允许怎么工作”,内核驱动负责“实际怎么跑起来”。
DTS 层面,我们主要配置三块内容。第一是 TCPC 芯片节点本身,把它的 I2C 地址、中断、复位引脚、供电、输入输出角色定义好;第二是 PD 策略数据,支持哪些 PDO 档位、是 dual-role(DRP)还是仅 sink、是否支持 PPS、source 和 sink 的能力值,这些参数直接影响协商结果;第三是 charger 节点和电源路径参数,比如充电芯片的 I2C 地址、输入限流、充电限流、OTG 配置等。
内核驱动层面,则是完成 TCPC 芯片的寄存器读写、TCPM 状态机的运行、PD 报文的收发解析、充电芯片控制等。对普通产品开发而言,TCPM 和 typec class 这些内核通用框架基本不需要大动,真正需要修改的多半是 charger 驱动、TCpc 芯片驱动里的厂商私有逻辑,以及跟平台相关的电源管理策略。
一句话总结就是:DTS 决定“能协商出什么档位”,内核决定“协商出来之后怎么把电送到电池”,Android 上层应用则展示“充到多少、功率多大”。这三层只要有一层配置错误,整条链路就会出问题。
2. DTS 配置核心细节
2.1 tcpc 节点配置要点
RK3588 平台上,TCPC 节点一般挂在 I2C 总线上,以 FUSB302 为例,设备树里大概是这样的结构:
&i2c2 { status = "okay"; fusb302: fusb302@22 { compatible = "fcs,fusb302"; reg = <0x22>; interrupt-parent = <&gpio1>; interrupts = <RK_PA0 IRQ_TYPE_LEVEL_LOW>; pinctrl-names = "default"; pinctrl-0 = <&fusb302_int>; tusb422 = <0>; /* 0: fusb302, 1: tusb422 */ tcpc-role = "dual"; tcpc-power-supply-candidate = <1>; /* 0: pwr supply 不参与PD协商 */ }; };这里的几个关键字段挨个说一下。reg = <0x22>是 FUSB302 的 I2C 从机地址,跟具体芯片硬件有关,买模块时问清楚或者看芯片手册。interrupt-parent和interrupts定义 TCPC 的中断引脚,要求这颗 GPIO 必须具备电平触发能力,FUSB302 的中断输出是低电平有效,所以触发方式是IRQ_TYPE_LEVEL_LOW。pinctrl里的fusb302_int是引脚复用配置,把 GPIO1_A0 配成普通输入功能。
tcpc-role = "dual"表示端口是双角色端口,既可以当 Source(比如OTG对外放电)也可以当 Sink(插入适配器充电),对应 DRP 模式。如果只做纯设备端、永远从适配器取电,那就改成"sink"。我个人建议即使产品不需要对外放电,DTS 里也先保留 dual,因为调试阶段有可能会用到 OTG 功能验证,后面再根据产品需求收紧也不迟。
真正决定 PD 协商档位的是 tcpc 节点下挂的pd-data和source-pdos/sink-pdos信息。以做 sink 设备为例,配置如下:
fusb302_data: fusb302_data { status = "okay"; tcpc-role = "dual"; pd-data { sink-info { sink-pdo-size = <2>; sink-pdo-list = <PDO_FIXED_5V_3A 0x0c>, <PDO_FIXED_9V_3A 0x0c>; }; source-info { source-pdo-size = <1>; source-pdo-list = <PDO_FIXED_5V_1A 0x0c>; }; }; };这里的PDO_FIXED_5V_3A是内核提供的宏定义,展开后是一个 32 位数,表示一个固定电压 PDO,电压 5000mV、最大电流 3000mA。PD 协议里每个 PDO 占 4 字节,内容包括电压、电流、功率上限等字段,内核宏把这些字段按协议格式编码好了,设备树里直接用宏就行。
有一点值得注意:sink-pdo-list不能说只是给 USB-PD 协议看的“能力声明”,它实际上也会影响 TCPM 在协商时优先选择哪个档位。PD 协商流程一般由 Source 端发 capabilities,Sink 端选择一个 PDO 发 request。TCPM 会根据本地 sink 能力、当前系统功耗需求、电池剩余电量等条件,决定请求哪一个档位。如果你把 9V 档放在 5V 的前面,系统可能优先去请求 9V,这在很多场景下不是最优解。
建议产品上严格根据适配器能力来同步调整 PDO 列表,比如适配器只支持 5V/9V 两档,就只写这两档,不要多写一个 12V。我曾经碰到过因为多配置了一个不存在的 20V PDO,导致适配器始终无法完成协商、系统一直走 5V 默认电压的问题,原因就是适配器不支持对应档位却收到了过高电压请求,协议层面不稳定。
2.2 Charger 与电源路径配置
TCPC 协商出电压电流档位之后,电要送到电池,中间还要过 charger 芯片这一关。RK3588 平台上常见的 charger 有 TI 的 BQ25700、BQ25601,还有 SG 的 SG2001、SY 的 SY6970 等,配置方式大同小异。以 BQ25700 为例,它挂在 I2C0 上,DTS 节点大概长这样:
&i2c0 { status = "okay"; bq25700: bq25700@6b { compatible = "ti,bq25700"; reg = <0x6b>; interrupt-parent = <&gpio0>; interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>; pinctrl-names = "default"; pinctrl-0 = <&charger_int>; ti,charge-current = <2000000>; /* 2A 充电电流 */ ti,charge-voltage = <4352000>; /* 4.352V 浮充电压 */ ti,input-current = <2000000>; /* 2A 输入限流 */ ti,input-voltage = <5000000>; /* 5V 输入电压限制 */ ti,watchdog-timeout = <5000>; /* 看门狗,单位 ms */ }; };这里ti,charge-current、ti,charge-voltage、ti,input-current和ti,input-voltage是 charger 芯片的初始配置值。PD 协商完成之后,TCPM 会根据新协商出的电压电流动态更新这些值:比如协商出 9V/3A,那输入电压限制会升到 9V,输入电流限制可能是 3A 或按系统策略降为 2.25A,充电电流则按电池温度和 SOC 阶段调整。
这三个电流限制的概念很容易混淆,我举一个实际场景:PD 适配器协商出 9V/3A,意味着 VBUS 侧能提供 9V、最大 3A 输入。charger 芯片内部有一个输入电流限制寄存器,BQ25700 会限制从端口抽的电流不超过设置值。同时还有一个充电电流限制,限制灌入电池的电流大小。这两个不相等,因为系统自身负载也会消耗电流。比如系统跑着屏幕、CPU 满载,要消耗 1A,那 charger 的输入限流如果设成 3A,扣除系统 1A,实际能给电池充电的电流只有 2A。所以在 USB 充电调试时,看到软件里输入限流 3A 但电池充电电流只有 2A,这完全是正常现象,不是驱动 bug。
电源路径上还有一个隐蔽但重要的配置,就是typec-power-supply-candidate和tcpc-power-supply-candidate这类字段。它们的作用是告诉 TCPM“哪个 charger 设备是跟这个 Type-C 口相关联的电源输入路径”,TCPM 协商完 PDO 之后,要把新的电源参数写到这个 charger 上。如果你有多路输入(USB 口 + 圆形 DC 口 + 无线充电),必须明确指定 PD 协商结果该跟哪颗 charger 关联,否则 TCPM 写错节点,充电芯片根本收不到指令。
2.3 一套可直接参考的完整 DTS 配置
下面贴一套我实际用过的、完整的最小配置,覆盖了 RK3588 平台上单 Type-C 口 + 单颗 FUSB302 + 单颗 BQ25700 的典型组合。我把核心部分抽出来,方便参考:
/ { fusb302 { compatible = "fcs,fusb302"; status = "okay"; i2c-bus = <&i2c2>; irq-gpio = <&gpio1 RK_PA0 IRQ_TYPE_LEVEL_LOW>; pinctrl-names = "default"; pinctrl-0 = <&fusb302_int>; tusb422 = <0>; tcpc-role = "dual"; tcpc-power-supply-candidate = <1>; typec-power-supply = <&bq25700>; pd-data { status = "okay"; id = <0x29>; pd_id = <0x201>; source-pdo-size = <1>; source-pdo-list = <PDO_FIXED_5V_1A 0x0c>; sink-pdo-size = <3>; sink-pdo-list = <PDO_FIXED_5V_3A 0x0c>, <PDO_FIXED_9V_3A 0x0c>, <PDO_FIXED_12V_3A 0x0c>; }; }; }; &i2c2 { status = "okay"; clock-frequency = <400000>; }; &i2c0 { status = "okay"; bq25700: charger@6b { compatible = "ti,bq25700"; reg = <0x6b>; interrupt-parent = <&gpio0>; interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>; pinctrl-names = "default"; pinctrl-0 = <&charger_int>; ti,charge-current = <2000000>; ti,charge-voltage = <4352000>; ti,input-current = <2000000>; ti,input-voltage = <5000000>; ti,watchdog-timeout = <5000>; }; };这套配置里有一个容易被忽略的点,就是typec-power-supply = <&bq25700>这一条。它的作用是把 TCPC 节点和 charger 节点绑定起来,TCPM 在完成 PD 协商后,把功率更新到这个 charger 提供的 power_supply 设备上。有些平台资料里会写成别的名字,比如power-supply或pd-charger,但本质上都是在告诉 TCPM 去操作哪个 charger。
如果产品有多个 Type-C 口或者还有无线充电输入,就需要在 TCPM 的代码或策略里做输入源选择,这是一个更复杂的主题,我这里先不展开。单 Type-C 口的产品,上面的配置基本够用。
2.4 DTS 宏定义与内核接口对应关系
PDO 在设备树里用的宏,最终会编译成 32 位整数。以PDO_FIXED_5V_3A为例,它本质上是这样一个位域结构:
#define PDO_FIXED(mv, ma, flags) \ ((mv) | ((ma) << 10) | ((flags) << 28))展开后大概就是0x0002C05A这样的值。这个宏在include/dt-bindings/usb/pd.h或内核的usb/pd.h里定义,DTS 和内核驱动共用同一套定义,避免了手工编码 32 位数带来的出错风险。所以设备树里写PDO_FIXED_5V_3A和内核层面读出来的值是一一对应的。
顺带提一句,PD 协议除了固定电压 PDO,还有 PPS(可编程电源)APDO。PPS 允许系统在一定范围内动态调整电压电流,比如 5V-20V 连续可调,常用于高功率快充。RK3588 平台的内核也是支持的,配置时会有类似APDO_VARIABLE_5V_20V_5A的宏,但实际调 PPS 需要适配器端也支持 PPS 协议,而且 PPS 下电压切换对充电芯片的控制算法要求更高。如果产品没有明确的 PPS 需求,我个人建议在 DTS 里先不配 PPS,把固定 PDO 档位调好再说,不然很容易引入额外的不稳定因素。
3. 内核驱动适配与修改实战
3.1 驱动调用链分析
设备树配好之后,真正的软件逻辑在内核驱动里。常见的 RK3588 平台 Android 12 内核中,跟 Type-C PD 相关的代码路径有这么几块:
drivers/usb/typec/tcpm/fusb302.c:FUSB302 芯片驱动,实现tcpc_dev结构体里的 read/write/interrupt 等回调drivers/usb/typec/tcpm/tcpm.c:TCPM 核心,Type-C 状态机和 PD 协议状态机都在这里drivers/usb/typec/class.c:typec class 设备,向上层暴露 typec port、partner、cable 等 sysfs 接口drivers/power/supply/charger/xxx.c:具体 charger 芯片驱动drivers/power/supply/rockchip_typec.c或drivers/usb/typec/tcpm/tcpc_notify.c:平台相关的中转代码,把 TCPM 的 power supply 事件转发给 charger 驱动
调用链大概是:FUSB302 检测到插入中断后调用tcpci_irq()接口,TCPM 里通过tcpm_do_irq_work处理这个中断,进入tcpm_state_machine,开始跑类型 C 状态机。当状态机推进到SNK_DISCOVERY、SNK_WAIT_CAPABILITIES、SNK_SELECT_CAPABILITY等状态时,会调用tcpm_pd_send_request发送 PD 请求。PD 协商完成之后,TCPM 会调用tcpsm_set_vbus或tcpc_set_power_supply这些函数,最终把新的 VBUS 电压、电流限制值送到 charger 驱动。
charger 驱动这边,通常注册一个power_supply设备,type 是POWER_SUPPLY_TYPE_USB或者POWER_SUPPLY_TYPE_MAINS。TCPM 通过power_supply_set_property或者直接操作 charger 驱动的私有接口,更新POWER_SUPPLY_PROP_ONLINE、POWER_SUPPLY_PROP_VOLTAGE_MAX、POWER_SUPPLY_PROP_CURRENT_MAX等属性。charger 驱动收到这些属性变化后,内部状态机会重算所有限流值,并写入充电芯片寄存器。
BQ25700 内部有寄存器和状态机,需要周期性检查输入电压是否在合理范围,如果系统负载突变导致输入电压跌到阈值以下,BQ25700 需要降低输入电流或者设置最小输入电压限制。这个逻辑是 charger 驱动里的经典难点。
3.2 常见修改点有哪些
用户拿到的 SDK 大多已经带了一套默认的 DTS 和驱动,但做产品时几乎总要改几处。
第一处是 PDO 列表调整。客户提供的适配器支持什么档位,DTS 里的sink-pdo-list就要同步调整,否则会出现协商档位过高、充电不稳定,或者协商档位过低、充电功率不足的问题。这个修改不是简单的增删 PDO,还要注意顺序,因为 TCPM 默认会按列表顺序去请求。
第二处是充电曲线调整。BQ25700 这类芯片支持完整的充电管理逻辑,包括 pre-charge、fast-charge、float voltage、charge termination 等。RK3588 平台默认的充电电压电流参数,不一定是电池规格要求的最优值。比如电池浮充电压 4.4V,SDK 默认 4.352V,虽然也能充,但电池充不满;如果电池支持快充,充电电流也可以从 2A 提到 3A 甚至更高。这里要跟硬件工程师核对电池数据手册,稳妥起见还是按照电池规格逐一验证。
第三处是输入限流策略。PD 协商出 9V/3A 之后,实际输入限流不一定直接设为 3A,因为系统负载、充电芯片发热、电池温度等都会影响安全电流。很多产品会做一个“动态输入限流”策略:比如屏幕亮起时输入限流降低,息屏待机时才允许最大输入限流。这个逻辑通常放在 charger 驱动的set_property回调里,跟 TCPM 联动。
第四处是兼容性的适配器兼容处理。市面上不少 QC2.0/QC3.0 的适配器也实现了 Type-C 口的 DCP/CDP 模式,但不一定完整支持 PD 协议。那 TCPM 在拔掉适配器或适配器遇到异常时,就要能回退到默认的 5V 充电模式,这个回退逻辑如果不完善,产品可能在某几个特定适配器上出现无法充电或充得很慢的问题。
3.3 日志、节点与常见调试手段
内核驱动改完之后,怎么验证配置生效?光看系统设置里的“正在充电”完全不够。我建议优先打开这几类内核日志和节点:
TCPM 驱动有独立的 debug 开关。内核配置里打开CONFIG_TYPEC_DP_DBG或者直接在内核 cmdline 中传入tcpm.dbg=1,可以在dmesg里看到详细的 PD 状态切换记录。我实际最常用的命令:
# 查看 USB/PD 相关内核输出 dmesg | grep -E "tcpm|fusb|typec|charger|bq25700|pd|usb"日志里通常会出现类似这样的关键行:
[ 12.345678] tcpm: source capabilities: 5000mV 3000mA [ 12.346000] tcpm: sink capabilities: 5000mV 3000mA, 9000mV 3000mA [ 12.347111] tcpm: cc1=0 cc2=3 vbus=0 -> SNK_DISCOVERY [ 12.348222] tcpm: Got source caps: 5000mV, 9000mV, 12000mV [ 12.349333] tcpm: Requesting 9000mV 3000mA [ 12.351111] tcpm: PD_RESET, returning to SNK_UNATTACHED日志里如果停在Requesting 9000mV但没有出现VBUS changed或者适配器不响应,那多半是 PDO 匹配失败或适配器不支持该电压档位。此时会反复PD_RESET,日志会一直刷状态机重跑的信息。
除了 dmesg,sysfs 节点也是快速判断协商结果的好工具。插入适配器后,可以查看:
# 查看 typec port 当前连接状态 cat /sys/class/typec/port0/port_type cat /sys/class/typec/port0/power_role cat /sys/class/typec/port0/data_role cat /sys/class/typec/port0/partner/source_caps # 查看 charger 的输入限制和充电状态 cat /sys/class/power_supply/usb/voltage_max cat /sys/class/power_supply/usb/current_max cat /sys/class/power_supply/main_charger/statussource_caps是适配器宣告的能力,voltage_max和current_max是当前协商出的输入上限。这两组数值一对比,就能立刻看出协商档位是不是符合预期。如果voltage_max还是 5000000,说明 9V 档位根本没协商成功。
4. 常见问题与排查实录
4.1 问题速查表
我把调试中遇到的高频问题做成了速查表,按现象、可能原因、定位方法、解决办法四列整理,方便你按图索骥。
| 现象 | 可能原因 | 定位方法 | 解决办法 |
|---|---|---|---|
| 插适配器完全无充电反应 | TCPC 中断脚错误 / I2C 通不上 / 硬件 CC 上下拉不对 | dmesg 看 fusb302 是否 probe 成功、是否有 IRQ 触发 | 核对 DTS 里 interrupt 和 pinctrl;I2C 上先独立读寄存器验证通路 |
| 一直 5V 充电,无法协商到 9V/12V | 适配器不支持 PD / PDO 列表不匹配 / TCPM 未进入 PD 协商流程 | 查看 source_caps 是否只列出 5V | 确认适配器规格和 sink 能力一致;检查 TCPM firmware 版本 |
| 协商到 9V 后系统重启或保护 | charger 输入电压限制或系统负载超过了电源能力 | 示波器抓 VBUS 波形,查看 charger 寄存器 OVP/UVP 状态 | 降低 PDO 电流档位;调整 charger 输入限流;增加 VBUS 电容 |
| 充电电流远小于预期 | 输入限流被压低 / 系统负载吃掉电流 / 电池温度限制 | 查看 main_charger/type 的 current_max 与 charge_current | 检查动态限流策略;核对电池温度曲线 |
| 拔线后系统立即掉电关机 | VBUS 掉电时序差 / 充电路径未及时切换 / PMIC 掉电检测不对 | 抓拔线瞬间波形、看 dmesg 有没有 PD_DISCONNECT | 检查 TCPM 的 disconnect 处理,VBUS 掉电时及时切换到电池供电 |
dmesg 中fusb302: probe failed | I2C 地址错误 / 供电没上 / 总线挂死 | I2C 工具独立读地址 | 确认硬件电路,检查 DTS 中 reg 值和实际 I2C 地址 |
4.2 典型排查案例
我挑三个实际项目里印象比较深的排查过程,仔细写一写。
第一个是“DTS 里配了 9V/3A,但实际协商只能到 5V/2A”。当时用的是某品牌 65W 氮化镓适配器,支持标准的 PPS 和 5V/9V/12V/15V/20V 固定档。DTS 里 sink-pdo-size 写的是 3,配置了 5V/3A、9V/3A、12V/3A。结果 dmesg 里 source capabilities 确实列出了 5V、9V、12V、20V,但 TCPM 请求 9V 后适配器端没回应,反复 PD_RESET,系统最终回退到 5V/2A。后来查了半天,发现是一个很傻的坑:sink-pdo-list的每一项都应该带0x0c这样的高低字节标志,但我第三组 PDO 后面没带,导致内核解析出错。这类格式类问题,DTS 写完后一定要先用dtc编译确认字节码正确再烧到板子上。
第二个是“协商到 9V 后整机电流哗哗涨,charge 电流反而上不去”。这个案例背后是热设计问题:Type-C PD 协商拉到 9V/3A 之后,输入功率到 27W,整机系统负载在 10W 左右,理论上电池快充电流可以到 3A 以上。但实际测出来充电电流只有 1.2A。后来查日志发现,charger 驱动里的charge_current被电池温度曲线限制了,电池温度到了 45℃,被策略压低了充电电流。这不是软件 bug,而是产品定义里的温控策略,开发时如果不知道这层策略,很容易误判为驱动问题。
第三个是“拔线后系统直接关机”。这个案例很折腾,现象是适配器在系统满载时被拔掉,RK3588 整机立刻掉电重启。抓波形发现 VBUS 电压跌落的瞬间,系统负载还挂在 VBUS 侧,而 battery 的电能通路还没切换过来。后来在 charger 驱动里加了掉电中断快速切换逻辑:当检测到输入源拔出时,先把 VSYS 切换到电池供电,再处理 PD 断开事件。这个顺序必须在驱动里显式控制,否则 PMIC 的掉电检测比 TCPM 状态机快,还没来得及切电池,系统就已经欠压掉电了。
4.3 一些实在的调试经验
最后说几个调试过程中的通用经验,这些在很多平台上都适用。
第一个是善用 sysfs 节点,而不是一上来就抓协议分析仪。协议分析仪能精确看到 SOP 报文内容和时序,但对板级工程师来说成本太高、操作也繁琐。先用/sys/class/typec和/sys/class/power_supply这两个目录里的节点,快速判断协议走没走通、档位有没有协商成功,大多数问题在 sysfs 层面就能定位到 80%。只有遇到协议层疑难杂症时,再上 TCPDump 或者协议分析仪,效率反而更高。
第二个是把 DTS 的改动记录跟 git log 绑定,每一次 PDO 调整、每一个限流参数变化都写清楚 commit 说明。PD 充电涉及到电源策略和电池安全,跟其他驱动的调试不一样,参数改动如果靠“大概试一下”的心态,后面把电池充胀了或者把充电芯片烧了才后悔。我自己的习惯是每次改完参数,先做一轮 10 分钟满载充电测试,确认 VBUS、VSYS、电池电压都稳定之后,再做 5V/9V/12V 各档位的切换测试。
第三个是看重启前后差异。有些问题只在冷启动时出现,有些问题只在热重启后出现。比如 TCPC 芯片和 charger 芯片的 I2C 初始化时序,如果 PMIC 的某个 rail 供电晚于 I2C 总线初始化,charger 驱动 probe 时读寄存器会失败,系统可能进入一个异常的默认状态。遇到这种问题,dmesg 和 sysfs 都看不出明显错误,但热重启后完全正常,冷机必现,基本就是供电时序的问题,需要硬件工程师配合确认上电顺序,软件上通过 DTS 里 regulator 的regulator-boot-on和regulator-always-on配置来做初步规避。
第四个是强烈建议给充电链路加一层“黑盒测试脚本”,就是用 shell 脚本定期检查 sysfs 节点状态,在自动化测试或者远程调试时有奇效。比如惊讶到一个适配器的 source_caps 变化、charger 的 current_max 保持在预期值、电池电压在充电时缓慢上升等。把这层监控加在项目的编译产物里,哪怕只是跑一晚上,第二天翻 log 也能发现很多随机性充电问题,而不是一天到晚蹲在实验室里插拔适配器。
5. 后续扩展方向
Type-C PD 充电调通之后,很多衍生功能就顺理成章了。一方面可以做 OTG 反向供电,把 RK3588 设备变成一个大功率移动电源类型的外设;另一方面可以对接 DP Alt Mode,把 Type-C 口用于视频输出,这时候 PD 的 dual-role 切换和 DP 带宽协商会更有意思。如果整机还支持无线充电或其他 DC 输入,那电源路径管理(Power Path Management)和输入源切换(Input Source Selection)就会变成新挑战,而 PD 协商的参数也会参与这些调度。
根据我个人经验,第一次调通 PD 充电一定要把协议栈层次理清楚,多借助 sysfs 和 dmesg 观察,而不是盲目改驱动代码。DTS 里的 PDO 列表和电源策略配置,值得花一个星期时间去逐一推敲和实测,这段投入会在后面整机可靠性测试里省下大把时间。
这篇文章里的 DTS 片段和驱动调用链,都是我实际调试时用过的或观察到的通用实践,虽然具体寄存器数值还是要以你手里的硬件原理图和 SDK 版本为准,但整体框架和排查思路,换哪颗 charger、换哪种 TCPC 芯片都是通用的。希望这些内容能帮你少踩几个坑,让 Type-C PD 充电在 RK3588 平台上一把点亮。