CherryUSB 中的 XHCI 驱动分析

0. 参考链接

  • USB基础知识概论
  • 《USB Complete》,全面介绍了 USB 相关的知识
  • 《USB in a Nutshell》,介绍了 USB 的必要知识

1. 关于USB

1.1 USB 是什么

  • USB是Universal Serial Bus的缩写,USB是一种简易、双向、快速、同步、即插即用(Plug and Play,PnP)且支持热插拔功能的串行接口

  • USB出现之前,计算机领域中的接口太多太繁杂,普通用户使用起来难度较大,接口之间的兼容性也较差

  • USB设计理念是,实现计算机设备和通讯设备的完美融合,支持即插即用不需要用户手动配置,提高接口扩展性在不同应用上使用统一的接口,总的来说,USB的出现,是希望通过此单个的USB接口,同时支持多种不同的应用,而且用户用起来也很方便,直接插上就能用了,也方便不同的设备的之间的互联

  • USB的优点包括,

      1. 通用性,使用同样的线缆和接口,支持多种类型的设备
      1. 可扩展,通过USB端口,集线器可以添加更多的端口,连接最多127个设备
      1. 热插拔,从协议层面支持用户随时连接和断开USB设备
      1. 自动配置,USB设备连接后,可以上报设备类型等信息,支持自动配置
      1. 易设置,USB设备不需要用户配置中断线,断开地址等
      1. 多种速率,支持多种速率的设备
      1. 高可靠,在硬件和协议层面保证通信的可靠性
  • USB设备,从物理上的逻辑结构来说,包含了主机Host端和设备Device端, 其中,主机Host端,有对应的硬件的USB的主机控制器Host Controller,而设备端,连接的是对应的USB设备

在这里插入图片描述

1.2 USB 接口

  • 下面就来简单的介绍一下不同的USB接口类型,即各种不同的插头插座:

  • USB的接口类型,根据接口形状不同,主要可以分为三大类:

      1. 普通的硬件直接叫做Type
      1. 然后有小型版本的叫Mini迷你的
      1. 和更加小的,叫做Micro微小的
  • 其中每一种大类中,又都可以分为两类

      1. A类(Type A)
      1. B类(Type B)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xhgOFU4T-1665644089017)(figsif_types.png)]

  • 这里具体讨论的 XHCI 的主机接口,对应A类插座和插头

usb connect

[usb3.0,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VFas9dCm-1665644089019)(figsif_tbl.png)]在这里插入图片描述

1.3 USB 主机有哪些

  • OHCI,Open Host Controller Interface,创立者是Compaq,Microsoft和National Semiconductor。只支持USB 1.1
  • UHCI,Universal Host Controller Interface,创立者是Intel。只支持USB 1.1
  • EHCI,Enhanced Host Controller Interface,创立者是Intel。支持 USB 2.0
  • xHCI,Extensible Host Controller Interface, 创立者是Intel。支持 USB 3.0 / USB 3.1
  • 更新的主机控制器这里就不讨论了

在这里插入图片描述

1.4 USB 相关的软件和工具

  • USB Device(如鼠标\键盘\U盘)的驱动和固件,USB Device需要相应标准的USB请求,完成数据的读取和写入,USB Device通常是由一个单片机完成,USB请求的响应由单片机的固件/裸机/RTOS完成
  • USB Host的驱动,USB Host需要按照标准,主动发起USB请求,接受USB Device返回的响应,常见的USB Host运行在Linux\Windows等大型操作系统上
  • USB 测试和协议分析软件,USB通信出现问题时,需要一些调试工具,常见的有以下几类,
      1. USB 硬件抓包工具,例如 Ellisys的USB Explorer 260硬件,配合Ellisys USB Analysis Software,可以实现USB数据抓包和分析,分析抓取出来的数据,判断通信过程是否符合期望,满足USB协议的规范定义。还有 Bus Hound,是一类软件总线协议分析器,通过传软件可以监视USB总线上的数据包
      1. USB 总线查看工具,例如 Windows 上的USBView, 可以浏览Windows上所有的USB主机控制器和USB设备,查看其详细信息
      1. USB 兼容性测试工具,例如 USB20CV,是一个USB标准化组织推荐的 USB 设备测试框架,运行在Windows上,可以测试USB Device是否符合规范

1.5 USB 协议

在这里插入图片描述

  • 这里主要讨论 USB 2.0协议,不同版本的 USB 协议,其最显著的差别在于通信速率的支持,USB 2.0可以支持Low,Full,High三种速率,3.0可以支持Super, 3.1可以支持 Super plus,2.0协议中主要有以下几部分
    1. 基本的电气规范,机械结构
    1. 主机和设备之间的拓扑关系
    1. 通信的数据流类型和速率
    1. 通信的机制,端点、管道的概念,控制、中断、批量和等时四种传输类型
    1. 数据包的具体定义,报告状态和握手协议
    1. USB HOST的驱动模型,USB总线枚举设备的过程,USB标准Request的定义
    1. USB HUB的规范,HUB的配置,Split传输等
    1. USB Device的规范

1.6 USB 协议细节

  • USB系统的核心是HOST,单个USB总线上,只能有一个HOST, 在支持OTG后,USB为了支持多个设备互联(比如一个数码相机和一个打印机),引入了HNP(Host Negotiation Protocol),主机协商协议,两个设备可以协商决定谁当HOST
  • 一个USB总线中,最多可以连接127个设备,设备的分类通过Class完成,常见的键盘鼠标(HID 0x3),U盘 (Mass Storage 0x8),集线器(Hub 0x9)

在这里插入图片描述

  • USB枚举,USB Emulation,对应的就是USB的Host和Device之间的对话,即Host根据Device所报告上来的参数,得知USB的device是啥类型的,具有啥功能,然后初始化相关参数,接下来,就USB Device就可以正常工作了,下面是关于windows下USB枚举的过程的总结:

      1. The host or hub detects the connection of a new device via the device’s pull up resistors on the data pair. The host waits for at least 100ms allowing for the plug to be inserted fully and for power to stabilise on the device.
      1. Host issues a reset placing the device is the default state. The device may now respond to the default address zero.
      1. The MS Windows host asks for the first 64 bytes of the Device Descriptor.
      1. After receiving the first 8 bytes of the Device Descriptor, it immediately issues another bus reset.
      1. The host now issues a Set Address command, placing the device in the addressed state.
      1. The host asks for the entire 18 bytes of the Device Descriptor.
      1. It then asks for 9 bytes of the Configuration Descriptor to determine the overall size.
      1. The host asks for 255 bytes of the Configuration Descriptor.
      1. Host asks for any String Descriptors if they were specified.
      1. At the end of Step 9, Windows will ask for a driver for your device. It is then common to see it request all the descriptors again before it issues a Set Configuration request.
  • USB协议中定义了四种传输(Transfer)类型

      1. 控制传输(Control Transfer)
      1. 中断传输(Interrupt Transfer)
      1. 批量传输(Bulk Transfer)
      1. 同步传输(Isochronous Transfer)
  • USB 描述符,USB协议本身很复杂,但方便在提供了统一的接口方式,这种方便的一个重要原因是 USB 协议规范了一套通信用的数据结构,也就是 USB 描述符,标准的USB设备有5种USB描述符:设备描述符,配置描述符,字符串描述符,接口描述符,端点描述符,USB标准化组织和各厂商根据特定的通信需要,还扩展了一系列描述符定义

      1. 设备描述符:一个设备只有一个设备描述符,由于描述当前设备的信息,如设备类型、产品ID,厂商ID等
      1. 配置描述符: 配置描述符定义了设备的配置信息,一个设备可以有多个配置描述符,每个配置代表了一种功能,有些设备支持多种功能,这样的设备就会提供多个配置描述符,不过主机在枚举USB设备的时候需要选择一个配置生效
      1. 接口描述符:接口描述符说明了接口所提供的配置,一个配置所拥有的接口数量通过配置描述符的bNumInterfaces决定,一个接口描述符对应一个特定功能,复合设备会支持多个功能,这时候会有多个接口描述符,例如一个带网卡的Hub, 会提供一个Class为0x9的接口描述符和一个0xa的接口描述符
      1. 端点描述符:USB设备中的每个端点都有自己的端点描述符,由接口描述符中的bNumEndpoint决定其数量,端点用于支持接口通信,一个双向通信的设备需要至少两个端点
  • USB 通信包,USB 2.0协议中,USB包由SOP,SYNC,Packet内容和EOP组成,SOP是起始包,SYNC是同步域,采用NRZI编码,Packet内容包括PID(包ID),地址,帧号,数据和CRC校验,最后EOP是结束包,不同的Packet内容构成令牌包、握手包和数据包等,多个包组合形成事务,是USB传输的基础

在这里插入图片描述

1.7 USB HUB

  • USB HUB用于设备扩展连接,所有USB DEVICE都连接在USB HUB的端口上。一个USB HOST总与一个根HUB (USB ROOT HUB)相连
  • USB HUB为其每个端口提供100mA电流供设备使用, 同时,USB HUB可以通过端口的电气变化诊断出设备的插拔操作,并通过响应USB HOST的数据包把端口状态汇报给USB HOST
  • 一般来说,USB设备与USB HUB间的连线长度不超过5m,USB系统的级联不能超过5级(包括ROOT HUB)

2. CherryUSB 分析

2.1 CherryUSB 框架

freamwork

  • CherryUSB 是一个用于嵌入式系统的 USB 主从协议栈,通过分析 CherryUSB 的框架结构,我们可以对USB 整体有一个感性认识
  • CherryUSB 作为主机协议栈,在中断模式下完成USB通信,可以支持 HUB、Mass Storage、HID类,实现USB 扩展、存储、人机交互等功能,支持 VIDEO、Remote NDIS 和 AxuNet类,实现视频传输、无线网络、有线以太网等功能
  • CherryUSB 作为从机协议栈,响应主机的USB请求,可以实现USB 扩展、存储、人机交互等设备功能,实现视频传输、无线网络、有线以太网等设备
  • 下面主要从主机协议栈出发进行介绍

2.2 CherryUSB 目录结构

  • core 目录实现了 USB HOST的功能
  • class 目录封装了一系列标准 USB 设备类型的接口
  • osal 目录针对不同的 RTOS 进行了适配
  • port 目录针对不同的 USB HOST控制器实现了功能
目录名描述
classusb class 类主从驱动
commonusb spec 定义、常用宏、标准接口定义
coreusb 主从协议栈核心实现
demo示例
docs文档
osalos 封装层
packet capture抓包文件(需要使用力科软件打开)
portusb 主从需要实现的 porting 接口
tools工具链接
.
├── class
├── common
├── core
├── demo
├── docs
├── osal
├── packet capture
└── port
└── tools

在这里插入图片描述

2.3 CherryUSB 源码分析

2.3.1 USB HOST 初始化

  • usbh_initialize 是初始化主机协议栈
  • (1) 清零了 USB BUS 上挂的设备数据
    -(2)获取了链接脚本中定义的class段起止地址,所有的 USB Class 驱动入口函数都在这个class段里
  • (3) roothub 是不用初始化的,初始化从第一个外挂的 hub 开始
struct usbh_devaddr_map {
    /**
     * alloctab[0]:addr from 0~31
     * alloctab[1]:addr from 32~63
     * alloctab[2]:addr from 64~95
     * alloctab[3]:addr from 96~127
     *
     */
    uint8_t next;         /* Next device address */
    uint32_t alloctab[4]; /* Bit allocation table */
};

struct usbh_bus {
    struct usbh_devaddr_map devgen;
} g_usbh_bus;

int usbh_initialize(void)
{
    memset(&g_usbh_bus, 0, sizeof(struct usbh_bus)); (1)
    
    extern uint32_t __usbh_class_info_start__;
    extern uint32_t __usbh_class_info_end__;
    usbh_class_info_table_begin = (struct usbh_class_info *)&__usbh_class_info_start__;
    usbh_class_info_table_end = (struct usbh_class_info *)&__usbh_class_info_end__; (2)

    /* devaddr 1 is for roothub */
    g_usbh_bus.devgen.next = 2; (3)

    usbh_hub_initialize();
    return 0;
}
  • 创建一个任务/线程,usbh_hub,入口函数为 usbh_hub_thread
  • (1)调用 port 中 HOST 控制器驱动实现的初始化函数 usb_hc_init,初始化硬件驱动
  • (2)完成 HOST 控制器初始化后,usbh_hub 任务就死等上报的hub事件了
  • (3)如果收到了hub事件,在 usbh_hub_events 中依次处理各个事件
  • 到这里为止,HOST 控制器的硬件初始化完成了,但是USB BUS上还没有设备,后面通过处理hub事件,协议栈会逐步把连接的设备枚举起来
int usbh_hub_initialize(void)
{
    usbh_roothub_register();

    hub_event_wait = usb_osal_sem_create(0);
    if (hub_event_wait == NULL) {
        return -1;
    }

    hub_thread = usb_osal_thread_create("usbh_hub", CONFIG_USBHOST_PSC_STACKSIZE, CONFIG_USBHOST_PSC_PRIO, usbh_hub_thread, NULL);
    if (hub_thread == NULL) {
        return -1;
    }
    return 0;
}

static void usbh_hub_thread(void *argument)
{
    size_t flags;
    int ret = 0;

    usb_hc_init();1while (1) {
        ret = usb_osal_sem_take(hub_event_wait, 0xffffffff);2if (ret < 0) {
            continue;
        }

        while (!usb_slist_isempty(&hub_event_head)) {3struct usbh_hub *hub = usb_slist_first_entry(&hub_event_head, struct usbh_hub, hub_event_list);
            flags = usb_osal_enter_critical_section();
            usb_slist_remove(&hub_event_head, &hub->hub_event_list);
            usb_osal_leave_critical_section(flags);
            usbh_hub_events(hub);
        }
    }
}

2.3.2 USB 设备的发现和端口事件的处理

  • 设备的发现和端口事件的上报是在中断中完成的
  • 协议栈要求port的每个HOST控制器驱动都实现 中断响应函数 USBH_IRQHandler,在中断中检测端口连接状态的变化,并上报端口事件
  • 不同的HOST控制器检查端口连接状态的方法不一样,这里以XHCI为例,(1) 进入中断后,检查XHCI的EVENT TRB,如果EVENT TRB的类型是Port Status Change (2),读取对应端口寄存器的状态,是否为刚刚改变(3),如果是,调用 usbh_roothub_thread_wakeup 上报端口事件,唤醒端口
  • usbh_hub_thread 检测到端口事件,阻塞状态解除,调用 usbh_hub_events 处理端口事件
void USBH_IRQHandler(void)
{
    uint32_t reg, status;
    struct xhci_s *xhci = &xhci_host;
    struct xhci_ring *evts = xhci->evts;
    struct xhci_pipe *work_pipe = NULL;

...

    /* check and ack event */
    for (;;) { (1)
        uint32_t nidx = evts->nidx; /* index of dequeue trb */
        uint32_t cs = evts->cs; /* cycle state toggle by xHc */
        struct xhci_trb *etrb = evts->ring + nidx; /* current trb */
        uint32_t control = etrb->control; /* trb control field */

        if ((control & TRB_C) != (cs ? 1 : 0)) /* if cycle state not toggle, no events need to handle */
            break;

        /* process event on etrb */
        uint32_t evt_type = TRB_TYPE_GET(control);
        uint32_t evt_cc = TRB_CC_GET(etrb->status); /* bit[31:24] completion code */
    
        if (ER_PORT_STATUS_CHANGE == evt_type) {2/* bit[31:24] port id, the port num of root hub port that generated this event */
            uint32_t port = TRB_PORT_ID_GET(etrb->ptr_low) - 1;
            uint32_t portsc = xhci_readl_port(xhci, port, XHCI_REG_OP_PORTS_PORTSC); /* Read status */

            xhci_print_port_state(__func__, port, portsc);
            if (portsc & XHCI_REG_OP_PORTS_PORTSC_CSC) {3usbh_roothub_thread_wakeup(port + 1); /* wakeup when connection status changed */
            }
  • 进入 usbh_hub_events 后,协议栈会复查端口状态(1),如果端口处于连接可用状态,通过 hub 的标志请求重置端口(2),端口重置完成后,会检查端口上报的连接速率(3),随后在 hub 上创建一个 child 节点,用于放置之后枚举的设备
  • 值得注意的是,对于挂在普通hub上的端口,usbh_hub_get_portstatus、usbh_hub_set_feature 等 hub 动作是走的 USB 标志请求,但是对于直接挂在root-hub上的端口,协议栈要求 HOST 控制器驱动实现 usbh_roothub_control,用于完成获取端口状态、重置端口等动作
  • 端口的准备工作完成后,即将进入下一个阶段,调用 usbh_enumerate 尝试枚举在端口上刚刚发现的 USB 设备 (4)
static void usbh_hub_events(struct usbh_hub *hub)
{

    。。。
        /* Read hub port status */
        ret = usbh_hub_get_portstatus(hub, port + 1, &port_status);1...
            /* Last, check connect status */
            if (portstatus & HUB_PORT_STATUS_CONNECTION) {
                ret = usbh_hub_set_feature(hub, port + 1, HUB_PORT_FEATURE_RESET);2if (ret < 0) {
                    USB_LOG_ERR("Failed to reset port %u,errorcode:%d\r\n", port, ret);
                    continue;
                }

                usb_osal_msleep(DELAY_TIME_AFTER_RESET);
                /* Read hub port status */
                ret = usbh_hub_get_portstatus(hub, port + 1, &port_status);
                if (ret < 0) {
                    USB_LOG_ERR("Failed to read port %u status, errorcode: %d\r\n", port + 1, ret);
                    continue;
                }

                portstatus = port_status.wPortStatus;
                portchange = port_status.wPortChange;
                if (!(portstatus & HUB_PORT_STATUS_RESET) && (portstatus & HUB_PORT_STATUS_ENABLE)) {
                    if (portchange & HUB_PORT_STATUS_C_RESET) {
                        ret = usbh_hub_clear_feature(hub, port + 1, HUB_PORT_FEATURE_C_RESET);
                        if (ret < 0) {
                            USB_LOG_ERR("Failed to clear port %u reset change, errorcode: %d\r\n", port, ret);
                        }
                    }

                    if (portstatus & HUB_PORT_STATUS_HIGH_SPEED) {3)
                        speed = USB_SPEED_HIGH;
                    } else if (portstatus & HUB_PORT_STATUS_LOW_SPEED) {
                        speed = USB_SPEED_LOW;
                    } else {
                        speed = USB_SPEED_FULL;
                    }

                    child = &hub->child[port];

                    memset(child, 0, sizeof(struct usbh_hubport));
                    child->parent = hub;
                    child->connected = true;
                    child->port = port + 1;
                    child->speed = speed;

                    USB_LOG_INFO("New %s device on Hub %u, Port %u connected\r\n", speed_table[speed], hub->index, port + 1);

                    if (usbh_enumerate(child) < 0) {4USB_LOG_ERR("Port %u enumerate fail\r\n", port + 1);
                    }
                } else {
                    USB_LOG_ERR("Failed to enable port %u\r\n", port + 1);
                    continue;
                }
            } else {
                
...
                   
                
}
2.3.3 创建控制通道EP0
  • 进入 USB 设备枚举流程后,首先为设备创建和配置端点 EP0 (1), EP0是 HOST 和 设备进行控制传输的通道,后续的 USB请求都通过 EP0完成
  • 在(2)处设置 EP0 的默认最大包长度 MPS,MPS 的值和设备连接速率有关,这个速率在之通过端口事件已经报上来了
  • 在(3)处将 EP0 设置为控制端点,在(4)处将 EP0 和它的端点数据结构绑定
  • 最后在(5)调用 usbh_pipe_alloc 创建 EP0,这个函数由port中的 HOST 控制器驱动实现,这一部分在 XHCI 里进行说明
int usbh_enumerate(struct usbh_hubport *hport)
{
...

    /* Configure EP0 with the default maximum packet size */
    usbh_hport_activate_ep0(hport); (1)
}

static int usbh_get_default_mps(int speed)
{
    switch (speed) {
        case USB_SPEED_LOW: /* For low speed, we use 8 bytes */
            return 8;
        case USB_SPEED_FULL: /* For full or high speed, we use 64 bytes */
        case USB_SPEED_HIGH:
            return 64;
        case USB_SPEED_SUPER: /* For super speed , we must use 512 bytes */
        case USB_SPEED_SUPER_PLUS:
            return 512;
        default:
            return 64;
    }
}

int usbh_hport_activate_ep0(struct usbh_hubport *hport)
{
    struct usbh_endpoint_cfg ep0_cfg = { 0 };

    ep0_cfg.ep_addr = 0x00;
    ep0_cfg.ep_interval = 0x00;
    ep0_cfg.ep_mps = usbh_get_default_mps(hport->speed);2)
    ep0_cfg.ep_type = USB_ENDPOINT_TYPE_CONTROL; (3)
    ep0_cfg.hport = hport; (4)

    usbh_pipe_alloc(&hport->ep0, &ep0_cfg); (5)
    return 0;
}
2.3.4 获取设备的最大包长度
  • 设备的信息需要通过设备描述符拿到,前面我们已经创建了EP0,现在要开始进行控制传输
  • 设备描述符的真正长度我们是不知道的,所以在(1)先读取8个字节,获取设备上报的 MPS (2)
  • 配置刚刚获取的 MPS 到 EP0(3),usbh_ep0_pipe_reconfigure也是 HOST 驱动提供的
    /* Read the first 8 bytes of the device descriptor */ 
    setup->bmRequestType = USB_REQUEST_DIR_IN | USB_REQUEST_STANDARD | USB_REQUEST_RECIPIENT_DEVICE;
    setup->bRequest = USB_REQUEST_GET_DESCRIPTOR;
    setup->wValue = (uint16_t)((USB_DESCRIPTOR_TYPE_DEVICE << 8) | 0);
    setup->wIndex = 0;
    setup->wLength = 8; (1)

    ret = usbh_control_transfer(hport->ep0, setup, ep0_request_buffer);
    if (ret < 0) {
        USB_LOG_ERR("Failed to get device descriptor,errorcode:%d\r\n", ret);
        goto errout;
    }

    parse_device_descriptor(hport, (struct usb_device_descriptor *)ep0_request_buffer, 8);

    /* Extract the correct max packetsize from the device descriptor */
    ep_mps = ((struct usb_device_descriptor *)ep0_request_buffer)->bMaxPacketSize0; (2)

    /* Reconfigure EP0 with the correct maximum packet size */
    usbh_ep0_pipe_reconfigure(hport->ep0, 0, ep_mps, hport->speed); (3)
2.3.5 获取/配置设备地址
  • 对于 XHCI, 设备地址通过 ENABLE_SLOT 命令从硬件分配,之前 usbh_pipe_alloc EP0的时候已经完成,在 (1)位置获取地址即可
  • 对于其它USB HOST,设备地址通过 USB 标志请求 SET_ADDRESS 完成,在(2)位置生成一个空闲的地址,通过 SET_ADDRESS 请求与设备绑定即可 (3)
#ifdef CONFIG_USBHOST_XHCI
    extern int usbh_get_xhci_devaddr(usbh_pipe_t * pipe);1/* Assign a function address to the device connected to this port */
    dev_addr = usbh_get_xhci_devaddr(hport->ep0);
    if (dev_addr < 0) {
        USB_LOG_ERR("Failed to allocate devaddr,errorcode:%d\r\n", ret);
        goto errout;
    }
#else
    /* Assign a function address to the device connected to this port */
    dev_addr = usbh_allocate_devaddr(&g_usbh_bus.devgen);2if (dev_addr < 0) {
        USB_LOG_ERR("Failed to allocate devaddr,errorcode:%d\r\n", ret);
        goto errout;
    }
#endif

    /* Set the USB device address */
    setup->bmRequestType = USB_REQUEST_DIR_OUT | USB_REQUEST_STANDARD | USB_REQUEST_RECIPIENT_DEVICE;
    setup->bRequest = USB_REQUEST_SET_ADDRESS;
    setup->wValue = dev_addr;
    setup->wIndex = 0;
    setup->wLength = 0;

    ret = usbh_control_transfer(hport->ep0, setup, NULL);3if (ret < 0) {
        USB_LOG_ERR("Failed to set devaddr,errorcode:%d\r\n", ret);
        goto errout;
    }

2.3.6 获取设备/配置/接口描述符

  • 现在我们可以获取完整的设备描述符了(1)
  • 然后获取设备支持的配置,也就是配置描述符(2)
  • 我们这里用默认的配置1初始化设备(3)
    /* Read the full device descriptor */
    setup->bmRequestType = USB_REQUEST_DIR_IN | USB_REQUEST_STANDARD | USB_REQUEST_RECIPIENT_DEVICE;
    setup->bRequest = USB_REQUEST_GET_DESCRIPTOR;
    setup->wValue = (uint16_t)((USB_DESCRIPTOR_TYPE_DEVICE << 8) | 0);
    setup->wIndex = 0;
    setup->wLength = USB_SIZEOF_DEVICE_DESC;

    ret = usbh_control_transfer(hport->ep0, setup, ep0_request_buffer); (1)
    if (ret < 0) {
        USB_LOG_ERR("Failed to get full device descriptor,errorcode:%d\r\n", ret);
        goto errout;
    }

...

    /* Read the first 9 bytes of the config descriptor */
    setup->bmRequestType = USB_REQUEST_DIR_IN | USB_REQUEST_STANDARD | USB_REQUEST_RECIPIENT_DEVICE;
    setup->bRequest = USB_REQUEST_GET_DESCRIPTOR;
    setup->wValue = (uint16_t)((USB_DESCRIPTOR_TYPE_CONFIGURATION << 8) | 0);
    setup->wIndex = 0;
    setup->wLength = USB_SIZEOF_CONFIG_DESC;

    ret = usbh_control_transfer(hport->ep0, setup, ep0_request_buffer); (2)
    if (ret < 0) {
        USB_LOG_ERR("Failed to get config descriptor,errorcode:%d\r\n", ret);
        goto errout;
    }

...

    /* Select device configuration 1 */
    setup->bmRequestType = USB_REQUEST_DIR_OUT | USB_REQUEST_STANDARD | USB_REQUEST_RECIPIENT_DEVICE;
    setup->bRequest = USB_REQUEST_SET_CONFIGURATION;
    setup->wValue = 1;3)
    setup->wIndex = 0;
    setup->wLength = 0;

    ret = usbh_control_transfer(hport->ep0, setup, NULL);
    if (ret < 0) {
        USB_LOG_ERR("Failed to set configuration,errorcode:%d\r\n", ret);
        goto errout;
    }

2.3.7 根据接口描述符选择设备类型

  • 设置了设备配置为1后,通用的设备枚举过程已经走完了,之后要走的是不同设备特有的枚举流程,比如U盘有U盘的流程,鼠标有鼠标的流程,这个过程的分流是通过接口描述符完成的
  • 接口描述符紧跟在配置描述符后面,在(1)我们遍历所有的设备接口,在(2)将指针指向一个接口描述符
  • 在 (3)调用 usbh_find_class_driver,依据接口描述符中上报的设备类型、子类型、协议、厂商和产品等信息,尝试获取匹配的设备驱动,用于完成特有设备枚举
  • 如果没找到匹配的设备,很抱歉设备枚举只能失败跳过了(4),如果找到了匹配的驱动,在(5)我们调用找到的方法初始化设备,完成设备枚举
    /*search supported class driver*/
    for (uint8_t i = 0; i < hport->config.config_desc.bNumInterfaces; i++) { (1)
        intf_desc = &hport->config.intf[i].altsetting[0].intf_desc;2struct usbh_class_driver *class_driver = (struct usbh_class_driver *)usbh_find_class_driver(intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol, hport->device_desc.idVendor, hport->device_desc.idProduct); 3()

        if (class_driver == NULL) { (4)
            USB_LOG_ERR("do not support Class:0x%02x,Subclass:0x%02x,Protocl:0x%02x\r\n",
                        intf_desc->bInterfaceClass,
                        intf_desc->bInterfaceSubClass,
                        intf_desc->bInterfaceProtocol);

            continue;
        }
        hport->config.intf[i].class_driver = class_driver;
        USB_LOG_INFO("Loading %s class driver\r\n", class_driver->driver_name);
        ret = CLASS_CONNECT(hport, i);5if (ret < 0) {
            ret = CLASS_DISCONNECT(hport, i);
            goto errout;
        }
    }

2.3.8 枚举一个功能设备(U盘为例)

  • 设备功能在class文件夹中实现,例如usbh_msc.c中实现了U盘的功能
  • 静态变量 msc_class_info 会被链接在class段,它包含了设备匹配flag(1),和 class 驱动
  • class 驱动主要包含了一个设备connect函数和disconect函数,usbh_msc_connect 或者枚举过程中被调用,完成专有设备的枚举
const struct usbh_class_driver msc_class_driver = {
    .driver_name = "msc",
    .connect = usbh_msc_connect,1.disconnect = usbh_msc_disconnect(2};

CLASS_INFO_DEFINE const struct usbh_class_info msc_class_info = {
    .match_flags = USB_CLASS_MATCH_INTF_CLASS | USB_CLASS_MATCH_INTF_SUBCLASS | USB_CLASS_MATCH_INTF_PROTOCOL,1.class = USB_DEVICE_CLASS_MASS_STORAGE,
    .subclass = MSC_SUBCLASS_SCSI,
    .protocol = MSC_PROTOCOL_BULK_ONLY,
    .vid = 0x00,
    .pid = 0x00,
    .class_driver = &msc_class_driver(2};
  • 在枚举U盘的过程中,首先动态分配一个U盘设备的数据结构(1),将U盘设备与发现它的端口绑定(2),记录U盘接口的信息(3)
  • 之后的流程是U盘设备的规定动作,获取U盘的信息(4),创建一个两个工作端点用于传输数据(5),U盘的数据用批量传输完成,需要一个bulk_in和一个bulk_out端点,分别支持读写,usbh_hport_activate_epx用于分配工作端点,最终调用的是HOST 驱动提供的usbh_pipe_alloc
  • 在(6)执行一个U盘规定的测试流程,测试完成,在(7)获取U盘的容量,表示它的枚举过程成功
  • 这里仿照Linux的命令,在完成U盘的枚举后,会有类似/dev/sda, /dev/sdb的设备可以找到
#define DEV_FORMAT "/dev/sd%c" 7

int usbh_hport_activate_epx(usbh_pipe_t *pipe, struct usbh_hubport *hport, struct usb_endpoint_descriptor *ep_desc)
{
    struct usbh_endpoint_cfg ep_cfg = { 0 };

    ep_cfg.ep_addr = ep_desc->bEndpointAddress;
    ep_cfg.ep_type = ep_desc->bmAttributes & USB_ENDPOINT_TYPE_MASK;
    ep_cfg.ep_mps = ep_desc->wMaxPacketSize & USB_MAXPACKETSIZE_MASK;
    ep_cfg.ep_interval = ep_desc->bInterval;
    ep_cfg.mult = (ep_desc->wMaxPacketSize & USB_MAXPACKETSIZE_ADDITIONAL_TRANSCATION_MASK) >> USB_MAXPACKETSIZE_ADDITIONAL_TRANSCATION_SHIFT;
    ep_cfg.hport = hport;

    USB_LOG_INFO("Ep=%02x Attr=%02u Mps=%d Interval=%02u Mult=%02u\r\n",
                 ep_cfg.ep_addr,
                 ep_desc->bmAttributes,
                 ep_cfg.ep_mps,
                 ep_cfg.ep_interval,
                 ep_cfg.mult);

    return usbh_pipe_alloc(pipe, &ep_cfg);
}

static int usbh_msc_connect(struct usbh_hubport *hport, uint8_t intf)
{
    struct usb_endpoint_descriptor *ep_desc;
    int ret;

    struct usbh_msc *msc_class = usb_malloc(sizeof(struct usbh_msc));1if (msc_class == NULL) {
        USB_LOG_ERR("Fail to alloc msc_class\r\n");
        return -ENOMEM;
    }

    memset(msc_class, 0, sizeof(struct usbh_msc));
    usbh_msc_devno_alloc(msc_class);
    msc_class->hport = hport;2)
    msc_class->intf = intf;3)

    hport->config.intf[intf].priv = msc_class;

    ret = usbh_msc_get_maxlun(msc_class, g_msc_buf);4if (ret < 0) {
        return ret;
    }

    USB_LOG_INFO("Get max LUN:%u\r\n", g_msc_buf[0] + 1);

    for (uint8_t i = 0; i < hport->config.intf[intf].altsetting[0].intf_desc.bNumEndpoints; i++) {
        ep_desc = &hport->config.intf[intf].altsetting[0].ep[i].ep_desc;
        if (ep_desc->bEndpointAddress & 0x80) {
            usbh_hport_activate_epx(&msc_class->bulkin, hport, ep_desc);5} else {
            usbh_hport_activate_epx(&msc_class->bulkout, hport, ep_desc);
        }
    }

    ret = usbh_msc_scsi_testunitready(msc_class);6if (ret < 0) {
        USB_LOG_ERR("Fail to scsi_testunitready\r\n");
        return ret;
    }
    ret = usbh_msc_scsi_inquiry(msc_class);
    if (ret < 0) {
        USB_LOG_ERR("Fail to scsi_inquiry\r\n");
        return ret;
    }
    ret = usbh_msc_scsi_readcapacity10(msc_class);7if (ret < 0) {
        USB_LOG_ERR("Fail to scsi_readcapacity10\r\n");
        return ret;
    }

    if (msc_class->blocksize > 0) {
        USB_LOG_INFO("Capacity info:\r\n");
        USB_LOG_INFO("Block num:%d,block size:%d\r\n", (unsigned int)msc_class->blocknum, (unsigned int)msc_class->blocksize);
    } else {
        USB_LOG_ERR("Invalid block size\r\n");
        return -ERANGE;
    }

    snprintf(hport->config.intf[intf].devname, CONFIG_USBHOST_DEV_NAMELEN, DEV_FORMAT, msc_class->sdchar);

    USB_LOG_INFO("Register MSC Class:%s\r\n", hport->config.intf[intf].devname);

    return ret;
 }
  • 这里以 U盘设备为例说明,其它设备根据其协议规定,也有类似的流程

2.3.9 使用一个功能设备(以U盘为例)

  • U盘设备枚举成功后,调用 usbh_find_class_instance 可以获取设备控制块(1),在(2)和(3)对U盘进行读写
        struct usbh_msc *msc_class = (struct usbh_msc *)usbh_find_class_instance("/dev/sda"); (1)
        if (msc_class == NULL) 
        {
            USB_LOG_RAW("do not find /dev/sda\r\n");
            goto err_exit;
        }

        /* get the partition table */
        ret = usbh_msc_scsi_read10(msc_class, 0, partition_table, 1); (2)
        if (ret < 0) 
        {
            USB_LOG_RAW("scsi_read10 error,ret:%d\r\n", ret);
            goto err_exit;
        }

        ret = usbh_msc_scsi_write10(msc_class, 0, partition_table, 1); (3)
        if (ret < 0) 
        {
            USB_LOG_RAW("scsi_write10 error,ret:%d\r\n", ret);
            goto err_exit;
        }

3. XHCI

3.1 XHCI 的基本概念

  • XHCI,即可扩展的主机控制器接口,是英特尔公司开发的一个USB主机控制器接口,它主要是面向USB 3.0,同时也支持USB 2.0及以下的设备,是UHCI/OHCI/EHCI等接口标准的升级版本

  • XHCI USB协议栈中有下列组件

      1. Application Software,使用USB驱动接口的应用程序,如FATFS- USBDISK
      1. Class Driver Software,特定USB设备类的驱动,如USB-MSC作为大容量存储器驱动
      1. USB Driver (USBD),USB控制器通用驱动,提供USB Driver Interface(USBDI),如USB标准请求
      1. Host Controller Driver (xHCD),XHCI控制器驱动,直接操作XHCI硬件
      1. Host Controller (xHC), XHCI控制器硬件
      1. USB Device,连接到USB总线上的设备
  • XHCI接口架构主要包括三大部分,

      1. 主机配置空间(Host Configuration Space),每个xHC实现都应包括一种通过系统软件识别和枚举主机控制器的方法,比如PCIe空间
      1. 寄存器空间(MMIO Space),寄存器空间表示xHC向驻留在内存地址空间中的系统软件提供的硬件寄存器
      1. 主机空间(Host Memory),主机空间由控制数据结构(设备上下文基地址阵列、设备上下文、传输环等)定义
  • XHCI控制器用的数据结构均要求首地址按64字节对齐,部分数据结构要求处于一个4096字节页中

3.2 CherryUSB 的 HOST Porting

  • CherryUSB 中适配 HOST 控制器需要实现以下接口

  • usb_hc_init 用于初始化 usb host controller 寄存器,设置 usb 引脚、时钟、中断等等。 此函数不对用户开放

    int usb_hc_init(void);
  • usbh_roothub_control 用来对 roothub 发起请求, 此函数不对用户开放
    int usbh_roothub_control(struct usb_setup_packet *setup, uint8_t *buf);

-usbh_ep0_pipe_reconfigure 重新设置端点 0 的 pipe 属性。 此函数不对用户开放

    int usbh_ep0_pipe_reconfigure(usbh_pipe_t pipe, uint8_t dev_addr, uint8_t ep_mps, uint8_t speed);
  • usbh_pipe_alloc 为端点分配 pipe。 此函数不对用户开放

    int usbh_pipe_alloc(usbh_pipe_t *pipe, const struct usbh_endpoint_cfg *ep_cfg);
  • usbh_pipe_free 释放端点的一些属性。 此函数不对用户开放

    int usbh_pipe_free(usbh_pipe_t pipe);
  • usbh_submit_urb 对某个地址上的端点进行数据请求。 此函数对用户开放

    int usbh_submit_urb(struct usbh_urb *urb);

其中, urb 结构体信息如下:


    struct usbh_urb {
        usbh_pipe_t pipe;
        struct usb_setup_packet *setup;
        uint8_t *transfer_buffer;
        uint32_t transfer_buffer_length;
        int transfer_flags;
        uint32_t actual_length;
        uint32_t timeout;
        int errorcode;
        uint32_t num_of_iso_packets;
        usbh_complete_callback_t complete;
        void *arg;
        struct usbh_iso_frame_packet iso_packet[];
    };
  • pipe 端点对应的 pipe 句柄
  • setup setup 请求缓冲区,端点0使用
  • transfer_buffer 传输的数据缓冲区
  • transfer_buffer_length 传输长度
  • transfer_flags 传输时携带的 flag
  • actual_length 实际传输长度
  • timeout 传输超时时间,为 0 该函数则为非阻塞,可在中断中使用
  • errorcode 错误码
  • num_of_iso_packets iso 帧或者微帧个数
  • complete 传输完成回调函数
  • arg 传输完成时携带的参数

3.3 XHCI Porting 源码分析

3.3.1 XHCI 控制器初始化

  • 协议栈会调用 usb_hc_init 初始化 XHCI 控制器,首先在(1)调用 BSP 的函数,完成开发板的配置,包括中断控制器、引脚复用等
  • 然后在(2)开始进行 XHCI 控制器初始化,对于 PCIe 上外挂的 XHCI 控制器,在(1)和 (2)之间还需要枚举 PCIe 总线上的 XHCI 控制器,获取 Bar 地址
int usb_hc_init(void)
{
    usb_hc_low_level_init(); (1)

    memset(&xhci_host, 0, sizeof(xhci_host));
    if (xhci_controller_setup(&xhci_host, CONFIG_XHCI_BASE_ADDR))2return -1;
    
    return 0;
}
  • 进入 XHCI 控制器初始化流程,首先初始化 XHCI 的寄存器空间,获取 XHCI 各部分寄存器的偏移量(1)(2)
  • 然后进行 XHCI 控制器的配置(3)
static void xhci_setup_mmio(struct xhci_s *xhci, unsigned long base_addr)
{
    xhci->base = base_addr;
    xhci->caps = xhci->base;

    /* add to register base to find the beginning of the Operational Register Space */
    xhci->op = xhci->base + readb(xhci->caps + XHCI_REG_CAP_CAPLENGTH);2)
    xhci->db = xhci->base + XHCI_REG_CAP_DBOFF_GET(readl(xhci->caps + XHCI_REG_CAP_DBOFF));
    xhci->ir = xhci->base + XHCI_REG_CAP_RTSOFF_GET(readl(xhci->caps + XHCI_REG_CAP_RTSOFF)) +
               XHCI_REG_RT_IR0;
    xhci->pr = xhci->op + XHCI_REG_OP_PORTS_BASE;

 ....
}

static int xhci_controller_setup(struct xhci_s *xhci, unsigned long baseaddr)
{
    USB_LOG_INFO("%s@0x%x\n", __func__, baseaddr);

    /* get register offset */
    xhci_setup_mmio(xhci, baseaddr);1if (xhci->version < 0x96 || xhci->version > 0x120) {
        USB_LOG_ERR("xHCI-0x%x not support\n", xhci->version);
        return -1;
    }

...

    USB_LOG_INFO("config XHCI ....\n");
    if (xhci_controller_configure(xhci)) { (3)
        USB_LOG_ERR("init XHCI failed !!!\n");
        return -1;
    } else {
        USB_LOG_INFO("init XHCI success !!!\n");
    }
}
  • 开始配置 XHCI 控制器,首先在(1)分配必要的数据结构,分别是设备槽(2)、事件槽(3)、命令 TRB 环和事件 TRB 环(4)
  • 随后检查 XHCI 控制器的状态(5),如果处于 Running 状态,先把 XHCI 控制器停止
  • 然后在(6)重置 XHCI 控制器,并等待重置完成
static int xhci_controller_configure(struct xhci_s *xhci)
{
    uint32_t reg;

    /* trbs */1)
    xhci->devs = usb_align(XHCI_ALIGMENT, sizeof(*xhci->devs) * (xhci->slots + 1)); /* device slot */2)
    xhci->eseg = usb_align(XHCI_ALIGMENT, sizeof(*xhci->eseg)); /* event segment */3)
    xhci->cmds = usb_align(XHCI_RING_SIZE, sizeof(*xhci->cmds)); /* command ring */4)
    xhci->evts = usb_align(XHCI_RING_SIZE, sizeof(*xhci->evts)); /* event ring */

...

    reg = readl(xhci->op + XHCI_REG_OP_USBCMD); (5)
    if (reg & XHCI_REG_OP_USBCMD_RUN_STOP) { /* if xHc running, stop it first */
        reg &= ~XHCI_REG_OP_USBCMD_RUN_STOP;
        writel(xhci->op + XHCI_REG_OP_USBCMD, reg); /* stop xHc */

        if (wait_bit(xhci->op + XHCI_REG_OP_USBSTS, 
                     XHCI_REG_OP_USBSTS_HCH, XHCI_REG_OP_USBSTS_HCH, 32) != 0) /* wait xHc halt */
            goto fail;
    }

    USB_LOG_DBG("%s: resetting\n", __func__);
    
    writel(xhci->op + XHCI_REG_OP_USBCMD, XHCI_REG_OP_USBCMD_HCRST); /* reset xHc */6if (wait_bit(xhci->op + XHCI_REG_OP_USBCMD, XHCI_REG_OP_USBCMD_HCRST, 0, 1000) != 0) /* wait reset process done */
        goto fail;

    if (wait_bit(xhci->op + XHCI_REG_OP_USBSTS, XHCI_REG_OP_USBSTS_CNR, 0, 1000) != 0) /* wait until xHc ready */
        goto fail;
    
  • 之后还需要将命令环、事件环的基地址写入寄存器,完成设置后,使能 XHCI 控制器中断,设置中断的间隔频率(1)(2)
  • 最后在(3)启动 XHCI 控制器
    /* enable port interrupt */
    writel(xhci->ir + XHCI_REG_RT_IR_IMOD, 500U);
    reg = readl(xhci->ir + XHCI_REG_RT_IR_IMAN);
    reg |= XHCI_REG_RT_IR_IMAN_IE;
    writel(xhci->ir + + XHCI_REG_RT_IR_IMAN, reg);1)

    reg = readl(xhci->op + XHCI_REG_OP_USBCMD);
    reg |= XHCI_REG_OP_USBCMD_INTE; /* enable interrupt */
    writel(xhci->op + XHCI_REG_OP_USBCMD, reg);2USB_LOG_DBG("Start xHc ....\n");

    reg = readl(xhci->op + XHCI_REG_OP_USBCMD);
    reg |= XHCI_REG_OP_USBCMD_RUN_STOP; /* start xHc */3writel(xhci->op + XHCI_REG_OP_USBCMD, reg);

    return 0U; /* Success */

3.3.2 XHCI 响应中断

  • XHCI 在端口状态变化、命令完成、传输完成等情况下会触发中断, CherryUSB需要 XHCI 在端口状态变化时上报端口事件,唤醒 USB 设备枚举
  • XHCI 每次进入中断,都会将事件 TRB 环上的一个条目的Cycle State置成1,因为事件TRB的生产者是XHCI硬件,消费者是XHCI驱动,Cycle State = 1 后表示存在一个事件需要 XHCI 驱动处理(4),通过读取当前 Cycle State = 1的TRB条目,可以获取事件源是端口状态变化还是命令完成等
  • 进入中断响应函数 USBH_IRQHandler 后,在(1)(2)ACK中断,防止中断重复上报
  • 如果当前的中断事件源是端口状态变化,在(3)处判断端口连接状态是否变化,然后通过usbh_roothub_thread_wakeup上报一个端口事件,后续在 CherryUSB处理端口事件时,会找XHCI确认是端口连接事件还是端口断开事件
  • 命令完成和传输完成是 XHCI 的私有中断事件,和 CherryUSB 无关,这里就不详细讨论了
  • 进入中断后可能同时存在多个事件,在(5)会继续处理下一个事件,直到处理完成
void USBH_IRQHandler(void)
{
    uint32_t reg, status;
    struct xhci_s *xhci = &xhci_host;
    struct xhci_ring *evts = xhci->evts;
    struct xhci_pipe *work_pipe = NULL;    

    USB_LOG_DBG("%s\n", __func__);

    status = readl(xhci->op + XHCI_REG_OP_USBSTS);
    status |= XHCI_REG_OP_USBSTS_EINT; /* clear interrupt status */
    writel(xhci->op + XHCI_REG_OP_USBSTS, status);1)

    reg = readl(xhci->ir + XHCI_REG_RT_IR_IMAN);
    reg |= XHCI_REG_RT_IR_IMAN_IP; /* ack interrupt */
    writel(xhci->ir + XHCI_REG_RT_IR_IMAN, reg);2/* check and ack event */
    for (;;) {
        uint32_t nidx = evts->nidx; /* index of dequeue trb */
        uint32_t cs = evts->cs; /* cycle state toggle by xHc */
        struct xhci_trb *etrb = evts->ring + nidx; /* current trb */
        uint32_t control = etrb->control; /* trb control field */
		
        if ((control & TRB_C) != (cs ? 1 : 0)) /* if cycle state not toggle, no events need to handle */4break;
...

        if (ER_PORT_STATUS_CHANGE == evt_type) {
            /* bit[31:24] port id, the port num of root hub port that generated this event */
            uint32_t port = TRB_PORT_ID_GET(etrb->ptr_low) - 1;
            uint32_t portsc = xhci_readl_port(xhci, port, XHCI_REG_OP_PORTS_PORTSC); /* Read status */

            xhci_print_port_state(__func__, port, portsc);
            if (portsc & XHCI_REG_OP_PORTS_PORTSC_CSC) { (3)
                usbh_roothub_thread_wakeup(port + 1); /* wakeup when connection status changed */
            }
        }
...

        /* move ring index, notify xhci */
        nidx++; /* head to next trb */ (5)
        if (nidx == XHCI_RING_ITEMS) {
            nidx = 0; /* roll-back if reach end of list */
            cs = cs ? 0 : 1;
            evts->cs = cs; /* sw toggle cycle state */
        }
        evts->nidx = nidx;
        uint64_t erdp = (uint64_t)(evts->ring + nidx);
        writeq(xhci->ir + XHCI_REG_RT_IR_ERDP, erdp | XHCI_REG_RT_IR_ERDP_EHB); /* bit[63:4] update current event ring dequeue pointer */        
    }

3.3.3 XHCI 的端点分配

  • USB 的传输分为四大类,控制传输、中断传输、批量传输和等时传输,控制传输主要用于发送接收描述符,中断传输用于鼠标键盘等突发的少量数据传输,批量传输用于U盘等大量的数据传输,我们这里还没有实现等时传输
  • 与USB传输紧密相关联的概念是端点(Endpoint),一般来说,每个传输都需要一个端点来实现,比如控制传输,规定用EP0完成,其它的传输,也会分配一个工作EP
  • CherryUSB 依赖 HOST 驱动实现的 usbh_pipe_alloc,在XHCI中分配EP时,首先在(1)分配一个端点配置上下文,在配置上下文中指定EP和SLOT的配置
  • 如果当前分配的是控制端点EP0,首先要将设备地址设置好,在(2)下发ENABLE_SLOT命令从XHCI硬件申请一个设备地址,然后在(3)下发ADDRESS_DEVICE命令,将前面分配的配置上下文生效
  • 如果当前分配的是工作端点EPX,表示设备已经存在,在(4)下发CONFIG_ENDPOINT命令,将前面分配的配置上下文生效
int usbh_pipe_alloc(usbh_pipe_t *pipe, const struct usbh_endpoint_cfg *ep_cfg)
{
...
    /* Allocate input context and initialize endpoint info. */
    struct xhci_inctx *in = xhci_alloc_inctx_config_ep(xhci, hport, epid);1if (!in){
        ret = -1;
        goto fail;
    }

    if (ppipe->epid == 1) { /* when allocate ep-0, allocate device first */
		...
        int slotid = xhci_cmd_enable_slot(xhci, ppipe); /* get slot-id */ (2)
        if (slotid < 0) {
            USB_LOG_ERR("%s: enable slot: failed\n", __func__);
            usb_free(dev);
            ret = -1;
            goto fail;
        } 

		...
        /* Send set_address command. */
        int cc = xhci_cmd_address_device(xhci, ppipe, in); (3)

		} else {
        /* Send configure command. */
        cc = xhci_cmd_configure_endpoint(xhci, ppipe, in, false);4}

3.3.4 XHCI 的命令处理

  • XHCI 规范定义了一系列命令,用于XHCI驱动向XHCI硬件发送指令,下面以ENABLE_SLOT为例简要说明,ENABLE_SLOT用于XHCI驱动向XHCI硬件申请一个设备地址,在枚举USB设备的过程中调用,调用成功后会返回一个Slot地址,作为设备地址使用
  • 调用 xhci_cmd_submit,发起一条XHCI命令(1),XHCI控制器只能同时处理一条命令,所有(2) 上锁,在(3)向命令TRB环加入一个条目,对于命令TRB环,生产者是XHCI驱动,消费者是XHCI硬件,因此驱动只要将TRB条目的Cycle State置位,让硬件读取即可
  • 发起命令后等待硬件处理完成,硬件完成命令处理后会触发中断,加入一个事件TRB条目,通过读取事件TRB条目,中断响应函数判断当前事件源是命令完成,在(4)将事件TRB条目复制保存
  • 同时,在任务上下文中,(5)处等待中断事件完成的信号量,获取命令完成的Completion Code(5)
  • 最后,如果ENABLE_SLOT成功完成,在(6)从之前复制的事件TRB条目中获取XHCI硬件分配出来的Slot id返回,作为USB设备的地址
void USBH_IRQHandler(void)
{
...
else if ((ER_COMMAND_COMPLETE == evt_type) || (ER_TRANSFER_COMPLETE == evt_type)) {
            struct xhci_trb  *rtrb = (void*)(unsigned long)etrb->ptr_low;
            struct xhci_ring *ring = XHCI_RING(rtrb); /* to align addr is ring base */
            struct xhci_trb  *evt = &ring->evt; /* first event trb */
            uint32_t eidx = rtrb - ring->ring + 1; /* calculate current evt trb index */

            memcpy(evt, etrb, sizeof(*etrb)); /* copy current trb to cmd/transfer ring */4)
            ring->eidx = eidx;
            ...
            }

/* Submit a command to the xhci controller ring */
static int xhci_cmd_submit(struct xhci_s *xhci, struct xhci_inctx *inctx, 
                           struct xhci_pipe *pipe, uint32_t flags)
{
...

    usb_osal_mutex_take(xhci->cmds->lock); /* handle command one by one */ (2)

    pipe->timeout = 5000;
    pipe->waiter = true;
    cur_cmd_pipe = pipe;

    xhci_trb_queue(xhci->cmds, inctx, 0, flags);3/* pass command trb to hardware */
    DSB();

    xhci_doorbell(xhci, 0, 0); /* 0 = db host controller, 0 = db targe hc command */
    int rc = xhci_event_wait(xhci, pipe, xhci->cmds);5usb_osal_mutex_give(xhci->cmds->lock);
}

static int xhci_cmd_enable_slot(struct xhci_s *xhci, struct xhci_pipe *pipe)
{
    int cc = xhci_cmd_submit(xhci, NULL, pipe, TRB_TYPE_SET(CR_ENABLE_SLOT));1if (cc != CC_SUCCESS)
        return cc;

    struct xhci_trb *evt = &(xhci->cmds->evt);
    return TRB_CR_SLOTID_GET(evt->control); /* bit [31:24] slot id */6}

3.3.5 XHCI 传输处理

  • 在 CherryUSB 中,USB 的四类传输都依赖 HOST 驱动实现的 usbh_submit_urb
  • 在 XHCI 中,首先判断当前传输用的EP类型,如果是控制EP,当前走的是控制传输xhci_xfer_setup,其它EP走xhci_xfer_normal,两者实际上都是向传输TRB环中加入几个条目,然后提醒XHCI硬件进行处理,例如在(3)加入了一个TRB条目,(4)通过XHCI 的 DoorBell 机制提醒硬件进行处理
  • 在 XHCI 中,USB 标准请求不会响应(2)
/* Signal the hardware to process events on a TRB ring */
static void xhci_doorbell(struct xhci_s *xhci, uint32_t slotid, uint32_t value)
{
    writel(xhci->db + slotid * XHCI_REG_DB_SIZE, value); /* bit[7:0] db target, is ep_id */4}

/* Submit a USB transfer request to the pipe's ring */
static void xhci_xfer_normal(struct xhci_s *xhci, struct xhci_pipe *pipe,
                             void *data, int datalen)
{
    /* Normal trb, used in bulk and interrupt transfer */
    xhci_trb_queue(&pipe->reqs, data, datalen, TRB_TYPE_SET(TR_NORMAL) | TRB_TR_IOC);3/* pass command trb to hardware */
    DSB();

    xhci_doorbell(xhci, pipe->slotid, pipe->epid);
}

int usbh_submit_urb(struct usbh_urb *urb)
{
...
    switch (ppipe->eptype){ (1)
        case USB_ENDPOINT_TYPE_CONTROL:
            USB_ASSERT(setup);
            if (setup->bRequest == USB_REQUEST_SET_ADDRESS){2/* Set address command sent during xhci_alloc_pipe. */
                goto skip_req;
            }

            USB_LOG_DBG("%s request-%d\n", __func__, setup->bRequest);
            /* send setup in/out for command */
            xhci_xfer_setup(xhci, ppipe, setup->bmRequestType & USB_EP_DIR_IN, (void*)setup, 
                            urb->transfer_buffer, urb->transfer_buffer_length);
            break;
        case USB_ENDPOINT_TYPE_INTERRUPT:      
        case USB_ENDPOINT_TYPE_BULK:
            xhci_xfer_normal(xhci, ppipe, urb->transfer_buffer, urb->transfer_buffer_length);
            break;
        default:
            USB_ASSERT(0U);
            break;    
    }
    ...
}
  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值