第四章:状态机总览——状态机在PD协议中的角色、DRP/DFP/UFP角色定义

好,咱们进入第四章。这一章我打算把PD协议里最核心的骨架——状态机,给你讲透。说白了,整个USB PD的通信过程,就是一堆状态机在跑来跑去。你搞懂了状态机,就等于拿到了PD协议的钥匙。

4.1 状态机在PD协议中的角色

先问个问题:为什么PD协议非要搞个状态机?

嗯,原因其实很简单。PD协议要处理的事情太复杂了——电压协商、角色切换、线缆检测、错误恢复……如果不用状态机,代码会乱成一锅粥。我见过一些新手写的PD代码,全是if-else嵌套,改一个bug能引出三个新bug。那场面,真是惨不忍睹。

状态机的作用,说白了就是三件事:

  • 明确当前在干什么——比如当前是“等待能力信息”状态,还是“协商电压”状态
  • 规定下一步能干什么——从当前状态只能跳到合法的下一个状态
  • 处理异常情况——超时了怎么办?收到非法消息怎么办?状态机里都有对应的处理路径

我个人习惯把PD协议的状态机比作一个地铁线路图。每个站就是一个状态,你只能沿着轨道走,不能乱跳。一旦脱轨(协议违规),就得回到起点重新来。

核心要点:USB PD规范定义了三种主要的状态机——端口提供者(Source)、端口消费者(Sink)、以及双角色端口(DRP)。每种角色都有自己的状态迁移图。

4.2 DFP、UFP、DRP——三种角色的定义

这三个缩写,你肯定见过。我刚开始学PD的时候,也经常搞混。咱们一个一个说清楚。

4.2.1 DFP(Downstream Facing Port)——下行端口

DFP,说白了就是“供电方”。它负责提供电压,是电源那一端。比如你的笔记本充电器,里面的PD芯片就是DFP角色。

DFP的特点:

  • 默认提供Vbus电压(通常是5V)
  • 负责发送能力信息(Source Capabilities)
  • 响应Sink的请求,调整输出电压和电流
  • 可以主动断开连接或切换角色

我在项目中遇到过一个问题:DFP在发送能力信息后,Sink迟迟不回复。这时候DFP应该怎么办?答案是启动超时定时器,超时后重发或者断开。这个细节很多新手会忽略。

4.2.2 UFP(Upstream Facing Port)——上行端口

UFP就是“用电方”。它从DFP那里获取电能。比如你的手机,插入充电器时就是UFP角色。

UFP的特点:

  • 初始时不提供Vbus,只检测Vbus是否存在
  • 收到DFP的能力信息后,从中选择一组电压/电流请求
  • 可以请求角色互换(比如从UFP变成DFP)
  • 负责检测线缆的CC引脚状态

这里有个坑,我曾经踩过:UFP在请求电压时,不能随便选一个值就发出去。必须按照规范里的“功率规则”来选。比如你选了一个5V/3A的配置,但你的线缆只支持3A,那就得降级。否则线缆会过热,严重的话会烧掉。

警告:UFP请求的功率不能超过DFP提供的能力范围,也不能超过线缆的承载能力。这是PD协议里最容易出问题的地方之一。

4.2.3 DRP(Dual Role Port)——双角色端口

DRP就厉害了。它既能当DFP,也能当UFP。说白了就是“可攻可守”。比如现在的Type-C笔记本,插上充电器时它是UFP(用电),插上U盘时它变成DFP(供电)。

DRP的工作方式:

  • 在CC引脚上交替发送“我是DFP”和“我是UFP”的信号
  • 这个交替周期叫“DRP切换周期”,通常是50ms到100ms
  • 当检测到对方设备后,根据策略决定最终角色
  • 可以通过协议消息请求角色互换

你想想看,DRP的代码实现比单纯的DFP或UFP复杂得多。因为它要同时处理两种角色的状态机。我早期做DRP实现时,就犯过一个低级错误——状态机切换时忘记清理定时器,导致超时中断乱飞。嗯,从那以后我写状态机都会先画一张完整的状态迁移图。

4.3 三种角色的状态机对比

咱们用一张表来对比一下:

特性 DFP UFP DRP
初始Vbus 提供5V 不提供 取决于角色
CC引脚行为 上拉电阻(Rp) 下拉电阻(Rd) 交替Rp/Rd
主要状态数 约12个 约10个 约20个(含切换)
典型应用 充电器、扩展坞 手机、平板 笔记本、移动电源
角色切换 可切换为UFP 可切换为DFP 原生支持

4.4 状态机在代码中的体现

说了这么多理论,咱们看看实际代码怎么写。我一般用枚举定义状态,用switch-case实现状态机。给你看个简化版的例子:

// 定义DFP的主要状态
typedef enum {
    DFP_STATE_DISABLED,        // 禁用状态
    DFP_STATE_IDLE,            // 空闲状态
    DFP_STATE_SRC_CAP_SENT,    // 已发送能力信息
    DFP_STATE_NEGOTIATION,     // 协商中
    DFP_STATE_READY,           // 就绪状态(供电中)
    DFP_STATE_ERROR_RECOVERY   // 错误恢复
} dfp_state_t;

// 状态机主循环
void dfp_state_machine(dfp_event_t event, void *param) {
    switch (current_state) {
        case DFP_STATE_IDLE:
            if (event == EVENT_CC_CONNECTED) {
                // 检测到设备接入,发送能力信息
                send_source_capabilities();
                current_state = DFP_STATE_SRC_CAP_SENT;
                start_timer(TIMER_SRC_CAP_WAIT, 100); // 100ms超时
            }
            break;
            
        case DFP_STATE_SRC_CAP_SENT:
            if (event == EVENT_GET_SINK_CAP) {
                // 收到Sink的能力请求
                send_sink_capabilities();
            } else if (event == EVENT_REQUEST) {
                // 收到功率请求
                process_power_request((request_t *)param);
                current_state = DFP_STATE_NEGOTIATION;
            } else if (event == EVENT_TIMEOUT) {
                // 超时处理——我曾经在这里吃过亏
                // 不能直接重发,要先检查CC状态
                if (cc_is_stable()) {
                    send_source_capabilities();
                    restart_timer(TIMER_SRC_CAP_WAIT, 100);
                } else {
                    current_state = DFP_STATE_ERROR_RECOVERY;
                }
            }
            break;
            
        // ... 其他状态处理
    }
}

小技巧:写状态机时,我建议每个状态都加上一个“进入动作”和“退出动作”。比如进入IDLE状态时关闭所有定时器,退出时开启。这样能避免很多状态残留问题。

4.5 角色选择策略——DRP的决策逻辑

DRP设备在接入时,怎么决定自己当DFP还是UFP?这里面有讲究。

常见的策略有:

  • 固定策略:比如笔记本默认当DFP,除非对方也是DFP才切换
  • 优先级策略:根据设备类型决定,比如充电器优先当DFP
  • 电池电量策略:电量低时当UFP(充电),电量高时当DFP(放电)
  • 用户选择策略:通过按键或软件设置手动选择

我记得有一次做移动电源项目,用的就是电池电量策略。当移动电源电量低于20%时,强制切换到UFP角色给自己充电。但这里有个坑——如果同时插着两个设备,角色切换会导致另一端的设备掉电。后来我们加了个延迟切换机制,才解决了这个问题。

4.6 本章小结

这一章咱们把状态机的角色讲清楚了。总结一下:

  • 状态机是PD协议的骨架,没有它代码就是一盘散沙
  • DFP是供电方,UFP是用电方,DRP是两者都能当
  • DRP的实现最复杂,需要同时维护两套状态机
  • 写状态机代码时,注意超时处理和状态清理

下一章咱们会深入DFP的状态机细节,把每个状态怎么迁移、怎么处理异常都掰开揉碎了讲。到时候我会拿一个实际项目的bug案例出来分析,保证让你印象深刻。

好,今天就到这里。有什么问题,咱们下节课见。