一、客户问题

  • 现象:客户端 App 在用户态读取 device 的连接状态时,出现以下异常:

    • 拔出线缆:用户态没有获取到 disconnect,获取到了suspend
    • 插入线缆:用户态首先获取到 disconnect,再获取到 connect
    • 用户需求:断开线缆时能立马在用户态获取到disconnect
  • 客户对齐:明确客户读取的是用户态的哪个状态?

    • udc state:cat /sys/class/udc/f8180000.usb/state (udc驱动状态,usb_gadget_set_state接口更新)

    • gadgetfs:usb/gadget/legacy/inode.c(gadget设备状态,call_gadget接口更新),比如当dwc2状态改变比如suspend,调用call_gadget(hsotg, suspend),执行流程如下:

      1
      call_gadget(hsotg, suspend) -> composite_suspend -> gadget设备的suspend -> gadgetfs_suspend

      inode.c上报状态实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  /* 最终inode.c中的gadgetfs_suspend,
* 通过next_event()上报事件到userspace,
* 用户态读取对应节点即可获取gadget设备状态 */
static void gadgetfs_disconnect (struct usb_gadget *gadget)
{
struct dev_data *dev = get_gadget_data (gadget);
...
INFO (dev, "disconnected\n");
next_event (dev, GADGETFS_DISCONNECT);
ep0_readable (dev);
...
}

static void gadgetfs_suspend (struct usb_gadget *gadget)
{
struct dev_data *dev = get_gadget_data (gadget);
...
INFO (dev, "suspended from state %d\n", dev->state);
next_event (dev, GADGETFS_SUSPEND);
ep0_readable (dev);
...
}

static struct usb_gadget_driver gadgetfs_driver = {
....
.disconnect = gadgetfs_disconnect,
.suspend = gadgetfs_suspend,
...
};
  • 客户对齐:使用的是gadgetfs上报的gadget设备状态,不是udc驱动层的状态

二、原因分析

  1. dwc2 USB IP (device 模式) 下,硬件 没有寄存器可以直接判断线缆是否断开

    gintsts_register

  2. 线缆移除时,device 只会产生 suspend 中断,中断函数回调 gadget 注册的 suspend

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static void dwc2_handle_usb_suspend_intr(struct dwc2_hsotg *hsotg)
    {
    ......
    if (!hsotg->params.no_clock_gating)
    dwc2_gadget_enter_clock_gating(hsotg);

    /* Change to L2 (suspend) state before releasing spinlock */
    hsotg->lx_state = DWC2_L2;

    /* Call gadget suspend callback */
    call_gadget(hsotg, suspend);
    ......
    }
  3. 插入线缆后,触发 dwc2_hsotg_irq: USBRst,调用 dwc2_hsotg_disconnect,触发gadget回调,通知gadgetfs disconnect,最后枚举完又会调用到connect相关函数,触发底层gadgetfs通知用户层connect,这就是为什么插入线缆,在用户层检测到先来一个disconnect,再检测到connect的原因:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    static irqreturn_t dwc2_hsotg_irq(int irq, void *pw)
    {
    ......
    if (gintsts & (GINTSTS_USBRST | GINTSTS_RESETDET)) {
    /* Report disconnection if it is not already done. */
    dwc2_hsotg_disconnect(hsotg);
    }
    ......
    }

    void dwc2_hsotg_disconnect(struct dwc2_hsotg *hsotg)
    {
    ......
    // 回调gadget的disconnect,触发gadgetfs更新用户态的gadget状态
    call_gadget(hsotg, disconnect);
    hsotg->lx_state = DWC2_L3;
    // 更新用户态udc驱动的状态
    usb_gadget_set_state(&hsotg->gadget, USB_STATE_NOTATTACHED);
    }

三、方案讨论

3.1 phy vbus 方案

通过读取phy offset为0x13的地址,bit 1表示当前vbus的状态:

phy_vbus

通过dwc2控制器来读取phy offset的数据:

GPVNDCTL

3.2 临时方案:用户空间补丁

  1. 内核默认参数
    no_clock_gating = false
    power_down = DWC2_POWER_DOWN_PARAM_NONE
    以上参数导致:suspend中断函数会将PHY clock关闭省电,导致VBUS 不可用,不可以通过读取phy的vbus来区分是suspend还是断开线缆,dwc2默认这个参数,本意就是不区分suspend和断开,毕竟断开线缆后触发的suspend中断中,也是调用call_gadget(hsotg, suspend)

  2. 解决方案
    用户不区分是suspend还是disconnect的情况下,在应用层中判断:当 App 读取到 suspend 状态时,读取 USB PHY VBUS 寄存器。 该寄存器能反映 VBUS 电压,不管是断开线缆还是host主动发起suspend,都会进入suspend中断,内核默认参数配置下会将phy clock关闭,所以这两种情况读出来VBUS都为0,都当成disconnect处理。

  3. 情况举例

  • 情况一:Suspend + Resume

    1
    2
    3
    4
    1. Host suspend → Device suspend intr → 关闭phy clock(VBUS=0) 
    -> gadget suspend -> gadgetfs上报用户态 -> 应用层读取vbus为0,通知为disconnect。
    2. Host Resume → Device dwc2_handle_wakeup_detected_intr
    -> 恢复phy clock(VBUS=1) → gadget resume -> gadgetfs上报用户态 -> 通知为resume。
  • 情况二:Disconnect + connect

    1
    2
    3
    1. Host disconnect → Device suspend intr → 关闭phy clock(VBUS=0) 
    -> gadget suspend -> gadgetfs上报用户态 -> 应用层读取vbus为0,通知为disconnect。
    2. Host connect → dwc2_hsotg_irq -> 后续参考:`原因分析章节第三点`
  1. 风险分析
  • 如果 Host 主动发送 suspend,此时读取 PHY VBUS 依然会得到 0
  • 因为:默认情况下: no_clock_gating = false, 不管是suspend还是disconnect,都会导致 PHY clock关闭,VBUS 不可用。导致无法准确区分 suspenddisconnect 状态。

3.3 最终方案:内核空间补丁

3.3.1 params.c

修改内核默认参数
no_clock_gating = true
power_down = DWC2_POWER_DOWN_PARAM_NONE
以上参数配置:suspend中断函数不会将PHY clock关闭省电,通过读取phy的vbus来区分是suspend还是断开线缆
params_patch

3.3.2 core_intr.c

1. 修改suspend中断服务函数,在里面读取vbus区分suspend和disconnect

core_intr_patch_1

2. 修改resume中断服务函数,正确处理resume恢复动作

core_intr_patch_2

3.3.3 u_serial.c

cdc gadget测试,Windows休眠后挂起时 tty 关闭了,resume时需要修改,防止访问空指针

u_serial_patch

四、测试步骤

disconnect测试热拔插线缆即可

4.1 Linux测试suspend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
anlogic@anlogic-Vostro-3660:/sys/bus/usb/devices/usb1/1-1$ cat configuration
CDC ACM config
anlogic@anlogic-Vostro-3660:/sys/bus/usb/devices/usb1/1-1$ echo auto | sudo tee ./power/control
auto
anlogic@anlogic-Vostro-3660:/sys/bus/usb/devices/usb1/1-1$ echo on | sudo tee ./power/control
on
anlogic@anlogic-Vostro-3660:/sys/bus/usb/devices/usb1/1-1$ cat /dev/ttyACM0
hello


host确定 gadget device:
cat configuration

host挂起(suspend) gadget device:
echo auto | sudo tee ./power/control

host唤醒 gadget device:
echo on | sudo tee ./power/control

测试是否恢复正常工作:
host: cat /dev/ttyACM0
device:echo hello > /dev/ttyGS0

gadget device查看状态:
cat /sys/class/udc/f8180000.usb/state
not attached或者configured

4.2 Windows测试suspend

不同电脑的usb现象不同,同一台电脑的不同usb端口现象也不同:

  1. 有点电脑点击休眠,usb直接断电,唤醒后重新供电,相当于热拔插。

  2. 有的电脑点击休眠,usb持续供电,不发送suspend,唤醒后直接工作。

  3. 有的电脑点击休眠,usb持续供电,不发送suspend,唤醒时,掉电再上电,相当于热拔插。

  4. 有的电脑点击休眠,usb持续供电,发送suspend,唤醒发送resume,触发device resume流程。

以上所有场景,在最终方案中,使用内核自带的cdc gadget测试都可pass。