app_power.c 学习笔记:从端口状态机到 DCDC 调压链路
app_power.c 学习笔记:从端口状态机到 DCDC 调压链路
本文是对 SDK 中
app_power.c电源控制逻辑的一次学习整理,重点围绕pwr_process()、drv_protocol_power_get()、drv_pwr_adjust()三个函数,梳理从协议功率获取到 DCDC 调压执行的完整链路。
一、学习目标
今天主要复习并梳理app_power.c中电源控制主线,目标是搞清楚三个问题:
- 系统什么时候开电、关电、进入 5V、安全握手、快充调压?
- 协议层协商出来的电压、电流是怎么传到 power 层的?
- power 层拿到目标电压电流后,如何加工并下发给 DCDC?
最终形成的完整链路如下:
协议层 / 快充协议 ↓ drv_protocol_power_get() ↓ pwr_process() ↓ drv_pwr_adjust() ↓ dev_ctrl() ↓ DCDC driver ↓ 硬件执行二、整体电源控制主线
这套 SDK 的电源控制不是某一个函数单独完成的,而是多个模块分层配合:
main.c ↓ 创建任务 / 初始化模块 ↓ app_port.c / app_prot.c ↓ 检测 Type-C / PD / 端口状态 ↓ app_power.c ↓ 根据端口状态控制电源 ↓ app_protect.c ↓ 异常保护 ↓ dev_ctrl() ↓ DCDC / MOS / ADC / GPIO 等底层驱动可以用一句话概括:
Port 负责判断状态,Power 负责执行电源策略,Protect 负责异常保护,Driver 负责真正操作硬件。
三、pwr_process():app_power 的电源状态机核心
pwr_process()是今天学习的重点之一。它的核心作用是:
根据当前端口状态
_status,决定当前端口应该关电、开 5V、等待握手、进入快充调压,还是保护关断。
函数开头会读取几个关键状态:
port_type_t_type=app_port_get_type(hpwr->id);port_state_t_status=app_port_get_status(hpwr->id);port_power_role_t_ppr=app_port_get_ppr(hpwr->id);port_protocol_t_protocol=app_port_get_protocol(hpwr->id);这几个变量决定了后续动作:
| 变量 | 作用 |
|---|---|
_status | 当前端口状态,例如空闲、拔出、握手、快充、保护挂起 |
_ppr | 当前电源角色,例如 Source、Sink、Bypass |
_protocol | 当前协议类型,例如 PD、QC、FCP、SCP、UFCS、VOOC |
pwr_process()的核心结构是:
switch(_status){casePORT_STATE_IDLE:break;casePORT_STATE_UNATTACHED:break;casePORT_STATE_VBUS_SAFE:break;casePORT_STATE_HANDSHAKE:break;casePORT_STATE_FAST_CHARGE:break;casePORT_STATE_ATTACHED_OCP_HANG:break;}也就是说,它本质上是一个电源状态机执行函数。
四、pwr_process()中几个重要状态
1.PORT_STATE_IDLE:空闲状态
空闲状态下,主要是清理当前端口的电源记录:
h_power[hpwr->id].curr_pwr.vol=0;h_power[hpwr->id].curr_pwr.curr=0;hpwr->vsafe5v_wait=false;可以理解为:
端口当前没有电源动作 ↓ 清空电压、电流记录 ↓ 等待下一次插入或状态变化2.PORT_STATE_UNATTACHED:拔出状态
当端口拔出时,代码会执行:
drv_pwr_vsafe0v(hpwr);app_port_set_status(hpwr->id,PORT_STATE_IDLE);含义是:
设备拔出 ↓ 关闭 VBUS ↓ 卸放到 0V ↓ 状态回到 IDLE这里的重点是:拔出后不能让 VBUS 残留高电压,所以要执行关断和卸放。
3.PORT_STATE_VBUS_SAFE:安全 5V 阶段
这个阶段不是快充,而是快充前的安全准备阶段。
Source 放电时
Source 表示当前端口对外供电。流程大致是:
进入 VBUS_SAFE ↓ DCDC 先建立安全 5V ↓ 检测 VBUS 是否达到安全范围 ↓ 打开 Source MOS ↓ 进入 HANDSHAKE核心动作包括:
drv_pwr_vsafe5v_src(hpwr);app_power_src_sw_on(hpwr->id);app_port_set_status(hpwr->id,PORT_STATE_HANDSHAKE);这里不是一上来就快充,而是先保证 5V 安全输出。
Sink 充电时
Sink 表示当前设备被外部充电器供电。流程大致是:
进入 VBUS_SAFE ↓ 准备接收外部 5V ↓ 确认 VBUS 没有异常高压 ↓ 打开 Sink MOS ↓ 进入 HANDSHAKE核心动作包括:
drv_pwr_vsafe5v_snk(hpwr);app_power_snk_sw_on(hpwr->id);app_port_set_status(hpwr->id,PORT_STATE_HANDSHAKE);总结一句话:
VBUS_SAFE 阶段的核心作用是先建立安全 5V,为后续协议握手和快充调压做准备。
4.PORT_STATE_HANDSHAKE:协议握手阶段
进入HANDSHAKE时,端口已经有了安全 5V,但协议不一定已经完成快充协商。
因此这里通常先使用默认 5V 和默认电流:
_power.vol=POWER_VSAFE_5V;不同角色下默认电流不同:
if(_ppr==PPR_SRC_SINGLE){_power.curr=POWER_CURR_3A3;}elseif(_ppr==PPR_SNK_SINGLE){_power.curr=POWER_CURR_1A5;}可以理解为:
HANDSHAKE 阶段 ↓ 快充还没真正开始 ↓ 先用默认 5V 维持供电或充电这里也可能调用drv_pwr_adjust(),但目的不是进入快充,而是维持默认 5V 电源状态。
5.PORT_STATE_FAST_CHARGE:快充调压阶段
这个阶段才是真正进入快充调压。
流程如下:
进入 FAST_CHARGE ↓ 根据 _protocol 判断当前协议类型 ↓ drv_protocol_power_get() 获取协议目标电压电流 ↓ 如果获取成功 ↓ drv_pwr_adjust() 调整 DCDC 电压电流核心代码逻辑是:
_ret=drv_protocol_power_get(hpwr,(port_protocol_t)(1<<i),_ppr,&_power);if(_ret==ERR_OK){drv_pwr_adjust(hpwr,_ppr,&_power);}所以快充阶段的关键链路是:
协议层协商结果 ↓ drv_protocol_power_get() ↓ _power.vol / _power.curr ↓ drv_pwr_adjust() ↓ DCDC 调压调流6. 保护挂起状态
例如:
casePORT_STATE_ATTACHED_OTP_HANG:casePORT_STATE_ATTACHED_UVP_HANG:casePORT_STATE_ATTACHED_OCP_HANG:{app_power_sw_off(hpwr->id);break;}这些状态表示端口处于保护挂起:
| 状态 | 含义 |
|---|---|
OTP_HANG | 过温保护挂起 |
UVP_HANG | 欠压 / 过放保护挂起 |
OCP_HANG | 过流 / 短路保护挂起 |
这说明app_protect.c和app_power.c之间不是孤立的。
典型流程是:
app_protect 检测到异常 ↓ 设置端口状态为 xxx_HANG ↓ pwr_process() 看到 HANG 状态 ↓ 关闭电源开关五、drv_protocol_power_get():从协议层获取目标功率
drv_protocol_power_get()的作用可以简单理解为:
问协议层一句话:当前应该使用几伏几安?
它不负责协议协商,也不直接调 DCDC,只负责把协议层已经得到的结果取出来,填到:
power->vol power->curr函数接口如下:
staticerrno_tdrv_protocol_power_get(app_power_t*hpwr,port_protocol_t_prot,port_power_role_t_ppr,app_bus_power_t*power)参数含义:
| 参数 | 含义 |
|---|---|
hpwr | 当前端口的 power 句柄 |
_prot | 当前协议类型,例如 PD、QC、FCP、SCP、UFCS |
_ppr | 当前电源角色,Source 或 Sink |
power | 输出参数,用来保存协议目标电压、电流 |
1. PD 协议分支
PD 分支的核心流程是:
读取 PD 当前电源切换状态 ↓ 如果正在切换电压 / 电流,返回 ERR_ERR ↓ 如果状态可用,读取 PD 协商功率 ↓ 填入 power->vol / power->curr ↓ 根据 Source Fixed PDO 情况做电流补偿 ↓ 返回 ERR_OK核心代码是:
_ps_status=ps_pd_power_transition_status_get(id2pdport(hpwr->id));if(_ps_status==PS_SRC_PRE_TRANSITION||_ps_status==PS_SNK_TRANSITION){returnERR_ERR;}ps_pd_power_transition_get(id2pdport(hpwr->id),(power_pd_t*)power);其中:
ps_pd_power_transition_get(...,(power_pd_t*)power);可以理解为:
从 PD 协议层拿出已经协商好的电压、电流 ↓ 写入 power 结构体例如 PD 协商结果是 9V / 3A,则:
power->vol = 9000 power->curr = 3000如果当前是 Source 且 PD 是 Fixed PDO,还会执行:
power->curr+=400;这属于电流 offset 补偿,用来应对硬件限流误差,避免过早触发限流。
2. 非 PD 协议分支
QC、FCP、SCP、UFCS、VOOC 等协议,大多通过:
prot_mp_ctrl(id2mpport(hpwr->id),PROT_IO_CTRL_CMD_MP_DISCHRG_POWER_TRANSITION,(void*)power);来获取协议目标电压电流。
可以理解为:
向多协议 MP 模块询问: 当前协议协商出来的是几伏几安?不同协议还会根据自身特点做限流或补偿,例如:
| 协议 | 处理逻辑 |
|---|---|
| QC/QC30 | Sink 最大 3A,Source 电流加 offset 后封顶 |
| AFC/FCP | 12V 档限制电流,避免功率过高 |
| SCP | 高压档限制电流,低压档允许更大电流,接近恒功率控制 |
| UFCS | 高压档电压补偿,电流小幅补偿 |
| VOOC | 电流最大限制到 5.6A |
六、drv_pwr_adjust():把目标功率加工成 DCDC 设置值
drv_pwr_adjust()的作用是:
把协议或策略给出的目标电压、电流,结合 Source/Sink 角色进行加工,然后通过
dev_ctrl()下发给 DCDC。
它和drv_protocol_power_get()的区别是:
| 函数 | 作用 |
|---|---|
drv_protocol_power_get() | 从协议层拿目标电压电流 |
drv_pwr_adjust() | 把目标电压电流加工成 DCDC 实际设置值 |
函数中有两个重要变量:
app_bus_power_t*_p_pwr_info=(app_bus_power_t*)arg;app_bus_power_t_pwr_info;含义如下:
| 变量 | 含义 |
|---|---|
_p_pwr_info | 原始目标值,来自协议或策略 |
_pwr_info | 加工后的实际 DCDC 设置值 |
七、Source 放电:电压往上补
Source 表示当前端口对外供电。
在 Source 放电时,主要考虑两个问题:
- 电流限流补偿
- IR Drop 线损补偿
1. 电流限流补偿
代码中会根据平台和电流档位对目标电流进行补偿,例如:
_pwr_info.curr=_p_pwr_info->curr;_pwr_info.curr+=500;这类补偿的目的不是随意超协议输出,而是为了修正硬件限流误差,避免实际电流还没达到协议目标就提前限流。
2. IR Drop 电压补偿
Source 对外供电时,线材、MOS、PCB 走线都会产生压降。
例如协议目标是 9V,但由于线损,负载端可能只收到 8.8V。
因此代码会根据电流计算补偿电压:
_vol_irdrop0=app_power_get_ibus(PORT_USB_PORT_1)/10;_vol_irdrop1=app_power_get_ibus(PORT_USB_PORT_2)/10;注释中说明:
1A 补 100mV所以:
1A → 补 100mV 2A → 补 200mV 3A → 补 300mV最终:
_pwr_info.vol=_p_pwr_info->vol+_vol_irdrop;即:
协议目标电压 + 线损补偿 = DCDC 实际设置电压例如:
协议目标:9V 线损补偿:0.2V DCDC 设置:9.2V八、Sink 充电:电压往下让,电流慢慢升
Sink 表示当前设备被外部充电器供电。
Sink 充电时和 Source 放电完全不同。
1. 充电自适应电压
Sink 充电时,DCDC 不会直接设置成外部输入电压,而是设置得低一点:
if(_p_pwr_info->vol>POWER_VOL_15V){_pwr_info.vol=_p_pwr_info->vol-POWER_VOL_1V5;}elseif(_p_pwr_info->vol>POWER_VOL_5V5){_pwr_info.vol=_p_pwr_info->vol-POWER_VOL_1V;}elseif(_p_pwr_info->vol>POWER_VOL_3V){_pwr_info.vol=_p_pwr_info->vol-POWER_VOL_0V5;}else{_pwr_info.vol=POWER_VOL_4V4;}可以理解为:
外部输入电压要比内部 DCDC 设置电压高一点,这样 DCDC 才有调节空间。
例如:
| 外部协议电压 | DCDC 实际设置 | 留出调节余量 |
|---|---|---|
| 20V | 18.5V | 1.5V |
| 12V | 11V | 1V |
| 9V | 8V | 1V |
| 5V | 4.5V | 0.5V |
核心理解:
Source 放电:电压往上补,保证负载端电压够 Sink 充电:电压往下让,保证 DCDC 有调节空间2. 充电电流缓增
Sink 充电时,电流也不会一下子拉满,而是通过operate_curr慢慢爬升:
_operate_curr=hpwr->operate_curr;if((_operate_curr<_pwr_info.curr)&&(ticker_out(hpwr->pwr_timer,TIME_PWR_CURRENT_STEP)==true)){hpwr->pwr_timer=ticker_read();_operate_curr+=CFG_PWR_CURRENT_STEP;}例如目标电流是 3000mA,实际过程可能是:
500mA ↓ 600mA ↓ 700mA ↓ ... ↓ 3000mA这样做的目的:
避免输入电压被瞬间拉低 避免 DCDC 冲击 避免 MOS 电流冲击 避免误触发 OCP 避免充电器误判异常总结一句话:
Sink 充电不是暴力拉电流,而是先留电压余量,再让电流慢慢爬升。
九、dev_ctrl():从 app 层到 driver 层的命令通道
当drv_pwr_adjust()算出最终要设置的电压、电流后,会调用:
dev_ctrl(hpwr->fd_dev_dcdc,DEV_IO_CTRL_CMD_DCDC_SET_VBUS,(void*)&_pwr_info.vol);dev_ctrl(hpwr->fd_dev_dcdc,DEV_IO_CTRL_CMD_DCDC_SET_IBUS,(void*)&_operate_curr);这里的含义是:
找到当前端口绑定的 DCDC 设备 ↓ 发送 SET_VBUS / SET_IBUS 命令 ↓ 由 DCDC driver 真正执行所以dev_ctrl()不是最终控制硬件的函数,而是 app 层通向 driver 层的统一命令入口。
十、今天打通的完整链路
今天最终理解的完整链路如下:
PD / QC / FCP / SCP / UFCS / VOOC 协议层 ↓ 协议层得到目标电压电流 ↓ drv_protocol_power_get() ↓ 写入 power->vol / power->curr ↓ pwr_process() ↓ 判断当前端口状态是否允许调电 ↓ drv_pwr_adjust() ↓ 根据 Source / Sink 做补偿、限流、缓启动 ↓ dev_ctrl() ↓ 下发 SET_VBUS / SET_IBUS ↓ DCDC driver ↓ 真正控制硬件也可以压缩成一句话:
协议层决定目标功率,pwr_process 判断状态,drv_protocol_power_get 获取目标,drv_pwr_adjust 加工目标,dev_ctrl 下发命令,DCDC 驱动执行硬件动作。
十一、今日学习收获
今天主要掌握了以下内容:
pwr_process()是app_power.c的电源状态机核心。VBUS_SAFE阶段是快充前的安全 5V 准备阶段。HANDSHAKE阶段还不是真正快充,主要是默认 5V 供电或充电。FAST_CHARGE阶段才会从协议层获取目标功率,并进入真正调压调流。drv_protocol_power_get()负责从协议层读取目标电压电流。drv_pwr_adjust()负责把目标电压电流加工成 DCDC 实际设置值。- Source 放电时,电压往上补,主要是补线损和限流误差。
- Sink 充电时,电压往下让,主要是给 DCDC 留调节空间。
- Sink 充电电流需要缓慢爬升,避免冲击和误触发保护。
dev_ctrl()是 app 层到 driver 层的统一命令入口。
十二、下一步学习方向
下一步可以继续看:
drv_protocol_reset_dtc()drv_ac_adapter_aout_dtc()pwr_telemetry()重点关注:
协议复位如何检测? 适配器掉压 / 掉档如何判断? power 层如何结合采样值做动态控制? protect 保护状态如何影响 power 状态机?当前阶段已经基本打通了:
端口状态 → 协议功率 → power 决策 → DCDC 调压 → 底层执行后面需要继续补强的是:
异常检测 → 协议复位 → 掉档处理 → protect 和 power 的联动