Lifecycle of a USB request

Glossary

  • HCI -- Host Controller Interface: A host controller interface driver is responsible for queueing USB requests to hardware, and managing the state of connected devices while operating as a USB host.
  • DCI -- Device controller interface: A device controller interface is responsible for queueing USB requests to a USB host that the device is connected to.

Allocation

The first step in a USB request's lifecycle is allocation. USB requests contain data from all of the drivers in the request stack in a single allocation. Each driver that is upstream of a USB device driver should provide a GetRequestSize method -- which returns the size it needs to contain its local request context. When a USB device driver allocates a request, it should invoke this method to determine the size of the parent's request context.

C example

size_t parent_req_size = usb_get_request_size(&usb);
usb_request_t* request;
usb_request_alloc(&request, transfer_length, endpoint_addr,
parent_req_size+sizeof(your_context_struct_t));
usb_request_complete_callback_t complete = {
      .callback = usb_request_complete,
      .ctx = your_context_pointer,
};
your_context_pointer.completion = complete;
usb_request_queue(&usb, request, &complete);
...

void usb_request_complete(void* cookie, usb_request_t* request) {
    your_context_struct_t data;
    // memcpy is needed to ensure alignment
    memcpy(&data, cookie, sizeof(data));
    // Do something here to process the response
    // ...

    // Requeue the request
    usb_request_queue(&data.usb, request, &data.completion);
}

C++ example

parent_req_size = usb.GetRequestSize();
std::optional<usb::Request<void>> req;
status = usb::Request<void>::Alloc(&req, transfer_length,
endpoint_addr, parent_req_size);
usb_request_complete_callback_t complete = {
      .callback =
          [](void* ctx, usb_request_t* request) {
            static_cast<YourDeviceClass*>(ctx)->YourHandlerFunction(request);
          },
      .ctx = this,
  };
usb.RequestQueue(req->take(), &complete);

C++ example (with lambdas)

size_t parent_size = usb_.GetRequestSize();
using Request = usb::CallbackRequest<sizeof(std::max_align_t) * 4>;
std::optional<Request> request;
Request::Alloc(&request, max_packet_size, endpoint_address,
parent_size, [=](Request request) {
    // Do some processing here.
    // ...
    // Re-queue the request
    Request::Queue(std::move(request), usb_client_);
});

Submission

You can submit requests using the RequestQueue method, or -- in the case of CallbackRequests (as seen here), using Request::Queue or simply request.Queue(client). In all cases, ownership of the USB request is transferred to the parent driver (usually usb-device).

The typical lifecycle of a USB request (from a device driver to either a host controller or device controller is as follows):

  • The USB device driver queues the request
  • The usb-device core driver receives the request, and now owns the request object.
  • The usb-device core driver injects its own callback (if the direct flag is not set), or passes through the request (if the direct flag is set) to the HCI or DCI driver.
  • The HCI or DCI driver now owns the request. The HCI or DCI driver submits this request to hardware.
  • The request completes. When this happens, if the direct flag was set, the callback in the device driver is invoked, and the device driver now owns the request. If the direct flag is not set, the usb-device (core) driver now owns the request.
  • If the core driver owns the request; it is added to a queue for dispatch by another thread.
  • The core driver eventually invokes the callback, and the request is now owned by the device driver. The device driver can now re-submit the request.

Cancellation

Requests may be cancelled by invoking CancelAll. When CancelAll completes, all requests are owned by the caller. Drivers implementing a CancelAll function (such as the usb-device core driver and any HCI/DCI drivers) are responsible for transferring ownership to their children with a ZX_ERR_CANCELLED status code.

Implementation notes for writers of HCI, DCI, or filter drivers

Implementing GetRequestSize

The value returned by GetRequestSize should equal the value of your parent's GetRequestSize + the size of your request context, including any padding that would be necessary to ensure proper alignment of your data structures (if applicable). If you are implementing an HCI or DCI driver, you must include sizeof(usb_request_t) in your size calculation in addition to any other data structures that you are storing. usb_request_t has no special alignment requirements, so it is not necessary to add padding for that structure.

Implementing RequestQueue

Implementors of RequestQueue temporarily assumes ownership of the USB request from its client driver. As an implementor of RequestQueue, you are allowed to access all fields of the usb_request_t, as well as any private data that you have appended to the usb_request_t structure (by requesting additional space through GetRequestSize), but you are not allowed to modify any data outside of your private area, which starts at parent_req_size bytes (past the end of usb_request_t).

Example USB request stack (HCI)

xHCI (host controller) -> usb-bus -> usb-device (core USB device driver) -> usb-mass-storage

Example USB request stack (DCI)

dwc2 (device-side controller) -> usb-peripheral (peripheral core driver) -> usb-function (core function driver) -> cdc-eth-function (ethernet peripheral mode driver)