4. OTG 设备状态机:主机模式、设备模式、角色切换过程

OTG 芯片说白了就是个“双面人”——既能在 A 口(主)和 B 口(从)之间横跳,还要在中间优雅地切换。很多工程师做 USB 开发时,觉得状态机就是一堆 if-else,结果上电握手机制没处理好,设备就卡在中间态了。我自己刚接触 OTG 时就吃过这亏,焊好电路板插到电脑上,D+ 和 D- 的电平死活不对——后来才发现是 ID 引脚状态没读准。

今天咱们把 OTG 状态机掰开揉碎了讲。你理解了状态迁移条件,后面调优传输才能“指哪打哪”。

4.1 OTG 设备的三大状态

OTG 设备本质上只有三种状态:主机模式(Host)、设备模式(Peripheral)和待定模式(Idle / Wait)。但实际芯片里会拆成 6~8 个子状态,典型的完整状态机包含:

  • Idle(空闲):设备未接入,供电/检测模块待机。
  • A_Host(A 设备作为主机):A 设备主动提供 VBus,枚举 B 设备。
  • A_Peripheral(A 设备作为外设):A 设备被切换为从设备,响应主机。
  • B_Peripheral(B 设备作为外设):B 设备默认从机,等待主机枚举。
  • B_Host(B 设备作为主机):B 设备通过 HNP 获得主机权,驱动 A。
  • SRP(会话请求协议):B 设备想发起通信,必须先请求 VBus。

我习惯把上面的状态画成一张箭头图——状态迁移条件就那么几个引脚信号。

OTG 设备状态机迁移图 OTG 设备状态机(A/B 双角色迁移) Idle A_Host A_Peripheral B_Peripheral B_Host SRP ID = 0 ID = 1 HNP 请求 B 接管主机 HNP 触发 释放总线 SRP 请求 会话结束 说明:ID 引脚电平决定初始角色;HNP 请求产生角色切换;SRP 用于 B 设备发起会话。

4.2 状态迁移的物理层条件

状态迁移靠什么?你想想看,无非三个东西:ID 引脚电平、VBus 检测、DP/DM 上行数据握手。我把它们整理成一张表格,方便你对照。

迁移动作 触发条件 典型延迟 常见踩坑点
Idle → A_Host ID = 0(A 口接地)且 VBus 有效 ≤ 10 ms ID 引脚滤波不好,误触 B_Host
Idle → B_Peripheral ID = 1(B 口浮空) ≤ 5 ms B 口默认上拉电阻未配置
A_Host → A_Peripheral HNP 过程:B 将 D+ 上拉,A 检测并释放 VBus 100 ~ 500 ms A 未及时关闭 VBus,B 无法拉高 D+
B_Peripheral → B_Host HNP 收到主机权,B 开始提供 VBus ≤ 50 ms B 的 VBus 驱动能力不足,掉电压
B_Host → B_Peripheral B 主动释放 VBus,A 恢复主机角色 ≤ 20 ms 释放时序不干净,A 检测毛刺
任意态 → Idle VBus 掉电超过 t_SRP_TIMER(SS 为 10ms) 100 ~ 200 ms 去耦电容放电时间过长

我的一点小习惯:做板级调试时,先把 ID 引脚用跳线帽拉到地或浮空,配合逻辑分析仪抓 DP/DM 电平,能快速判断状态机卡在哪一步。你有空可以试试,比自己读寄存器快得多。

4.3 角色切换的全过程(HNP 详解)

角色切换不是简单的“交换引脚”,而是靠 HNP(Host Negotiation Protocol) 完成的。说白了就是:B 设备想让 A 设备把主机权让出来。我曾在某款双外设的 hub 项目里,因为 HNP 的时序没对齐,A 设备永远不肯交出总线。后来查了 datasheet,发现是 DP 上下拉电阻的切换太快了。

HNP 的标准步骤是这样:

  1. 初始配置:A 口(ID=0)作为主机,提供 VBus;B 口(ID=1)作为外设,默认不拉 DP。
  2. B 发请求:B 设备将 D+ 上拉(Full-Speed 指示),同时保持 D- 浮空。A 设备检测到上行信号后,开始准备切换。
  3. A 释放 VBus:A 设备关闭 VBus 输出,进入 Peripheral 模式。此时总线电压会从 5V 下降,B 设备必须等待 VBus 降到 4.4V 以下才能接管。
  4. B 接管:B 设备检测到 VBus 低于阈值 (4.4V),启动自己的 VBus 电源,并将 DP 的状态从上拉改为下拉(表示自己已是主机)。同时等待 A 设备枚举完成。
  5. 新的枚举:A 设备作为外设,响应 B 主机的设备描述符请求。角色切换完成。

为什么我在项目中遇到切换失败?

有一次我调试的 OTG 外设,B 设备在步骤 2 把 DP 上拉后,A 设备收到了但没立即关闭 VBus——原因是 A 设备的固件里有一个 200ms 的软件延时。而 B 设备在 50ms 内没看到 VBus 下降,就认为 A 拒绝切换,于是超时退出。后来我把 A 端的延时砍到 10ms,一切正常。所以,HNP 的时间窗很窄:从 B 拉高 DP 到 A 关闭 VBus,建议控制在 50ms 以内。

4.4 会话请求协议(SRP)细节

SRP 是 B 设备的“敲门砖”。如果没有主机在场(VBus 为 0),B 设备想发起通信,就必须先请求 A 设备提供 VBus。这个过程分为 SRP 数据线脉冲和 SRP VBus 脉冲。

脉冲类型 时序长度 作用 我的提醒
数据线脉冲(DP 上拉) 5 ~ 10 ms 通知 A 设备:B 想说话 脉宽太短 A 收不到,太长浪费功耗
VBus 脉冲 20 ~ 30 ms 采集 VBus 上的电流,证明 B 有能力驱动总线 VBus 拉高时间不够,A 认为无效

SRP 结束后,A 设备会尝试打开 VBus 并检测 DP/DM 的状态。如果 B 设备已经按上述时序完成了脉冲,A 就会进入 A_Host 状态去枚举 B。否则 A 超时回到 Idle。

我曾经掉过的坑:SRP 的 VBus 脉冲需要至少 20ms 的持续高电平。有一次我踩了这个线——只给了15ms——结果 A 设备因为电源管理模块的采样周期是 10ms,刚好没采到高电平,SRP 失败。所以,算上硬件抖动的余量,我个人建议脉冲时间给到 25ms 以上。

4.5 状态机的软件实现要点

写 OTG 状态机的时候,我习惯用状态表+统一的中断入口。下面给一个简化的伪代码框架(基于 STM32 OTG_FS):

// 状态枚举
typedef enum {
    OTG_STATE_IDLE,
    OTG_STATE_A_HOST,
    OTG_STATE_A_PERIPHERAL,
    OTG_STATE_B_PERIPHERAL,
    OTG_STATE_B_HOST,
    OTG_STATE_SRP
} otg_state_t;

// 主循环
while(1) {
    current_state = otg_read_state();
    switch(current_state) {
        case OTG_STATE_IDLE:
            if (ID_PIN == 0 && VBUS_VALID) {
                otg_set_state(OTG_STATE_A_HOST);
                otg_enable_host();
            } else if (ID_PIN == 1) {
                otg_set_state(OTG_STATE_B_PERIPHERAL);
                otg_enable_peripheral();
            }
            break;
        case OTG_STATE_A_HOST:
            if (HNPRequestDetected()) {
                otg_release_vbus();
                otg_set_state(OTG_STATE_A_PERIPHERAL);
            }
            break;
        case OTG_STATE_B_PERIPHERAL:
            if (HNPTriggered()) {
                otg_provide_vbus();
                otg_set_state(OTG_STATE_B_HOST);
            }
            break;
        // 其余状态类似...
        default:
            otg_set_state(OTG_STATE_IDLE);
    }
    // delay 1ms or wait for interrupt
}

嗯,这里要注意:状态切换必须配合硬件中断。轮询方式容易错过 ID 引脚上升/下降沿,导致状态机抖动。我在实际项目中全部用上升/下降双边沿触发中断,再加上 5ms 去抖动计时器,效果稳定。

4.6 调优检查清单

做 OTG 传输优化时,你可以按这个清单排查状态机:

  • ID 引脚:有没有加上拉/下拉电阻?电平变化是否被滤波消抖?
  • VBus 检测:阈值设置是否正确(一般 4.4V/4.75V 判断)?去耦电容大小是否影响放电时间?
  • HNP 时序:B 拉高 DP 到 A 关闭 VBus 的间隔是否在 50ms 内?A 释放 VBus 后 B 接管是否在 100ms 内完成?
  • SRP 脉冲:数据线脉宽够不够 5ms?VBus 脉宽有 25ms 以上吗?
  • 软件功耗:处于 Idle 时,是否关闭了不用的时钟/外设?

最后补一句经验之谈:你如果第一次接触 OTG,别在状态机里堆太多分支。先让“Idle → A_Host → A_Peripheral”这个单向链路跑通,再扩展 B 路径。稳扎稳打,比上来就搞全双工切换要好得多。